From 0321a7f0713014003c2bbf1be62e33c3adc5082a Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Fri, 16 Oct 2015 12:34:44 -0700 Subject: [PATCH 01/15] Merge pull request #135 from kwarodom/fibaroSmokeSensor Fibaro Smoke Sensor: initial device type --- .../fibaro-smoke-sensor.groovy | 455 ++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy diff --git a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy new file mode 100644 index 0000000..4b1ef56 --- /dev/null +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy @@ -0,0 +1,455 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Battery" //attributes: battery + capability "Configuration" //commands: configure() + capability "Sensor" + capability "Smoke Detector" //attributes: smoke ("detected","clear","tested") + capability "Temperature Measurement" //attributes: temperature + attribute "tamper", "enum", ["detected", "clear"] + attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"] + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B" + } + simulator { + //battery + for (int i in [0, 5, 10, 15, 50, 99, 100]) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i) + ).incomingMessage() + } + status "battery 100%": "command: 8003, payload: 64" + status "battery 5%": "command: 8003, payload: 05" + //smoke + status "smoke detected": "command: 7105, payload: 01 01" + status "smoke clear": "command: 7105, payload: 01 00" + status "smoke tested": "command: 7105, payload: 01 03" + //temperature + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1) + ).incomingMessage() + } + } + preferences { + input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration", + title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph" + input description: "Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN", + title: "To check smoke detection state", displayDuringSetup: true, type: "paragraph", element: "paragraph" + input description: "Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings", + title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph" + input "smokeSensorSensitivity", "enum", title: "Smoke Sensor Sensitivity", options: ["High","Medium","Low"], defaultValue: "${smokeSensorSensitivity}", displayDuringSetup: true + input "zwaveNotificationStatus", "enum", title: "Notifications Status", options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], + defaultValue: "${zwaveNotificationStatus}", displayDuringSetup: true + input "visualIndicatorNotificationStatus", "enum", title: "Visual Indicator Notifications Status", + options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], + defaultValue: "${visualIndicatorNotificationStatus}", displayDuringSetup: true + input "soundNotificationStatus", "enum", title: "Sound Notifications Status", + options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], + defaultValue: "${soundNotificationStatus}", displayDuringSetup: true + input "temperatureReportInterval", "enum", title: "Temperature Report Interval", + options: ["reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${temperatureReportInterval}", displayDuringSetup: true + input "temperatureReportHysteresis", "number", title: "Temperature Report Hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true + input "temperatureThreshold", "number", title: "Overheat Temperature Threshold", description: "Available settings: 0 or 2-100 C", range: "0..100", displayDuringSetup: true + input "excessTemperatureSignalingInterval", "enum", title: "Excess Temperature Signaling Interval", + options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${excessTemperatureSignalingInterval}", displayDuringSetup: true + input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave Range Indication Interval", + options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${lackOfZwaveRangeIndicationInterval}", displayDuringSetup: true + } + tiles (scale: 2){ + multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){ + tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66") + attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff") + } + tileAttribute ("device.battery", key: "SECONDARY_CONTROL") { + attributeState "battery", label:'Battery: ${currentValue}%', unit:"%" + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"%" + } + valueTile("temperature", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "temperature", label:'${currentValue}°', unit:"C" + } + valueTile("heatAlarm", "device.heatAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "clear", label:'TEMPERATURE OK', backgroundColor:"#ffffff" + state "overheat detected", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff" + state "rapid temperature rise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff" + state "underheat detected", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff" + } + valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "clear", label:'NO TAMPER', backgroundColor:"#ffffff" + state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff" + } + + main "smoke" + details(["smoke","temperature"]) + } +} + +def updated() { + log.debug "Updated with settings: ${settings}" + setConfigured("false") //wait until the next time device wakeup to send configure command +} + +def parse(String description) { + log.debug "parse() >> description: $description" + def result = null + if (description.startsWith("Err 106")) { + log.debug "parse() >> Err 106" + result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, + descriptionText: "This sensor failed to complete the network security key exchange. " + + "If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + } else if (description != "updated") { + log.debug "parse() >> zwave.parse(description)" + def cmd = zwave.parse(description, [0x31: 5, 0x71: 3, 0x84: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd" + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: false) +} + + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + setConfigured("true") //when battery is reported back meaning configuration is done + //Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler + state.lastbatt = now() + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + def msg = cmd.status == 0 ? "try again later" : + cmd.status == 1 ? "try again in $cmd.waitTime seconds" : + cmd.status == 2 ? "request queued" : "sorry" + createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + setSecured() + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1]) + if (encapsulatedCommand) { + log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" + setSecured() + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + if (getDataValue("MSR")?.startsWith("010F-0C02")){ + response(configure()) //configure device using SmartThings default settings + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)" + createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true) + //after device securely joined the network, call configure() to config device + setSecured() + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + if (getDataValue("MSR")?.startsWith("010F-0C02")){ + response(configure()) //configure device using SmartThings default settings + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd" + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + result << createEvent(name: "tamper", value: "clear", displayed: false) + break + case 3: + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName casing was opened") + break + } + } else if (cmd.notificationType == 1) { //Smoke Alarm (V2) + log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)" + result << smokeAlarmEvent(cmd.event) + } else if (cmd.notificationType == 4) { // Heat Alarm (V2) + log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)" + result << heatAlarmEvent(cmd.event) + } else { + log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def smokeAlarmEvent(value) { + log.debug "smokeAlarmEvent(value): $value" + def map = [name: "smoke"] + if (value == 1 || value == 2) { + map.value = "detected" + map.descriptionText = "$device.displayName detected smoke" + } else if (value == 0) { + map.value = "clear" + map.descriptionText = "$device.displayName is clear (no smoke)" + } else if (value == 3) { + map.value = "tested" + map.descriptionText = "$device.displayName smoke alarm test" + } else if (value == 4) { + map.value = "replacement required" + map.descriptionText = "$device.displayName replacement required" + } else { + map.value = "unknown" + map.descriptionText = "$device.displayName unknown event" + } + createEvent(map) +} + +def heatAlarmEvent(value) { + log.debug "heatAlarmEvent(value): $value" + def map = [name: "heatAlarm"] + if (value == 1 || value == 2) { + map.value = "overheat detected" + map.descriptionText = "$device.displayName overheat detected" + } else if (value == 0) { + map.value = "clear" + map.descriptionText = "$device.displayName heat alarm cleared (no overheat)" + } else if (value == 3 || value == 4) { + map.value = "rapid temperature rise" + map.descriptionText = "$device.displayName rapid temperature rise" + } else if (value == 5 || value == 6) { + map.value = "underheat detected" + map.descriptionText = "$device.displayName underheat detected" + } else { + map.value = "unknown" + map.descriptionText = "$device.displayName unknown event" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd" + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + def cmds = [] + /* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */ + if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) { + result << response(configure()) // configure a newly joined device or joined device with preference update + } else { + //Only ask for battery if we haven't had a BatteryReport in a while + if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) { + log.debug("Device has been configured sending >> batteryGet()") + cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format() + cmds << "delay 1200" + } + log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + result << response(cmds) //tell device back to sleep + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd" + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + break + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd" + createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd" + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def result = [] + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + log.debug "After device is securely joined, send commands to update tiles" + result << zwave.batteryV1.batteryGet() + result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) + result << zwave.wakeUpV1.wakeUpNoMoreInformation() + [[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))] +} + +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + def result = [] + if (cmd.nodeId.any { it == zwaveHubNodeId }) { + result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}") + } else if (cmd.groupingIdentifier == 1) { + result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}") + result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId)) + } + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "General zwaveEvent cmd: ${cmd}" + createEvent(descriptionText: cmd.toString(), isStateChange: false) +} + +def configure() { +// This sensor joins as a secure device if you tripple-click the button to include it + log.debug "configure() >> isSecured() : ${isSecured()}" + if (!isSecured()) { + log.debug "Fibaro smoke sensor not sending configure until secure" + return [] + } else { + log.info "${device.displayName} is configuring its settings" + def request = [] + + //1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr) + request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId) + + //2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW + if (smokeSensorSensitivity && smokeSensorSensitivity != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, + scaledConfigurationValue: + smokeSensorSensitivity == "High" ? 1 : + smokeSensorSensitivity == "Medium" ? 2 : + smokeSensorSensitivity == "Low" ? 3 : 2) + } + //3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable + if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){ + request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0) + } + //4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification + if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0) + } + //5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification + if (soundNotificationStatus && soundNotificationStatus != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0) + } + //6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) + if (temperatureReportInterval && temperatureReportInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180) + } else { //send SmartThings default configuration + request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180) + } + //7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C) + if (temperatureReportHysteresis && temperatureReportHysteresis != null) { + request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis) + } + //8. Temperature threshold: 1-100 (C), default 55 (C) + if (temperatureThreshold && temperatureThreshold != null) { + request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold) + } + //9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) + if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180) + } else { //send SmartThings default configuration + request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180) + } + //10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours) + if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160) + } else { + request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160) + } + //11. get battery level when device is paired + request += zwave.batteryV1.batteryGet() + + //12. get temperature reading from device + request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) + + commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + } +} + +private def getTimeOptionValueMap() { [ + "5 minutes" : 30, + "15 minutes" : 90, + "30 minutes" : 180, + "1 hour" : 360, + "6 hours" : 2160, + "12 hours" : 4320, + "18 hours" : 6480, + "24 hours" : 8640, + "reports inactive" : 0, +]} + +private def getNotificationOptionValueMap() { [ + "disabled" : 0, + "casing opened" : 1, + "exceeding temperature threshold" : 2, + "lack of Z-Wave range" : 4, + "all notifications" : 7, +]} + +private command(physicalgraph.zwave.Command cmd) { + if (isSecured()) { + log.info "Sending secured command: ${cmd}" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + log.info "Sending unsecured command: ${cmd}" + cmd.format() + } +} + +private commands(commands, delay=200) { + log.info "inside commands: ${commands}" + delayBetween(commands.collect{ command(it) }, delay) +} + +private setConfigured(configure) { + updateDataValue("configured", configure) +} +private isConfigured() { + getDataValue("configured") == "true" +} +private setSecured() { + updateDataValue("secured", "true") +} +private isSecured() { + getDataValue("secured") == "true" +} From f55ea8b2f872ecbd8f17472a7fadc044b1c176e4 Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Wed, 2 Dec 2015 11:37:09 -0600 Subject: [PATCH 02/15] Fix Homeseer Multi Instance encap parse PROB-398 --- .../homeseer-multisensor.groovy | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy b/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy index b6d495e..778eda8 100644 --- a/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy +++ b/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy @@ -80,19 +80,12 @@ def parse(String description) { if (cmd) { result = zwaveEvent(cmd) } - // log.debug "Parsed ${description.inspect()} to ${result.inspect()}" + log.debug "Parsed ${description.inspect()} to ${result.inspect()}" return result } def zwaveEvent(physicalgraph.zwave.commands.multiinstancev1.MultiInstanceCmdEncap cmd) { - def encapsulated = null - if (cmd.respondsTo("encapsulatedCommand")) { - encapsulated = cmd.encapsulatedCommand() - } else { - def hex1 = { n -> String.format("%02X", n) } - def sorry = "command: ${hex1(cmd.commandClass)}${hex1(cmd.command)}, payload: " + cmd.parameter.collect{ hex1(it) }.join(" ") - encapsulated = zwave.parse(sorry, [0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1]) - } + def encapsulated = cmd.encapsulatedCommand([0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1]) return encapsulated ? zwaveEvent(encapsulated) : null } From 6fe60146a433e84a869a1b93ff3ca3c3d7257a83 Mon Sep 17 00:00:00 2001 From: rboy1 Date: Tue, 2 Feb 2016 10:35:10 -0500 Subject: [PATCH 03/15] Bugfixes for codeReports 1. Missing break in case 32 for AlarmReport 2. Bugfix - Not reporting all codes programmed into the lock causing SmartApps to fail while awaiting a codeReport notification (once a code is programmed into a lock it should be notified because there is no way to read back codes from the lock, apps awaiting a codeReport will go into an infinite programming loop to trying to update the codes and awaiting a codeReport notification indicating a successful programming) --- devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy index fc44982..0535ffa 100644 --- a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy +++ b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy @@ -275,6 +275,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { case 32: map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] allCodesDeleted() + break case 33: map = [ name: "codeReport", value: cmd.alarmLevel, data: [ code: "" ], isStateChange: true ] map.descriptionText = "$device.displayName code $cmd.alarmLevel was deleted" @@ -341,14 +342,14 @@ def zwaveEvent(UserCodeReport cmd) { map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: code ] ] map.descriptionText = "$device.displayName code $cmd.userIdentifier is set" map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) - map.isStateChange = (code != decrypt(state[name])) + map.isStateChange = true } result << createEvent(map) } else { map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: "" ] ] if (state.blankcodes && state["reset$name"]) { // we deleted this code so we can tell that our new code gets set map.descriptionText = "$device.displayName code $cmd.userIdentifier was reset" - map.displayed = map.isStateChange = false + map.displayed = map.isStateChange = true result << createEvent(map) state["set$name"] = state["reset$name"] result << response(setCode(cmd.userIdentifier, state["reset$name"])) @@ -360,7 +361,7 @@ def zwaveEvent(UserCodeReport cmd) { map.descriptionText = "$device.displayName code $cmd.userIdentifier is not set" } map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) - map.isStateChange = state[name] as Boolean + map.isStateChange = true result << createEvent(map) } code = "" From bd9a1d1dc59524a2eedbff8e4bc2713643c91cef Mon Sep 17 00:00:00 2001 From: boggebe Date: Mon, 8 Feb 2016 16:03:01 -0600 Subject: [PATCH 04/15] Closure was causing sandbox issues locally --- .../smartsense-multi-sensor.groovy | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 7c6a423..de53cb3 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -487,11 +487,6 @@ def enrollResponse() { } private Map parseAxis(String description) { - def hexToSignedInt = { hexVal -> - def unsignedVal = hexToInt(hexVal) - unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal - } - def z = hexToSignedInt(description[0..3]) def y = hexToSignedInt(description[10..13]) def x = hexToSignedInt(description[20..23]) @@ -518,6 +513,11 @@ private Map parseAxis(String description) { getXyzResult(xyzResults, description) } +private hexToSignedInt(hexVal) { + def unsignedVal = hexToInt(hexVal) + unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal +} + def garageEvent(zValue) { def absValue = zValue.abs() def contactValue = null From fe2b36cd301df7ef50238c5aa33213153176d770 Mon Sep 17 00:00:00 2001 From: Ben Boggess Date: Wed, 10 Feb 2016 10:46:48 -0600 Subject: [PATCH 05/15] Revert "Convert closure to method" --- .../smartsense-multi-sensor.groovy | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index de53cb3..7c6a423 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -487,6 +487,11 @@ def enrollResponse() { } private Map parseAxis(String description) { + def hexToSignedInt = { hexVal -> + def unsignedVal = hexToInt(hexVal) + unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal + } + def z = hexToSignedInt(description[0..3]) def y = hexToSignedInt(description[10..13]) def x = hexToSignedInt(description[20..23]) @@ -513,11 +518,6 @@ private Map parseAxis(String description) { getXyzResult(xyzResults, description) } -private hexToSignedInt(hexVal) { - def unsignedVal = hexToInt(hexVal) - unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal -} - def garageEvent(zValue) { def absValue = zValue.abs() def contactValue = null From 12220da75081ad533b24e09a99803feae0582719 Mon Sep 17 00:00:00 2001 From: boggebe Date: Wed, 10 Feb 2016 10:57:10 -0600 Subject: [PATCH 06/15] Convert closure to method --- .../smartsense-multi-sensor.groovy | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 70cc9ee..f6a5bde 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -487,11 +487,6 @@ def enrollResponse() { } private Map parseAxis(String description) { - def hexToSignedInt = { hexVal -> - def unsignedVal = hexToInt(hexVal) - unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal - } - def z = hexToSignedInt(description[0..3]) def y = hexToSignedInt(description[10..13]) def x = hexToSignedInt(description[20..23]) @@ -518,6 +513,11 @@ private Map parseAxis(String description) { getXyzResult(xyzResults, description) } +private hexToSignedInt(hexVal) { + def unsignedVal = hexToInt(hexVal) + unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal +} + def garageEvent(zValue) { def absValue = zValue.abs() def contactValue = null From a82717744e1cec364033e58b646246671d3d9820 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Tue, 9 Feb 2016 13:45:19 -0800 Subject: [PATCH 07/15] DVCSMP-1480 Fixed ArrayIndexOutOfBoundsException Fixed ArrayIndexOutOfBoundsException from events that lack values to some fields in a few LAN Connect SmartApps. --- .../samsung-tv-connect.groovy | 48 +++++------------ .../wemo-connect.src/wemo-connect.groovy | 52 ++++++------------- 2 files changed, 30 insertions(+), 70 deletions(-) diff --git a/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy b/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy index 6a15dc9..79bcb68 100644 --- a/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy +++ b/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy @@ -316,60 +316,40 @@ private def parseEventMessage(String description) { parts.each { part -> part = part.trim() if (part.startsWith('devicetype:')) { - def valueString = part.split(":")[1].trim() - event.devicetype = valueString + part -= "devicetype:" + event.devicetype = part.trim() } else if (part.startsWith('mac:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.mac = valueString - } + part -= "mac:" + event.mac = part.trim() } else if (part.startsWith('networkAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.ip = valueString - } + part -= "networkAddress:" + event.ip = part.trim() } else if (part.startsWith('deviceAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.port = valueString - } + part -= "deviceAddress:" + event.port = part.trim() } else if (part.startsWith('ssdpPath:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.ssdpPath = valueString - } + part -= "ssdpPath:" + event.ssdpPath = part.trim() } else if (part.startsWith('ssdpUSN:')) { part -= "ssdpUSN:" - def valueString = part.trim() - if (valueString) { - event.ssdpUSN = valueString - } + event.ssdpUSN = part.trim() } else if (part.startsWith('ssdpTerm:')) { part -= "ssdpTerm:" - def valueString = part.trim() - if (valueString) { - event.ssdpTerm = valueString - } + event.ssdpTerm = part.trim() } else if (part.startsWith('headers')) { part -= "headers:" - def valueString = part.trim() - if (valueString) { - event.headers = valueString - } + event.headers = part.trim() } else if (part.startsWith('body')) { part -= "body:" - def valueString = part.trim() - if (valueString) { - event.body = valueString - } + event.body = part.trim() } } event diff --git a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy index 1f12c15..a5819e1 100644 --- a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy +++ b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy @@ -473,68 +473,48 @@ private def parseXmlBody(def body) { } private def parseDiscoveryMessage(String description) { - def device = [:] + def event = [:] def parts = description.split(',') parts.each { part -> part = part.trim() if (part.startsWith('devicetype:')) { - def valueString = part.split(":")[1].trim() - device.devicetype = valueString + part -= "devicetype:" + event.devicetype = part.trim() } else if (part.startsWith('mac:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.mac = valueString - } + part -= "mac:" + event.mac = part.trim() } else if (part.startsWith('networkAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.ip = valueString - } + part -= "networkAddress:" + event.ip = part.trim() } else if (part.startsWith('deviceAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.port = valueString - } + part -= "deviceAddress:" + event.port = part.trim() } else if (part.startsWith('ssdpPath:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.ssdpPath = valueString - } + part -= "ssdpPath:" + event.ssdpPath = part.trim() } else if (part.startsWith('ssdpUSN:')) { part -= "ssdpUSN:" - def valueString = part.trim() - if (valueString) { - device.ssdpUSN = valueString - } + event.ssdpUSN = part.trim() } else if (part.startsWith('ssdpTerm:')) { part -= "ssdpTerm:" - def valueString = part.trim() - if (valueString) { - device.ssdpTerm = valueString - } + event.ssdpTerm = part.trim() } else if (part.startsWith('headers')) { part -= "headers:" - def valueString = part.trim() - if (valueString) { - device.headers = valueString - } + event.headers = part.trim() } else if (part.startsWith('body')) { part -= "body:" - def valueString = part.trim() - if (valueString) { - device.body = valueString - } + event.body = part.trim() } } - device + event } def doDeviceSync(){ From bfd68228bc28833f815d114d9e4d17aa728c56a4 Mon Sep 17 00:00:00 2001 From: Nathan Cauffman Date: Tue, 8 Sep 2015 12:55:12 -0700 Subject: [PATCH 08/15] # This is a combination of 3 commits. # The first commit's message is: MSA-68: Spruce Irrigation controller and soil moisture sensors. Modifying 'Spruce Irrigation' Updated Spruce Scheduler, better stability, notifications, moisture & season adjustments Updated device types, start scheduler from device Code review for ST publication Review for ST submission Delete spruce-sensor.groovy Delete spruce-scheduler-v2-1.groovy Moisture control adjsut # This is the 2nd commit message: Deleted spruce-controller.groovy from 'Spruce Irrigation' # This is the 3rd commit message: Deleted spruce-sensor-v1.groovy from 'Spruce Irrigation' --- .../spruce-scheduler.groovy | 1559 +++++++++++++++++ 1 file changed, 1559 insertions(+) create mode 100644 smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy diff --git a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy new file mode 100644 index 0000000..c3a8c46 --- /dev/null +++ b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy @@ -0,0 +1,1559 @@ +/** + * Spruce Scheduler Pre-release V2.2 10/13/2015 + * + * Thanks Jason for the scheduler improvements + * + * Copyright 2015 Plaid Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * +-------Fixes V2.2------------- +-History log messages condensed +-Seasonal adjustment redefined -> weekly & daily +-Learn mode redefined +-No Learn redefined to operate any available days +-ZoneSettings page redefined -> required to setup zones +-Weather rain updated to fix error with some weather stations +-Contact time delay added +-new plants moisture and season redefined +* +* +-------Fixes V2.1------------- +-Many fixes, code cleanup by Jason C +-open fields leading to unexpected errors +-setting and summary improvements +-multi controller support +-Day to run mapping +-Contact delays optimized +-Warning notification added +-manual start subscription added + * + */ + +definition( + name: "Spruce Scheduler", + namespace: "Plaidsystems", + author: "NCauffman", + description: "Spruce automatic water scheduling app V2.2", + category: "Green Living", + iconUrl: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", + iconX2Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", + iconX3Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png") + +preferences { + page(name: "startPage") + page(name: "autoPage") + page(name: "zipcodePage") + page(name: "weatherPage") + page(name: "globalPage") + page(name: "contactPage") + page(name: "delayPage") + page(name: "zonePage") + + page(name: "zoneSettingsPage") + page(name: "zoneSetPage") + page(name: "plantSetPage") + page(name: "sprinklerSetPage") + page(name: "optionSetPage") + +} + +def startPage(){ + dynamicPage(name: "startPage", title: "Spruce Smart Irrigation setup V2.2", install: true, uninstall: true) + { + section(""){ + href(name: "globalPage", title: "Schedule settings", required: false, page: "globalPage", + image: "http://www.plaidsystems.com/smartthings/st_settings.png", + description: "Watering On: ${enableString()}\nWatering Time: ${startTimeString()}\nDays:${daysString()}\nNotifications:\n${notifyString()}") + + } + + 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()}\nSeasonal Adjust: ${seasonalAdjString()}") + } + + 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(""){ + href(name: "delayPage", title: "Valve and Contact delays", required: false, page: "delayPage", + image: "http://www.plaidsystems.com/smartthings/st_timer.png", + description: "Valve Delay: ${pumpDelayString()} s\nContact Sensor: ${contactSensorString()}\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 from 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 + 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 "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. 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']]) + } + + section("Push Notifications") { + input (name: "notify", type: "enum", title: "Select what push notifications to receive.", required: false, multiple: true, + metadata: [values: ['Warnings', 'Daily', 'Weekly', 'Weather', 'Moisture']]) + } + + } +} + + +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 "rainDelay", "decimal", title: "inches of rain that will delay watering, default: 0.2", defaultValue: '.2', required: false + input "isSeason", "bool", title: "Enable Seasonal Weather Adjustment:", defaultValue: 'true', metadata: [values: ['true', 'false']] + } + } +} + +def zipcodePage() { + return dynamicPage(name: "zipcodePage", title: "Spruce weather station setup") { + section(""){input "zipcode", "text", title: "Zipcode or WeatherUnderground station id. Default value is current location.", defaultValue: "${location.zipCode}", required: false, submitOnChange: true + } + + section(""){href title: "Search WeatherUnderground.com for weather stations", + description: "After page loads, select Change Station for a list of weather stations. You will need to copy the station code into the zipcode field above", + required: false, style:"embedded", + image: "http://www.plaidsystems.com/smartthings/wu.png", + url: "http://www.wunderground.com/q/${location.zipCode}" + } + } +} + +private startTimeString() +{ + def newtime = "${settings["startTime"]}" + if ("${settings["startTime"]}" == "null") return "Please set!" + else return "${hhmm(newtime)}" +} + +def enableString() +{ + if(enable) return "${enable}" + return "False" +} + +def contactSensorString() +{ + if(contact) return "${contact} \n Delay: ${contactDelay} mins" + return "None" +} + +def seasonalAdjString() +{ + if(isSeason) return "${isSeason}" + return "False" +} +def syncString() +{ + if(sync) return "${sync}" + return "None" +} +def notifyString() +{ + def notifyString = "" + if("${settings["notify"]}" != "null") { + if (notify.contains('Weekly')) notifyString = "${notifyString} Weekly" + if (notify.contains('Daily')) notifyString = "${notifyString} Daily" + if (notify.contains('Weather')) notifyString = "${notifyString} Weather" + if (notify.contains('Warnings')) notifyString = "${notifyString} Warnings" + if (notify.contains('Moisture')) notifyString = "${notifyString} Moisture" + } + if(notifyString == "") + notifyString = " None" + return notifyString +} +def daysString() +{ + def daysString = "" + if ("${settings["days"]}" != "null") { + if(days.contains('Even') || days.contains('Odd')) { + if (days.contains('Even')) daysString = "${daysString} Even" + if (days.contains('Odd')) daysString = "${daysString} Odd" + } else { + if (days.contains('Monday')) daysString = "${daysString} M" + if (days.contains('Tuesday')) daysString = "${daysString} Tu" + if (days.contains('Wednesday')) daysString = "${daysString} W" + if (days.contains('Thursday')) daysString = "${daysString} Th" + if (days.contains('Friday')) daysString = "${daysString} F" + if (days.contains('Saturday')) daysString = "${daysString} Sa" + if (days.contains('Sunday')) daysString = "${daysString} Su" + } + } + if(daysString == "") + daysString = " Any" + return daysString +} + + +private 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)) + f.format(t) +} + +def pumpDelayString() +{ + if ("${settings["pumpDelay"]}" == "null") return "5" + else return "${settings["pumpDelay"]}" +} + +def delayPage() { + dynamicPage(name: "delayPage", title: "Additional Options") { + section(""){ + paragraph image: "http://www.plaidsystems.com/smartthings/st_timer.png", + title: "Pump and Master valve delay", + required: false, + "Setting a delay is optional. If you have master valves or a pump suppling water then you can set a delay here. The delay is the time between the valve or pump turning on and the water valves opening. This is also the delay between valves opening" + } + section("") { + input "pumpDelay", "number", title: "Set a delay in seconds?", defaultValue: '5', required: false + } + section(""){ + paragraph image: "http://www.plaidsystems.com/smartthings/st_pause.png", + title: "Contact delays", + required: false, + "Selecting contacts is optional. When a selected contact sensor is opened, water immediately stops and will not resume until closed. Caution: if a contact is set and left open, the watering program will never run." + } + section("") { + input name: "contact", title: "Select water delay contacts", type: "capability.contactSensor", multiple: true, required: false + + input "contactDelay", "number", title: "How many minutes delay after contact is closed?", defaultValue: '1', 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." + input "sync", "capability.switch", title: "Select Master Controller", description: "Select Spruce Controller to sync", required: false, multiple: false + } + } +} + +def zonePage() { + def z1Par=[zoneP:"1"], z2Par=[zoneP:"2"], z3Par=[zoneP:"3"], z4Par=[zoneP:"4"], z5Par=[zoneP:"5"], z6Par=[zoneP:"6"], z7Par=[zoneP:"7"], + z8Par=[zoneP:"8"],z9Par = [zoneP:"9"], z10Par = [zoneP:"10"], z11Par = [zoneP:"11"], z12Par = [zoneP:"12"], z13Par = [zoneP:"13"], + z14Par = [zoneP:"14"], z15Par = [zoneP:"15"], z16Par = [zoneP:"16"] + + dynamicPage(name: "zonePage", title: "Zone setup", install: false, uninstall: false) { + section("") { + href(name: "hrefWithImage", title: "Zone configuration", page: "zoneSettingsPage", + description: "${zoneString()}", + required: false, + image: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png") + } + + if (zoneNumber >= 1){ + section(""){ + href(name: "z1Page", title: "1: ${getname("1")}", required: false, page: "zoneSetPage", + image: "${getimage("1")}", + params: z1Par, + description: "${display("1")}" ) + } + } + if (zoneNumber >= 2){ + section(""){ + href(name: "z2Page", title: "2: ${getname("2")}", required: false, page: "zoneSetPage", + image: "${getimage("2")}", + params: z2Par, + description: "${display("2")}" ) + } + } + if (zoneNumber >= 3){ + section(""){ + href(name: "z3Page", title: "3: ${getname("3")}", required: false, page: "zoneSetPage", + image: "${getimage("3")}", + params: z3Par, + description: "${display("3")}" ) + } + } + if (zoneNumber >= 4){ + section(""){ + href(name: "z4Page", title: "4: ${getname("4")}", required: false, page: "zoneSetPage", + image: "${getimage("4")}", + params: z4Par, + description: "${display("4")}" ) + } + } + if (zoneNumber >= 5){ + section(""){ + href(name: "z5Page", title: "5: ${getname("5")}", required: false, page: "zoneSetPage", + image: "${getimage("5")}", + params: z5Par, + description: "${display("5")}" ) + } + } + if (zoneNumber >= 6){ + section(""){ + href(name: "z6Page", title: "6: ${getname("6")}", required: false, page: "zoneSetPage", + image: "${getimage("6")}", + params: z6Par, + description: "${display("6")}" ) + } + } + if (zoneNumber >= 7){ + section(""){ + href(name: "z7Page", title: "7: ${getname("7")}", required: false, page: "zoneSetPage", + image: "${getimage("7")}", + params: z7Par, + description: "${display("7")}" ) + } + } + if (zoneNumber >= 8){ + section(""){ + href(name: "z8Page", title: "8: ${getname("8")}", required: false, page: "zoneSetPage", + image: "${getimage("8")}", + params: z8Par, + description: "${display("8")}" ) + } + } + if (zoneNumber >= 9){ + section(""){ + href(name: "z9Page", title: "9: ${getname("9")}", required: false, page: "zoneSetPage", + image: "${getimage("9")}", + params: z9Par, + description: "${display("9")}" ) + } + } + if (zoneNumber >= 10){ + section(""){ + href(name: "z10Page", title: "10: ${getname("10")}", required: false, page: "zoneSetPage", + image: "${getimage("10")}", + params: z10Par, + description: "${display("10")}" ) + } + } + if (zoneNumber >= 11){ + section(""){ + href(name: "z11Page", title: "11: ${getname("11")}", required: false, page: "zoneSetPage", + image: "${getimage("11")}", + params: z11Par, + description: "${display("11")}" ) + } + } + if (zoneNumber >= 12){ + section(""){ + href(name: "z12Page", title: "12: ${getname("12")}", required: false, page: "zoneSetPage", + image: "${getimage("12")}", + params: z12Par, + description: "${display("12")}" ) + } + } + if (zoneNumber >= 13){ + section(""){ + href(name: "z13Page", title: "13: ${getname("13")}", required: false, page: "zoneSetPage", + image: "${getimage("13")}", + params: z13Par, + description: "${display("13")}" ) + } + } + if (zoneNumber >= 14){ + section(""){ + href(name: "z14Page", title: "14: ${getname("14")}", required: false, page: "zoneSetPage", + image: "${getimage("14")}", + params: z14Par, + description: "${display("14")}" ) + } + } + if (zoneNumber >= 15){ + section(""){ + href(name: "z15Page", title: "15: ${getname("15")}", required: false, page: "zoneSetPage", + image: "${getimage("15")}", + params: z15Par, + description: "${display("15")}" ) + } + } + if (zoneNumber >= 16){ + section(""){ + href(name: "z16Page", title: "16: ${getname("16")}", required: false, page: "zoneSetPage", + image: "${getimage("16")}", + params: z16Par, + description: "${display("16")}" ) + } + } + + + } +} + +def zoneString(){ + def numberString = "Add zones to setup" + if (zoneNumber) numberString = "Setup: " + "${zoneNumber}" + " zones" + if (learn) numberString += "\nLearn: enabled" + else numberString += "\nLearn: disabled" + return numberString + } + +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 "gain", "number", title: "Increase or decrease all water times by this %, enter a negative or positive value, Default: 0", required: false, defaultValue: '0', range: "*..*", submitOnChange: true + paragraph image: "http://www.plaidsystems.com/smartthings/st_sensor_200_r.png", + title: "Moisture sensor learn mode", + "Learn mode: Watering times will be adjusted based on the assigned moisture sensor and watering will follow a schedule.\n\nNo Learn mode: Zones with moisture sensors will water on any available days when the low moisture setpoint has been reached." + input "learn", "bool", title: "Enable learning (with moisture sensors)", defaultValue: 'true', metadata: [values: ['true', 'false']] + } + } +} + +def zoneSetPage(params){ + dynamicPage(name: "zoneSetPage", title: "Zone ${setPage("${params?.zoneP}")} Setup") { + section(""){ + 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}", submitOnChange: true + } + 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") + } + section(""){ + href(name: "toplantSetPage", title: "Landscape Select: ${setString("plant")}", required: false, page: "plantSetPage", + image: "${getimage("${settings["plant${state.app}"]}")}", + description: "Set landscape type") + } + + section(""){ + href(name: "tooptionSetPage", title: "Options: ${setString("option")}", required: false, page: "optionSetPage", + image: "${getimage("${settings["option${state.app}"]}")}", + description: "Set watering options") + } + section(""){ + paragraph image: "http://www.plaidsystems.com/smartthings/st_sensor_200_r.png", + title: "Moisture sensor settings", + "Select a soil moisture sensor to monitor and control watering. The soil moisture target value is optional and is the target low value. Spruce will use a default value based on settings, however you may override this setting to modify soil moisture threshold" + + 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 time adjustments", "" + + input "minWeek${state.app}", "number", title: "Water time per week (minutes). Default: 0 = autoadjust", required: false + + input "perDay${state.app}", "number", title: "Guideline value for dividing minutes per week into watering days, a high value means more water per day. Default: 20", defaultValue: '20', required: false + } + + } +} + +def setString(i){ + if (i == "zone"){ + if (settings["zone${state.app}"] != null) return "${settings["zone${state.app}"]}" + else return "Not Set" + } + if (i == "plant"){ + if (settings["plant${state.app}"] != null) return "${settings["plant${state.app}"]}" + else return "Not Set" + } + if (i == "option"){ + if (settings["option${state.app}"] != null) return "${settings["option${state.app}"]}" + else return "Not Set" + } +} + +def plantSetPage(){ + dynamicPage(name: "plantSetPage", title: "${settings["name${state.app}"]} Landscape Select") { + section(""){ + 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", + "Select Lawn for typical grass applications" + + 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", + "Select Lawn for typical grass applications" + + paragraph image: "http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png", + title: "Shrubs", + "Select Garden for vegetable gardens" + + paragraph image: "http://www.plaidsystems.com/smartthings/st_trees_225_r.png", + title: "Trees", + "Select Lawn for typical grass applications" + + 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", + "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", + 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", + "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", + "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", + "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", + "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", + "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", + 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']] + } + section(""){ + 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", + "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", + "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", + "The sprinklers will run for 1 long duration" + + 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", + "Cycle 3x will break the water period up into 3 shorter cycles to help minimize runoff and maximize adsorption" + } + } +} + +def setPage(i){ + if (i != "null") state.app = i + return state.app +} + +def getaZoneSummary(zone) +{ + def daysString = "" + def dpw = initDPW(zone) + def runTime = calcRunTime(initTPW(zone), dpw) + if ( !learn && (settings["sensor${zone}"] != null) ) { + 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} minutes, ${daysString}" +} + +def getZoneSummary() +{ + def summary = "" + if (learn) summary = "Moisture Learning enabled" + else summary = "Moisture Learning disabled" + def zone = 1 + createDPWMap() + while(zone <= 16) { + def zoneSum = getaZoneSummary(zone) + if (nozzle(zone) == 4) summary = "${summary}\n${zone}: ${settings["zone${zone}"]}" + else if ("${runTime}" != "0" && "${initDPW(zone)}" != "0") summary = "${summary}\n${zoneSum}" + zone++ + } + if(summary == "") return zoneString() //"Setup all 16 zones" + + return summary +} + +def display(i) +{ + def displayString = "" + def dpw = initDPW(i) + def runTime = calcRunTime(initTPW(i), dpw) + if ("${settings["zone${i}"]}" != "null") displayString += "${settings["zone${i}"]} : " + if ("${settings["plant${i}"]}" != "null") displayString += "${settings["plant${i}"]} : " + if ("${settings["option${i}"]}" != "null") displayString += "${settings["option${i}"]} : " + if ("${settings["sensor${i}"]}" != "null") displayString += "${settings["sensor${i}"]} : " + if ("${runTime}" != "0" && "${dpw}" != "0") displayString += "${runTime} minutes, ${dpw} days per week" + return "${displayString}" +} + +def getimage(i){ + if ("${settings["zone${i}"]}" == "Off") return "http://www.plaidsystems.com/smartthings/off2.png" + else if ("${settings["zone${i}"]}" == "Master Valve") return "http://www.plaidsystems.com/smartthings/master.png" + else if ("${settings["zone${i}"]}" == "Pump") return "http://www.plaidsystems.com/smartthings/pump.png" + else if ("${settings["plant${i}"]}" != "null" && "${settings["zone${i}"]}" != "null")// && "${settings["option${i}"]}" != "null") + i = "${settings["plant${i}"]}" + + switch("${i}"){ + case "null": + return "http://www.plaidsystems.com/smartthings/off2.png" + case "Off": + return "http://www.plaidsystems.com/smartthings/off2.png" + case "Lawn": + return "http://www.plaidsystems.com/smartthings/st_lawn_200_r.png" + case "Garden": + return "http://www.plaidsystems.com/smartthings/st_garden_225_r.png" + case "Flowers": + return "http://www.plaidsystems.com/smartthings/st_flowers_225_r.png" + case "Shrubs": + return "http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png" + case "Trees": + return "http://www.plaidsystems.com/smartthings/st_trees_225_r.png" + case "Xeriscape": + return "http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png" + case "New Plants": + return "http://www.plaidsystems.com/smartthings/st_newplants_225_r.png" + case "Spray": + return "http://www.plaidsystems.com/smartthings/st_spray_225_r.png" + case "Rotor": + return "http://www.plaidsystems.com/smartthings/st_rotor_225_r.png" + case "Drip": + return "http://www.plaidsystems.com/smartthings/st_drip_225_r.png" + case "Master Valve": + return "http://www.plaidsystems.com/smartthings/st_master_225_r.png" + case "Pump": + return "http://www.plaidsystems.com/smartthings/st_pump_225_r.png" + case "Slope": + return "http://www.plaidsystems.com/smartthings/st_slope_225_r.png" + case "Sand": + return "http://www.plaidsystems.com/smartthings/st_sand_225_r.png" + case "Clay": + return "http://www.plaidsystems.com/smartthings/st_clay_225_r.png" + case "No Cycle": + return "http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png" + case "Cycle 2x": + return "http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png" + case "Cycle 3x": + return "http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png" + default: + return "http://www.plaidsystems.com/smartthings/off2.png" + } + } + +def getname(i) { + if ("${settings["name${i}"]}" != "null") return "${settings["name${i}"]}" + else return "Zone $i" +} + +def zipString() { + if (!zipcode) return "${location.zipCode}" + //add pws for correct weatherunderground lookup + if (!zipcode.isNumber()) return "pws:${zipcode}" + else return "${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.seasonAdj = 0 + state.weekseasonAdj = 0 + log.debug "Installed with settings: ${settings}" + installSchedule() +} + +def updated() { + unsubscribe() + unschedule() + log.debug "Installed with settings: ${settings}" + installSchedule() +} + +def installSchedule(){ + if(switches && startTime) { + def checkTime = timeToday(startTime, location.timeZone) + if(enable) { + subscribe switches, "switch.programOn", manualStart + schedule(checkTime, Check) + note("schedule", "Schedule set to start at ${startTimeString()}", "w") + writeSettings() + } + else note("disable", "Automatic watering turned off, set active in app to resume scheduled watering.", "w") + } +} + +//write initial zone settings to device at install/update +def writeSettings() +{ + def cyclesMap = [:] + 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.seasonAdj) state.seasonAdj = 0 + if(!state.weekseasonAdj) state.weekseasonAdj = 0 + setSeason() + //add pumpdelay @ 1 + cyclesMap."1" = pumpDelayString() + def zone = 1 + def cycle = 0 + while(zone <= 17) + { + if(nozzle(zone) == 4) cycle = 4 + else cycle = cycles(zone) + //offset by 1, due to pumpdelay @ 1 + cyclesMap."${zone+1}" = "${cycle}" + if (zone <= 16) { + state.tpwMap.putAt(zone-1, initTPW(zone)) + state.dpwMap.putAt(zone-1, initDPW(zone)) + } + zone++ + } + switches.settingsMap(cyclesMap, 4001) +} + +//get day of week integer +def 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) + } + def today = new Date().format("EEEE", location.timeZone) + return mapDay.get(today) +} + +// Get string of run days from dpwMap +def getRunDays(day1,day2,day3,day4,day5,day6,day7) +{ + def str = "" + if(day1) + str += "M" + if(day2) + str += "T" + if(day3) + str += "W" + if(day4) + str += "Th" + if(day5) + str += "F" + if(day6) + str += "Sa" + if(day7) + str += "Su" + if(string == "") + str = "0 Days/week" + return str +} + +//start water program +def cycleOn(){ + subscribe switches, "switch.off", cycleOff + if (sync != null) subscribe sync, "status.finished", syncOn + if (contact != null){ + subscribe contact, "contact.open", doorOpen + subscribe contact, "contact.closed", doorClosed + } + if (sync != null && !sync.currentValue('status').contains('finished')) note("pause", "waiting for $sync to complete before starting schedule", "w") + else if (contact == null || !contact.currentValue('contact').contains('open')) runIn(15, resume) //15 second delay to allow writesettings to finish resume -> switches.on + else note("pause", "$contact opened $switches paused watering", "w") +} + +//when switch reports off, watering program is finished +def cycleOff(evt){ + if (contact == null || !contact.currentValue('contact').contains('open')){ + note("finished", "finished watering for today", "d") + unsubscribe(contact) + } +} + +//start check +def manualStart(evt){ + Check() +} + + +//run check each day at scheduled time +def Check(){ + // Create weekly water summary, if requested, on Sunday + if(notify && notify.contains('Weekly') && (getWeekDay() == 7)) + { + def zone = 1 + def zoneSummary = "" + while(zone <= 16) { + if(settings["zone${zone}"] != null && settings["zone${zone}"] != 'Off' && nozzle(zone) != 4) { + def sum = getaZoneSummary(zone) + zoneSummary = "${zoneSummary} ${sum}" + } + zone++ + } + log.debug "Weekly water summary: ${zoneSummary}" + sendPush "Weekly water summary: ${zoneSummary}" + } + + def runNowMap = [] + if (isDay() == false){ + switches.programOff() + note("skipping", "No watering allowed today.", "d") + } + else if (isWeather() == false) + { + //get & set watering times for today + runNowMap = cycleLoop() + if (runNowMap) + { + note("active", "${runNowMap}", "d") + cycleOn() //start water program + } + else { + switches.programOff() + note("skipping", "No watering scheduled for today.", "d") + } + } + else switches.programOff() +} + +//get todays schedule +def cycleLoop() +{ + def zone = 1 + def cyc = 0 + def rtime = 0 + def timeMap = [:] + def pumpMap = "" + def runNowMap = "" + def soilString = "" + + while(zone <= 16) + { + rtime = 0 + if(settings["zone${zone}"] != null && settings["zone${zone}"] != 'Off' && nozzle(zone) != 4) + { + // First check if we run this zone today, use either dpwMap or even/odd date + def dpw = getDPW(zone) + def runToday = 0 + if (days && (days.contains('Even') || days.contains('Odd'))) { + def daynum = new Date().format("dd", location.timeZone) + int dayint = Integer.parseInt(daynum) + if(days.contains('Odd') && (dayint +1) % Math.round(31 / (dpw * 4)) == 0) runToday = 1 + if(days.contains('Even') && dayint % Math.round(31 / (dpw * 4)) == 0) runToday = 1 + } else { + def weekDay = getWeekDay()-1 + def dpwMap = getDPWDays(dpw) + def today = dpwMap[weekDay] + log.debug "Zone: ${zone} dpw: ${dpw} weekDay: ${weekDay} dpwMap: ${dpwMap} today: ${today}" + runToday = dpwMap[weekDay] //1 or 0 + } + //if no learn check moisture sensors on available days + if ( (isDay() == true) && !learn && (settings["sensor${zone}"] != null) ) runToday = 1 + + if(runToday) + { + //def soil = moisture(zone) + def soil = moisture(zone) + soilString += "${soil[1]}" + + // Run this zone if soil moisture needed or if it is a weekly + // We run at least once a week and let moisture lower the time if needed + if ( (soil[0] == 1 ) || (learn && dpw == 1) ) + { + cyc = cycles(zone) + dpw = getDPW(zone) + rtime = calcRunTime(getTPW(zone), dpw) + //daily weather adjust if no sensor + if(isSeason && settings["sensor${zone}"] == null) rtime = Math.round(rtime / cyc * state.seasonAdj / 100) + // runTime is total run time devided by num cycles + else rtime = Math.round(rtime / cyc) + runNowMap += "${settings["name${zone}"]}: ${cyc} x ${rtime} min\n" + log.debug"Zone ${zone} Map: ${cyc} x ${rtime} min" + } + } + } + if (nozzle(zone) == 4) pumpMap += "${settings["name${zone}"]}: ${settings["zone${zone}"]} on\n" + timeMap."${zone+1}" = "${rtime}" + zone++ + } + if (soilString) { + soilString = "Moisture Sensors:\n" + soilString + note("moisture", "${soilString}","m") + } + //send settings to Spruce Controller + switches.settingsMap(timeMap,4002) + if (runNowMap) return runNowMap += pumpMap + return runNowMap +} + +//Initialize Days per week, based on TPW, perDay and daysAvailable settings +def initDPW(i){ + if(initTPW(i) > 0) { + def dpw + def perDay = 20 + if(settings["perDay${i}"]) perDay = settings["perDay${i}"].toInteger() + dpw = Math.round(initTPW(i) / perDay) + if(dpw <= 1) return 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(initTPW(i) / perDay < 3.0) return 2 + else return 4 + if(daysAvailable() < dpw) return daysAvailable() + return dpw + } + return 0 +} + +// Get current days per week value, calls init if not defined +def getDPW(zone) +{ + def i = zone.toInteger() + if(state.dpwMap) return state.dpwMap.get(i-1) + return initDPW(i) +} + +//Initialize Time per Week +def initTPW(i){ + if("${settings["zone${i}"]}" == "null" || nozzle(i) == 0 || nozzle(i) == 4 || plant(i) == 0) return 0 + + // apply gain adjustment + def gainAdjust = 100 + if (gain && gain != 0) gainAdjust += gain + + // apply seasonal adjustment is enabled and not set to new plants + def seasonAdjust = 100 + if (state.weekseasonAdj && isSeason && settings["plant${i}"] != "New Plants") seasonAdjust = state.weekseasonAdj + + def zone = i.toInteger() + def tpw = 0 + + // Use learned, previous tpw if it is available + if(state.tpwMap) tpw = state.tpwMap.get(zone-1) + // set user time with season adjust + if(settings["minWeek${i}"] != null && settings["minWeek${i}"] != 0) tpw = Math.round(("${settings["minWeek${i}"]}").toInteger() * seasonAdjust / 100) + + // initial tpw calculation + if (tpw == null || tpw == 0) tpw = Math.round(plant(i) * nozzle(i) * gainAdjust / 100 * seasonAdjust / 100) + // apply gain to all zones --obsolete with learn implementation-- + //else if (gainAdjust != 100) twp = Math.round(tpw * gainAdjust / 100) + + //if (tpw <= 3) tpw = 3 + return tpw +} + +// Get the current time per week, calls init if not defined +def getTPW(zone) +{ + def i = zone.toInteger() + if(state.tpwMap) return state.tpwMap.get(i-1) + return initTPW(i) +} + +// Calculate daily run time based on tpw and dpw +def calcRunTime(tpw, dpw) +{ + def duration = 0 + if(tpw > 0 && dpw > 0) { + duration = Math.round(tpw / dpw) + } + return duration +} + +// Check the moisture level of a zone returning dry (1) or wet (0) and adjust tpw if overly dry/wet +def moisture(i) +{ + // No Sensor on this zone + if(settings["sensor${i}"] == null) { + return [1,""] + } + + // Check if sensor is reporting 6-> 12 hours + def sixHours = new Date(now() - (1000 * 60 * 60 * 12).toLong()) + def recentActivity = (settings["sensor${i}"].eventsSince(sixHours)?.findAll { it.name == "temperature" }) + if (recentActivity == []) { + //change to seperate warning note? + note("warning", "Please check ${settings["sensor${i}"]}, no activity in the last 12 hours", "w") + return [1, "Please check ${settings["sensor${i}"]}, no activity in the last 12 hours\n"] //change to 1 + } + + def latestHum = settings["sensor${i}"].latestValue("humidity") + def spHum = getDrySp(i).toInteger() + //def moistureList = [] + if (!learn) + { + // no learn mode, only looks at target moisture level + if(latestHum <= spHum) { + //dry soil + 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"] + } + } + + def tpw = getTPW(i) + def tpwAdjust = Math.round((spHum - latestHum) * getDPW(i)) + // Only adjust tpw if outside of 1 percent, otherwise rounding will keep it the same anyways + if (tpwAdjust >= -1 && tpwAdjust <= 1) { + tpwAdjust = 0 + } + + def moistureSum = "" + if(tpwAdjust != 0) { + def newTPW = Math.round(tpw + (tpw * tpwAdjust / 100)) + if (newTPW <= 5) note("warning", "Please check ${settings["sensor${i}"]}, Zone ${i} time per week is very low: ${newTPW} mins/week","w") + if (newTPW >= 150) note("warning", "Please check ${settings["sensor${i}"]}, Zone ${i} time per week is very high: ${newTPW} mins/week","w") + state.tpwMap.putAt(i-1, newTPW) + state.dpwMap.putAt(i-1, initDPW(i)) + moistureSum = "Zone ${i}: ${settings["sensor${i}"]} moisture is: ${latestHum}%, SP is ${spHum}% time adjusted by ${tpwAdjust}% to ${newTPW} mins/week\n" + } else { + moistureSum = "Zone ${i}: ${settings["sensor${i}"]} moisture is: ${latestHum}%, SP is ${spHum}% no time adjustment\n" + } + //note("moisture", "${moistureSum}","m") + return [1, "${moistureSum}"] +} + +//notifications to device, pushed if requested +def note(status, message, type){ + log.debug "${status}: ${message}" + switches.notify("${status}", "${message}") + if(notify) + { + if (notify.contains('Daily') && type == "d"){ + sendPush "${message}" + } + if (notify.contains('Weather') && type == "f"){ + sendPush "${message}" + } + if (notify.contains('Warnings') && type == "w"){ + sendPush "${message}" + } + if (notify.contains('Moisture') && type == "m"){ + sendPush "${message}" + } + } +} + +//days available +def daysAvailable(){ + int dayCount = 0 + if("${settings["days"]}" == "null") dayCount = 7 + else if(days){ + if (days.contains('Even') || days.contains('Odd')) { + dayCount = 4 + if(days.contains('Even') && days.contains('Odd')) dayCount = 7 + } else { + if (days.contains('Monday')) dayCount += 1 + if (days.contains('Tuesday')) dayCount += 1 + if (days.contains('Wednesday')) dayCount += 1 + if (days.contains('Thursday')) dayCount += 1 + if (days.contains('Friday')) dayCount += 1 + if (days.contains('Saturday')) dayCount += 1 + if (days.contains('Sunday')) dayCount += 1 + } + } + return dayCount +} + +//get moisture SP +def getDrySp(i){ + if ("${settings["sensorSp${i}"]}" != "null") return "${settings["sensorSp${i}"]}" + else if (settings["plant${i}"] == "New Plants") return 40 + else{ + switch (settings["option${i}"]) { + case "Sand": + return 15 + case "Clay": + return 35 + default: + return 20 + } + } +} + +//zone: ['Off', 'Spray', 'rotor', 'Drip', 'Master Valve', 'Pump'] +def nozzle(i){ + def getT = settings["zone${i}"] + if (!getT) return 0 + switch(getT) { + case "Spray": + return 1 + case "Rotor": + return 1.4 + case "Drip": + return 2.4 + case "Master Valve": + return 4 + case "Pump": + return 4 + default: + return 0 + } +} + +//plant: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants'] +def plant(i){ + def getP = settings["plant${i}"] + if(!getP) return 0 + switch(getP) { + case "Lawn": + return 60 + case "Garden": + return 50 + case "Flowers": + return 40 + case "Shrubs": + return 30 + case "Trees": + return 20 + case "Xeriscape": + return 30 + case "New Plants": + return 80 + default: + return 0 + } +} + +//option: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x'] +def cycles(i){ + def getC = settings["option${i}"] + if(!getC) return 2 + switch(getC) { + case "Slope": + return 3 + case "Sand": + return 1 + case "Clay": + return 2 + case "No Cycle": + return 1 + case "Cycle 2x": + return 2 + case "Cycle 3x": + return 3 + default: + return 2 + } +} + +//check if day is allowed +def isDay() { + if ("${settings["days"]}" == "null") return true + + def today = new Date().format("EEEE", location.timeZone) + def daynum = new Date().format("dd", location.timeZone) + int dayint = Integer.parseInt(daynum) + + log.debug "today: ${today} ${dayint}, days: ${days}" + + if (days.contains(today)) return true + if (days.contains("Even") && (dayint % 2 == 0)) return true + if (days.contains("Odd") && (dayint % 2 != 0)) return true + + return false +} + +//set season adjustment & remove season adjustment +def setSeason() { + + def zone = 1 + while(zone <= 16) { + if ( !learn || (settings["sensor${zone}"] == null) ) { + state.tpwMap.putAt(zone-1, 0) + def tpw = initTPW(zone) + //def newTPW = Math.round(tpw * tpwAdjust / 100) + state.tpwMap.putAt(zone-1, tpw) + state.dpwMap.putAt(zone-1, initDPW(zone)) + } + log.debug "Zone ${zone}: seasonaly adjusted by ${state.weekseasonAdj-100}% to ${tpw}" + zone++ + } +} + +//check weather +def isWeather(){ + def wzipcode = "${zipString()}" + + // Forecast rain + def sdata = getWeatherFeature("forecast10day", wzipcode) + def qpf = sdata.forecast.simpleforecast.forecastday.qpf_allday.mm + def qpfTodayIn = 0 + if (qpf.get(0).isNumber()) qpfTodayIn = Math.round(qpf.get(0).toInteger() /25.4 * 100) /100 + def qpfTomIn = 0 + if (qpf.get(1).isNumber()) qpfTomIn = Math.round(qpf.get(1).toInteger() /25.4 * 100) /100 + + // current conditions + def cond = getWeatherFeature("conditions", wzipcode) + def TRain = 0 + if (cond.current_observation.precip_today_metric.isNumber()) TRain = Math.round(cond.current_observation.precip_today_metric.toInteger() /25.4 * 100) /100 + + // reported rain + def yCond = getWeatherFeature("yesterday", wzipcode) + def YRain = 0 + if (yCond.history.dailysummary.precipi.get(0).isNumber()) YRain = yCond.history.dailysummary.precipi.get(0) + + if(TRain > qpfTodayIn) qpfTodayIn = TRain + + //state.Rain = [S,M,T,W,T,F,S] + //state.Rain = [0,0.43,3,0,0,0,0] + def day = getWeekDay() + state.Rain.putAt(day - 1, YRain) + def i = 0 + def weeklyRain = 0 + while (i <= 6){ + def factor = 0 + if ((day - i) > 0) factor = day - i + else factor = day + 7 - i + weeklyRain += Math.round(state.Rain.get(i).toFloat() / factor * 100)/100 + i++ + } + //note("season", "weeklyRain ${weeklyRain} ${state.Rain}", "d") + + //get highs + def getHigh = sdata.forecast.simpleforecast.forecastday.high.fahrenheit + def avgHigh = Math.round((getHigh.get(0).toInteger() + getHigh.get(1).toInteger() + getHigh.get(2).toInteger() + getHigh.get(3).toInteger() + getHigh.get(4).toInteger())/5) + + def citydata = getWeatherFeature("geolookup", wzipcode) + def weatherString = "${citydata.location.city} weather\n Today: ${getHigh.get(0)}F, ${qpfTodayIn}in rain\n Tomorrow: ${getHigh.get(1)}F, ${qpfTomIn}in rain\n Yesterday: ${YRain}in rain " + + if (isSeason) + { + //daily adjust + state.seasonAdj = Math.round(getHigh.get(0).toInteger()/avgHigh *100) + weatherString += "\n Adjusted ${state.seasonAdj - 100}% for Today" + + // Apply seasonal adjustment on Monday each week or at install + if(getWeekDay() == 1 || state.weekseasonAdj == 0) { + + //get humidity + def gethum = sdata.forecast.simpleforecast.forecastday.avehumidity + def humWeek = Math.round((gethum.get(0).toInteger() + gethum.get(1).toInteger() + gethum.get(2).toInteger() + gethum.get(3).toInteger() + gethum.get(4).toInteger())/5) + + //get daylight + def astro = getWeatherFeature("astronomy", wzipcode) + def getsunRH = astro.moon_phase.sunrise.hour + def getsunRM = astro.moon_phase.sunrise.minute + def getsunSH = astro.moon_phase.sunset.hour + def getsunSM = astro.moon_phase.sunset.minute + def daylight = ((getsunSH.toInteger() * 60) + getsunSM.toInteger())-((getsunRH.toInteger() * 60) + getsunRM.toInteger()) + + //set seasonal adjustment + state.weekseasonAdj = Math.round((daylight/700 * avgHigh/75) * ((1-(humWeek/100)) * avgHigh/75)*100) + + //apply seasonal time adjustment + weatherString += "\n Applying seasonal adjustment of ${state.weekseasonAdj-100}% this week" + //note("season", "Applying seasonal adjustment of ${state.weekseasonAdj-100}% this week", "f") + setSeason() + } + } + + note("season", "${weatherString}" , "f") + + def rainmessage = "" + if (switches.latestValue("rainsensor") == "rainSensoron"){ + note("raintoday", "is skipping watering, rain sensor is on.", "d") + return true + } + else if (qpfTodayIn >= rainDelay){ + note("raintoday", "is skipping watering, ${qpfTodayIn}in rain today.", "d") + return true + } + else if (qpfTomIn >= rainDelay){ + note("raintom", "is skipping watering, ${qpfTomIn}in rain expected tomorrow.", "d") + return true + } + else if (weeklyRain >= rainDelay){ + note("rainy", "is skipping watering, ${weeklyRain}in average rain over the past week.", "d") + return true + } + else return false + +} + +def doorOpen(evt){ + note("pause", "$contact opened $switches paused watering", "w") + switches.off() +} + +def doorClosed(evt){ + note("active", "$contact closed $switches will resume watering in $contactDelay minutes", "w") + runIn(contactDelay * 60, resume) +} + +def resume(){ + switches.on() +} + +def syncOn(evt){ + note("active", "$sync complete, starting scheduled program", "w") + cycleOn() +} + +def getDPWDays(dpw) +{ + if(dpw == 1) + return state.DPWDays1 + if(dpw == 2) + return state.DPWDays2 + if(dpw == 3) + return state.DPWDays3 + if(dpw == 4) + return state.DPWDays4 + if(dpw == 5) + return state.DPWDays5 + if(dpw == 6) + return state.DPWDays6 + if(dpw == 7) + return state.DPWDays7 + return [0,0,0,0,0,0,0] +} + +// Create a map of what days each possible DPW value will run on +// Example: User sets allowed days to Monday Wed and Fri +// Map would look like: DPWDays1:[1,0,0,0,0,0,0] (run on Monday) +// DPWDays2:[1,0,0,0,1,0,0] (run on Monday and Friday) +// 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.DPWDays2 = [] + state.DPWDays3 = [] + state.DPWDays4 = [] + state.DPWDays5 = [] + state.DPWDays6 = [] + state.DPWDays7 = [] + 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() + def i = 0 + def int[] daysAvailable = [0,1,2,3,4,5,6] + if(days) + { + if (days.contains('Even') || days.contains('Odd')) { + return + } + if (days.contains('Monday')) { + daysAvailable[i] = 0 + i++ + } + if (days.contains('Tuesday')) { + daysAvailable[i] = 1 + i++ + } + if (days.contains('Wednesday')) { + daysAvailable[i] = 2 + i++ + } + if (days.contains('Thursday')) { + daysAvailable[i] = 3 + i++ + } + if (days.contains('Friday')) { + daysAvailable[i] = 4 + i++ + } + if (days.contains('Saturday')) { + daysAvailable[i] = 5 + i++ + } + if (days.contains('Sunday')) { + daysAvailable[i] = 6 + i++ + } + + if(i != ndaysAvailable) { + log.debug "ERROR: days and daysAvailable do not match." + log.debug "${i} ${ndaysAvailable}" + } + } + //log.debug "Ndays: ${ndaysAvailable} Available Days: ${daysAvailable}" + def maxday = -1 + def max = -1 + def days = new int[7] + 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}" + days[0] = maxday + } + + // Find successive maxes for the following days + if(a > 1) { + def lmax = max + def lmaxday = maxday + max = -1 + for(def 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(def 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 == days[d]) + max = -1 + } + //log.debug"max: ${max} maxday: ${maxday}" + days[a-1] = maxday + } + } + + // Set the runDays map using the calculated maxdays + for(def 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 == days[c]) + runDays[a][b] = 1 + } + } + } + + } + + //log.debug "DPW: ${runDays}" + state.DPWDays1 = runDays[0] + state.DPWDays2 = runDays[1] + state.DPWDays3 = runDays[2] + state.DPWDays4 = runDays[3] + state.DPWDays5 = runDays[4] + state.DPWDays6 = runDays[5] + state.DPWDays7 = runDays[6] +} \ No newline at end of file From 0e3bd5aa740c0b2027cb52eaf0976dcae7f2356c Mon Sep 17 00:00:00 2001 From: Nathan Cauffman Date: Tue, 8 Sep 2015 12:55:12 -0700 Subject: [PATCH 09/15] MSA-68: Spruce Irrigation controller and soil moisture sensors. Merge in bug fixes and renames to match with new repository layout. Deleted spruce-controller.groovy from 'Spruce Irrigation' Deleted spruce-sensor-v1.groovy from 'Spruce Irrigation' Deleted spruce-scheduler.groovy from 'Spruce Irrigation' Modifying 'Spruce Irrigation' Merge in bug fixes and renames to match with new repository layout. --- .../spruce-controller.groovy | 627 ++++++++++++++++++ .../spruce-sensor.src/spruce-sensor.groovy | 397 +++++++++++ .../spruce-scheduler.groovy | 188 ++++-- 3 files changed, 1147 insertions(+), 65 deletions(-) create mode 100644 devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy create mode 100644 devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy diff --git a/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy new file mode 100644 index 0000000..0025def --- /dev/null +++ b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy @@ -0,0 +1,627 @@ +/** + * Spruce Controller - Pre Release V2 10/11/2015 + * + * Copyright 2015 Plaid Systems + * + * Author: NC + * Date: 2015-11 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + -----------V3 updates-11-2015------------ + -Start program button updated to signal schedule check in Scheduler + 11/17 alarm "0" -> 0 (ln 305) + */ + +metadata { + definition (name: "Spruce Controller", namespace: "plaidsystems", author: "NCauffman") { + capability "Switch" + capability "Configuration" + capability "Refresh" + capability "Actuator" + capability "Valve" + + attribute "switch", "string" + attribute "switch1", "string" + attribute "switch2", "string" + attribute "switch8", "string" + attribute "switch5", "string" + attribute "switch3", "string" + attribute "switch4", "string" + attribute "switch6", "string" + attribute "switch7", "string" + attribute "switch9", "string" + attribute "switch10", "string" + attribute "switch11", "string" + attribute "switch12", "string" + attribute "switch13", "string" + attribute "switch14", "string" + attribute "switch15", "string" + attribute "switch16", "string" + attribute "status", "string" + + command "programOn" + command "programOff" + command "on" + command "off" + command "z1on" + command "z1off" + command "z2on" + command "z2off" + command "z3on" + command "z3off" + command "z4on" + command "z4off" + command "z5on" + command "z5off" + command "z6on" + command "z6off" + command "z7on" + command "z7off" + command "z8on" + command "z8off" + command "z9on" + command "z9off" + command "z10on" + command "z10off" + command "z11on" + command "z11off" + command "z12on" + command "z12off" + command "z13on" + command "z13off" + command "z14on" + command "z14off" + command "z15on" + command "z15off" + command "z16on" + command "z16off" + command "offtime" + + command "refresh" + command "rain" + command "manual" + command "setDisplay" + + command "settingsMap" + command "writeTime" + command "writeType" + command "notify" + command "updated" + + fingerprint endpointId: "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", profileId: "0104", deviceId: "0002", deviceVersion: "00", inClusters: "0000,0003,0004,0005,0006,000F", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZ16-01" + + } + + // simulator metadata + simulator { + // status messages + + // reply messages + + } + preferences { + input description: "Press Configure button after making changes to these preferences", displayDuringSetup: true, type: "paragraph", element: "paragraph", title: "" + input "RainEnable", "bool", title: "Rain Sensor Attached?", required: false, displayDuringSetup: true + input "ManualTime", "number", title: "Automatic shutoff time when a zone is turned on manually?", required: false, displayDuringSetup: true + } + + // UI tile definitions + tiles { + + standardTile("status", "device.status") { + state "schedule", label: 'Schedule Set', icon: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_t.png" + state "finished", label: 'Spruce Finished', icon: "st.Outdoor.outdoor5", backgroundColor: "#46c2e8" + state "raintoday", label: 'Rain Today', icon: "st.custom.wuk.nt_chancerain" + state "rainy", label: 'Previous Rain', icon: "st.custom.wuk.nt_chancerain" + state "raintom", label: 'Rain Tomorrow', icon: "st.custom.wuk.nt_chancerain" + state "donewweek", label: 'Spruce Finished', icon: "st.Outdoor.outdoor5", backgroundColor: "#52c435" + state "skipping", label: 'Skip Today', icon: "st.Outdoor.outdoor20", backgroundColor: "#36cfe3" + state "moisture", label: '', icon: "st.Weather.weather2", backgroundColor: "#36cfe3" + state "pause", label: 'PAUSE', icon: "st.contact.contact.open", backgroundColor: "#f2a51f" + state "active", label: 'Active', icon: "st.Outdoor.outdoor12", backgroundColor: "#3DC72E" + state "season", label: 'Seasonal Adjustment', icon: "st.Outdoor.outdoor17", backgroundColor: "#ffb900" + state "disable", label: 'Disabled', icon: "st.secondary.off", backgroundColor: "#888888" + state "warning", label: '', icon: "st.categories.damageAndDanger", backgroundColor: "#ffff7f" + state "alarm", label: 'Alarm', icon: "st.categories.damageAndDanger", backgroundColor: "#f9240c" + } + standardTile("switch", "device.switch") { + //state "programOff", label: 'Start Program', action: "programOn", icon: "st.sonos.play-icon", backgroundColor: "#a9a9a9" + state "off", label: 'Start Program', action: "programOn", icon: "st.sonos.play-icon", backgroundColor: "#a9a9a9" + state "programOn", label: 'Initialize Program', action: "programOff", icon: "st.contact.contact.open", backgroundColor: "#f6e10e" + state "on", label: 'Program Running', action: "off", icon: "st.Outdoor.outdoor12", backgroundColor: "#3DC72E" + } + standardTile("rainsensor", "device.rainsensor") { + state "rainSensrooff", label: 'Rain Sensor Clear', icon: "st.Weather.weather14", backgroundColor: "#a9a9a9" + state "rainSensoron", label: 'Rain Detected', icon: "st.Weather.weather10", backgroundColor: "#f6e10e" + } + standardTile("switch1", "device.switch1") { + state "z1off", label: '1', action: "z1on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z1on", label: '1', action: "z1off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch2", "device.switch2") { + state "z2off", label: '2', action: "z2on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z2on", label: '2', action: "z2off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch3", "device.switch3", inactiveLabel: false) { + state "z3off", label: '3', action: "z3on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z3on", label: '3', action: "z3off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch4", "device.switch4", inactiveLabel: false) { + state "z4off", label: '4', action: "z4on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z4on", label: '4', action: "z4off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch5", "device.switch5", inactiveLabel: false) { + state "z5off", label: '5', action: "z5on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z5on", label: '5', action: "z5off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch6", "device.switch6", inactiveLabel: false) { + state "z6off", label: '6', action: "z6on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z6on", label: '6', action: "z6off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch7", "device.switch7", inactiveLabel: false) { + state "z7off", label: '7', action: "z7on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z7on", label: '7', action: "z7off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch8", "device.switch8", inactiveLabel: false) { + state "z8off", label: '8', action: "z8on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z8on", label: '8', action: "z8off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch9", "device.switch9", inactiveLabel: false) { + state "z9off", label: '9', action: "z9on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z9on", label: '9', action: "z9off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch10", "device.switch10", inactiveLabel: false) { + state "z10off", label: '10', action: "z10on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z10on", label: '10', action: "z10off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch11", "device.switch11", inactiveLabel: false) { + state "z11off", label: '11', action: "z11on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z11on", label: '11', action: "z11off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch12", "device.switch12", inactiveLabel: false) { + state "z12off", label: '12', action: "z12on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z12on", label: '12', action: "z12off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch13", "device.switch13", inactiveLabel: false) { + state "z13off", label: '13', action: "z13on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z13on", label: '13', action: "z13off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch14", "device.switch14", inactiveLabel: false) { + state "z14off", label: '14', action: "z14on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z14on", label: '14', action: "z14off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch15", "device.switch15", inactiveLabel: false) { + state "z15off", label: '15', action: "z15on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z15on", label: '15', action: "z15off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("switch16", "device.switch16", inactiveLabel: false) { + state "z16off", label: '16', action: "z16on", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "z16on", label: '16', action: "z16off", icon: "st.valves.water.open", backgroundColor: "#46c2e8" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", action: "refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main (["status"]) + details(["status","rainsensor","switch","switch1","switch2","switch3","switch4","switch5","switch6","switch7","switch8","switch9","switch10","switch11","switch12","switch13","switch14","switch15","switch16","refresh","configure"]) + } +} + +def programOn(){ + sendEvent(name: "switch", value: "programOn", descriptionText: "Program turned on") + } + +def programOff(){ + sendEvent(name: "switch", value: "off", descriptionText: "Program turned off") + off() + } + +def updated(){ + log.debug "updated" +} + +// Parse incoming device messages to generate events +def parse(String description) { + //log.debug "Parse description $description" + def result = null + def map = [:] + if (description?.startsWith("read attr -")) { + def descMap = parseDescriptionAsMap(description) + //log.debug "Desc Map: $descMap" + //using 000F cluster instead of 0006 (switch) because ST does not differentiate between EPs and processes all as switch + if (descMap.cluster == "000F" && descMap.attrId == "0055") { + log.debug "Zone" + map = getZone(descMap) + } + else if (descMap.cluster == "0009" && descMap.attrId == "0000") { + log.debug "Alarm" + map = getAlarm(descMap) + } + } + + if (map) { + result = createEvent(map) + } + log.debug "Parse returned $map $result" + return result +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def getZone(descMap){ + def map = [:] + + def EP = Integer.parseInt(descMap.endpoint.trim(), 16) + + String onoff + if(descMap.value == "00"){ + onoff = "off" + } + else onoff = "on" + + if (EP == 1){ + map.name = "switch" + map.value = onoff + map.descriptionText = "${device.displayName} turned sprinkler program $onoff" + } + + else if (EP == 18) { + map.name = "rainsensor" + map.value = "rainSensor" + onoff + map.descriptionText = "${device.displayName} rain sensor is $onoff" + } + else { + EP -= 1 + map.name = "switch" + EP + map.value = "z" + EP + onoff + map.descriptionText = "${device.displayName} turned Zone $EP $onoff" + } + + map.isStateChange = true + map.displayed = true + return map +} + +def getAlarm(descMap){ + def map = [:] + map.name = "status" + def alarmID = Integer.parseInt(descMap.value.trim(), 16) + log.debug "${alarmID}" + if(alarmID <= 0) map.descriptionText = "${device.displayName} has rebooted, no other alarms" + else map.descriptionText = "${device.displayName} rebooted, reported error on zone ${alarmID - 1}, please check zone is working correctly" + map.value = "alarm" + map.isStateChange = true + map.displayed = true + return map +} + +//status notify and change status +def notify(value, text){ + sendEvent(name:"status", value:"$value", descriptionText:"$text", isStateChange: true, display: false) + +} + +//prefrences - rain sensor, manual time +def rain() { + log.debug "Rain $RainEnable" + if (RainEnable) "st wattr 0x${device.deviceNetworkId} 18 0x0F 0x51 0x10 {01}" + else "st wattr 0x${device.deviceNetworkId} 18 0x0F 0x51 0x10 {00}" +} +def manual(){ + log.debug "Time $ManualTime" + def mTime = 10 + if (ManualTime) mTime = ManualTime + def manualTime = hex(mTime) + "st wattr 0x${device.deviceNetworkId} 1 6 0x4002 0x21 {00${manualTime}}" + + } + +//write switch time settings map +def settingsMap(WriteTimes, attrType){ + log.debug WriteTimes + + def i = 1 + def runTime + def sendCmds = [] + while(i <= 17){ + + if (WriteTimes."${i}"){ + runTime = hex(Integer.parseInt(WriteTimes."${i}")) + log.debug "${i} : $runTime" + + if (attrType == 4001) sendCmds.push("st wattr 0x${device.deviceNetworkId} ${i} 0x06 0x4001 0x21 {00${runTime}}") + else sendCmds.push("st wattr 0x${device.deviceNetworkId} ${i} 0x06 0x4002 0x21 {00${runTime}}") + sendCmds.push("delay 500") + } + i++ + } + return sendCmds +} + +//send switch time +def writeType(wEP, cycle){ + log.debug "wt ${wEP} ${cycle}" + "st wattr 0x${device.deviceNetworkId} ${wEP} 0x06 0x4001 0x21 {00" + hex(cycle) + "}" + } +//send switch off time +def writeTime(wEP, runTime){ + "st wattr 0x${device.deviceNetworkId} ${wEP} 0x06 0x4002 0x21 {00" + hex(runTime) + "}" + } + +//set reporting and binding +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting and Bindings ${device.deviceNetworkId} ${device.zigbeeId}" + sendEvent(name: 'configuration',value: 100, descriptionText: "Configuration initialized") + + def configCmds = [ + //program on/off + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x09 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + //zones 1-8 + "zdo bind 0x${device.deviceNetworkId} 2 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 3 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 4 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 5 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 6 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 7 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 8 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 9 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + //zones 9-16 + "zdo bind 0x${device.deviceNetworkId} 10 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 11 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 12 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 13 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 14 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 15 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 16 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 17 1 0x0F {${device.zigbeeId}} {}", "delay 1000", + //rain sensor + "zdo bind 0x${device.deviceNetworkId} 18 1 0x0F {${device.zigbeeId}} {}", + + "zcl global send-me-a-report 6 0 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 2", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 3", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 4", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 5", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 6", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 7", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 8", "delay 500", + + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 9", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 10", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 11", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 12", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 13", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 14", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 15", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 16", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 17", "delay 500", + + "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 18", "delay 500", + + "zcl global send-me-a-report 0x09 0x00 0x21 1 0 {00}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 500" + ] + return configCmds + rain() + manual() +} + + + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} + +def refresh() { + + log.debug "refresh" + def refreshCmds = [ + + "st rattr 0x${device.deviceNetworkId} 1 0x0F 0x55", "delay 500", + + "st rattr 0x${device.deviceNetworkId} 2 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 3 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 4 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 5 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 6 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 7 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 8 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 9 0x0F 0x55", "delay 500", + + "st rattr 0x${device.deviceNetworkId} 10 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 11 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 12 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 13 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 14 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 15 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 16 0x0F 0x55", "delay 500", + "st rattr 0x${device.deviceNetworkId} 17 0x0F 0x55", "delay 500", + + "st rattr 0x${device.deviceNetworkId} 18 0x0F 0x51","delay 500", + + ] + return refreshCmds + rain() + manual() +} + +// Commands to device +//zones on - 8 +def on() { + //sendEvent(name:"status", value:"active", descriptionText:"Program Running", isStateChange: true, display: false) + log.debug "on" + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +} +def off() { + log.debug "off" + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +} +def z1on() { + "st cmd 0x${device.deviceNetworkId} 2 6 1 {}" +} +def z1off() { + "st cmd 0x${device.deviceNetworkId} 2 6 0 {}" +} +def z2on() { + "st cmd 0x${device.deviceNetworkId} 3 6 1 {}" +} +def z2off() { + "st cmd 0x${device.deviceNetworkId} 3 6 0 {}" +} +def z3on() { + "st cmd 0x${device.deviceNetworkId} 4 6 1 {}" +} +def z3off() { + "st cmd 0x${device.deviceNetworkId} 4 6 0 {}" +} +def z4on() { + "st cmd 0x${device.deviceNetworkId} 5 6 1 {}" +} +def z4off() { + "st cmd 0x${device.deviceNetworkId} 5 6 0 {}" +} +def z5on() { + "st cmd 0x${device.deviceNetworkId} 6 6 1 {}" +} +def z5off() { + "st cmd 0x${device.deviceNetworkId} 6 6 0 {}" +} +def z6on() { + "st cmd 0x${device.deviceNetworkId} 7 6 1 {}" +} +def z6off() { + "st cmd 0x${device.deviceNetworkId} 7 6 0 {}" +} +def z7on() { + "st cmd 0x${device.deviceNetworkId} 8 6 1 {}" +} +def z7off() { + "st cmd 0x${device.deviceNetworkId} 8 6 0 {}" +} +def z8on() { + "st cmd 0x${device.deviceNetworkId} 9 6 1 {}" +} +def z8off() { + "st cmd 0x${device.deviceNetworkId} 9 6 0 {}" +} + +//zones 9 - 16 +def z9on() { + "st cmd 0x${device.deviceNetworkId} 10 6 1 {}" +} +def z9off() { + "st cmd 0x${device.deviceNetworkId} 10 6 0 {}" +} +def z10on() { + "st cmd 0x${device.deviceNetworkId} 11 6 1 {}" +} +def z10off() { + "st cmd 0x${device.deviceNetworkId} 11 6 0 {}" +} +def z11on() { + "st cmd 0x${device.deviceNetworkId} 12 6 1 {}" +} +def z11off() { + "st cmd 0x${device.deviceNetworkId} 12 6 0 {}" +} +def z12on() { + "st cmd 0x${device.deviceNetworkId} 13 6 1 {}" +} +def z12off() { + "st cmd 0x${device.deviceNetworkId} 13 6 0 {}" +} +def z13on() { + "st cmd 0x${device.deviceNetworkId} 14 6 1 {}" +} +def z13off() { + "st cmd 0x${device.deviceNetworkId} 14 6 0 {}" +} +def z14on() { + "st cmd 0x${device.deviceNetworkId} 15 6 1 {}" +} +def z14off() { + "st cmd 0x${device.deviceNetworkId} 15 6 0 {}" +} +def z15on() { + "st cmd 0x${device.deviceNetworkId} 16 6 1 {}" +} +def z15off() { + "st cmd 0x${device.deviceNetworkId} 16 6 0 {}" +} +def z16on() { + "st cmd 0x${device.deviceNetworkId} 17 6 1 {}" +} +def z16off() { + "st cmd 0x${device.deviceNetworkId} 17 6 0 {}" +} \ No newline at end of file diff --git a/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy new file mode 100644 index 0000000..944ca52 --- /dev/null +++ b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy @@ -0,0 +1,397 @@ +/** + * Spruce Sensor -Pre-release V2 10/8/2015 + * + * Copyright 2014 Plaid Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + -------10/20/2015 Updates-------- + -Fix/add battery reporting interval to update + -remove polling and/or refresh(?) + */ +metadata { + definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "NCauffman") { + + capability "Configuration" + capability "Battery" + capability "Relative Humidity Measurement" + capability "Temperature Measurement" + capability "Sensor" + //capability "Polling" + + attribute "maxHum", "string" + attribute "minHum", "string" + + command "resetHumidity" + command "refresh" + + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01" + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph", title: "" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + input "interval", "number", title: "Measurement Interval 1-120 minutes (default: 10 minutes)", description: "Set how often you would like to check soil moisture in minutes", range: "1..120", defaultValue: 10, displayDuringSetup: false + input "resetMinMax", "bool", title: "Reset Humidity min and max", required: false, displayDuringSetup: false + } + + tiles { + valueTile("temperature", "device.temperature", canChangeIcon: false, canChangeBackground: false) { + state "temperature", 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", width: 2, height: 2, canChangeIcon: false, canChangeBackground: true) { + state "humidity", label:'${currentValue}%', unit:"", + backgroundColors:[ + [value: 0, color: "#635C0C"], + [value: 16, color: "#EBEB21"], + [value: 22, color: "#C7DE6A"], + [value: 42, color: "#9AD290"], + [value: 64, color: "#44B621"], + [value: 80, color: "#3D79D9"], + [value: 96, color: "#0A50C2"] + ] + } + + valueTile("maxHum", "device.maxHum", canChangeIcon: false, canChangeBackground: false) { + state "maxHum", label:'High ${currentValue}%', unit:"", + backgroundColors:[ + [value: 0, color: "#635C0C"], + [value: 16, color: "#EBEB21"], + [value: 22, color: "#C7DE6A"], + [value: 42, color: "#9AD290"], + [value: 64, color: "#44B621"], + [value: 80, color: "#3D79D9"], + [value: 96, color: "#0A50C2"] + ] + } + valueTile("minHum", "device.minHum", canChangeIcon: false, canChangeBackground: false) { + state "minHum", label:'Low ${currentValue}%', unit:"", + backgroundColors:[ + [value: 0, color: "#635C0C"], + [value: 16, color: "#EBEB21"], + [value: 22, color: "#C7DE6A"], + [value: 42, color: "#9AD290"], + [value: 64, color: "#44B621"], + [value: 80, color: "#3D79D9"], + [value: 96, color: "#0A50C2"] + ] + } + + valueTile("battery", "device.battery", decoration: "flat", canChangeIcon: false, canChangeBackground: false) { + state "battery", label:'${currentValue}% battery' + } + + main (["humidity"]) + details(["humidity","maxHum","minHum","temperature","battery"]) + } +} + +def parse(String description) { + log.debug "Parse description $description config: ${device.latestValue('configuration')} interval: $interval" + + Map map = [:] + + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) { + map = parseCustomMessage(description) + } + def result = map ? createEvent(map) : null + + //check in configuration change + if (!device.latestValue('configuration')) result = poll() + if (device.latestValue('configuration').toInteger() != interval && interval != null) { + result = poll() + } + log.debug "result: $result" + return result + +} + + + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def linkText = getLinkText(device) + //log.debug "Catchall" + def descMap = zigbee.parse(description) + + //check humidity configuration is complete + if (descMap.command == 0x07 && descMap.clusterId == 0x0405){ + def configInterval = 10 + if (interval != null) configInterval = interval + sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration Successful") + //setConfig() + log.debug "config complete" + //return resultMap = [name: 'configuration', value: configInterval, descriptionText: "Settings configured successfully"] + } + else if (descMap.command == 0x0001){ + def hexString = "${hex(descMap.data[5])}" + "${hex(descMap.data[4])}" + def intString = Integer.parseInt(hexString, 16) + //log.debug "command: $descMap.command clusterid: $descMap.clusterId $hexString $intString" + + if (descMap.clusterId == 0x0402){ + def value = getTemperature(hexString) + resultMap = getTemperatureResult(value) + } + else if (descMap.clusterId == 0x0405){ + def value = Math.round(new BigDecimal(intString / 100)).toString() + resultMap = getHumidityResult(value) + + } + else return null + } + else return null + + return resultMap +} + +private Map parseReportAttributeMessage(String description) { + def descMap = parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + log.debug "Report Attributes" + + Map resultMap = [:] + if (descMap.cluster == "0001" && descMap.attrId == "0000") { + resultMap = getBatteryResult(descMap.value) + } + return resultMap +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + + log.debug "parseCustom" + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + else if (description?.startsWith('humidity: ')) { + def pct = (description - "humidity: " - "%").trim() + if (pct.isNumber()) { + def value = Math.round(new BigDecimal(pct)).toString() + resultMap = getHumidityResult(value) + } else { + log.error "invalid humidity: ${pct}" + } + } + return resultMap +} + +private Map getHumidityResult(value) { + def linkText = getLinkText(device) + def maxHumValue = 0 + def minHumValue = 0 + if (device.currentValue("maxHum") != null) maxHumValue = device.currentValue("maxHum").toInteger() + if (device.currentValue("minHum") != null) minHumValue = device.currentValue("minHum").toInteger() + log.debug "Humidity max: ${maxHumValue} min: ${minHumValue}" + def compare = value.toInteger() + + if (compare > maxHumValue) { + sendEvent(name: 'maxHum', value: value, unit: '%', descriptionText: "${linkText} soil moisture high is ${value}%") + } + else if (((compare < minHumValue) || (minHumValue <= 2)) && (compare != 0)) { + sendEvent(name: 'minHum', value: value, unit: '%', descriptionText: "${linkText} soil moisture low is ${value}%") + } + + return [ + name: 'humidity', + value: value, + unit: '%', + descriptionText: "${linkText} soil moisture is ${value}%" + ] +} + + + +def getTemperature(value) { + def celsius = (Integer.parseInt(value, 16).shortValue()/100) + //log.debug "Report Temp $value : $celsius C" + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +private Map getTemperatureResult(value) { + log.debug "Temperature: $value" + def linkText = getLinkText(device) + + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} is ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] +} + +private Map getBatteryResult(value) { + log.debug 'Battery' + def linkText = getLinkText(device) + + def result = [ + name: 'battery' + ] + + def min = 2500 + def percent = ((Integer.parseInt(value, 16) - min) / 5) + percent = Math.max(0, Math.min(percent, 100.0)) + result.value = Math.round(percent) + + def descriptionText + if (percent < 10) result.descriptionText = "${linkText} battery is getting low $percent %." + else result.descriptionText = "${linkText} battery is ${result.value}%" + + return result +} + +def resetHumidity(){ + def linkText = getLinkText(device) + def minHumValue = 0 + def maxHumValue = 0 + sendEvent(name: 'minHum', value: minHumValue, unit: '%', descriptionText: "${linkText} min soil moisture reset to ${minHumValue}%") + sendEvent(name: 'maxHum', value: maxHumValue, unit: '%', descriptionText: "${linkText} max soil moisture reset to ${maxHumValue}%") +} + +def setConfig(){ + def configInterval = 100 + if (interval != null) configInterval = interval + sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration initialized") +} + +//when device preferences are changed +def updated(){ + log.debug "device updated" + if (!device.latestValue('configuration')) configure() + else{ + if (resetMinMax == true) resetHumidity() + if (device.latestValue('configuration').toInteger() != interval && interval != null){ + sendEvent(name: 'configuration',value: 0, descriptionText: "Settings changed and will update at next report. Measure interval set to ${interval} mins") + } + } +} + +//poll +def poll() { + log.debug "poll called" + List cmds = [] + if (!device.latestValue('configuration')) cmds += configure() + else if (device.latestValue('configuration').toInteger() != interval && interval != null) { + cmds += intervalUpdate() + } + //cmds += refresh() + log.debug "commands $cmds" + return cmds?.collect { new physicalgraph.device.HubAction(it) } +} + +//update intervals +def intervalUpdate(){ + log.debug "intervalUpdate" + def minReport = 10 + def maxReport = 610 + if (interval != null) { + minReport = interval + maxReport = interval * 61 + } + [ + "zcl global send-me-a-report 0x405 0x0000 0x21 $minReport $maxReport {6400}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + "zcl global send-me-a-report 1 0x0000 0x21 0x0C 0 {0500}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + ] +} + +def refresh() { + log.debug "refresh" + [ + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} 1 0x405 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} 1 1 0" + ] +} + +//configure +def configure() { + //set minReport = measurement in minutes + def minReport = 10 + def maxReport = 610 + + //String zigbeeId = swapEndianHex(device.hub.zigbeeId) + //log.debug "zigbeeid ${device.zigbeeId} deviceId ${device.deviceNetworkId}" + if (!device.zigbeeId) sendEvent(name: 'configuration',value: 0, descriptionText: "Device Zigbee Id not found, remove and attempt to rejoin device") + else sendEvent(name: 'configuration',value: 100, descriptionText: "Configuration initialized") + //log.debug "Configuring Reporting and Bindings. min: $minReport max: $maxReport " + + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x405 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 1000", + + //temperature + "zcl global send-me-a-report 0x402 0x0000 0x29 1 0 {3200}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + //min = soil measure interval + "zcl global send-me-a-report 0x405 0x0000 0x21 $minReport $maxReport {6400}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + //min = battery measure interval 1 = 1 hour + "zcl global send-me-a-report 1 0x0000 0x21 0x0C 0 {0500}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500" + ] + refresh() +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} \ No newline at end of file diff --git a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy index c3a8c46..ff9d43a 100644 --- a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy +++ b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy @@ -1,8 +1,7 @@ /** - * Spruce Scheduler Pre-release V2.2 10/13/2015 - * - * Thanks Jason for the scheduler improvements + * Spruce Scheduler Pre-release V2.5 12/22/2016 * + * * Copyright 2015 Plaid Systems * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -14,6 +13,17 @@ * 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. * + +-------v2.51--------------------- + schedule function changed so runIn does not overwrite and cancel schedule + -ln 769 schedule cycleOn-> checkOn + -ln 841 checkOn function + -ln 863 state.run = false + +-------Fixes + -changed weather from def to Map + -ln 968 if(runnowmap) -> pumpmap + -------Fixes V2.2------------- -History log messages condensed -Seasonal adjustment redefined -> weekly & daily @@ -39,9 +49,9 @@ definition( name: "Spruce Scheduler", - namespace: "Plaidsystems", + namespace: "plaidsystems", author: "NCauffman", - description: "Spruce automatic water scheduling app V2.2", + description: "Spruce automatic water scheduling app v2.5", category: "Green Living", iconUrl: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", iconX2Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", @@ -66,7 +76,7 @@ preferences { } def startPage(){ - dynamicPage(name: "startPage", title: "Spruce Smart Irrigation setup V2.2", install: true, uninstall: true) + dynamicPage(name: "startPage", title: "Spruce Smart Irrigation setup V2.51", install: true, uninstall: true) { section(""){ href(name: "globalPage", title: "Schedule settings", required: false, page: "globalPage", @@ -137,8 +147,8 @@ def weatherPage() { required: false, image: "http://www.plaidsystems.com/smartthings/rain.png") - input "rainDelay", "decimal", title: "inches of rain that will delay watering, default: 0.2", defaultValue: '.2', required: false - input "isSeason", "bool", title: "Enable Seasonal Weather Adjustment:", defaultValue: 'true', 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']] } } } @@ -427,11 +437,11 @@ 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 "gain", "number", title: "Increase or decrease all water times by this %, enter a negative or positive value, Default: 0", required: false, defaultValue: '0', range: "*..*", submitOnChange: true + input "gain", "number", title: "Increase or decrease all water times by this %, enter a negative or positive value, Default: 0", required: false paragraph image: "http://www.plaidsystems.com/smartthings/st_sensor_200_r.png", title: "Moisture sensor learn mode", "Learn mode: Watering times will be adjusted based on the assigned moisture sensor and watering will follow a schedule.\n\nNo Learn mode: Zones with moisture sensors will water on any available days when the low moisture setpoint has been reached." - input "learn", "bool", title: "Enable learning (with moisture sensors)", defaultValue: 'true', metadata: [values: ['true', 'false']] + input "learn", "bool", title: "Enable learning (with moisture sensors)", metadata: [values: ['true', 'false']] } } } @@ -441,11 +451,11 @@ def zoneSetPage(params){ section(""){ paragraph image: "http://www.plaidsystems.com/smartthings/st_${state.app}.png", title: "Current Settings", - "${display("${state.app}")}" + "${display("${state.app}")}" } section(""){ - input "name${state.app}", "text", title: "Zone name?", required: false, defaultValue: "Zone ${state.app}", submitOnChange: true - } + 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}"]}")}", @@ -473,12 +483,12 @@ def zoneSetPage(params){ } section(""){ paragraph image: "http://www.plaidsystems.com/smartthings/st_timer.png", - title: "Optional time adjustments", "" + title: "Enter total watering time per week or ", "" input "minWeek${state.app}", "number", title: "Water time per week (minutes). Default: 0 = autoadjust", required: false input "perDay${state.app}", "number", title: "Guideline value for dividing minutes per week into watering days, a high value means more water per day. Default: 20", defaultValue: '20', required: false - } + } } } @@ -737,6 +747,7 @@ 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.fail = 0 state.seasonAdj = 0 state.weekseasonAdj = 0 log.debug "Installed with settings: ${settings}" @@ -752,10 +763,13 @@ def updated() { def installSchedule(){ if(switches && startTime) { - def checkTime = timeToday(startTime, location.timeZone) + def runTime = timeToday(startTime, location.timeZone) + def checktime = timeToday(startTime, location.timeZone).getTime() - 120000 + log.debug "checktime: $checktime runtime: $runTime" if(enable) { subscribe switches, "switch.programOn", manualStart - schedule(checkTime, Check) + schedule(checktime, Check) + schedule(runTime, checkOn) note("schedule", "Schedule set to start at ${startTimeString()}", "w") writeSettings() } @@ -825,23 +839,30 @@ def getRunDays(day1,day2,day3,day4,day5,day6,day7) str = "0 Days/week" return str } - + +def checkOn(){ + cycleOn() +} + //start water program -def cycleOn(){ - subscribe switches, "switch.off", cycleOff - if (sync != null) subscribe sync, "status.finished", syncOn - if (contact != null){ - subscribe contact, "contact.open", doorOpen - subscribe contact, "contact.closed", doorClosed - } - if (sync != null && !sync.currentValue('status').contains('finished')) note("pause", "waiting for $sync to complete before starting schedule", "w") - else if (contact == null || !contact.currentValue('contact').contains('open')) runIn(15, resume) //15 second delay to allow writesettings to finish resume -> switches.on - else note("pause", "$contact opened $switches paused watering", "w") +def cycleOn(){ + if (state.run == true){ + subscribe switches, "switch.off", cycleOff + if (sync != null) subscribe sync, "status.finished", syncOn + if (contact != null){ + subscribe contact, "contact.open", doorOpen + subscribe contact, "contact.closed", doorClosed + } + if (sync != null && !sync.currentValue('status').contains('finished')) note("pause", "waiting for $sync to complete before starting schedule", "w") + else if (contact == null || !contact.currentValue('contact').contains('open')) resume() //runIn(15, resume) //15 second delay to allow writesettings to finish resume -> switches.on + else note("pause", "$contact opened $switches paused watering", "w") + } } //when switch reports off, watering program is finished def cycleOff(evt){ - if (contact == null || !contact.currentValue('contact').contains('open')){ + state.run = false + if (contact == null || !contact.currentValue('contact').contains('open')){ note("finished", "finished watering for today", "d") unsubscribe(contact) } @@ -849,12 +870,29 @@ def cycleOff(evt){ //start check def manualStart(evt){ - Check() + + def runNowMap = [] + runNowMap = cycleLoop() + if (runNowMap) + { + state.run = true + runNowMap = "Water will begin in 1 minute:\n" + runNowMap + note("active", "${runNowMap}", "d") + runIn(60, cycleOn) //start water program + } + + else { + switches.programOff() + state.run = false + note("skipping", "No watering scheduled for today.", "d") + } + } //run check each day at scheduled time def Check(){ + state.run = true // Create weekly water summary, if requested, on Sunday if(notify && notify.contains('Weekly') && (getWeekDay() == 7)) { @@ -874,23 +912,30 @@ def Check(){ def runNowMap = [] if (isDay() == false){ switches.programOff() + state.run = false note("skipping", "No watering allowed today.", "d") } else if (isWeather() == false) - { + { //get & set watering times for today - runNowMap = cycleLoop() + runNowMap = cycleLoop() if (runNowMap) { + state.run = true + runNowMap = "Water will begin in 2 minutes:\n" + runNowMap note("active", "${runNowMap}", "d") - cycleOn() //start water program + //cycleOn() //start water program } else { switches.programOff() + state.run = false note("skipping", "No watering scheduled for today.", "d") } } - else switches.programOff() + else { + switches.programOff() + state.run = false + } } //get todays schedule @@ -907,6 +952,7 @@ def cycleLoop() while(zone <= 16) { rtime = 0 + //change to tpw(?) if(settings["zone${zone}"] != null && settings["zone${zone}"] != 'Off' && nozzle(zone) != 4) { // First check if we run this zone today, use either dpwMap or even/odd date @@ -925,12 +971,11 @@ def cycleLoop() runToday = dpwMap[weekDay] //1 or 0 } //if no learn check moisture sensors on available days - if ( (isDay() == true) && !learn && (settings["sensor${zone}"] != null) ) runToday = 1 + if (!learn && (settings["sensor${zone}"] != null) ) runToday = 1 if(runToday) { - //def soil = moisture(zone) - def soil = moisture(zone) + def soil = moisture(zone) soilString += "${soil[1]}" // Run this zone if soil moisture needed or if it is a weekly @@ -939,7 +984,7 @@ def cycleLoop() { cyc = cycles(zone) dpw = getDPW(zone) - rtime = calcRunTime(getTPW(zone), dpw) + rtime = calcRunTime(getTPW(zone), dpw) //daily weather adjust if no sensor if(isSeason && settings["sensor${zone}"] == null) rtime = Math.round(rtime / cyc * state.seasonAdj / 100) // runTime is total run time devided by num cycles @@ -960,7 +1005,7 @@ def cycleLoop() //send settings to Spruce Controller switches.settingsMap(timeMap,4002) if (runNowMap) return runNowMap += pumpMap - return runNowMap + return runNowMap } //Initialize Days per week, based on TPW, perDay and daysAvailable settings @@ -991,7 +1036,7 @@ def getDPW(zone) //Initialize Time per Week def initTPW(i){ - if("${settings["zone${i}"]}" == "null" || nozzle(i) == 0 || nozzle(i) == 4 || plant(i) == 0) return 0 + if("${settings["zone${i}"]}" == null || nozzle(i) == 0 || nozzle(i) == 4 || plant(i) == 0) return 0 // apply gain adjustment def gainAdjust = 100 @@ -1242,36 +1287,45 @@ def setSeason() { //def newTPW = Math.round(tpw * tpwAdjust / 100) state.tpwMap.putAt(zone-1, tpw) state.dpwMap.putAt(zone-1, initDPW(zone)) + log.debug "Zone ${zone}: seasonaly adjusted by ${state.weekseasonAdj-100}% to ${tpw}" } - log.debug "Zone ${zone}: seasonaly adjusted by ${state.weekseasonAdj-100}% to ${tpw}" + zone++ } } //check weather -def isWeather(){ +def isWeather(){ def wzipcode = "${zipString()}" - + // Forecast rain - def sdata = getWeatherFeature("forecast10day", wzipcode) + Map sdata = getWeatherFeature("forecast10day", wzipcode) + + log.debug sdata.response + if(sdata.response.containsKey('error') || sdata == null) { + note("season", "Weather API error, skipping weather check" , "f") + return false + } def qpf = sdata.forecast.simpleforecast.forecastday.qpf_allday.mm def qpfTodayIn = 0 if (qpf.get(0).isNumber()) qpfTodayIn = Math.round(qpf.get(0).toInteger() /25.4 * 100) /100 + log.debug "qpfTodayIn ${qpfTodayIn}" def qpfTomIn = 0 if (qpf.get(1).isNumber()) qpfTomIn = Math.round(qpf.get(1).toInteger() /25.4 * 100) /100 - + log.debug "qpfTomIn ${qpfTomIn}" // current conditions - def cond = getWeatherFeature("conditions", wzipcode) + Map cond = getWeatherFeature("conditions", wzipcode) + def TRain = 0 if (cond.current_observation.precip_today_metric.isNumber()) TRain = Math.round(cond.current_observation.precip_today_metric.toInteger() /25.4 * 100) /100 - + log.debug "TRain ${TRain}" // reported rain - def yCond = getWeatherFeature("yesterday", wzipcode) + Map yCond = getWeatherFeature("yesterday", wzipcode) def YRain = 0 if (yCond.history.dailysummary.precipi.get(0).isNumber()) YRain = yCond.history.dailysummary.precipi.get(0) if(TRain > qpfTodayIn) qpfTodayIn = TRain - + log.debug "TRain ${TRain} qpfTodayIn ${qpfTodayIn}" //state.Rain = [S,M,T,W,T,F,S] //state.Rain = [0,0.43,3,0,0,0,0] def day = getWeekDay() @@ -1282,18 +1336,20 @@ def isWeather(){ def factor = 0 if ((day - i) > 0) factor = day - i else factor = day + 7 - i - weeklyRain += Math.round(state.Rain.get(i).toFloat() / factor * 100)/100 + def getrain = state.Rain.get(i) + weeklyRain += Math.round(getrain.toFloat() / factor * 100)/100 i++ - } + } + log.debug "weeklyRain ${weeklyRain}" //note("season", "weeklyRain ${weeklyRain} ${state.Rain}", "d") - + //get highs def getHigh = sdata.forecast.simpleforecast.forecastday.high.fahrenheit def avgHigh = Math.round((getHigh.get(0).toInteger() + getHigh.get(1).toInteger() + getHigh.get(2).toInteger() + getHigh.get(3).toInteger() + getHigh.get(4).toInteger())/5) - def citydata = getWeatherFeature("geolookup", wzipcode) + Map citydata = getWeatherFeature("geolookup", wzipcode) def weatherString = "${citydata.location.city} weather\n Today: ${getHigh.get(0)}F, ${qpfTodayIn}in rain\n Tomorrow: ${getHigh.get(1)}F, ${qpfTomIn}in rain\n Yesterday: ${YRain}in rain " - + if (isSeason) { //daily adjust @@ -1308,7 +1364,7 @@ def isWeather(){ def humWeek = Math.round((gethum.get(0).toInteger() + gethum.get(1).toInteger() + gethum.get(2).toInteger() + gethum.get(3).toInteger() + gethum.get(4).toInteger())/5) //get daylight - def astro = getWeatherFeature("astronomy", wzipcode) + Map astro = getWeatherFeature("astronomy", wzipcode) def getsunRH = astro.moon_phase.sunrise.hour def getsunRM = astro.moon_phase.sunrise.minute def getsunSH = astro.moon_phase.sunset.hour @@ -1323,28 +1379,29 @@ def isWeather(){ //note("season", "Applying seasonal adjustment of ${state.weekseasonAdj-100}% this week", "f") setSeason() } - } - - note("season", "${weatherString}" , "f") + } - def rainmessage = "" - if (switches.latestValue("rainsensor") == "rainSensoron"){ + note("season", weatherString , "f") + + def setrainDelay = "0.2" + if (rainDelay) setrainDelay = rainDelay + if (switches.latestValue("rainsensor") == "rainsensoron"){ note("raintoday", "is skipping watering, rain sensor is on.", "d") return true } - else if (qpfTodayIn >= rainDelay){ + else if (qpfTodayIn > setrainDelay.toFloat()){ note("raintoday", "is skipping watering, ${qpfTodayIn}in rain today.", "d") return true } - else if (qpfTomIn >= rainDelay){ + else if (qpfTomIn > setrainDelay.toFloat()){ note("raintom", "is skipping watering, ${qpfTomIn}in rain expected tomorrow.", "d") return true } - else if (weeklyRain >= rainDelay){ + else if (weeklyRain > setrainDelay.toFloat()){ note("rainy", "is skipping watering, ${weeklyRain}in average rain over the past week.", "d") return true } - else return false + return false } @@ -1360,6 +1417,7 @@ def doorClosed(evt){ def resume(){ switches.on() + state.fail = 10 } def syncOn(evt){ @@ -1556,4 +1614,4 @@ def createDPWMap() { state.DPWDays5 = runDays[4] state.DPWDays6 = runDays[5] state.DPWDays7 = runDays[6] -} \ No newline at end of file +} From 81fb356a216b13d5b3d32e017fb97a878edc87e9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Fri, 12 Feb 2016 14:31:04 -0500 Subject: [PATCH 10/15] PROB-537 - Fix error in line 335 --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 053c266..b8dd89b 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -326,6 +326,7 @@ def addBulbs() { d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) } log.debug "created ${d.displayName} with id $dni" + d.refresh() } else { log.debug "$dni in not longer paired to the Hue Bridge or ID changed" } @@ -333,8 +334,8 @@ def addBulbs() { //backwards compatable newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name]) + d.refresh() } - d.refresh() } else { log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'" if (bulbs instanceof java.util.Map) { @@ -774,4 +775,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) { private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } -} \ No newline at end of file +} From d9aa1e378d1188c47214d59db9bc95a18f6901c7 Mon Sep 17 00:00:00 2001 From: vlaminck Date: Mon, 15 Feb 2016 21:39:06 -0600 Subject: [PATCH 11/15] remove segmented style input to prevent iOS crash --- .../smartthings/gentle-wake-up.src/gentle-wake-up.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy index 9b5f11c..11f295c 100644 --- a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy +++ b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy @@ -201,8 +201,8 @@ def completionPage() { section("Switches") { input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true) - if (completionSwitches || androidClient()) { - input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on") + if (completionSwitches) { + input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on") input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)") } } From 664af57708210f96efda286a6282de7418bfada8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Tue, 16 Feb 2016 09:35:21 -0500 Subject: [PATCH 12/15] Removed canInstallLabs() --- .../hue-connect.src/hue-connect.groovy | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index b8dd89b..b8d3f54 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -35,23 +35,11 @@ preferences { } def mainPage() { - if(canInstallLabs()) { - def bridges = bridgesDiscovered() - if (state.username && bridges) { - return bulbDiscovery() - } else { - return bridgeDiscovery() - } + def bridges = bridgesDiscovered() + if (state.username && bridges) { + return bulbDiscovery() } else { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { - section("Upgrade") { - paragraph "$upgradeNeeded" - } - } + return bridgeDiscovery() } } @@ -765,10 +753,6 @@ private String convertHexToIP(hex) { [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } -private Boolean canInstallLabs() { - return hasAllHubsOver("000.011.00603") -} - private Boolean hasAllHubsOver(String desiredFirmware) { return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } } From 86e097ba0a0297d277938cddcc8e050e81374b66 Mon Sep 17 00:00:00 2001 From: dylanbijnagte Date: Fri, 18 Dec 2015 12:27:57 -0600 Subject: [PATCH 13/15] add event translation --- .../i18n/messages.properties | 29 +++++++ .../notify-me-when.src/notify-me-when.groovy | 84 ++++++++++--------- 2 files changed, 74 insertions(+), 39 deletions(-) create mode 100644 smartapps/smartthings/notify-me-when.src/i18n/messages.properties diff --git a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties new file mode 100644 index 0000000..a50bd54 --- /dev/null +++ b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties @@ -0,0 +1,29 @@ +'''Acceleration Detected'''.ko=가속화 감지됨 +'''Arrival Of'''.ko=도착 +'''Both Push and SMS?'''.ko=푸시 메시지와 SMS를 모두 사용하시겠습니까? +'''Button Pushed'''.ko=버튼이 눌렸습니다 +'''Contact Closes'''.ko=접점 닫힘 +'''Contact Opens'''.ko=접점 열림 +'''Departure Of'''.ko=출발 +'''Message Text'''.ko=문자 메시지 +'''Minutes'''.ko=분 +'''Motion Here'''.ko=동작 +'''Phone Number (for SMS, optional)'''.ko=휴대전화 번호(문자 메시지 - 옵션) +'''Receive notifications when anything happens in your home.'''.ko=집 안에 무슨 일이 일어나면 알림이 전송됩니다. +'''Smoke Detected'''.ko=연기가 감지되었습니다 +'''Switch Turned Off'''.ko=스위치 꺼짐 +'''Switch Turned On'''.ko=스위치 꺼짐 +'''Choose one or more, when...'''.ko=다음의 경우 하나 이상 선택 +'''Yes'''.ko=예 +'''No'''.ko=아니요 +'''Send this message (optional, sends standard status message if not specified)'''.ko=이 메시지 전송(선택적, 지정되지 않은 경우 표준 상태 메시지를 보냅니다) +'''Via a push notification and/or an SMS message'''.ko=푸시 알림 및/또는 문자 메시지를 통해 +'''Set for specific mode(s)'''.ko=특정 모드 설정 +'''Tap to set'''.ko=눌러서 설정 +'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지작 간 최소 시간(선택 사항, 모든 메시지의 기본 설정) +'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 거주자는 적절한 국가 코드를 입력했는지 확인하십시오 +'''Water Sensor Wet'''.ko=Water Sensor에서 물이 감지되었습니다 +'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 +'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 +'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 +'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 diff --git a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy index 12fa2a7..f124260 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -20,19 +20,19 @@ * 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell. */ definition( - name: "Notify Me When", - namespace: "smartthings", - author: "SmartThings", - description: "Receive notifications when anything happens in your home.", - category: "Convenience", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" + name: "Notify Me When", + namespace: "smartthings", + author: "SmartThings", + description: "Receive notifications when anything happens in your home.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" ) preferences { section("Choose one or more, when..."){ input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw - input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true @@ -47,11 +47,11 @@ preferences { input "messageText", "text", title: "Message Text", required: false } section("Via a push notification and/or an SMS message"){ - input("recipients", "contact", title: "Send notifications to") { - input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false - paragraph "If outside the US please make sure to enter the proper country code" - input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] - } + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false + paragraph "If outside the US please make sure to enter the proper country code" + input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] + } } section("Minimum time between messages (optional, defaults to every message)") { input "frequency", "decimal", title: "Minutes", required: false @@ -71,7 +71,7 @@ def updated() { def subscribeToEvents() { subscribe(button, "button.pushed", eventHandler) //tw - subscribe(contact, "contact.open", eventHandler) + subscribe(contact, "contact.open", eventHandler) subscribe(contactClosed, "contact.closed", eventHandler) subscribe(acceleration, "acceleration.active", eventHandler) subscribe(motion, "motion.active", eventHandler) @@ -99,49 +99,55 @@ def eventHandler(evt) { } private sendMessage(evt) { - def msg = messageText ?: defaultText(evt) + String msg = messageText + Map options = [:] + + if (!messageText) { + msg = defaultText(evt) + options = [translatable: true, triggerEvent: evt] + } log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" - if (location.contactBookEnabled) { - sendNotificationToContacts(msg, recipients) - } - else { + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients, options) + } else { + if (!phone || pushAndPhone != 'No') { + log.debug 'sending push' + options.method = 'push' + //sendPush(msg) + } + if (phone) { + options.phone = phone + log.debug 'sending SMS' + //sendSms(phone, msg) + } + sendNotification(msg, options) + } - if (!phone || pushAndPhone != "No") { - log.debug "sending push" - sendPush(msg) - } - if (phone) { - log.debug "sending SMS" - sendSms(phone, msg) - } - } if (frequency) { state[evt.deviceId] = now() } } private defaultText(evt) { - if (evt.name == "presence") { - if (evt.value == "present") { + if (evt.name == 'presence') { + if (evt.value == 'present') { if (includeArticle) { - "$evt.linkText has arrived at the $location.name" + '{{ triggerEvent.linkText }} has arrived at the {{ location.name }}' } else { - "$evt.linkText has arrived at $location.name" + '{{ triggerEvent.linkText }} has arrived at {{ location.name }}' } - } - else { + } else { if (includeArticle) { - "$evt.linkText has left the $location.name" + '{{ triggerEvent.linkText }} has left the {{ location.name }}' } else { - "$evt.linkText has left $location.name" + '{{ triggerEvent.linkText }} has left {{ location.name }}' } } - } - else { - evt.descriptionText + } else { + '{{ triggerEvent.descriptionText }}' } } From 512bd3adc41d349d81f3041160fcc89f2e7cc1b8 Mon Sep 17 00:00:00 2001 From: dylanbijnagte Date: Tue, 16 Feb 2016 11:30:20 -0600 Subject: [PATCH 14/15] add missing translations --- .../smartthings/notify-me-when.src/i18n/messages.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties index a50bd54..a190968 100644 --- a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties +++ b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties @@ -27,3 +27,5 @@ '''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 '''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 '''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 +'''Assign a name'''.ko=이름 배정 +'''Choose Modes'''.ko=모드 선택 From 7d07b93694f4f1fd6f4fa7ad19931f111277a796 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Tue, 16 Feb 2016 14:38:37 -0500 Subject: [PATCH 15/15] PROB-870 - Harmony fails to save credentials It seams like the user removed some activities on Harmony side without removing them from SmartThings. This is causing an issue when adding new activities. This fix checks if the activity still exists before creating a new device. --- .../logitech-harmony-connect.groovy | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 03c5ccc..9fc1a5d 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -419,9 +419,11 @@ def addDevice() { def d = getChildDevice(dni) if(!d) { def newAction = state.HarmonyActivities.find { it.key == dni } - d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) - log.trace "created ${d.displayName} with id $dni" - poll() + if (newAction) { + d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) + log.trace "created ${d.displayName} with id $dni" + poll() + } } else { log.trace "found ${d.displayName} with id $dni already exists" }