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 +}