From aea133120a3aa76d4f832c940d84d51385377b01 Mon Sep 17 00:00:00 2001 From: Bob Florian Date: Thu, 20 Dec 2018 11:33:10 -0800 Subject: [PATCH 1/7] WWST-2091 Converted SmartWeather Station to TWC APIs (#3802) --- .../smartweather-station-tile.groovy | 627 +++++++++--------- 1 file changed, 317 insertions(+), 310 deletions(-) diff --git a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy index aa1a939132f..854314d829c 100644 --- a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy +++ b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy @@ -17,355 +17,362 @@ * Date: 2013-04-30 */ metadata { - definition (name: "SmartWeather Station Tile", namespace: "smartthings", author: "SmartThings") { - capability "Illuminance Measurement" - capability "Temperature Measurement" - capability "Relative Humidity Measurement" - capability "Ultraviolet Index" - capability "Sensor" - - attribute "localSunrise", "string" - attribute "localSunset", "string" - attribute "city", "string" - attribute "timeZoneOffset", "string" - attribute "weather", "string" - attribute "wind", "string" - attribute "weatherIcon", "string" - attribute "forecastIcon", "string" - attribute "feelsLike", "string" - attribute "percentPrecip", "string" - attribute "alert", "string" - attribute "alertKeys", "string" - attribute "sunriseDate", "string" - attribute "sunsetDate", "string" - attribute "lastUpdate", "string" - - command "refresh" - } - - preferences { - input "zipCode", "text", title: "Zip Code (optional)", required: false - } - - tiles { - valueTile("temperature", "device.temperature") { - state "default", label:'${currentValue}°', - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] - } - - valueTile("humidity", "device.humidity", decoration: "flat") { - state "default", label:'${currentValue}% humidity' - } - - standardTile("weatherIcon", "device.weatherIcon", decoration: "flat") { - state "chanceflurries", icon:"st.custom.wu1.chanceflurries", label: "" - state "chancerain", icon:"st.custom.wu1.chancerain", label: "" - state "chancesleet", icon:"st.custom.wu1.chancesleet", label: "" - state "chancesnow", icon:"st.custom.wu1.chancesnow", label: "" - state "chancetstorms", icon:"st.custom.wu1.chancetstorms", label: "" - state "clear", icon:"st.custom.wu1.clear", label: "" - state "cloudy", icon:"st.custom.wu1.cloudy", label: "" - state "flurries", icon:"st.custom.wu1.flurries", label: "" - state "fog", icon:"st.custom.wu1.fog", label: "" - state "hazy", icon:"st.custom.wu1.hazy", label: "" - state "mostlycloudy", icon:"st.custom.wu1.mostlycloudy", label: "" - state "mostlysunny", icon:"st.custom.wu1.mostlysunny", label: "" - state "partlycloudy", icon:"st.custom.wu1.partlycloudy", label: "" - state "partlysunny", icon:"st.custom.wu1.partlysunny", label: "" - state "rain", icon:"st.custom.wu1.rain", label: "" - state "sleet", icon:"st.custom.wu1.sleet", label: "" - state "snow", icon:"st.custom.wu1.snow", label: "" - state "sunny", icon:"st.custom.wu1.sunny", label: "" - state "tstorms", icon:"st.custom.wu1.tstorms", label: "" - state "cloudy", icon:"st.custom.wu1.cloudy", label: "" - state "partlycloudy", icon:"st.custom.wu1.partlycloudy", label: "" - state "nt_chanceflurries", icon:"st.custom.wu1.nt_chanceflurries", label: "" - state "nt_chancerain", icon:"st.custom.wu1.nt_chancerain", label: "" - state "nt_chancesleet", icon:"st.custom.wu1.nt_chancesleet", label: "" - state "nt_chancesnow", icon:"st.custom.wu1.nt_chancesnow", label: "" - state "nt_chancetstorms", icon:"st.custom.wu1.nt_chancetstorms", label: "" - state "nt_clear", icon:"st.custom.wu1.nt_clear", label: "" - state "nt_cloudy", icon:"st.custom.wu1.nt_cloudy", label: "" - state "nt_flurries", icon:"st.custom.wu1.nt_flurries", label: "" - state "nt_fog", icon:"st.custom.wu1.nt_fog", label: "" - state "nt_hazy", icon:"st.custom.wu1.nt_hazy", label: "" - state "nt_mostlycloudy", icon:"st.custom.wu1.nt_mostlycloudy", label: "" - state "nt_mostlysunny", icon:"st.custom.wu1.nt_mostlysunny", label: "" - state "nt_partlycloudy", icon:"st.custom.wu1.nt_partlycloudy", label: "" - state "nt_partlysunny", icon:"st.custom.wu1.nt_partlysunny", label: "" - state "nt_sleet", icon:"st.custom.wu1.nt_sleet", label: "" - state "nt_rain", icon:"st.custom.wu1.nt_rain", label: "" - state "nt_sleet", icon:"st.custom.wu1.nt_sleet", label: "" - state "nt_snow", icon:"st.custom.wu1.nt_snow", label: "" - state "nt_sunny", icon:"st.custom.wu1.nt_sunny", label: "" - state "nt_tstorms", icon:"st.custom.wu1.nt_tstorms", label: "" - state "nt_cloudy", icon:"st.custom.wu1.nt_cloudy", label: "" - state "nt_partlycloudy", icon:"st.custom.wu1.nt_partlycloudy", label: "" - } - - valueTile("feelsLike", "device.feelsLike", decoration: "flat") { - state "default", label:'feels like ${currentValue}°' - } - - valueTile("wind", "device.wind", decoration: "flat") { - state "default", label:'wind ${currentValue} mph' - } - - valueTile("weather", "device.weather", decoration: "flat") { - state "default", label:'${currentValue}' - } - - valueTile("city", "device.city", decoration: "flat") { - state "default", label:'${currentValue}' - } - - valueTile("percentPrecip", "device.percentPrecip", decoration: "flat") { - state "default", label:'${currentValue}% precip' - } - - valueTile("ultravioletIndex", "device.ultravioletIndex", decoration: "flat") { - state "default", label:'${currentValue} UV index' - } - - valueTile("alert", "device.alert", width: 2, height: 1, decoration: "flat") { - state "default", label:'${currentValue}' - } - - standardTile("refresh", "device.weather", decoration: "flat") { - state "default", label: "", action: "refresh", icon:"st.secondary.refresh" - } - - valueTile("rise", "device.localSunrise", decoration: "flat") { - state "default", label:'Sunrise ${currentValue}' - } - - valueTile("set", "device.localSunset", decoration: "flat") { - state "default", label:'Sunset ${currentValue}' - } - - valueTile("light", "device.illuminance", decoration: "flat") { - state "default", label:'${currentValue} lux' - } - - valueTile("lastUpdate", "device.lastUpdate", width: 3, height: 1, decoration: "flat") { - state "default", label:'Last update:\n${currentValue}' - } - - main(["temperature", "weatherIcon","feelsLike"]) - details(["temperature", "humidity", "weatherIcon", "feelsLike", "wind", "weather", "city", "percentPrecip", "ultravioletIndex", "alert", "refresh", "rise", "set", "light", "lastUpdate"])} + definition (name: "SmartWeather Station Tile", namespace: "smartthings", author: "SmartThings") { + capability "Illuminance Measurement" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Ultraviolet Index" + capability "Sensor" + capability "Refresh" + + attribute "localSunrise", "string" + attribute "localSunset", "string" + attribute "city", "string" + attribute "timeZoneOffset", "string" + attribute "weather", "string" + attribute "wind", "string" + attribute "windVector", "string" + attribute "weatherIcon", "string" + attribute "forecastIcon", "string" + attribute "feelsLike", "string" + attribute "percentPrecip", "string" + attribute "alert", "string" + attribute "alertKeys", "string" + attribute "sunriseDate", "string" + attribute "sunsetDate", "string" + attribute "lastUpdate", "string" + attribute "uvDescription", "string" + attribute "forecastToday", "string" + attribute "forecastTonight", "string" + attribute "forecastTomorrow", "string" + } + + preferences { + input "zipCode", "text", title: "Zip Code (optional)", required: false + } + + tiles { + valueTile("temperature", "device.temperature") { + state "default", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + valueTile("humidity", "device.humidity", decoration: "flat") { + state "default", label:'${currentValue}% humidity' + } + + standardTile("weatherIcon", "device.weatherIcon", decoration: "flat") { + state "00", icon:"https://smartthings-twc-icons.s3.amazonaws.com/00.png", label: "" + state "01", icon:"https://smartthings-twc-icons.s3.amazonaws.com/01.png", label: "" + state "02", icon:"https://smartthings-twc-icons.s3.amazonaws.com/02.png", label: "" + state "03", icon:"https://smartthings-twc-icons.s3.amazonaws.com/03.png", label: "" + state "04", icon:"https://smartthings-twc-icons.s3.amazonaws.com/04.png", label: "" + state "05", icon:"https://smartthings-twc-icons.s3.amazonaws.com/05.png", label: "" + state "06", icon:"https://smartthings-twc-icons.s3.amazonaws.com/06.png", label: "" + state "07", icon:"https://smartthings-twc-icons.s3.amazonaws.com/07.png", label: "" + state "08", icon:"https://smartthings-twc-icons.s3.amazonaws.com/08.png", label: "" + state "09", icon:"https://smartthings-twc-icons.s3.amazonaws.com/09.png", label: "" + state "10", icon:"https://smartthings-twc-icons.s3.amazonaws.com/10.png", label: "" + state "11", icon:"https://smartthings-twc-icons.s3.amazonaws.com/11.png", label: "" + state "12", icon:"https://smartthings-twc-icons.s3.amazonaws.com/12.png", label: "" + state "13", icon:"https://smartthings-twc-icons.s3.amazonaws.com/13.png", label: "" + state "14", icon:"https://smartthings-twc-icons.s3.amazonaws.com/14.png", label: "" + state "15", icon:"https://smartthings-twc-icons.s3.amazonaws.com/15.png", label: "" + state "16", icon:"https://smartthings-twc-icons.s3.amazonaws.com/16.png", label: "" + state "17", icon:"https://smartthings-twc-icons.s3.amazonaws.com/17.png", label: "" + state "18", icon:"https://smartthings-twc-icons.s3.amazonaws.com/18.png", label: "" + state "19", icon:"https://smartthings-twc-icons.s3.amazonaws.com/19.png", label: "" + state "20", icon:"https://smartthings-twc-icons.s3.amazonaws.com/20.png", label: "" + state "21", icon:"https://smartthings-twc-icons.s3.amazonaws.com/21.png", label: "" + state "22", icon:"https://smartthings-twc-icons.s3.amazonaws.com/22.png", label: "" + state "23", icon:"https://smartthings-twc-icons.s3.amazonaws.com/23.png", label: "" + state "24", icon:"https://smartthings-twc-icons.s3.amazonaws.com/24.png", label: "" + state "25", icon:"https://smartthings-twc-icons.s3.amazonaws.com/25.png", label: "" + state "26", icon:"https://smartthings-twc-icons.s3.amazonaws.com/26.png", label: "" + state "27", icon:"https://smartthings-twc-icons.s3.amazonaws.com/27.png", label: "" + state "28", icon:"https://smartthings-twc-icons.s3.amazonaws.com/28.png", label: "" + state "29", icon:"https://smartthings-twc-icons.s3.amazonaws.com/29.png", label: "" + state "30", icon:"https://smartthings-twc-icons.s3.amazonaws.com/30.png", label: "" + state "31", icon:"https://smartthings-twc-icons.s3.amazonaws.com/31.png", label: "" + state "32", icon:"https://smartthings-twc-icons.s3.amazonaws.com/32.png", label: "" + state "33", icon:"https://smartthings-twc-icons.s3.amazonaws.com/33.png", label: "" + state "34", icon:"https://smartthings-twc-icons.s3.amazonaws.com/34.png", label: "" + state "35", icon:"https://smartthings-twc-icons.s3.amazonaws.com/35.png", label: "" + state "36", icon:"https://smartthings-twc-icons.s3.amazonaws.com/36.png", label: "" + state "37", icon:"https://smartthings-twc-icons.s3.amazonaws.com/37.png", label: "" + state "38", icon:"https://smartthings-twc-icons.s3.amazonaws.com/38.png", label: "" + state "39", icon:"https://smartthings-twc-icons.s3.amazonaws.com/39.png", label: "" + state "40", icon:"https://smartthings-twc-icons.s3.amazonaws.com/40.png", label: "" + state "41", icon:"https://smartthings-twc-icons.s3.amazonaws.com/41.png", label: "" + state "42", icon:"https://smartthings-twc-icons.s3.amazonaws.com/42.png", label: "" + state "43", icon:"https://smartthings-twc-icons.s3.amazonaws.com/43.png", label: "" + state "44", icon:"https://smartthings-twc-icons.s3.amazonaws.com/44.png", label: "" + state "45", icon:"https://smartthings-twc-icons.s3.amazonaws.com/45.png", label: "" + state "46", icon:"https://smartthings-twc-icons.s3.amazonaws.com/46.png", label: "" + state "47", icon:"https://smartthings-twc-icons.s3.amazonaws.com/47.png", label: "" + state "na", icon:"https://smartthings-twc-icons.s3.amazonaws.com/na.png", label: "" + } + + valueTile("feelsLike", "device.feelsLike", decoration: "flat") { + state "default", label:'feels like ${currentValue}°' + } + + valueTile("wind", "device.windVector", decoration: "flat") { + state "default", label:'wind ${currentValue}' + } + + valueTile("weather", "device.weather", decoration: "flat") { + state "default", label:'${currentValue}' + } + + valueTile("city", "device.city", decoration: "flat") { + state "default", label:'${currentValue}' + } + + valueTile("percentPrecip", "device.percentPrecip", decoration: "flat") { + state "default", label:'${currentValue}% precip' + } + + valueTile("ultravioletIndex", "device.uvDescription", decoration: "flat") { + state "default", label:'UV ${currentValue}' + } + + valueTile("alert", "device.alert", width: 2, height: 1, decoration: "flat") { + state "default", label:'${currentValue}' + } + + standardTile("refresh", "device.weather", decoration: "flat") { + state "default", label: "", action: "refresh", icon:"st.secondary.refresh" + } + + valueTile("rise", "device.localSunrise", decoration: "flat") { + state "default", label:'Sunrise ${currentValue}' + } + + valueTile("set", "device.localSunset", decoration: "flat") { + state "default", label:'Sunset ${currentValue}' + } + + valueTile("light", "device.illuminance", decoration: "flat") { + state "default", label:'${currentValue} lux' + } + + valueTile("city", "device.forecastToday", decoration: "flat") { + state "default", label:'Today: ${currentValue}' + } + + valueTile("city", "device.forecastTonight", decoration: "flat") { + state "default", label:'Tonight: ${currentValue}' + } + + valueTile("city", "device.forecastTomorrow", decoration: "flat") { + state "default", label:'Tomorrow: ${currentValue}' + } + + valueTile("lastUpdate", "device.lastUpdate", width: 3, height: 1, decoration: "flat") { + state "default", label:'Last update:\n${currentValue}' + } + + main(["temperature", "weatherIcon","feelsLike"]) + details(["temperature", "humidity", "weatherIcon", "feelsLike", "wind", + "weather", "city", "percentPrecip", "ultravioletIndex", "alert", + "refresh", "rise", "set", "light", "lastUpdate"])} } // parse events into attributes def parse(String description) { - log.debug "Parsing '${description}'" + log.debug "Parsing '${description}'" } def installed() { - poll() - runEvery30Minutes(poll) + poll() + runEvery30Minutes(poll) } def uninstalled() { - unschedule() + unschedule() } // handle commands def poll() { - log.debug "WUSTATION: Executing 'poll', location: ${location.name}" - - // Last update time stamp - def timeStamp = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) - sendEvent(name: "lastUpdate", value: timeStamp) - - // Current conditions - def obs = get("conditions")?.current_observation - if (obs) { - def weatherIcon = obs.icon_url.split("/")[-1].split("\\.")[0] - - if(getTemperatureScale() == "C") { - send(name: "temperature", value: Math.round(obs.temp_c), unit: "C") - send(name: "feelsLike", value: Math.round(obs.feelslike_c as Double), unit: "C") - } else { - send(name: "temperature", value: Math.round(obs.temp_f), unit: "F") - send(name: "feelsLike", value: Math.round(obs.feelslike_f as Double), unit: "F") - } - - send(name: "humidity", value: obs.relative_humidity[0..-2] as Integer, unit: "%") - send(name: "weather", value: obs.weather) - send(name: "weatherIcon", value: weatherIcon, displayed: false) - send(name: "wind", value: Math.round(obs.wind_mph) as String, unit: "MPH") // as String because of bug in determining state change of 0 numbers - - if (obs.local_tz_offset != device.currentValue("timeZoneOffset")) { - send(name: "timeZoneOffset", value: obs.local_tz_offset, isStateChange: true) - } - - def cityValue = "${obs.display_location.city}, ${obs.display_location.state}" - if (cityValue != device.currentValue("city")) { - send(name: "city", value: cityValue, isStateChange: true) - } - - send(name: "ultravioletIndex", value: Math.round(obs.UV as Double)) - - // Sunrise / Sunset - def a = get("astronomy")?.moon_phase - def today = localDate("GMT${obs.local_tz_offset}") - def ltf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm") - ltf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}")) - def utf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - utf.setTimeZone(TimeZone.getTimeZone("GMT")) - - def sunriseDate = ltf.parse("${today} ${a.sunrise.hour}:${a.sunrise.minute}") - def sunsetDate = ltf.parse("${today} ${a.sunset.hour}:${a.sunset.minute}") + log.info "WUSTATION: Executing 'poll', location: ${location.name}" + + // Last update time stamp + def timeStamp = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + sendEvent(name: "lastUpdate", value: timeStamp) + + // Current conditions + def tempUnits = getTemperatureScale() + def obs = getTwcConditions(zipCode) + if (obs) { + // TODO def weatherIcon = obs.icon_url.split("/")[-1].split("\\.")[0] + + send(name: "temperature", value: obs.temperature, unit: tempUnits) + send(name: "feelsLike", value: obs.temperatureFeelsLike, unit: tempUnits) + + send(name: "humidity", value: obs.relativeHumidity, unit: "%") + send(name: "weather", value: obs.wxPhraseShort) + send(name: "weatherIcon", value: obs.iconCode as String, displayed: false) + send(name: "wind", value: obs.windSpeed as String, unit: tempUnits == "C" ? "KPH" : "MPH") // as String because of bug in determining state change of 0 numbers + send(name: "windVector", value: "${obs.windDirectionCardinal}@${obs.windSpeed}", unit: tempUnits == "C" ? "KPH" : "MPH") + log.trace "Getting location info" + def loc = getTwcLocation(zipCode).location + def cityValue = "${loc.city}, ${loc.adminDistrictCode} ${loc.countryCode}" + if (cityValue != device.currentValue("city")) { + send(name: "city", value: cityValue, isStateChange: true) + } + + send(name: "ultravioletIndex", value: obs.uvIndex) + send(name: "uvDescription", value: obs.uvDescription) + + def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + + def sunriseDate = dtf.parse(obs.sunriseTimeLocal) + log.info "'${obs.sunriseTimeLocal}'" + + def sunsetDate = dtf.parse(obs.sunsetTimeLocal) def tf = new java.text.SimpleDateFormat("h:mm a") - tf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}")) + tf.setTimeZone(TimeZone.getTimeZone(loc.ianaTimeZone)) + def localSunrise = "${tf.format(sunriseDate)}" def localSunset = "${tf.format(sunsetDate)}" send(name: "localSunrise", value: localSunrise, descriptionText: "Sunrise today is at $localSunrise") send(name: "localSunset", value: localSunset, descriptionText: "Sunset today at is $localSunset") - send(name: "illuminance", value: estimateLux(sunriseDate, sunsetDate, weatherIcon)) - - // Forecast - def f = get("forecast") - def f1= f?.forecast?.simpleforecast?.forecastday - if (f1) { - def icon = f1[0].icon_url.split("/")[-1].split("\\.")[0] - def value = f1[0].pop as String // as String because of bug in determining state change of 0 numbers - send(name: "percentPrecip", value: value, unit: "%") - send(name: "forecastIcon", value: icon, displayed: false) - } - else { - log.warn "Forecast not found" - } - - // Alerts - def alerts = get("alerts")?.alerts - def newKeys = alerts?.collect{it.type + it.date_epoch} ?: [] - log.debug "WUSTATION: newKeys = $newKeys" - log.trace device.currentState("alertKeys") - def oldKeys = device.currentState("alertKeys")?.jsonValue - log.debug "WUSTATION: oldKeys = $oldKeys" - - def noneString = "no current weather alerts" - if (!newKeys && oldKeys == null) { - send(name: "alertKeys", value: newKeys.encodeAsJSON(), displayed: false) - send(name: "alert", value: noneString, descriptionText: "${device.displayName} has no current weather alerts", isStateChange: true) - } - else if (newKeys != oldKeys) { - if (oldKeys == null) { - oldKeys = [] - } - send(name: "alertKeys", value: newKeys.encodeAsJSON(), displayed: false) - - def newAlerts = false - alerts.each {alert -> - if (!oldKeys.contains(alert.type + alert.date_epoch)) { - def msg = "${alert.description} from ${alert.date} until ${alert.expires}" - send(name: "alert", value: pad(alert.description), descriptionText: msg, isStateChange: true) - newAlerts = true - } - } - - if (!newAlerts && device.currentValue("alert") != noneString) { - send(name: "alert", value: noneString, descriptionText: "${device.displayName} has no current weather alerts", isStateChange: true) - } - } - } - else { - log.warn "No response from Weather Underground API" - } + send(name: "illuminance", value: estimateLux(obs, sunriseDate, sunsetDate)) + + // Forecast + def f = getTwcForecast(zipCode) + if (f) { + def icon = f.daypart[0].iconCode[0] ?: f.daypart[0].iconCode[1] + def value = f.daypart[0].precipChance[0] ?: f.daypart[0].precipChance[1] + def narrative = f.daypart[0].narrative + send(name: "percentPrecip", value: value, unit: "%") + send(name: "forecastIcon", value: icon, displayed: false) + send(name: "forecastToday", value: narrative[0]) + send(name: "forecastTonight", value: narrative[1]) + send(name: "forecastTomorrow", value: narrative[2]) + } + else { + log.warn "Forecast not found" + } + + // Alerts + def alerts = getTwcAlerts(zipCode) + if (alerts) { + alerts.each {alert -> + def msg = alert.headlineText + if (alert.effectiveTimeLocal && !msg.contains(" from ")) { + msg += " from ${parseAlertTime(alert.effectiveTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.effectiveTimeLocalTimeZone))}" + } + if (alert.expireTimeLocal && !msg.contains(" until ")) { + msg += " until ${parseAlertTime(alert.expireTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.expireTimeLocalTimeZone))}" + } + send(name: "alert", value: msg, descriptionText: msg) + } + } + else { + send(name: "alert", value: "No current alerts", descriptionText: msg) + } + } + else { + log.warn "No response from TWC API" + } +} + +def parseAlertTime(s) { + def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + def s2 = s.replaceAll(/([0-9][0-9]):([0-9][0-9])$/,'$1$2') + dtf.parse(s2) } def refresh() { - poll() + poll() } def configure() { - poll() + poll() } private pad(String s, size = 25) { - def n = (size - s.size()) / 2 - if (n > 0) { - def sb = "" - n.times {sb += " "} - sb += s - n.times {sb += " "} - return sb - } - else { - return s - } + def n = (size - s.size()) / 2 + if (n > 0) { + def sb = "" + n.times {sb += " "} + sb += s + n.times {sb += " "} + return sb + } + else { + return s + } } private get(feature) { - getWeatherFeature(feature, zipCode) + getWeatherFeature(feature, zipCode) } private localDate(timeZone) { - def df = new java.text.SimpleDateFormat("yyyy-MM-dd") - df.setTimeZone(TimeZone.getTimeZone(timeZone)) - df.format(new Date()) + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + df.setTimeZone(TimeZone.getTimeZone(timeZone)) + df.format(new Date()) } private send(map) { - log.debug "WUSTATION: event: $map" - sendEvent(map) + log.debug "WUSTATION: event: $map" + sendEvent(map) } -private estimateLux(sunriseDate, sunsetDate, weatherIcon) { - def lux = 0 - def now = new Date().time - if (now > sunriseDate.time && now < sunsetDate.time) { - //day - switch(weatherIcon) { - case 'tstorms': - lux = 200 - break - case ['cloudy', 'fog', 'rain', 'sleet', 'snow', 'flurries', - 'chanceflurries', 'chancerain', 'chancesleet', - 'chancesnow', 'chancetstorms']: - lux = 1000 - break - case 'mostlycloudy': - lux = 2500 - break - case ['partlysunny', 'partlycloudy', 'hazy']: - lux = 7500 - break - default: - //sunny, clear - lux = 10000 - } - - //adjust for dusk/dawn - def afterSunrise = now - sunriseDate.time - def beforeSunset = sunsetDate.time - now - def oneHour = 1000 * 60 * 60 - - if(afterSunrise < oneHour) { - //dawn - lux = (long)(lux * (afterSunrise/oneHour)) - } else if (beforeSunset < oneHour) { - //dusk - lux = (long)(lux * (beforeSunset/oneHour)) - } - } - else { - //night - always set to 10 for now - //could do calculations for dusk/dawn too - lux = 10 - } - - lux -} +private estimateLux(obs, sunriseDate, sunsetDate) { + def lux = 0 + if (obs.dayOrNight == 'N') { + lux = 10 + } + else { + //day + switch(obs.iconCode) { + case '04': + lux = 200 + break + case ['05', '06', '07', '08', '09', '10', + '11', '12', '13','14', '15','17','18','19','20', + '21','22','23','24','25','26']: + lux = 1000 + break + case ['27', '28']: + lux = 2500 + break + case ['29', '30']: + lux = 7500 + break + default: + //sunny, clear + lux = 10000 + } + + //adjust for dusk/dawn + def now = new Date().time + def afterSunrise = now - sunriseDate.time + def beforeSunset = sunsetDate.time - now + def oneHour = 1000 * 60 * 60 + + if(afterSunrise < oneHour) { + //dawn + lux = (long)(lux * (afterSunrise/oneHour)) + } else if (beforeSunset < oneHour) { + //dusk + lux = (long)(lux * (beforeSunset/oneHour)) + } + } + lux +} \ No newline at end of file From 3f59659886902b8010c7960dc336a807927a96e9 Mon Sep 17 00:00:00 2001 From: Bob Florian Date: Thu, 20 Dec 2018 17:51:20 -0800 Subject: [PATCH 2/7] WWST-2091 SmartWeather Station tile updates (#3815) --- .../smartweather-station-tile.groovy | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy index 854314d829c..81efe704295 100644 --- a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy +++ b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy @@ -51,8 +51,8 @@ metadata { input "zipCode", "text", title: "Zip Code (optional)", required: false } - tiles { - valueTile("temperature", "device.temperature") { + tiles(scale: 2) { + valueTile("temperature", "device.temperature", height: 2, width: 2) { state "default", label:'${currentValue}°', backgroundColors:[ [value: 31, color: "#153591"], @@ -65,11 +65,11 @@ metadata { ] } - valueTile("humidity", "device.humidity", decoration: "flat") { - state "default", label:'${currentValue}% humidity' + valueTile("feelsLike", "device.feelsLike", decoration: "flat", height: 1, width: 2) { + state "default", label:'Feels like ${currentValue}°' } - standardTile("weatherIcon", "device.weatherIcon", decoration: "flat") { + standardTile("weatherIcon", "device.weatherIcon", decoration: "flat", height: 2, width: 2) { state "00", icon:"https://smartthings-twc-icons.s3.amazonaws.com/00.png", label: "" state "01", icon:"https://smartthings-twc-icons.s3.amazonaws.com/01.png", label: "" state "02", icon:"https://smartthings-twc-icons.s3.amazonaws.com/02.png", label: "" @@ -121,70 +121,73 @@ metadata { state "na", icon:"https://smartthings-twc-icons.s3.amazonaws.com/na.png", label: "" } - valueTile("feelsLike", "device.feelsLike", decoration: "flat") { - state "default", label:'feels like ${currentValue}°' + valueTile("humidity", "device.humidity", decoration: "flat", height: 1, width: 2) { + state "default", label:'${currentValue}% humidity' } - valueTile("wind", "device.windVector", decoration: "flat") { - state "default", label:'wind ${currentValue}' + valueTile("wind", "device.windVector", decoration: "flat", height: 1, width: 2) { + state "default", label:'Wind\n${currentValue}' } - valueTile("weather", "device.weather", decoration: "flat") { + valueTile("weather", "device.weather", decoration: "flat", height: 1, width: 2) { state "default", label:'${currentValue}' } - valueTile("city", "device.city", decoration: "flat") { + valueTile("city", "device.city", decoration: "flat", height: 1, width: 2) { state "default", label:'${currentValue}' } - valueTile("percentPrecip", "device.percentPrecip", decoration: "flat") { + valueTile("percentPrecip", "device.percentPrecip", decoration: "flat", height: 1, width: 2) { state "default", label:'${currentValue}% precip' } - valueTile("ultravioletIndex", "device.uvDescription", decoration: "flat") { + valueTile("ultravioletIndex", "device.uvDescription", decoration: "flat", height: 1, width: 2) { state "default", label:'UV ${currentValue}' } - valueTile("alert", "device.alert", width: 2, height: 1, decoration: "flat") { + valueTile("alert", "device.alert", decoration: "flat", height: 2, width: 6) { state "default", label:'${currentValue}' } - standardTile("refresh", "device.weather", decoration: "flat") { + standardTile("refresh", "device.weather", decoration: "flat", height: 1, width: 2) { state "default", label: "", action: "refresh", icon:"st.secondary.refresh" } - valueTile("rise", "device.localSunrise", decoration: "flat") { + valueTile("rise", "device.localSunrise", decoration: "flat", height: 1, width: 2) { state "default", label:'Sunrise ${currentValue}' } - valueTile("set", "device.localSunset", decoration: "flat") { + valueTile("set", "device.localSunset", decoration: "flat", height: 1, width: 2) { state "default", label:'Sunset ${currentValue}' } - valueTile("light", "device.illuminance", decoration: "flat") { + valueTile("light", "device.illuminance", decoration: "flat", height: 1, width: 2) { state "default", label:'${currentValue} lux' } - valueTile("city", "device.forecastToday", decoration: "flat") { - state "default", label:'Today: ${currentValue}' + valueTile("today", "device.forecastToday", decoration: "flat", height: 1, width: 3) { + state "default", label:'Today:\n${currentValue}' } - valueTile("city", "device.forecastTonight", decoration: "flat") { - state "default", label:'Tonight: ${currentValue}' + valueTile("tonight", "device.forecastTonight", decoration: "flat", height: 1, width: 3) { + state "default", label:'Tonight:\n${currentValue}' } - valueTile("city", "device.forecastTomorrow", decoration: "flat") { - state "default", label:'Tomorrow: ${currentValue}' + valueTile("tomorrow", "device.forecastTomorrow", decoration: "flat", height: 1, width: 3) { + state "default", label:'Tomorrow:\n${currentValue}' } - valueTile("lastUpdate", "device.lastUpdate", width: 3, height: 1, decoration: "flat") { + valueTile("lastUpdate", "device.lastUpdate", decoration: "flat", height: 1, width: 3) { state "default", label:'Last update:\n${currentValue}' } main(["temperature", "weatherIcon","feelsLike"]) - details(["temperature", "humidity", "weatherIcon", "feelsLike", "wind", - "weather", "city", "percentPrecip", "ultravioletIndex", "alert", - "refresh", "rise", "set", "light", "lastUpdate"])} + details(["temperature", "feelsLike", "weatherIcon", "humidity", "wind", + "weather", "city", "percentPrecip", "ultravioletIndex", "light", + "rise", "set", + "refresh", + "today", "tonight", "tomorrow", "lastUpdate", + "alert"])} } // parse events into attributes @@ -211,6 +214,7 @@ def poll() { // Current conditions def tempUnits = getTemperatureScale() + def windUnits = tempUnits == "C" ? "KPH" : "MPH" def obs = getTwcConditions(zipCode) if (obs) { // TODO def weatherIcon = obs.icon_url.split("/")[-1].split("\\.")[0] @@ -221,8 +225,8 @@ def poll() { send(name: "humidity", value: obs.relativeHumidity, unit: "%") send(name: "weather", value: obs.wxPhraseShort) send(name: "weatherIcon", value: obs.iconCode as String, displayed: false) - send(name: "wind", value: obs.windSpeed as String, unit: tempUnits == "C" ? "KPH" : "MPH") // as String because of bug in determining state change of 0 numbers - send(name: "windVector", value: "${obs.windDirectionCardinal}@${obs.windSpeed}", unit: tempUnits == "C" ? "KPH" : "MPH") + send(name: "wind", value: obs.windSpeed as String, unit: windUnits) // as String because of bug in determining state change of 0 numbers + send(name: "windVector", value: "${obs.windDirectionCardinal} ${obs.windSpeed} ${windUnits}") log.trace "Getting location info" def loc = getTwcLocation(zipCode).location def cityValue = "${loc.city}, ${loc.adminDistrictCode} ${loc.countryCode}" From eaa5d8e88d8c37a08c59639e34f3967111619582 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 20 Dec 2018 15:10:49 -0500 Subject: [PATCH 3/7] WWST-2088 - WWST-2089 - WWST-2090 - New weather API (#3813) --- .../smart-windows.src/smart-windows.groovy | 240 +++++++++--------- .../ready-for-rain.src/ready-for-rain.groovy | 48 +--- .../severe-weather-alert.groovy | 149 +++++------ 3 files changed, 197 insertions(+), 240 deletions(-) diff --git a/smartapps/egid/smart-windows.src/smart-windows.groovy b/smartapps/egid/smart-windows.src/smart-windows.groovy index b30833532e4..0d5438fcb62 100644 --- a/smartapps/egid/smart-windows.src/smart-windows.groovy +++ b/smartapps/egid/smart-windows.src/smart-windows.groovy @@ -1,16 +1,16 @@ /** * Smart Windows - * Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). + * Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). * * Copyright 2014 Eric Gideon * - * Based in part on the "When it's going to rain" SmartApp by the SmartThings team, + * Based in part on the "When it's going to rain" SmartApp by the SmartThings team, * primarily the message throttling code. * * 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 + * 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 @@ -18,148 +18,136 @@ * */ definition( - name: "Smart Windows", - namespace: "egid", - author: "Eric Gideon", - description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your location will be used instead.", - iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png", - iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png", - pausable: true + name: "Smart Windows", + namespace: "egid", + author: "Eric Gideon", + description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your location will be used instead.", + iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png", + iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png", + pausable: true ) - preferences { if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { - section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } - } - - section( "Set the temperature range for your comfort zone..." ) { - input "minTemp", "number", title: "Minimum temperature" - input "maxTemp", "number", title: "Maximum temperature" - } - section( "Select windows to check..." ) { - input "sensors", "capability.contactSensor", multiple: true - } - section( "Select temperature devices to monitor..." ) { - input "inTemp", "capability.temperatureMeasurement", title: "Indoor" - input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false - } - - if (location.channelName != 'samsungtv') { - section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } - } - - section( "Notifications" ) { - input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false - input "retryPeriod", "number", title: "Minutes between notifications:" - } + section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } + } + + section( "Set the temperature range for your comfort zone..." ) { + input "minTemp", "number", title: "Minimum temperature" + input "maxTemp", "number", title: "Maximum temperature" + } + section( "Select windows to check..." ) { + input "sensors", "capability.contactSensor", multiple: true + } + section( "Select temperature devices to monitor..." ) { + input "inTemp", "capability.temperatureMeasurement", title: "Indoor" + input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false + } + + if (location.channelName != 'samsungtv') { + section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } + } + + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + input "retryPeriod", "number", title: "Minutes between notifications:" + } } - def installed() { - log.debug "Installed: $settings" - subscribe( inTemp, "temperature", temperatureHandler ) + log.debug "Installed: $settings" + subscribe( inTemp, "temperature", temperatureHandler ) } def updated() { - log.debug "Updated: $settings" - unsubscribe() - subscribe( inTemp, "temperature", temperatureHandler ) + log.debug "Updated: $settings" + unsubscribe() + subscribe( inTemp, "temperature", temperatureHandler ) } - def temperatureHandler(evt) { - def currentOutTemp = null - if ( outTemp ) { - currentOutTemp = outTemp.latestValue("temperature") - } else { - log.debug "No external temperature device set. Checking WUnderground...." - currentOutTemp = weatherCheck() - } - - def currentInTemp = evt.doubleValue - def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' } - - log.trace "Temp event: $evt" - log.info "In: $currentInTemp; Out: $currentOutTemp" - - // Don't spam notifications - // *TODO* use state.foo from Severe Weather Alert to do this better - if (!retryPeriod) { - def retryPeriod = 30 - } - def timeAgo = new Date(now() - (1000 * 60 * retryPeriod).toLong()) - def recentEvents = inTemp.eventsSince(timeAgo) - log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriod minutes" - - // Figure out if we should notify - if ( currentInTemp > minTemp && currentInTemp < maxTemp ) { - log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp." - log.debug "No notifications sent." - } else if ( currentInTemp > maxTemp ) { - // Too warm. Can we do anything? - - def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1 - - if ( !alreadyNotified ) { - if ( currentOutTemp < maxTemp && !openWindows ) { - send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else if ( currentOutTemp > maxTemp && openWindows ) { - send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else { - log.debug "No notifications sent. Everything is in the right place." - } - } else { - log.debug "Already notified! No notifications sent." - } - } else if ( currentInTemp < minTemp ) { - // Too cold! Is it warmer outside? - - def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1 - - if ( !alreadyNotified ) { - if ( currentOutTemp > minTemp && !openWindows ) { - send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else if ( currentOutTemp < minTemp && openWindows ) { - send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else { - log.debug "No notifications sent. Everything is in the right place." - } - } else { - log.debug "Already notified! No notifications sent." - } - } + def currentOutTemp = null + if ( outTemp ) { + currentOutTemp = outTemp.latestValue("temperature") + } else { + log.debug "No external temperature device set. Checking The Weather Company..." + currentOutTemp = weatherCheck() + } + + def currentInTemp = evt.doubleValue + def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' } + + log.trace "Temp event: $evt" + log.info "In: $currentInTemp; Out: $currentOutTemp" + + // Don't spam notifications + // *TODO* use state.foo from Severe Weather Alert to do this better + if (!retryPeriod) { + def retryPeriod = 30 + } + def timeAgo = new Date(now() - (1000 * 60 * retryPeriod).toLong()) + def recentEvents = inTemp.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriod minutes" + + // Figure out if we should notify + if ( currentInTemp > minTemp && currentInTemp < maxTemp ) { + log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp." + log.debug "No notifications sent." + } else if ( currentInTemp > maxTemp ) { + // Too warm. Can we do anything? + + def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1 + + if ( !alreadyNotified ) { + if ( currentOutTemp < maxTemp && !openWindows ) { + send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp > maxTemp && openWindows ) { + send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } else if ( currentInTemp < minTemp ) { + // Too cold! Is it warmer outside? + def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1 + if ( !alreadyNotified ) { + if ( currentOutTemp > minTemp && !openWindows ) { + send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp < minTemp && openWindows ) { + send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } } def weatherCheck() { - def json - if (location.channelName != 'samsungtv') - json = getWeatherFeature("conditions", zipCode) - else - json = getWeatherFeature("conditions") - def currentTemp = json?.current_observation?.temp_f - - if ( currentTemp ) { - log.trace "Temp: $currentTemp (WeatherUnderground)" - return currentTemp - } else { - log.warn "Did not get a temp: $json" - return false - } + def obs = getTwcConditions(zipCode) + def currentTemp = obs.temperature + if ( currentTemp ) { + log.trace "Temp: $currentTemp (The Weather Company)" + return currentTemp + } else { + log.warn "Did not get a temp: $obs" + return false + } } private send(msg) { - if ( sendPushMessage != "No" ) { - log.debug( "sending push message" ) - sendPush( msg ) + if ( sendPushMessage != "No" ) { + log.debug( "sending push message" ) + sendPush( msg ) sendEvent(linkText:app.label, descriptionText:msg, eventType:"SOLUTION_EVENT", displayed: true, name:"summary") - } - - if ( phone1 ) { - log.debug( "sending text message" ) - sendSms( phone1, msg ) - } - - log.info msg + } + if ( phone1 ) { + log.debug( "sending text message" ) + sendSms( phone1, msg ) + } + log.info msg } diff --git a/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy b/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy index f290624f428..b1355c41b4f 100644 --- a/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy +++ b/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy @@ -78,13 +78,8 @@ def scheduleCheck(evt) { // Only need to poll if we haven't checked in a while - and if something is left open. if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) { log.info("Something's open - let's check the weather.") - def response - if (location.channelName != 'samsungtv') - response = getWeatherFeature("forecast", zipCode) - else - response = getWeatherFeature("forecast") + def response = getTwcForecast(zipCode) def weather = isStormy(response) - if(weather) { send("${open.join(', ')} ${plural} open and ${weather} coming.") } @@ -123,34 +118,19 @@ private send(msg) { } } -private isStormy(json) { - def types = ["rain", "snow", "showers", "sprinkles", "precipitation"] - def forecast = json?.forecast?.txt_forecast?.forecastday?.first() - def result = false - - if(forecast) { - def text = forecast?.fcttext?.toLowerCase() - - log.debug(text) - - if(text) { - for (int i = 0; i < types.size() && !result; i++) { - if(text.contains(types[i])) { - result = types[i] +private isStormy(forecast) { + def result = false + if(forecast) { + def text = forecast.daypart?.precipType[0][0] + if(text) { + log.info("We got ${text}") + result = text + } else { + log.info("Got forecast, nothing coming soon.") } - } - } - - else { - log.warn("Got forecast, couldn't parse.") + } else { + log.warn("Did not get a forecast: ${forecast}") } - } - - else { - log.warn("Did not get a forecast: ${json}") - } - - state.lastCheck = ["time": now(), "result": result] - - return result + state.lastCheck = ["time": now(), "result": result] + return result } diff --git a/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy b/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy index 5fa7bebd1d6..a26a656b982 100644 --- a/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy +++ b/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy @@ -27,118 +27,107 @@ definition( ) preferences { - page name: "mainPage", install: true, uninstall: true + page name: "mainPage", install: true, uninstall: true } def mainPage() { - dynamicPage(name: "mainPage") { - if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { - section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } - } + dynamicPage(name: "mainPage") { + if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { + section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } + } - if (location.channelName != 'samsungtv') { - section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } - } + if (location.channelName != 'samsungtv') { + section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } + } - if (location.contactBookEnabled || phone1 || phone2 || phone3) { - section("In addition to push notifications, send text alerts to...") { - input("recipients", "contact", title: "Send notifications to") { - input "phone1", "phone", title: "Phone Number 1", required: false - input "phone2", "phone", title: "Phone Number 2", required: false - input "phone3", "phone", title: "Phone Number 3", required: false - } - } - } + if (location.contactBookEnabled || phone1 || phone2 || phone3) { + section("In addition to push notifications, send text alerts to...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone Number 1", required: false + input "phone2", "phone", title: "Phone Number 2", required: false + input "phone3", "phone", title: "Phone Number 3", required: false + } + } + } - section([mobileOnly:true]) { - label title: "Assign a name", required: false - mode title: "Set for specific mode(s)" - } - } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } } def installed() { - log.debug "Installed with settings: ${settings}" - scheduleJob() + log.debug "Installed with settings: ${settings}" + scheduleJob() } def updated() { - log.debug "Updated with settings: ${settings}" + log.debug "Updated with settings: ${settings}" unschedule() - scheduleJob() + scheduleJob() } def scheduleJob() { - def sec = Math.round(Math.floor(Math.random() * 60)) - def min = Math.round(Math.floor(Math.random() * 60)) - def cron = "$sec $min * * * ?" - schedule(cron, "checkForSevereWeather") + def sec = Math.round(Math.floor(Math.random() * 60)) + def min = Math.round(Math.floor(Math.random() * 60)) + def cron = "$sec $min * * * ?" + schedule(cron, "checkForSevereWeather") } def checkForSevereWeather() { - def alerts - if(locationIsDefined()) { - if(zipcodeIsValid()) { - alerts = getWeatherFeature("alerts", zipCode)?.alerts - } else { - log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode" - alerts = getWeatherFeature("alerts")?.alerts - } - } else { - log.warn "Severe Weather Alert: Location is not defined" - } - - def newKeys = alerts?.collect{it.type + it.date_epoch} ?: [] - log.debug "Severe Weather Alert: newKeys: $newKeys" - - def oldKeys = state.alertKeys ?: [] - log.debug "Severe Weather Alert: oldKeys: $oldKeys" - - if (newKeys != oldKeys) { - - state.alertKeys = newKeys + def alerts + if(locationIsDefined()) { + if(!(zipcodeIsValid())) { + log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode" + } + alerts = getTwcAlerts(zipCode) + } else { + log.warn "Severe Weather Alert: Location is not defined" + } - alerts.each {alert -> - if (!oldKeys.contains(alert.type + alert.date_epoch) && descriptionFilter(alert.description)) { - def msg = "Weather Alert! ${alert.description} from ${alert.date} until ${alert.expires}" - send(msg) - } - } - } + if (alerts) { + alerts.each {alert -> + def msg = alert.headlineText + if (alert.effectiveTimeLocal && !msg.contains(" from ")) { + msg += " from ${parseAlertTime(alert.effectiveTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.effectiveTimeLocalTimeZone))}" + } + if (alert.expireTimeLocal && !msg.contains(" until ")) { + msg += " until ${parseAlertTime(alert.expireTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.expireTimeLocalTimeZone))}" + } + send(msg) + } + } else { + log.info "No current alerts" + } } def descriptionFilter(String description) { - def filterList = ["special", "statement", "test"] - def passesFilter = true - filterList.each() { word -> - if(description.toLowerCase().contains(word)) { passesFilter = false } - } - passesFilter + def filterList = ["special", "statement", "test"] + def passesFilter = true + filterList.each() { word -> + if(description.toLowerCase().contains(word)) { passesFilter = false } + } + passesFilter } def locationIsDefined() { - zipcodeIsValid() || location.zipCode || ( location.latitude && location.longitude ) + zipcodeIsValid() || location.zipCode || ( location.latitude && location.longitude ) } def zipcodeIsValid() { - zipcode && zipcode.isNumber() && zipcode.size() == 5 + zipcode && zipcode.isNumber() && zipcode.size() == 5 } private send(message) { - if (location.contactBookEnabled) { - log.debug("sending notifications to: ${recipients?.size()}") - sendNotificationToContacts(msg, recipients) + sendPush message + if (settings.phone1) { + sendSms phone1, message } - else { - sendPush message - if (settings.phone1) { - sendSms phone1, message - } - if (settings.phone2) { - sendSms phone2, message - } - if (settings.phone3) { - sendSms phone3, message - } + if (settings.phone2) { + sendSms phone2, message + } + if (settings.phone3) { + sendSms phone3, message } } From 6d3b5dc0ce29552a450fd0a9316dfaaacbfb00f3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Fri, 21 Dec 2018 16:14:20 -0500 Subject: [PATCH 4/7] WWST-2092 - WWST-2093 - WWST-2095 - Weather API update (#3816) --- .../stelpro-ki-thermostat.groovy | 686 ++++---- .../stelpro-ki-zigbee-thermostat.groovy | 8 +- .../stelpro-maestro-thermostat.groovy | 10 +- .../talking-alarm-clock.groovy | 1550 ++++++++--------- .../shabbat-and-holiday-modes.groovy | 154 +- 5 files changed, 1196 insertions(+), 1212 deletions(-) diff --git a/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy b/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy index 91694c9e8ce..853b15f8e59 100644 --- a/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy +++ b/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy @@ -4,7 +4,7 @@ * 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 + * 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 @@ -19,214 +19,214 @@ import physicalgraph.zwave.commands.* metadata { - definition (name: "Stelpro Ki Thermostat", namespace: "stelpro", author: "Stelpro", ocfDeviceType: "oic.d.thermostat") { - capability "Actuator" - capability "Temperature Measurement" - capability "Temperature Alarm" - capability "Thermostat" - capability "Thermostat Mode" - capability "Thermostat Operating State" - capability "Thermostat Heating Setpoint" - capability "Configuration" - capability "Sensor" - capability "Refresh" - capability "Health Check" - - // Right now this can disrupt device health if the device is currently offline -- it would be erroneously marked online. - //attribute "outsideTemp", "number" - - command "setOutdoorTemperature" - command "quickSetOutTemp" // Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp - command "increaseHeatSetpoint" - command "decreaseHeatSetpoint" - command "eco" // Command does not exist in "Thermostat Mode" - command "updateWeather" - - fingerprint deviceId: "0x0806", inClusters: "0x5E,0x86,0x72,0x40,0x43,0x31,0x85,0x59,0x5A,0x73,0x20,0x42", mfr: "0239", prod: "0001", model: "0001", deviceJoinName: "Stelpro Ki Thermostat" - } - - // simulator metadata - simulator { } - - preferences { - input("heatdetails", "enum", title: "Do you want a detailed operating state notification?", options: ["No", "Yes"], defaultValue: "No", required: true, displayDuringSetup: true) - input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "[Do not use space](Blank = No Forecast)") - } - - tiles(scale : 2) { - multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4, canChangeIcon: true) { - tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { - attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal") - } - tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { - attributeState("VALUE_UP", action: "increaseHeatSetpoint") - attributeState("VALUE_DOWN", action: "decreaseHeatSetpoint") - } - tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { - attributeState("idle", backgroundColor:"#44b621") - attributeState("heating", backgroundColor:"#ffa81e") - } - tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { - attributeState("heat", label:'${name}') - attributeState("eco", label:'${name}') - } - tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { - attributeState("heatingSetpoint", label:'${currentValue}°') - } - } - standardTile("mode", "device.thermostatMode", width: 2, height: 2) { - state "heat", label:'${name}', action:"eco", nextState:"eco", icon:"st.Home.home29" - state "eco", label:'${name}', action:"heat", nextState:"heat", icon:"st.Outdoor.outdoor3" - } - valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2) { - state "heatingSetpoint", label:'Setpoint ${currentValue}°', backgroundColors:[ - // Celsius - [value: 0, color: "#153591"], - [value: 7, color: "#1e9cbb"], - [value: 15, color: "#90d2a7"], - [value: 23, color: "#44b621"], - [value: 28, color: "#f1d801"], - [value: 35, color: "#d04e00"], - [value: 37, color: "#bc2323"], - // Fahrenheit - [value: 40, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] - } - standardTile("temperatureAlarm", "device.temperatureAlarm", decoration: "flat", width: 2, height: 2) { - state "default", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" - state "cleared", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" - state "freeze", label: 'Freeze', icon: "st.alarm.temperature.freeze", backgroundColor: "#bc2323" - state "heat", label: 'Overheat', icon: "st.alarm.temperature.overheat", backgroundColor: "#bc2323" - } - standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - main ("thermostatMulti") - details(["thermostatMulti", "mode", "heatingSetpoint", "temperatureAlarm", "refresh"]) - } + definition (name: "Stelpro Ki Thermostat", namespace: "stelpro", author: "Stelpro", ocfDeviceType: "oic.d.thermostat") { + capability "Actuator" + capability "Temperature Measurement" + capability "Temperature Alarm" + capability "Thermostat" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Thermostat Heating Setpoint" + capability "Configuration" + capability "Sensor" + capability "Refresh" + capability "Health Check" + + // Right now this can disrupt device health if the device is currently offline -- it would be erroneously marked online. + //attribute "outsideTemp", "number" + + command "setOutdoorTemperature" + command "quickSetOutTemp" // Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp + command "increaseHeatSetpoint" + command "decreaseHeatSetpoint" + command "eco" // Command does not exist in "Thermostat Mode" + command "updateWeather" + + fingerprint deviceId: "0x0806", inClusters: "0x5E,0x86,0x72,0x40,0x43,0x31,0x85,0x59,0x5A,0x73,0x20,0x42", mfr: "0239", prod: "0001", model: "0001", deviceJoinName: "Stelpro Ki Thermostat" + } + + // simulator metadata + simulator { } + + preferences { + input("heatdetails", "enum", title: "Do you want a detailed operating state notification?", options: ["No", "Yes"], defaultValue: "No", required: true, displayDuringSetup: true) + input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "[Do not use space](Blank = No Forecast)") + } + + tiles(scale : 2) { + multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "increaseHeatSetpoint") + attributeState("VALUE_DOWN", action: "decreaseHeatSetpoint") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("heat", label:'${name}') + attributeState("eco", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("heatingSetpoint", label:'${currentValue}°') + } + } + standardTile("mode", "device.thermostatMode", width: 2, height: 2) { + state "heat", label:'${name}', action:"eco", nextState:"eco", icon:"st.Home.home29" + state "eco", label:'${name}', action:"heat", nextState:"heat", icon:"st.Outdoor.outdoor3" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2) { + state "heatingSetpoint", label:'Setpoint ${currentValue}°', backgroundColors:[ + // Celsius + [value: 0, color: "#153591"], + [value: 7, color: "#1e9cbb"], + [value: 15, color: "#90d2a7"], + [value: 23, color: "#44b621"], + [value: 28, color: "#f1d801"], + [value: 35, color: "#d04e00"], + [value: 37, color: "#bc2323"], + // Fahrenheit + [value: 40, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + standardTile("temperatureAlarm", "device.temperatureAlarm", decoration: "flat", width: 2, height: 2) { + state "default", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "cleared", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "freeze", label: 'Freeze', icon: "st.alarm.temperature.freeze", backgroundColor: "#bc2323" + state "heat", label: 'Overheat', icon: "st.alarm.temperature.overheat", backgroundColor: "#bc2323" + } + standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main ("thermostatMulti") + details(["thermostatMulti", "mode", "heatingSetpoint", "temperatureAlarm", "refresh"]) + } } def getSupportedThermostatModes() { - ["heat", "eco"] + ["heat", "eco"] } def getMinSetpointIndex() { - 0 + 0 } def getMaxSetpointIndex() { - 1 + 1 } def getThermostatSetpointRange() { - (getTemperatureScale() == "C") ? [5, 30] : [41, 86] + (getTemperatureScale() == "C") ? [5, 30] : [41, 86] } def getHeatingSetpointRange() { - thermostatSetpointRange + thermostatSetpointRange } def getSetpointStep() { - (getTemperatureScale() == "C") ? 0.5 : 1.0 + (getTemperatureScale() == "C") ? 0.5 : 1.0 } def setupHealthCheck() { - // Device-Watch simply pings if no device events received for 32min(checkInterval) - sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } def configureSupportedRanges() { - sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) - sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) - sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) } def installed() { - sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) - setupHealthCheck() + setupHealthCheck() - configureSupportedRanges() + configureSupportedRanges() } def updated() { - setupHealthCheck() + setupHealthCheck() - configureSupportedRanges() + configureSupportedRanges() - unschedule(scheduledUpdateWeather) - if (settings.zipcode) { - runEvery1Hour(scheduledUpdateWeather) - scheduledUpdateWeather() - } + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + runEvery1Hour(scheduledUpdateWeather) + scheduledUpdateWeather() + } } def parse(String description) { - // If the user installed with an old DTH version, update so that the new mobile client will work - if (!device.currentValue("supportedThermostatModes")) { - configureSupportedRanges() - } - - if (description == "updated") { - return null - } - - // Class, version - def map = createEvent(zwaveEvent(zwave.parse(description, [0x40:2, 0x43:2, 0x31:3, 0x42:1, 0x20:1, 0x85: 2]))) - if (!map) { - return null - } - - def result = [map] - // This logic is to appease the (now deprecated but still sort-of used) consolidated - // Thermostat capability gods. - if (map.isStateChange && map.name == "heatingSetpoint") { - result << createEvent([ - name: "thermostatSetpoint", - value: map.value, - unit: map.unit, - data: [thermostatSetpointRange: thermostatSetpointRange] - ]) - } - - log.debug "Parse returned $result" - result + // If the user installed with an old DTH version, update so that the new mobile client will work + if (!device.currentValue("supportedThermostatModes")) { + configureSupportedRanges() + } + + if (description == "updated") { + return null + } + + // Class, version + def map = createEvent(zwaveEvent(zwave.parse(description, [0x40:2, 0x43:2, 0x31:3, 0x42:1, 0x20:1, 0x85: 2]))) + if (!map) { + return null + } + + def result = [map] + // This logic is to appease the (now deprecated but still sort-of used) consolidated + // Thermostat capability gods. + if (map.isStateChange && map.name == "heatingSetpoint") { + result << createEvent([ + name: "thermostatSetpoint", + value: map.value, + unit: map.unit, + data: [thermostatSetpointRange: thermostatSetpointRange] + ]) + } + + log.debug "Parse returned $result" + result } def updateWeather() { - log.debug "updating weather" - def weather - // If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast. - if (settings.zipcode) { - log.debug "ZipCode: ${settings.zipcode}" - weather = getWeatherFeature("conditions", settings.zipcode) - - // Check if the variable is populated, otherwise return. - if (!weather) { - log.debug("Something went wrong, no data found.") - return false - } - - def locationScale = getTemperatureScale() - def tempToSend = (locationScale == "C") ? weather.current_observation.temp_c : weather.current_observation.temp_f - log.debug("Outdoor Temperature: ${tempToSend} ${locationScale}") - // Right now this can disrupt device health if the device is - // currently offline -- it would be erroneously marked online. - //sendEvent(name: 'outsideTemp', value: tempToSend) - setOutdoorTemperature(tempToSend) - } + log.debug "updating weather" + def weather + // If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast. + if (settings.zipcode) { + log.debug "ZipCode: ${settings.zipcode}" + weather = getTwcConditions(settings.zipcode) + + // Check if the variable is populated, otherwise return. + if (!weather) { + log.debug("Something went wrong, no data found.") + return false + } + + def locationScale = getTemperatureScale() + def tempToSend = weather.temperature + log.debug("Outdoor Temperature: ${tempToSend} ${locationScale}") + // Right now this can disrupt device health if the device is + // currently offline -- it would be erroneously marked online. + //sendEvent(name: 'outsideTemp', value: tempToSend) + setOutdoorTemperature(tempToSend) + } } def scheduledUpdateWeather() { - def actions = updateWeather() + def actions = updateWeather() - if (actions) { - sendHubCommand(actions) - } + if (actions) { + sendHubCommand(actions) + } } // Command Implementations @@ -235,281 +235,281 @@ def scheduledUpdateWeather() { * PING is used by Device-Watch in attempt to reach the Device **/ def ping() { - log.debug "ping()" - zwave.sensorMultilevelV3.sensorMultilevelGet().format() + log.debug "ping()" + zwave.sensorMultilevelV3.sensorMultilevelGet().format() } def poll() { - log.debug "poll()" - delayBetween([ - updateWeather(), - zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format(), - zwave.thermostatModeV2.thermostatModeGet().format(), - zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format(), - zwave.sensorMultilevelV3.sensorMultilevelGet().format() // current temperature - ], 100) + log.debug "poll()" + delayBetween([ + updateWeather(), + zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format(), + zwave.thermostatModeV2.thermostatModeGet().format(), + zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format(), + zwave.sensorMultilevelV3.sensorMultilevelGet().format() // current temperature + ], 100) } // Event Generation def zwaveEvent(thermostatsetpointv2.ThermostatSetpointReport cmd) { - def cmdScale = cmd.scale == 1 ? "F" : "C" - def temp; - float tempfloat; - def map = [:] + def cmdScale = cmd.scale == 1 ? "F" : "C" + def temp; + float tempfloat; + def map = [:] - if (cmd.scaledValue >= 327 || - cmd.setpointType != thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1) { - return [:] - } - temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) - tempfloat = (Math.round(temp.toFloat() * 2)) / 2 - map.value = tempfloat + if (cmd.scaledValue >= 327 || + cmd.setpointType != thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1) { + return [:] + } + temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) + tempfloat = (Math.round(temp.toFloat() * 2)) / 2 + map.value = tempfloat - map.unit = getTemperatureScale() - map.displayed = false - map.name = "heatingSetpoint" - map.data = [heatingSetpointRange: heatingSetpointRange] + map.unit = getTemperatureScale() + map.displayed = false + map.name = "heatingSetpoint" + map.data = [heatingSetpointRange: heatingSetpointRange] - // So we can respond with same format - state.size = cmd.size - state.scale = cmd.scale - state.precision = cmd.precision + // So we can respond with same format + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision - map + map } def zwaveEvent(sensormultilevelv3.SensorMultilevelReport cmd) { - def temp - float tempfloat - def format - def map = [:] - - if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1) { - map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) - map.unit = getTemperatureScale() - map.name = "temperature" - - temp = map.value - // The specific values checked below represent ambient temperature alarm indicators - if (temp == "32765") { // 0x7FFD - map.name = "temperatureAlarm" - map.value = "freeze" - map.unit = "" - } else if (temp == "32767") { // 0x7FFF - map.name = "temperatureAlarm" - map.value = "heat" - map.unit = "" - } else if (temp == "-32768"){ // 0x8000 - map.name = "temperatureAlarm" - map.value = "cleared" - map.unit = "" - } else { - tempfloat = (Math.round(temp.toFloat() * 2)) / 2 - map.value = tempfloat - } - } else if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_RELATIVE_HUMIDITY_VERSION_2) { - map.value = cmd.scaledSensorValue - map.unit = "%" - map.name = "humidity" - } - - map + def temp + float tempfloat + def format + def map = [:] + + if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1) { + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + + temp = map.value + // The specific values checked below represent ambient temperature alarm indicators + if (temp == "32765") { // 0x7FFD + map.name = "temperatureAlarm" + map.value = "freeze" + map.unit = "" + } else if (temp == "32767") { // 0x7FFF + map.name = "temperatureAlarm" + map.value = "heat" + map.unit = "" + } else if (temp == "-32768"){ // 0x8000 + map.name = "temperatureAlarm" + map.value = "cleared" + map.unit = "" + } else { + tempfloat = (Math.round(temp.toFloat() * 2)) / 2 + map.value = tempfloat + } + } else if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_RELATIVE_HUMIDITY_VERSION_2) { + map.value = cmd.scaledSensorValue + map.unit = "%" + map.name = "humidity" + } + + map } def zwaveEvent(thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) { - def map = [:] - def operatingState = zwaveOperatingStateToString(cmd.operatingState) + def map = [:] + def operatingState = zwaveOperatingStateToString(cmd.operatingState) - if (operatingState) { - map.name = "thermostatOperatingState" - map.value = operatingState + if (operatingState) { + map.name = "thermostatOperatingState" + map.value = operatingState - if (settings.heatdetails == "No") { - map.displayed = false - } - } else { - log.trace "${device.displayName} sent invalid operating state $value" - } + if (settings.heatdetails == "No") { + map.displayed = false + } + } else { + log.trace "${device.displayName} sent invalid operating state $value" + } - map + map } def zwaveEvent(thermostatmodev2.ThermostatModeReport cmd) { - def map = [:] - def mode = zwaveModeToString(cmd.mode) + def map = [:] + def mode = zwaveModeToString(cmd.mode) - if (mode) { - map.name = "thermostatMode" - map.value = mode - map.data = [supportedThermostatModes: supportedThermostatModes] - } else { - log.trace "${device.displayName} sent invalid mode $value" - } + if (mode) { + map.name = "thermostatMode" + map.value = mode + map.data = [supportedThermostatModes: supportedThermostatModes] + } else { + log.trace "${device.displayName} sent invalid mode $value" + } - map + map } def zwaveEvent(associationv2.AssociationReport cmd) { - delayBetween([ - zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:0).format(), - zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(), - poll() - ], 2300) + delayBetween([ + zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:0).format(), + zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(), + poll() + ], 2300) } def zwaveEvent(thermostatmodev2.ThermostatModeSupportedReport cmd) { - log.debug "Zwave event received: $cmd" + log.debug "Zwave event received: $cmd" } def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.warn "Unexpected zwave command $cmd" + log.warn "Unexpected zwave command $cmd" } def refresh() { - poll() + poll() } def configure() { - unschedule(scheduledUpdateWeather) - if (settings.zipcode) { - runEvery1Hour(scheduledUpdateWeather) - } - poll() + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + runEvery1Hour(scheduledUpdateWeather) + } + poll() } def setHeatingSetpoint(preciseDegrees) { - float minSetpoint = thermostatSetpointRange[minSetpointIndex] - float maxSetpoint = thermostatSetpointRange[maxSetpointIndex] - - if (preciseDegrees >= minSetpoint && preciseDegrees <= maxSetpoint) { - def degrees = new BigDecimal(preciseDegrees).setScale(1, BigDecimal.ROUND_HALF_UP) - log.trace "setHeatingSetpoint($degrees)" - def deviceScale = state.scale ?: 1 - def deviceScaleString = deviceScale == 2 ? "C" : "F" - def locationScale = getTemperatureScale() - def p = (state.precision == null) ? 1 : state.precision - def setpointType = thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1 - - def convertedDegrees = degrees - if (locationScale == "C" && deviceScaleString == "F") { - convertedDegrees = celsiusToFahrenheit(degrees) - } else if (locationScale == "F" && deviceScaleString == "C") { - convertedDegrees = fahrenheitToCelsius(degrees) - } - - delayBetween([ - zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: setpointType, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: setpointType).format() - ], 1000) - } else { - log.debug "heatingSetpoint $preciseDegrees out of range! (supported: $minSetpoint - $maxSetpoint ${getTemperatureScale()})" - } + float minSetpoint = thermostatSetpointRange[minSetpointIndex] + float maxSetpoint = thermostatSetpointRange[maxSetpointIndex] + + if (preciseDegrees >= minSetpoint && preciseDegrees <= maxSetpoint) { + def degrees = new BigDecimal(preciseDegrees).setScale(1, BigDecimal.ROUND_HALF_UP) + log.trace "setHeatingSetpoint($degrees)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + def setpointType = thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1 + + def convertedDegrees = degrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } + + delayBetween([ + zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: setpointType, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), + zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: setpointType).format() + ], 1000) + } else { + log.debug "heatingSetpoint $preciseDegrees out of range! (supported: $minSetpoint - $maxSetpoint ${getTemperatureScale()})" + } } // Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp def quickSetOutTemp(outsideTemp) { - setOutdoorTemperature(outsideTemp) + setOutdoorTemperature(outsideTemp) } def setOutdoorTemperature(outsideTemp) { - def degrees = outsideTemp as Double - def locationScale = getTemperatureScale() - def p = (state.precision == null) ? 1 : state.precision - def deviceScale = (locationScale == "C") ? 0 : 1 - def sensorType = sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1 + def degrees = outsideTemp as Double + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + def deviceScale = (locationScale == "C") ? 0 : 1 + def sensorType = sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1 - log.debug "setOutdoorTemperature: ${degrees}" - zwave.sensorMultilevelV3.sensorMultilevelReport(sensorType: sensorType, scale: deviceScale, precision: p, scaledSensorValue: degrees).format() + log.debug "setOutdoorTemperature: ${degrees}" + zwave.sensorMultilevelV3.sensorMultilevelReport(sensorType: sensorType, scale: deviceScale, precision: p, scaledSensorValue: degrees).format() } def increaseHeatSetpoint() { - float currentSetpoint = device.currentValue("heatingSetpoint") + float currentSetpoint = device.currentValue("heatingSetpoint") - currentSetpoint = currentSetpoint + setpointStep - setHeatingSetpoint(currentSetpoint) + currentSetpoint = currentSetpoint + setpointStep + setHeatingSetpoint(currentSetpoint) } def decreaseHeatSetpoint() { - float currentSetpoint = device.currentValue("heatingSetpoint") + float currentSetpoint = device.currentValue("heatingSetpoint") - currentSetpoint = currentSetpoint - setpointStep - setHeatingSetpoint(currentSetpoint) + currentSetpoint = currentSetpoint - setpointStep + setHeatingSetpoint(currentSetpoint) } def getModeNumericMap() {[ - "heat": thermostatmodev2.ThermostatModeReport.MODE_HEAT, - "eco": thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT + "heat": thermostatmodev2.ThermostatModeReport.MODE_HEAT, + "eco": thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT ]} def zwaveModeToString(mode) { - if (thermostatmodev2.ThermostatModeReport.MODE_HEAT == mode) { - return "heat" - } else if (thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT == mode) { - return "eco" - } - return null + if (thermostatmodev2.ThermostatModeReport.MODE_HEAT == mode) { + return "heat" + } else if (thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT == mode) { + return "eco" + } + return null } def zwaveOperatingStateToString(state) { - if (thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE == state) { - return "idle" - } else if (thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING == state) { - return "heating" - } - return null + if (thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE == state) { + return "idle" + } else if (thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING == state) { + return "heating" + } + return null } def setCoolingSetpoint(coolingSetpoint) { - log.trace "${device.displayName} does not support cool setpoint" + log.trace "${device.displayName} does not support cool setpoint" } def heat() { - log.trace "heat mode applied" - setThermostatMode("heat") + log.trace "heat mode applied" + setThermostatMode("heat") } def eco() { - log.trace "eco mode applied" - setThermostatMode("eco") + log.trace "eco mode applied" + setThermostatMode("eco") } def off() { - log.trace "${device.displayName} does not support off mode" + log.trace "${device.displayName} does not support off mode" } def auto() { - log.trace "${device.displayName} does not support auto mode" + log.trace "${device.displayName} does not support auto mode" } def emergencyHeat() { - log.trace "${device.displayName} does not support emergency heat mode" + log.trace "${device.displayName} does not support emergency heat mode" } def cool() { - log.trace "${device.displayName} does not support cool mode" + log.trace "${device.displayName} does not support cool mode" } def setThermostatMode(value) { - if (supportedThermostatModes.contains(value)) { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeNumericMap[value]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], 1000) - } else { - log.trace "${device.displayName} does not support $value mode" - } + if (supportedThermostatModes.contains(value)) { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: modeNumericMap[value]).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], 1000) + } else { + log.trace "${device.displayName} does not support $value mode" + } } def fanOn() { - log.trace "${device.displayName} does not support fan on" + log.trace "${device.displayName} does not support fan on" } def fanAuto() { - log.trace "${device.displayName} does not support fan auto" + log.trace "${device.displayName} does not support fan auto" } def fanCirculate() { - log.trace "${device.displayName} does not support fan circulate" + log.trace "${device.displayName} does not support fan circulate" } def setThermostatFanMode() { - log.trace "${device.displayName} does not support fan mode" + log.trace "${device.displayName} does not support fan mode" } diff --git a/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy b/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy index f1f7c19af86..e29d2bf128a 100644 --- a/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy +++ b/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy @@ -337,16 +337,16 @@ def updateWeather() { // If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast. if (settings.zipcode) { log.debug "ZipCode: ${settings.zipcode}" - weather = getWeatherFeature("conditions", settings.zipcode) + weather = getTwcConditions(settings.zipcode) // Check if the variable is populated, otherwise return. if (!weather) { log.debug("Something went wrong, no data found.") return false } - + def locationScale = getTemperatureScale() - def tempToSend = (locationScale == "C") ? weather.current_observation.temp_c : weather.current_observation.temp_f + def tempToSend = weather.temperature log.debug("Outdoor Temperature: ${tempToSend} ${locationScale}") // Right now this can disrupt device health if the device is // currently offline -- it would be erroneously marked online. @@ -569,5 +569,3 @@ def auto() { def fanAuto() { log.debug "${device.displayName} does not support fan auto" } - - diff --git a/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy b/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy index b18253d4705..0996843a6a2 100644 --- a/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy +++ b/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy @@ -60,7 +60,7 @@ metadata { input("away_setpoint", "enum", title: "Away Setpoint", options: ["5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "10.5", "11", "11.5", "12", "12.5", "13", "13.5", "14", "14.5", "15", "5.5", "15.5", "16", "16.5", "17", "17.5", "18", "18.5", "19", "19.5", "20", "20.5", "21", "21.5", "22", "22.5", "23", "24", "24.5", "25", "25.5", "26", "26.5", "27", "27.5", "28", "28.5", "29", "29.5", "30"], defaultValue: "17", required: true) input("vacation_setpoint", "enum", title: "Vacation Setpoint", options: ["5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "10.5", "11", "11.5", "12", "12.5", "13", "13.5", "14", "14.5", "15", "5.5", "15.5", "16", "16.5", "17", "17.5", "18", "18.5", "19", "19.5", "20", "20.5", "21", "21.5", "22", "22.5", "23", "24", "24.5", "25", "25.5", "26", "26.5", "27", "27.5", "28", "28.5", "29", "29.5", "30"], defaultValue: "13", required: true) input("standby_setpoint", "enum", title: "Standby Setpoint", options: ["5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "10.5", "11", "11.5", "12", "12.5", "13", "13.5", "14", "14.5", "15", "5.5", "15.5", "16", "16.5", "17", "17.5", "18", "18.5", "19", "19.5", "20", "20.5", "21", "21.5", "22", "22.5", "23", "24", "24.5", "25", "25.5", "26", "26.5", "27", "27.5", "28", "28.5", "29", "29.5", "30"], defaultValue: "5", required: true) - */ + */ } tiles(scale : 2) { @@ -326,16 +326,16 @@ def updateWeather() { // If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast. if (settings.zipcode) { log.debug "ZipCode: ${settings.zipcode}" - weather = getWeatherFeature("conditions", settings.zipcode) + weather = getTwcConditions(settings.zipcode) // Check if the variable is populated, otherwise return. if (!weather) { log.debug("Something went wrong, no data found.") return false } - + def locationScale = getTemperatureScale() - def tempToSend = (locationScale == "C") ? weather.current_observation.temp_c : weather.current_observation.temp_f + def tempToSend = weather.temperature log.debug("Outdoor Temperature: ${tempToSend} ${locationScale}") // Right now this can disrupt device health if the device is // currently offline -- it would be erroneously marked online. @@ -534,5 +534,3 @@ def auto() { def fanAuto() { log.debug "${device.displayName} does not support fan auto" } - - diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy index a773d9f740d..3113bb172c8 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy +++ b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy @@ -7,7 +7,7 @@ * Version - 1.3.0 5/29/15 - Further code optimizations and addition of alarm summary action * Version - 1.3.1 5/30/15 - Fixed one small code syntax issue in Scenario D * Version - 1.4.0 6/7/15 - Revised About screen, enhanced the weather forecast voice summary, added a mode change option with alarm, and added secondary alarm options - * Version - 1.4.1 6/9/15 - Changed the mode change speech to make it clear when the mode change is taking place + * Version - 1.4.1 6/9/15 - Changed the mode change speech to make it clear when the mode change is taking place * Version - 1.4.2 6/10/15 - To prevent accidental triggering of summary, put in a mode switch restriction * Version - 1.4.3 6/12/15 - Syntax issues and minor GUI fixes * Version - 1.4.4 6/15/15 - Fixed a bug with Phrase change at alarm time @@ -25,7 +25,7 @@ * for the specific language governing permissions and limitations under the License. * */ - + definition( name: "Talking Alarm Clock", namespace: "MichaelStruck", @@ -39,48 +39,48 @@ definition( ) preferences { - page name:"pageMain" - page name:"pageSetupScenarioA" - page name:"pageSetupScenarioB" - page name:"pageSetupScenarioC" - page name:"pageSetupScenarioD" - page name:"pageWeatherSettingsA" //technically, these 4 pages should not be dynamic, but are here to work around a crash on the Andriod app - page name:"pageWeatherSettingsB" - page name:"pageWeatherSettingsC" - page name:"pageWeatherSettingsD" + page name:"pageMain" + page name:"pageSetupScenarioA" + page name:"pageSetupScenarioB" + page name:"pageSetupScenarioC" + page name:"pageSetupScenarioD" + page name:"pageWeatherSettingsA" //technically, these 4 pages should not be dynamic, but are here to work around a crash on the Andriod app + page name:"pageWeatherSettingsB" + page name:"pageWeatherSettingsC" + page name:"pageWeatherSettingsD" } // Show setup page def pageMain() { - dynamicPage(name: "pageMain", install: true, uninstall: true) { + dynamicPage(name: "pageMain", install: true, uninstall: true) { section ("Alarms") { href "pageSetupScenarioA", title: getTitle(ScenarioNameA, 1), description: getDesc(A_timeStart, A_sonos, A_day, A_mode), state: greyOut(ScenarioNameA, A_sonos, A_timeStart, A_alarmOn, A_alarmType) if (ScenarioNameA && A_sonos && A_timeStart && A_alarmType){ - input "A_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "true", submitOnChange:true + input "A_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "true", submitOnChange:true } } section { href "pageSetupScenarioB", title: getTitle(ScenarioNameB, 2), description: getDesc(B_timeStart, B_sonos, B_day, B_mode), state: greyOut(ScenarioNameB, B_sonos, B_timeStart, B_alarmOn, B_alarmType) if (ScenarioNameB && B_sonos && B_timeStart && B_alarmType){ - input "B_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true - } + input "B_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + } } section { href "pageSetupScenarioC", title: getTitle(ScenarioNameC, 3), description: getDesc(C_timeStart, C_sonos, C_day, C_mode), state: greyOut(ScenarioNameC, C_sonos, C_timeStart, C_alarmOn, C_alarmType) if (ScenarioNameC && C_sonos && C_timeStart && C_alarmType){ - input "C_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true - } + input "C_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + } } section { href "pageSetupScenarioD", title: getTitle(ScenarioNameD, 4), description: getDesc(D_timeStart, D_sonos, D_day, D_mode), state: greyOut(ScenarioNameD, D_sonos, D_timeStart, D_alarmOn, D_alarmType) if (ScenarioNameD && D_sonos && D_timeStart && D_alarmType){ - input "D_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + input "D_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true } } section([title:"Options", mobileOnly:true]) { input "alarmSummary", "bool", title: "Enable Alarm Summary", defaultValue: "false", submitOnChange:true if (alarmSummary) { - href "pageAlarmSummary", title: "Alarm Summary Settings", description: "Tap to configure alarm summary settings", state: "complete" + href "pageAlarmSummary", title: "Alarm Summary Settings", description: "Tap to configure alarm summary settings", state: "complete" } input "zipCode", "text", title: "Zip Code", required: false label title:"Assign a name", required: false @@ -90,385 +90,385 @@ def pageMain() { } page(name: "pageAlarmSummary", title: "Alarm Summary Settings") { - section { - input "summarySonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: false + section { + input "summarySonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: false input "summaryVolume", "number", title: "Set the summary volume", description: "0-100%", required: false input "summaryDisabled", "bool", title: "Include disabled or unconfigured alarms in summary", defaultValue: "false" input "summaryMode", "mode", title: "Speak summary only during the following modes...", multiple: true, required: false - } + } } //Show "pageSetupScenarioA" page def pageSetupScenarioA() { dynamicPage(name: "pageSetupScenarioA") { - section("Alarm settings") { - input "ScenarioNameA", "text", title: "Scenario Name", multiple: false, required: true - input "A_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameA", "text", title: "Scenario Name", multiple: false, required: true + input "A_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "A_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "A_timeStart", "time", title: "Time to trigger alarm", required: true - input "A_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "A_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "A_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "A_timeStart", "time", title: "Time to trigger alarm", required: true + input "A_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "A_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "A_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (A_alarmType != "3") { - if (A_alarmType == "1"){ - input "A_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (A_alarmType == "1"){ + input "A_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (A_alarmType == "2"){ - input "A_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "A_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (A_alarmType == "1"){ - section ("Alarm sound options"){ - input "A_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "A_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } + section ("Alarm sound options"){ + input "A_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "A_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } if (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1")) { - section ("Voice greeting options") { - input "A_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false - href "pageWeatherSettingsA", title: "Weather Reporting Settings", description: getWeatherDesc(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp), state: greyOut1(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp) - } - } - if (A_alarmType == "3" || (A_alarmType == "1" && A_secondAlarm =="2") || (A_alarmType == "2" && A_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "A_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(A_sonos, 1) - } - } + section ("Voice greeting options") { + input "A_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsA", title: "Weather Reporting Settings", description: getWeatherDesc(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp), state: greyOut1(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp) + } + } + if (A_alarmType == "3" || (A_alarmType == "1" && A_secondAlarm =="2") || (A_alarmType == "2" && A_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "A_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(A_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "A_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersA", title: "Dimmer Settings", description: dimmerDesc(A_dimmers), state: greyOutOption(A_dimmers), submitOnChange:true + input "A_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersA", title: "Dimmer Settings", description: dimmerDesc(A_dimmers), state: greyOutOption(A_dimmers), submitOnChange:true href "pageThermostatsA", title: "Thermostat Settings", description: thermostatDesc(A_thermostats, A_temperatureH, A_temperatureC), state: greyOutOption(A_thermostats), submitOnChange:true - if ((A_switches || A_dimmers || A_thermostats) && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ - input "A_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((A_switches || A_dimmers || A_thermostats) && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } - section ("Other actions at alarm time"){ + section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "A_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (A_phrase && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ - input "A_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "A_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (A_phrase && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "A_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (A_triggerMode && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ - input "A_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (A_triggerMode && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersA", title: "Dimmer Settings") { - section { - input "A_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "A_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "A_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "A_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsA", title: "Thermostat Settings") { - section { - input "A_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "A_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "A_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "A_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "A_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsA() { - dynamicPage(name: "pageWeatherSettingsA", title: "Weather Reporting Settings") { - section { - input "A_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "A_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "A_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "A_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "A_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "A_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsA", title: "Weather Reporting Settings") { + section { + input "A_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "A_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "A_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "A_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "A_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "A_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } //Show "pageSetupScenarioB" page def pageSetupScenarioB() { dynamicPage(name: "pageSetupScenarioB") { - section("Alarm settings") { - input "ScenarioNameB", "text", title: "Scenario Name", multiple: false, required: true - input "B_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameB", "text", title: "Scenario Name", multiple: false, required: true + input "B_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "B_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "B_timeStart", "time", title: "Time to trigger alarm", required: true - input "B_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "B_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "B_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "B_timeStart", "time", title: "Time to trigger alarm", required: true + input "B_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "B_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "B_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (B_alarmType != "3") { - if (B_alarmType == "1"){ - input "B_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (B_alarmType == "1"){ + input "B_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (B_alarmType == "2"){ - input "B_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "B_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (B_alarmType == "1"){ - section ("Alarm sound options"){ - input "B_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "B_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } - if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")){ - section ("Voice greeting options") { - input "B_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + section ("Alarm sound options"){ + input "B_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "B_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")){ + section ("Voice greeting options") { + input "B_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false href "pageWeatherSettingsB", title: "Weather Reporting Settings", description: getWeatherDesc(B_weatherReport, B_includeSunrise, B_includeSunset, B_includeTemp, B_humidity, B_localTemp), state: greyOut1(B_weatherReport, B_includeSunrise, B_includeSunset, B_includeTemp, B_humidity, B_localTemp) - } - } - if (B_alarmType == "3" || (B_alarmType == "1" && B_secondAlarm =="2") || (B_alarmType == "2" && B_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "B_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(B_sonos, 1) - } - } + } + } + if (B_alarmType == "3" || (B_alarmType == "1" && B_secondAlarm =="2") || (B_alarmType == "2" && B_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "B_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(B_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "B_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersB", title: "Dimmer Settings", description: dimmerDesc(B_dimmers), state: greyOutOption(B_dimmers), submitOnChange:true + input "B_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersB", title: "Dimmer Settings", description: dimmerDesc(B_dimmers), state: greyOutOption(B_dimmers), submitOnChange:true href "pageThermostatsB", title: "Thermostat Settings", description: thermostatDesc(B_thermostats, B_temperatureH, B_temperatureC), state: greyOutOption(B_thermostats), submitOnChange:true - if ((B_switches || B_dimmers || B_thermostats) && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ - input "B_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((B_switches || B_dimmers || B_thermostats) && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "B_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (B_phrase && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ - input "B_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "B_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (B_phrase && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "B_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (B_triggerMode && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ - input "B_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (B_triggerMode && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersB", title: "Dimmer Settings") { - section { - input "B_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "B_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "B_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "B_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsB", title: "Thermostat Settings") { - section { - input "B_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "B_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "B_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "B_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "B_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsB() { - dynamicPage(name: "pageWeatherSettingsB", title: "Weather Reporting Settings") { - section { - input "B_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "B_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "B_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "B_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "B_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "B_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsB", title: "Weather Reporting Settings") { + section { + input "B_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "B_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "B_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "B_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "B_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "B_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } //Show "pageSetupScenarioC" page def pageSetupScenarioC() { dynamicPage(name: "pageSetupScenarioC") { - section("Alarm settings") { - input "ScenarioNameC", "text", title: "Scenario Name", multiple: false, required: true - input "C_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameC", "text", title: "Scenario Name", multiple: false, required: true + input "C_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "C_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "C_timeStart", "time", title: "Time to trigger alarm", required: true - input "C_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "C_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "C_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "C_timeStart", "time", title: "Time to trigger alarm", required: true + input "C_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "C_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "C_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (C_alarmType != "3") { - if (C_alarmType == "1"){ - input "C_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (C_alarmType == "1"){ + input "C_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (C_alarmType == "2"){ - input "C_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "C_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (C_alarmType == "1"){ - section ("Alarm sound options"){ - input "C_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "C_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } - + section ("Alarm sound options"){ + input "C_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "C_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1")) { - section ("Voice greeting options") { - input "C_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false - href "pageWeatherSettingsC", title: "Weather Reporting Settings", description: getWeatherDesc(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, A_humidity, C_localTemp), state: greyOut1(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, C_humidity, C_localTemp) } - } - - if (C_alarmType == "3" || (C_alarmType == "1" && C_secondAlarm =="2") || (C_alarmType == "2" && C_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "C_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(C_sonos, 1) - } - } + section ("Voice greeting options") { + input "C_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsC", title: "Weather Reporting Settings", description: getWeatherDesc(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, A_humidity, C_localTemp), state: greyOut1(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, C_humidity, C_localTemp) } + } + + if (C_alarmType == "3" || (C_alarmType == "1" && C_secondAlarm =="2") || (C_alarmType == "2" && C_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "C_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(C_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "C_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersC", title: "Dimmer Settings", description: dimmerDesc(C_dimmers), state: greyOutOption(C_dimmers), submitOnChange:true + input "C_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersC", title: "Dimmer Settings", description: dimmerDesc(C_dimmers), state: greyOutOption(C_dimmers), submitOnChange:true href "pageThermostatsC", title: "Thermostat Settings", description: thermostatDesc(C_thermostats, C_temperatureH, C_temperatureC), state: greyOutOption(C_thermostats), submitOnChange:true - if ((C_switches || C_dimmers || C_thermostats) && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ - input "C_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((C_switches || C_dimmers || C_thermostats) && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "C_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (C_phrase && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ - input "C_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "C_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (C_phrase && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "C_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (C_triggerMode && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ - input "C_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (C_triggerMode && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersC", title: "Dimmer Settings") { - section { - input "C_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "C_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "C_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "C_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsC", title: "Thermostat Settings") { - section { - input "C_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "C_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "C_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "C_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "C_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsC() { - dynamicPage(name: "pageWeatherSettingsC", title: "Weather Reporting Settings") { - section { - input "C_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "C_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "C_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "C_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "C_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "C_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsC", title: "Weather Reporting Settings") { + section { + input "C_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "C_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "C_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "C_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "C_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "C_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } //Show "pageSetupScenarioD" page def pageSetupScenarioD() { dynamicPage(name: "pageSetupScenarioD") { - section("Alarm settings") { - input "ScenarioNameD", "text", title: "Scenario Name", multiple: false, required: true - input "D_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameD", "text", title: "Scenario Name", multiple: false, required: true + input "D_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "D_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "D_timeStart", "time", title: "Time to trigger alarm", required: true - input "D_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "D_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "D_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "D_timeStart", "time", title: "Time to trigger alarm", required: true + input "D_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "D_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "D_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (D_alarmType != "3") { - if (D_alarmType == "1"){ - input "D_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (D_alarmType == "1"){ + input "D_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (D_alarmType == "2"){ - input "D_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "D_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (D_alarmType == "1"){ - section ("Alarm sound options"){ - input "D_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "D_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } - + section ("Alarm sound options"){ + input "D_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "D_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1")) { - section ("Voice greeting options") { - input "D_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false - href "pageWeatherSettingsD", title: "Weather Reporting Settings", description: getWeatherDesc(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp), state: greyOut1(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp) } - } - - if (D_alarmType == "3" || (D_alarmType == "1" && D_secondAlarm =="2") || (D_alarmType == "2" && D_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "D_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(D_sonos, 1) - } - } + section ("Voice greeting options") { + input "D_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsD", title: "Weather Reporting Settings", description: getWeatherDesc(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp), state: greyOut1(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp) } + } + + if (D_alarmType == "3" || (D_alarmType == "1" && D_secondAlarm =="2") || (D_alarmType == "2" && D_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "D_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(D_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "D_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersD", title: "Dimmer Settings", description: dimmerDesc(D_dimmers), state: greyOutOption(D_dimmers), submitOnChange:true + input "D_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersD", title: "Dimmer Settings", description: dimmerDesc(D_dimmers), state: greyOutOption(D_dimmers), submitOnChange:true href "pageThermostatsD", title: "Thermostat Settings", description: thermostatDesc(D_thermostats, D_temperatureH, D_temperatureC), state: greyOutOption(D_thermostats), submitOnChange:true - if ((D_switches || D_dimmers || D_thermostats) && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ - input "D_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((D_switches || D_dimmers || D_thermostats) && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "D_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (D_phrase && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ - input "D_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "D_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (D_phrase && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "D_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (D_triggerMode && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ - input "D_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (D_triggerMode && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersD", title: "Dimmer Settings") { - section { - input "D_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "D_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "D_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "D_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsD", title: "Thermostat Settings") { - section { - input "D_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "D_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "D_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "D_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "D_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsD() { - dynamicPage(name: "pageWeatherSettingsD", title: "Weather Reporting Settings") { - section { - input "D_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "D_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "D_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "D_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "D_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "D_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsD", title: "Weather Reporting Settings") { + section { + input "D_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "D_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "D_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "D_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "D_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "D_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } page(name: "pageAbout", title: "About ${textAppName()}") { @@ -493,445 +493,445 @@ def updated() { } def initialize() { - if (A_alarmType =="1"){ - alarmSoundUri(A_soundAlarm, A_soundLength, 1) + if (A_alarmType =="1"){ + alarmSoundUri(A_soundAlarm, A_soundLength, 1) } if (B_alarmType =="1"){ - alarmSoundUri(B_soundAlarm, B_soundLength, 2) + alarmSoundUri(B_soundAlarm, B_soundLength, 2) } if (C_alarmType =="1"){ - alarmSoundUri(C_soundAlarm, C_soundLength, 3) + alarmSoundUri(C_soundAlarm, C_soundLength, 3) } if (D_alarmType =="1"){ - alarmSoundUri(D_soundAlarm, D_soundLength, 4) + alarmSoundUri(D_soundAlarm, D_soundLength, 4) } - + if (alarmSummary && summarySonos) { - subscribe(app, appTouchHandler) + subscribe(app, appTouchHandler) } if (ScenarioNameA && A_timeStart && A_sonos && A_alarmOn && A_alarmType){ - schedule (A_timeStart, alarm_A) + schedule (A_timeStart, alarm_A) if (A_musicTrack){ - saveSelectedSong(A_sonos, A_musicTrack, 1) + saveSelectedSong(A_sonos, A_musicTrack, 1) } - } + } if (ScenarioNameB && B_timeStart && B_sonos &&B_alarmOn && B_alarmType){ - schedule (B_timeStart, alarm_B) + schedule (B_timeStart, alarm_B) if (B_musicTrack){ - saveSelectedSong(B_sonos, B_musicTrack, 2) + saveSelectedSong(B_sonos, B_musicTrack, 2) } - } + } if (ScenarioNameC && C_timeStart && C_sonos && C_alarmOn && C_alarmType){ - schedule (C_timeStart, alarm_C) + schedule (C_timeStart, alarm_C) if (C_musicTrack){ - saveSelectedSong(C_sonos, C_musicTrack, 3) + saveSelectedSong(C_sonos, C_musicTrack, 3) } - } - if (ScenarioNameD && D_timeStart && D_sonos && D_alarmOn && D_alarmType){ - schedule (D_timeStart, alarm_D) + } + if (ScenarioNameD && D_timeStart && D_sonos && D_alarmOn && D_alarmType){ + schedule (D_timeStart, alarm_D) if (D_musicTrack){ - saveSelectedSong(D_sonos, D_musicTrack, 4) + saveSelectedSong(D_sonos, D_musicTrack, 4) } - } + } } //-------------------------------------- def alarm_A() { - if ((!A_mode || A_mode.contains(location.mode)) && getDayOk(A_day)) { + if ((!A_mode || A_mode.contains(location.mode)) && getDayOk(A_day)) { if (A_switches || A_dimmers || A_thermostats) { - def dimLevel = A_level as Integer + def dimLevel = A_level as Integer A_switches?.on() - A_dimmers?.setLevel(dimLevel) + A_dimmers?.setLevel(dimLevel) if (A_thermostats) { - def thermostatState = A_thermostats.currentThermostatMode - if (thermostatState == "auto") { - A_thermostats.setHeatingSetpoint(A_temperatureH) - A_thermostats.setCoolingSetpoint(A_temperatureC) - } - else if (thermostatState == "heat") { - A_thermostats.setHeatingSetpoint(A_temperatureH) - log.info "Set $A_thermostats Heat $A_temperatureH°" - } - else { - A_thermostats.setCoolingSetpoint(A_temperatureC) - log.info "Set $A_thermostats Cool $A_temperatureC°" - } - } + def thermostatState = A_thermostats.currentThermostatMode + if (thermostatState == "auto") { + A_thermostats.setHeatingSetpoint(A_temperatureH) + A_thermostats.setCoolingSetpoint(A_temperatureC) + } + else if (thermostatState == "heat") { + A_thermostats.setHeatingSetpoint(A_temperatureH) + log.info "Set $A_thermostats Heat $A_temperatureH°" + } + else { + A_thermostats.setCoolingSetpoint(A_temperatureC) + log.info "Set $A_thermostats Cool $A_temperatureC°" + } + } } if (A_phrase) { - location.helloHome.execute(A_phrase) + location.helloHome.execute(A_phrase) } - + if (A_triggerMode && location.mode != A_triggerMode) { - if (location.modes?.find{it.name == A_triggerMode}) { - setLocationMode(A_triggerMode) - } + if (location.modes?.find{it.name == A_triggerMode}) { + setLocationMode(A_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${A_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${A_triggerMode}'" + } + } + if (A_volume) { - A_sonos.setLevel(A_volume) - } - + A_sonos.setLevel(A_volume) + } + if (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1")) { - state.fullMsgA = "" - if (A_wakeMsg) { - getGreeting(A_wakeMsg, 1) - } - + state.fullMsgA = "" + if (A_wakeMsg) { + getGreeting(A_wakeMsg, 1) + } + if (A_weatherReport || A_humidity || A_includeTemp || A_localTemp) { - getWeatherReport(1, A_weatherReport, A_humidity, A_includeTemp, A_localTemp) - } - - if (A_includeSunrise || A_includeSunset) { - getSunriseSunset(1, A_includeSunrise, A_includeSunset) - } - + getWeatherReport(1, A_weatherReport, A_humidity, A_includeTemp, A_localTemp) + } + + if (A_includeSunrise || A_includeSunset) { + getSunriseSunset(1, A_includeSunrise, A_includeSunset) + } + if ((A_switches || A_dimmers || A_thermostats) && A_confirmSwitches) { - getOnConfimation(A_switches, A_dimmers, A_thermostats, 1) - } - + getOnConfimation(A_switches, A_dimmers, A_thermostats, 1) + } + if (A_phrase && A_confirmPhrase) { - getPhraseConfirmation(1, A_phrase) - } - + getPhraseConfirmation(1, A_phrase) + } + if (A_triggerMode && A_confirmMode){ - getModeConfirmation(A_triggerMode, 1) + getModeConfirmation(A_triggerMode, 1) } - + state.soundA = textToSpeech(state.fullMsgA, true) - } - + } + if (A_alarmType == "1"){ - if (A_secondAlarm == "1" && state.soundAlarmA){ - A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.soundA.uri) - } + if (A_secondAlarm == "1" && state.soundAlarmA){ + A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.soundA.uri) + } if (A_secondAlarm == "2" && state.selectedSongA && state.soundAlarmA){ - A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.selectedSongA) + A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.selectedSongA) } if (!A_secondAlarm){ - A_sonos.playTrack(state.soundAlarmA.uri) + A_sonos.playTrack(state.soundAlarmA.uri) } } - + if (A_alarmType == "2") { - if (A_secondAlarmMusic && state.selectedSongA){ - A_sonos.playSoundAndTrack (state.soundA.uri, state.soundA.duration, state.selectedSongA) + if (A_secondAlarmMusic && state.selectedSongA){ + A_sonos.playSoundAndTrack (state.soundA.uri, state.soundA.duration, state.selectedSongA) } else { - A_sonos.playTrack(state.soundA.uri) + A_sonos.playTrack(state.soundA.uri) } } - + if (A_alarmType == "3") { - A_sonos.playTrack(state.selectedSongA) + A_sonos.playTrack(state.selectedSongA) } - } + } } def alarm_B() { - if ((!B_mode || B_mode.contains(location.mode)) && getDayOk(B_day)) { + if ((!B_mode || B_mode.contains(location.mode)) && getDayOk(B_day)) { if (B_switches || B_dimmers || B_thermostats) { - def dimLevel = B_level as Integer + def dimLevel = B_level as Integer B_switches?.on() - B_dimmers?.setLevel(dimLevel) + B_dimmers?.setLevel(dimLevel) if (B_thermostats) { - def thermostatState = B_thermostats.currentThermostatMode - if (thermostatState == "auto") { - B_thermostats.setHeatingSetpoint(B_temperatureH) - B_thermostats.setCoolingSetpoint(B_temperatureC) - } - else if (thermostatState == "heat") { - B_thermostats.setHeatingSetpoint(B_temperatureH) - log.info "Set $B_thermostats Heat $B_temperatureH°" - } - else { - B_thermostats.setCoolingSetpoint(B_temperatureC) - log.info "Set $B_thermostats Cool $B_temperatureC°" - } - } + def thermostatState = B_thermostats.currentThermostatMode + if (thermostatState == "auto") { + B_thermostats.setHeatingSetpoint(B_temperatureH) + B_thermostats.setCoolingSetpoint(B_temperatureC) + } + else if (thermostatState == "heat") { + B_thermostats.setHeatingSetpoint(B_temperatureH) + log.info "Set $B_thermostats Heat $B_temperatureH°" + } + else { + B_thermostats.setCoolingSetpoint(B_temperatureC) + log.info "Set $B_thermostats Cool $B_temperatureC°" + } + } } if (B_phrase) { - location.helloHome.execute(B_phrase) + location.helloHome.execute(B_phrase) } - + if (B_triggerMode && location.mode != B_triggerMode) { - if (location.modes?.find{it.name == B_triggerMode}) { - setLocationMode(B_triggerMode) - } + if (location.modes?.find{it.name == B_triggerMode}) { + setLocationMode(B_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${B_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${B_triggerMode}'" + } + } + if (B_volume) { - B_sonos.setLevel(B_volume) - } - + B_sonos.setLevel(B_volume) + } + if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")) { - state.fullMsgB = "" - if (B_wakeMsg) { - getGreeting(B_wakeMsg, 2) - } - + state.fullMsgB = "" + if (B_wakeMsg) { + getGreeting(B_wakeMsg, 2) + } + if (B_weatherReport || B_humidity || B_includeTemp || B_localTemp) { - getWeatherReport(2, B_weatherReport, B_humidity, B_includeTemp, B_localTemp) - } - - if (B_includeSunrise || B_includeSunset) { - getSunriseSunset(2, B_includeSunrise, B_includeSunset) - } - + getWeatherReport(2, B_weatherReport, B_humidity, B_includeTemp, B_localTemp) + } + + if (B_includeSunrise || B_includeSunset) { + getSunriseSunset(2, B_includeSunrise, B_includeSunset) + } + if ((B_switches || B_dimmers || B_thermostats) && B_confirmSwitches) { - getOnConfimation(B_switches, B_dimmers, B_thermostats, 2) - } - + getOnConfimation(B_switches, B_dimmers, B_thermostats, 2) + } + if (B_phrase && B_confirmPhrase) { - getPhraseConfirmation(2, B_phrase) - } - + getPhraseConfirmation(2, B_phrase) + } + if (B_triggerMode && B_confirmMode){ - getModeConfirmation(B_triggerMode, 2) + getModeConfirmation(B_triggerMode, 2) } - + state.soundB = textToSpeech(state.fullMsgB, true) - } - + } + if (B_alarmType == "1"){ - if (B_secondAlarm == "1" && state.soundAlarmB) { - B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.soundB.uri) - } + if (B_secondAlarm == "1" && state.soundAlarmB) { + B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.soundB.uri) + } if (B_secondAlarm == "2" && state.selectedSongB && state.soundAlarmB){ - B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.selectedSongB) + B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.selectedSongB) } if (!B_secondAlarm){ - B_sonos.playTrack(state.soundAlarmB.uri) + B_sonos.playTrack(state.soundAlarmB.uri) } } - + if (B_alarmType == "2") { - if (B_secondAlarmMusic && state.selectedSongB){ - B_sonos.playSoundAndTrack (state.soundB.uri, state.soundB.duration, state.selectedSongB) + if (B_secondAlarmMusic && state.selectedSongB){ + B_sonos.playSoundAndTrack (state.soundB.uri, state.soundB.duration, state.selectedSongB) } else { - B_sonos.playTrack(state.soundB.uri) + B_sonos.playTrack(state.soundB.uri) } } - + if (B_alarmType == "3") { - B_sonos.playTrack(state.selectedSongB) + B_sonos.playTrack(state.selectedSongB) } - } + } } def alarm_C() { - if ((!C_mode || C_mode.contains(location.mode)) && getDayOk(C_day)) { + if ((!C_mode || C_mode.contains(location.mode)) && getDayOk(C_day)) { if (C_switches || C_dimmers || C_thermostats) { - def dimLevel = C_level as Integer + def dimLevel = C_level as Integer C_switches?.on() - C_dimmers?.setLevel(dimLevel) + C_dimmers?.setLevel(dimLevel) if (C_thermostats) { - def thermostatState = C_thermostats.currentThermostatMode - if (thermostatState == "auto") { - C_thermostats.setHeatingSetpoint(C_temperatureH) - C_thermostats.setCoolingSetpoint(C_temperatureC) - } - else if (thermostatState == "heat") { - C_thermostats.setHeatingSetpoint(C_temperatureH) - log.info "Set $C_thermostats Heat $C_temperatureH°" - } - else { - C_thermostats.setCoolingSetpoint(C_temperatureC) - log.info "Set $C_thermostats Cool $C_temperatureC°" - } - } + def thermostatState = C_thermostats.currentThermostatMode + if (thermostatState == "auto") { + C_thermostats.setHeatingSetpoint(C_temperatureH) + C_thermostats.setCoolingSetpoint(C_temperatureC) + } + else if (thermostatState == "heat") { + C_thermostats.setHeatingSetpoint(C_temperatureH) + log.info "Set $C_thermostats Heat $C_temperatureH°" + } + else { + C_thermostats.setCoolingSetpoint(C_temperatureC) + log.info "Set $C_thermostats Cool $C_temperatureC°" + } + } } if (C_phrase) { - location.helloHome.execute(C_phrase) + location.helloHome.execute(C_phrase) } - + if (C_triggerMode && location.mode != C_triggerMode) { - if (location.modes?.find{it.name == C_triggerMode}) { - setLocationMode(C_triggerMode) - } + if (location.modes?.find{it.name == C_triggerMode}) { + setLocationMode(C_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${C_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${C_triggerMode}'" + } + } + if (C_volume) { - C_sonos.setLevel(C_volume) - } - + C_sonos.setLevel(C_volume) + } + if (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1")) { - state.fullMsgC = "" - if (C_wakeMsg) { - getGreeting(C_wakeMsg, 3) - } - + state.fullMsgC = "" + if (C_wakeMsg) { + getGreeting(C_wakeMsg, 3) + } + if (C_weatherReport || C_humidity || C_includeTemp || C_localTemp) { - getWeatherReport(3, C_weatherReport, C_humidity, C_includeTemp, C_localTemp) - } - - if (C_includeSunrise || C_includeSunset) { - getSunriseSunset(3, C_includeSunrise, C_includeSunset) - } - + getWeatherReport(3, C_weatherReport, C_humidity, C_includeTemp, C_localTemp) + } + + if (C_includeSunrise || C_includeSunset) { + getSunriseSunset(3, C_includeSunrise, C_includeSunset) + } + if ((C_switches || C_dimmers || C_thermostats) && C_confirmSwitches) { - getOnConfimation(C_switches, C_dimmers, C_thermostats, 3) - } - + getOnConfimation(C_switches, C_dimmers, C_thermostats, 3) + } + if (C_phrase && C_confirmPhrase) { - getPhraseConfirmation(3, C_phrase) - } - + getPhraseConfirmation(3, C_phrase) + } + if (C_triggerMode && C_confirmMode){ - getModeConfirmation(C_triggerMode, 3) + getModeConfirmation(C_triggerMode, 3) } - + state.soundC = textToSpeech(state.fullMsgC, true) - } - + } + if (C_alarmType == "1"){ - if (C_secondAlarm == "1" && state.soundAlarmC){ - C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.soundC.uri) - } + if (C_secondAlarm == "1" && state.soundAlarmC){ + C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.soundC.uri) + } if (C_secondAlarm == "2" && state.selectedSongC && state.soundAlarmC){ - C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.selectedSongC) + C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.selectedSongC) } if (!C_secondAlarm){ - C_sonos.playTrack(state.soundAlarmC.uri) + C_sonos.playTrack(state.soundAlarmC.uri) } } - + if (C_alarmType == "2") { - if (C_secondAlarmMusic && state.selectedSongC){ - C_sonos.playSoundAndTrack (state.soundC.uri, state.soundC.duration, state.selectedSongC) + if (C_secondAlarmMusic && state.selectedSongC){ + C_sonos.playSoundAndTrack (state.soundC.uri, state.soundC.duration, state.selectedSongC) } else { - C_sonos.playTrack(state.soundC.uri) + C_sonos.playTrack(state.soundC.uri) } } - + if (C_alarmType == "3") { - C_sonos.playTrack(state.selectedSongC) + C_sonos.playTrack(state.selectedSongC) } - } + } } def alarm_D() { - if ((!D_mode || D_mode.contains(location.mode)) && getDayOk(D_day)) { + if ((!D_mode || D_mode.contains(location.mode)) && getDayOk(D_day)) { if (D_switches || D_dimmers || D_thermostats) { - def dimLevel = D_level as Integer + def dimLevel = D_level as Integer D_switches?.on() - D_dimmers?.setLevel(dimLevel) + D_dimmers?.setLevel(dimLevel) if (D_thermostats) { - def thermostatState = D_thermostats.currentThermostatMode - if (thermostatState == "auto") { - D_thermostats.setHeatingSetpoint(D_temperatureH) - D_thermostats.setCoolingSetpoint(D_temperatureC) - } - else if (thermostatState == "heat") { - D_thermostats.setHeatingSetpoint(D_temperatureH) - log.info "Set $D_thermostats Heat $D_temperatureH°" - } - else { - D_thermostats.setCoolingSetpoint(D_temperatureC) - log.info "Set $D_thermostats Cool $D_temperatureC°" - } - } + def thermostatState = D_thermostats.currentThermostatMode + if (thermostatState == "auto") { + D_thermostats.setHeatingSetpoint(D_temperatureH) + D_thermostats.setCoolingSetpoint(D_temperatureC) + } + else if (thermostatState == "heat") { + D_thermostats.setHeatingSetpoint(D_temperatureH) + log.info "Set $D_thermostats Heat $D_temperatureH°" + } + else { + D_thermostats.setCoolingSetpoint(D_temperatureC) + log.info "Set $D_thermostats Cool $D_temperatureC°" + } + } } if (D_phrase) { - location.helloHome.execute(D_phrase) + location.helloHome.execute(D_phrase) } - + if (D_triggerMode && location.mode != D_triggerMode) { - if (location.modes?.find{it.name == D_triggerMode}) { - setLocationMode(D_triggerMode) - } + if (location.modes?.find{it.name == D_triggerMode}) { + setLocationMode(D_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${D_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${D_triggerMode}'" + } + } + if (D_volume) { - D_sonos.setLevel(D_volume) - } - + D_sonos.setLevel(D_volume) + } + if (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1")) { - state.fullMsgD = "" - if (D_wakeMsg) { - getGreeting(D_wakeMsg, 4) - } - + state.fullMsgD = "" + if (D_wakeMsg) { + getGreeting(D_wakeMsg, 4) + } + if (D_weatherReport || D_humidity || D_includeTemp || D_localTemp) { - getWeatherReport(4, D_weatherReport, D_humidity, D_includeTemp, D_localTemp) - } - - if (D_includeSunrise || D_includeSunset) { - getSunriseSunset(4, D_includeSunrise, D_includeSunset) - } - + getWeatherReport(4, D_weatherReport, D_humidity, D_includeTemp, D_localTemp) + } + + if (D_includeSunrise || D_includeSunset) { + getSunriseSunset(4, D_includeSunrise, D_includeSunset) + } + if ((D_switches || D_dimmers || D_thermostats) && D_confirmSwitches) { - getOnConfimation(D_switches, D_dimmers, D_thermostats, 4) - } - + getOnConfimation(D_switches, D_dimmers, D_thermostats, 4) + } + if (D_phrase && D_confirmPhrase) { - getPhraseConfirmation(4, D_phrase) - } - + getPhraseConfirmation(4, D_phrase) + } + if (D_triggerMode && D_confirmMode){ - getModeConfirmation(D_triggerMode, 4) + getModeConfirmation(D_triggerMode, 4) } - + state.soundD = textToSpeech(state.fullMsgD, true) - } - + } + if (D_alarmType == "1"){ - if (D_secondAlarm == "1" && state.soundAlarmD){ - D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.soundD.uri) - } + if (D_secondAlarm == "1" && state.soundAlarmD){ + D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.soundD.uri) + } if (D_secondAlarm == "2" && state.selectedSongD && state.soundAlarmD){ - D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.selectedSongD) + D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.selectedSongD) } if (!D_secondAlarm){ - D_sonos.playTrack(state.soundAlarmD.uri) + D_sonos.playTrack(state.soundAlarmD.uri) } } - + if (D_alarmType == "2") { - if (D_secondAlarmMusic && state.selectedSongD){ - D_sonos.playSoundAndTrack (state.soundD.uri, state.soundD.duration, state.selectedSongD) + if (D_secondAlarmMusic && state.selectedSongD){ + D_sonos.playSoundAndTrack (state.soundD.uri, state.soundD.duration, state.selectedSongD) } else { - D_sonos.playTrack(state.soundD.uri) + D_sonos.playTrack(state.soundD.uri) } } - + if (D_alarmType == "3") { - D_sonos.playTrack(state.selectedSongD) + D_sonos.playTrack(state.selectedSongD) } - } + } } def appTouchHandler(evt){ - if (!summaryMode || summaryMode.contains(location.mode)) { - state.summaryMsg = "The following is a summary of the alarm settings. " - getSummary (A_alarmOn, ScenarioNameA, A_timeStart, 1) - getSummary (B_alarmOn, ScenarioNameB, B_timeStart, 2) - getSummary (C_alarmOn, ScenarioNameC, C_timeStart, 3) - getSummary (D_alarmOn, ScenarioNameD, D_timeStart, 4) - - log.debug "Summary message = ${state.summaryMsg}" - def summarySound = textToSpeech(state.summaryMsg, true) - if (summaryVolume) { - summarySonos.setLevel(summaryVolume) - } - summarySonos.playTrack(summarySound.uri) - } + if (!summaryMode || summaryMode.contains(location.mode)) { + state.summaryMsg = "The following is a summary of the alarm settings. " + getSummary (A_alarmOn, ScenarioNameA, A_timeStart, 1) + getSummary (B_alarmOn, ScenarioNameB, B_timeStart, 2) + getSummary (C_alarmOn, ScenarioNameC, C_timeStart, 3) + getSummary (D_alarmOn, ScenarioNameD, D_timeStart, 4) + + log.debug "Summary message = ${state.summaryMsg}" + def summarySound = textToSpeech(state.summaryMsg, true) + if (summaryVolume) { + summarySonos.setLevel(summaryVolume) + } + summarySonos.playTrack(summarySound.uri) + } } def getSummary (alarmOn, scenarioName, timeStart, num){ @@ -949,161 +949,161 @@ def getSummary (alarmOn, scenarioName, timeStart, num){ //-------------------------------------- def getDesc(timeStart, sonos, day, mode) { - def desc = "Tap to set alarm" - if (timeStart) { - desc = "Alarm set to " + parseDate(timeStart,"", "h:mm a") +" on ${sonos}" - + def desc = "Tap to set alarm" + if (timeStart) { + desc = "Alarm set to " + parseDate(timeStart,"", "h:mm a") +" on ${sonos}" + def dayListSize = day ? day.size() : 7 - + if (day && dayListSize < 7) { - desc = desc + " on" + desc = desc + " on" for (dayName in day) { - desc = desc + " ${dayName}" - dayListSize = dayListSize -1 + desc = desc + " ${dayName}" + dayListSize = dayListSize -1 if (dayListSize) { - desc = "${desc}, " - } - } + desc = "${desc}, " + } + } } else { - desc = desc + " every day" - } - + desc = desc + " every day" + } + if (mode) { - def modeListSize = mode.size() - def modePrefix =" in the following modes: " - if (modeListSize == 1) { - modePrefix = " in the following mode: " - } - desc = desc + "${modePrefix}" - for (modeName in mode) { - desc = desc + "'${modeName}'" - modeListSize = modeListSize -1 - if (modeListSize) { - desc = "${desc}, " - } - else { - desc = "${desc}" - } - } - } - else { - desc = desc + " in all modes" + def modeListSize = mode.size() + def modePrefix =" in the following modes: " + if (modeListSize == 1) { + modePrefix = " in the following mode: " + } + desc = desc + "${modePrefix}" + for (modeName in mode) { + desc = desc + "'${modeName}'" + modeListSize = modeListSize -1 + if (modeListSize) { + desc = "${desc}, " + } + else { + desc = "${desc}" + } + } + } + else { + desc = desc + " in all modes" } } - desc + desc } def greyOut(scenario, sonos, alarmTime, alarmOn, alarmType){ - def result = scenario && sonos && alarmTime && alarmOn && alarmType ? "complete" : "" + def result = scenario && sonos && alarmTime && alarmOn && alarmType ? "complete" : "" } def greyOut1(param1, param2, param3, param4, param5, param6){ - def result = param1 || param2 || param3 || param4 || param5 || param6 ? "complete" : "" + def result = param1 || param2 || param3 || param4 || param5 || param6 ? "complete" : "" } def getWeatherDesc(param1, param2, param3, param4, param5, param6) { - def title = param1 || param2 || param3 || param4 || param5 || param6 ? "Tap to edit weather reporting options" : "Tap to setup weather reporting options" + def title = param1 || param2 || param3 || param4 || param5 || param6 ? "Tap to edit weather reporting options" : "Tap to setup weather reporting options" } def greyOutOption(param){ - def result = param ? "complete" : "" + def result = param ? "complete" : "" } def getTitle(scenario, num) { - def title = scenario ? scenario : "Alarm ${num} not configured" + def title = scenario ? scenario : "Alarm ${num} not configured" } def dimmerDesc(dimmer){ - def desc = dimmer ? "Tap to edit dimmer settings" : "Tap to set dimmer setting" + def desc = dimmer ? "Tap to edit dimmer settings" : "Tap to set dimmer setting" } def thermostatDesc(thermostat, heating, cooling){ - def tempText + def tempText if (heating || cooling){ - if (heating){ - tempText = "${heating} heat" + if (heating){ + tempText = "${heating} heat" } if (cooling){ - tempText = "${cooling} cool" + tempText = "${cooling} cool" } - if (heating && cooling) { - tempText ="${heating} heat / ${cooling} cool" + if (heating && cooling) { + tempText ="${heating} heat / ${cooling} cool" } } else { - tempText="Tap to edit thermostat settings" + tempText="Tap to edit thermostat settings" } - + def desc = thermostat ? "${tempText}" : "Tap to set thermostat settings" - return desc + return desc } private getDayOk(dayList) { - def result = true - if (dayList) { - result = dayList.contains(getDay()) - } - result + def result = true + if (dayList) { + result = dayList.contains(getDay()) + } + result } private getDay(){ - def df = new java.text.SimpleDateFormat("EEEE") - if (location.timeZone) { - df.setTimeZone(location.timeZone) - } - else { - df.setTimeZone(TimeZone.getTimeZone("America/New_York")) - } - def day = df.format(new Date()) + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) } private parseDate(date, epoch, type){ def parseDate = "" if (epoch){ - long longDate = Long.valueOf(epoch).longValue() + long longDate = Long.valueOf(epoch).longValue() parseDate = new Date(longDate).format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone) } else { - parseDate = date + parseDate = date } new Date().parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", parseDate).format("${type}", timeZone(parseDate)) } private getSunriseSunset(scenario, includeSunrise, includeSunset){ - if (location.timeZone || zipCode) { - def todayDate = new Date() - def s = getSunriseAndSunset(zipcode: zipCode, date: todayDate) - def riseTime = parseDate("", s.sunrise.time, "h:mm a") - def setTime = parseDate ("", s.sunset.time, "h:mm a") - def msg = "" - def currTime = now() + if (location.timeZone || zipCode) { + def todayDate = new Date() + def s = getSunriseAndSunset(zipcode: zipCode, date: todayDate) + def riseTime = parseDate("", s.sunrise.time, "h:mm a") + def setTime = parseDate ("", s.sunset.time, "h:mm a") + def msg = "" + def currTime = now() def verb1 = currTime >= s.sunrise.time ? "rose" : "will rise" def verb2 = currTime >= s.sunset.time ? "set" : "will set" - + if (includeSunrise && includeSunset) { - msg = "The sun ${verb1} this morning at ${riseTime} and ${verb2} at ${setTime}. " - } - else if (includeSunrise && !includeSunset) { - msg = "The sun ${verb1} this morning at ${riseTime}. " - } - else if (!includeSunrise && includeSunset) { - msg = "The sun ${verb2} tonight at ${setTime}. " - } - compileMsg(msg, scenario) - } - else { - msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information. " - compileMsg(msg, scenario) - } + msg = "The sun ${verb1} this morning at ${riseTime} and ${verb2} at ${setTime}. " + } + else if (includeSunrise && !includeSunset) { + msg = "The sun ${verb1} this morning at ${riseTime}. " + } + else if (!includeSunrise && includeSunset) { + msg = "The sun ${verb2} tonight at ${setTime}. " + } + compileMsg(msg, scenario) + } + else { + msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information. " + compileMsg(msg, scenario) + } } private getGreeting(msg, scenario) { - def day = getDay() + def day = getDay() def time = parseDate("", now(), "h:mm a") def month = parseDate("", now(), "MMMM") def year = parseDate("", now(), "yyyy") def dayNum = parseDate("", now(), "dd") - msg = msg.replace('%day%', day) + msg = msg.replace('%day%', day) msg = msg.replace('%date%', "${month} ${dayNum}, ${year}") msg = msg.replace('%time%', "${time}") msg = "${msg} " @@ -1111,199 +1111,188 @@ private getGreeting(msg, scenario) { } private getWeatherReport(scenario, weatherReport, humidity, includeTemp, localTemp) { - if (location.timeZone || zipCode) { - def isMetric = location.temperatureScale == "C" + if (location.timeZone || zipCode) { + def isMetric = location.temperatureScale == "C" def sb = new StringBuilder() - + if (includeTemp){ - def current = getWeatherFeature("conditions", zipCode) - if (isMetric) { - sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees. " - } - else { - sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees. " - } - } - + def current = = getTwcConditions(zipCode) + sb << "The current temperature is ${Math.round(current.temperature)} degrees. " + } + if (localTemp){ - sb << "The local temperature is ${Math.round(localTemp.currentTemperature)} degrees. " + sb << "The local temperature is ${Math.round(localTemp.currentTemperature)} degrees. " } if (humidity) { - sb << "The local relative humidity is ${humidity.currentValue("humidity")}%. " + sb << "The local relative humidity is ${humidity.currentValue("humidity")}%. " } - + if (weatherReport) { - def weather = getWeatherFeature("forecast", zipCode) - + def weather = getTwcForecast(zipCode) sb << "Today's forecast is " - if (isMetric) { - sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric - } - else { - sb << weather.forecast.txt_forecast.forecastday[0].fcttext - } - } - - def msg = sb.toString() + sb << weather.daypart[0].narrative[0] + } + + def msg = sb.toString() msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') msg = msg.replaceAll(/([0-9]+)F/,'$1 degrees') - compileMsg(msg, scenario) - } - else { - msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts." - compileMsg(msg, scenario) + compileMsg(msg, scenario) + } + else { + msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts." + compileMsg(msg, scenario) } } private getOnConfimation(switches, dimmers, thermostats, scenario) { - def msg = "" + def msg = "" if ((switches || dimmers) && !thermostats) { - msg = "All switches" + msg = "All switches" } if (!switches && !dimmers && thermostats) { - msg = "All Thermostats" + msg = "All Thermostats" } if ((switches || dimmers) && thermostats) { - msg = "All switches and thermostats" - } + msg = "All switches and thermostats" + } msg = "${msg} are now on and set. " compileMsg(msg, scenario) } private getPhraseConfirmation(scenario, phrase) { - def msg="The Smart Things Hello Home phrase, ${phrase}, has been activated. " - compileMsg(msg, scenario) + def msg="The Smart Things Hello Home phrase, ${phrase}, has been activated. " + compileMsg(msg, scenario) } private getModeConfirmation(mode, scenario) { - def msg="The Smart Things mode is now being set to, ${mode}. " - compileMsg(msg, scenario) + def msg="The Smart Things mode is now being set to, ${mode}. " + compileMsg(msg, scenario) } private compileMsg(msg, scenario) { - log.debug "msg = ${msg}" - if (scenario == 1) {state.fullMsgA = state.fullMsgA + "${msg}"} - if (scenario == 2) {state.fullMsgB = state.fullMsgB + "${msg}"} - if (scenario == 3) {state.fullMsgC = state.fullMsgC + "${msg}"} - if (scenario == 4) {state.fullMsgD = state.fullMsgD + "${msg}"} + log.debug "msg = ${msg}" + if (scenario == 1) {state.fullMsgA = state.fullMsgA + "${msg}"} + if (scenario == 2) {state.fullMsgB = state.fullMsgB + "${msg}"} + if (scenario == 3) {state.fullMsgC = state.fullMsgC + "${msg}"} + if (scenario == 4) {state.fullMsgD = state.fullMsgD + "${msg}"} } private alarmSoundUri(selection, length, scenario){ - def soundUri = "" - def soundLength = "" + def soundUri = "" + def soundLength = "" switch(selection) { - case "1": - soundLength = length >0 && length < 8 ? length : 8 + case "1": + soundLength = length >0 && length < 8 ? length : 8 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmAlien.mp3", duration: "${soundLength}"] - break + break case "2": - soundLength = length >0 && length < 12 ? length : 12 + soundLength = length >0 && length < 12 ? length : 12 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmBell.mp3", duration: "${soundLength}"] - break + break case "3": - soundLength = length >0 && length < 20 ? length : 20 + soundLength = length >0 && length < 20 ? length : 20 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmBuzzer.mp3", duration: "${soundLength}"] - break + break case "4": - soundLength = length >0 && length < 20 ? length : 20 + soundLength = length >0 && length < 20 ? length : 20 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmFire.mp3", duration: "${soundLength}"] - break + break case "5": - soundLength = length >0 && length < 2 ? length : 2 + soundLength = length >0 && length < 2 ? length : 2 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmRooster.mp3", duration: "${soundLength}"] - break + break case "6": - soundLength = length >0 && length < 20 ? length : 20 + soundLength = length >0 && length < 20 ? length : 20 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmSiren.mp3", duration: "${soundLength}"] - break + break } - if (scenario == 1) {state.soundAlarmA = soundUri} - if (scenario == 2) {state.soundAlarmB = soundUri} - if (scenario == 3) {state.soundAlarmC = soundUri} - if (scenario == 4) {state.soundAlarmD = soundUri} -} + if (scenario == 1) {state.soundAlarmA = soundUri} + if (scenario == 2) {state.soundAlarmB = soundUri} + if (scenario == 3) {state.soundAlarmC = soundUri} + if (scenario == 4) {state.soundAlarmD = soundUri} +} //Sonos Aquire Track from SmartThings code private songOptions(sonos, scenario) { - if (sonos){ - // Make sure current selection is in the set - def options = new LinkedHashSet() - if (scenario == 1){ - if (state.selectedSongA?.station) { - options << state.selectedSongA.station - } - else if (state.selectedSongA?.description) { - options << state.selectedSongA.description - } - } - if (scenario == 2){ - if (state.selectedSongB?.station) { - options << state.selectedSongB.station - } - else if (state.selectedSongB?.description) { - options << state.selectedSongB.description - } - } - if (scenario == 3){ - if (state.selectedSongC?.station) { - options << state.selectedSongC.station - } - else if (state.selectedSongC?.description) { - options << state.selectedSongC.description - } - } - if (scenario == 4){ - if (state.selectedSongD?.station) { - options << state.selectedSongD.station - } - else if (state.selectedSongD?.description) { - options << state.selectedSongD.description - } - } - // Query for recent tracks - def states = sonos.statesSince("trackData", new Date(0), [max:30]) - def dataMaps = states.collect{it.jsonValue} - options.addAll(dataMaps.collect{it.station}) - - log.trace "${options.size()} songs in list" - options.take(20) as List - } + if (sonos){ + // Make sure current selection is in the set + def options = new LinkedHashSet() + if (scenario == 1){ + if (state.selectedSongA?.station) { + options << state.selectedSongA.station + } + else if (state.selectedSongA?.description) { + options << state.selectedSongA.description + } + } + if (scenario == 2){ + if (state.selectedSongB?.station) { + options << state.selectedSongB.station + } + else if (state.selectedSongB?.description) { + options << state.selectedSongB.description + } + } + if (scenario == 3){ + if (state.selectedSongC?.station) { + options << state.selectedSongC.station + } + else if (state.selectedSongC?.description) { + options << state.selectedSongC.description + } + } + if (scenario == 4){ + if (state.selectedSongD?.station) { + options << state.selectedSongD.station + } + else if (state.selectedSongD?.description) { + options << state.selectedSongD.description + } + } + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List + } } private saveSelectedSong(sonos, song, scenario) { - try { - def thisSong = song - log.info "Looking for $thisSong" - def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} - log.info "Searching ${songs.size()} records" - - def data = songs.find {s -> s.station == thisSong} - log.info "Found ${data?.station}" - if (data) { - if (scenario == 1) {state.selectedSongA = data} + try { + def thisSong = song + log.info "Looking for $thisSong" + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + if (scenario == 1) {state.selectedSongA = data} if (scenario == 2) {state.selectedSongB = data} if (scenario == 3) {state.selectedSongC = data} if (scenario == 4) {state.selectedSongD = data} - log.debug "Selected song for Scenario ${scenario} = ${data}" - } - else if (song == state.selectedSongA?.station || song == state.selectedSongB?.station || song == state.selectedSongC?.station || song == state.selectedSongD?.station) { - log.debug "Selected existing entry '$song', which is no longer in the last 20 list" - } - else { - log.warn "Selected song '$song' not found" - } - } - catch (Throwable t) { - log.error t - } + log.debug "Selected song for Scenario ${scenario} = ${data}" + } + else if (song == state.selectedSongA?.station || song == state.selectedSongB?.station || song == state.selectedSongC?.station || song == state.selectedSongD?.station) { + log.debug "Selected existing entry '$song', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$song' not found" + } + } + catch (Throwable t) { + log.error t + } } //Version/Copyright/Information/Help private def textAppName() { - def text = "Talking Alarm Clock" -} + def text = "Talking Alarm Clock" +} private def textVersion() { def text = "Version 1.4.5 (06/17/2015)" @@ -1315,22 +1304,22 @@ private def textCopyright() { private def textLicense() { def text = - "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"+ - "\n\n"+ - " http://www.apache.org/licenses/LICENSE-2.0"+ - "\n\n"+ - "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." + "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"+ + "\n\n"+ + " http://www.apache.org/licenses/LICENSE-2.0"+ + "\n\n"+ + "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." } private def textHelp() { - def text = - "Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with " + + def text = + "Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with " + "switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time. "+ "You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report. " + "Variables that can be used in the voice greeting include %day%, %time% and %date%.\n\n"+ @@ -1338,4 +1327,3 @@ private def textHelp() { "speak a summary of the alarms enabled or disabled without having to go into the application itself. This " + "functionality is optional and can be configured from the main setup page." } - diff --git a/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy b/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy index 35a90ea6f66..6d8bd11ca6d 100644 --- a/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy +++ b/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy @@ -19,21 +19,21 @@ definition( ) preferences { - - section("At Candlelighting Change Mode To:") + + section("At Candlelighting Change Mode To:") { - input "startMode", "mode", title: "Mode?" - } - section("At Havdalah Change Mode To:") + input "startMode", "mode", title: "Mode?" + } + section("At Havdalah Change Mode To:") { - input "endMode", "mode", title: "Mode?" - } - section("Havdalah Offset (Usually 50 or 72)") { - input "havdalahOffset", "number", title: "Minutes After Sundown", required:true - } - section("Your ZipCode") { - input "zipcode", "text", title: "ZipCode", required:true - } + input "endMode", "mode", title: "Mode?" + } + section("Havdalah Offset (Usually 50 or 72)") { + input "havdalahOffset", "number", title: "Minutes After Sundown", required:true + } + section("Your ZipCode") { + input "zipcode", "text", title: "ZipCode", required:true + } section( "Notifications" ) { input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false input "phone", "phone", title: "Send a Text Message?", required: false @@ -42,28 +42,28 @@ preferences { } def installed() { - log.debug "Installed with settings: ${settings}" - initialize() + log.debug "Installed with settings: ${settings}" + initialize() } def updated() { - log.debug "Updated with settings: ${settings}" - unsubscribe() - initialize() + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() } def initialize() { poll(); - schedule("0 0 8 1/1 * ? *", poll) + schedule("0 0 8 1/1 * ? *", poll) } //Check hebcal for today's candle lighting or havdalah def poll() { - + unschedule("endChag") unschedule("setChag") - Hebcal_WebRequest() + Hebcal_WebRequest() }//END def poll() @@ -79,8 +79,8 @@ def Hebcal_WebRequest(){ def today = new Date().format("yyyy-MM-dd") //def today = "2014-11-14" def zip = settings.zip as String -def locale = getWeatherFeature("geolookup", zip) -def timezone = TimeZone.getTimeZone(locale.location.tz_long) +def locale = getTwcLocation(zipCode).location +def timezone = TimeZone.getTimeZone(locale.ianaTimeZone) def hebcal_date def hebcal_category def hebcal_title @@ -94,39 +94,39 @@ def urlRequest = "http://www.hebcal.com/hebcal/?v=1&cfg=json&nh=off&nx=off&year= log.trace "${urlRequest}" def hebcal = { response -> - hebcal_date = response.data.items.date - hebcal_category = response.data.items.category - hebcal_title = response.data.items.title - - for (int i = 0; i < hebcal_date.size; i++) + hebcal_date = response.data.items.date + hebcal_category = response.data.items.category + hebcal_title = response.data.items.title + + for (int i = 0; i < hebcal_date.size; i++) { - if(hebcal_date[i].split("T")[0]==today) + if(hebcal_date[i].split("T")[0]==today) { - if(hebcal_category[i]=="candles") - { - candlelightingLocalTime = HebCal_GetTime12(hebcal_title[i]) + if(hebcal_category[i]=="candles") + { + candlelightingLocalTime = HebCal_GetTime12(hebcal_title[i]) pushMessage = "Candle Lighting is at ${candlelightingLocalTime}" candlelightingLocalTime = HebCal_GetTime24(hebcal_date[i]) - candlelighting = timeToday(candlelightingLocalTime, timezone) - - sendMessage(pushMessage) - schedule(candlelighting, setChag) + candlelighting = timeToday(candlelightingLocalTime, timezone) + + sendMessage(pushMessage) + schedule(candlelighting, setChag) log.debug pushMessage - }//END if(hebcal_category=="candles") - - else if(hebcal_category[i]=="havdalah") - { - havdalahLocalTime = HebCal_GetTime12(hebcal_title[i]) + }//END if(hebcal_category=="candles") + + else if(hebcal_category[i]=="havdalah") + { + havdalahLocalTime = HebCal_GetTime12(hebcal_title[i]) pushMessage = "Havdalah is at ${havdalahLocalTime}" havdalahLocalTime = HebCal_GetTime24(hebcal_date[i]) - havdalah = timeToday(havdalahLocalTime, timezone) + havdalah = timeToday(havdalahLocalTime, timezone) testmessage = "Scheduling for ${havdalah}" - schedule(havdalah, endChag) + schedule(havdalah, endChag) log.debug pushMessage log.debug testmessage - }//END if(hebcal_category=="havdalah"){ + }//END if(hebcal_category=="havdalah"){ }//END if(hebcal_date[i].split("T")[0]==today) - + }//END for (int i = 0; i < hebcal_date.size; i++) }//END def hebcal = { response -> httpGet(urlRequest, hebcal); @@ -151,49 +151,49 @@ return returnTime -----------------------------------------------*/ def setChag() { - - if (location.mode != startMode) - { - if (location.modes?.find{it.name == startMode}) + + if (location.mode != startMode) + { + if (location.modes?.find{it.name == startMode}) { - setLocationMode(startMode) - //sendMessage("Changed the mode to '${startMode}'") + setLocationMode(startMode) + //sendMessage("Changed the mode to '${startMode}'") def dayofweek = new Date().format("EEE") - if(dayofweek=='Fri'){ - sendMessage("Shabbat Shalom!") - } - else{ - sendMessage("Chag Sameach!") - } - - }//END if (location.modes?.find{it.name == startMode}) - else + if(dayofweek=='Fri'){ + sendMessage("Shabbat Shalom!") + } + else{ + sendMessage("Chag Sameach!") + } + + }//END if (location.modes?.find{it.name == startMode}) + else { - sendMessage("Tried to change to undefined mode '${startMode}'") - }//END else - }//END if (location.mode != newMode) - + sendMessage("Tried to change to undefined mode '${startMode}'") + }//END else + }//END if (location.mode != newMode) + unschedule("setChag") }//END def setChag() def endChag() { - - if (location.mode != endMode) - { - if (location.modes?.find{it.name == endMode}) + + if (location.mode != endMode) + { + if (location.modes?.find{it.name == endMode}) { - setLocationMode(endMode) - sendMessage("Changed the mode to '${endMode}'") - }//END if (location.modes?.find{it.name == endMode}) - else + setLocationMode(endMode) + sendMessage("Changed the mode to '${endMode}'") + }//END if (location.modes?.find{it.name == endMode}) + else { - sendMessage("Tried to change to undefined mode '${endMode}'") - }//END else - }//END if (location.mode != endMode) - - //sendMessage("Shavuah Tov!") + sendMessage("Tried to change to undefined mode '${endMode}'") + }//END else + }//END if (location.mode != endMode) + + //sendMessage("Shavuah Tov!") unschedule("endChag") }//END def setChag() From acd5454a9e72d78e7299ecba039445478b2cb818 Mon Sep 17 00:00:00 2001 From: juano2310 Date: Sun, 23 Dec 2018 22:12:36 -0500 Subject: [PATCH 5/7] WWST-2093 - Typo fix --- .../talking-alarm-clock.src/talking-alarm-clock.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy index 3113bb172c8..424573d074e 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy +++ b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy @@ -1116,7 +1116,7 @@ private getWeatherReport(scenario, weatherReport, humidity, includeTemp, localTe def sb = new StringBuilder() if (includeTemp){ - def current = = getTwcConditions(zipCode) + def current = getTwcConditions(zipCode) sb << "The current temperature is ${Math.round(current.temperature)} degrees. " } From 789d4d34bbd85a171ea7f0753f64e2630f457471 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Mon, 31 Dec 2018 08:39:36 -0500 Subject: [PATCH 6/7] WWST-2094 - Convert Spruce Scheduler to TWC (#3820) --- .../spruce-scheduler.groovy | 2938 ++++++++--------- 1 file changed, 1427 insertions(+), 1511 deletions(-) diff --git a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy index 61033bf0071..2441ef2be73 100644 --- a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy +++ b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy @@ -1,7 +1,7 @@ /** * Spruce Scheduler Pre-release V2.53.1 - Updated 11/07/2016, BAB * - * + * * Copyright 2015 Plaid Systems * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -26,7 +26,7 @@ -Major revision by BAB * */ - + definition( name: "Spruce Scheduler", namespace: "plaidsystems", @@ -38,7 +38,7 @@ definition( iconX3Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", pausable: true ) - + preferences { page(name: 'startPage') page(name: 'autoPage') @@ -47,14 +47,14 @@ preferences { page(name: 'globalPage') page(name: 'contactPage') page(name: 'delayPage') - page(name: 'zonePage') + page(name: 'zonePage') - page(name: 'zoneSettingsPage') + page(name: 'zoneSettingsPage') page(name: 'zoneSetPage') page(name: 'plantSetPage') page(name: 'sprinklerSetPage') page(name: 'optionSetPage') - + //found at bottom - transition pages page(name: 'zoneSetPage1') page(name: 'zoneSetPage2') @@ -71,76 +71,76 @@ preferences { page(name: 'zoneSetPage13') page(name: 'zoneSetPage14') page(name: 'zoneSetPage15') - page(name: 'zoneSetPage16') + page(name: 'zoneSetPage16') } - + def startPage(){ dynamicPage(name: 'startPage', title: 'Spruce Smart Irrigation setup', install: true, uninstall: true) - { + { section(''){ href(name: 'globalPage', title: 'Schedule settings', required: false, page: 'globalPage', - image: 'http://www.plaidsystems.com/smartthings/st_settings.png', + image: 'http://www.plaidsystems.com/smartthings/st_settings.png', description: "Schedule: ${enableString()}\nWatering Time: ${startTimeString()}\nDays:${daysString()}\nNotifications:${notifyString()}" ) } - - section(''){ + + section(''){ href(name: 'weatherPage', title: 'Weather Settings', required: false, page: 'weatherPage', image: 'http://www.plaidsystems.com/smartthings/st_rain_225_r.png', description: "Weather from: ${zipString()}\nRain Delay: ${isRainString()}\nSeasonal Adjust: ${seasonalAdjString()}" ) } - - section(''){ + + section(''){ href(name: 'zonePage', title: 'Zone summary and setup', required: false, page: 'zonePage', image: 'http://www.plaidsystems.com/smartthings/st_zone16_225.png', description: "${getZoneSummary()}" ) } - - section(''){ + + section(''){ href(name: 'delayPage', title: 'Valve delays & Pause controls', required: false, page: 'delayPage', image: 'http://www.plaidsystems.com/smartthings/st_timer.png', description: "Valve Delay: ${pumpDelayString()} s\n${waterStoppersString()}\nSchedule Sync: ${syncString()}" ) } - + section(''){ href(title: 'Spruce Irrigation Knowledge Base', //page: 'customPage', - description: 'Explore our knowledge base for more information on Spruce and Spruce sensors. Contact form is ' + - 'also available here.', - required: false, style:'embedded', - image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png', + description: 'Explore our knowledge base for more information on Spruce and Spruce sensors. Contact form is ' + + 'also available here.', + required: false, style:'embedded', + image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png', url: 'http://support.spruceirrigation.com' ) } } } - + def globalPage() { dynamicPage(name: 'globalPage', title: '') { section('Spruce schedule Settings') { - label title: 'Schedule Name:', description: 'Name this schedule', required: false + label title: 'Schedule Name:', description: 'Name this schedule', required: false input 'switches', 'capability.switch', title: 'Spruce Irrigation Controller:', description: 'Select a Spruce controller', required: true, multiple: false - } + } section('Program Scheduling'){ input 'enable', 'bool', title: 'Enable watering:', defaultValue: 'true', metadata: [values: ['true', 'false']] input 'enableManual', 'bool', title: 'Enable this schedule for manual start, only 1 schedule should be enabled for manual start at a time!', defaultValue: 'true', metadata: [values: ['true', 'false']] - input 'startTime', 'time', title: 'Watering start time', required: true + input 'startTime', 'time', title: 'Watering start time', required: true paragraph(image: 'http://www.plaidsystems.com/smartthings/st_calander.png', - title: 'Selecting watering days', - 'Selecting watering days is optional. Spruce will optimize your watering schedule automatically. ' + + title: 'Selecting watering days', + 'Selecting watering days is optional. Spruce will optimize your watering schedule automatically. ' + 'If your area has water restrictions or you prefer set days, select the days to meet your requirements. ') - input (name: 'days', type: 'enum', title: 'Water only on these days...', required: false, multiple: true, metadata: [values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Even', 'Odd']]) - } + input (name: 'days', type: 'enum', title: 'Water only on these days...', required: false, multiple: true, metadata: [values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Even', 'Odd']]) + } section('Push Notifications') { - input(name: 'notify', type: 'enum', title: 'Select what push notifications to receive.', required: false, - multiple: true, metadata: [values: ['Daily', 'Delays', 'Warnings', 'Weather', 'Moisture', 'Events']]) + input(name: 'notify', type: 'enum', title: 'Select what push notifications to receive.', required: false, + multiple: true, metadata: [values: ['Daily', 'Delays', 'Warnings', 'Weather', 'Moisture', 'Events']]) input('recipients', 'contact', title: 'Send push notifications to', required: false, multiple: true) input(name: 'logAll', type: 'bool', title: 'Log all notices to Hello Home?', defaultValue: 'false', options: ['true', 'false']) - } + } } } @@ -148,175 +148,145 @@ def weatherPage() { dynamicPage(name: 'weatherPage', title: 'Weather settings') { section('Location to get weather forecast and conditions:') { href(name: 'hrefWithImage', title: "${zipString()}", page: 'zipcodePage', - description: 'Set local weather station', - required: false, - image: 'http://www.plaidsystems.com/smartthings/rain.png' - ) - input 'isRain', 'bool', title: 'Enable Rain check:', metadata: [values: ['true', 'false']] + description: 'Set local weather station', + required: false, + image: 'http://www.plaidsystems.com/smartthings/rain.png' + ) + input 'isRain', 'bool', title: 'Enable Rain check:', metadata: [values: ['true', 'false']] input 'rainDelay', 'decimal', title: 'inches of rain that will delay watering, default: 0.2', required: false input 'isSeason', 'bool', title: 'Enable Seasonal Weather Adjustment:', metadata: [values: ['true', 'false']] - } - } + } + } } - + def zipcodePage() { return dynamicPage(name: 'zipcodePage', title: 'Spruce weather station setup') { section(''){ - input(name: 'zipcode', type: 'text', title: 'Zipcode or WeatherUnderground station id. Default value is current Zip code', - defaultValue: getPWSID(), required: false, submitOnChange: true ) - } - - section(''){ - paragraph(image: 'http://www.plaidsystems.com/smartthings/wu.png', title: 'WeatherUnderground Personal Weather Stations (PWS)', - required: false, - 'To automatically select the PWS nearest to your hub location, select the toggle below and clear the ' + - 'location field above') - input(name: 'nearestPWS', type: 'bool', title: 'Use nearest PWS', options: ['true', 'false'], - defaultValue: false, submitOnChange: true) - href(title: 'Or, Search WeatherUnderground.com for your desired PWS', - description: 'After page loads, select "Change Station" for a list of weather stations. ' + - 'You will need to copy the station code into the location field above', - required: false, style:'embedded', - url: (location.latitude && location.longitude)? "http://www.wunderground.com/cgi-bin/findweather/hdfForecast?query=${location.latitude}%2C${location.longitude}" : - "http://www.wunderground.com/q/${location.zipCode}") + input(name: 'zipcode', type: 'text', title: 'Zipcode. Default value is current Zip code', + defaultValue: getPWSID(), required: false, submitOnChange: true ) } } } private String getPWSID() { - String PWSID = location.zipCode - if (zipcode) PWSID = zipcode - if (nearestPWS && !zipcode) { - // find the nearest PWS to the hub's geo location - String geoLocation = location.zipCode - // use coordinates, if available - if (location.latitude && location.longitude) geoLocation = "${location.latitude}%2C${location.longitude}" - Map wdata = getWeatherFeature('geolookup', geoLocation) - if (wdata && wdata.response && !wdata.response.containsKey('error')) { // if we get good data - if (wdata.response.features.containsKey('geolookup') && (wdata.response.features.geolookup.toInteger() == 1) && wdata.location) { - PWSID = wdata.location.nearby_weather_stations.pws.station[0].id - } - else log.debug "bad response" - } - else log.debug "null or error" - } - log.debug "Nearest PWS ${PWSID}" - return PWSID -} - -private String startTimeString(){ - if (!startTime) return 'Please set!' else return hhmm(startTime) -} - -private String enableString(){ + String PWSID = location.zipCode + if (zipcode) PWSID = zipcode + return PWSID +} + +private String startTimeString(){ + if (!startTime) return 'Please set!' else return hhmm(startTime) +} + +private String enableString(){ if(enable && enableManual) return 'On & Manual Set' - else if (enable) return 'On & Manual Off' + else if (enable) return 'On & Manual Off' else if (enableManual) return 'Off & Manual Set' else return 'Off' } private String waterStoppersString(){ - String stoppers = 'Contact Sensor' - if (settings.contacts) { - if (settings.contacts.size() != 1) stoppers += 's' - stoppers += ': ' - int i = 1 - settings.contacts.each { - if ( i > 1) stoppers += ', ' - stoppers += it.displayName - i++ - } - stoppers = "${stoppers}\nPause: When ${settings.contactStop}\n" - } - else { - stoppers += ': None\n' - } - stoppers += "Switch" - if (settings.toggles) { - if (settings.toggles.size() != 1) stoppers += 'es' - stoppers += ': ' - int i = 1 - settings.toggles.each { - if ( i > 1) stoppers += ', ' - stoppers += it.displayName - i++ - } - stoppers = "${stoppers}\nPause: When switched ${settings.toggleStop}\n" - } - else { - stoppers += ': None\n' - } - int cd = 10 - if (settings.contactDelay && settings.contactDelay > 10) cd = settings.contactDelay.toInteger() - stoppers += "Restart Delay: ${cd} secs" - return stoppers + String stoppers = 'Contact Sensor' + if (settings.contacts) { + if (settings.contacts.size() != 1) stoppers += 's' + stoppers += ': ' + int i = 1 + settings.contacts.each { + if ( i > 1) stoppers += ', ' + stoppers += it.displayName + i++ + } + stoppers = "${stoppers}\nPause: When ${settings.contactStop}\n" + } + else { + stoppers += ': None\n' + } + stoppers += "Switch" + if (settings.toggles) { + if (settings.toggles.size() != 1) stoppers += 'es' + stoppers += ': ' + int i = 1 + settings.toggles.each { + if ( i > 1) stoppers += ', ' + stoppers += it.displayName + i++ + } + stoppers = "${stoppers}\nPause: When switched ${settings.toggleStop}\n" + } + else { + stoppers += ': None\n' + } + int cd = 10 + if (settings.contactDelay && settings.contactDelay > 10) cd = settings.contactDelay.toInteger() + stoppers += "Restart Delay: ${cd} secs" + return stoppers } private String isRainString(){ - if (settings.isRain && !settings.rainDelay) return '0.2' as String + if (settings.isRain && !settings.rainDelay) return '0.2' as String if (settings.isRain) return settings.rainDelay as String else return 'Off' -} - +} + private String seasonalAdjString(){ - if(settings.isSeason) return 'On' else return 'Off' + if(settings.isSeason) return 'On' else return 'Off' } private String syncString(){ - if (settings.sync) return "${settings.sync.displayName}" else return 'None' + if (settings.sync) return "${settings.sync.displayName}" else return 'None' } private String notifyString(){ - String notifyStr = '' - if(settings.notify) { - if (settings.notify.contains('Daily')) notifyStr += ' Daily' - //if (settings.notify.contains('Weekly')) notifyStr += ' Weekly' - if (settings.notify.contains('Delays')) notifyStr += ' Delays' - if (settings.notify.contains('Warnings')) notifyStr += ' Warnings' - if (settings.notify.contains('Weather')) notifyStr += ' Weather' - if (settings.notify.contains('Moisture')) notifyStr += ' Moisture' - if (settings.notify.contains('Events')) notifyStr += ' Events' - } - if (notifyStr == '') notifyStr = ' None' - if (settings.logAll) notifyStr += '\nSending all Notifications to Hello Home log' - - return notifyStr + String notifyStr = '' + if(settings.notify) { + if (settings.notify.contains('Daily')) notifyStr += ' Daily' + //if (settings.notify.contains('Weekly')) notifyStr += ' Weekly' + if (settings.notify.contains('Delays')) notifyStr += ' Delays' + if (settings.notify.contains('Warnings')) notifyStr += ' Warnings' + if (settings.notify.contains('Weather')) notifyStr += ' Weather' + if (settings.notify.contains('Moisture')) notifyStr += ' Moisture' + if (settings.notify.contains('Events')) notifyStr += ' Events' + } + if (notifyStr == '') notifyStr = ' None' + if (settings.logAll) notifyStr += '\nSending all Notifications to Hello Home log' + + return notifyStr } private String daysString(){ - String daysString = '' + String daysString = '' if (days){ - if(days.contains('Even') || days.contains('Odd')) { - if (days.contains('Even')) daysString += ' Even' - if (days.contains('Odd')) daysString += ' Odd' - } + if(days.contains('Even') || days.contains('Odd')) { + if (days.contains('Even')) daysString += ' Even' + if (days.contains('Odd')) daysString += ' Odd' + } else { - if (days.contains('Monday')) daysString += ' M' - if (days.contains('Tuesday')) daysString += ' Tu' - if (days.contains('Wednesday')) daysString += ' W' - if (days.contains('Thursday')) daysString += ' Th' - if (days.contains('Friday')) daysString += ' F' - if (days.contains('Saturday')) daysString += ' Sa' - if (days.contains('Sunday')) daysString += ' Su' + if (days.contains('Monday')) daysString += ' M' + if (days.contains('Tuesday')) daysString += ' Tu' + if (days.contains('Wednesday')) daysString += ' W' + if (days.contains('Thursday')) daysString += ' Th' + if (days.contains('Friday')) daysString += ' F' + if (days.contains('Saturday')) daysString += ' Sa' + if (days.contains('Sunday')) daysString += ' Su' } } if(daysString == '') return ' Any' else return daysString } - + private String hhmm(time, fmt = 'h:mm a'){ def t = timeToday(time, location.timeZone) def f = new java.text.SimpleDateFormat(fmt) f.setTimeZone(location.timeZone ?: timeZone(time)) return f.format(t) } - + private String pumpDelayString(){ if (!pumpDelay) return '0' else return pumpDelay as String } - + def delayPage() { dynamicPage(name: 'delayPage', title: 'Additional Options') { section(''){ @@ -328,7 +298,7 @@ def delayPage() { 'On->Valve Off->delay->...' input name: 'pumpDelay', type: 'number', title: 'Set a delay in seconds?', defaultValue: '0', required: false } - + section(''){ paragraph(image: 'http://www.plaidsystems.com/smartthings/st_pause.png', title: 'Pause Control Contacts & Switches', @@ -337,167 +307,167 @@ def delayPage() { 'toggled, water immediately stops and will not resume until all of the contact sensors are closed and all of ' + 'the switches are reset.\n\nCaution: if all contacts or switches are left in the stop state, the dependent ' + 'schedule(s) will never run.') - input(name: 'contacts', title: 'Select water delay contact sensors', type: 'capability.contactSensor', multiple: true, - required: false, submitOnChange: true) + input(name: 'contacts', title: 'Select water delay contact sensors', type: 'capability.contactSensor', multiple: true, + required: false, submitOnChange: true) // if (settings.contact) settings.contact = null // 'contact' has been deprecated - if (contacts) - input(name: 'contactStop', title: 'Stop watering when sensors are...', type: 'enum', required: (settings.contacts != null), - options: ['open', 'closed'], defaultValue: 'open') - input(name: 'toggles', title: 'Select water delay switches', type: 'capability.switch', multiple: true, required: false, - submitOnChange: true) - if (toggles) - input(name: 'toggleStop', title: 'Stop watering when switches are...', type: 'enum', - required: (settings.toggles != null), options: ['on', 'off'], defaultValue: 'off') - input(name: 'contactDelay', type: 'number', title: 'Restart watering how many seconds after all contacts and switches ' + - 'are reset? (minimum 10s)', defaultValue: '10', required: false) - } - + if (contacts) + input(name: 'contactStop', title: 'Stop watering when sensors are...', type: 'enum', required: (settings.contacts != null), + options: ['open', 'closed'], defaultValue: 'open') + input(name: 'toggles', title: 'Select water delay switches', type: 'capability.switch', multiple: true, required: false, + submitOnChange: true) + if (toggles) + input(name: 'toggleStop', title: 'Stop watering when switches are...', type: 'enum', + required: (settings.toggles != null), options: ['on', 'off'], defaultValue: 'off') + input(name: 'contactDelay', type: 'number', title: 'Restart watering how many seconds after all contacts and switches ' + + 'are reset? (minimum 10s)', defaultValue: '10', required: false) + } + section(''){ paragraph image: 'http://www.plaidsystems.com/smartthings/st_spruce_controller_250.png', - title: 'Controller Sync', - required: false, - 'For multiple controllers only. This schedule will wait for the selected controller to finish before ' + - 'starting. Do not set with a single controller!' - input name: 'sync', type: 'capability.switch', title: 'Select Master Controller', description: 'Only use this setting with multiple controllers', required: false, multiple: false + title: 'Controller Sync', + required: false, + 'For multiple controllers only. This schedule will wait for the selected controller to finish before ' + + 'starting. Do not set with a single controller!' + input name: 'sync', type: 'capability.switch', title: 'Select Master Controller', description: 'Only use this setting with multiple controllers', required: false, multiple: false } } } - -def zonePage() { + +def zonePage() { dynamicPage(name: 'zonePage', title: 'Zone setup', install: false, uninstall: false) { - section('') { + section('') { href(name: 'hrefWithImage', title: 'Zone configuration', page: 'zoneSettingsPage', description: "${zoneString()}", - required: false, + required: false, image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png') } - if (zoneActive('1')){ + if (zoneActive('1')){ section(''){ href(name: 'z1Page', title: "1: ${getname("1")}", required: false, page: 'zoneSetPage1', - image: "${getimage("1")}", - description: "${display("1")}" ) + image: "${getimage("1")}", + description: "${display("1")}" ) } } if (zoneActive('2')){ section(''){ href(name: 'z2Page', title: "2: ${getname("2")}", required: false, page: 'zoneSetPage2', - image: "${getimage("2")}", - description: "${display("2")}" ) + image: "${getimage("2")}", + description: "${display("2")}" ) } } if (zoneActive('3')){ section(''){ href(name: 'z3Page', title: "3: ${getname("3")}", required: false, page: 'zoneSetPage3', - image: "${getimage("3")}", - description: "${display("3")}" ) + image: "${getimage("3")}", + description: "${display("3")}" ) } } if (zoneActive('4')){ section(''){ href(name: 'z4Page', title: "4: ${getname("4")}", required: false, page: 'zoneSetPage4', - image: "${getimage("4")}", - description: "${display("4")}" ) + image: "${getimage("4")}", + description: "${display("4")}" ) } } if (zoneActive('5')){ section(''){ href(name: 'z5Page', title: "5: ${getname("5")}", required: false, page: 'zoneSetPage5', - image: "${getimage("5")}", - description: "${display("5")}" ) + image: "${getimage("5")}", + description: "${display("5")}" ) } } if (zoneActive('6')){ section(''){ href(name: 'z6Page', title: "6: ${getname("6")}", required: false, page: 'zoneSetPage6', - image: "${getimage("6")}", - description: "${display("6")}" ) + image: "${getimage("6")}", + description: "${display("6")}" ) } } - if (zoneActive('7')){ + if (zoneActive('7')){ section(''){ href(name: 'z7Page', title: "7: ${getname("7")}", required: false, page: 'zoneSetPage7', - image: "${getimage("7")}", - description: "${display("7")}" ) + image: "${getimage("7")}", + description: "${display("7")}" ) } } if (zoneActive('8')){ section(''){ href(name: 'z8Page', title: "8: ${getname("8")}", required: false, page: 'zoneSetPage8', - image: "${getimage("8")}", - description: "${display("8")}" ) + image: "${getimage("8")}", + description: "${display("8")}" ) } } if (zoneActive('9')){ section(''){ href(name: 'z9Page', title: "9: ${getname("9")}", required: false, page: 'zoneSetPage9', - image: "${getimage("9")}", + image: "${getimage("9")}", description: "${display("9")}" ) } } if (zoneActive('10')){ section(''){ href(name: 'z10Page', title: "10: ${getname("10")}", required: false, page: 'zoneSetPage10', - image: "${getimage("10")}", + image: "${getimage("10")}", description: "${display("10")}" ) } } if (zoneActive('11')){ section(''){ href(name: 'z11Page', title: "11: ${getname("11")}", required: false, page: 'zoneSetPage11', - image: "${getimage("11")}", + image: "${getimage("11")}", description: "${display("11")}" ) } } if (zoneActive('12')){ section(''){ href(name: 'z12Page', title: "12: ${getname("12")}", required: false, page: 'zoneSetPage12', - image: "${getimage("12")}", + image: "${getimage("12")}", description: "${display("12")}" ) } } if (zoneActive('13')){ section(''){ href(name: 'z13Page', title: "13: ${getname("13")}", required: false, page: 'zoneSetPage13', - image: "${getimage("13")}", + image: "${getimage("13")}", description: "${display("13")}" ) } } if (zoneActive('14')){ section(''){ href(name: 'z14Page', title: "14: ${getname("14")}", required: false, page: 'zoneSetPage14', - image: "${getimage("14")}", + image: "${getimage("14")}", description: "${display("14")}" ) } } if (zoneActive('15')){ section(''){ href(name: 'z15Page', title: "15: ${getname("15")}", required: false, page: 'zoneSetPage15', - image: "${getimage("15")}", + image: "${getimage("15")}", description: "${display("15")}" ) } } if (zoneActive('16')){ section(''){ href(name: 'z16Page', title: "16: ${getname("16")}", required: false, page: 'zoneSetPage16', - image: "${getimage("16")}", + image: "${getimage("16")}", description: "${display("16")}" ) } - } + } } } // Verify whether a zone is active /*//Code for fresh install private boolean zoneActive(String zoneStr){ - if (!zoneNumber) return false - if (zoneNumber.contains(zoneStr)) return true // don't display zones that are not selected + if (!zoneNumber) return false + if (zoneNumber.contains(zoneStr)) return true // don't display zones that are not selected return false } */ -//code change for ST update file -> change input to zoneNumberEnum -private boolean zoneActive(z){ - if (!zoneNumberEnum && zoneNumber && zoneNumber >= z.toInteger()) return true +//code change for ST update file -> change input to zoneNumberEnum +private boolean zoneActive(z){ + if (!zoneNumberEnum && zoneNumber && zoneNumber >= z.toInteger()) return true else if (!zoneNumberEnum && zoneNumber && zoneNumber != z.toInteger()) return false else if (zoneNumberEnum && zoneNumberEnum.contains(z)) return true return false @@ -505,7 +475,7 @@ private boolean zoneActive(z){ private String zoneString() { - String numberString = 'Add zones to setup' + String numberString = 'Add zones to setup' if (zoneNumber) numberString = "Zones enabled: ${zoneNumber}" if (learn) numberString = "${numberString}\nSensor mode: Adaptive" else numberString = "${numberString}\nSensor mode: Delay" @@ -513,256 +483,256 @@ private String zoneString() { } def zoneSettingsPage() { - dynamicPage(name: 'zoneSettingsPage', title: 'Zone Configuration') { - section(''){ - //input (name: "zoneNumber", type: "number", title: "Enter number of zones to configure?",description: "How many valves do you have? 1-16", required: true)//, defaultValue: 16) - input 'zoneNumberEnum', 'enum', title: 'Select zones to configure', multiple: true, metadata: [values: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16']] + dynamicPage(name: 'zoneSettingsPage', title: 'Zone Configuration') { + section(''){ + //input (name: "zoneNumber", type: "number", title: "Enter number of zones to configure?",description: "How many valves do you have? 1-16", required: true)//, defaultValue: 16) + input 'zoneNumberEnum', 'enum', title: 'Select zones to configure', multiple: true, metadata: [values: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16']] input 'gain', 'number', title: 'Increase or decrease all water times by this %, enter a negative or positive value, Default: 0', required: false, range: '-99..99' - paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', - title: 'Moisture sensor adapt mode', - 'Adaptive mode enabled: Watering times will be adjusted based on the assigned moisture sensor.\n\nAdaptive mode ' + - 'disabled (Delay): Zones with moisture sensors will water on any available days when the low moisture setpoint has ' + - 'been reached.' - input 'learn', 'bool', title: 'Enable Adaptive Moisture Control (with moisture sensors)', metadata: [values: ['true', 'false']] - } - } + paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', + title: 'Moisture sensor adapt mode', + 'Adaptive mode enabled: Watering times will be adjusted based on the assigned moisture sensor.\n\nAdaptive mode ' + + 'disabled (Delay): Zones with moisture sensors will water on any available days when the low moisture setpoint has ' + + 'been reached.' + input 'learn', 'bool', title: 'Enable Adaptive Moisture Control (with moisture sensors)', metadata: [values: ['true', 'false']] + } + } } -def zoneSetPage() { +def zoneSetPage() { dynamicPage(name: 'zoneSetPage', title: "Zone ${state.app} Setup") { section(''){ - paragraph image: "http://www.plaidsystems.com/smartthings/st_${state.app}.png", - title: 'Current Settings', - "${display("${state.app}")}" + paragraph image: "http://www.plaidsystems.com/smartthings/st_${state.app}.png", + title: 'Current Settings', + "${display("${state.app}")}" } - + section(''){ input "name${state.app}", 'text', title: 'Zone name?', required: false, defaultValue: "Zone ${state.app}" } - - section(''){ - href(name: 'tosprinklerSetPage', title: "Sprinkler type: ${setString('zone')}", required: false, page: 'sprinklerSetPage', - image: "${getimage("${settings."zone${state.app}"}")}", + + section(''){ + href(name: 'tosprinklerSetPage', title: "Sprinkler type: ${setString('zone')}", required: false, page: 'sprinklerSetPage', + image: "${getimage("${settings."zone${state.app}"}")}", //description: "Set sprinkler nozzle type or turn zone off") - description: 'Sprinkler type descriptions') + description: 'Sprinkler type descriptions') input "zone${state.app}", 'enum', title: 'Sprinkler Type', multiple: false, required: false, defaultValue: 'Off', submitOnChange: true, metadata: [values: ['Off', 'Spray', 'Rotor', 'Drip', 'Master Valve', 'Pump']] } - - section(''){ + + section(''){ href(name: 'toplantSetPage', title: "Landscape Select: ${setString('plant')}", required: false, page: 'plantSetPage', image: "${getimage("${settings["plant${state.app}"]}")}", //description: "Set landscape type") description: 'Landscape type descriptions') input "plant${state.app}", 'enum', title: 'Landscape', multiple: false, required: false, submitOnChange: true, metadata: [values: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']] - } - - section(''){ + } + + section(''){ href(name: 'tooptionSetPage', title: "Options: ${setString('option')}", required: false, page: 'optionSetPage', image: "${getimage("${settings["option${state.app}"]}")}", //description: "Set watering options") description: 'Watering option descriptions') - input "option${state.app}", 'enum', title: 'Options', multiple: false, required: false, defaultValue: 'Cycle 2x', submitOnChange: true,metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] + input "option${state.app}", 'enum', title: 'Options', multiple: false, required: false, defaultValue: 'Cycle 2x', submitOnChange: true,metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] } - + section(''){ paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', - title: 'Moisture sensor settings', + title: 'Moisture sensor settings', 'Select a soil moisture sensor to monitor and control watering. The soil moisture target value is set to a default value but can be adjusted to tune watering' input "sensor${state.app}", 'capability.relativeHumidityMeasurement', title: 'Select moisture sensor?', required: false, multiple: false input "sensorSp${state.app}", 'number', title: "Minimum moisture sensor target value, Setpoint: ${getDrySp(state.app)}", required: false } - + section(''){ paragraph image: 'http://www.plaidsystems.com/smartthings/st_timer.png', - title: 'Optional: Enter total watering time per week', + title: 'Optional: Enter total watering time per week', 'This value will replace the calculated time from other settings' input "minWeek${state.app}", 'number', title: 'Minimum water time per week.\nDefault: 0 = autoadjust', description: 'minutes per week', required: false input "perDay${state.app}", 'number', title: 'Guideline value for time per day, this divides minutes per week into watering days. Default: 20', defaultValue: '20', required: false } } -} +} private String setString(String type) { - switch (type) { - case 'zone': - if (settings."zone${state.app}") return settings."zone${state.app}" else return 'Not Set' - break - case 'plant': - if (settings."plant${state.app}") return settings."plant${state.app}" else return 'Not Set' - break - case 'option': - if (settings."option${state.app}") return settings."option${state.app}" else return 'Not Set' - break - default: - return '????' - } -} - -def plantSetPage() { + switch (type) { + case 'zone': + if (settings."zone${state.app}") return settings."zone${state.app}" else return 'Not Set' + break + case 'plant': + if (settings."plant${state.app}") return settings."plant${state.app}" else return 'Not Set' + break + case 'option': + if (settings."option${state.app}") return settings."option${state.app}" else return 'Not Set' + break + default: + return '????' + } +} + +def plantSetPage() { dynamicPage(name: 'plantSetPage', title: "${settings["name${state.app}"]} Landscape Select") { section(''){ - paragraph image: 'http://www.plaidsystems.com/img/st_${state.app}.png', + paragraph image: 'http://www.plaidsystems.com/img/st_${state.app}.png', title: "${settings["name${state.app}"]}", "Current settings ${display("${state.app}")}" //input "plant${state.app}", "enum", title: "Landscape", multiple: false, required: false, submitOnChange: true, metadata: [values: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']] - } + } section(''){ - paragraph image: 'http://www.plaidsystems.com/smartthings/st_lawn_200_r.png', - title: 'Lawn', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_lawn_200_r.png', + title: 'Lawn', 'Select Lawn for typical grass applications' - paragraph image: 'http://www.plaidsystems.com/smartthings/st_garden_225_r.png', - title: 'Garden', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_garden_225_r.png', + title: 'Garden', 'Select Garden for vegetable gardens' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_flowers_225_r.png', - title: 'Flowers', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_flowers_225_r.png', + title: 'Flowers', 'Select Flowers for beds with smaller seasonal plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png', - title: 'Shrubs', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png', + title: 'Shrubs', 'Select Shrubs for beds with larger established plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_trees_225_r.png', - title: 'Trees', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_trees_225_r.png', + title: 'Trees', 'Select Trees for deep rooted areas without other plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png', - title: 'Xeriscape', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png', + title: 'Xeriscape', 'Reduces water for native or drought tolorent plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_newplants_225_r.png', - title: 'New Plants', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_newplants_225_r.png', + title: 'New Plants', 'Increases watering time per week and reduces automatic adjustments to help establish new plants. No weekly seasonal adjustment and moisture setpoint set to 40.' } } } - + def sprinklerSetPage(){ dynamicPage(name: 'sprinklerSetPage', title: "${settings["name${state.app}"]} Sprinkler Select") { section(''){ - paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", + paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", title: "${settings["name${state.app}"]}", "Current settings ${display("${state.app}")}" //input "zone${state.app}", "enum", title: "Sprinkler Type", multiple: false, required: false, defaultValue: 'Off', metadata: [values: ['Off', 'Spray', 'Rotor', 'Drip', 'Master Valve', 'Pump']] } section(''){ - paragraph image: 'http://www.plaidsystems.com/smartthings/st_spray_225_r.png', - title: 'Spray', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_spray_225_r.png', + title: 'Spray', 'Spray sprinkler heads spray a fan of water over the lawn. The water is applied evenly and can be turned on for a shorter duration of time.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_rotor_225_r.png', - title: 'Rotor', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_rotor_225_r.png', + title: 'Rotor', 'Rotor sprinkler heads rotate, spraying a stream over the lawn. Because they move back and forth across the lawn, they require a longer water period.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_drip_225_r.png', - title: 'Drip', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_drip_225_r.png', + title: 'Drip', 'Drip lines or low flow emitters water slowely to minimize evaporation, because they are low flow, they require longer watering periods.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_master_225_r.png', - title: 'Master', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_master_225_r.png', + title: 'Master', 'Master valves will open before watering begins. Set the delay between master opening and watering in delay settings.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_pump_225_r.png', - title: 'Pump', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_pump_225_r.png', + title: 'Pump', 'Attach a pump relay to this zone and the pump will turn on before watering begins. Set the delay between pump start and watering in delay settings.' } } } - + def optionSetPage(){ dynamicPage(name: 'optionSetPage', title: "${settings["name${state.app}"]} Options") { section(''){ - paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", + paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", title: "${settings["name${state.app}"]}", "Current settings ${display("${state.app}")}" - //input "option${state.app}", "enum", title: "Options", multiple: false, required: false, defaultValue: 'Cycle 2x', metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] + //input "option${state.app}", "enum", title: "Options", multiple: false, required: false, defaultValue: 'Cycle 2x', metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] } section(''){ - paragraph image: 'http://www.plaidsystems.com/smartthings/st_slope_225_r.png', - title: 'Slope', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_slope_225_r.png', + title: 'Slope', 'Slope sets the sprinklers to cycle 3x, each with a short duration to minimize runoff' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_sand_225_r.png', - title: 'Sand', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_sand_225_r.png', + title: 'Sand', 'Sandy soil drains quickly and requires more frequent but shorter intervals of water' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_clay_225_r.png', - title: 'Clay', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_clay_225_r.png', + title: 'Clay', 'Clay sets the sprinklers to cycle 2x, each with a short duration to maximize absorption' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png', - title: 'No Cycle', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png', + title: 'No Cycle', 'The sprinklers will run for 1 long duration' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png', - title: 'Cycle 2x', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png', + title: 'Cycle 2x', 'Cycle 2x will break the water period up into 2 shorter cycles to help minimize runoff and maximize adsorption' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png', - title: 'Cycle 3x', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png', + title: 'Cycle 3x', 'Cycle 3x will break the water period up into 3 shorter cycles to help minimize runoff and maximize adsorption' } } } - + def setPage(i){ if (i) state.app = i return state.app } private String getaZoneSummary(int zone){ - if (!settings."zone${zone}" || (settings."zone${zone}" == 'Off')) return "${zone}: Off" - - String daysString = '' + if (!settings."zone${zone}" || (settings."zone${zone}" == 'Off')) return "${zone}: Off" + + String daysString = '' int tpw = initTPW(zone) - int dpw = initDPW(zone) - int runTime = calcRunTime(tpw, dpw) - - if ( !learn && (settings."sensor${zone}")) { - daysString = 'if Moisture is low on: ' - dpw = daysAvailable() - } - if (days && (days.contains('Even') || days.contains('Odd'))) { - if (dpw == 1) daysString = 'Every 8 days' - if (dpw == 2) daysString = 'Every 4 days' - if (dpw == 4) daysString = 'Every 2 days' - if (days.contains('Even') && days.contains('Odd')) daysString = 'any day' - } - else { - def int[] dpwMap = [0,0,0,0,0,0,0] - dpwMap = getDPWDays(dpw) - daysString += getRunDays(dpwMap) - } - return "${zone}: ${runTime} min, ${daysString}" + int dpw = initDPW(zone) + int runTime = calcRunTime(tpw, dpw) + + if ( !learn && (settings."sensor${zone}")) { + daysString = 'if Moisture is low on: ' + dpw = daysAvailable() + } + if (days && (days.contains('Even') || days.contains('Odd'))) { + if (dpw == 1) daysString = 'Every 8 days' + if (dpw == 2) daysString = 'Every 4 days' + if (dpw == 4) daysString = 'Every 2 days' + if (days.contains('Even') && days.contains('Odd')) daysString = 'any day' + } + else { + def int[] dpwMap = [0,0,0,0,0,0,0] + dpwMap = getDPWDays(dpw) + daysString += getRunDays(dpwMap) + } + return "${zone}: ${runTime} min, ${daysString}" } private String getZoneSummary(){ - String summary = '' + String summary = '' if (learn) summary = 'Moisture Learning enabled' else summary = 'Moisture Learning disabled' - + int zone = 1 createDPWMap() - while(zone <= 16) { - if (nozzle(zone) == 4) summary = "${summary}\n${zone}: ${settings."zone${zone}"}" - else if ( (initDPW(zone) != 0) && zoneActive(zone.toString())) summary = "${summary}\n${getaZoneSummary(zone)}" - zone++ + while(zone <= 16) { + if (nozzle(zone) == 4) summary = "${summary}\n${zone}: ${settings."zone${zone}"}" + else if ( (initDPW(zone) != 0) && zoneActive(zone.toString())) summary = "${summary}\n${getaZoneSummary(zone)}" + zone++ } - if (summary) return summary else return zoneString() //"Setup all 16 zones" + if (summary) return summary else return zoneString() //"Setup all 16 zones" } - + private String display(String i){ - //log.trace "display(${i})" - String displayString = '' + //log.trace "display(${i})" + String displayString = '' int tpw = initTPW(i.toInteger()) int dpw = initDPW(i.toInteger()) int runTime = calcRunTime(tpw, dpw) - if (settings."zone${i}") displayString += settings."zone${i}" + ' : ' - if (settings."plant${i}") displayString += settings."plant${i}" + ' : ' - if (settings."option${i}") displayString += settings."option${i}" + ' : ' + if (settings."zone${i}") displayString += settings."zone${i}" + ' : ' + if (settings."plant${i}") displayString += settings."plant${i}" + ' : ' + if (settings."option${i}") displayString += settings."option${i}" + ' : ' int j = i.toInteger() if (settings."sensor${i}") { - displayString += settings."sensor${i}" + displayString += settings."sensor${i}" displayString += "=${getDrySp(j)}% : " } if ((runTime != 0) && (dpw != 0)) displayString = "${displayString}${runTime} minutes, ${dpw} days per week" @@ -770,18 +740,18 @@ private String display(String i){ } private String getimage(String image){ - String imageStr = image - if (image.isNumber()) { - String zoneStr = settings."zone${image}" - if (zoneStr) { - if (zoneStr == 'Off') return 'http://www.plaidsystems.com/smartthings/off2.png' - if (zoneStr == 'Master Valve') return 'http://www.plaidsystems.com/smartthings/master.png' - if (zoneStr == 'Pump') return 'http://www.plaidsystems.com/smartthings/pump.png' - - if (settings."plant${image}") imageStr = settings."plant${image}" // default assume asking for the plant image - } - } - // OK, lookup the requested image + String imageStr = image + if (image.isNumber()) { + String zoneStr = settings."zone${image}" + if (zoneStr) { + if (zoneStr == 'Off') return 'http://www.plaidsystems.com/smartthings/off2.png' + if (zoneStr == 'Master Valve') return 'http://www.plaidsystems.com/smartthings/master.png' + if (zoneStr == 'Pump') return 'http://www.plaidsystems.com/smartthings/pump.png' + + if (settings."plant${image}") imageStr = settings."plant${image}" // default assume asking for the plant image + } + } + // OK, lookup the requested image switch (imageStr) { case "null": case null: @@ -825,256 +795,255 @@ private String getimage(String image){ case "Cycle 3x": return 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png' default: - return 'http://www.plaidsystems.com/smartthings/off2.png' + return 'http://www.plaidsystems.com/smartthings/off2.png' } } - -private String getname(String i) { + +private String getname(String i) { if (settings."name${i}") return settings."name${i}" else return "Zone ${i}" } private String zipString() { if (!settings.zipcode) return "${location.zipCode}" - //add pws for correct weatherunderground lookup if (!settings.zipcode.isNumber()) return "pws:${settings.zipcode}" else return settings.zipcode } - + //app install def installed() { - state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - state.Rain = [0,0,0,0,0,0,0] - state.daycount = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - atomicState.run = false // must be atomic - used to recover from crashes - state.pauseTime = null - atomicState.startTime = null - atomicState.finishTime = null // must be atomic - used to recover from crashes - + state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + state.Rain = [0,0,0,0,0,0,0] + state.daycount = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + atomicState.run = false // must be atomic - used to recover from crashes + state.pauseTime = null + atomicState.startTime = null + atomicState.finishTime = null // must be atomic - used to recover from crashes + log.debug "Installed with settings: ${settings}" installSchedule() } - -def updated() { + +def updated() { log.debug "Updated with settings: ${settings}" - installSchedule() + installSchedule() } - + def installSchedule(){ - if (!state.seasonAdj) state.seasonAdj = 100.0 - if (!state.weekseasonAdj) state.weekseasonAdj = 0 - if (state.daysAvailable != 0) state.daysAvailable = 0 // force daysAvailable to be initialized by daysAvailable() - state.daysAvailable = daysAvailable() // every time we save the schedule + if (!state.seasonAdj) state.seasonAdj = 100.0 + if (!state.weekseasonAdj) state.weekseasonAdj = 0 + if (state.daysAvailable != 0) state.daysAvailable = 0 // force daysAvailable to be initialized by daysAvailable() + state.daysAvailable = daysAvailable() // every time we save the schedule if (atomicState.run) { - attemptRecovery() // clean up if we crashed earlier + attemptRecovery() // clean up if we crashed earlier } else { - unsubscribe() //added back in to reset manual subscription + unsubscribe() //added back in to reset manual subscription resetEverything() } - subscribe(app, appTouch) // enable the "play" button for this schedule + subscribe(app, appTouch) // enable the "play" button for this schedule Random rand = new Random() long randomOffset = 0 - - // always collect rainfall + + // always collect rainfall int randomSeconds = rand.nextInt(59) - if (settings.isRain || settings.isSeason) schedule("${randomSeconds} 57 23 1/1 * ? *", getRainToday) // capture today's rainfall just before midnight + if (settings.isRain || settings.isSeason) schedule("${randomSeconds} 57 23 1/1 * ? *", getRainToday) // capture today's rainfall just before midnight if (settings.switches && settings.startTime && settings.enable){ - randomOffset = rand.nextInt(60000) + 20000 + randomOffset = rand.nextInt(60000) + 20000 def checktime = timeToday(settings.startTime, location.timeZone).getTime() + randomOffset //log.debug "randomOffset ${randomOffset} checktime ${checktime}" - schedule(checktime, preCheck) //check weather & Days + schedule(checktime, preCheck) //check weather & Days writeSettings() note('schedule', "${app.label}: Starts at ${startTimeString()}", 'i') } - else { - unschedule( preCheck ) - note('disable', "${app.label}: Automatic watering disabled or setup is incomplete", 'a') - } + else { + unschedule( preCheck ) + note('disable', "${app.label}: Automatic watering disabled or setup is incomplete", 'a') + } } // Called to find and repair after crashes - called by installSchedule() and busy() private boolean attemptRecovery() { - if (!atomicState.run) { - return false // only clean up if we think we are still running - } - else { // Hmmm...seems we were running before... - def csw = settings.switches.currentSwitch - def cst = settings.switches.currentStatus - switch (csw) { - case 'on': // looks like this schedule is running the controller at the moment - if (!atomicState.startTime) { // cycleLoop cleared the startTime, but cycleOn() didn't set it - log.debug "${app.label}: crashed in cycleLoop(), cycleOn() never started, cst is ${cst} - resetting" - resetEverything() // reset and try again...it's probably not us running the controller, though - return false - } - // We have a startTime... - if (!atomicState.finishTime) { // started, but we don't think we're done yet..so it's probably us! - runIn(15, cycleOn) // goose the cycle, just in case - note('active', "${app.label}: schedule is apparently already running", 'i') - return true - } - - // hmmm...switch is on and we think we're finished...probably somebody else is running...let busy figure it out - resetEverything() - return false - break - - case 'off': // switch is off - did we finish? - if (atomicState.finishTime) { // off and finished, let's just reset things - resetEverything() - return false - } - - if (switches.currentStatus != 'pause') { // off and not paused - probably another schedule, let's clean up - resetEverything() - return false - } - - // off and not finished, and paused, we apparently crashed while paused - runIn(15, cycleOn) - return true - break - - case 'programOn': // died while manual program running? - case 'programWait': // looks like died previously before we got started, let's try to clean things up - resetEverything() - if (atomicState.finishTime) atomicState.finishTime = null - if ((cst == 'active') || atomicState.startTime) { // if we announced we were in preCheck, or made it all the way to cycleOn before it crashed - settings.switches.programOff() // only if we think we actually started (cycleOn() started) - // probably kills manual cycles too, but we'll let that go for now - } - if (atomicState.startTime) atomicState.startTime = null - note ('schedule', "Looks like ${app.label} crashed recently...cleaning up", c) - return false - break - - default: - log.debug "attemptRecovery(): atomicState.run == true, and I've nothing left to do" - return true - } - } + if (!atomicState.run) { + return false // only clean up if we think we are still running + } + else { // Hmmm...seems we were running before... + def csw = settings.switches.currentSwitch + def cst = settings.switches.currentStatus + switch (csw) { + case 'on': // looks like this schedule is running the controller at the moment + if (!atomicState.startTime) { // cycleLoop cleared the startTime, but cycleOn() didn't set it + log.debug "${app.label}: crashed in cycleLoop(), cycleOn() never started, cst is ${cst} - resetting" + resetEverything() // reset and try again...it's probably not us running the controller, though + return false + } + // We have a startTime... + if (!atomicState.finishTime) { // started, but we don't think we're done yet..so it's probably us! + runIn(15, cycleOn) // goose the cycle, just in case + note('active', "${app.label}: schedule is apparently already running", 'i') + return true + } + + // hmmm...switch is on and we think we're finished...probably somebody else is running...let busy figure it out + resetEverything() + return false + break + + case 'off': // switch is off - did we finish? + if (atomicState.finishTime) { // off and finished, let's just reset things + resetEverything() + return false + } + + if (switches.currentStatus != 'pause') { // off and not paused - probably another schedule, let's clean up + resetEverything() + return false + } + + // off and not finished, and paused, we apparently crashed while paused + runIn(15, cycleOn) + return true + break + + case 'programOn': // died while manual program running? + case 'programWait': // looks like died previously before we got started, let's try to clean things up + resetEverything() + if (atomicState.finishTime) atomicState.finishTime = null + if ((cst == 'active') || atomicState.startTime) { // if we announced we were in preCheck, or made it all the way to cycleOn before it crashed + settings.switches.programOff() // only if we think we actually started (cycleOn() started) + // probably kills manual cycles too, but we'll let that go for now + } + if (atomicState.startTime) atomicState.startTime = null + note ('schedule', "Looks like ${app.label} crashed recently...cleaning up", c) + return false + break + + default: + log.debug "attemptRecovery(): atomicState.run == true, and I've nothing left to do" + return true + } + } } // reset everything to the initial (not running) state private def resetEverything() { - if (atomicState.run) atomicState.run = false // we're not running the controller any more - unsubAllBut() // release manual, switches, sync, contacts & toggles - - // take care not to unschedule preCheck() or getRainToday() - unschedule(cycleOn) - unschedule(checkRunMap) - unschedule(writeCycles) - unschedule(subOff) + if (atomicState.run) atomicState.run = false // we're not running the controller any more + unsubAllBut() // release manual, switches, sync, contacts & toggles - if (settings.enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) + // take care not to unschedule preCheck() or getRainToday() + unschedule(cycleOn) + unschedule(checkRunMap) + unschedule(writeCycles) + unschedule(subOff) + + if (settings.enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) } // unsubscribe from ALL events EXCEPT app.touch private def unsubAllBut() { - unsubscribe(settings.switches) - unsubWaterStoppers() - if (settings.sync) unsubscribe(settings.sync) + unsubscribe(settings.switches) + unsubWaterStoppers() + if (settings.sync) unsubscribe(settings.sync) } // enable the "Play" button in SmartApp list def appTouch(evt) { - log.debug "appTouch(): atomicState.run = ${atomicState.run}" + log.debug "appTouch(): atomicState.run = ${atomicState.run}" - runIn(2, preCheck) // run it off a schedule, so we can see how long it takes in the app.state + runIn(2, preCheck) // run it off a schedule, so we can see how long it takes in the app.state } // true if one of the stoppers is in Stop state private boolean isWaterStopped() { - if (settings.contacts && settings.contacts.currentContact.contains(settings.contactStop)) return true + if (settings.contacts && settings.contacts.currentContact.contains(settings.contactStop)) return true - if (settings.toggles && settings.toggles.currentSwitch.contains(settings.toggleStop)) return true + if (settings.toggles && settings.toggles.currentSwitch.contains(settings.toggleStop)) return true - return false + return false } // watch for water stoppers private def subWaterStop() { - if (settings.contacts) { - unsubscribe(settings.contacts) - subscribe(settings.contacts, "contact.${settings.contactStop}", waterStop) - } - if (settings.toggles) { - unsubscribe(settings.toggles) - subscribe(settings.toggles, "switch.${settings.toggleStop}", waterStop) - } + if (settings.contacts) { + unsubscribe(settings.contacts) + subscribe(settings.contacts, "contact.${settings.contactStop}", waterStop) + } + if (settings.toggles) { + unsubscribe(settings.toggles) + subscribe(settings.toggles, "switch.${settings.toggleStop}", waterStop) + } } // watch for water starters private def subWaterStart() { - if (settings.contacts) { - unsubscribe(settings.contacts) - def cond = (settings.contactStop == 'open') ? 'closed' : 'open' - subscribe(settings.contacts, "contact.${cond}", waterStart) - } - if (settings.toggles) { - unsubscribe(settings.toggles) - def cond = (settings.toggleStop == 'on') ? 'off' : 'on' - subscribe(settings.toggles, "switch.${cond}", waterStart) - } + if (settings.contacts) { + unsubscribe(settings.contacts) + def cond = (settings.contactStop == 'open') ? 'closed' : 'open' + subscribe(settings.contacts, "contact.${cond}", waterStart) + } + if (settings.toggles) { + unsubscribe(settings.toggles) + def cond = (settings.toggleStop == 'on') ? 'off' : 'on' + subscribe(settings.toggles, "switch.${cond}", waterStart) + } } // stop watching water stoppers and starters private def unsubWaterStoppers() { - if (settings.contacts) unsubscribe(settings.contacts) - if (settings.toggles) unsubscribe(settings.toggles) + if (settings.contacts) unsubscribe(settings.contacts) + if (settings.toggles) unsubscribe(settings.toggles) } // which of the stoppers are in stop mode? private String getWaterStopList() { - String deviceList = '' - int i = 1 - if (settings.contacts) { - settings.contacts.each { - if (it.currentContact == settings.contactStop) { - if (i > 1) deviceList += ', ' - deviceList = "${deviceList}${it.displayName} is ${settings.contactStop}" - i++ - } - } - } - if (settings.toggles) { - settings.toggles.each { - if (it.currentSwitch == settings.toggleStop) { - if (i > 1) deviceList += ', ' - deviceList = "${deviceList}${it.displayName} is ${settings.toggleStop}" - i++ - } - } - } - return deviceList + String deviceList = '' + int i = 1 + if (settings.contacts) { + settings.contacts.each { + if (it.currentContact == settings.contactStop) { + if (i > 1) deviceList += ', ' + deviceList = "${deviceList}${it.displayName} is ${settings.contactStop}" + i++ + } + } + } + if (settings.toggles) { + settings.toggles.each { + if (it.currentSwitch == settings.toggleStop) { + if (i > 1) deviceList += ', ' + deviceList = "${deviceList}${it.displayName} is ${settings.toggleStop}" + i++ + } + } + } + return deviceList } //write initial zone settings to device at install/update -def writeSettings(){ - if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - if (!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - if (state.setMoisture) state.setMoisture = null // not using any more - if (!state.seasonAdj) state.seasonAdj = 100.0 - if (!state.weekseasonAdj) state.weekseasonAdj = 0 - setSeason() +def writeSettings(){ + if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + if (!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + if (state.setMoisture) state.setMoisture = null // not using any more + if (!state.seasonAdj) state.seasonAdj = 100.0 + if (!state.weekseasonAdj) state.weekseasonAdj = 0 + setSeason() } //get day of week integer int getWeekDay(day) { - def weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - def mapDay = [Monday:1, Tuesday:2, Wednesday:3, Thursday:4, Friday:5, Saturday:6, Sunday:7] - if(day && weekdays.contains(day)) { - return mapDay.get(day).toInteger() + def weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + def mapDay = [Monday:1, Tuesday:2, Wednesday:3, Thursday:4, Friday:5, Saturday:6, Sunday:7] + if(day && weekdays.contains(day)) { + return mapDay.get(day).toInteger() } - def today = new Date().format('EEEE', location.timeZone) - return mapDay.get(today).toInteger() + def today = new Date().format('EEEE', location.timeZone) + return mapDay.get(today).toInteger() } // Get string of run days from dpwMap @@ -1094,28 +1063,28 @@ private String getRunDays(day1,day2,day3,day4,day5,day6,day7) //start manual schedule def manualStart(evt){ - boolean running = attemptRecovery() // clean up if prior run crashed - - if (settings.enableManual && !running && (settings.switches.currentStatus != 'pause')){ - if (settings.sync && ( (settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') ) { - note('skipping', "${app.label}: Manual run aborted, ${settings.sync.displayName} appears to be busy", 'a') - } - else { + boolean running = attemptRecovery() // clean up if prior run crashed + + if (settings.enableManual && !running && (settings.switches.currentStatus != 'pause')){ + if (settings.sync && ( (settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') ) { + note('skipping', "${app.label}: Manual run aborted, ${settings.sync.displayName} appears to be busy", 'a') + } + else { def runNowMap = [] - runNowMap = cycleLoop(0) + runNowMap = cycleLoop(0) - if (runNowMap) { + if (runNowMap) { atomicState.run = true settings.switches.programWait() subscribe(settings.switches, 'switch.off', cycleOff) - runIn(60, cycleOn) // start water program + runIn(60, cycleOn) // start water program // note that manual DOES abide by waterStoppers (if configured) String newString = '' int tt = state.totalTime if (tt) { - int hours = tt / 60 // DON'T Math.round this one + int hours = tt / 60 // DON'T Math.round this one int mins = tt - (hours * 60) String hourString = '' String s = '' @@ -1126,57 +1095,57 @@ def manualStart(evt){ newString = "run time: ${hourString}${mins} minute${s}:\n" } - note('active', "${app.label}: Manual run, watering in 1 minute: ${newString}${runNowMap}", 'd') + note('active', "${app.label}: Manual run, watering in 1 minute: ${newString}${runNowMap}", 'd') } else note('skipping', "${app.label}: Manual run failed, check configuration", 'a') - } - } + } + } else note('skipping', "${app.label}: Manual run aborted, ${settings.switches.displayName} appears to be busy", 'a') } //true if another schedule is running boolean busy(){ - // Check if we are already running, crashed or somebody changed the schedule time while this schedule is running + // Check if we are already running, crashed or somebody changed the schedule time while this schedule is running if (atomicState.run){ - if (!attemptRecovery()) { // recovery will clean out any prior crashes and correct state of atomicState.run - return false // (atomicState.run = false) - } - else { - // don't change the current status, in case the currently running schedule is in off/paused mode - note(settings.switches.currentStatus, "${app.label}: Already running, skipping additional start", 'i') - return true - } + if (!attemptRecovery()) { // recovery will clean out any prior crashes and correct state of atomicState.run + return false // (atomicState.run = false) + } + else { + // don't change the current status, in case the currently running schedule is in off/paused mode + note(settings.switches.currentStatus, "${app.label}: Already running, skipping additional start", 'i') + return true + } } // Not already running... - + // Moved from cycleOn() - don't even start pre-check until the other controller completes its cycle if (settings.sync) { - if ((settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') { + if ((settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') { subscribe(settings.sync, 'switch.off', syncOn) note('delayed', "${app.label}: Waiting for ${settings.sync.displayName} to complete before starting", 'c') return true } } - + // Check that the controller isn't paused while running some other schedule def csw = settings.switches.currentSwitch def cst = settings.switches.currentStatus - if ((csw == 'off') && (cst != 'pause')) { // off && !paused: controller is NOT in use - log.debug "switches ${csw}, status ${cst} (1st)" - resetEverything() // get back to the start state - return false - } - - if (isDay()) { // Yup, we need to run today, so wait for the other schedule to finish - log.debug "switches ${csw}, status ${cst} (3rd)" - resetEverything() - subscribe(settings.switches, 'switch.off', busyOff) - note('delayed', "${app.label}: Waiting for currently running schedule to complete before starting", 'c') - return true - } - + if ((csw == 'off') && (cst != 'pause')) { // off && !paused: controller is NOT in use + log.debug "switches ${csw}, status ${cst} (1st)" + resetEverything() // get back to the start state + return false + } + + if (isDay()) { // Yup, we need to run today, so wait for the other schedule to finish + log.debug "switches ${csw}, status ${cst} (3rd)" + resetEverything() + subscribe(settings.switches, 'switch.off', busyOff) + note('delayed', "${app.label}: Waiting for currently running schedule to complete before starting", 'c') + return true + } + // Somthing is running, but we don't need to run today anyway - don't need to do busyOff() // (Probably should never get here, because preCheck() should check isDay() before calling busy() log.debug "Another schedule is running, but ${app.label} is not scheduled for today anyway" @@ -1184,83 +1153,83 @@ boolean busy(){ } def busyOff(evt){ - def cst = settings.switches.currentStatus - if ((settings.switches.currentSwitch == 'off') && (cst != 'pause')) { // double check that prior schedule is done - unsubscribe(switches) // we don't want any more button pushes until preCheck runs - Random rand = new Random() // just in case there are multiple schedules waiting on the same controller - int randomSeconds = rand.nextInt(120) + 15 - runIn(randomSeconds, preCheck) // no message so we don't clog the system - note('active', "${app.label}: ${settings.switches} finished, starting in ${randomSeconds} seconds", 'i') - } + def cst = settings.switches.currentStatus + if ((settings.switches.currentSwitch == 'off') && (cst != 'pause')) { // double check that prior schedule is done + unsubscribe(switches) // we don't want any more button pushes until preCheck runs + Random rand = new Random() // just in case there are multiple schedules waiting on the same controller + int randomSeconds = rand.nextInt(120) + 15 + runIn(randomSeconds, preCheck) // no message so we don't clog the system + note('active', "${app.label}: ${settings.switches} finished, starting in ${randomSeconds} seconds", 'i') + } } //run check every day def preCheck() { if (!isDay()) { - log.debug "preCheck() Skipping: ${app.label} is not scheduled for today" // silent - no note - //if (!atomicState.run && enableManual) subscribe(switches, 'switch.programOn', manualStart) // only if we aren't running already - return - } - - if (!busy()) { - atomicState.run = true // set true before doing anything, atomic in case we crash (busy() set it false if !busy) - settings.switches.programWait() // take over the controller so other schedules don't mess with us - runIn(45, checkRunMap) // schedule checkRunMap() before doing weather check, gives isWeather 45s to complete - // because that seems to be a little more than the max that the ST platform allows - unsubAllBut() // unsubscribe to everything except appTouch() - subscribe(settings.switches, 'switch.off', cycleOff) // and start setting up for today's cycle - def start = now() - note('active', "${app.label}: Starting...", 'd') // - def end = now() - log.debug "preCheck note active ${end - start}ms" - - if (isWeather()) { // set adjustments and check if we shold skip because of rain - resetEverything() // if so, clean up our subscriptions - switches.programOff() // and release the controller - } - else { - log.debug 'preCheck(): running checkRunMap in 2 seconds' //COOL! We finished before timing out, and we're supposed to water today - runIn(2, checkRunMap) // jack the schedule so it runs sooner! - } - } + log.debug "preCheck() Skipping: ${app.label} is not scheduled for today" // silent - no note + //if (!atomicState.run && enableManual) subscribe(switches, 'switch.programOn', manualStart) // only if we aren't running already + return + } + + if (!busy()) { + atomicState.run = true // set true before doing anything, atomic in case we crash (busy() set it false if !busy) + settings.switches.programWait() // take over the controller so other schedules don't mess with us + runIn(45, checkRunMap) // schedule checkRunMap() before doing weather check, gives isWeather 45s to complete + // because that seems to be a little more than the max that the ST platform allows + unsubAllBut() // unsubscribe to everything except appTouch() + subscribe(settings.switches, 'switch.off', cycleOff) // and start setting up for today's cycle + def start = now() + note('active', "${app.label}: Starting...", 'd') // + def end = now() + log.debug "preCheck note active ${end - start}ms" + + if (isWeather()) { // set adjustments and check if we shold skip because of rain + resetEverything() // if so, clean up our subscriptions + switches.programOff() // and release the controller + } + else { + log.debug 'preCheck(): running checkRunMap in 2 seconds' //COOL! We finished before timing out, and we're supposed to water today + runIn(2, checkRunMap) // jack the schedule so it runs sooner! + } + } } //start water program -def cycleOn(){ - if (atomicState.run) { // block if manually stopped during precheck which goes to cycleOff +def cycleOn(){ + if (atomicState.run) { // block if manually stopped during precheck which goes to cycleOff - if (!isWaterStopped()) { // make sure ALL the contacts and toggles aren't paused + if (!isWaterStopped()) { // make sure ALL the contacts and toggles aren't paused // All clear, let's start running! subscribe(settings.switches, 'switch.off', cycleOff) - subWaterStop() // subscribe to all the pause contacts and toggles + subWaterStop() // subscribe to all the pause contacts and toggles resume() - + // send the notification AFTER we start the controller (in case note() causes us to run over our execution time limit) String newString = "${app.label}: Starting..." if (!atomicState.startTime) { - atomicState.startTime = now() // if we haven't already started - if (atomicState.startTime) atomicState.finishTime = null // so recovery in busy() knows we didn't finish - if (state.pauseTime) state.pauseTime = null - if (state.totalTime) { - String finishTime = new Date(now() + (60000 * state.totalTime).toLong()).format('EE @ h:mm a', location.timeZone) - newString = "${app.label}: Starting - ETC: ${finishTime}" - } - } - else if (state.pauseTime) { // resuming after a pause - - def elapsedTime = Math.round((now() - state.pauseTime) / 60000) // convert ms to minutes - int tt = state.totalTime + elapsedTime + 1 - state.totalTime = tt // keep track of the pauses, and the 1 minute delay above - String finishTime = new Date(atomicState.startTime + (60000 * tt).toLong()).format('EE @ h:mm a', location.timeZone) - state.pauseTime = null - newString = "${app.label}: Resuming - New ETC: ${finishTime}" + atomicState.startTime = now() // if we haven't already started + if (atomicState.startTime) atomicState.finishTime = null // so recovery in busy() knows we didn't finish + if (state.pauseTime) state.pauseTime = null + if (state.totalTime) { + String finishTime = new Date(now() + (60000 * state.totalTime).toLong()).format('EE @ h:mm a', location.timeZone) + newString = "${app.label}: Starting - ETC: ${finishTime}" + } + } + else if (state.pauseTime) { // resuming after a pause + + def elapsedTime = Math.round((now() - state.pauseTime) / 60000) // convert ms to minutes + int tt = state.totalTime + elapsedTime + 1 + state.totalTime = tt // keep track of the pauses, and the 1 minute delay above + String finishTime = new Date(atomicState.startTime + (60000 * tt).toLong()).format('EE @ h:mm a', location.timeZone) + state.pauseTime = null + newString = "${app.label}: Resuming - New ETC: ${finishTime}" } note('active', newString, 'd') } else { // Ready to run, but one of the control contacts is still open, so we wait - subWaterStart() // one of them is paused, let's wait until the are all clear! + subWaterStart() // one of them is paused, let's wait until the are all clear! note('pause', "${app.label}: Watering paused, ${getWaterStopList()}", 'c') } } @@ -1270,38 +1239,38 @@ def cycleOn(){ def cycleOff(evt){ if (atomicState.run) { - def ft = new Date() - atomicState.finishTime = ft // this is important to reset the schedule after failures in busy() - String finishTime = ft.format('h:mm a', location.timeZone) - note('finished', "${app.label}: Finished watering at ${finishTime}", 'd') - } + def ft = new Date() + atomicState.finishTime = ft // this is important to reset the schedule after failures in busy() + String finishTime = ft.format('h:mm a', location.timeZone) + note('finished', "${app.label}: Finished watering at ${finishTime}", 'd') + } else { - log.debug "${settings.switches} turned off" // is this a manual off? perhaps we should send a note? + log.debug "${settings.switches} turned off" // is this a manual off? perhaps we should send a note? } - resetEverything() // all done here, back to starting state + resetEverything() // all done here, back to starting state } //run check each day at scheduled time def checkRunMap(){ - - //check if isWeather returned true or false before checking + + //check if isWeather returned true or false before checking if (atomicState.run) { //get & set watering times for today - def runNowMap = [] - runNowMap = cycleLoop(1) // build the map - - if (runNowMap) { - runIn(60, cycleOn) // start water - subscribe(settings.switches, 'switch.off', cycleOff) // allow manual off before cycleOn() starts - if (atomicState.startTime) atomicState.startTime = null // these were already cleared in cycleLoop() above - if (state.pauseTime) state.pauseTime = null // ditto + def runNowMap = [] + runNowMap = cycleLoop(1) // build the map + + if (runNowMap) { + runIn(60, cycleOn) // start water + subscribe(settings.switches, 'switch.off', cycleOff) // allow manual off before cycleOn() starts + if (atomicState.startTime) atomicState.startTime = null // these were already cleared in cycleLoop() above + if (state.pauseTime) state.pauseTime = null // ditto // leave atomicState.finishTime alone so that recovery in busy() knows we never started if cycleOn() doesn't clear it - + String newString = '' int tt = state.totalTime if (tt) { - int hours = tt / 60 // DON'T Math.round this one + int hours = tt / 60 // DON'T Math.round this one int mins = tt - (hours * 60) String hourString = '' String s = '' @@ -1319,20 +1288,20 @@ def checkRunMap(){ switches.programOff() if (enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) note('skipping', "${app.label}: No watering today", 'd') - if (atomicState.run) atomicState.run = false // do this last, so that the above note gets sent to the controller + if (atomicState.run) atomicState.run = false // do this last, so that the above note gets sent to the controller } - } + } else { - log.debug 'checkRunMap(): atomicState.run = false' // isWeather cancelled us out before we got started + log.debug 'checkRunMap(): atomicState.run = false' // isWeather cancelled us out before we got started } } //get todays schedule def cycleLoop(int i) { - boolean isDebug = false - if (isDebug) log.debug "cycleLoop(${i})" - + boolean isDebug = false + if (isDebug) log.debug "cycleLoop(${i})" + int zone = 1 int dpw = 0 int tpw = 0 @@ -1344,7 +1313,7 @@ def cycleLoop(int i) String soilString = '' int totalCycles = 0 int totalTime = 0 - if (atomicState.startTime) atomicState.startTime = null // haven't started yet + if (atomicState.startTime) atomicState.startTime = null // haven't started yet while(zone <= 16) { @@ -1352,111 +1321,111 @@ def cycleLoop(int i) def setZ = settings."zone${zone}" if ((setZ && (setZ != 'Off')) && (nozzle(zone) != 4) && zoneActive(zone.toString())) { - // First check if we run this zone today, use either dpwMap or even/odd date - dpw = getDPW(zone) - int runToday = 0 - // if manual, or every day allowed, or zone uses a sensor, then we assume we can today - // - preCheck() has already verified that today isDay() - if ((i == 0) || (state.daysAvailable == 7) || (settings."sensor${zone}")) { - runToday = 1 - } - else { - - dpw = getDPW(zone) // figure out if we need to run (if we don't already know we do) - if (settings.days && (settings.days.contains('Even') || settings.days.contains('Odd'))) { - def daynum = new Date().format('dd', location.timeZone) - int dayint = Integer.parseInt(daynum) - if (settings.days.contains('Odd') && (((dayint +1) % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 - else if (settings.days.contains('Even') && ((dayint % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 - } - else { - int weekDay = getWeekDay()-1 - def dpwMap = getDPWDays(dpw) - runToday = dpwMap[weekDay] //1 or 0 - if (isDebug) log.debug "Zone: ${zone} dpw: ${dpw} weekDay: ${weekDay} dpwMap: ${dpwMap} runToday: ${runToday}" - - } - } - - // OK, we're supposed to run (or at least adjust the sensors) - if (runToday == 1) - { - def soil - if (i == 0) soil = moisture(0) // manual - else soil = moisture(zone) // moisture check - soilString = "${soilString}${soil[1]}" - - // Run this zone if soil moisture needed - if ( soil[0] == 1 ) - { - cyc = cycles(zone) - tpw = getTPW(zone) - dpw = getDPW(zone) // moisture() may have changed DPW - - rtime = calcRunTime(tpw, dpw) - //daily weather adjust if no sensor - if(settings.isSeason && (!settings.learn || !settings."sensor${zone}")) { - - - rtime = Math.round(((rtime / cyc) * (state.seasonAdj / 100.0)) + 0.4) - } - else { - rtime = Math.round((rtime / cyc) + 0.4) // let moisture handle the seasonAdjust for Adaptive (learn) zones - } - totalCycles += cyc - totalTime += (rtime * cyc) - runNowMap += "${settings."name${zone}"}: ${cyc} x ${rtime} min\n" - if (isDebug) log.debug "Zone ${zone} Map: ${cyc} x ${rtime} min - totalTime: ${totalTime}" - } - } - } + // First check if we run this zone today, use either dpwMap or even/odd date + dpw = getDPW(zone) + int runToday = 0 + // if manual, or every day allowed, or zone uses a sensor, then we assume we can today + // - preCheck() has already verified that today isDay() + if ((i == 0) || (state.daysAvailable == 7) || (settings."sensor${zone}")) { + runToday = 1 + } + else { + + dpw = getDPW(zone) // figure out if we need to run (if we don't already know we do) + if (settings.days && (settings.days.contains('Even') || settings.days.contains('Odd'))) { + def daynum = new Date().format('dd', location.timeZone) + int dayint = Integer.parseInt(daynum) + if (settings.days.contains('Odd') && (((dayint +1) % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 + else if (settings.days.contains('Even') && ((dayint % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 + } + else { + int weekDay = getWeekDay()-1 + def dpwMap = getDPWDays(dpw) + runToday = dpwMap[weekDay] //1 or 0 + if (isDebug) log.debug "Zone: ${zone} dpw: ${dpw} weekDay: ${weekDay} dpwMap: ${dpwMap} runToday: ${runToday}" + + } + } + + // OK, we're supposed to run (or at least adjust the sensors) + if (runToday == 1) + { + def soil + if (i == 0) soil = moisture(0) // manual + else soil = moisture(zone) // moisture check + soilString = "${soilString}${soil[1]}" + + // Run this zone if soil moisture needed + if ( soil[0] == 1 ) + { + cyc = cycles(zone) + tpw = getTPW(zone) + dpw = getDPW(zone) // moisture() may have changed DPW + + rtime = calcRunTime(tpw, dpw) + //daily weather adjust if no sensor + if(settings.isSeason && (!settings.learn || !settings."sensor${zone}")) { + + + rtime = Math.round(((rtime / cyc) * (state.seasonAdj / 100.0)) + 0.4) + } + else { + rtime = Math.round((rtime / cyc) + 0.4) // let moisture handle the seasonAdjust for Adaptive (learn) zones + } + totalCycles += cyc + totalTime += (rtime * cyc) + runNowMap += "${settings."name${zone}"}: ${cyc} x ${rtime} min\n" + if (isDebug) log.debug "Zone ${zone} Map: ${cyc} x ${rtime} min - totalTime: ${totalTime}" + } + } + } if (nozzle(zone) == 4) pumpMap += "${settings."name${zone}"}: ${settings."zone${zone}"} on\n" timeMap."${zone+1}" = "${rtime}" - zone++ - } - - if (soilString) { - String seasonStr = '' - String plus = '' - float sa = state.seasonAdj - if (settings.isSeason && (sa != 100.0) && (sa != 0.0)) { - float sadj = sa - 100.0 - if (sadj > 0.0) plus = '+' //display once in cycleLoop() - int iadj = Math.round(sadj) - if (iadj != 0) seasonStr = "Adjusting ${plus}${iadj}% for weather forecast\n" - } + zone++ + } + + if (soilString) { + String seasonStr = '' + String plus = '' + float sa = state.seasonAdj + if (settings.isSeason && (sa != 100.0) && (sa != 0.0)) { + float sadj = sa - 100.0 + if (sadj > 0.0) plus = '+' //display once in cycleLoop() + int iadj = Math.round(sadj) + if (iadj != 0) seasonStr = "Adjusting ${plus}${iadj}% for weather forecast\n" + } note('moisture', "${app.label} Sensor status:\n${seasonStr}${soilString}" /* + seasonStr + soilString */,'m') } if (!runNowMap) { - return runNowMap // nothing to run today + return runNowMap // nothing to run today } //send settings to Spruce Controller switches.settingsMap(timeMap,4002) - runIn(30, writeCycles) - - // meanwhile, calculate our total run time + runIn(30, writeCycles) + + // meanwhile, calculate our total run time int pDelay = 0 if (settings.pumpDelay && settings.pumpDelay.isNumber()) pDelay = settings.pumpDelay.toInteger() totalTime += Math.round(((pDelay * (totalCycles-1)) / 60.0)) // add in the pump startup and inter-zone delays state.totalTime = totalTime - if (state.pauseTime) state.pauseTime = null // and we haven't paused yet - // but let cycleOn() reset finishTime - return (runNowMap + pumpMap) + if (state.pauseTime) state.pauseTime = null // and we haven't paused yet + // but let cycleOn() reset finishTime + return (runNowMap + pumpMap) } //send cycle settings def writeCycles(){ - //log.trace "writeCycles()" - def cyclesMap = [:] + //log.trace "writeCycles()" + def cyclesMap = [:] //add pumpdelay @ 1 cyclesMap."1" = pumpDelayString() int zone = 1 - int cycle = 0 + int cycle = 0 while(zone <= 17) - { + { if(nozzle(zone) == 4) cycle = 4 else cycle = cycles(zone) //offset by 1, due to pumpdelay @ 1 @@ -1467,195 +1436,195 @@ def writeCycles(){ } def resume(){ - log.debug 'resume()' - settings.switches.zon() + log.debug 'resume()' + settings.switches.zon() } def syncOn(evt){ - // double check that the switch is actually finished and not just paused - if ((settings.sync.currentSwitch == 'off') && (settings.sync.currentStatus != 'pause')) { - resetEverything() // back to our known state - Random rand = new Random() // just in case there are multiple schedules waiting on the same controller - int randomSeconds = rand.nextInt(120) + 15 - runIn(randomSeconds, preCheck) // no message so we don't clog the system - note('schedule', "${app.label}: ${settings.sync} finished, starting in ${randomSeconds} seconds", 'c') - } // else, it is just pausing...keep waiting for the next "off" + // double check that the switch is actually finished and not just paused + if ((settings.sync.currentSwitch == 'off') && (settings.sync.currentStatus != 'pause')) { + resetEverything() // back to our known state + Random rand = new Random() // just in case there are multiple schedules waiting on the same controller + int randomSeconds = rand.nextInt(120) + 15 + runIn(randomSeconds, preCheck) // no message so we don't clog the system + note('schedule', "${app.label}: ${settings.sync} finished, starting in ${randomSeconds} seconds", 'c') + } // else, it is just pausing...keep waiting for the next "off" } // handle start of pause session def waterStop(evt){ - log.debug "waterStop: ${evt.displayName}" - - unschedule(cycleOn) // in case we got stopped again before cycleOn starts from the restart - unsubscribe(settings.switches) - subWaterStart() - - if (!state.pauseTime) { // only need to do this for the first event if multiple contacts - state.pauseTime = now() - - String cond = evt.value - switch (cond) { - case 'open': - cond = 'opened' - break - case 'on': - cond = 'switched on' - break - case 'off': - cond = 'switched off' - break - //case 'closed': - // cond = 'closed' - // break - case null: - cond = '????' - break - default: - break - } - note('pause', "${app.label}: Watering paused - ${evt.displayName} ${cond}", 'c') // set to Paused - } - if (settings.switches.currentSwitch != 'off') { - runIn(30, subOff) - settings.switches.off() // stop the water - } - else - subscribe(settings.switches, 'switch.off', cycleOff) + log.debug "waterStop: ${evt.displayName}" + + unschedule(cycleOn) // in case we got stopped again before cycleOn starts from the restart + unsubscribe(settings.switches) + subWaterStart() + + if (!state.pauseTime) { // only need to do this for the first event if multiple contacts + state.pauseTime = now() + + String cond = evt.value + switch (cond) { + case 'open': + cond = 'opened' + break + case 'on': + cond = 'switched on' + break + case 'off': + cond = 'switched off' + break + //case 'closed': + // cond = 'closed' + // break + case null: + cond = '????' + break + default: + break + } + note('pause', "${app.label}: Watering paused - ${evt.displayName} ${cond}", 'c') // set to Paused + } + if (settings.switches.currentSwitch != 'off') { + runIn(30, subOff) + settings.switches.off() // stop the water + } + else + subscribe(settings.switches, 'switch.off', cycleOff) } // This is a hack to work around the delay in response from the controller to the above programOff command... // We frequently see the off notification coming a long time after the command is issued, so we try to catch that so that // we don't prematurely exit the cycle. def subOff() { - subscribe(settings.switches, 'switch.off', offPauseCheck) + subscribe(settings.switches, 'switch.off', offPauseCheck) } def offPauseCheck( evt ) { - unsubscribe(settings.switches) - subscribe(settings.switches, 'switch.off', cycleOff) - if (/*(switches.currentSwitch != 'off') && */ (settings.switches.currentStatus != 'pause')) { // eat the first off while paused - cycleOff(evt) - } + unsubscribe(settings.switches) + subscribe(settings.switches, 'switch.off', cycleOff) + if (/*(switches.currentSwitch != 'off') && */ (settings.switches.currentStatus != 'pause')) { // eat the first off while paused + cycleOff(evt) + } } -// handle end of pause session +// handle end of pause session def waterStart(evt){ - if (!isWaterStopped()){ // only if ALL of the selected contacts are not open - def cDelay = 10 + if (!isWaterStopped()){ // only if ALL of the selected contacts are not open + def cDelay = 10 if (settings.contactDelay > 10) cDelay = settings.contactDelay runIn(cDelay, cycleOn) - - unsubscribe(settings.switches) - subWaterStop() // allow stopping again while we wait for cycleOn to start - - log.debug "waterStart(): enabling device is ${evt.device} ${evt.value}" - - String cond = evt.value - switch (cond) { - case 'open': - cond = 'opened' - break - case 'on': - cond = 'switched on' - break - case 'off': - cond = 'switched off' - break - //case 'closed': - // cond = 'closed' - // break - case null: - cond = '????' - break - default: - break - } - // let cycleOn() change the status to Active - keep us paused until then - + + unsubscribe(settings.switches) + subWaterStop() // allow stopping again while we wait for cycleOn to start + + log.debug "waterStart(): enabling device is ${evt.device} ${evt.value}" + + String cond = evt.value + switch (cond) { + case 'open': + cond = 'opened' + break + case 'on': + cond = 'switched on' + break + case 'off': + cond = 'switched off' + break + //case 'closed': + // cond = 'closed' + // break + case null: + cond = '????' + break + default: + break + } + // let cycleOn() change the status to Active - keep us paused until then + note('pause', "${app.label}: ${evt.displayName} ${cond}, watering in ${cDelay} seconds", 'c') - } - else { - log.debug "waterStart(): one down - ${evt.displayName}" - } + } + else { + log.debug "waterStart(): one down - ${evt.displayName}" + } } //Initialize Days per week, based on TPW, perDay and daysAvailable settings int initDPW(int zone){ - //log.debug "initDPW(${zone})" - if(!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - - int tpw = getTPW(zone) // was getTPW -does not update times in scheduler without initTPW - int dpw = 0 - - if(tpw > 0) { + //log.debug "initDPW(${zone})" + if(!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + + int tpw = getTPW(zone) // was getTPW -does not update times in scheduler without initTPW + int dpw = 0 + + if(tpw > 0) { float perDay = 20.0 if(settings."perDay${zone}") perDay = settings."perDay${zone}".toFloat() - - dpw = Math.round(tpw.toFloat() / perDay) - if(dpw <= 1) dpw = 1 - // 3 days per week not allowed for even or odd day selection - if(dpw == 3 && days && (days.contains('Even') || days.contains('Odd')) && !(days.contains('Even') && days.contains('Odd'))) - if((tpw.toFloat() / perDay) < 3.0) dpw = 2 else dpw = 4 - int daycheck = daysAvailable() // initialize & optimize daysAvailable - if (daycheck < dpw) dpw = daycheck - } - state.dpwMap[zone-1] = dpw + + dpw = Math.round(tpw.toFloat() / perDay) + if(dpw <= 1) dpw = 1 + // 3 days per week not allowed for even or odd day selection + if(dpw == 3 && days && (days.contains('Even') || days.contains('Odd')) && !(days.contains('Even') && days.contains('Odd'))) + if((tpw.toFloat() / perDay) < 3.0) dpw = 2 else dpw = 4 + int daycheck = daysAvailable() // initialize & optimize daysAvailable + if (daycheck < dpw) dpw = daycheck + } + state.dpwMap[zone-1] = dpw return dpw } // Get current days per week value, calls init if not defined int getDPW(int zone) { - if (state.dpwMap) return state.dpwMap[zone-1] else return initDPW(zone) + if (state.dpwMap) return state.dpwMap[zone-1] else return initDPW(zone) } //Initialize Time per Week -int initTPW(int zone) { +int initTPW(int zone) { //log.trace "initTPW(${zone})" if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - + int n = nozzle(zone) def zn = settings."zone${zone}" if (!zn || (zn == 'Off') || (n == 0) || (n == 4) || (plant(zone) == 0) || !zoneActive(zone.toString())) return 0 - + // apply gain adjustment float gainAdjust = 100.0 if (settings.gain && settings.gain != 0) gainAdjust += settings.gain - + // apply seasonal adjustment if enabled and not set to new plants float seasonAdjust = 100.0 def wsa = state.weekseasonAdj - if (wsa && isSeason && (settings."plant${zone}" != 'New Plants')) seasonAdjust = wsa - - int tpw = 0 - // Use learned, previous tpw if it is available - if ( settings."sensor${zone}" ) { - seasonAdjust = 100.0 // no weekly seasonAdjust if this zone uses a sensor - if(state.tpwMap && settings.learn) tpw = state.tpwMap[zone-1] - } - - // set user-specified minimum time with seasonal adjust - int minWeek = 0 - def mw = settings."minWeek${zone}" - if (mw) minWeek = mw.toInteger() + if (wsa && isSeason && (settings."plant${zone}" != 'New Plants')) seasonAdjust = wsa + + int tpw = 0 + // Use learned, previous tpw if it is available + if ( settings."sensor${zone}" ) { + seasonAdjust = 100.0 // no weekly seasonAdjust if this zone uses a sensor + if(state.tpwMap && settings.learn) tpw = state.tpwMap[zone-1] + } + + // set user-specified minimum time with seasonal adjust + int minWeek = 0 + def mw = settings."minWeek${zone}" + if (mw) minWeek = mw.toInteger() if (minWeek != 0) { - tpw = Math.round(minWeek * (seasonAdjust / 100.0)) - } + tpw = Math.round(minWeek * (seasonAdjust / 100.0)) + } else if (!tpw || (tpw == 0)) { // use calculated tpw - tpw = Math.round((plant(zone) * nozzle(zone) * (gainAdjust / 100.0) * (seasonAdjust / 100.0))) + tpw = Math.round((plant(zone) * nozzle(zone) * (gainAdjust / 100.0) * (seasonAdjust / 100.0))) } - state.tpwMap[zone-1] = tpw + state.tpwMap[zone-1] = tpw return tpw } // Get the current time per week, calls init if not defined int getTPW(int zone) { - if (state.tpwMap) return state.tpwMap[zone-1] else return initTPW(zone) + if (state.tpwMap) return state.tpwMap[zone-1] else return initTPW(zone) } // Calculate daily run time based on tpw and dpw int calcRunTime(int tpw, int dpw) -{ +{ int duration = 0 if ((tpw > 0) && (dpw > 0)) duration = Math.round(tpw.toFloat() / dpw.toFloat()) return duration @@ -1664,41 +1633,41 @@ int calcRunTime(int tpw, int dpw) // Check the moisture level of a zone returning dry (1) or wet (0) and adjust tpw if overly dry/wet def moisture(int i) { - boolean isDebug = false - if (isDebug) log.debug "moisture(${i})" - - def endMsecs = 0 - // No Sensor on this zone or manual start skips moisture checking altogether - if ((i == 0) || !settings."sensor${i}") { + boolean isDebug = false + if (isDebug) log.debug "moisture(${i})" + + def endMsecs = 0 + // No Sensor on this zone or manual start skips moisture checking altogether + if ((i == 0) || !settings."sensor${i}") { return [1,''] } // Ensure that the sensor has reported within last 48 hours int spHum = getDrySp(i) int hours = 48 - def yesterday = new Date(now() - (/* 1000 * 60 * 60 */ 3600000 * hours).toLong()) - float latestHum = settings."sensor${i}".latestValue('humidity').toFloat() // state = 29, value = 29.13 + def yesterday = new Date(now() - (/* 1000 * 60 * 60 */ 3600000 * hours).toLong()) + float latestHum = settings."sensor${i}".latestValue('humidity').toFloat() // state = 29, value = 29.13 def lastHumDate = settings."sensor${i}".latestState('humidity').date if (lastHumDate < yesterday) { - note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, no humidity reports in the last ${hours} hours", 'a') + note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, no humidity reports in the last ${hours} hours", 'a') - if (latestHum < spHum) - latestHum = spHum - 1.0 // amke sure we water and do seasonal adjustments, but not tpw adjustments - else - latestHum = spHum + 0.99 // make sure we don't water, do seasonal adjustments, but not tpw adjustments + if (latestHum < spHum) + latestHum = spHum - 1.0 // amke sure we water and do seasonal adjustments, but not tpw adjustments + else + latestHum = spHum + 0.99 // make sure we don't water, do seasonal adjustments, but not tpw adjustments } if (!settings.learn) { // in Delay mode, only looks at target moisture level, doesn't try to adjust tpw // (Temporary) seasonal adjustment WILL be applied in cycleLoop(), as if we didn't have a sensor - if (latestHum <= spHum.toFloat()) { + if (latestHum <= spHum.toFloat()) { //dry soil - return [1,"${settings."name${i}"}, Watering: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] - } + return [1,"${settings."name${i}"}, Watering: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] + } else { - //wet soil - return [0,"${settings."name${i}"}, Skipping: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] + //wet soil + return [0,"${settings."name${i}"}, Skipping: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] } } @@ -1711,116 +1680,116 @@ def moisture(int i) if (isDebug) log.debug "moisture(${i}): tpw: ${tpw}, dpw: ${dpw}, cycles: ${cpd} (before adjustment)" - + float diffHum = 0.0 if (latestHum > 0.0) diffHum = (spHum - latestHum) / 100.0 else { - diffHum = 0.02 // Safety valve in case sensor is reporting 0% humidity (e.g., somebody pulled it out of the ground or flower pot) - note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, it is currently reading 0%", 'a') - } - - int daysA = state.daysAvailable - int minimum = cpd * dpw // minimum of 1 minute per scheduled days per week (note - can be 1*1=1) - if (minimum < daysA) minimum = daysA // but at least 1 minute per available day - int tpwAdjust = 0 - - if (diffHum > 0.01) { // only adjust tpw if more than 1% of target SP - tpwAdjust = Math.round(((tpw * diffHum) + 0.5) * dpw * cpd) // Compute adjustment as a function of the current tpw - float adjFactor = 2.0 / daysA // Limit adjustments to 200% per week - spread over available days - if (tpwAdjust > (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) + 0.5) // limit fast rise - if (tpwAdjust < minimum) tpwAdjust = minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time + diffHum = 0.02 // Safety valve in case sensor is reporting 0% humidity (e.g., somebody pulled it out of the ground or flower pot) + note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, it is currently reading 0%", 'a') + } + + int daysA = state.daysAvailable + int minimum = cpd * dpw // minimum of 1 minute per scheduled days per week (note - can be 1*1=1) + if (minimum < daysA) minimum = daysA // but at least 1 minute per available day + int tpwAdjust = 0 + + if (diffHum > 0.01) { // only adjust tpw if more than 1% of target SP + tpwAdjust = Math.round(((tpw * diffHum) + 0.5) * dpw * cpd) // Compute adjustment as a function of the current tpw + float adjFactor = 2.0 / daysA // Limit adjustments to 200% per week - spread over available days + if (tpwAdjust > (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) + 0.5) // limit fast rise + if (tpwAdjust < minimum) tpwAdjust = minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time } else if (diffHum < -0.01) { - if (diffHum < -0.05) diffHum = -0.05 // try not to over-compensate for a heavy rainstorm... - tpwAdjust = Math.round(((tpw * diffHum) - 0.5) * dpw * cpd) - float adjFactor = -0.6667 / daysA // Limit adjustments to 66% per week - if (tpwAdjust < (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) - 0.5) // limit slow decay - if (tpwAdjust > (-1 * minimum)) tpwAdjust = -1 * minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time + if (diffHum < -0.05) diffHum = -0.05 // try not to over-compensate for a heavy rainstorm... + tpwAdjust = Math.round(((tpw * diffHum) - 0.5) * dpw * cpd) + float adjFactor = -0.6667 / daysA // Limit adjustments to 66% per week + if (tpwAdjust < (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) - 0.5) // limit slow decay + if (tpwAdjust > (-1 * minimum)) tpwAdjust = -1 * minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time } - + int seasonAdjust = 0 if (isSeason) { - float sa = state.seasonAdj - if ((sa != 100.0) && (sa != 0.0)) { - float sadj = sa - 100.0 - if (sa > 0.0) - seasonAdjust = Math.round(((sadj / 100.0) * tpw) + 0.5) - else - seasonAdjust = Math.round(((sadj / 100.0) * tpw) - 0.5) - } - } - if (isDebug) log.debug "moisture(${i}): diffHum: ${diffHum}, tpwAdjust: ${tpwAdjust} seasonAdjust: ${seasonAdjust}" - - // Now, adjust the tpw. - // With seasonal adjustments enabled, tpw can go up or down independent of the difference in the sensor vs SP - int newTPW = tpw + tpwAdjust + seasonAdjust - + float sa = state.seasonAdj + if ((sa != 100.0) && (sa != 0.0)) { + float sadj = sa - 100.0 + if (sa > 0.0) + seasonAdjust = Math.round(((sadj / 100.0) * tpw) + 0.5) + else + seasonAdjust = Math.round(((sadj / 100.0) * tpw) - 0.5) + } + } + if (isDebug) log.debug "moisture(${i}): diffHum: ${diffHum}, tpwAdjust: ${tpwAdjust} seasonAdjust: ${seasonAdjust}" + + // Now, adjust the tpw. + // With seasonal adjustments enabled, tpw can go up or down independent of the difference in the sensor vs SP + int newTPW = tpw + tpwAdjust + seasonAdjust + int perDay = 20 - def perD = settings."perDay${i}" + def perD = settings."perDay${i}" if (perD) perDay = perD.toInteger() - if (perDay == 0) perDay = daysA * cpd // at least 1 minute per cycle per available day - if (newTPW < perDay) newTPW = perDay // make sure we have always have enough for 1 day of minimum water - - int adjusted = 0 - if ((tpwAdjust + seasonAdjust) > 0) { // needs more water - int maxTPW = daysA * 120 // arbitrary maximum of 2 hours per available watering day per week - if (newTPW > maxTPW) newTPW = maxTPW // initDPW() below may spread this across more days - if (newTPW > (maxTPW * 0.75)) note('warning', "${app.label}: Please check ${settings["sensor${i}"]}, ${settings."name${i}"} time per week seems high: ${newTPW} mins/week",'a') - if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? - state.tpwMap[i-1] = newTPW - dpw = initDPW(i) // need to recalculate days per week since tpw changed - initDPW() stores the value into dpwMap - adjusted = newTPW - tpw // so that the adjustment note is accurate - } - } - else if ((tpwAdjust + seasonAdjust) < 0) { // Needs less water - // Find the minimum tpw - minimum = cpd * daysA // at least 1 minute per cycle per available day - int minLimit = 0 - def minL = settings."minWeek${i}" - if (minL) minLimit = minL.toInteger() // unless otherwise specified in configuration - if (minLimit > 0) { - if (newTPW < minLimit) newTPW = minLimit // use configured minutes per week as the minimum - } else if (newTPW < minimum) { - newTPW = minimum // else at least 1 minute per cycle per available day - note('warning', "${app.label}: Please check ${settings."sensor${i}"}, ${settings."name${i}"} time per week is very low: ${newTPW} mins/week",'a') - } - if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? - state.tpwMap[i-1] = newTPW // store the new tpw - dpw = initDPW(i) // may need to reclac days per week - initDPW() now stores the value into state.dpwMap - avoid doing that twice - adjusted = newTPW - tpw // so that the adjustment note is accurate + if (perDay == 0) perDay = daysA * cpd // at least 1 minute per cycle per available day + if (newTPW < perDay) newTPW = perDay // make sure we have always have enough for 1 day of minimum water + + int adjusted = 0 + if ((tpwAdjust + seasonAdjust) > 0) { // needs more water + int maxTPW = daysA * 120 // arbitrary maximum of 2 hours per available watering day per week + if (newTPW > maxTPW) newTPW = maxTPW // initDPW() below may spread this across more days + if (newTPW > (maxTPW * 0.75)) note('warning', "${app.label}: Please check ${settings["sensor${i}"]}, ${settings."name${i}"} time per week seems high: ${newTPW} mins/week",'a') + if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? + state.tpwMap[i-1] = newTPW + dpw = initDPW(i) // need to recalculate days per week since tpw changed - initDPW() stores the value into dpwMap + adjusted = newTPW - tpw // so that the adjustment note is accurate + } + } + else if ((tpwAdjust + seasonAdjust) < 0) { // Needs less water + // Find the minimum tpw + minimum = cpd * daysA // at least 1 minute per cycle per available day + int minLimit = 0 + def minL = settings."minWeek${i}" + if (minL) minLimit = minL.toInteger() // unless otherwise specified in configuration + if (minLimit > 0) { + if (newTPW < minLimit) newTPW = minLimit // use configured minutes per week as the minimum + } else if (newTPW < minimum) { + newTPW = minimum // else at least 1 minute per cycle per available day + note('warning', "${app.label}: Please check ${settings."sensor${i}"}, ${settings."name${i}"} time per week is very low: ${newTPW} mins/week",'a') + } + if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? + state.tpwMap[i-1] = newTPW // store the new tpw + dpw = initDPW(i) // may need to reclac days per week - initDPW() now stores the value into state.dpwMap - avoid doing that twice + adjusted = newTPW - tpw // so that the adjustment note is accurate } } // else no adjustments, or adjustments cancelled each other out. - + String moistureSum = '' String adjStr = '' String plus = '' if (adjusted > 0) plus = '+' if (adjusted != 0) adjStr = ", ${plus}${adjusted} min" if (Math.abs(adjusted) > 1) adjStr = "${adjStr}s" - if (diffHum >= 0.0) { // water only if ground is drier than SP - moistureSum = "> ${settings."name${i}"}, Water: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" + if (diffHum >= 0.0) { // water only if ground is drier than SP + moistureSum = "> ${settings."name${i}"}, Water: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" return [1, moistureSum] - } - else { // not watering + } + else { // not watering moistureSum = "> ${settings."name${i}"}, Skip: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" - return [0, moistureSum] + return [0, moistureSum] } return [0, moistureSum] -} +} //get moisture SP int getDrySp(int i){ if (settings."sensorSp${i}") return settings."sensorSp${i}".toInteger() // configured SP - if (settings."plant${i}" == 'New Plants') return 40 // New Plants get special care + if (settings."plant${i}" == 'New Plants') return 40 // New Plants get special care - switch (settings."option${i}") { // else, defaults based off of soil type + switch (settings."option${i}") { // else, defaults based off of soil type case 'Sand': return 22 case 'Clay': - return 38 + return 38 default: return 28 } @@ -1829,132 +1798,132 @@ int getDrySp(int i){ //notifications to device, pushed if requested def note(String statStr, String msg, String msgType) { - // send to debug first (near-zero cost) - log.debug "${statStr}: ${msg}" + // send to debug first (near-zero cost) + log.debug "${statStr}: ${msg}" - // notify user second (small cost) - boolean notifyController = true + // notify user second (small cost) + boolean notifyController = true if(settings.notify || settings.logAll) { - String spruceMsg = "Spruce ${msg}" - switch(msgType) { - case 'd': - if (settings.notify && settings.notify.contains('Daily')) { // always log the daily events to the controller - sendIt(spruceMsg) - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'c': - if (settings.notify && settings.notify.contains('Delays')) { - sendIt(spruceMsg) - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'i': - if (settings.notify && settings.notify.contains('Events')) { - sendIt(spruceMsg) - //notifyController = false // no need to notify controller unless we don't notify the user - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'f': - notifyController = false // no need to notify the controller, ever - if (settings.notify && settings.notify.contains('Weather')) { - sendIt(spruceMsg) - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'a': - notifyController = false // no need to notify the controller, ever - if (settings.notify && settings.notify.contains('Warnings')) { - sendIt(spruceMsg) - } else - sendNotificationEvent(spruceMsg) // Special case - make sure this goes into the Hello Home log, if not notifying - break - case 'm': - if (settings.notify && settings.notify.contains('Moisture')) { - sendIt(spruceMsg) - //notifyController = false // no need to notify controller unless we don't notify the user - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - default: - break - } - } - // finally, send to controller DTH, to change the state and to log important stuff in the event log - if (notifyController) { // do we really need to send these to the controller? - // only send status updates to the controller if WE are running, or nobody else is - if (atomicState.run || ((settings.switches.currentSwitch == 'off') && (settings.switches.currentStatus != 'pause'))) { - settings.switches.notify(statStr, msg) - - } - else { // we aren't running, so we don't want to change the status of the controller - // send the event using the current status of the switch, so we don't change it - //log.debug "note - direct sendEvent()" - settings.switches.notify(settings.switches.currentStatus, msg) - - } + String spruceMsg = "Spruce ${msg}" + switch(msgType) { + case 'd': + if (settings.notify && settings.notify.contains('Daily')) { // always log the daily events to the controller + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'c': + if (settings.notify && settings.notify.contains('Delays')) { + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'i': + if (settings.notify && settings.notify.contains('Events')) { + sendIt(spruceMsg) + //notifyController = false // no need to notify controller unless we don't notify the user + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'f': + notifyController = false // no need to notify the controller, ever + if (settings.notify && settings.notify.contains('Weather')) { + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'a': + notifyController = false // no need to notify the controller, ever + if (settings.notify && settings.notify.contains('Warnings')) { + sendIt(spruceMsg) + } else + sendNotificationEvent(spruceMsg) // Special case - make sure this goes into the Hello Home log, if not notifying + break + case 'm': + if (settings.notify && settings.notify.contains('Moisture')) { + sendIt(spruceMsg) + //notifyController = false // no need to notify controller unless we don't notify the user + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + default: + break + } + } + // finally, send to controller DTH, to change the state and to log important stuff in the event log + if (notifyController) { // do we really need to send these to the controller? + // only send status updates to the controller if WE are running, or nobody else is + if (atomicState.run || ((settings.switches.currentSwitch == 'off') && (settings.switches.currentStatus != 'pause'))) { + settings.switches.notify(statStr, msg) + + } + else { // we aren't running, so we don't want to change the status of the controller + // send the event using the current status of the switch, so we don't change it + //log.debug "note - direct sendEvent()" + settings.switches.notify(settings.switches.currentStatus, msg) + + } } } def sendIt(String msg) { - if (location.contactBookEnabled && settings.recipients) { - sendNotificationToContacts(msg, settings.recipients, [event: true]) + if (location.contactBookEnabled && settings.recipients) { + sendNotificationToContacts(msg, settings.recipients, [event: true]) } else { - sendPush( msg ) + sendPush( msg ) } } //days available int daysAvailable(){ - // Calculate days available for watering and save in state variable for future use + // Calculate days available for watering and save in state variable for future use def daysA = state.daysAvailable - if (daysA && (daysA > 0)) { // state.daysAvailable has already calculated and stored in state.daysAvailable - return daysA - } - - if (!settings.days) { // settings.days = "" --> every day is available - state.daysAvailable = 7 - return 7 // every day is allowed - } - - int dayCount = 0 // settings.days specified, need to calculate state.davsAvailable (once) - if (settings.days.contains('Even') || settings.days.contains('Odd')) { + if (daysA && (daysA > 0)) { // state.daysAvailable has already calculated and stored in state.daysAvailable + return daysA + } + + if (!settings.days) { // settings.days = "" --> every day is available + state.daysAvailable = 7 + return 7 // every day is allowed + } + + int dayCount = 0 // settings.days specified, need to calculate state.davsAvailable (once) + if (settings.days.contains('Even') || settings.days.contains('Odd')) { dayCount = 4 if(settings.days.contains('Even') && settings.days.contains('Odd')) dayCount = 7 - } + } else { - if (settings.days.contains('Monday')) dayCount += 1 - if (settings.days.contains('Tuesday')) dayCount += 1 - if (settings.days.contains('Wednesday')) dayCount += 1 - if (settings.days.contains('Thursday')) dayCount += 1 - if (settings.days.contains('Friday')) dayCount += 1 - if (settings.days.contains('Saturday')) dayCount += 1 - if (settings.days.contains('Sunday')) dayCount += 1 - } - + if (settings.days.contains('Monday')) dayCount += 1 + if (settings.days.contains('Tuesday')) dayCount += 1 + if (settings.days.contains('Wednesday')) dayCount += 1 + if (settings.days.contains('Thursday')) dayCount += 1 + if (settings.days.contains('Friday')) dayCount += 1 + if (settings.days.contains('Saturday')) dayCount += 1 + if (settings.days.contains('Sunday')) dayCount += 1 + } + state.daysAvailable = dayCount return dayCount -} - +} + //zone: ['Off', 'Spray', 'rotor', 'Drip', 'Master Valve', 'Pump'] int nozzle(int i){ - String getT = settings."zone${i}" + String getT = settings."zone${i}" if (!getT) return 0 - - switch(getT) { + + switch(getT) { case 'Spray': return 1 case 'Rotor': @@ -1969,12 +1938,12 @@ int nozzle(int i){ return 0 } } - + //plant: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants'] int plant(int i){ - String getP = settings."plant${i}" + String getP = settings."plant${i}" if(!getP) return 0 - + switch(getP) { case 'Lawn': return 60 @@ -1994,12 +1963,12 @@ int plant(int i){ return 0 } } - + //option: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x'] -int cycles(int i){ - String getC = settings."option${i}" +int cycles(int i){ + String getC = settings."option${i}" if(!getC) return 2 - + switch(getC) { case 'Slope': return 3 @@ -2012,129 +1981,77 @@ int cycles(int i){ case 'Cycle 2x': return 2 case 'Cycle 3x': - return 3 + return 3 default: return 2 - } + } } - + //check if day is allowed boolean isDay() { - - if (daysAvailable() == 7) return true // every day is allowed - + + if (daysAvailable() == 7) return true // every day is allowed + def daynow = new Date() - String today = daynow.format('EEEE', location.timeZone) + String today = daynow.format('EEEE', location.timeZone) if (settings.days.contains(today)) return true def daynum = daynow.format('dd', location.timeZone) - int dayint = Integer.parseInt(daynum) + int dayint = Integer.parseInt(daynum) if (settings.days.contains('Even') && (dayint % 2 == 0)) return true if (settings.days.contains('Odd') && (dayint % 2 != 0)) return true - return false + return false } //set season adjustment & remove season adjustment def setSeason() { boolean isDebug = false if (isDebug) log.debug 'setSeason()' - + int zone = 1 - while(zone <= 16) { - if ( !settings.learn || !settings."sensor${zone}" || state.tpwMap[zone-1] == 0) { + while(zone <= 16) { + if ( !settings.learn || !settings."sensor${zone}" || state.tpwMap[zone-1] == 0) { - int tpw = initTPW(zone) // now updates state.tpwMap - int dpw = initDPW(zone) // now updates state.dpwMap + int tpw = initTPW(zone) // now updates state.tpwMap + int dpw = initDPW(zone) // now updates state.dpwMap if (isDebug) { - if (!settings.learn && (tpw != 0) && (state.weekseasonAdj != 0)) { - log.debug "Zone ${zone}: seasonally adjusted by ${state.weekseasonAdj-100}% to ${tpw}" - } + if (!settings.learn && (tpw != 0) && (state.weekseasonAdj != 0)) { + log.debug "Zone ${zone}: seasonally adjusted by ${state.weekseasonAdj-100}% to ${tpw}" + } } - } + } zone++ - } + } } //capture today's total rainfall - scheduled for just before midnight each day def getRainToday() { - def wzipcode = zipString() - Map wdata = getWeatherFeature('conditions', wzipcode) - if (!wdata) { - - note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null", 'a') - } - else { - if (!wdata.response || wdata.response.containsKey('error')) { - log.debug wdata.response - note('warning', "${app.label}: Please check Zipcode/PWS setting, error:\n${wdata.response.error.type}: ${wdata.response.error.description}" , 'a') - } - else { - float TRain = 0.0 - if (wdata.current_observation.precip_today_in.isNumber()) { // WU can return "t" for "Trace" - we'll assume that means 0.0 - TRain = wdata.current_observation.precip_today_in.toFloat() - if (TRain > 25.0) TRain = 25.0 - else if (TRain < 0.0) TRain = 0.0 // WU sometimes returns -999 for "estimated" locations - log.debug "getRainToday(): ${wdata.current_observation.precip_today_in} / ${TRain}" - } - int day = getWeekDay() // what day is it today? - if (day == 7) day = 0 // adjust: state.Rain order is Su,Mo,Tu,We,Th,Fr,Sa - state.Rain[day] = TRain as Float // store today's total rainfall - } + def wzipcode = zipString() + def conditionsData = getTwcConditions(wzipcode) + if (!conditionsData) { + note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null", 'a') + } else { + float TRain = 0.0 + if (conditionsData.precip24Hour.isNumber()) { + TRain = conditionsData.precip24Hour.toFloat() + if (TRain > 25.0) TRain = 25.0 + else if (TRain < 0.0) TRain = 0.0 + log.debug "getRainToday(): ${conditionsData.precip24Hour} / ${TRain}" + } + int day = getWeekDay() // what day is it today? + if (day == 7) day = 0 // adjust: state.Rain order is Su,Mo,Tu,We,Th,Fr,Sa + state.Rain[day] = TRain as Float // store today's total rainfall } } //check weather, set seasonal adjustment factors, skip today if rainy boolean isWeather(){ - def startMsecs = 0 - def endMsecs = 0 - boolean isDebug = false - if (isDebug) log.debug 'isWeather()' - - if (!settings.isRain && !settings.isSeason) return false // no need to do any of this - - String wzipcode = zipString() - if (isDebug) log.debug "isWeather(): ${wzipcode}" - - // get only the data we need - // Moved geolookup to installSchedule() - String featureString = 'forecast/conditions' - if (settings.isSeason) featureString = "${featureString}/astronomy" - if (isDebug) startMsecs= now() - Map wdata = getWeatherFeature(featureString, wzipcode) - if (isDebug) { - endMsecs = now() - log.debug "isWeather() getWeatherFeature elapsed time: ${endMsecs - startMsecs}ms" - } - if (wdata && wdata.response) { - if (isDebug) log.debug wdata.response - if (wdata.response.containsKey('error')) { - if (wdata.response.error.type != 'invalidfeature') { - note('warning', "${app.label}: Please check Zipcode/PWS setting, error:\n${wdata.response.error.type}: ${wdata.response.error.description}" , 'a') - return false - } - else { - // Will find out which one(s) weren't reported later (probably never happens now that we don't ask for history) - log.debug 'Rate limited...one or more WU features unavailable at this time.' - } - } - } - else { - if (isDebug) log.debug 'wdata is null' - note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null" , 'a') - return false - } - - String city = wzipcode - - + if (!settings.isRain && !settings.isSeason) return false + String wzipcode = zipString() + String city = getTwcLocation(wzipcode).location.city ?: wzipcode + def forecastData = getTwcForecast(wzipcode) + def conditionsData = getTwcConditions(wzipcode) - if (wdata.current_observation) { - if (wdata.current_observation.observation_location.city != '') city = wdata.current_observation.observation_location.city - else if (wdata.current_observation.observation_location.full != '') city = wdata.current_observation.display_location.full - - if (wdata.current_observation.estimated.estimated) city = "${city} (est)" - } - // OK, we have good data, let's start the analysis float qpfTodayIn = 0.0 float qpfTomIn = 0.0 @@ -2143,122 +2060,123 @@ boolean isWeather(){ float TRain = 0.0 float YRain = 0.0 float weeklyRain = 0.0 - + if (settings.isRain) { - if (isDebug) log.debug 'isWeather(): isRain' - - // Get forecasted rain for today and tomorrow - - if (!wdata.forecast) { - log.debug 'isWeather(): Unable to get weather forecast.' - return false - } - if (wdata.forecast.simpleforecast.forecastday[0].qpf_allday.in.isNumber()) qpfTodayIn = wdata.forecast.simpleforecast.forecastday[0].qpf_allday.in.toFloat() - if (wdata.forecast.simpleforecast.forecastday[0].pop.isNumber()) popToday = wdata.forecast.simpleforecast.forecastday[0].pop.toFloat() - if (wdata.forecast.simpleforecast.forecastday[1].qpf_allday.in.isNumber()) qpfTomIn = wdata.forecast.simpleforecast.forecastday[1].qpf_allday.in.toFloat() - if (wdata.forecast.simpleforecast.forecastday[1].pop.isNumber()) popTom = wdata.forecast.simpleforecast.forecastday[1].pop.toFloat() - if (qpfTodayIn > 25.0) qpfTodayIn = 25.0 - else if (qpfTodayIn < 0.0) qpfTodayIn = 0.0 - if (qpfTomIn > 25.0) qpfTomIn = 25.0 - else if (qpfTomIn < 0.0) qpfTomIn = 0.0 - - // Get rainfall so far today - - if (!wdata.current_observation) { - log.debug 'isWeather(): Unable to get current weather conditions.' - return false - } - if (wdata.current_observation.precip_today_in.isNumber()) { - TRain = wdata.current_observation.precip_today_in.toFloat() - if (TRain > 25.0) TRain = 25.0 // Ignore runaway weather - else if (TRain < 0.0) TRain = 0.0 // WU can return -999 for estimated locations - } - if (TRain > (qpfTodayIn * (popToday / 100.0))) { // Not really what PoP means, but use as an adjustment factor of sorts - qpfTodayIn = TRain // already have more rain than was forecast for today, so use that instead - popToday = 100 // we KNOW this rain happened - } - - // Get yesterday's rainfall - int day = getWeekDay() - YRain = state.Rain[day - 1] - - if (isDebug) log.debug "TRain ${TRain} qpfTodayIn ${qpfTodayIn} @ ${popToday}%, YRain ${YRain}" - - int i = 0 - while (i <= 6){ // calculate (un)weighted average (only heavy rainstorms matter) - int factor = 0 - if ((day - i) > 0) factor = day - i else factor = day + 7 - i - float getrain = state.Rain[i] - if (factor != 0) weeklyRain += (getrain / factor) - i++ - } - - if (isDebug) log.debug "isWeather(): weeklyRain ${weeklyRain}" - } - - if (isDebug) log.debug 'isWeather(): build report' + log.debug 'isWeather(): isRain' + + // Get forecasted rain for today and tomorrow + if (!forecastData) { + log.debug 'isWeather(): Unable to get weather forecast.' + return false + } + qpfTodayIn = forecastData.daypart[0].qpf.toFloat() + popToday = forecastData.daypart[0].precipChance.toFloat() + qpfTomIn = forecastData.daypart[1].qpf.toFloat() + popTom = forecastData.daypart[1].precipChance.toFloat() + if (qpfTodayIn > 25.0) qpfTodayIn = 25.0 + else if (qpfTodayIn < 0.0) qpfTodayIn = 0.0 + if (qpfTomIn > 25.0) qpfTomIn = 25.0 + else if (qpfTomIn < 0.0) qpfTomIn = 0.0 + + // Get rainfall so far today + + if (!conditionsData) { + log.debug 'isWeather(): Unable to get current weather conditions.' + return false + } + if (conditionsData.precip24Hour.isNumber()) { + TRain = conditionsData.precip24Hour.toFloat() + if (TRain > 25.0) TRain = 25.0 // Ignore runaway weather + else if (TRain < 0.0) TRain = 0.0 // WU can return -999 for estimated locations + } + if (TRain > (qpfTodayIn * (popToday / 100.0))) { // Not really what PoP means, but use as an adjustment factor of sorts + qpfTodayIn = TRain // already have more rain than was forecast for today, so use that instead + popToday = 100 // we KNOW this rain happened + } + + // Get yesterday's rainfall + int day = getWeekDay() + YRain = state.Rain[day - 1] + + log.debug "TRain ${TRain} qpfTodayIn ${qpfTodayIn} @ ${popToday}%, YRain ${YRain}" + + int i = 0 + while (i <= 6){ // calculate (un)weighted average (only heavy rainstorms matter) + int factor = 0 + if ((day - i) > 0) factor = day - i else factor = day + 7 - i + float getrain = state.Rain[i] + if (factor != 0) weeklyRain += (getrain / factor) + i++ + } + + log.debug "isWeather(): weeklyRain ${weeklyRain}" + } + + log.debug 'isWeather(): build report' //get highs - int highToday = 0 - int highTom = 0 - if (wdata.forecast.simpleforecast.forecastday[0].high.fahrenheit.isNumber()) highToday = wdata.forecast.simpleforecast.forecastday[0].high.fahrenheit.toInteger() - if (wdata.forecast.simpleforecast.forecastday[1].high.fahrenheit.isNumber()) highTom = wdata.forecast.simpleforecast.forecastday[1].high.fahrenheit.toInteger() - + int highToday = 0 + int highTom = 0 + highToday = forecastData.daypart[0].temperature[0].toInteger() + highTom = forecastData.daypart[1].temperature[0].toInteger() + String weatherString = "${app.label}: ${city} weather:\n TDA: ${highToday}F" if (settings.isRain) weatherString = "${weatherString}, ${qpfTodayIn}in rain (${Math.round(popToday)}% PoP)" weatherString = "${weatherString}\n TMW: ${highTom}F" if (settings.isRain) weatherString = "${weatherString}, ${qpfTomIn}in rain (${Math.round(popTom)}% PoP)\n YDA: ${YRain}in rain" - + if (settings.isSeason) - { - if (!settings.isRain) { // we need to verify we have good data first if we didn't do it above - - if (!wdata.forecast) { - log.debug 'Unable to get weather forecast' - return false - } - } - - // is the temp going up or down for the next few days? - float heatAdjust = 100.0 - float avgHigh = highToday.toFloat() - if (highToday != 0) { - // is the temp going up or down for the next few days? - int totalHigh = highToday - int j = 1 - int highs = 1 - while (j < 4) { // get forecasted high for next 3 days - if (wdata.forecast.simpleforecast.forecastday[j].high.fahrenheit.isNumber()) { - totalHigh += wdata.forecast.simpleforecast.forecastday[j].high.fahrenheit.toInteger() - highs++ - } - j++ - } - if ( highs > 0 ) avgHigh = (totalHigh / highs) - heatAdjust = avgHigh / highToday - } - if (isDebug) log.debug "highToday ${highToday}, avgHigh ${avgHigh}, heatAdjust ${heatAdjust}" - - //get humidity + { + if (!settings.isRain) { // we need to verify we have good data first if we didn't do it above + + if (!forecastData) { + log.debug 'Unable to get weather forecast' + return false + } + } + + // is the temp going up or down for the next few days? + float heatAdjust = 100.0 + float avgHigh = highToday.toFloat() + if (highToday != 0) { + // is the temp going up or down for the next few days? + int totalHigh = highToday + int j = 1 + int highs = 1 + while (j < 4) { // get forecasted high for next 3 days + if (forecastData.daypart[j].temperature[0].isNumber()) { + totalHigh += forecastData.daypart[j].temperature[0].toInteger() + highs++ + } + j++ + } + if ( highs > 0 ) avgHigh = (totalHigh / highs) + heatAdjust = avgHigh / highToday + } + log.debug "highToday ${highToday}, avgHigh ${avgHigh}, heatAdjust ${heatAdjust}" + + //get humidity int humToday = 0 - if (wdata.forecast.simpleforecast.forecastday[0].avehumidity.isNumber()) - humToday = wdata.forecast.simpleforecast.forecastday[0].avehumidity.toInteger() - + def avehumidity = (forecastData.daypart[0].relativeHumidity[0] + forecastData.daypart[0].relativeHumidity[1])/2 + if (avehumidity.isNumber()) + humToday = avehumidity.toInteger() + float humAdjust = 100.0 float avgHum = humToday.toFloat() if (humToday != 0) { - int j = 1 - int highs = 1 - int totalHum = humToday - while (j < 4) { // get forcasted humitidty for today and the next 3 days - if (wdata.forecast.simpleforecast.forecastday[j].avehumidity.isNumber()) { - totalHum += wdata.forecast.simpleforecast.forecastday[j].avehumidity.toInteger() - highs++ - } - j++ - } - if (highs > 1) avgHum = totalHum / highs - humAdjust = 1.5 - ((0.5 * avgHum) / humToday) // basically, half of the delta % between today and today+3 days + int j = 1 + int highs = 1 + int totalHum = humToday + while (j < 4) { // get forcasted humitidty for today and the next 3 days + avehumidity = (forecastData.daypart[j].relativeHumidity[0] + forecastData.daypart[j].relativeHumidity[1])/2 + if (favehumidity.isNumber()) { + totalHum += avehumidity.toInteger() + highs++ + } + j++ + } + if (highs > 1) avgHum = totalHum / highs + humAdjust = 1.5 - ((0.5 * avgHum) / humToday) // basically, half of the delta % between today and today+3 days } if (isDebug) log.debug "humToday ${humToday}, avgHum ${avgHum}, humAdjust ${humAdjust}" @@ -2269,124 +2187,123 @@ boolean isWeather(){ //wetter over next 3 days, less water // //Note: these should never get to be very large, and work best if allowed to cumulate over time (watering amount will change marginally - // as days get warmer/cooler and drier/wetter) - def sa = ((heatAdjust + humAdjust) / 2) * 100.0 - state.seasonAdj = sa - sa = sa - 100.0 + // as days get warmer/cooler and drier/wetter) + def sa = ((heatAdjust + humAdjust) / 2) * 100.0 + state.seasonAdj = sa + sa = sa - 100.0 String plus = '' if (sa > 0) plus = '+' weatherString = "${weatherString}\n Adjusting ${plus}${Math.round(sa)}% for weather forecast" - + // Apply seasonal adjustment on Monday each week or at install if ((getWeekDay() == 1) || (state.weekseasonAdj == 0)) { //get daylight - if (wdata.sun_phase) { - int getsunRH = 0 - int getsunRM = 0 - int getsunSH = 0 - int getsunSM = 0 - - if (wdata.sun_phase.sunrise.hour.isNumber()) getsunRH = wdata.sun_phase.sunrise.hour.toInteger() - if (wdata.sun_phase.sunrise.minute.isNumber()) getsunRM = wdata.sun_phase.sunrise.minute.toInteger() - if (wdata.sun_phase.sunset.hour.isNumber()) getsunSH = wdata.sun_phase.sunset.hour.toInteger() - if (wdata.sun_phase.sunset.minute.isNumber()) getsunSM = wdata.sun_phase.sunset.minute.toInteger() - - int daylight = ((getsunSH * 60) + getsunSM)-((getsunRH * 60) + getsunRM) - if (daylight >= 850) daylight = 850 - - //set seasonal adjustment - //seasonal q (fudge) factor - float qFact = 75.0 - - // (Daylight / 11.66 hours) * ( Average of ((Avg Temp / 70F) + ((1/2 of Average Humidity) / 65.46))) * calibration quotient - // Longer days = more water (day length constant = approx USA day length at fall equinox) - // Higher temps = more water - // Lower humidity = more water (humidity constant = USA National Average humidity in July) - float wa = ((daylight / 700.0) * (((avgHigh / 70.0) + (1.5-((avgHum * 0.5) / 65.46))) / 2.0) * qFact) - state.weekseasonAdj = wa - - //apply seasonal time adjustment - plus = '' - if (wa != 0) { - if (wa > 100.0) plus = '+' - String waStr = String.format('%.2f', (wa - 100.0)) - weatherString = "${weatherString}\n Seasonal adjustment of ${waStr}% for the week" - } - setSeason() - } + if (conditionsData.sunriseTimeLocal && conditionsData.sunsetTimeLocal) { + DateFormat hours = new SimpleDateFormat("HH"); + DateFormat minutes = new SimpleDateFormat("mm"); + String nowAsISO = hours.format(new Date()); + + int getsunRH = hours.format(conditionsData.sunriseTimeLocal).toInteger() + int getsunRM = minutes.format(conditionsData.sunriseTimeLocal).toInteger() + int getsunSH = hours.format(conditionsData.sunsetTimeLocal).toInteger() + int getsunSM = minutes.format(conditionsData.sunsetTimeLocal).toInteger() + + int daylight = ((getsunSH * 60) + getsunSM)-((getsunRH * 60) + getsunRM) + if (daylight >= 850) daylight = 850 + + //set seasonal adjustment + //seasonal q (fudge) factor + float qFact = 75.0 + + // (Daylight / 11.66 hours) * ( Average of ((Avg Temp / 70F) + ((1/2 of Average Humidity) / 65.46))) * calibration quotient + // Longer days = more water (day length constant = approx USA day length at fall equinox) + // Higher temps = more water + // Lower humidity = more water (humidity constant = USA National Average humidity in July) + float wa = ((daylight / 700.0) * (((avgHigh / 70.0) + (1.5-((avgHum * 0.5) / 65.46))) / 2.0) * qFact) + state.weekseasonAdj = wa + + //apply seasonal time adjustment + plus = '' + if (wa != 0) { + if (wa > 100.0) plus = '+' + String waStr = String.format('%.2f', (wa - 100.0)) + weatherString = "${weatherString}\n Seasonal adjustment of ${waStr}% for the week" + } + setSeason() + } else { - log.debug 'isWeather(): Unable to get sunrise/set info for today.' + log.debug 'isWeather(): Unable to get sunrise/set info for today.' } } } note('season', weatherString , 'f') - // if only doing seasonal adjustments, we are done - if (!settings.isRain) return false - + // if only doing seasonal adjustments, we are done + if (!settings.isRain) return false + float setrainDelay = 0.2 if (settings.rainDelay) setrainDelay = settings.rainDelay.toFloat() - // if we have no sensors, rain causes us to skip watering for the day - if (!anySensors()) { - if (settings.switches.latestValue('rainsensor') == 'rainsensoron'){ - note('raintoday', "${app.label}: skipping, rain sensor is on", 'd') - return true - } - float popRain = qpfTodayIn * (popToday / 100.0) - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintoday', "${app.label}: skipping, ${rainStr}in of rain is probable today", 'd') - return true - } - popRain += qpfTomIn * (popTom / 100.0) - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintom', "${app.label}: skipping, ${rainStr}in of rain is probable today + tomorrow", 'd') - return true - } - if (weeklyRain > setrainDelay){ - String rainStr = String.format('%.2f', weeklyRain) - note('rainy', "${app.label}: skipping, ${rainStr}in weighted average rain over the past week", 'd') - return true - } - } + // if we have no sensors, rain causes us to skip watering for the day + if (!anySensors()) { + if (settings.switches.latestValue('rainsensor') == 'rainsensoron'){ + note('raintoday', "${app.label}: skipping, rain sensor is on", 'd') + return true + } + float popRain = qpfTodayIn * (popToday / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintoday', "${app.label}: skipping, ${rainStr}in of rain is probable today", 'd') + return true + } + popRain += qpfTomIn * (popTom / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintom', "${app.label}: skipping, ${rainStr}in of rain is probable today + tomorrow", 'd') + return true + } + if (weeklyRain > setrainDelay){ + String rainStr = String.format('%.2f', weeklyRain) + note('rainy', "${app.label}: skipping, ${rainStr}in weighted average rain over the past week", 'd') + return true + } + } else { // we have at least one sensor in the schedule - // Ignore rain sensor & historical rain - only skip if more than setrainDelay is expected before midnight tomorrow - float popRain = (qpfTodayIn * (popToday / 100.0)) - TRain // ignore rain that has already fallen so far today - sensors should already reflect that - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintoday', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today", 'd') - return true - } - popRain += qpfTomIn * (popTom / 100.0) - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintom', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today + tomorrow", 'd') - return true - } + // Ignore rain sensor & historical rain - only skip if more than setrainDelay is expected before midnight tomorrow + float popRain = (qpfTodayIn * (popToday / 100.0)) - TRain // ignore rain that has already fallen so far today - sensors should already reflect that + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintoday', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today", 'd') + return true + } + popRain += qpfTomIn * (popTom / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintom', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today + tomorrow", 'd') + return true + } } if (isDebug) log.debug "isWeather() ends" - return false + return false } // true if ANY of this schedule's zones are on and using sensors private boolean anySensors() { - int zone=1 - while (zone <= 16) { - def zoneStr = settings."zone${zone}" - if (zoneStr && (zoneStr != 'Off') && settings."sensor${zone}") return true - zone++ - } - return false + int zone=1 + while (zone <= 16) { + def zoneStr = settings."zone${zone}" + if (zoneStr && (zoneStr != 'Off') && settings."sensor${zone}") return true + zone++ + } + return false } def getDPWDays(int dpw){ - if (dpw && (dpw.isNumber()) && (dpw >= 1) && (dpw <= 7)) { - return state."DPWDays${dpw}" - } else - return [0,0,0,0,0,0,0] + if (dpw && (dpw.isNumber()) && (dpw >= 1) && (dpw <= 7)) { + return state."DPWDays${dpw}" + } else + return [0,0,0,0,0,0,0] } // Create a map of what days each possible DPW value will run on @@ -2396,63 +2313,63 @@ def getDPWDays(int dpw){ // DPWDays3:[1,0,1,0,1,0,0] (run on Monday Wed and Fri) // Everything runs on the first day possible, starting with Monday. def createDPWMap() { - state.DPWDays1 = [] + state.DPWDays1 = [] state.DPWDays2 = [] state.DPWDays3 = [] state.DPWDays4 = [] state.DPWDays5 = [] state.DPWDays6 = [] state.DPWDays7 = [] - //def NDAYS = 7 + //def NDAYS = 7 // day Distance[NDAYS][NDAYS], easier to just define than calculate everytime def int[][] dayDistance = [[0,1,2,3,3,2,1],[1,0,1,2,3,3,2],[2,1,0,1,2,3,3],[3,2,1,0,1,2,3],[3,3,2,1,0,1,2],[2,3,3,2,1,0,1],[1,2,3,3,2,1,0]] - def ndaysAvailable = daysAvailable() - int i = 0 + def ndaysAvailable = daysAvailable() + int i = 0 // def int[] daysAvailable = [0,1,2,3,4,5,6] def int[] daysAvailable = [0,0,0,0,0,0,0] if(settings.days) { - if (settings.days.contains('Even') || settings.days.contains('Odd')) { - return - } - if (settings.days.contains('Monday')) { - daysAvailable[i] = 0 - i++ - } - if (settings.days.contains('Tuesday')) { - daysAvailable[i] = 1 - i++ - } - if (settings.days.contains('Wednesday')) { - daysAvailable[i] = 2 - i++ - } - if (settings.days.contains('Thursday')) { - daysAvailable[i] = 3 - i++ - } - if (settings.days.contains('Friday')) { - daysAvailable[i] = 4 - i++ - } - if (settings.days.contains('Saturday')) { - daysAvailable[i] = 5 - i++ - } - if (settings.days.contains('Sunday')) { - daysAvailable[i] = 6 - i++ - } - if(i != ndaysAvailable) { - log.debug 'ERROR: days and daysAvailable do not match in setup - overriding' - log.debug "${i} ${ndaysAvailable}" - ndaysAvailable = i // override incorrect setup execution - state.daysAvailable = i - } - } - else { // all days are available if settings.days == "" - daysAvailable = [0,1,2,3,4,5,6] + if (settings.days.contains('Even') || settings.days.contains('Odd')) { + return + } + if (settings.days.contains('Monday')) { + daysAvailable[i] = 0 + i++ + } + if (settings.days.contains('Tuesday')) { + daysAvailable[i] = 1 + i++ + } + if (settings.days.contains('Wednesday')) { + daysAvailable[i] = 2 + i++ + } + if (settings.days.contains('Thursday')) { + daysAvailable[i] = 3 + i++ + } + if (settings.days.contains('Friday')) { + daysAvailable[i] = 4 + i++ + } + if (settings.days.contains('Saturday')) { + daysAvailable[i] = 5 + i++ + } + if (settings.days.contains('Sunday')) { + daysAvailable[i] = 6 + i++ + } + if(i != ndaysAvailable) { + log.debug 'ERROR: days and daysAvailable do not match in setup - overriding' + log.debug "${i} ${ndaysAvailable}" + ndaysAvailable = i // override incorrect setup execution + state.daysAvailable = i + } + } + else { // all days are available if settings.days == "" + daysAvailable = [0,1,2,3,4,5,6] } //log.debug "Ndays: ${ndaysAvailable} Available Days: ${daysAvailable}" def maxday = -1 @@ -2461,104 +2378,104 @@ def createDPWMap() { def int[][] runDays = [[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0]] for(def a=0; a < ndaysAvailable; a++) { - // Figure out next day using the dayDistance map, getting the farthest away day (max value) - if(a > 0 && ndaysAvailable >= 2 && a != ndaysAvailable-1) { - if(a == 1) { - for(def c=1; c < ndaysAvailable; c++) { - def d = dayDistance[daysAvailable[0]][daysAvailable[c]] - if(d > max) { - max = d - maxday = daysAvailable[c] - } - } - //log.debug "max: ${max} maxday: ${maxday}" - dDays[0] = maxday - } - - // Find successive maxes for the following days - if(a > 1) { - def lmax = max - def lmaxday = maxday - max = -1 - for(int c = 1; c < ndaysAvailable; c++) { - def d = dayDistance[daysAvailable[0]][daysAvailable[c]] - def t = d > max - if (a % 2 == 0) t = d >= max - if(d < lmax && d >= max) { - if(d == max) { - d = dayDistance[lmaxday][daysAvailable[c]] - if(d > dayDistance[lmaxday][maxday]) { - max = d - maxday = daysAvailable[c] - } - } - else { - max = d - maxday = daysAvailable[c] - } - } - } - lmax = 5 - while(max == -1) { - lmax = lmax -1 - for(int c = 1; c < ndaysAvailable; c++) { - def d = dayDistance[daysAvailable[0]][daysAvailable[c]] - if(d < lmax && d >= max) { - if(d == max) { - d = dayDistance[lmaxday][daysAvailable[c]] - if(d > dayDistance[lmaxday][maxday]) { - max = d - maxday = daysAvailable[c] - } - } - else { - max = d - maxday = daysAvailable[c] - } - } - } - for (def d=0; d< a-2; d++) { - if(maxday == dDays[d]) max = -1 - } - } - //log.debug "max: ${max} maxday: ${maxday}" - dDays[a-1] = maxday - } - } - - // Set the runDays map using the calculated maxdays - for(int b=0; b < 7; b++) { - // Runs every day available - if(a == ndaysAvailable-1) { - runDays[a][b] = 0 - for (def c=0; c < ndaysAvailable; c++) { - if(b == daysAvailable[c]) runDays[a][b] = 1 - } - } - else { - // runs weekly, use first available day - if(a == 0) { - if(b == daysAvailable[0]) - runDays[a][b] = 1 - else - runDays[a][b] = 0 - } - else { - // Otherwise, start with first available day - if(b == daysAvailable[0]) - runDays[a][b] = 1 - else { - runDays[a][b] = 0 - for(def c=0; c < a; c++) - if(b == dDays[c]) - runDays[a][b] = 1 - } - } - } - } - } - - //log.debug "DPW: ${runDays}" + // Figure out next day using the dayDistance map, getting the farthest away day (max value) + if(a > 0 && ndaysAvailable >= 2 && a != ndaysAvailable-1) { + if(a == 1) { + for(def c=1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + if(d > max) { + max = d + maxday = daysAvailable[c] + } + } + //log.debug "max: ${max} maxday: ${maxday}" + dDays[0] = maxday + } + + // Find successive maxes for the following days + if(a > 1) { + def lmax = max + def lmaxday = maxday + max = -1 + for(int c = 1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + def t = d > max + if (a % 2 == 0) t = d >= max + if(d < lmax && d >= max) { + if(d == max) { + d = dayDistance[lmaxday][daysAvailable[c]] + if(d > dayDistance[lmaxday][maxday]) { + max = d + maxday = daysAvailable[c] + } + } + else { + max = d + maxday = daysAvailable[c] + } + } + } + lmax = 5 + while(max == -1) { + lmax = lmax -1 + for(int c = 1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + if(d < lmax && d >= max) { + if(d == max) { + d = dayDistance[lmaxday][daysAvailable[c]] + if(d > dayDistance[lmaxday][maxday]) { + max = d + maxday = daysAvailable[c] + } + } + else { + max = d + maxday = daysAvailable[c] + } + } + } + for (def d=0; d< a-2; d++) { + if(maxday == dDays[d]) max = -1 + } + } + //log.debug "max: ${max} maxday: ${maxday}" + dDays[a-1] = maxday + } + } + + // Set the runDays map using the calculated maxdays + for(int b=0; b < 7; b++) { + // Runs every day available + if(a == ndaysAvailable-1) { + runDays[a][b] = 0 + for (def c=0; c < ndaysAvailable; c++) { + if(b == daysAvailable[c]) runDays[a][b] = 1 + } + } + else { + // runs weekly, use first available day + if(a == 0) { + if(b == daysAvailable[0]) + runDays[a][b] = 1 + else + runDays[a][b] = 0 + } + else { + // Otherwise, start with first available day + if(b == daysAvailable[0]) + runDays[a][b] = 1 + else { + runDays[a][b] = 0 + for(def c=0; c < a; c++) + if(b == dDays[c]) + runDays[a][b] = 1 + } + } + } + } + } + + //log.debug "DPW: ${runDays}" state.DPWDays1 = runDays[0] state.DPWDays2 = runDays[1] state.DPWDays3 = runDays[2] @@ -2570,67 +2487,66 @@ def createDPWMap() { //transition page to populate app state - this is a fix for WP param def zoneSetPage1(){ - state.app = 1 + state.app = 1 zoneSetPage() } def zoneSetPage2(){ - state.app = 2 + state.app = 2 zoneSetPage() } def zoneSetPage3(){ - state.app = 3 + state.app = 3 zoneSetPage() } def zoneSetPage4(){ - state.app = 4 + state.app = 4 zoneSetPage() } def zoneSetPage5(){ - state.app = 5 + state.app = 5 zoneSetPage() } def zoneSetPage6(){ - state.app = 6 + state.app = 6 zoneSetPage() } def zoneSetPage7(){ - state.app = 7 + state.app = 7 zoneSetPage() } def zoneSetPage8(){ - state.app = 8 + state.app = 8 zoneSetPage() } def zoneSetPage9(i){ - state.app = 9 + state.app = 9 zoneSetPage() } def zoneSetPage10(){ - state.app = 10 + state.app = 10 zoneSetPage() } def zoneSetPage11(){ - state.app = 11 + state.app = 11 zoneSetPage() } def zoneSetPage12(){ - state.app = 12 + state.app = 12 zoneSetPage() } def zoneSetPage13(){ - state.app = 13 + state.app = 13 zoneSetPage() } def zoneSetPage14(){ - state.app = 14 + state.app = 14 zoneSetPage() } def zoneSetPage15(){ - state.app = 15 + state.app = 15 zoneSetPage() } def zoneSetPage16(){ - state.app = 16 + state.app = 16 zoneSetPage() } - From 2237934260bfd0ee80a3df6c63093cbacea76846 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Mon, 31 Dec 2018 12:54:33 -0500 Subject: [PATCH 7/7] Fix for SimpleDateFormat error (#3836) --- .../plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy index 2441ef2be73..b7c8b86ec4f 100644 --- a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy +++ b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy @@ -2200,8 +2200,8 @@ boolean isWeather(){ //get daylight if (conditionsData.sunriseTimeLocal && conditionsData.sunsetTimeLocal) { - DateFormat hours = new SimpleDateFormat("HH"); - DateFormat minutes = new SimpleDateFormat("mm"); + def hours = new java.text.SimpleDateFormat("HH"); + def minutes = new java.text.SimpleDateFormat("mm"); String nowAsISO = hours.format(new Date()); int getsunRH = hours.format(conditionsData.sunriseTimeLocal).toInteger()