/*
* Copyright 2025 SanderSoft™
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Ambient Weather Station
*
* Author: Kurt Sanders, SanderSoft™
*
* Dates: 2018,2019,2020,2021,2022,2023,2024,2025
*/
#include kurtsanders.AWSLibrary
@Field static String PARENT_DEVICE_NAME = "Ambient Weather Station"
@Field static final String VERSION = "6.7.3"
//************************************ Version Specific ***********************************
String appModified() { return "Oct-24-2025" }
//*************************************** Constants ***************************************
String appNameVersion() { return "Ambient Weather Station " + VERSION }
String appShortName() { return "STAmbientWeather " + VERSION }
String DTHName() { return PARENT_DEVICE_NAME }
String DTHRemoteSensorName() { return PARENT_DEVICE_NAME + " Remote Sensor"}
String DTHPMSensorName() { return "Ambient Particulate Monitor"}
String DTHDNI() { return "${app.id}:MyAmbientWeatherStation" }
String DTHDNIRemoteSensorName() { return "${app.id}:MyAmbientRemoteSensor"}
String DTHDNIPMName() { return "${app.id}:MyAmbientParticulateMonitor"}
String DTHDNIActionTiles() { return "${app.id}:MyAmbientSmartWeatherStationTile" }
Integer MaxNumRemoteSensors() { return 8 }
String DTHnamespace() { return NAMESPACE }
String appAuthor() { return AUTHOR_NAME }
String AppImg(imgName) { return GITHUB_IMAGES_LINK + "${imgName}" }
String wikiURL(pageName) { return "https://github.com/KurtSanders/STAmbientWeather/wiki/$pageName"}
Integer wm2lux(value) { return (value * 126.7).toInteger() }
Integer wm2fc(value) { return (wm2lux(value) * 0.0929).toInteger() }
// ============================================================================================================
// This APP key is ONLY for this application - Do not copy or use elsewhere
@Field static final String APPKEY = "33054086b3d745779f5ac35e147baa76f13e75d44ea245388ba598911905fb50"
// ============================================================================================================
definition(
name : PARENT_DEVICE_NAME,
namespace : NAMESPACE,
author : AUTHOR_NAME,
description : "Integrate your Ambient™ Weather Station and remote weather/soil/particle monitor sensors to Hubitat™",
category : "",
iconUrl : "",
iconX2Url : "",
documentationLink : COMM_LINK,
singleInstance : false,
pausable : false
)
preferences {
page(name: "apiPage")
page(name: "mainPage")
page(name: "optionsPage")
page(name: "unitsPage")
page(name: "remoteSensorPage")
page(name: "notifyPage")
page(name: "DataPage")
}
def apiPage() {
if (apiKey == null) {
log.info "${app.name}: First Run: Initializing default 'Units of Measure' values to Imperial system"
app.updateSetting("tempUnits", [type: "enum", value: "°F"])
app.updateSetting("windUnits", [type: "enum", value: "mph"])
app.updateSetting("measureUnits", [type: "enum", value: "in"])
app.updateSetting("baroUnits", [type: "enum", value: "inHg"])
app.updateSetting("solarRadiationTileDisplayUnits", [type: "enum", value: "W/m²"])
}
if (keepDataKeysAllOption == null) {
log.info "${app.name}: First Run: Initializing default keepDataKeysAllOption = True"
app.updateSetting("keepDataKeysAllOption", [type: "bool", value: true])
}
dynamicPage(name: "apiPage", submitOnChange: true, nextPage: "mainPage", uninstall: true, install: false ) {
section(sectionHeader("Ambient Weather Station API Key")) {
input ( name: "apiKey", type: "text",
title: fmtTitle("Enter your Ambient Weather Station API Key below (Required)"),
required: true
)
paragraph ""
href(name: "APIKeyLink",
title: fmtTitle("Here is a WebLink to display your Ambient Weather Station Account Page with API key (scroll to the bottom of the page to copy your API string)"),
required: false,
style: "external",
url: "https://ambientweather.net/account",
description: fmtTitle("tap to view your weather station account page and copy your API key")
)
}
}
}
def mainPage() {
def apiappSetupCompleteBool = false
if (apiKey == state.apiKey && state.ambientMap) {
apiappSetupCompleteBool = true
} else {
state.apiKey = apiKey
apiappSetupCompleteBool = AmbientStationData(0)
}
def setupMessage = ""
def setupTitle = "${appNameVersion()} API Settings Check"
def nextPageName = getAllChildDevices().count{it}>0?"optionsPage":"remoteSensorPage"
state.retry = 0
def AmbientStationDataRC = (state.ambientMap)?true:false
if (!state.ambientMap) {
AmbientStationDataRC = AmbientStationData(0)
}
if (apiappSetupCompleteBool && AmbientStationDataRC) {
setupMessage = "SUCCESS! You have completed entering a valid Ambient API Key for ${appNameVersion()}. "
setupMessage += (weatherStationMac)?"Please Press 'Next' for additional configuration choices.":"I found ${state.ambientMap.size()} reporting weather station(s)."
setupTitle = "Please confirm the Ambient Weather Station Information below and if correct, Tap 'NEXT' to continue to the 'Settings' page'"
} else {
setupMessage = "Ambient API Setup INCOMPLETE or MISSING!\n\nPlease check and/or complete the REQUIRED Ambient Weather API key setup for ${appNameVersion()}.\n\nAPI Error message: ${state.httpError}"
nextPageName = null
}
dynamicPage(name: "mainPage", title: setupTitle, submitOnChange: true, nextPage: nextPageName, uninstall:true, install:false) {
section(hideable: apiappSetupCompleteBool, hidden: apiappSetupCompleteBool, sectionHeader(setupMessage) ) {
paragraph "The API string key is used to securely connect your weather station to ${appNameVersion()}."
paragraph image: AppImg("blue-ball-100.jpg"),
title: "Required API Key",
required: false,
informationList("apiHelp")
href(name: "hrefReadme",
title: fmtTitle("${appNameVersion()} Setup/Read Me Page"),
required: false,
style: "external",
url: "https://github.com/KurtSanders/STAmbientWeather#hubitat-installation",
description: "tap to view the Setup/Read Me page")
href(name: "hrefAmbient",
title: fmtTitle("Ambient Weather Dashboard Account Page for API Key"),
required: false,
style: "external",
url: "https://dashboard.ambientweather.net/account",
description: "tap to login and view your Ambient Weather's dashboard")
}
if (apiappSetupCompleteBool && AmbientStationDataRC) {
if (weatherStationMac) {
setStateWeatherStationData()
state.weatherStationMac = weatherStationMac
countRemoteTempHumiditySensors()
section (sectionHeader("Ambient Weather Station Information")) {
paragraph image: AppImg("blue-ball-100.jpg"),
title: fmtTitle("${state.weatherStationName}"),
required: false,
"Name: ${state.weatherStationName}" +
"\nLocation: ${state.weatherStationLocation?:'Not Provided'}" +
"\nMac Address: ${state.ambientMap[state.weatherStationDataIndex].macAddress}" +
"\nRemote Temp/Hydro/Moisture Sensors: ${state.countRemoteTempHumiditySensors}" +
"\nAQIN Particulate Monitor Sensor: ${state.countParticulateMonitors}"
href(name: "Weather Station Options",
page: nextPageName,
description: "")
}
} else {
def weatherStationList = [:]
def stationlocation
state.ambientMap.each {
stationlocation = it.info.location?:it.info.containsKey("coords")?it.info.coords.location:''
weatherStationList << [[ "${it.macAddress}" : "${it.info.name}${stationlocation?' @ ':''}${stationlocation}" ]]
}
section ("Ambient Weather Station Information") {
input (name: "weatherStationMac", submitOnChange: true, type: "enum",
title: fmtTitle("Select the Weather Station to Install"),
options: weatherStationList,
multiple: false,
required: true
)
}
}
}
section ("STAmbientWeather™ - ${appAuthor()}") {
href(name: "hrefVersions",
image: AppImg("readme.png"),
title: fmtTitle("Release Notes for ${VERSION} : ${appModified()}"),
required: false,
style:"embedded",
url: wikiURL("Features-by-Version")
)
}
}
}
def unitsPage() {
dynamicPage(name: "unitsPage", title: "Ambient Units of Measure Settings for: '${state.weatherStationName}'",
uninstall : false,
install : false ) {
section("Weather Station Unit of Measure Options") {
input ( name: "tempUnits", type: "enum",
title: fmtTitle("Select Temperature Units of Measure"),
options: ['°F':'Fahrenheit °F','°C':'Celsius °C'],
defaultValue: "°F",
required: true
)
input ( name: "windUnits", type: "enum",
title: fmtTitle("Select Wind Speed Units of Measure"),
options: ['mph':'Miles per Hour','fps':'Feet per Second','mps':'Meter per Second','kph':'Kilometers per Hour','knotts':'Knotts'],
defaultValue: "mph",
required: true
)
input ( name: "measureUnits", type: "enum",
title: fmtTitle("Select Rainfall Units of Measure"),
options: ['in':'Inches','mm':'Millimeters','cm':'Centimeters'],
defaultValue: "in",
required: true
)
input ( name: "baroUnits", type: "enum",
title: fmtTitle("Select Barometer Units of Measure"),
options: ['inHg':'inHg','mmHg':'mmHg', 'hPa':'hPa'],
defaultValue: "inHg",
required: true
)
input ( name: "solarRadiationTileDisplayUnits", type: "enum",
title: fmtTitle("Select Solar Radiation ('Light') Units of Measure"),
options: ['W/m²':'Imperial Units (W/m²)','lux':'Metric Units (lux)', 'fc':'Foot Candles (fc)'],
defaultValue: "W/m²",
required: true
)
input ( name: "solarRadiationDecimalFormat", type: "enum",
title: fmtTitle("Select Solar Radiation ('Light') Decimal Format"),
options: [0,1,2],
defaultValue: 0,
required: true
)
}
}
}
def DataPage() {
dynamicPage(name: "DataPage", title: getFormat("title", myText="Ambient Weather Station Data Key Import Selector"), submitOnChange: true,
uninstall : false,
install : false ) {
section() {
input ( name: "keepDataKeysAllOption", type: "bool",
title: "Select ALL Data Keys?' Toggle OFF to ONLY import partial weather data keys into Hubitat for '${state.weatherStationName} Weather Station'",
required: true,
submitOnChange: true,
defaultValue:true
)
}
if(!keepDataKeysAllOption) {
section () {
def dateKeystoKeep = ["date","tz","dateutc"]
def dataValues = new JsonSlurper().parseText(JsonOutput.toJson(state.respdata))
dataValues = dataValues[state.weatherStationDataIndex].lastData.keySet()
dataValues.removeAll(dateKeystoKeep)
section (getFormat("header-blue","Partial Data Keys Selector")) {
def wikiURL = "https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs"
paragraph("This Wiki lists all the data key parameters that a AMbient Weather Station device might send. Note: Not all devices send all the data key parameters.\n\nWeather Data Name Wiki Documentation Wiki (opens in a new browser tab/window, close tab/window to return): Click Here")
input ( name: "keepDataKeys", type: "enum",
multiple: true,
title: "Select the Data Keys of your weather station to ONLY import into Hubitat for '${state.weatherStationName}.'",
options: dataValues.sort(),
offerAll: false,
submitOnChange: true,
required: true
)
}
}
}
selectWeatherKeys(state.respdata)
}
}
def selectWeatherKeys(respdata) {
// Keep these time/date related data keys in the filtered dataset
def dateKeystoKeep = ["date","tz","dateutc"]
logDebug "==> Start state.ambientMap= ${state.ambientMap}"
if (keepDataKeysAllOption) {
logDebug "Keeping ALL Data Keys..."
state.ambientMap = respdata
} else if (keepDataKeys) {
if (debugVerbose) {
logDebug "Partial Data Keys Selected"
logDebug "Keeping only '${keepDataKeys}' keys"
}
keepDataKeys.addAll(dateKeystoKeep) // Add back the date related data keys to the keepDataKeys array
AWSmap = new JsonSlurper().parseText(JsonOutput.toJson(state.respdata))
AWSmap[state.weatherStationDataIndex].lastData.keySet().retainAll(keepDataKeys)
state.ambientMap = AWSmap
if (debugVerbose) {
logDebug "==> End state.ambientMap keys = ${state.ambientMap[state.weatherStationDataIndex].lastData.size()}"
}
} else {
logWarn "keepDataKeys LIST is Null => ${keepDataKeys}.. Changing 'keepDataKeysAllOption' to 'True' in AWS Preferences"
app.updateSetting("keepDataKeysAllOption", true)
state.ambientMap = respdata
}
if (debugVerbose) {
logDebug "==> End state.ambientMap= ${state.ambientMap}"
logDebug "==> End state.ambientMap keys = ${state.ambientMap[state.weatherStationDataIndex].lastData.size()}"
}
}
def optionsPage () {
logInfo "Ambient Weather Station: Mac: ${weatherStationMac}, Name/Loc: ${state.weatherStationName}/${state.weatherStationLocation}"
if (app.label.contains('WebLink to ${app.name}")
paragraph ("")
}
section(sectionHeader("Ambient Weather Station (AWS) Preference Settings for: '${state.weatherStationName}'")) {
input ( name: "schedulerFreq", type: "enum",
title: fmtTitle("Poll the Ambient Weather Station Server every"),
options: POLLING_OPTIONS_MAP,
required: true,
defaultValue: '15 mins'
)
input (name: "showBattery", type: "bool",
title: fmtTitle("Show battery level from sensor(s)? (Ambient devices only report 0% and 100%)"),
defaultValue: true,
required: true
)
href(name: "Weather Units of Measure",
title: fmtTitle("Select Weather Units of Measure"),
required: false,
defaultValue: "Tap to Select Units",
description: tempUnits?"${unitsSet()}":"Tap to Select Units",
page: "unitsPage")
href(name: "Weather Station Filtered Data Key Import Selector",
title: fmtTitle("Select/DeSelect the weather station data keys to import (Required)"),
required: true,
defaultValue: "Tap to Select Partial Data Keys to Import",
description: keepDataKeysAllOption?"All ${state?.ambientMap[state.weatherStationDataIndex].lastData.size()-3} Weather Data Keys are selected. Tap to Select Partial Weather Data Keys":"${keepDataKeys?.size()} of ${state.respdata[state?.weatherStationDataIndex].lastData.size()-3} Data Keys Selected. Tap to Modify Selection or Select ALL Weather Data Keys",
page: "DataPage")
href(name: "Activate Weather Alerts/Notification",
title: fmtTitle("Weather Alerts/Notification (Optional)"),
required: false,
defaultValue: (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled]))?"Alerts Activated ":"Tap to Activate Alerts",
description: (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled]))?"Alerts Activated ":"Tap to Activate Alerts",
page: "notifyPage")
href(name: "Weather Sensor Device Label Management",
title: fmtTitle("Weather Sensor Names"),
description: "Tap to Change the Device Label of ${state.countRemoteTempHumiditySensors + state.countParticulateMonitors + 1} Remote Weather Sensors",
required: false,
page: "remoteSensorPage")
}
section (sectionHeader('Name this instance of Ambient Weather Station')) {
label ( name: "name",
title: fmtTitle("Assign a name to this SmartApp"),
state: (name ? "complete" : null),
defaultValue: state.weatherStationName,
required: false,
submitOnChange: true
)
}
section(sectionHeader("AWS Logging Options")) {
if (logLevel == null && logLevelTime == null) {
log.info "${app.name}: Setting Innital logLevel and LogLevelTime defaults"
app.updateSetting(logLevel, [type: "enum", value: [5]])
app.updateSetting(logLevelTime, [type: "enum", value: [30]])
}
//Logging Options
input name: "logLevel", type: "enum", title: fmtTitle("Logging Level"), submitOnChange: true,
description: fmtDesc("Logs selected level and above"), defaultValue: 3, options: LOG_LEVELS
input name: "logLevelTime", type: "enum", title: fmtTitle("Logging Level Time"), submitOnChange: true,
description: fmtDesc("Time to enable Debug/Trace logging"),defaultValue: 10, options: LOG_TIMES
input name: "SyncLogOptions", type: "button", title: "Sync Log Options in all Devices"
}
}
}
def remoteSensorPage() {
getAllChildDevices().each {
logDebug "Device: ${it.deviceNetworkId} = ${it.label}"
app.updateSetting(it.deviceNetworkId, [type: "string", value: it.label])
}
dynamicPage(name: "remoteSensorPage", nextPage: "optionsPage", uninstall:false, install : false ) {
if (state.ambientMap[state.weatherStationDataIndex].lastData?.tempinf) {
section (sectionHeader("Enter a location name for your AWS Weather console located inside the house?")) {
input (name: "${DTHDNIRemoteSensorName()}0",
type: "text",
title: "",
required: true,
defaultValue: 'Kitchen',
submitOnChange: true
)
}
}
def i = 1
def lastData = state.ambientMap[state.weatherStationDataIndex].lastData
if ( state?.countRemoteTempHumiditySensors > 0) {
section(sectionHeader("Provide Location names for your ${state?.countRemoteTempHumiditySensors} remote temperature/hydro sensors")) {
paragraph getFormat("text-red",
"You MUST create short descriptive names for each remote sensor or accept the default provided. Do not use ANY special characters in the device names.\n\n" +
"Please note that remote sensors are numbered based in the bit switch on the Ambient Weather sensor (1-8) and reported on Ambient Network API as 'tempNf' or 'soiltempN' where N is an integer 1-8. " +
"If a remote weather/soil sensor is deleted from your AWS network or non responsive from your group of Ambient remote sensors, you may have to re-verify and/or rename the remainder of the remote sensors in this app and manually delete that device sensor from the Hubitat 'Devices' page.")
for (i; i <= MaxNumRemoteSensors(); i++) {
if (lastData["temp${i}f"]) {
input (
name: "${DTHDNIRemoteSensorName()}${i}",
type: "text",
title: getImage("checkMarkGreen") + fmtTitle("Ambient Weather Station Remote Device #${i} → Temp Sensor (Current: ${lastData["temp${i}f"]}°)"),
defaultValue: "Temp Sensor #${i}",
required: true
)
}
if (lastData["soiltemp${i}"]) {
input (
name: "${DTHDNIRemoteSensorName()}${i}",
type: "text",
title: getImage("checkMarkGreen") + fmtTitle("Ambient Weather Station Remote Device #${i} → Soil Temp Sensor (Current: ${lastData["soiltemp${i}"]}°)"),
defaultValue: "Soil Temp Sensor #${i}",
required: true
)
}
if (lastData["soilhum${i}"]) {
input (
name: "${DTHDNIRemoteSensorName()}${i}",
type: "text",
title: getImage("checkMarkGreen") + fmtTitle("Ambient Weather Station Remote Device #${i} → Soil Moisture Sensor (Current: ${lastData["soilhum${i}"]})"),
defaultValue: "Soil Moisture Sensor #${i}",
required: true
)
}
}
}
}
if (lastData.findAll { it.key.startsWith("pm") }.size() > 0) {
section() {
paragraph "Ambient Weather Station Particulate Monitor Location Name"
paragraph image: AppImg("ambient-weather-pm25.jpg"),
title: fmtTitle("Provide a friendly short name for your Ambient Particulate Monitor PM10/PM25/AQIN/CO2"),
required: false,
null
input (
name: "${DTHDNIPMName()}",
type: "text",
title: fmtTitle("Ambient Weather Station Particulate Monitor PM10/PM25/AQIN/CO2"),
defaultValue: "AWS Particle Monitor",
required: true
)
}
}
}
}
def notifyPage() {
dynamicPage(name: "notifyPage", title: "Weather Alerts/Notifications", uninstall: false, install: false) {
section(sectionHeader("Enable Pushover™ and/or Twilio™ service(s). (Must install virtual device(s) and have an active service account):")) {
input ("pushoverEnabled", "bool", title: "Use Pushover™ and/or Twilio™ Service(s) for Alert Notifications", required: false, submitOnChange: true)
if (pushoverEnabled) {
input(name: "pushoverDevices", type: "capability.notification", title: "", required: false, multiple: true,
description: "Select notification device(s)", submitOnChange: true)
paragraph ""
}
}
if (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled])) {
section (sectionHeader("Weather Station Notify Options")) {
input ( name: "notifyAlertFreq", type: "enum",
required: checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled]),
title: "Restrict notification(s) per event type to once every NUMBER of hours (Default is 24, Once/day)",
options: [0,1,2,4,6,12,24],
defaultValue: 24,
submitOnChange: true,
multiple: false
)
if (state.ambientMap[state.weatherStationDataIndex].lastData.keySet().grep(~/^temp1?f$/)) {
input ( name: "notifyAlertLowTemp", type: "number", required: false,
title: "Notify when a temperature value is EQUAL OR BELOW this value. Leave field blank to cancel notification."
)
input ( name: "notifyAlertHighTemp", type: "number", required: false,
title: "Notify when a temperature value is EQUAL OR ABOVE this value. Leave field blank to cancel notification."
)
}
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('hourlyrainin')) {
input ( name: "notifyRain", type: "bool", required: false,
title: "Notify when RAIN is detected"
)
}
}
}
section (hideable: true, hidden: true, sectionHeader("Last Notification Times")) {
paragraph image: "",
required: false,
SMSNotifcationHistory()
}
}
}
private static boolean checkRequired(vars) {
def rc = false
vars.each {
if ((it) || it==true) {
rc = true
}
}
return rc
}
def appButtonHandler(String buttonName) {
logDebug "appButtonHandler: buttonName: ${buttonName}"
if (buttonName == "SyncLogOptions") {
logDebug "logLevel: ${settings.logLevel} logLevelTime: ${settings.logLevelTime}"
Integer level = settings.logLevel as Integer ?: 0
Integer time = settings.logLevelTime as Integer ?: 0
logInfo "level: ${LOG_LEVELS[level]} and time: ${LOG_TIMES[time]}"
logInfo "${app.name}: Current LogLevel is ${getLogLevelInfo()}"
logInfo "Synchronizing all AWS devices to 'level: ${LOG_LEVELS[level]}' and time: '${LOG_TIMES[time]}'"
getAllChildDevices().each {
logInfo "Before Sync → Current LogLevel of '${it.label}' is ${it.getLogLevelInfo()}"
it.syncLogLevelApp2Children(level, time)
logInfo "After Sync → Current LogLevel of '${it.label}' is ${it.getLogLevelInfo()}"
}
}
}
def initialize() {
logInfo "initialize()"
def now = now()
// Initialize/Reset Alert Warnings DateTime values
state.notifyAlertLowTempDT = 0
state.notifyAlertHighTempDT = 0
state.notifyRainDT = 0
state.notifySevereAlertDT = 0
state.notifyAlertFreq = notifyAlertFreq?:24
state.tempUnitsDisplay = tempUnits
state.windUnitsDisplay = windUnits
state.measureUnitsDisplay = measureUnits
state.baroUnitsDisplay = baroUnits
// Check for all devices needed to run this app
addAmbientChildDevice()
// Set user defined refresh rate
if(state.schedulerFreq!=schedulerFreq) {
logDebug "state.schedulerFreq → ${state.schedulerFreq} and schedulerFreq → ${schedulerFreq}"
def d = getChildDevice(state.deviceId)
logInfo "Updating your Cron REFRESH schedule from ${state.schedulerFreq?:0} mins to ${schedulerFreq} mins and updating ${d} AWS 'scheduleFreqMin' device attribute"
setScheduler(schedulerFreq)
d.sendEvent(name: "scheduleFreqMin", value: schedulerFreq)
state.schedulerFreq = schedulerFreq
}
checkLogLevel()
}
def installed() {
logInfo "installed()"
state.deviceId = DTHDNI()
initialize()
runIn(10, refresh)
}
def updated() {
logInfo "updated()"
initialize()
getAllChildDevices().each {
logInfo "Clearing current states of device '${it.label}' at '${it.deviceNetworkId}'"
it.deleteDeviceData()
}
refresh()
}
def setPollingInterval(pollingInterval=null) {
logDebug "==> setPollingInterval(${pollingInterval})"
// Set user defined refresh rate
if (POLLING_OPTIONS_MAP.containsKey(pollingInterval)) {
app.updateSetting("schedulerFreq", [type: "enum", value: pollingInterval])
if(state.schedulerFreq!=schedulerFreq) {
logDebug "state.schedulerFreq → ${state.schedulerFreq} and schedulerFreq → ${schedulerFreq}"
def d = getChildDevice(state.deviceId)
logInfo "Updating your Cron REFRESH schedule from ${state.schedulerFreq?:0} mins to ${schedulerFreq} mins and updating ${d.name} device attribute"
setScheduler(schedulerFreq)
d.sendEvent(name: "scheduleFreqMin", value: schedulerFreq)
state.schedulerFreq = schedulerFreq
}
} else {
logErr "Invalid polling interval '${pollingInterval}'. Valid polling interval keys are ${POLLING_OPTIONS_MAP.keySet()}"
return false
}
return true
}
def scheduleCheckReset(quiet=false) {
if (schedulerFreq!='0'){
setScheduler(schedulerFreq)
if (!quiet) {
Date start = new Date()
Date end = new Date()
use( TimeCategory ) {
end = start + schedulerFreq.toInteger().minutes
}
logInfo "Reset the next CRON Refresh to ~${schedulerFreq} mins from now (${end.format("h:mm:ss a", location.timeZone)}) to avoid excessive HTTP requests"
}
}
}
def refresh() {
updateMyLabel('refreshing')
logInfo "Device: 'Refresh ALL'"
def runID = new Random().nextInt(10000)
main(runID)
}
def autoScheduleHandler() {
def runID = new Random().nextInt(10000)
logInfo "Executing Cron Schedule runID: ${runID} every ${schedulerFreq} min(s)"
main(runID)
}
def main(runID=null) {
runID = (runID)?:new Random().nextInt(10000)
logInfo "Main(#${runID}) Section: Executing Ambient Weather Station API's for: '${state.weatherStationName}'"
// Ambient Weather Station API
ambientWeatherStation(runID)
// Notify Events Check
notifyEvents()
updateMyLabel('updated')
}
def retryQuick(data) {
logInfo "retryQuick #${state.retry} RunID: ${data.runID}"
// Ambient Weather Station API
updateMyLabel('retry')
ambientWeatherStation(data.runID)
// Notify Events Check
notifyEvents()
}
def ambientWeatherStation(runID="missing runID") {
// Ambient Weather Station
logInfo "Executing full ambientWeatherStation routine runID: ${runID}"
def d = getChildDevice(state.deviceId)
def okTOSendEvent = true
def remoteSensorDNI = ""
def now = new Date().format('EEE MMM d, h:mm:ss a',location.timeZone)
def nowTime = new Date().format('h:mm a',location.timeZone).toLowerCase()
def currentDT = new Date()
def sendEventOptions = ""
if (AmbientStationData(runID)) {
logDebug "httpget resp status = ${state.respStatus}"
logInfo "Processing Ambient Weather data returned from AmbientStationData)"
setStateWeatherStationData()
convertStateWeatherStationData()
if (settings.logLevel > 4) {
state.ambientMap[state.weatherStationDataIndex].each{ k, v ->
logTrace "${k} = ${v}"
if (k instanceof Map) {
k.each { x, y ->
logTrace "${x} = ${y}"
}
}
if (v instanceof Map) {
v.each { x, y ->
logTrace "${x} = ${y}"
}
}
}
}
logDebug "Checking Weather Station data array for 'Last Rain Date' information..."
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('lastRain')) {
logDebug "Weather Station has 'Last Rain Date' information...Processing"
def dateRain = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", state.ambientMap[state.weatherStationDataIndex].lastData.lastRain)
use (groovy.time.TimeCategory) {
def lastRainDuration = ((currentDT - dateRain) =~ /(.+)\b,/)[0][1]
logDebug ("lastRainDuration -> ${lastRainDuration}")
if (lastRainDuration) {
d.sendEvent(name:"lastRainDuration", value: lastRainDuration, displayed: false)
}
}
}
d.sendEvent(name:"scheduleFreqMin" , value: schedulerFreq, descriptionText: "AWS Polling Interval")
d.sendEvent(name:"lastSTupdate" , value: tileLastUpdated())
d.sendEvent(name:"macAddress" , value: state.ambientMap[state.weatherStationDataIndex].macAddress)
// Update Main Weather Device with Remote Sensor 1 values if tempf does not exist, same with humidity
if (!state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('tempf')) {
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('temp1f')) {
d.sendEvent(name:"temperature", value: state.ambientMap[state.weatherStationDataIndex].lastData.temp1f, units: state.tempUnitsDisplay)
}
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('humidity1')) {
d.sendEvent(name:"humidity", value: state.ambientMap[state.weatherStationDataIndex].lastData.humidity1, units: "%", displayed: false)
d.sendEvent(name:"humidity_display", value: "${state.ambientMap[state.weatherStationDataIndex].lastData.humidity1}%")
}
}
// Update Main Weather Device with Remote Sensor 1 values if tempinf does not exist, same with humidityin
if (!state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('tempinf')) {
logDebug "Fixing Main Station for inside temp"
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('temp1f')) {
d.sendEvent(name:"tempinf", value: state.ambientMap[state.weatherStationDataIndex].lastData.temp1f, units: state.tempUnitsDisplay, displayed: false)
d.sendEvent(name:"tempinf_display", value: "${state.ambientMap[state.weatherStationDataIndex].lastData.temp1f}${state.tempUnitsDisplay}")
}
}
if (!state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('humidityin')) {
logDebug "Fixing Main Station for inside humidity"
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('humidity1')) {
d.sendEvent(name:"humidityin", value: state.ambientMap[state.weatherStationDataIndex].lastData.humidity1, units: "%", displayed: false)
d.sendEvent(name:"humidityin_display", value: "${state.ambientMap[state.weatherStationDataIndex].lastData.humidity1}%")
}
}
state.ambientServerDate=convertToCurrentTimeZone(state.respdata[state.weatherStationDataIndex].lastData.date)
// Send AWS Info metaata events
def infoBase = state.ambientMap[state.weatherStationDataIndex].info
d.sendEvent(name: 'pwsName', value: infoBase?.name)
d.sendEvent(name: 'location', value: infoBase?.location)
d.sendEvent(name: 'lat', value: infoBase?.coords.coords.lat)
d.sendEvent(name: 'lon', value: infoBase?.coords.coords.lon)
d.sendEvent(name: 'address', value: infoBase?.coords.address)
d.sendEvent(name: 'elevation', value: infoBase?.coords.elevation)
// Loop through the weather data elements creating events
state.ambientMap[state.weatherStationDataIndex].lastData.each{ k, v ->
logDebug "Received Data ${k} = ${v}"
// Post weather data as a displayed string value
switch(k) {
case ~/.*rain.*/:
d.sendEvent(name: "${k}_display", value: "${v} ${state.measureUnitsDisplay}")
break
case ~/^barom.*/:
d.sendEvent(name: "${k}_display", value: "${v} ${state.baroUnitsDisplay}")
break
case ~/^tempi?n?f$|^dewPoint$|^feelsLikein$|^feelsLike$/:
d.sendEvent(name: "${k}_display", value: "${v}${state.tempUnitsDisplay}")
break
case ~/^wind.*|^maxdailygust$/:
d.sendEvent(name: "${k}_display", value: "${v} ${state.windUnitsDisplay}")
break
case ~/^humidity($|1|in)/:
d.sendEvent(name: "${k}_display", value: "${v}%")
break
case ~/^batt.*/:
// Change device battery level to 100% if the User preferences showBattery value has been defined and false
if ( (showBattery != null) && (!showBattery) ) {
v = 1
state.ambientMap[state.weatherStationDataIndex].lastData["${k}"] = v
}
break
default:
break
}
// Post weather data as numeric values except for dates, etc
okTOSendEvent = true
switch (k) {
case 'dateutc':
okTOSendEvent = false
break
case 'date':
v = state.ambientServerDate
break
case 'battin':
k='battery'
d.sendEvent(name: k, value: v.toInteger()*100, units:'%', displayed: false)
break
case 'battout':
k='battery'
v=v.toInteger()*100
break
case ~/^batt[0-9].*/:
okTOSendEvent = false
break
case 'lastRain':
v=convertToCurrentTimeZone(v)
break
case 'lightning_time':
def lightning_datetime = new Date(v).toString()
v=lightning_datetime
break
case 'tempf':
k='temperature'
break
case ~/^feelsLike$|^feelsLikein$/:
break
case 'windspeedmph':
// Send windSpeed as wind for Hubitat™
d.sendEvent(name: "wind" , value: v , displayed: false)
d.sendEvent(name: "windSpeed" , value: v , displayed: false)
break
case 'winddir':
def winddirectionState = degToCompass(state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, true)
logDebug "Wind Direction -> ${winddirectionState}"
d.sendEvent(name:'winddirection', value: winddirectionState, displayed: false)
d.sendEvent(name:'wind_cardinal', value: degToCompass(state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, false), displayed: false)
d.sendEvent(name:'winddir2', value: winddirectionState + " (" + state.ambientMap[state.weatherStationDataIndex].lastData.winddir + "º)")
// Send winddir as windVector for Hubitat™
d.sendEvent(name:'windVector', value: state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, displayed: false)
d.sendEvent(name:'windDirection', value: state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, displayed: false)
break
case 'uv':
def UVInumRange
switch (v) {
case {it < 3}:
UVInumRange="Low (${v})"
break
case {it < 6}:
UVInumRange="Medium (${v})"
break
case {it < 8}:
UVInumRange="High (${v})"
break
case {it < 11}:
UVInumRange="Very High (${v})"
break
default:
UVInumRange="Extreme (${v})"
break
}
d.sendEvent(name: 'ultravioletIndexDisplay', value: UVInumRange )
k='ultravioletIndex'
break
case 'yearlyrainin':
k='totalrainin'
break
case 'solarradiation':
logDebug "==> solarRadiation Raw = ${v}"
logDebug "==> solarRadiation Decimal Format= ${solarRadiationDecimalFormat}"
// Check to see if the user has set a decimal format for solar radiation
if (solarRadiationDecimalFormat) {
if (v.toInteger() > 0) {
def formatSpecifier = "%." + solarRadiationDecimalFormat + "f"
logDebug "==> formatSpecifier= ${formatSpecifier}"
v = String.format(formatSpecifier, v)
}
}
logDebug "==> solarRadiation decimal formatted = ${v}"
switch(solarRadiationTileDisplayUnits) {
case ('lux'):
v = wm2lux(v)
break
case ('fc'):
v = wm2fc(v)
break
default:
break
}
d.sendEvent(name: 'solarradiation_display', value: sprintf("%s %s",v,solarRadiationTileDisplayUnits?:'W/m²'), units: solarRadiationTileDisplayUnits?:'W/m²')
d.sendEvent(name: k, value: v, units: solarRadiationTileDisplayUnits?:'W/m²')
k='illuminance'
break
// Weather Console Sensors
case 'tempinf':
remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}0")
if (remoteSensorDNI) {
logDebug "Posted temperature with value ${v} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: "temperature", value: v, units: state.tempUnitsDisplay)
remoteSensorDNI.sendEvent(name:"date", value: state.ambientServerDate, displayed: false)
remoteSensorDNI.sendEvent(name:"lastSTupdate", value: tileLastUpdated(), displayed: false)
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('battout') ) {
remoteSensorDNI.sendEvent(name:"battery", value: state.ambientMap[state.weatherStationDataIndex].lastData.battout.toInteger()*100, displayed: false)
}
} else {
logErr "Missing ${DTHDNIRemoteSensorName()}0"
}
break
case 'humidityin':
remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}0")
if (remoteSensorDNI) {
logDebug "Posted humidity with value ${v} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: "humidity", value: v, units: "%", displayed: false)
remoteSensorDNI.sendEvent(name: "humidity_display", value: "${v}%")
} else {
logErr "Missing ${DTHDNIRemoteSensorName()}0"
}
break
// Post values for remote temperature & humidity sensors
case ~/^temp[0-9][0-9]?f$|^soiltemp[0-9][0-9]?$/:
def remoteIndexNumber = k.findAll( /\d+/ )[0]
remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${remoteIndexNumber}")
logDebug "${k} = ${remoteSensorDNI}"
if (remoteSensorDNI) {
logDebug "Posted temperature with value ${v} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: "temperature", value: v, units: state.tempUnitsDisplay)
remoteSensorDNI.sendEvent(name:"lastSTupdate", value: tileLastUpdated(), displayed: false)
remoteSensorDNI.sendEvent(name:"date", value: state.ambientServerDate, displayed: false)
String batteryFieldName = "batt" + remoteIndexNumber.toString()
logDebug "batteryFieldName for '${k}' = ${batteryFieldName}"
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey(batteryFieldName)) {
def battValue = state.ambientMap[state.weatherStationDataIndex].lastData."${batteryFieldName}".toInteger()*100
logDebug "batteryFieldName = ${batteryFieldName} = ${battValue}%"
remoteSensorDNI.sendEvent(name:"battery", value: battValue, displayed: false)
}
} else {
logErr "Missing ST Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
}
okTOSendEvent = false
break
case ~/^dewPoint\d/:
remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
logDebug "${k} = ${remoteSensorDNI}"
if (remoteSensorDNI) {
logDebug "Posted ${k.toUpperCase()}: ${v} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: "dewpoint", value: v, units: state.tempUnitsDisplay)
remoteSensorDNI.sendEvent(name: "dewPoint", value: v, units: state.tempUnitsDisplay)
remoteSensorDNI.sendEvent(name: "dewPoint_display", value: "${v}${state.tempUnitsDisplay}", units: state.tempUnitsDisplay)
} else {
logErr "Missing HE Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
}
okTOSendEvent = false
break
case ~/^feelsLike\d/:
remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
logDebug "Posted ${k.toUpperCase()} = ${remoteSensorDNI}"
if (remoteSensorDNI) {
logDebug "Posted ${k.toUpperCase()}: ${v} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: "feelsLike", value: v, units: state.tempUnitsDisplay)
remoteSensorDNI.sendEvent(name: "feelsLike_display", value: "${v}${state.tempUnitsDisplay}", units: state.tempUnitsDisplay)
} else {
logErr "Missing HE Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
}
okTOSendEvent = false
break
case ~/^feelsLikein\d/:
remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
logDebug "Posted ${k.toUpperCase()} = ${remoteSensorDNI}"
if (remoteSensorDNI) {
logDebug "Posted ${k.toUpperCase()}: ${v} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: "feelsLikein", value: v, units: state.tempUnitsDisplay)
remoteSensorDNI.sendEvent(name: "feelsLikein_display", value: "${v}${state.tempUnitsDisplay}", units: state.tempUnitsDisplay)
} else {
logErr "Missing HE Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
}
okTOSendEvent = false
break
case ~/^humidity[0-9][0-9]?$|^soilhum[0-9][0-9]?$/:
remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
logDebug "Posted ${k.toUpperCase()} = ${remoteSensorDNI}"
if (remoteSensorDNI) {
logDebug "Posted humidity with value ${v} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: "humidity", value: v, units: "%", displayed: false)
remoteSensorDNI.sendEvent(name: "humidity_display", value: "${v}%")
} else {
logErr "Missing ST Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
}
okTOSendEvent = false
break
// Post values for Particle Monitor which report PM10/PM25/AQI/CO2
case ~/(^pm.*)|(^co2.*)|(^aqi.*)/:
remoteSensorDNI = getChildDevice("${DTHDNIPMName()}")
logDebug "${k} = ${remoteSensorDNI}"
if (remoteSensorDNI) {
def sensorUnits = ''
switch (k) {
case ~/^aqi.*/:
sensorUnits = ''
break
case ~/^co2.*/:
sensorUnits = 'ppm'
break
case ~/^pm\d\d.*/:
sensorUnits = 'µg/m3'
break
case ~/.*temp.*/:
sensorUnits = '°F'
break
case ~/.*humidity.*/:
sensorUnits = '%'
break
default:
sensorUnits = ''
break
}
logDebug "Posted ${k}: ${v} ${sensorUnits} -> ${remoteSensorDNI}"
remoteSensorDNI.sendEvent(name: k, value: v, units: sensorUnits)
remoteSensorDNI.sendEvent(name:"lastSTupdate", value: tileLastUpdated(), displayed: false)
remoteSensorDNI.sendEvent(name:"date", value: state.ambientServerDate, displayed: false)
if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey("batt_25")) {
remoteSensorDNI.sendEvent(name:"battery", value: state.ambientMap[state.weatherStationDataIndex].lastData.batt_25.toInteger()*100, displayed: false)
} else if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey("batt_co2")) {
remoteSensorDNI.sendEvent(name:"battery", value: state.ambientMap[state.weatherStationDataIndex].lastData.batt_co2.toInteger()*100, displayed: false)
}
} else {
logErr "Missing HE Device ${DTHDNIPMName()} for ${k}"
}
okTOSendEvent = false
break
default:
break
}
if (okTOSendEvent){
logDebug "okTOSendEvent: name: ${k} = value: ${v}"
switch (k) {
case ('battery'):
case ('date'):
d.sendEvent(name: k, value: v, displayed : false)
break
case ~/^temp.*/:
d.sendEvent(name: k, value: v, units: state.tempUnitsDisplay, displayed : false)
break
case ~/^feelsLike$|^feelsLikein$/:
d.sendEvent(name: k, value: v, units: state.tempUnitsDisplay)
break
case ('dewPointin'):
case ('dewPoint'):
d.sendEvent(name: k, value: v, units: state.tempUnitsDisplay, displayed : false)
d.sendEvent(name: k.toLowerCase(), value: v, units : state.tempUnitsDisplay, displayed: false )
break
case ('illuminance'):
d.sendEvent(name: k, value: v, units: solarRadiationTileDisplayUnits?:'W/m²', displayed : false)
break
case ~/^humidity.*/:
d.sendEvent(name: k, value: v, units: '%', displayed : false)
break
case ~/.*rain.*/:
d.sendEvent(name: k, value: v, units: state.measureUnitsDisplay, displayed : false)
break
case ('windir'):
d.sendEvent(name: k, value: v, units: 'º', displayed : false)
break
case ~/^wind.*/:
case ('maxdailygust'):
d.sendEvent(name: k, value: v, units: state.windUnitsDisplay, displayed : false)
break
case ~/^barom.*/:
d.sendEvent(name: k, value: v, units: state.baroUnitsDisplay, displayed : false)
break
default:
d.sendEvent(name: k, value: v)
break
}
}
}
} else {
logDebug "AmbientStationData did not return any weather data"
}
}
def AmbientStationData(runID="????") {
def df = new java.text.SimpleDateFormat("hh:mm:ss a")
df.setTimeZone(location.timeZone)
def currentGETAmbientStationData = now()
state.lastGETAmbientStationData = state.lastGETAmbientStationData?:now()
logInfo "Start: AmbientStationData runID: ${runID} at ${df.format(new Date())}"
def timeSecsLastRun = (((currentGETAmbientStationData - state.lastGETAmbientStationData)/1000).toInteger())
logInfo "AmbientStationData Time difference is ${timeSecsLastRun} secs between last execution"
if (runID!=0 && timeSecsLastRun < 2) {
logWarn "Aborting AmbientStationData run ${runID}: Too Short for API Limits"
return
}
state.lastGETAmbientStationData = currentGETAmbientStationData
if(!state.apiKey){
logErr "Severe Error: The API key is UNDEFINED in ${app.name}'s IDE 'App Settings' field, fatal error now exiting"
return false
}
state.retry = state.retry?:0
scheduleCheckReset(true)
if (state.retry.toInteger()>0) {
logInfo "Executing Retry AmbientStationData re-attempt #${state.retry} for RunID: ${runID}"
}
def params = [
uri : "https://api.ambientweather.net",
path : "/v1/devices",
contentType : 'application/json',
query : [
"applicationKey" : APPKEY,
"apiKey" : state.apiKey
]
]
try {
httpGet(params) { resp ->
// get the data from the response body
state.respdata = resp.data
selectWeatherKeys(resp.data)
state.respStatus = resp.status
state.remove("httpError")
if (resp.status != 200) {
logErr "AmbientWeather.Net: response status code: ${resp.status}: response: ${resp.data}"
return false
}
if (state.weatherStationDataIndex) {
countRemoteTempHumiditySensors()
}
if (state.retry.toInteger()>0) {
logInfo "SUCCESS: Retry AmbientStationData re-attempt #${state.retry} for runID: ${runID}"
state.retry = 0
updateMyLabel('updated')
}
}
} catch (e) {
logDebug "Ambient Weather Station API Data runID ${runID}: ${e}"
resp?.headers.each {
logTrace "${it.name}: ${it.value}"
}
state.httpError = e.toString().toLowerCase()
if (e.toString().contains("unauthorized")) {
updateMyLabel('unauthorized')
return false
}
state.retry = state.retry.toInteger() + 1
if (state.retry.toInteger()<4) {
logInfo "Waiting 10 seconds to Try HttpGet Again runID ${runID}: Attempt #${state.retry}"
updateMyLabel('retry')
runIn(10, 'retryQuick', [overwrite: true, data: [runID: "${runID}"]])
}
return false
}
logInfo "SUCCESS: AmbientStationData successfully updated for runID: ${runID}"
updateMyLabel('updated')
return true
}
def addAmbientChildDevice() {
// add Ambient Weather Reporter Station devices
// Derive a Short Name for the Weather Station and Remote Sensors
// Create/Validate Weather Console Device
def AWSName = "${state.weatherStationName}-Console"
def AWSLabel = "AWS-Console"
def AWSDNI = getChildDevice(state.deviceId)
if (!AWSDNI) {
logInfo "NEW: Adding Ambient Device: ${AWSName} with DNI: ${state.deviceId}"
try {
addChildDevice(DTHnamespace(), DTHName(), DTHDNI(), null, ["name": AWSName, "label": AWSLabel, completedSetup: true])
} catch(ex) {
logErr "The Ambient Weather Device Handler '${DTHName()}' was not found in your 'My Device Handlers', Error-> '${ex}'. Please run HPM Repair option for Ambient Weather Station"
return false
}
logInfo "Success: Added ${AWSName} with DNI: ${DTHDNI()}"
} else {
logInfo "Verified Weather Station '${getChildDevice(state.deviceId).label}' = DNI: '${DTHDNI()}'"
}
// add Ambient Weather Remote Sensor Device(s)
def remoteSensorNamePref
def remoteSensorLabelPref
def remoteSensorNameDNI
def remoteSensorNumber
settings.each { key, value ->
if ( key.startsWith(DTHDNIRemoteSensorName()) ) {
remoteSensorNamePref = "${state.weatherStationName}-${value}"
remoteSensorLabelPref = "AWS-${value}"
remoteSensorNameDNI = getChildDevice(key)
remoteSensorNumber = key.reverse()[0..0]
if (remoteSensorNumber.toInteger() <= MaxNumRemoteSensors()) {
if (!remoteSensorNameDNI) {
logInfo "NEW: Adding Remote Sensor #${remoteSensorNumber}: ${remoteSensorLabelPref}"
try {
addChildDevice(DTHnamespace(), DTHRemoteSensorName(), "${key}", null, ["name": remoteSensorNamePref, "label": remoteSensorLabelPref, completedSetup: true])
} catch(ex) {
logErr "The Ambient Weather Device Handler '${DTHRemoteSensorName()}' was not found in your 'My Device Handlers', Error-> '${ex}'. Please run HPM Repair option for Ambient Weather Station."
return false
}
logInfo "Success Added Ambient Remote Sensor: ${remoteSensorLabelPref} with DNI: ${key}"
} else {
logInfo "Verified Remote Sensor #${remoteSensorNumber} of ${state.countRemoteTempHumiditySensors} Exists: ${getChildDevice(key).label} = DNI: ${key}"
// Update Device Label Values
if (remoteSensorNameDNI.label != value) {
logInfo "Renaming Device ${remoteSensorNameDNI.deviceNetworkId}: Old Device Label: ${remoteSensorNameDNI.label} → New Device Label: ${value}"
remoteSensorNameDNI.label = value
}
}
} else {
logWarn "Device ${remoteSensorNumber} DNI: ${key} '${remoteSensorNameDNI.name}' exceeds # of remote sensors (${state.countRemoteTempHumiditySensors}) reporting from Ambient -> ACTION REQUIRED"
logWarn "Please verify that all Ambient Remote Sensors are online and reporting to Ambient Network. If so, please manually delete the device in the 'Devices' view"
}
}
}
// add Ambient Weather Particulate Monitor Device(s)
def PMkey = "${DTHDNIPMName()}"
def PMvalue = settings.find{ it.key == "${DTHDNIPMName()}" }?.value
if(PMvalue) {
remoteSensorNamePref = "${state.weatherStationName}${PMvalue?'-'+PMvalue:''}"
remoteSensorLabelPref = "AWS-${PMvalue}"
remoteSensorNameDNI = getChildDevice(PMkey)
if (!remoteSensorNameDNI) {
logInfo "NEW: Adding Particulate Monitor device: ${remoteSensorLabelPref}"
try {
addChildDevice(DTHnamespace(), DTHPMSensorName(), "${PMkey}", null, ["name": remoteSensorNamePref, "label": remoteSensorLabelPref, completedSetup: true])
} catch(ex) {
logErr "The Ambient Weather Device Handler '${DTHPMSensorName()}' was not found in your 'My Device Handlers', Error-> '${ex}'. Please install this in the IDE's 'My Device Handlers'"
return false
}
logInfo "Success Added Ambient Particulate Monitor: ${remoteSensorLabelPref} with DNI: ${PMkey}"
} else {
if(infoVerbose){logInfo "Verified Particulate Monitor ${state.countParticulateMonitors} Exists: ${remoteSensorLabelPref} = DNI: ${PMkey}"}
// Update Device Label Values
if (remoteSensorNameDNI.label != PMvalue) {
logInfo "Renaming Device ${remoteSensorNameDNI.deviceNetworkId}: Old Device Label: ${remoteSensorNameDNI.label} → New Device Label: ${PMvalue}"
remoteSensorNameDNI.label = PMvalue
}
}
}
}
def degToCompass(num,longTitles=true) {
if (num) {
def val = Math.floor((num.toFloat() / 22.5) + 0.5).toInteger()
def arr = []
if (longTitles) {
arr = ["N", "North NE", "NE", "East NE", "E", "East SE", "SE", "South SE", "S", "South SW", "SW", "West SW", "W", "West NW", "NW", "North NW"]
} else {
arr = ["N", "N NE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
}
return arr[(val % 16)]
}
return "N/A"
}
def setScheduler(schedulerFreq) {
def scheduleHandler = 'autoScheduleHandler'
unschedule(scheduleHandler)
def randonInt = Math.abs(new Random().nextInt() % 59) + 1
if(infoVerbose){logInfo "Auto Schedule Refresh Rate is now -> ${schedulerFreq} mins"}
switch(schedulerFreq) {
case '0':
logInfo "Auto Schedule Refresh Rate is now: OFF"
break
case '1':
runEvery1Minute(scheduleHandler)
break
case '2':
schedule("0 ${randonInt}/2 * * * ?",scheduleHandler)
break
case '3':
schedule("0 ${randonInt}/3 * * * ?",scheduleHandler)
break
case '4':
schedule("0 ${randonInt}/4 * * * ?",scheduleHandler)
break
case '5':
runEvery5Minutes(scheduleHandler)
break
case '10':
runEvery10Minutes(scheduleHandler)
break
case '15':
runEvery15Minutes(scheduleHandler)
break
case '30':
runEvery30Minutes(scheduleHandler)
break
case '60':
runEvery1Hour(scheduleHandler)
break
case '120':
schedule("0 ${randonInt} 0/2 * * ?",scheduleHandler)
break
case '180':
runEvery3Hours(scheduleHandler)
break
default :
unschedule()
break
}
}
def tileLastUpdated() {
def now = new Date().format('EEE MMM dd, hh:mm:ss a',location.timeZone)
return now
}
def informationList(variable) {
switch(variable) {
case ("apiHelp") :
// Help Text for API Key
variable = [
"You MUST enter your Ambient Weather API key in the ${appNameVersion()}.",
"Visit your Ambient Weather Dashboards's Account page.",
"Create/Copy your API key from the bottom of the page",
"Return to your Hubitat App.",
"Exit the SmartApp and Start ${appNameVersion()} Setup again."
]
break
default:
break
}
if (variable instanceof List) {
def numberedText = ""
variable.eachWithIndex { item, index ->
numberedText += "${index+1}. ${item}"
numberedText += (index
if (k.value) {
date = new Date(k.value).format("MMM-DD-YYYY h:mm a", location.timeZone)
dateToday = new Date(k.value).format("MMM-DD-YYYY", location.timeZone)
if (today==dateToday) {
date = "Today @ " + new Date(k.value).format("h:mm a", location.timeZone)
}
msg += "${index+1}) ${k.key} : ${date}\n"
} else {
msg += "${index+1}) ${k.key} : --\n"
}
}
return msg
}
def notifyEvents() {
if (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled])){
def now = now()
def msg
def tempCheck = state.ambientMap[state.weatherStationDataIndex].lastData.tempf?:state.ambientMap[state.weatherStationDataIndex].lastData.temp1f
def ambientWeatherStationName = "${DTHName()} - '${state.weatherStationName}'"
if ( (notifyAlertLowTemp) && (tempCheck) && (tempCheck.toInteger()<=notifyAlertLowTemp) ) {
msg = "${ambientWeatherStationName}: LOW TEMP ALERT: Current temperature of ${tempCheck}${state.tempUnitsDisplay} <= ${notifyAlertLowTemp}${state.tempUnitsDisplay}"
if (lastNotifyDT(state.notifyAlertLowTempDT, "Low Temp")) {
send_message(msg)
state.notifyAlertLowTempDT = now
}
}
if ( (notifyAlertHighTemp) && (tempCheck) && (tempCheck.toInteger()>=notifyAlertHighTemp) ) {
msg = "${ambientWeatherStationName}: HIGH TEMP ALERT: Current temperature of ${tempCheck}${state.tempUnitsDisplay} >= ${notifyAlertHighTemp}${state.tempUnitsDisplay}"
if (lastNotifyDT(state.notifyAlertHighTempDT, "High Temp")) {
state.notifyAlertHighTempDT = now
send_message(msg)
}
}
if ( (notifyRain) && (state.ambientMap[state.weatherStationDataIndex].lastData.hourlyrainin) && (state.ambientMap[state.weatherStationDataIndex].lastData.hourlyrainin.toFloat()>0) ){
msg = "${ambientWeatherStationName}: RAIN DETECTED ALERT: Current hourly rain sensor reading of ${state.ambientMap[state.weatherStationDataIndex].lastData.hourlyrainin} ${state.measureUnitsDisplay}/hr"
if (lastNotifyDT(state.notifyRainDT, "Rain")) {
state.notifyRainDT = now
send_message(msg)
}
}
}
}
def lastNotifyDT(lastDT, eventName) {
if (!lastDT) { return true }
def now = now()/1000
def date = new Date(lastDT).format("MMM-DD-YYYY h:mm:ss a", location.timeZone)
def hours = ((now-(lastDT/1000))/3600).toFloat().round(1)
def days = (hours/24).toFloat().round(1)
def rc = hours>=state.notifyAlertFreq.toInteger()
logInfo "This '${eventName}' event was last sent on ${date}: ${days} days, ${hours} hours ago"
logInfo "${eventName} Alert Every ${notifyAlertFreq} hours: ${rc?'OK to SMS':'TOO EARLY TO SEND'}"
return rc
}
def convertStateWeatherStationData() {
// Check to see if Units of Measure have been defined in the preferences section, otherwise default to Hub's location for imperial or metric
if (tempUnits == null) {
def tempUnitsSmartThingsScale = getTemperatureScale()
logWarn "Missing 'Units of Measure' App Preference Setting Values: ALL Default Units of Measure will be based on your hub's location temperature preference of '${tempUnitsSmartThingsScale}'"
logWarn "Please run '${state.weatherStationName}' SmartAPP install to select your default Units of Measure for display"
state.tempUnitsDisplay = "°${tempUnitsSmartThingsScale}"
state.windUnitsDisplay = (tempUnitsSmartThingsScale == "F") ? "mph" : "kph"
state.measureUnitsDisplay = (tempUnitsSmartThingsScale == "F") ? "in" : "cm"
state.baroUnitsDisplay = (tempUnitsSmartThingsScale == "F") ? "inHg" : "mmHg"
}
logDebug "tempUnitsDisplay = ${state.tempUnitsDisplay}, windUnitsDisplay = ${state.windUnitsDisplay}, measureUnitsDisplay = ${state.measureUnitsDisplay}, baroUnitsDisplay = ${state.baroUnitsDisplay}"
def tempVar = null
def newAmbientMap = [:]
newAmbientMap = state.ambientMap
newAmbientMap[state.weatherStationDataIndex].lastData.each{ k, v ->
tempVar = null
switch (k) {
case ~/^temp.*/:
case ~/^feelsLike.*/:
case 'dewPoint':
if (state.tempUnitsDisplay == '°C') {
tempVar = String.format("%.01f",(v-32)*5/9)
}
break
case ~/.*rain.*/:
try {
if (state.measureUnitsDisplay == 'cm') {
tempVar = String.format("%.02f",v*2.54)
} else if (state.measureUnitsDisplay == 'mm') {
tempVar = String.format("%.02f",v*25.4)
}
} catch(Exception ex) {
tempVar = 0
}
break
case ~/^winddir.*/:
break
case ~/^wind.*/:
case ('maxdailygust'):
if (state.windUnitsDisplay == 'kph') {
tempVar = String.format("%.02f",v*1.609344)
} else if (state.windUnitsDisplay == 'fps') {
tempVar = String.format("%.02f",v*2/3)
} else if (state.windUnitsDisplay == 'mps') {
tempVar = String.format("%.02f",v*0.44704)
} else if (state.windUnitsDisplay == 'knotts') {
tempVar = String.format("%.02f",v*0.86898)
}
break
case ~/^barom.*/:
if (state.baroUnitsDisplay == 'mmHg') {
tempVar = String.format("%.02f",v*25.4)
} else if (state.baroUnitsDisplay == 'hPa') {
tempVar = String.format("%.02f",v*33.86389)
}
break
default:
break
}
if(tempVar != null) {
logDebug "tempVar k=${k}, v=${v} tempVar=${tempVar}"
newAmbientMap[state.weatherStationDataIndex].lastData."${k}" = tempVar.toFloat()
}
}
state.ambientMap = [:]
state.ambientMap = newAmbientMap
}
def setStateWeatherStationData() {
if (weatherStationMac) {
state.weatherStationDataIndex = state.ambientMap.findIndexOf {
it.macAddress in [weatherStationMac]
}
}
state.weatherStationDataIndex = state.weatherStationDataIndex?:0
state.weatherStationMac = state.weatherStationMac?:state.ambientMap[state.weatherStationDataIndex].macAddress
state.weatherStationName = state.ambientMap[state.weatherStationDataIndex].info.name
state.weatherStationLocation = state.ambientMap[state.weatherStationDataIndex].info.location?:state.ambientMap[state.weatherStationDataIndex].info.containsKey("coords")?state.ambientMap[state.weatherStationDataIndex].info.coords.location:''
countRemoteTempHumiditySensors()
countParticulateMonitors()
}
def countRemoteTempHumiditySensors() {
state.countRemoteTempHumiditySensors = state.ambientMap[state.weatherStationDataIndex].lastData.keySet().count { it.matches('^temp[0-9][0-9]?f|^soiltemp[0-9]?[0-9]?|^soilhum[0-9]?[0-9]?') }
return state.countRemoteTempHumiditySensors
}
def countParticulateMonitors() {
def pmDevice = state.ambientMap[state.weatherStationDataIndex].lastData?.keySet().count { it.matches('^pm.*') }
if (pmDevice == 0) state.countParticulateMonitors = 0
else state.countParticulateMonitors = 1
return state.countParticulateMonitors
}
def unitsSet() {
if ([tempUnits, windUnits, measureUnits, baroUnits, solarRadiationTileDisplayUnits].findAll({it != null}).join()=='') return "Tap to Select"
return sprintf("%s, %s, %s, %s, %s", tempUnits, windUnits, measureUnits, baroUnits, solarRadiationTileDisplayUnits)
}
def alertFilterList() {
def x = [
"ABV":"ABV - Rawinsonde Data Above 100 Millibars",
"ADA":"ADA - Alarm/Alert Administrative Msg",
"ADM":"ADM - Alert Administrative Message",
"ADR":"ADR - NWS Administrative Message",
"ADV":"ADV - Generic Space Environment Advisory",
"AFD":"AFD - Area Forecast Discussion",
"AFM":"AFM - Area Forecast Matrices",
"AFP":"AFP - Area Forecast Product",
"AFW":"AFW - Fire Weather Matrix",
"AGF":"AGF - Agricultural Forecast",
"AGO":"AGO - Agricultural Observations",
"ALT":"ALT - Space Environment Alert",
"AQA":"AQA - Air Quality Alert",
"AQI":"AQI - Air Quality Index Statement",
"ASA":"ASA - Air Stagnation Advisory",
"AVA":"AVA - Avalanche Watch",
"AVW":"AVW - Avalanche Warning",
"AWO":"AWO - Area Weather Outlook",
"AWS":"AWS - Area Weather Summary",
"AWU":"AWU - Area Weather Update",
"AWW":"AWW - Airport Weather Warning",
"BOY":"BOY - Buoy Report",
"BRG":"BRG - Coast Guard Observations",
"BRT":"BRT - Hourly Roundup for Weather Radio",
"CAE":"CAE - Child Abduction Emergency",
"CCF":"CCF - Coded City Forecast",
"CDW":"CDW - Civil Danger Warning",
"CEM":"CEM - Civil Emergency Message",
"CF6":"CF6 - WFO Monthly/Daily Climate Data",
"CFP":"CFP - Convective Forecast Product",
"CFW":"CFW - Coastal Flood Warnings/Watches/Statements",
"CGR":"CGR - Coast Guard Surface Report",
"CHG":"CHG - Computer Hurricane Guidance",
"CLA":"CLA - Climatological Report (Annual)",
"CLI":"CLI - Climatological Report (Daily)",
"CLM":"CLM - Climatological Report (Monthly)",
"CLQ":"CLQ - Climatological Report (Quarterly)",
"CLS":"CLS - Climatological Report (Seasonal)",
"CLT":"CLT - Climate Report",
"CMM":"CMM - Coded Climatological Monthly Means",
"COD":"COD - Coded Analysis and Forecasts",
"CPF":"CPF - Great Lakes Port Forecast",
"CUR":"CUR - Routine Space Environment Products",
"CWA":"CWA - Center (CWSU) Weather Advisory",
"CWF":"CWF - Coastal Waters Forecast",
"CWS":"CWS - Center (CWSU) Weather Statement",
"DAY":"DAY - Routine Space Environment Product (Daily)",
"DDO":"DDO - Daily Dispersion Outlook",
"DGT":"DGT - Drought Information Statement",
"DSA":"DSA - Unnumbered Depression / Suspicious Area Advisory",
"DSM":"DSM - ASOS Daily Summary",
"DSW":"DSW - Dust Storm Warning and Dust Advisory",
"EFP":"EFP - 3 To 5 Day Extended Forecast",
"EOL":"EOL - Average 6 To 10 Day Weather Outlook (Local)",
"EQI":"EQI - Tsunami Bulletin",
"EQR":"EQR - Earthquake Report",
"EQW":"EQW - Earthquake Warning",
"ESF":"ESF - Flood Potential Outlook",
"ESG":"ESG - Extended Streamflow Guidance",
"ESP":"ESP - Extended Streamflow Prediction",
"ESS":"ESS - Water Supply Outlook",
"EVI":"EVI - Evacuation Immediate",
"EWW":"EWW - Extreme Wind Warning",
"FA0":"FA0 - Aviation Area Forecasts (Pacific)",
"FA1":"FA1 - Aviation Area Forecasts (Northeast)",
"FA2":"FA2 - Aviation Area Forecasts (Southeast)",
"FA3":"FA3 - Aviation Area Forecasts (North Central)",
"FA4":"FA4 - Aviation Area Forecasts (South Central)",
"FA5":"FA5 - Aviation Area Forecasts (Rocky Mountains)",
"FA6":"FA6 - Aviation Area Forecasts (West Coast)",
"FA7":"FA7 - Aviation Area Forecasts (Juneau, AK)",
"FA8":"FA8 - Aviation Area Forecasts (Anchorage, AK)",
"FA9":"FA9 - Aviation Area Forecasts (Fairbanks, AK)",
"FD0":"FD0 - 24 Hr Fd Winds Aloft Fcst (45,000 and 53,000 Ft)",
"FD1":"FD1 - 6 Hour Winds Aloft Forecast",
"FD2":"FD2 - 12 Hour Winds Aloft Forecast",
"FD3":"FD3 - 24 Hour Winds Aloft Forecast",
"FD4":"FD4 - Winds Aloft Forecast",
"FD5":"FD5 - Winds Aloft Forecast",
"FD6":"FD6 - Winds Aloft Forecast",
"FD7":"FD7 - Winds Aloft Forecast",
"FD8":"FD8 - 6 Hour Fd Winds Aloft Fcst (45,000 and 53,000 Ft)",
"FD9":"FD9 - 12 Hr Fd Winds Aloft Fcst (45,000 and 53,000 Ft)",
"FDI":"FDI - Fire Danger Indices",
"FFA":"FFA - Flash Flood Watch",
"FFG":"FFG - Flash Flood Guidance",
"FFH":"FFH - Headwater Guidance",
"FFS":"FFS - Flash Flood Statement",
"FFW":"FFW - Flash Flood Warning",
"FLN":"FLN - National Flood Summary",
"FLS":"FLS - Flood Statement",
"FLW":"FLW - Flood Warning",
"FOF":"FOF - Upper Wind Fallout Forecast",
"FRW":"FRW - Fire Warning",
"FSH":"FSH - Natl Marine Fisheries Administrative Service Message",
"FTM":"FTM - WSR-88D Radar Outage Notification / Free Text Message",
"FTP":"FTP - FOUS Prog Max/Min Temp/Pop Guidance",
"FWA":"FWA - Fire Weather Administrative Message",
"FWD":"FWD - Fire Weather Outlook Discussion",
"FWF":"FWF - Routine Fire Wx Fcst (With/Without 6-10 Day Outlook)",
"FWL":"FWL - Land Management Forecasts",
"FWM":"FWM - Miscellaneous Fire Weather Product",
"FWN":"FWN - Fire Weather Notification",
"FWO":"FWO - Fire Weather Observation",
"FWS":"FWS - Suppression Forecast",
"FZL":"FZL - Freezing Level Data (RADAT)",
"GLF":"GLF - Great Lakes Forecast",
"GLS":"GLS - Great Lakes Storm Summary",
"GRE":"GRE - GREEN",
"HD1":"HD1 - RFC Derived QPF Data Product",
"HD2":"HD2 - RFC Derived QPF Data Product",
"HD3":"HD3 - RFC Derived QPF Data Product",
"HD4":"HD4 - RFC Derived QPF Data Product",
"HD7":"HD7 - RFC Derived QPF Data Product",
"HD8":"HD8 - RFC Derived QPF Data Product",
"HD9":"HD9 - RFC Derived QPF Data Product",
"HLS":"HLS - Hurricane Local Statement",
"HMD":"HMD - Hydrometeorological Discussion",
"HML":"HML - AHPS XML",
"HMW":"HMW - Hazardous Materials Warning",
"HP1":"HP1 - RFC QPF Verification Product",
"HP2":"HP2 - RFC QPF Verification Product",
"HP3":"HP3 - RFC QPF Verification Product",
"HP4":"HP4 - RFC QPF Verification Product",
"HP5":"HP5 - RFC QPF Verification Product",
"HP6":"HP6 - RFC QPF Verification Product",
"HP7":"HP7 - RFC QPF Verification Product",
"HP8":"HP8 - RFC QPF Verification Product",
"HRR":"HRR - Weather Roundup",
"HSF":"HSF - High Seas Forecast",
"HWO":"HWO - Hazardous Weather Outlook",
"HWR":"HWR - Hourly Weather Roundup",
"HYD":"HYD - Daily Hydrometeorological Products",
"HYM":"HYM - Monthly Hydrometeorological Plain Language Product",
"ICE":"ICE - Ice Forecast",
"IDM":"IDM - Ice Drift Vectors",
"INI":"INI - ADMINISTR [NOUS51 KWBC]",
"IOB":"IOB - Ice Observation",
"KPA":"KPA - Keep Alive Message",
"LAE":"LAE - Local Area Emergency",
"LCD":"LCD - Preliminary Local Climatological Data",
"LCO":"LCO - Local Cooperative Observation",
"LEW":"LEW - Law Enforcement Warning",
"LFP":"LFP - Local Forecast",
"LKE":"LKE - Lake Stages",
"LLS":"LLS - Low-Level Sounding",
"LOW":"LOW - Low Temperatures",
"LSR":"LSR - Local Storm Report",
"LTG":"LTG - Lightning Data",
"MAN":"MAN - Rawinsonde Observation Mandatory Levels",
"MAP":"MAP - Mean Areal Precipitation",
"MAW":"MAW - Amended Marine Forecast",
"MFM":"MFM - Marine Forecast Matrix",
"MIM":"MIM - Marine Interpretation Message",
"MIS":"MIS - Miscellaneous Local Product",
"MOB":"MOB - MOB Observations",
"MON":"MON - Routine Space Environment Product Issued Monthly",
"MRP":"MRP - Techniques Development Laboratory Marine Product",
"MSM":"MSM - ASOS Monthly Summary Message",
"MTR":"MTR - METAR Formatted Surface Weather Observation",
"MTT":"MTT - METAR Test Message",
"MVF":"MVF - Marine Verification Coded Message",
"MWS":"MWS - Marine Weather Statement",
"MWW":"MWW - Marine Weather Message",
"NOU":"NOU - Weather Reconnaisance Flights",
"NOW":"NOW - Short Term Forecast",
"NOX":"NOX - Data Mgt Message",
"NPW":"NPW - Non-Precipitation Warnings / Watches / Advisories",
"NSH":"NSH - Nearshore Marine Forecast",
"NUW":"NUW - Nuclear Power Plant Warning",
"NWR":"NWR - NOAA Weather Radio Forecast",
"OAV":"OAV - Other Aviation Products",
"OBS":"OBS - Observations",
"OFA":"OFA - Offshore Aviation Area Forecast",
"OFF":"OFF - Offshore Forecast",
"OMR":"OMR - Other Marine Products",
"OPU":"OPU - Other Public Products",
"OSO":"OSO - Other Surface Observations",
"OSW":"OSW - Ocean Surface Winds",
"OUA":"OUA - Other Upper Air Data",
"OZF":"OZF - Zone Forecast",
"PFM":"PFM - Point Forecast Matrices",
"PFW":"PFW - Fire Weather Point Forecast Matrices",
"PLS":"PLS - Plain Language Ship Report",
"PMD":"PMD - Prognostic Meteorological Discussion",
"PNS":"PNS - Public Information Statement",
"POE":"POE - Probability of Exceed",
"PRB":"PRB - Heat Index Forecast Tables",
"PRC":"PRC - State Pilot Report Collective",
"PRE":"PRE - Preliminary Forecasts",
"PSH":"PSH - Post Storm Hurricane Report",
"PTS":"PTS - Probabilistic Outlook Points",
"PWO":"PWO - Public Severe Weather Outlook",
"PWS":"PWS - Tropical Cyclone Probabilities",
"QPF":"QPF - Quantitative Precipitation Forecast",
"QPS":"QPS - Quantitative Precipitation Statement",
"RDF":"RDF - Revised Digital Forecast",
"REC":"REC - Recreational Report",
"RER":"RER - Record Report",
"RET":"RET - EAS Activation Request",
"RFD":"RFD - Rangeland Fire Danger Forecast",
"RFI":"RFI - RFI Observation",
"RFR":"RFR - Route Forecast",
"RFW":"RFW - Red Flag Warning",
"RHW":"RHW - Radiological Hazard Warning",
"RNS":"RNS - Rain Information Statement",
"RR1":"RR1 - Hydro-Met Data Report Part 1",
"RR2":"RR2 - Hydro-Met Data Report Part 2",
"RR3":"RR3 - Hydro-Met Data Report Part 3",
"RR4":"RR4 - Hydro-Met Data Report Part 4",
"RR5":"RR5 - Hydro-Met Data Report Part 5",
"RR6":"RR6 - Hydro-Met Data Report Part 6",
"RR7":"RR7 - Hydro-Met Data Report Part 7",
"RR8":"RR8 - Hydro-Met Data Report Part 8",
"RR9":"RR9 - Hydro-Met Data Report Part 9",
"RRA":"RRA - Automated Hydrologic Observation Sta Report (AHOS)",
"RRM":"RRM - Miscellaneous Hydrologic Data",
"RRS":"RRS - HADS Data",
"RRY":"RRY - ASOS SHEF Hourly Routine Test Message",
"RSD":"RSD - Daily Snotel Data",
"RSM":"RSM - Monthly Snotel Data",
"RTP":"RTP - Regional Max/Min Temp and Precipitation Table",
"RVA":"RVA - River Summary",
"RVD":"RVD - Daily River Forecasts",
"RVF":"RVF - River Forecast",
"RVI":"RVI - River Ice Statement",
"RVM":"RVM - Miscellaneous River Product",
"RVR":"RVR - River Recreation Statement",
"RVS":"RVS - River Statement",
"RWR":"RWR - Regional Weather Roundup",
"RWS":"RWS - Regional Weather Summary",
"SAB":"SAB - Special Avalanche Bulletin",
"SAF":"SAF - Speci Agri Wx Fcst / Advisory / Flying Farmer Fcst Outlook",
"SAG":"SAG - Snow Avalanche Guidance",
"SAT":"SAT - APT Prediction",
"SAW":"SAW - Prelim Notice of Watch & Cancellation Msg (Aviation)",
"SCC":"SCC - Storm Summary",
"SCD":"SCD - Supplementary Climatological Data (ASOS)",
"SCN":"SCN - Soil Climate Analysis Network Data",
"SCP":"SCP - Satellite Cloud Product",
"SCS":"SCS - Selected Cities Summary",
"SDO":"SDO - Supplementary Data Observation (ASOS)",
"SDS":"SDS - Special Dispersion Statement",
"SEL":"SEL - Severe Local Storm Watch and Watch Cancellation Msg",
"SEV":"SEV - SPC Watch Point Information Message",
"SFP":"SFP - State Forecast",
"SFT":"SFT - Tabular State Forecast",
"SGL":"SGL - Rawinsonde Observation Significant Levels",
"SHP":"SHP - Surface Ship Report at Synoptic Time",
"SIG":"SIG - International Sigmet / Convective Sigmet",
"SIM":"SIM - Satellite Interpretation Message",
"SLS":"SLS - Severe Local Storm Watch and Areal Outline",
"SMF":"SMF - Smoke Management Weather Forecast",
"SMW":"SMW - Special Marine Warning",
"SOO":"SOO - SOO Product",
"SPE":"SPE - Satellite Precipitation Estimates (TXUS20 KWBC)",
"SPF":"SPF - Storm Strike Probability Bulletin (TPC)",
"SPS":"SPS - Special Weather Statement",
"SPW":"SPW - Shelter in Place Warning",
"SQW":"SQW - Snow Squall Warning",
"SRD":"SRD - Surf Discussion",
"SRF":"SRF - Surf Forecast",
"SRG":"SRG - Soaring Guidance",
"SSM":"SSM - Main Synoptic Hour Surface Observation",
"STA":"STA - Network and Severe Weather Statistical Summaries",
"STD":"STD - Satellite Tropical Disturbance Summary",
"STO":"STO - Road Condition Reports (State Agencies)",
"STP":"STP - State Max/Min Temperature and Precipitation Table",
"STQ":"STQ - Spot Forecast Request",
"SUM":"SUM - Space Weather Message",
"SVR":"SVR - Severe Thunderstorm Warning",
"SVS":"SVS - Severe Weather Statement",
"SWO":"SWO - Severe Storm Outlook Narrative (AC)",
"SWS":"SWS - State Weather Summary",
"SYN":"SYN - Regional Weather Synopsis",
"TAF":"TAF - Terminal Aerodrome Forecast",
"TAP":"TAP - Terminal Alerting Products",
"TAV":"TAV - Travelers Forecast Table",
"TCA":"TCA - Aviation Tropical Cyclone Advisory",
"TCD":"TCD - Tropical Cyclone Discussion",
"TCE":"TCE - Tropical Cyclone Position Estimate",
"TCM":"TCM - Marine/Aviation Tropical Cyclone Advisory",
"TCP":"TCP - Public Tropical Cyclone Advisory",
"TCS":"TCS - Satellite Tropical Cyclone Summary",
"TCU":"TCU - Tropical Cyclone Update",
"TCV":"TCV - Tropical Cyclone Watch/Warning Break Points",
"TIB":"TIB - Tsunami Bulletin",
"TID":"TID - Tide Report",
"TMA":"TMA - Tsunami Tide/Seismic Message Acknowledgement",
"TOE":"TOE - 911 Telephone Outage Emergency",
"TOR":"TOR - Tornado Warning",
"TPT":"TPT - Temperature Precipitation Table (Natl and Intnl)",
"TSU":"TSU - Tsunami Watch/Warning",
"TUV":"TUV - Weather Bulletin",
"TVL":"TVL - Travelers Forecast",
"TWB":"TWB - Transcribed Weather Broadcast",
"TWD":"TWD - Tropical Weather Discussion",
"TWO":"TWO - Tropical Weather Outlook and Summary",
"TWS":"TWS - Tropical Weather Summary",
"URN":"URN - Aircraft Reconnaissance",
"UVI":"UVI - Ultraviolet Index",
"VAA":"VAA - Volcanic Activity Advisory",
"VER":"VER - Forecast Verification Statistics",
"VFT":"VFT - Terminal Aerodrome Forecast (TAF) Verification",
"VOW":"VOW - Volcano Warning",
"WA0":"WA0 - Airmet (Pacific)",
"WA1":"WA1 - Airmet (Northeast)",
"WA2":"WA2 - Airmet (Southeast)",
"WA3":"WA3 - Airmet (North Central)",
"WA4":"WA4 - Airmet (South Central)",
"WA5":"WA5 - Airmet (Rocky Mountains)",
"WA6":"WA6 - Airmet (West Coast)",
"WA7":"WA7 - Airmet (Juneau, AK)",
"WA8":"WA8 - Airmet (Anchorage, AK)",
"WA9":"WA9 - Airmet (Fairbanks, AK)",
"WAR":"WAR - Space Environment Warning",
"WAT":"WAT - Space Environment Watch",
"WCN":"WCN - Weather Watch Clearance Notification",
"WCR":"WCR - Weekly Weather and Crop Report",
"WDA":"WDA - Weekly Data for Agriculture",
"WDU":"WDU - Warning Decision Update",
"WEK":"WEK - Routine Space Environment Product Issued Weekly",
"WOU":"WOU - Tornado/Severe Thunderstorm Watch",
"WS1":"WS1 - Sigmet (Northeast)",
"WS2":"WS2 - Sigmet (Southeast)",
"WS3":"WS3 - Sigmet (North Central)",
"WS4":"WS4 - Sigmet (South Central)",
"WS5":"WS5 - Sigmet (Rocky Mountains)",
"WS6":"WS6 - Sigmet (West Coast)",
"WST":"WST - Tropical Cyclone Sigmet",
"WSV":"WSV - Volcanic Activity Sigmet",
"WSW":"WSW - Winter Weather Warnings / Watches / Advisories",
"WWA":"WWA - Watch Status Report",
"WWP":"WWP - Severe Thunderstorm / Tornado Watch Probabilities",
"ZFP":"ZFP - Zone Forecast Product"
]
return x
}
// ======= Pushover Routines ============
def send_message(msgData) {
if (sendPushEnabled) {sendPush(msgData)}
if (sendSMSEnabled) {sendSms(mobilePhone, msgData)}
if (pushoverEnabled) {sendPushoverMessage(msgData)}
}
def sendPushoverMessage(msgData) {
if (settings.pushoverDevices != null) {
settings.pushoverDevices.each { // Use notification devices on Hubitat
it.deviceNotification(msgData)
}
}
}
def findMyPushoverDevices() {
Boolean validated = false
List pushoverDevices = []
Map params = [
uri: "https://api.pushover.net",
path: "/1/users/validate.json",
contentType: "application/json",
requestContentType: "application/json",
body: [token: pushoverToken.trim() as String, user: pushoverUser.trim() as String] as Map
]
try {
httpPostJson(params) { resp ->
if(resp?.status != 200) {
logErr "Received HTTP error ${resp.status}. Check your User and App Pushover keys!"
} else {
if(resp?.data) {
if(resp?.data?.status && resp?.data?.status == 1) validated = true
if(resp?.data?.devices) {
logDebug "Found (${resp?.data?.devices?.size()}) Pushover Devices..."
pushoverDevices = resp?.data?.devices
} else {
logErr "Device List is empty"
pushoverDevices ['No devices found, Check your User and App Pushover keys!']
}
} else { validated = false }
}
logDebug "findMyPushoverDevices | Validated: ${validated} | Resp | status: ${resp?.status} | data: ${resp?.data}"
}
} catch (Exception ex) {
if(ex instanceof groovyx.net.http.HttpResponseException && ex?.response) {
logErr "findMyPushoverDevices HttpResponseException | Status: (${ex?.response?.status}) | Data: ${ex?.response?.data}"
} else logErr "An invalid key was probably entered. PushOver Server Returned: ${ex}"
}
return pushoverDevices
}
def pushoverResponse(resp, data) {
try {
Map headers = resp?.getHeaders()
def limit = headers["X-Limit-App-Limit"]
def remain = headers["X-Limit-App-Remaining"]
def resetDt = headers["X-Limit-App-Reset"]
if(resp?.status == 200) {
logDebug "Message Received by Pushover Server ${(remain && limit) ? " | Monthly Messages Remaining (${remain} of ${limit})" : ""}"
} else if (resp?.status == 429) {
logWarn "Couldn't Send Pushover Notification... You have reached your (${limit}) notification limit for the month"
} else {
if(resp?.hasError()) {
logErr "pushoverResponse: status: ${resp.status} | errorMessage: ${resp?.getErrorMessage()}"
logErr "Received HTTP error ${resp?.status}. Check your keys!"
}
}
} catch (ex) {
if(ex instanceof groovyx.net.http.HttpResponseException && ex?.response) {
def rData = (ex?.response?.data && ex?.response?.data != "") ? " | Data: ${ex?.response?.data}" : ""
logErr "pushoverResponse() HttpResponseException | Status: (${ex?.response?.status})${rData}"
} else { logErr "pushoverResponse() Exception:", ex }
}
}