diff --git a/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy index baf2f1a..df12e78 100644 --- a/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy +++ b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy @@ -1,6 +1,5 @@ /** - * Spruce Controller - Pre Release V2 10/11/2015 - * + * Spruce Controller V2_4 Big Tiles * * Copyright 2015 Plaid Systems * * Author: NC @@ -21,82 +20,96 @@ */ metadata { - definition (name: "Spruce Controller", namespace: "plaidsystems", author: "NCauffman") { - capability "Switch" - capability "Configuration" - capability "Refresh" - capability "Actuator" - capability "Valve" + definition (name: 'Spruce Controller', namespace: 'plaidsystems', author: 'Plaid Systems') { + 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" + 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 'rainsensor', 'string' + attribute 'status', 'string' + attribute 'tileMessage', 'string' + attribute 'minutes', 'string' + attribute 'VALUE_UP', 'string' + attribute 'VALUE_DOWN', '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 'levelUp' + command 'levelDown' + command 'programOn' + command 'programOff' + command 'programWait' + command 'programEnd' - command "refresh" - command "rain" - command "manual" - command "setDisplay" + command 'on' + command 'off' + command 'zon' + command 'zoff' + 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 "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" + command 'config' + command 'refresh' + command 'rain' + command 'manual' + command 'manualTime' + command 'settingsMap' + command 'writeTime' + command 'writeType' + command 'notify' + command 'updated' + + //ST release + //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' + //new release + 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,0009,000A,000F", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZ16-01" } @@ -104,162 +117,230 @@ metadata { simulator { // status messages - // reply 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 - } + input description: 'If you have a rain sensor wired to the rain sensor input on the Spruce controller, turn it on here.', displayDuringSetup: true, type: 'paragraph', element: 'paragraph', title: 'Rain Sensor' + input description: 'The SYNC SETTINGS button must be pressed after making a change to the Rain sensor:', displayDuringSetup: false, type: 'paragraph', element: 'paragraph', title: '' + input 'RainEnable', 'bool', title: 'Rain Sensor Attached?', required: false, displayDuringSetup: true + input description: 'Adjust manual water time with arrows on main tile. The time indicated in the first small tile indicates the time the zone will water when manually switched on.', displayDuringSetup: false, type: 'paragraph', element: 'paragraph', title: '' + } // 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: "#00A0DC" - 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: "#e86d13" - 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: "#cccccc" - state "warning", label: '', icon: "st.categories.damageAndDanger", backgroundColor: "#ffff7f" - state "alarm", label: 'Alarm', icon: "st.categories.damageAndDanger", backgroundColor: "#f9240c" + multiAttributeTile(name:"switchall", type:"generic", width:6, height:4) { + tileAttribute('device.status', key: 'PRIMARY_CONTROL') { + attributeState 'schedule', label: 'Ready', icon: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_top.png' + attributeState 'finished', label: 'Finished', icon: 'st.Outdoor.outdoor5', backgroundColor: '#46c2e8' + attributeState 'raintoday', label: 'Rain Today', icon: 'http://www.plaidsystems.com/smartthings/st_rain.png', backgroundColor: '#d65fe3' + attributeState 'rainy', label: 'Rain', icon: 'http://www.plaidsystems.com/smartthings/st_rain.png', backgroundColor: '#d65fe3' + attributeState 'raintom', label: 'Rain Tomorrow', icon: 'http://www.plaidsystems.com/smartthings/st_rain.png', backgroundColor: '#d65fe3' + attributeState 'donewweek', label: 'Finished', icon: 'st.Outdoor.outdoor5', backgroundColor: '#00A0DC' + attributeState 'skipping', label: 'Skip', icon: 'st.Outdoor.outdoor20', backgroundColor: '#46c2e8' + attributeState 'moisture', label: 'Ready', icon: 'st.Weather.weather2', backgroundColor: '#46c2e8' + attributeState 'pause', label: 'PAUSE', icon: 'st.contact.contact.open', backgroundColor: '#e86d13' + attributeState 'delayed', label: 'Delayed', icon: 'st.contact.contact.open', backgroundColor: '#e86d13' + attributeState 'active', label: 'Active', icon: 'st.Outdoor.outdoor12', backgroundColor: '#3DC72E' + attributeState 'season', label: 'Adjust', icon: 'st.Outdoor.outdoor17', backgroundColor: '#ffb900' + attributeState 'disable', label: 'Off', icon: 'st.secondary.off', backgroundColor: '#cccccc' + attributeState 'warning', label: 'Warning', icon: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_top_yellow.png' + attributeState 'alarm', label: 'Alarm', icon: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_s_red.png', backgroundColor: '#e66565' + } + + tileAttribute("device.minutes", key: "VALUE_CONTROL") { + attributeState "VALUE_UP", action: "levelUp" + attributeState "VALUE_DOWN", action: "levelDown" + } + + tileAttribute("device.tileMessage", key: "SECONDARY_CONTROL") { + attributeState "tileMessage", label: '${currentValue}' + } + } - 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: "#00A0DC" - } - 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: "#00A0DC" + valueTile('minutes', 'device.minutes'){ + state 'minutes', label: '${currentValue} min' + } + valueTile('dummy', 'device.minutes'){ + state 'minutes', label: '' + } + standardTile('switch', 'device.switch', width:2, height:2) { + state 'off', label: 'Start', action: 'programOn', icon: 'st.Outdoor.outdoor12', backgroundColor: '#a9a9a9' + state 'programOn', label: 'Wait', action: 'programOff', icon: 'st.contact.contact.open', backgroundColor: '#f6e10e' + state 'programWait', label: 'Wait', action: 'programEnd', icon: 'st.contact.contact.open', backgroundColor: '#f6e10e' + state 'on', label: 'Running', action: 'programEnd', icon: 'st.Outdoor.outdoor12', backgroundColor: '#3DC72E' } - 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: "#00A0DC" + standardTile("rainsensor", "device.rainsensor", decoration: 'flat') { + state "rainSensoroff", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_on.png' + state "rainSensoron", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_on_blue_small.png' + state "disable", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_x_small.png' + state "enable", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_on.png' } - 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: "#00A0DC" + standardTile('switch1', 'device.switch1', inactiveLabel: false) { + 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: '#00A0DC' } - 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: "#00A0DC" - } - 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: "#00A0DC" - } - 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: "#00A0DC" - } - 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: "#00A0DC" - } - 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: "#00A0DC" - } - 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: "#00A0DC" + standardTile('switch2', 'device.switch2', inactiveLabel: false) { + 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: '#00A0DC' } - 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: "#00A0DC" + 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: '#00A0DC' } - 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: "#00A0DC" + 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: '#00A0DC' } - 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: "#00A0DC" + 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: '#00A0DC' } - 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: "#00A0DC" + 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: '#00A0DC' } - 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: "#00A0DC" + 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: '#00A0DC' + } + 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: '#00A0DC' + } + 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: '#00A0DC' + } + 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: '#00A0DC' + } + 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: '#00A0DC' + } + 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: '#00A0DC' + } + 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: '#00A0DC' + } + 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: '#00A0DC' + } + 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: '#00A0DC' } - 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: "#00A0DC" + 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: '#00A0DC' } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", action: "refresh", icon:"st.secondary.refresh" + standardTile('refresh', 'device.switch', inactiveLabel: false, decoration: 'flat') { + state 'default', action: 'refresh', icon:'st.secondary.refresh'//-icon' } - standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { - state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" - } + standardTile('configure', 'device.configure', inactiveLabel: false, decoration: 'flat') { + state 'configure', label:'', action:'configuration.configure', icon:'http://www.plaidsystems.com/smartthings/st_syncsettings.png'//sync_icon_small.png' + } - main (["status"]) - details(["status","rainsensor","switch","switch1","switch2","switch3","switch4","switch5","switch6","switch7","switch8","switch9","switch10","switch11","switch12","switch13","switch14","switch15","switch16","refresh","configure"]) - } + main (['switchall']) + details(['switchall','minutes','rainsensor','switch1','switch2','switch3','switch4','switch','switch5','switch6','switch7','switch8','switch9','switch10','switch11','switch12','refresh','configure','switch13','switch14','switch15','switch16']) + } } +//used for schedule def programOn(){ - sendEvent(name: "switch", value: "programOn", descriptionText: "Program turned on") + sendEvent(name: 'switch', value: 'programOn', descriptionText: 'Program turned on') + } + +def programWait(){ + sendEvent(name: 'switch', value: 'programWait', descriptionText: "Initializing Schedule") + } + +def programEnd(){ + //sets switch to off and tells schedule switch is off/schedule complete with manaual + sendEvent(name: 'switch', value: 'off', descriptionText: 'Program manually turned off') + zoff() } def programOff(){ - sendEvent(name: "switch", value: "off", descriptionText: "Program turned off") + sendEvent(name: 'switch', value: 'off', descriptionText: 'Program turned off') off() } - -def updated(){ - log.debug "updated" + +//set minutes +def levelUp(){ + def newvalue = 1 + if (device.latestValue('minutes') != null) newvalue = device.latestValue('minutes').toInteger()+1 + if (newvalue >= 60) newvalue = 60 + def value = newvalue.toString() + log.debug value + sendEvent(name: 'minutes', value: "${value}", descriptionText: "Manual Time set to ${value}", display: false) +} + +def levelDown(){ + def newvalue = device.latestValue('minutes').toInteger()-1 + if (newvalue <= 0) newvalue = 1 + def value = newvalue.toString() + log.debug value + sendEvent(name: 'minutes', value: "${value}", descriptionText: "Manual Time set to ${value}", display: false) } // Parse incoming device messages to generate events def parse(String description) { - //log.debug "Parse description $description" + log.debug "Parse description ${description}" def result = null def map = [:] - if (description?.startsWith("read attr -")) { + 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" + if (descMap.cluster == '000F' && descMap.attrId == '0055') { + log.debug 'Zone' map = getZone(descMap) } - else if (descMap.cluster == "0009" && descMap.attrId == "0000") { - log.debug "Alarm" + 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" + else if (description?.startsWith('catchall: 0104 0009')){ + log.debug 'Sync settings to controller complete' + if (device.latestValue('status') != 'alarm'){ + def configEvt = createEvent(name: 'status', value: 'schedule', descriptionText: "Sync settings to controller complete") + def configMsg = createEvent(name: 'tileMessage', value: 'Sync settings to controller complete', descriptionText: "Sync settings to controller complete", displayed: false) + result = [configEvt, configMsg] + } + return result + } + + if (map) { + result = createEvent(map) + //configure after reboot + if (map.value == 'warning' || map.value == 'alarm'){ + def cmds = config() + def alarmEvt = createEvent(name: 'tileMessage', value: map.descriptionText, descriptionText: "${map.descriptionText}", displayed: false) + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + createEvent(map) + alarmEvt + return result + } + else if (map.name == 'rainsensor'){ + def rainEvt = createEvent(name: 'tileMessage', value: map.descriptionText, descriptionText: "${map.descriptionText}", displayed: false) + result = [createEvent(map), rainEvt] + return result + } + } + if (map) log.debug "Parse returned ${map} ${result}" return result } def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") + (description - 'read attr - ').split(',').inject([:]) { map, param -> + def nameAndValue = param.split(':') map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] } } @@ -270,27 +351,28 @@ def getZone(descMap){ def EP = Integer.parseInt(descMap.endpoint.trim(), 16) String onoff - if(descMap.value == "00"){ - onoff = "off" + if(descMap.value == '00'){ + onoff = 'off' } - else onoff = "on" + else onoff = 'on' if (EP == 1){ - map.name = "switch" + map.name = 'switch' map.value = onoff - map.descriptionText = "${device.displayName} turned sprinkler program $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" + map.name = 'rainsensor' + log.debug "Rain enable: ${RainEnable}, sensor: ${onoff}" + 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.name = 'switch' + EP + map.value = 'z' + EP + onoff + map.descriptionText = "${device.displayName} turned Zone $EP ${onoff}" } map.isStateChange = true @@ -300,37 +382,59 @@ def getZone(descMap){ def getAlarm(descMap){ def map = [:] - map.name = "status" + 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.value = 'alarm' map.displayed = true + map.isStateChange = true + if(alarmID <= 0){ + map.descriptionText = "${device.displayName} reboot, no other alarms" + map.value = 'warning' + //map.isStateChange = false + } + else map.descriptionText = "${device.displayName} reboot, reported zone ${alarmID - 1} error, please check zone is working correctly, press SYNC SETTINGS button to clear" + return map } //status notify and change status -def notify(value, text){ - sendEvent(name:"status", value:"$value", descriptionText:"$text", isStateChange: true, display: false) +def notify(String val, String txt){ + sendEvent(name: 'status', value: val, descriptionText: txt, isStateChange: true, display: false) + + //String txtShort = txt.take(100) + sendEvent(name: 'tileMessage', value: txt, descriptionText: "", isStateChange: true, display: false) +} +def updated(){ + log.debug "updated" + } //prefrences - rain sensor, manual time def rain() { - log.debug "Rain $RainEnable" + log.debug "Rain sensor: ${RainEnable}" + if (RainEnable) sendEvent(name: 'rainsensor', value: 'enable', descriptionText: "${device.displayName} rain sensor is enabled", isStateChange: true) + else sendEvent(name: 'rainsensor', value: 'disable', descriptionText: "${device.displayName} rain sensor is disabled", isStateChange: true) + 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}}" + +def manualTime(value){ + sendEvent(name: 'minutes', value: "${value}", descriptionText: "Manual Time set to ${value}", display: false) +} + +def manual(){ + def newManaul = 10 + if (device.latestValue('minutes')) newManaul = device.latestValue('minutes').toInteger() + log.debug "Manual Zone runtime ${newManaul} mins" + def manualTime = hex(newManaul) - } + def sendCmds = [] + sendCmds.push("st wattr 0x${device.deviceNetworkId} 1 6 0x4002 0x21 {00${manualTime}}") + return sendCmds +} //write switch time settings map def settingsMap(WriteTimes, attrType){ @@ -366,13 +470,20 @@ def writeTime(wEP, runTime){ //set reporting and binding def configure() { + + sendEvent(name: 'status', value: 'schedule', descriptionText: "Syncing settings to controller") + sendEvent(name: 'minutes', value: "10", descriptionText: "Manual Time set to 10 mins", display: false) + sendEvent(name: 'tileMessage', value: 'Syncing settings to controller', descriptionText: 'Syncing settings to controller') + config() +} + +def config(){ String zigbeeId = swapEndianHex(device.hub.zigbeeId) - log.debug "Confuguring Reporting and Bindings ${device.deviceNetworkId} ${device.zigbeeId}" - sendEvent(name: 'configuration',value: 100, descriptionText: "Configuration initialized") + log.debug "Configuring Reporting and Bindings ${device.deviceNetworkId} ${device.zigbeeId}" def configCmds = [ - //program on/off + //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", @@ -458,38 +569,16 @@ def configure() { "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() + return configCmds + rain() } - - -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 = [ - + log.debug "refresh pressed" + sendEvent(name: 'tileMessage', value: 'Refresh', descriptionText: 'Refresh') + + def refreshCmds = [ + "st rattr 0x${device.deviceNetworkId} 1 0x0F 0x55", "delay 500", "st rattr 0x${device.deviceNetworkId} 2 0x0F 0x55", "delay 500", @@ -513,64 +602,96 @@ def refresh() { "st rattr 0x${device.deviceNetworkId} 18 0x0F 0x51","delay 500", ] - return refreshCmds + rain() + manual() + + return refreshCmds +} + +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 +} + +//on & off redefined for Alexa to start manual schedule +def on() { + log.debug 'Alexa on' + //schedule subscribes to programOn + sendEvent(name: 'switch', value: 'programOn', descriptionText: 'Alexa turned program on') +} +def off() { + log.debug 'Alexa off' + sendEvent(name: 'switch', value: 'off', descriptionText: 'Alexa turned program off') + zoff() } // 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 zon() { + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" } -def off() { - log.debug "off" - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +def zoff() { + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" } def z1on() { - "st cmd 0x${device.deviceNetworkId} 2 6 1 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "st cmd 0x${device.deviceNetworkId} 9 6 1 {}" } def z8off() { "st cmd 0x${device.deviceNetworkId} 9 6 0 {}" @@ -578,50 +699,51 @@ def z8off() { //zones 9 - 16 def z9on() { - "st cmd 0x${device.deviceNetworkId} 10 6 1 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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 {}" + return manual() + "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/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy index ff9d43a..54837a8 100644 --- a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy +++ b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy @@ -1,5 +1,5 @@ /** - * Spruce Scheduler Pre-release V2.5 12/22/2016 + * Spruce Scheduler Pre-release V2.53.1 - Updated 11/07/2016, BAB * * * Copyright 2015 Plaid Systems @@ -13,1200 +13,1955 @@ * 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 --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 + +-------v2.53.1------------------- +-ln 210: enableManual string modified +-ln 496: added code for old ST app zoneNumber number to convert to enum for app update compatibility +-ln 854: unschedule if NOT running to clear/correct manual subscription +-ln 863: weather scheduled if rain OR seasonal enabled, both off is no weather check scheduled +-ln 1083: added sync check to manual start +-ln 1538: corrected contact delay minimum fro 5s to 10s + +-------v2.52--------------------- + -Major revision by BAB * */ definition( name: "Spruce Scheduler", namespace: "plaidsystems", - author: "NCauffman", - description: "Spruce automatic water scheduling app v2.5", + author: "Plaid Systems", + description: "Setup schedules for Spruce irrigation controller", 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: '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") - + page(name: 'zoneSettingsPage') + page(name: 'zoneSetPage') + page(name: 'plantSetPage') + page(name: 'sprinklerSetPage') + page(name: 'optionSetPage') + + //found at bottom - transition pages + page(name: 'zoneSetPage1') + page(name: 'zoneSetPage2') + page(name: 'zoneSetPage3') + page(name: 'zoneSetPage4') + page(name: 'zoneSetPage5') + page(name: 'zoneSetPage6') + page(name: 'zoneSetPage7') + page(name: 'zoneSetPage8') + page(name: 'zoneSetPage9') + page(name: 'zoneSetPage10') + page(name: 'zoneSetPage11') + page(name: 'zoneSetPage12') + page(name: 'zoneSetPage13') + page(name: 'zoneSetPage14') + page(name: 'zoneSetPage15') + page(name: 'zoneSetPage16') } def startPage(){ - dynamicPage(name: "startPage", title: "Spruce Smart Irrigation setup V2.51", install: true, uninstall: true) + dynamicPage(name: 'startPage', title: 'Spruce Smart Irrigation setup', install: true, uninstall: true) { - section(""){ - href(name: "globalPage", title: "Schedule settings", required: false, page: "globalPage", - image: "http://www.plaidsystems.com/smartthings/st_settings.png", - description: "Watering On: ${enableString()}\nWatering Time: ${startTimeString()}\nDays:${daysString()}\nNotifications:\n${notifyString()}") + section(''){ + href(name: 'globalPage', title: 'Schedule settings', required: false, page: 'globalPage', + image: 'http://www.plaidsystems.com/smartthings/st_settings.png', + description: "Schedule: ${enableString()}\nWatering Time: ${startTimeString()}\nDays:${daysString()}\nNotifications:${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()}\nRain Delay: ${isRainString()}\nSeasonal Adjust: ${seasonalAdjString()}" + ) + } - 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: "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']]) - } - + section(''){ + href(name: 'delayPage', title: 'Valve delays & Pause controls', required: false, page: 'delayPage', + image: 'http://www.plaidsystems.com/smartthings/st_timer.png', + description: "Valve Delay: ${pumpDelayString()} s\n${waterStoppersString()}\nSchedule Sync: ${syncString()}" + ) + } + + section(''){ + href(title: 'Spruce Irrigation Knowledge Base', //page: 'customPage', + description: 'Explore our knowledge base for more information on Spruce and Spruce sensors. Contact form is ' + + 'also available here.', + required: false, style:'embedded', + image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png', + 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 'enableManual', 'bool', title: 'Enable this schedule for manual start, only 1 schedule should be enabled for manual start at a time!', defaultValue: 'true', metadata: [values: ['true', 'false']] + input 'startTime', 'time', title: 'Watering start time', required: true + 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: ['Daily', 'Delays', 'Warnings', 'Weather', 'Moisture', 'Events']]) + input('recipients', 'contact', title: 'Send push notifications to', required: false, multiple: true) + input(name: 'logAll', type: 'bool', title: 'Log all notices to Hello Home?', defaultValue: 'false', options: ['true', 'false']) + } + } +} + 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", required: false - input "isSeason", "bool", title: "Enable Seasonal Weather Adjustment:", metadata: [values: ['true', 'false']] + dynamicPage(name: 'weatherPage', title: 'Weather settings') { + section('Location to get weather forecast and conditions:') { + href(name: 'hrefWithImage', title: "${zipString()}", page: 'zipcodePage', + description: 'Set local weather station', + required: false, + image: 'http://www.plaidsystems.com/smartthings/rain.png' + ) + input 'isRain', 'bool', title: 'Enable Rain check:', metadata: [values: ['true', 'false']] + input 'rainDelay', 'decimal', title: 'inches of rain that will delay watering, default: 0.2', required: false + input 'isSeason', 'bool', title: 'Enable Seasonal Weather Adjustment:', metadata: [values: ['true', 'false']] } } } def zipcodePage() { - return dynamicPage(name: "zipcodePage", title: "Spruce weather station setup") { - section(""){input "zipcode", "text", title: "Zipcode or WeatherUnderground station id. Default value is current location.", defaultValue: "${location.zipCode}", required: false, submitOnChange: true - } + return dynamicPage(name: 'zipcodePage', title: 'Spruce weather station setup') { + section(''){ + input(name: 'zipcode', type: 'text', title: 'Zipcode or WeatherUnderground station id. Default value is current Zip code', + defaultValue: getPWSID(), required: false, submitOnChange: true ) + } - section(""){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" + section(''){ + paragraph(image: 'http://www.plaidsystems.com/smartthings/wu.png', title: 'WeatherUnderground Personal Weather Stations (PWS)', + required: false, + 'To automatically select the PWS nearest to your hub location, select the toggle below and clear the ' + + 'location field above') + input(name: 'nearestPWS', type: 'bool', title: 'Use nearest PWS', options: ['true', 'false'], + defaultValue: false, submitOnChange: true) + href(title: 'Or, Search WeatherUnderground.com for your desired PWS', + description: 'After page loads, select "Change Station" for a list of weather stations. ' + + 'You will need to copy the station code into the location field above', + required: false, style:'embedded', + url: (location.latitude && location.longitude)? "http://www.wunderground.com/cgi-bin/findweather/hdfForecast?query=${location.latitude}%2C${location.longitude}" : + "http://www.wunderground.com/q/${location.zipCode}") } } - if(daysString == "") - daysString = " Any" - return daysString +} + +private String getPWSID() { + String PWSID = location.zipCode + if (zipcode) PWSID = zipcode + if (nearestPWS && !zipcode) { + // find the nearest PWS to the hub's geo location + String geoLocation = location.zipCode + // use coordinates, if available + if (location.latitude && location.longitude) geoLocation = "${location.latitude}%2C${location.longitude}" + Map wdata = getWeatherFeature('geolookup', geoLocation) + if (wdata && wdata.response && !wdata.response.containsKey('error')) { // if we get good data + if (wdata.response.features.containsKey('geolookup') && (wdata.response.features.geolookup.toInteger() == 1) && wdata.location) { + PWSID = wdata.location.nearby_weather_stations.pws.station[0].id + } + else log.debug "bad response" + } + else log.debug "null or error" + } + log.debug "Nearest PWS ${PWSID}" + return PWSID } +private String startTimeString(){ + if (!startTime) return 'Please set!' else return hhmm(startTime) +} + +private String enableString(){ + if(enable && enableManual) return 'On & Manual Set' + else if (enable) return 'On & Manual Off' + else if (enableManual) return 'Off & Manual Set' + else return 'Off' +} + +private String waterStoppersString(){ + String stoppers = 'Contact Sensor' + if (settings.contacts) { + if (settings.contacts.size() != 1) stoppers += 's' + stoppers += ': ' + int i = 1 + settings.contacts.each { + if ( i > 1) stoppers += ', ' + stoppers += it.displayName + i++ + } + stoppers = "${stoppers}\nPause: When ${settings.contactStop}\n" + } + else { + stoppers += ': None\n' + } + stoppers += "Switch" + if (settings.toggles) { + if (settings.toggles.size() != 1) stoppers += 'es' + stoppers += ': ' + int i = 1 + settings.toggles.each { + if ( i > 1) stoppers += ', ' + stoppers += it.displayName + i++ + } + stoppers = "${stoppers}\nPause: When switched ${settings.toggleStop}\n" + } + else { + stoppers += ': None\n' + } + int cd = 10 + if (settings.contactDelay && settings.contactDelay > 10) cd = settings.contactDelay.toInteger() + stoppers += "Restart Delay: ${cd} secs" + return stoppers +} + +private String isRainString(){ + if (settings.isRain && !settings.rainDelay) return '0.2' as String + if (settings.isRain) return settings.rainDelay as String else return 'Off' +} -private hhmm(time, fmt = "h:mm a") -{ +private String seasonalAdjString(){ + if(settings.isSeason) return 'On' else return 'Off' +} + +private String syncString(){ + if (settings.sync) return "${settings.sync.displayName}" else return 'None' +} + +private String notifyString(){ + String notifyStr = '' + if(settings.notify) { + if (settings.notify.contains('Daily')) notifyStr += ' Daily' + //if (settings.notify.contains('Weekly')) notifyStr += ' Weekly' + if (settings.notify.contains('Delays')) notifyStr += ' Delays' + if (settings.notify.contains('Warnings')) notifyStr += ' Warnings' + if (settings.notify.contains('Weather')) notifyStr += ' Weather' + if (settings.notify.contains('Moisture')) notifyStr += ' Moisture' + if (settings.notify.contains('Events')) notifyStr += ' Events' + } + if (notifyStr == '') notifyStr = ' None' + if (settings.logAll) notifyStr += '\nSending all Notifications to Hello Home log' + + return notifyStr +} + +private String daysString(){ + String daysString = '' + if (days){ + if(days.contains('Even') || days.contains('Odd')) { + if (days.contains('Even')) daysString += ' Even' + if (days.contains('Odd')) daysString += ' Odd' + } + else { + if (days.contains('Monday')) daysString += ' M' + if (days.contains('Tuesday')) daysString += ' Tu' + if (days.contains('Wednesday')) daysString += ' W' + if (days.contains('Thursday')) daysString += ' Th' + if (days.contains('Friday')) daysString += ' F' + if (days.contains('Saturday')) daysString += ' Sa' + if (days.contains('Sunday')) daysString += ' Su' + } + } + if(daysString == '') return ' Any' + + else return daysString +} + +private String hhmm(time, fmt = 'h:mm a'){ def t = timeToday(time, location.timeZone) def f = new java.text.SimpleDateFormat(fmt) f.setTimeZone(location.timeZone ?: timeZone(time)) - f.format(t) + return f.format(t) } -def pumpDelayString() -{ - if ("${settings["pumpDelay"]}" == "null") return "5" - else return "${settings["pumpDelay"]}" +private String pumpDelayString(){ + if (!pumpDelay) return '0' else return pumpDelay as String + + } 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", + 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" + 'Setting a delay is optional, default is 0. If you have a pump that feeds water directly into your valves, ' + + 'set this to 0. To fill a tank or build pressure, you may increase the delay.\n\nStart->Pump On->delay->Valve ' + + 'On->Valve Off->delay->...' + input name: 'pumpDelay', type: 'number', title: 'Set a delay in seconds?', defaultValue: '0', required: false } - 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", + + section(''){ + paragraph(image: 'http://www.plaidsystems.com/smartthings/st_pause.png', + title: 'Pause Control Contacts & Switches', 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." + 'Selecting contacts or control switches is optional. When a selected contact sensor is opened or switch is ' + + 'toggled, water immediately stops and will not resume until all of the contact sensors are closed and all of ' + + 'the switches are reset.\n\nCaution: if all contacts or switches are left in the stop state, the dependent ' + + 'schedule(s) will never run.') + input(name: 'contacts', title: 'Select water delay contact sensors', type: 'capability.contactSensor', multiple: true, + required: false, submitOnChange: true) + // if (settings.contact) settings.contact = null // 'contact' has been deprecated + if (contacts) + input(name: 'contactStop', title: 'Stop watering when sensors are...', type: 'enum', required: (settings.contacts != null), + options: ['open', 'closed'], defaultValue: 'open') + input(name: 'toggles', title: 'Select water delay switches', type: 'capability.switch', multiple: true, required: false, + submitOnChange: true) + if (toggles) + input(name: 'toggleStop', title: 'Stop watering when switches are...', type: 'enum', + required: (settings.toggles != null), options: ['on', 'off'], defaultValue: 'off') + input(name: 'contactDelay', type: 'number', title: 'Restart watering how many seconds after all contacts and switches ' + + 'are reset? (minimum 10s)', defaultValue: '10', required: false) } - 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 + + section(''){ + paragraph image: 'http://www.plaidsystems.com/smartthings/st_spruce_controller_250.png', + title: 'Controller Sync', + required: false, + 'For multiple controllers only. This schedule will wait for the selected controller to finish before ' + + 'starting. Do not set with a single controller!' + input name: 'sync', type: 'capability.switch', title: 'Select Master Controller', description: 'Only use this setting with multiple controllers', required: false, multiple: false } } } -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", +def zonePage() { + 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") + 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, + if (zoneActive('1')){ + section(''){ + href(name: 'z1Page', title: "1: ${getname("1")}", required: false, page: 'zoneSetPage1', + image: "${getimage("1")}", description: "${display("1")}" ) } } - if (zoneNumber >= 2){ - section(""){ - href(name: "z2Page", title: "2: ${getname("2")}", required: false, page: "zoneSetPage", + if (zoneActive('2')){ + section(''){ + href(name: 'z2Page', title: "2: ${getname("2")}", required: false, page: 'zoneSetPage2', image: "${getimage("2")}", - params: z2Par, description: "${display("2")}" ) } } - if (zoneNumber >= 3){ - section(""){ - href(name: "z3Page", title: "3: ${getname("3")}", required: false, page: "zoneSetPage", + if (zoneActive('3')){ + section(''){ + href(name: 'z3Page', title: "3: ${getname("3")}", required: false, page: 'zoneSetPage3', image: "${getimage("3")}", - params: z3Par, description: "${display("3")}" ) } } - if (zoneNumber >= 4){ - section(""){ - href(name: "z4Page", title: "4: ${getname("4")}", required: false, page: "zoneSetPage", + if (zoneActive('4')){ + section(''){ + href(name: 'z4Page', title: "4: ${getname("4")}", required: false, page: 'zoneSetPage4', image: "${getimage("4")}", - params: z4Par, description: "${display("4")}" ) } } - if (zoneNumber >= 5){ - section(""){ - href(name: "z5Page", title: "5: ${getname("5")}", required: false, page: "zoneSetPage", + if (zoneActive('5')){ + section(''){ + href(name: 'z5Page', title: "5: ${getname("5")}", required: false, page: 'zoneSetPage5', image: "${getimage("5")}", - params: z5Par, description: "${display("5")}" ) } } - if (zoneNumber >= 6){ - section(""){ - href(name: "z6Page", title: "6: ${getname("6")}", required: false, page: "zoneSetPage", + if (zoneActive('6')){ + section(''){ + href(name: 'z6Page', title: "6: ${getname("6")}", required: false, page: 'zoneSetPage6', image: "${getimage("6")}", - params: z6Par, description: "${display("6")}" ) } } - if (zoneNumber >= 7){ - section(""){ - href(name: "z7Page", title: "7: ${getname("7")}", required: false, page: "zoneSetPage", + if (zoneActive('7')){ + section(''){ + href(name: 'z7Page', title: "7: ${getname("7")}", required: false, page: 'zoneSetPage7', image: "${getimage("7")}", - params: z7Par, description: "${display("7")}" ) } } - if (zoneNumber >= 8){ - section(""){ - href(name: "z8Page", title: "8: ${getname("8")}", required: false, page: "zoneSetPage", + if (zoneActive('8')){ + section(''){ + href(name: 'z8Page', title: "8: ${getname("8")}", required: false, page: 'zoneSetPage8', image: "${getimage("8")}", - params: z8Par, description: "${display("8")}" ) } } - if (zoneNumber >= 9){ - section(""){ - href(name: "z9Page", title: "9: ${getname("9")}", required: false, page: "zoneSetPage", + if (zoneActive('9')){ + section(''){ + href(name: 'z9Page', title: "9: ${getname("9")}", required: false, page: 'zoneSetPage9', image: "${getimage("9")}", - params: z9Par, description: "${display("9")}" ) } } - if (zoneNumber >= 10){ - section(""){ - href(name: "z10Page", title: "10: ${getname("10")}", required: false, page: "zoneSetPage", + if (zoneActive('10')){ + section(''){ + href(name: 'z10Page', title: "10: ${getname("10")}", required: false, page: 'zoneSetPage10', image: "${getimage("10")}", - params: z10Par, description: "${display("10")}" ) } } - if (zoneNumber >= 11){ - section(""){ - href(name: "z11Page", title: "11: ${getname("11")}", required: false, page: "zoneSetPage", + if (zoneActive('11')){ + section(''){ + href(name: 'z11Page', title: "11: ${getname("11")}", required: false, page: 'zoneSetPage11', image: "${getimage("11")}", - params: z11Par, description: "${display("11")}" ) } } - if (zoneNumber >= 12){ - section(""){ - href(name: "z12Page", title: "12: ${getname("12")}", required: false, page: "zoneSetPage", + if (zoneActive('12')){ + section(''){ + href(name: 'z12Page', title: "12: ${getname("12")}", required: false, page: 'zoneSetPage12', image: "${getimage("12")}", - params: z12Par, description: "${display("12")}" ) } } - if (zoneNumber >= 13){ - section(""){ - href(name: "z13Page", title: "13: ${getname("13")}", required: false, page: "zoneSetPage", + if (zoneActive('13')){ + section(''){ + href(name: 'z13Page', title: "13: ${getname("13")}", required: false, page: 'zoneSetPage13', image: "${getimage("13")}", - params: z13Par, description: "${display("13")}" ) } } - if (zoneNumber >= 14){ - section(""){ - href(name: "z14Page", title: "14: ${getname("14")}", required: false, page: "zoneSetPage", + if (zoneActive('14')){ + section(''){ + href(name: 'z14Page', title: "14: ${getname("14")}", required: false, page: 'zoneSetPage14', image: "${getimage("14")}", - params: z14Par, description: "${display("14")}" ) } } - if (zoneNumber >= 15){ - section(""){ - href(name: "z15Page", title: "15: ${getname("15")}", required: false, page: "zoneSetPage", + if (zoneActive('15')){ + section(''){ + href(name: 'z15Page', title: "15: ${getname("15")}", required: false, page: 'zoneSetPage15', image: "${getimage("15")}", - params: z15Par, description: "${display("15")}" ) } } - if (zoneNumber >= 16){ - section(""){ - href(name: "z16Page", title: "16: ${getname("16")}", required: false, page: "zoneSetPage", + if (zoneActive('16')){ + section(''){ + href(name: 'z16Page', title: "16: ${getname("16")}", required: false, page: 'zoneSetPage16', 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" + +// Verify whether a zone is active +/*//Code for fresh install +private boolean zoneActive(String zoneStr){ + if (!zoneNumber) return false + if (zoneNumber.contains(zoneStr)) return true // don't display zones that are not selected + return false +} +*/ +//code change for ST update file -> change input to zoneNumberEnum +private boolean zoneActive(z){ + if (!zoneNumberEnum && zoneNumber && zoneNumber >= z.toInteger()) return true + else if (!zoneNumberEnum && zoneNumber && zoneNumber != z.toInteger()) return false + else if (zoneNumberEnum && zoneNumberEnum.contains(z)) return true + return false +} + + +private String zoneString() { + String numberString = 'Add zones to setup' + if (zoneNumber) numberString = "Zones enabled: ${zoneNumber}" + if (learn) numberString = "${numberString}\nSensor mode: Adaptive" + else numberString = "${numberString}\nSensor mode: Delay" 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 - 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)", metadata: [values: ['true', 'false']] - } - } } -def zoneSetPage(params){ - dynamicPage(name: "zoneSetPage", title: "Zone ${setPage("${params?.zoneP}")} Setup") { - section(""){ +def zoneSettingsPage() { + dynamicPage(name: 'zoneSettingsPage', title: 'Zone Configuration') { + section(''){ + //input (name: "zoneNumber", type: "number", title: "Enter number of zones to configure?",description: "How many valves do you have? 1-16", required: true)//, defaultValue: 16) + input 'zoneNumberEnum', 'enum', title: 'Select zones to configure', multiple: true, metadata: [values: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16']] + input 'gain', 'number', title: 'Increase or decrease all water times by this %, enter a negative or positive value, Default: 0', required: false, range: '-99..99' + paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', + title: 'Moisture sensor adapt mode', + 'Adaptive mode enabled: Watering times will be adjusted based on the assigned moisture sensor.\n\nAdaptive mode ' + + 'disabled (Delay): Zones with moisture sensors will water on any available days when the low moisture setpoint has ' + + 'been reached.' + input 'learn', 'bool', title: 'Enable Adaptive Moisture Control (with moisture sensors)', metadata: [values: ['true', 'false']] + } + } +} + +def zoneSetPage() { + dynamicPage(name: 'zoneSetPage', title: "Zone ${state.app} Setup") { + section(''){ paragraph image: "http://www.plaidsystems.com/smartthings/st_${state.app}.png", - title: "Current Settings", + title: 'Current Settings', "${display("${state.app}")}" - } - section(""){ - input "name${state.app}", "text", title: "Zone name?", required: false, defaultValue: "Zone ${state.app}" - } - section(""){ - href(name: "tosprinklerSetPage", title: "Sprinkler type: ${setString("zone")}", required: false, page: "sprinklerSetPage", - image: "${getimage("${settings["zone${state.app}"]}")}", - description: "Set sprinkler nozzle type or turn zone off") - } - section(""){ - href(name: "toplantSetPage", title: "Landscape Select: ${setString("plant")}", required: false, page: "plantSetPage", + } + + section(''){ + input "name${state.app}", 'text', title: 'Zone name?', required: false, defaultValue: "Zone ${state.app}" + } + + section(''){ + href(name: 'tosprinklerSetPage', title: "Sprinkler type: ${setString('zone')}", required: false, page: 'sprinklerSetPage', + image: "${getimage("${settings."zone${state.app}"}")}", + //description: "Set sprinkler nozzle type or turn zone off") + description: 'Sprinkler type descriptions') + input "zone${state.app}", 'enum', title: 'Sprinkler Type', multiple: false, required: false, defaultValue: 'Off', submitOnChange: true, metadata: [values: ['Off', 'Spray', 'Rotor', 'Drip', 'Master Valve', 'Pump']] + } + + section(''){ + href(name: 'toplantSetPage', title: "Landscape Select: ${setString('plant')}", required: false, page: 'plantSetPage', image: "${getimage("${settings["plant${state.app}"]}")}", - description: "Set landscape type") + //description: "Set landscape type") + description: 'Landscape type descriptions') + input "plant${state.app}", 'enum', title: 'Landscape', multiple: false, required: false, submitOnChange: true, metadata: [values: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']] } - section(""){ - href(name: "tooptionSetPage", title: "Options: ${setString("option")}", required: false, page: "optionSetPage", + 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 + //description: "Set watering options") + description: 'Watering option descriptions') + input "option${state.app}", 'enum', title: 'Options', multiple: false, required: false, defaultValue: 'Cycle 2x', submitOnChange: true,metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] } - section(""){ - paragraph image: "http://www.plaidsystems.com/smartthings/st_timer.png", - 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 + + 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 set to a default value but can be adjusted to tune watering' + input "sensor${state.app}", 'capability.relativeHumidityMeasurement', title: 'Select moisture sensor?', required: false, multiple: false + input "sensorSp${state.app}", 'number', title: "Minimum moisture sensor target value, Setpoint: ${getDrySp(state.app)}", required: false + } + + section(''){ + paragraph image: 'http://www.plaidsystems.com/smartthings/st_timer.png', + title: 'Optional: Enter total watering time per week', + 'This value will replace the calculated time from other settings' + input "minWeek${state.app}", 'number', title: 'Minimum water time per week.\nDefault: 0 = autoadjust', description: 'minutes per week', required: false + input "perDay${state.app}", 'number', title: 'Guideline value for time per day, this divides minutes per week into watering days. Default: 20', defaultValue: '20', required: false } - } } -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" - } +private String setString(String type) { + switch (type) { + case 'zone': + if (settings."zone${state.app}") return settings."zone${state.app}" else return 'Not Set' + break + case 'plant': + if (settings."plant${state.app}") return settings."plant${state.app}" else return 'Not Set' + break + case 'option': + if (settings."option${state.app}") return settings."option${state.app}" else return 'Not Set' + break + default: + return '????' + } } -def plantSetPage(){ - dynamicPage(name: "plantSetPage", title: "${settings["name${state.app}"]} Landscape Select") { - section(""){ - paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", +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" + //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_flowers_225_r.png', + title: 'Flowers', + 'Select Flowers for beds with smaller seasonal plants' - paragraph image: "http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png", - title: "Shrubs", - "Select Garden for vegetable gardens" + paragraph image: 'http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png', + title: 'Shrubs', + 'Select Shrubs for beds with larger established plants' - paragraph image: "http://www.plaidsystems.com/smartthings/st_trees_225_r.png", - title: "Trees", - "Select Lawn for typical grass applications" + paragraph image: 'http://www.plaidsystems.com/smartthings/st_trees_225_r.png', + title: 'Trees', + 'Select Trees for deep rooted areas without other plants' - paragraph image: "http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png", - title: "Xeriscape", - "Reduces water for native or drought tolorent plants" + 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." - } - } + 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(""){ + 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']] - + //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." + 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_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_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_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." - } + 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(""){ + 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']] + //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" + 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_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_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_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_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" - } + 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 + if (i) 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}" +private String getaZoneSummary(int zone){ + if (!settings."zone${zone}" || (settings."zone${zone}" == 'Off')) return "${zone}: Off" + + String daysString = '' + int tpw = initTPW(zone) + int dpw = initDPW(zone) + int runTime = calcRunTime(tpw, dpw) + + if ( !learn && (settings."sensor${zone}")) { + daysString = 'if Moisture is low on: ' + dpw = daysAvailable() + } + if (days && (days.contains('Even') || days.contains('Odd'))) { + if (dpw == 1) daysString = 'Every 8 days' + if (dpw == 2) daysString = 'Every 4 days' + if (dpw == 4) daysString = 'Every 2 days' + if (days.contains('Even') && days.contains('Odd')) daysString = 'any day' + } + else { + def int[] dpwMap = [0,0,0,0,0,0,0] + dpwMap = getDPWDays(dpw) + daysString += getRunDays(dpwMap) + } + return "${zone}: ${runTime} min, ${daysString}" } -def getZoneSummary() -{ - def summary = "" - if (learn) summary = "Moisture Learning enabled" - else summary = "Moisture Learning disabled" - def zone = 1 +private String getZoneSummary(){ + String summary = '' + if (learn) summary = 'Moisture Learning enabled' else summary = 'Moisture Learning disabled' + + int 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 (nozzle(zone) == 4) summary = "${summary}\n${zone}: ${settings."zone${zone}"}" + else if ( (initDPW(zone) != 0) && zoneActive(zone.toString())) summary = "${summary}\n${getaZoneSummary(zone)}" + zone++ } - if(summary == "") return zoneString() //"Setup all 16 zones" - - return summary + if (summary) return summary else return zoneString() //"Setup all 16 zones" } -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}" +private String display(String i){ + //log.trace "display(${i})" + String displayString = '' + int tpw = initTPW(i.toInteger()) + int dpw = initDPW(i.toInteger()) + int runTime = calcRunTime(tpw, dpw) + if (settings."zone${i}") displayString += settings."zone${i}" + ' : ' + if (settings."plant${i}") displayString += settings."plant${i}" + ' : ' + if (settings."option${i}") displayString += settings."option${i}" + ' : ' + int j = i.toInteger() + if (settings."sensor${i}") { + displayString += settings."sensor${i}" + displayString += "=${getDrySp(j)}% : " + } + if ((runTime != 0) && (dpw != 0)) displayString = "${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}"){ +private String getimage(String image){ + String imageStr = image + if (image.isNumber()) { + String zoneStr = settings."zone${image}" + if (zoneStr) { + if (zoneStr == 'Off') return 'http://www.plaidsystems.com/smartthings/off2.png' + if (zoneStr == 'Master Valve') return 'http://www.plaidsystems.com/smartthings/master.png' + if (zoneStr == 'Pump') return 'http://www.plaidsystems.com/smartthings/pump.png' + + if (settings."plant${image}") imageStr = settings."plant${image}" // default assume asking for the plant image + } + } + // OK, lookup the requested image + switch (imageStr) { 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": + 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 '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" + return 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png' default: - return "http://www.plaidsystems.com/smartthings/off2.png" - } + return 'http://www.plaidsystems.com/smartthings/off2.png' } +} -def getname(i) { - if ("${settings["name${i}"]}" != "null") return "${settings["name${i}"]}" - else return "Zone $i" +private String getname(String i) { + if (settings."name${i}") return settings."name${i}" else return "Zone ${i}" } -def zipString() { - if (!zipcode) return "${location.zipCode}" +private String zipString() { + if (!settings.zipcode) return "${location.zipCode}" //add pws for correct weatherunderground lookup - if (!zipcode.isNumber()) return "pws:${zipcode}" - else return "${zipcode}" + if (!settings.zipcode.isNumber()) return "pws:${settings.zipcode}" + else return settings.zipcode } //app install def installed() { - state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - state.Rain = [0,0,0,0,0,0,0] - state.fail = 0 - state.seasonAdj = 0 - state.weekseasonAdj = 0 + state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + state.Rain = [0,0,0,0,0,0,0] + state.daycount = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + atomicState.run = false // must be atomic - used to recover from crashes + state.pauseTime = null + atomicState.startTime = null + atomicState.finishTime = null // must be atomic - used to recover from crashes + log.debug "Installed with settings: ${settings}" installSchedule() } -def updated() { - unsubscribe() - unschedule() - log.debug "Installed with settings: ${settings}" +def updated() { + log.debug "Updated with settings: ${settings}" installSchedule() } -def installSchedule(){ - if(switches && startTime) { - 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(runTime, checkOn) - note("schedule", "Schedule set to start at ${startTimeString()}", "w") +def installSchedule(){ + if (!state.seasonAdj) state.seasonAdj = 100.0 + if (!state.weekseasonAdj) state.weekseasonAdj = 0 + if (state.daysAvailable != 0) state.daysAvailable = 0 // force daysAvailable to be initialized by daysAvailable() + state.daysAvailable = daysAvailable() // every time we save the schedule + + if (atomicState.run) { + attemptRecovery() // clean up if we crashed earlier + } + else { + unsubscribe() //added back in to reset manual subscription + resetEverything() + } + subscribe(app, appTouch) // enable the "play" button for this schedule + Random rand = new Random() + long randomOffset = 0 + + // always collect rainfall + int randomSeconds = rand.nextInt(59) + if (settings.isRain || settings.isSeason) schedule("${randomSeconds} 57 23 1/1 * ? *", getRainToday) // capture today's rainfall just before midnight + + if (settings.switches && settings.startTime && settings.enable){ + + randomOffset = rand.nextInt(60000) + 20000 + def checktime = timeToday(settings.startTime, location.timeZone).getTime() + randomOffset + //log.debug "randomOffset ${randomOffset} checktime ${checktime}" + schedule(checktime, preCheck) //check weather & Days writeSettings() - } - else note("disable", "Automatic watering turned off, set active in app to resume scheduled watering.", "w") + note('schedule', "${app.label}: Starts at ${startTimeString()}", 'i') + } + else { + unschedule( preCheck ) + note('disable', "${app.label}: Automatic watering disabled or setup is incomplete", 'a') + } +} + +// Called to find and repair after crashes - called by installSchedule() and busy() +private boolean attemptRecovery() { + if (!atomicState.run) { + return false // only clean up if we think we are still running + } + else { // Hmmm...seems we were running before... + def csw = settings.switches.currentSwitch + def cst = settings.switches.currentStatus + switch (csw) { + case 'on': // looks like this schedule is running the controller at the moment + if (!atomicState.startTime) { // cycleLoop cleared the startTime, but cycleOn() didn't set it + log.debug "${app.label}: crashed in cycleLoop(), cycleOn() never started, cst is ${cst} - resetting" + resetEverything() // reset and try again...it's probably not us running the controller, though + return false + } + // We have a startTime... + if (!atomicState.finishTime) { // started, but we don't think we're done yet..so it's probably us! + runIn(15, cycleOn) // goose the cycle, just in case + note('active', "${app.label}: schedule is apparently already running", 'i') + return true + } + + // hmmm...switch is on and we think we're finished...probably somebody else is running...let busy figure it out + resetEverything() + return false + break + + case 'off': // switch is off - did we finish? + if (atomicState.finishTime) { // off and finished, let's just reset things + resetEverything() + return false + } + + if (switches.currentStatus != 'pause') { // off and not paused - probably another schedule, let's clean up + resetEverything() + return false + } + + // off and not finished, and paused, we apparently crashed while paused + runIn(15, cycleOn) + return true + break + + case 'programOn': // died while manual program running? + case 'programWait': // looks like died previously before we got started, let's try to clean things up + resetEverything() + if (atomicState.finishTime) atomicState.finishTime = null + if ((cst == 'active') || atomicState.startTime) { // if we announced we were in preCheck, or made it all the way to cycleOn before it crashed + settings.switches.programOff() // only if we think we actually started (cycleOn() started) + // probably kills manual cycles too, but we'll let that go for now + } + if (atomicState.startTime) atomicState.startTime = null + note ('schedule', "Looks like ${app.label} crashed recently...cleaning up", c) + return false + break + + default: + log.debug "attemptRecovery(): atomicState.run == true, and I've nothing left to do" + return true + } + } +} + +// reset everything to the initial (not running) state +private def resetEverything() { + if (atomicState.run) atomicState.run = false // we're not running the controller any more + unsubAllBut() // release manual, switches, sync, contacts & toggles + + // take care not to unschedule preCheck() or getRainToday() + unschedule(cycleOn) + unschedule(checkRunMap) + unschedule(writeCycles) + unschedule(subOff) + + if (settings.enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) +} + +// unsubscribe from ALL events EXCEPT app.touch +private def unsubAllBut() { + unsubscribe(settings.switches) + unsubWaterStoppers() + if (settings.sync) unsubscribe(settings.sync) + +} + +// enable the "Play" button in SmartApp list +def appTouch(evt) { + + log.debug "appTouch(): atomicState.run = ${atomicState.run}" + + runIn(2, preCheck) // run it off a schedule, so we can see how long it takes in the app.state +} + +// true if one of the stoppers is in Stop state +private boolean isWaterStopped() { + if (settings.contacts && settings.contacts.currentContact.contains(settings.contactStop)) return true + + if (settings.toggles && settings.toggles.currentSwitch.contains(settings.toggleStop)) return true + + return false +} + +// watch for water stoppers +private def subWaterStop() { + if (settings.contacts) { + unsubscribe(settings.contacts) + subscribe(settings.contacts, "contact.${settings.contactStop}", waterStop) + } + if (settings.toggles) { + unsubscribe(settings.toggles) + subscribe(settings.toggles, "switch.${settings.toggleStop}", waterStop) + } +} + +// watch for water starters +private def subWaterStart() { + if (settings.contacts) { + unsubscribe(settings.contacts) + def cond = (settings.contactStop == 'open') ? 'closed' : 'open' + subscribe(settings.contacts, "contact.${cond}", waterStart) + } + if (settings.toggles) { + unsubscribe(settings.toggles) + def cond = (settings.toggleStop == 'on') ? 'off' : 'on' + subscribe(settings.toggles, "switch.${cond}", waterStart) + } +} + +// stop watching water stoppers and starters +private def unsubWaterStoppers() { + if (settings.contacts) unsubscribe(settings.contacts) + if (settings.toggles) unsubscribe(settings.toggles) +} + +// which of the stoppers are in stop mode? +private String getWaterStopList() { + String deviceList = '' + int i = 1 + if (settings.contacts) { + settings.contacts.each { + if (it.currentContact == settings.contactStop) { + if (i > 1) deviceList += ', ' + deviceList = "${deviceList}${it.displayName} is ${settings.contactStop}" + i++ + } + } + } + if (settings.toggles) { + settings.toggles.each { + if (it.currentSwitch == settings.toggleStop) { + if (i > 1) deviceList += ', ' + deviceList = "${deviceList}${it.displayName} is ${settings.toggleStop}" + i++ + } + } + } + return deviceList +} + +//write initial zone settings to device at install/update +def writeSettings(){ + if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + if (!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + if (state.setMoisture) state.setMoisture = null // not using any more + if (!state.seasonAdj) state.seasonAdj = 100.0 + if (!state.weekseasonAdj) state.weekseasonAdj = 0 + setSeason() +} + +//get day of week integer +int getWeekDay(day) +{ + def weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + def mapDay = [Monday:1, Tuesday:2, Wednesday:3, Thursday:4, Friday:5, Saturday:6, Sunday:7] + if(day && weekdays.contains(day)) { + return mapDay.get(day).toInteger() + } + def today = new Date().format('EEEE', location.timeZone) + return mapDay.get(today).toInteger() +} + +// Get string of run days from dpwMap +private String getRunDays(day1,day2,day3,day4,day5,day6,day7) +{ + String 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(str == '') str = '0 Days/week' + return str +} + +//start manual schedule +def manualStart(evt){ + boolean running = attemptRecovery() // clean up if prior run crashed + + if (settings.enableManual && !running && (settings.switches.currentStatus != 'pause')){ + if (settings.sync && ( (settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') ) { + note('skipping', "${app.label}: Manual run aborted, ${settings.sync.displayName} appears to be busy", 'a') + } + else { + def runNowMap = [] + runNowMap = cycleLoop(0) + + if (runNowMap) { + atomicState.run = true + settings.switches.programWait() + subscribe(settings.switches, 'switch.off', cycleOff) + + runIn(60, cycleOn) // start water program + + // note that manual DOES abide by waterStoppers (if configured) + String newString = '' + int tt = state.totalTime + if (tt) { + int hours = tt / 60 // DON'T Math.round this one + int mins = tt - (hours * 60) + String hourString = '' + String s = '' + if (hours > 1) s = 's' + if (hours > 0) hourString = "${hours} hour${s} & " + s = 's' + if (mins == 1) s = '' + newString = "run time: ${hourString}${mins} minute${s}:\n" + } + + note('active', "${app.label}: Manual run, watering in 1 minute: ${newString}${runNowMap}", 'd') + } + else note('skipping', "${app.label}: Manual run failed, check configuration", 'a') + } + } + else note('skipping', "${app.label}: Manual run aborted, ${settings.switches.displayName} appears to be busy", 'a') +} + +//true if another schedule is running +boolean busy(){ + // Check if we are already running, crashed or somebody changed the schedule time while this schedule is running + if (atomicState.run){ + if (!attemptRecovery()) { // recovery will clean out any prior crashes and correct state of atomicState.run + return false // (atomicState.run = false) + } + else { + // don't change the current status, in case the currently running schedule is in off/paused mode + note(settings.switches.currentStatus, "${app.label}: Already running, skipping additional start", 'i') + return true + } + } + // Not already running... + + // Moved from cycleOn() - don't even start pre-check until the other controller completes its cycle + if (settings.sync) { + if ((settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') { + subscribe(settings.sync, 'switch.off', syncOn) + + note('delayed', "${app.label}: Waiting for ${settings.sync.displayName} to complete before starting", 'c') + return true + } + } + + // Check that the controller isn't paused while running some other schedule + def csw = settings.switches.currentSwitch + def cst = settings.switches.currentStatus + + if ((csw == 'off') && (cst != 'pause')) { // off && !paused: controller is NOT in use + log.debug "switches ${csw}, status ${cst} (1st)" + resetEverything() // get back to the start state + return false + } + + if (isDay()) { // Yup, we need to run today, so wait for the other schedule to finish + log.debug "switches ${csw}, status ${cst} (3rd)" + resetEverything() + subscribe(settings.switches, 'switch.off', busyOff) + note('delayed', "${app.label}: Waiting for currently running schedule to complete before starting", 'c') + return true + } + + // Somthing is running, but we don't need to run today anyway - don't need to do busyOff() + // (Probably should never get here, because preCheck() should check isDay() before calling busy() + log.debug "Another schedule is running, but ${app.label} is not scheduled for today anyway" + return true +} + +def busyOff(evt){ + def cst = settings.switches.currentStatus + if ((settings.switches.currentSwitch == 'off') && (cst != 'pause')) { // double check that prior schedule is done + unsubscribe(switches) // we don't want any more button pushes until preCheck runs + Random rand = new Random() // just in case there are multiple schedules waiting on the same controller + int randomSeconds = rand.nextInt(120) + 15 + runIn(randomSeconds, preCheck) // no message so we don't clog the system + note('active', "${app.label}: ${settings.switches} finished, starting in ${randomSeconds} seconds", 'i') + } +} + +//run check every day +def preCheck() { + + if (!isDay()) { + log.debug "preCheck() Skipping: ${app.label} is not scheduled for today" // silent - no note + //if (!atomicState.run && enableManual) subscribe(switches, 'switch.programOn', manualStart) // only if we aren't running already + return + } + + if (!busy()) { + atomicState.run = true // set true before doing anything, atomic in case we crash (busy() set it false if !busy) + settings.switches.programWait() // take over the controller so other schedules don't mess with us + runIn(45, checkRunMap) // schedule checkRunMap() before doing weather check, gives isWeather 45s to complete + // because that seems to be a little more than the max that the ST platform allows + unsubAllBut() // unsubscribe to everything except appTouch() + subscribe(settings.switches, 'switch.off', cycleOff) // and start setting up for today's cycle + def start = now() + note('active', "${app.label}: Starting...", 'd') // + def end = now() + log.debug "preCheck note active ${end - start}ms" + + if (isWeather()) { // set adjustments and check if we shold skip because of rain + resetEverything() // if so, clean up our subscriptions + switches.programOff() // and release the controller + } + else { + log.debug 'preCheck(): running checkRunMap in 2 seconds' //COOL! We finished before timing out, and we're supposed to water today + runIn(2, checkRunMap) // jack the schedule so it runs sooner! + } + } +} + +//start water program +def cycleOn(){ + if (atomicState.run) { // block if manually stopped during precheck which goes to cycleOff + + if (!isWaterStopped()) { // make sure ALL the contacts and toggles aren't paused + // All clear, let's start running! + subscribe(settings.switches, 'switch.off', cycleOff) + subWaterStop() // subscribe to all the pause contacts and toggles + resume() + + // send the notification AFTER we start the controller (in case note() causes us to run over our execution time limit) + String newString = "${app.label}: Starting..." + if (!atomicState.startTime) { + atomicState.startTime = now() // if we haven't already started + if (atomicState.startTime) atomicState.finishTime = null // so recovery in busy() knows we didn't finish + if (state.pauseTime) state.pauseTime = null + if (state.totalTime) { + String finishTime = new Date(now() + (60000 * state.totalTime).toLong()).format('EE @ h:mm a', location.timeZone) + newString = "${app.label}: Starting - ETC: ${finishTime}" + } + } + else if (state.pauseTime) { // resuming after a pause + + def elapsedTime = Math.round((now() - state.pauseTime) / 60000) // convert ms to minutes + int tt = state.totalTime + elapsedTime + 1 + state.totalTime = tt // keep track of the pauses, and the 1 minute delay above + String finishTime = new Date(atomicState.startTime + (60000 * tt).toLong()).format('EE @ h:mm a', location.timeZone) + state.pauseTime = null + newString = "${app.label}: Resuming - New ETC: ${finishTime}" + } + note('active', newString, 'd') + } + else { + // Ready to run, but one of the control contacts is still open, so we wait + subWaterStart() // one of them is paused, let's wait until the are all clear! + note('pause', "${app.label}: Watering paused, ${getWaterStopList()}", 'c') + } } } - -//write initial zone settings to device at install/update -def writeSettings() + +//when switch reports off, watering program is finished +def cycleOff(evt){ + + if (atomicState.run) { + def ft = new Date() + atomicState.finishTime = ft // this is important to reset the schedule after failures in busy() + String finishTime = ft.format('h:mm a', location.timeZone) + note('finished', "${app.label}: Finished watering at ${finishTime}", 'd') + } + else { + log.debug "${settings.switches} turned off" // is this a manual off? perhaps we should send a note? + } + resetEverything() // all done here, back to starting state +} + +//run check each day at scheduled time +def checkRunMap(){ + + //check if isWeather returned true or false before checking + if (atomicState.run) { + + //get & set watering times for today + def runNowMap = [] + runNowMap = cycleLoop(1) // build the map + + if (runNowMap) { + runIn(60, cycleOn) // start water + subscribe(settings.switches, 'switch.off', cycleOff) // allow manual off before cycleOn() starts + if (atomicState.startTime) atomicState.startTime = null // these were already cleared in cycleLoop() above + if (state.pauseTime) state.pauseTime = null // ditto + // leave atomicState.finishTime alone so that recovery in busy() knows we never started if cycleOn() doesn't clear it + + String newString = '' + int tt = state.totalTime + if (tt) { + int hours = tt / 60 // DON'T Math.round this one + int mins = tt - (hours * 60) + String hourString = '' + String s = '' + if (hours > 1) s = 's' + if (hours > 0) hourString = "${hours} hour${s} & " + s = 's' + if (mins == 1) s = '' + newString = "run time: ${hourString}${mins} minute${s}:\n" + } + note('active', "${app.label}: Watering in 1 minute, ${newString}${runNowMap}", 'd') + } + else { + unsubscribe(settings.switches) + unsubWaterStoppers() + switches.programOff() + if (enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) + note('skipping', "${app.label}: No watering today", 'd') + if (atomicState.run) atomicState.run = false // do this last, so that the above note gets sent to the controller + } + } + else { + log.debug 'checkRunMap(): atomicState.run = false' // isWeather cancelled us out before we got started + } +} + +//get todays schedule +def cycleLoop(int i) { - 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 + boolean isDebug = false + if (isDebug) log.debug "cycleLoop(${i})" + + int zone = 1 + int dpw = 0 + int tpw = 0 + int cyc = 0 + int rtime = 0 + def timeMap = [:] + def pumpMap = "" + def runNowMap = "" + String soilString = '' + int totalCycles = 0 + int totalTime = 0 + if (atomicState.startTime) atomicState.startTime = null // haven't started yet + + while(zone <= 16) + { + rtime = 0 + def setZ = settings."zone${zone}" + if ((setZ && (setZ != 'Off')) && (nozzle(zone) != 4) && zoneActive(zone.toString())) { + + // First check if we run this zone today, use either dpwMap or even/odd date + dpw = getDPW(zone) + int runToday = 0 + // if manual, or every day allowed, or zone uses a sensor, then we assume we can today + // - preCheck() has already verified that today isDay() + if ((i == 0) || (state.daysAvailable == 7) || (settings."sensor${zone}")) { + runToday = 1 + } + else { + + dpw = getDPW(zone) // figure out if we need to run (if we don't already know we do) + if (settings.days && (settings.days.contains('Even') || settings.days.contains('Odd'))) { + def daynum = new Date().format('dd', location.timeZone) + int dayint = Integer.parseInt(daynum) + if (settings.days.contains('Odd') && (((dayint +1) % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 + else if (settings.days.contains('Even') && ((dayint % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 + } + else { + int weekDay = getWeekDay()-1 + def dpwMap = getDPWDays(dpw) + runToday = dpwMap[weekDay] //1 or 0 + if (isDebug) log.debug "Zone: ${zone} dpw: ${dpw} weekDay: ${weekDay} dpwMap: ${dpwMap} runToday: ${runToday}" + + } + } + + // OK, we're supposed to run (or at least adjust the sensors) + if (runToday == 1) + { + def soil + if (i == 0) soil = moisture(0) // manual + else soil = moisture(zone) // moisture check + soilString = "${soilString}${soil[1]}" + + // Run this zone if soil moisture needed + if ( soil[0] == 1 ) + { + cyc = cycles(zone) + tpw = getTPW(zone) + dpw = getDPW(zone) // moisture() may have changed DPW + + rtime = calcRunTime(tpw, dpw) + //daily weather adjust if no sensor + if(settings.isSeason && (!settings.learn || !settings."sensor${zone}")) { + + + rtime = Math.round(((rtime / cyc) * (state.seasonAdj / 100.0)) + 0.4) + } + else { + rtime = Math.round((rtime / cyc) + 0.4) // let moisture handle the seasonAdjust for Adaptive (learn) zones + } + totalCycles += cyc + totalTime += (rtime * cyc) + runNowMap += "${settings."name${zone}"}: ${cyc} x ${rtime} min\n" + if (isDebug) log.debug "Zone ${zone} Map: ${cyc} x ${rtime} min - totalTime: ${totalTime}" + } + } + } + if (nozzle(zone) == 4) pumpMap += "${settings."name${zone}"}: ${settings."zone${zone}"} on\n" + timeMap."${zone+1}" = "${rtime}" + zone++ + } + + if (soilString) { + String seasonStr = '' + String plus = '' + float sa = state.seasonAdj + if (settings.isSeason && (sa != 100.0) && (sa != 0.0)) { + float sadj = sa - 100.0 + if (sadj > 0.0) plus = '+' //display once in cycleLoop() + int iadj = Math.round(sadj) + if (iadj != 0) seasonStr = "Adjusting ${plus}${iadj}% for weather forecast\n" + } + note('moisture', "${app.label} Sensor status:\n${seasonStr}${soilString}" /* + seasonStr + soilString */,'m') + } + + if (!runNowMap) { + return runNowMap // nothing to run today + } + + //send settings to Spruce Controller + switches.settingsMap(timeMap,4002) + runIn(30, writeCycles) + + // meanwhile, calculate our total run time + int pDelay = 0 + if (settings.pumpDelay && settings.pumpDelay.isNumber()) pDelay = settings.pumpDelay.toInteger() + totalTime += Math.round(((pDelay * (totalCycles-1)) / 60.0)) // add in the pump startup and inter-zone delays + state.totalTime = totalTime + + if (state.pauseTime) state.pauseTime = null // and we haven't paused yet + // but let cycleOn() reset finishTime + return (runNowMap + pumpMap) +} + +//send cycle settings +def writeCycles(){ + //log.trace "writeCycles()" + def cyclesMap = [:] + //add pumpdelay @ 1 cyclesMap."1" = pumpDelayString() - def zone = 1 - def cycle = 0 + int zone = 1 + int 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) + switches.settingsMap(cyclesMap, 4001) } -// 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 +def resume(){ + log.debug 'resume()' + settings.switches.zon() } -def checkOn(){ - cycleOn() -} - -//start water program -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){ - state.run = false - if (contact == null || !contact.currentValue('contact').contains('open')){ - note("finished", "finished watering for today", "d") - unsubscribe(contact) - } +def syncOn(evt){ + // double check that the switch is actually finished and not just paused + if ((settings.sync.currentSwitch == 'off') && (settings.sync.currentStatus != 'pause')) { + resetEverything() // back to our known state + Random rand = new Random() // just in case there are multiple schedules waiting on the same controller + int randomSeconds = rand.nextInt(120) + 15 + runIn(randomSeconds, preCheck) // no message so we don't clog the system + note('schedule', "${app.label}: ${settings.sync} finished, starting in ${randomSeconds} seconds", 'c') + } // else, it is just pausing...keep waiting for the next "off" } -//start check -def manualStart(evt){ +// handle start of pause session +def waterStop(evt){ + log.debug "waterStop: ${evt.displayName}" - 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)) - { - 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() - state.run = false - note("skipping", "No watering allowed today.", "d") - } - else if (isWeather() == false) - { - //get & set watering times for today - runNowMap = cycleLoop() - if (runNowMap) - { - state.run = true - runNowMap = "Water will begin in 2 minutes:\n" + runNowMap - note("active", "${runNowMap}", "d") - //cycleOn() //start water program - } - else { - switches.programOff() - state.run = false - note("skipping", "No watering scheduled for today.", "d") - } - } - else { - switches.programOff() - state.run = false - } -} - -//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 - //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 - 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 (!learn && (settings["sensor${zone}"] != null) ) runToday = 1 - - if(runToday) - { - 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" - } - } + unschedule(cycleOn) // in case we got stopped again before cycleOn starts from the restart + unsubscribe(settings.switches) + subWaterStart() + + if (!state.pauseTime) { // only need to do this for the first event if multiple contacts + state.pauseTime = now() + + String cond = evt.value + switch (cond) { + case 'open': + cond = 'opened' + break + case 'on': + cond = 'switched on' + break + case 'off': + cond = 'switched off' + break + //case 'closed': + // cond = 'closed' + // break + case null: + cond = '????' + break + default: + break } - 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 + note('pause', "${app.label}: Watering paused - ${evt.displayName} ${cond}", 'c') // set to Paused + } + if (settings.switches.currentSwitch != 'off') { + runIn(30, subOff) + settings.switches.off() // stop the water + } + else + subscribe(settings.switches, 'switch.off', cycleOff) +} + +// This is a hack to work around the delay in response from the controller to the above programOff command... +// We frequently see the off notification coming a long time after the command is issued, so we try to catch that so that +// we don't prematurely exit the cycle. +def subOff() { + subscribe(settings.switches, 'switch.off', offPauseCheck) +} + +def offPauseCheck( evt ) { + unsubscribe(settings.switches) + subscribe(settings.switches, 'switch.off', cycleOff) + if (/*(switches.currentSwitch != 'off') && */ (settings.switches.currentStatus != 'pause')) { // eat the first off while paused + cycleOff(evt) + } +} + +// handle end of pause session +def waterStart(evt){ + if (!isWaterStopped()){ // only if ALL of the selected contacts are not open + def cDelay = 10 + if (settings.contactDelay > 10) cDelay = settings.contactDelay + runIn(cDelay, cycleOn) + + unsubscribe(settings.switches) + subWaterStop() // allow stopping again while we wait for cycleOn to start + + log.debug "waterStart(): enabling device is ${evt.device} ${evt.value}" + + String cond = evt.value + switch (cond) { + case 'open': + cond = 'opened' + break + case 'on': + cond = 'switched on' + break + case 'off': + cond = 'switched off' + break + //case 'closed': + // cond = 'closed' + // break + case null: + cond = '????' + break + default: + break + } + // let cycleOn() change the status to Active - keep us paused until then + + note('pause', "${app.label}: ${evt.displayName} ${cond}, watering in ${cDelay} seconds", 'c') + } + else { + log.debug "waterStart(): one down - ${evt.displayName}" + } } //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 +int initDPW(int zone){ + //log.debug "initDPW(${zone})" + if(!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + + int tpw = getTPW(zone) // was getTPW -does not update times in scheduler without initTPW + int dpw = 0 + + if(tpw > 0) { + float perDay = 20.0 + if(settings."perDay${zone}") perDay = settings."perDay${zone}".toFloat() + + dpw = Math.round(tpw.toFloat() / perDay) + if(dpw <= 1) dpw = 1 // 3 days per week not allowed for even or odd day selection if(dpw == 3 && days && (days.contains('Even') || days.contains('Odd')) && !(days.contains('Even') && days.contains('Odd'))) - if(initTPW(i) / perDay < 3.0) return 2 - else return 4 - if(daysAvailable() < dpw) return daysAvailable() - return dpw + if((tpw.toFloat() / perDay) < 3.0) dpw = 2 else dpw = 4 + int daycheck = daysAvailable() // initialize & optimize daysAvailable + if (daycheck < dpw) dpw = daycheck } - return 0 + state.dpwMap[zone-1] = dpw + return dpw } // 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) +int getDPW(int zone) { + if (state.dpwMap) return state.dpwMap[zone-1] else return initDPW(zone) } //Initialize Time per Week -def initTPW(i){ - if("${settings["zone${i}"]}" == null || nozzle(i) == 0 || nozzle(i) == 4 || plant(i) == 0) return 0 +int initTPW(int zone) { + //log.trace "initTPW(${zone})" + if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + + int n = nozzle(zone) + def zn = settings."zone${zone}" + if (!zn || (zn == 'Off') || (n == 0) || (n == 4) || (plant(zone) == 0) || !zoneActive(zone.toString())) return 0 // apply gain adjustment - def gainAdjust = 100 - if (gain && gain != 0) gainAdjust += gain + float gainAdjust = 100.0 + if (settings.gain && settings.gain != 0) gainAdjust += settings.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 + // apply seasonal adjustment if enabled and not set to new plants + float seasonAdjust = 100.0 + def wsa = state.weekseasonAdj + if (wsa && isSeason && (settings."plant${zone}" != 'New Plants')) seasonAdjust = wsa - def zone = i.toInteger() - def tpw = 0 + int tpw = 0 + // Use learned, previous tpw if it is available + if ( settings."sensor${zone}" ) { + seasonAdjust = 100.0 // no weekly seasonAdjust if this zone uses a sensor + if(state.tpwMap && settings.learn) tpw = state.tpwMap[zone-1] + } - // 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 + // set user-specified minimum time with seasonal adjust + int minWeek = 0 + def mw = settings."minWeek${zone}" + if (mw) minWeek = mw.toInteger() + if (minWeek != 0) { + tpw = Math.round(minWeek * (seasonAdjust / 100.0)) + } + else if (!tpw || (tpw == 0)) { // use calculated tpw + tpw = Math.round((plant(zone) * nozzle(zone) * (gainAdjust / 100.0) * (seasonAdjust / 100.0))) + } + state.tpwMap[zone-1] = tpw return tpw } // Get the current time per week, calls init if not defined -def getTPW(zone) +int getTPW(int zone) { - def i = zone.toInteger() - if(state.tpwMap) return state.tpwMap.get(i-1) - return initTPW(i) + if (state.tpwMap) return state.tpwMap[zone-1] else return initTPW(zone) } // Calculate daily run time based on tpw and dpw -def calcRunTime(tpw, dpw) +int calcRunTime(int tpw, int dpw) { - def duration = 0 - if(tpw > 0 && dpw > 0) { - duration = Math.round(tpw / dpw) - } + int duration = 0 + if ((tpw > 0) && (dpw > 0)) duration = Math.round(tpw.toFloat() / dpw.toFloat()) 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) +def moisture(int i) { - // No Sensor on this zone - if(settings["sensor${i}"] == null) { - return [1,""] + boolean isDebug = false + if (isDebug) log.debug "moisture(${i})" + + def endMsecs = 0 + // No Sensor on this zone or manual start skips moisture checking altogether + if ((i == 0) || !settings."sensor${i}") { + return [1,''] } - // 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 + // Ensure that the sensor has reported within last 48 hours + int spHum = getDrySp(i) + int hours = 48 + def yesterday = new Date(now() - (/* 1000 * 60 * 60 */ 3600000 * hours).toLong()) + float latestHum = settings."sensor${i}".latestValue('humidity').toFloat() // state = 29, value = 29.13 + def lastHumDate = settings."sensor${i}".latestState('humidity').date + if (lastHumDate < yesterday) { + note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, no humidity reports in the last ${hours} hours", 'a') + + if (latestHum < spHum) + latestHum = spHum - 1.0 // amke sure we water and do seasonal adjustments, but not tpw adjustments + else + latestHum = spHum + 0.99 // make sure we don't water, do seasonal adjustments, but not tpw adjustments } - - def latestHum = settings["sensor${i}"].latestValue("humidity") - def spHum = getDrySp(i).toInteger() - //def moistureList = [] - if (!learn) + + if (!settings.learn) { - // no learn mode, only looks at target moisture level - if(latestHum <= spHum) { + // in Delay mode, only looks at target moisture level, doesn't try to adjust tpw + // (Temporary) seasonal adjustment WILL be applied in cycleLoop(), as if we didn't have a sensor + if (latestHum <= spHum.toFloat()) { //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"] + 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 + //in Adaptive mode + int tpw = getTPW(i) + int dpw = getDPW(i) + int cpd = cycles(i) + + + + + if (isDebug) log.debug "moisture(${i}): tpw: ${tpw}, dpw: ${dpw}, cycles: ${cpd} (before adjustment)" + + float diffHum = 0.0 + if (latestHum > 0.0) diffHum = (spHum - latestHum) / 100.0 + else { + diffHum = 0.02 // Safety valve in case sensor is reporting 0% humidity (e.g., somebody pulled it out of the ground or flower pot) + note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, it is currently reading 0%", 'a') + } + + int daysA = state.daysAvailable + int minimum = cpd * dpw // minimum of 1 minute per scheduled days per week (note - can be 1*1=1) + if (minimum < daysA) minimum = daysA // but at least 1 minute per available day + int tpwAdjust = 0 + + if (diffHum > 0.01) { // only adjust tpw if more than 1% of target SP + tpwAdjust = Math.round(((tpw * diffHum) + 0.5) * dpw * cpd) // Compute adjustment as a function of the current tpw + float adjFactor = 2.0 / daysA // Limit adjustments to 200% per week - spread over available days + if (tpwAdjust > (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) + 0.5) // limit fast rise + if (tpwAdjust < minimum) tpwAdjust = minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time + } else if (diffHum < -0.01) { + if (diffHum < -0.05) diffHum = -0.05 // try not to over-compensate for a heavy rainstorm... + tpwAdjust = Math.round(((tpw * diffHum) - 0.5) * dpw * cpd) + float adjFactor = -0.6667 / daysA // Limit adjustments to 66% per week + if (tpwAdjust < (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) - 0.5) // limit slow decay + if (tpwAdjust > (-1 * minimum)) tpwAdjust = -1 * minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time } - 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" + int seasonAdjust = 0 + if (isSeason) { + float sa = state.seasonAdj + if ((sa != 100.0) && (sa != 0.0)) { + float sadj = sa - 100.0 + if (sa > 0.0) + seasonAdjust = Math.round(((sadj / 100.0) * tpw) + 0.5) + else + seasonAdjust = Math.round(((sadj / 100.0) * tpw) - 0.5) + } } - //note("moisture", "${moistureSum}","m") - return [1, "${moistureSum}"] + if (isDebug) log.debug "moisture(${i}): diffHum: ${diffHum}, tpwAdjust: ${tpwAdjust} seasonAdjust: ${seasonAdjust}" + + // Now, adjust the tpw. + // With seasonal adjustments enabled, tpw can go up or down independent of the difference in the sensor vs SP + int newTPW = tpw + tpwAdjust + seasonAdjust + + int perDay = 20 + def perD = settings."perDay${i}" + if (perD) perDay = perD.toInteger() + if (perDay == 0) perDay = daysA * cpd // at least 1 minute per cycle per available day + if (newTPW < perDay) newTPW = perDay // make sure we have always have enough for 1 day of minimum water + + int adjusted = 0 + if ((tpwAdjust + seasonAdjust) > 0) { // needs more water + int maxTPW = daysA * 120 // arbitrary maximum of 2 hours per available watering day per week + if (newTPW > maxTPW) newTPW = maxTPW // initDPW() below may spread this across more days + if (newTPW > (maxTPW * 0.75)) note('warning', "${app.label}: Please check ${settings["sensor${i}"]}, ${settings."name${i}"} time per week seems high: ${newTPW} mins/week",'a') + if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? + state.tpwMap[i-1] = newTPW + dpw = initDPW(i) // need to recalculate days per week since tpw changed - initDPW() stores the value into dpwMap + adjusted = newTPW - tpw // so that the adjustment note is accurate + } + } + else if ((tpwAdjust + seasonAdjust) < 0) { // Needs less water + // Find the minimum tpw + minimum = cpd * daysA // at least 1 minute per cycle per available day + int minLimit = 0 + def minL = settings."minWeek${i}" + if (minL) minLimit = minL.toInteger() // unless otherwise specified in configuration + if (minLimit > 0) { + if (newTPW < minLimit) newTPW = minLimit // use configured minutes per week as the minimum + } else if (newTPW < minimum) { + newTPW = minimum // else at least 1 minute per cycle per available day + note('warning', "${app.label}: Please check ${settings."sensor${i}"}, ${settings."name${i}"} time per week is very low: ${newTPW} mins/week",'a') + } + if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? + state.tpwMap[i-1] = newTPW // store the new tpw + dpw = initDPW(i) // may need to reclac days per week - initDPW() now stores the value into state.dpwMap - avoid doing that twice + adjusted = newTPW - tpw // so that the adjustment note is accurate + } + } + // else no adjustments, or adjustments cancelled each other out. + + String moistureSum = '' + String adjStr = '' + String plus = '' + if (adjusted > 0) plus = '+' + if (adjusted != 0) adjStr = ", ${plus}${adjusted} min" + if (Math.abs(adjusted) > 1) adjStr = "${adjStr}s" + if (diffHum >= 0.0) { // water only if ground is drier than SP + moistureSum = "> ${settings."name${i}"}, Water: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" + return [1, moistureSum] + } + else { // not watering + moistureSum = "> ${settings."name${i}"}, Skip: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" + return [0, moistureSum] + } + return [0, moistureSum] } - + +//get moisture SP +int getDrySp(int i){ + if (settings."sensorSp${i}") return settings."sensorSp${i}".toInteger() // configured SP + + + if (settings."plant${i}" == 'New Plants') return 40 // New Plants get special care + + + switch (settings."option${i}") { // else, defaults based off of soil type + case 'Sand': + return 22 + case 'Clay': + return 38 + default: + return 28 + } +} + //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}" - } +def note(String statStr, String msg, String msgType) { + + // send to debug first (near-zero cost) + log.debug "${statStr}: ${msg}" + + // notify user second (small cost) + boolean notifyController = true + if(settings.notify || settings.logAll) { + String spruceMsg = "Spruce ${msg}" + switch(msgType) { + case 'd': + if (settings.notify && settings.notify.contains('Daily')) { // always log the daily events to the controller + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'c': + if (settings.notify && settings.notify.contains('Delays')) { + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'i': + if (settings.notify && settings.notify.contains('Events')) { + sendIt(spruceMsg) + //notifyController = false // no need to notify controller unless we don't notify the user + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'f': + notifyController = false // no need to notify the controller, ever + if (settings.notify && settings.notify.contains('Weather')) { + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'a': + notifyController = false // no need to notify the controller, ever + if (settings.notify && settings.notify.contains('Warnings')) { + sendIt(spruceMsg) + } else + sendNotificationEvent(spruceMsg) // Special case - make sure this goes into the Hello Home log, if not notifying + break + case 'm': + if (settings.notify && settings.notify.contains('Moisture')) { + sendIt(spruceMsg) + //notifyController = false // no need to notify controller unless we don't notify the user + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + default: + break + } + } + // finally, send to controller DTH, to change the state and to log important stuff in the event log + if (notifyController) { // do we really need to send these to the controller? + // only send status updates to the controller if WE are running, or nobody else is + if (atomicState.run || ((settings.switches.currentSwitch == 'off') && (settings.switches.currentStatus != 'pause'))) { + settings.switches.notify(statStr, msg) + + } + else { // we aren't running, so we don't want to change the status of the controller + // send the event using the current status of the switch, so we don't change it + //log.debug "note - direct sendEvent()" + settings.switches.notify(settings.switches.currentStatus, msg) + + } + } +} + +def sendIt(String msg) { + if (location.contactBookEnabled && settings.recipients) { + sendNotificationToContacts(msg, settings.recipients, [event: true]) + } + else { + sendPush( msg ) } } //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 - } - } +int daysAvailable(){ + + // Calculate days available for watering and save in state variable for future use + def daysA = state.daysAvailable + if (daysA && (daysA > 0)) { // state.daysAvailable has already calculated and stored in state.daysAvailable + return daysA + } + + if (!settings.days) { // settings.days = "" --> every day is available + state.daysAvailable = 7 + return 7 // every day is allowed + } + + int dayCount = 0 // settings.days specified, need to calculate state.davsAvailable (once) + if (settings.days.contains('Even') || settings.days.contains('Odd')) { + dayCount = 4 + if(settings.days.contains('Even') && settings.days.contains('Odd')) dayCount = 7 + } + else { + if (settings.days.contains('Monday')) dayCount += 1 + if (settings.days.contains('Tuesday')) dayCount += 1 + if (settings.days.contains('Wednesday')) dayCount += 1 + if (settings.days.contains('Thursday')) dayCount += 1 + if (settings.days.contains('Friday')) dayCount += 1 + if (settings.days.contains('Saturday')) dayCount += 1 + if (settings.days.contains('Sunday')) dayCount += 1 + } + + state.daysAvailable = dayCount 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}"] +int nozzle(int i){ + String getT = settings."zone${i}" if (!getT) return 0 + switch(getT) { - case "Spray": + case 'Spray': return 1 - case "Rotor": + case 'Rotor': return 1.4 - case "Drip": + case 'Drip': return 2.4 - case "Master Valve": + case 'Master Valve': return 4 - case "Pump": + case 'Pump': return 4 default: return 0 @@ -1214,23 +1969,24 @@ def nozzle(i){ } //plant: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants'] -def plant(i){ - def getP = settings["plant${i}"] +int plant(int i){ + String getP = settings."plant${i}" if(!getP) return 0 + switch(getP) { - case "Lawn": + case 'Lawn': return 60 - case "Garden": + case 'Garden': return 50 - case "Flowers": + case 'Flowers': return 40 - case "Shrubs": + case 'Shrubs': return 30 - case "Trees": + case 'Trees': return 20 - case "Xeriscape": + case 'Xeriscape': return 30 - case "New Plants": + case 'New Plants': return 80 default: return 0 @@ -1238,21 +1994,22 @@ def plant(i){ } //option: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x'] -def cycles(i){ - def getC = settings["option${i}"] +int cycles(int i){ + String getC = settings."option${i}" if(!getC) return 2 + switch(getC) { - case "Slope": + case 'Slope': return 3 - case "Sand": + case 'Sand': return 1 - case "Clay": + case 'Clay': return 2 - case "No Cycle": + case 'No Cycle': return 1 - case "Cycle 2x": + case 'Cycle 2x': return 2 - case "Cycle 3x": + case 'Cycle 3x': return 3 default: return 2 @@ -1260,188 +2017,374 @@ def cycles(i){ } //check if day is allowed -def isDay() { - if ("${settings["days"]}" == "null") return true +boolean isDay() { + + if (daysAvailable() == 7) return true // every day is allowed - 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 + def daynow = new Date() + String today = daynow.format('EEEE', location.timeZone) + if (settings.days.contains(today)) return true + def daynum = daynow.format('dd', location.timeZone) + int dayint = Integer.parseInt(daynum) + if (settings.days.contains('Even') && (dayint % 2 == 0)) return true + if (settings.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++ - } + boolean isDebug = false + if (isDebug) log.debug 'setSeason()' + + int zone = 1 + while(zone <= 16) { + if ( !settings.learn || !settings."sensor${zone}" || state.tpwMap[zone-1] == 0) { + + int tpw = initTPW(zone) // now updates state.tpwMap + int dpw = initDPW(zone) // now updates state.dpwMap + if (isDebug) { + if (!settings.learn && (tpw != 0) && (state.weekseasonAdj != 0)) { + log.debug "Zone ${zone}: seasonally adjusted by ${state.weekseasonAdj-100}% to ${tpw}" + } + } + } + zone++ + } } -//check weather -def isWeather(){ - def wzipcode = "${zipString()}" - - // Forecast rain - 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 +//capture today's total rainfall - scheduled for just before midnight each day +def getRainToday() { + def wzipcode = zipString() + Map wdata = getWeatherFeature('conditions', wzipcode) + if (!wdata) { + + note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null", 'a') + } + else { + if (!wdata.response || wdata.response.containsKey('error')) { + log.debug wdata.response + note('warning', "${app.label}: Please check Zipcode/PWS setting, error:\n${wdata.response.error.type}: ${wdata.response.error.description}" , 'a') + } + else { + float TRain = 0.0 + if (wdata.current_observation.precip_today_in.isNumber()) { // WU can return "t" for "Trace" - we'll assume that means 0.0 + TRain = wdata.current_observation.precip_today_in.toFloat() + if (TRain > 25.0) TRain = 25.0 + else if (TRain < 0.0) TRain = 0.0 // WU sometimes returns -999 for "estimated" locations + log.debug "getRainToday(): ${wdata.current_observation.precip_today_in} / ${TRain}" + } + int day = getWeekDay() // what day is it today? + if (day == 7) day = 0 // adjust: state.Rain order is Su,Mo,Tu,We,Th,Fr,Sa + state.Rain[day] = TRain as Float // store today's total rainfall + } } - def 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 - 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 - 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() - 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 - 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") - +} + +//check weather, set seasonal adjustment factors, skip today if rainy +boolean isWeather(){ + def startMsecs = 0 + def endMsecs = 0 + boolean isDebug = false + if (isDebug) log.debug 'isWeather()' + + if (!settings.isRain && !settings.isSeason) return false // no need to do any of this + + String wzipcode = zipString() + if (isDebug) log.debug "isWeather(): ${wzipcode}" + + // get only the data we need + // Moved geolookup to installSchedule() + String featureString = 'forecast/conditions' + if (settings.isSeason) featureString = "${featureString}/astronomy" + if (isDebug) startMsecs= now() + Map wdata = getWeatherFeature(featureString, wzipcode) + if (isDebug) { + endMsecs = now() + log.debug "isWeather() getWeatherFeature elapsed time: ${endMsecs - startMsecs}ms" + } + if (wdata && wdata.response) { + if (isDebug) log.debug wdata.response + if (wdata.response.containsKey('error')) { + if (wdata.response.error.type != 'invalidfeature') { + note('warning', "${app.label}: Please check Zipcode/PWS setting, error:\n${wdata.response.error.type}: ${wdata.response.error.description}" , 'a') + return false + } + else { + // Will find out which one(s) weren't reported later (probably never happens now that we don't ask for history) + log.debug 'Rate limited...one or more WU features unavailable at this time.' + } + } + } + else { + if (isDebug) log.debug 'wdata is null' + note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null" , 'a') + return false + } + + String city = wzipcode + + + + if (wdata.current_observation) { + if (wdata.current_observation.observation_location.city != '') city = wdata.current_observation.observation_location.city + else if (wdata.current_observation.observation_location.full != '') city = wdata.current_observation.display_location.full + + if (wdata.current_observation.estimated.estimated) city = "${city} (est)" + } + + // OK, we have good data, let's start the analysis + float qpfTodayIn = 0.0 + float qpfTomIn = 0.0 + float popToday = 50.0 + float popTom = 50.0 + float TRain = 0.0 + float YRain = 0.0 + float weeklyRain = 0.0 + + if (settings.isRain) { + if (isDebug) log.debug 'isWeather(): isRain' + + // Get forecasted rain for today and tomorrow + + if (!wdata.forecast) { + log.debug 'isWeather(): Unable to get weather forecast.' + return false + } + if (wdata.forecast.simpleforecast.forecastday[0].qpf_allday.in.isNumber()) qpfTodayIn = wdata.forecast.simpleforecast.forecastday[0].qpf_allday.in.toFloat() + if (wdata.forecast.simpleforecast.forecastday[0].pop.isNumber()) popToday = wdata.forecast.simpleforecast.forecastday[0].pop.toFloat() + if (wdata.forecast.simpleforecast.forecastday[1].qpf_allday.in.isNumber()) qpfTomIn = wdata.forecast.simpleforecast.forecastday[1].qpf_allday.in.toFloat() + if (wdata.forecast.simpleforecast.forecastday[1].pop.isNumber()) popTom = wdata.forecast.simpleforecast.forecastday[1].pop.toFloat() + if (qpfTodayIn > 25.0) qpfTodayIn = 25.0 + else if (qpfTodayIn < 0.0) qpfTodayIn = 0.0 + if (qpfTomIn > 25.0) qpfTomIn = 25.0 + else if (qpfTomIn < 0.0) qpfTomIn = 0.0 + + // Get rainfall so far today + + if (!wdata.current_observation) { + log.debug 'isWeather(): Unable to get current weather conditions.' + return false + } + if (wdata.current_observation.precip_today_in.isNumber()) { + TRain = wdata.current_observation.precip_today_in.toFloat() + if (TRain > 25.0) TRain = 25.0 // Ignore runaway weather + else if (TRain < 0.0) TRain = 0.0 // WU can return -999 for estimated locations + } + if (TRain > (qpfTodayIn * (popToday / 100.0))) { // Not really what PoP means, but use as an adjustment factor of sorts + qpfTodayIn = TRain // already have more rain than was forecast for today, so use that instead + popToday = 100 // we KNOW this rain happened + } + + // Get yesterday's rainfall + int day = getWeekDay() + YRain = state.Rain[day - 1] + + if (isDebug) log.debug "TRain ${TRain} qpfTodayIn ${qpfTodayIn} @ ${popToday}%, YRain ${YRain}" + + int i = 0 + while (i <= 6){ // calculate (un)weighted average (only heavy rainstorms matter) + int factor = 0 + if ((day - i) > 0) factor = day - i else factor = day + 7 - i + float getrain = state.Rain[i] + if (factor != 0) weeklyRain += (getrain / factor) + i++ + } + + if (isDebug) log.debug "isWeather(): weeklyRain ${weeklyRain}" + } + + if (isDebug) log.debug 'isWeather(): build report' + //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) + int highToday = 0 + int highTom = 0 + if (wdata.forecast.simpleforecast.forecastday[0].high.fahrenheit.isNumber()) highToday = wdata.forecast.simpleforecast.forecastday[0].high.fahrenheit.toInteger() + if (wdata.forecast.simpleforecast.forecastday[1].high.fahrenheit.isNumber()) highTom = wdata.forecast.simpleforecast.forecastday[1].high.fahrenheit.toInteger() + + String weatherString = "${app.label}: ${city} weather:\n TDA: ${highToday}F" + if (settings.isRain) weatherString = "${weatherString}, ${qpfTodayIn}in rain (${Math.round(popToday)}% PoP)" + weatherString = "${weatherString}\n TMW: ${highTom}F" + if (settings.isRain) weatherString = "${weatherString}, ${qpfTomIn}in rain (${Math.round(popTom)}% PoP)\n YDA: ${YRain}in rain" - 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 - state.seasonAdj = Math.round(getHigh.get(0).toInteger()/avgHigh *100) - weatherString += "\n Adjusted ${state.seasonAdj - 100}% for Today" + if (settings.isSeason) + { + if (!settings.isRain) { // we need to verify we have good data first if we didn't do it above + + if (!wdata.forecast) { + log.debug 'Unable to get weather forecast' + return false + } + } + + // is the temp going up or down for the next few days? + float heatAdjust = 100.0 + float avgHigh = highToday.toFloat() + if (highToday != 0) { + // is the temp going up or down for the next few days? + int totalHigh = highToday + int j = 1 + int highs = 1 + while (j < 4) { // get forecasted high for next 3 days + if (wdata.forecast.simpleforecast.forecastday[j].high.fahrenheit.isNumber()) { + totalHigh += wdata.forecast.simpleforecast.forecastday[j].high.fahrenheit.toInteger() + highs++ + } + j++ + } + if ( highs > 0 ) avgHigh = (totalHigh / highs) + heatAdjust = avgHigh / highToday + } + if (isDebug) log.debug "highToday ${highToday}, avgHigh ${avgHigh}, heatAdjust ${heatAdjust}" + + //get humidity + int humToday = 0 + if (wdata.forecast.simpleforecast.forecastday[0].avehumidity.isNumber()) + humToday = wdata.forecast.simpleforecast.forecastday[0].avehumidity.toInteger() + + float humAdjust = 100.0 + float avgHum = humToday.toFloat() + if (humToday != 0) { + int j = 1 + int highs = 1 + int totalHum = humToday + while (j < 4) { // get forcasted humitidty for today and the next 3 days + if (wdata.forecast.simpleforecast.forecastday[j].avehumidity.isNumber()) { + totalHum += wdata.forecast.simpleforecast.forecastday[j].avehumidity.toInteger() + highs++ + } + j++ + } + if (highs > 1) avgHum = totalHum / highs + humAdjust = 1.5 - ((0.5 * avgHum) / humToday) // basically, half of the delta % between today and today+3 days + } + if (isDebug) log.debug "humToday ${humToday}, avgHum ${avgHum}, humAdjust ${humAdjust}" + + //daily adjustment - average of heat and humidity factors + //hotter over next 3 days, more water + //cooler over next 3 days, less water + //drier over next 3 days, more water + //wetter over next 3 days, less water + // + //Note: these should never get to be very large, and work best if allowed to cumulate over time (watering amount will change marginally + // as days get warmer/cooler and drier/wetter) + def sa = ((heatAdjust + humAdjust) / 2) * 100.0 + state.seasonAdj = sa + sa = sa - 100.0 + String plus = '' + if (sa > 0) plus = '+' + weatherString = "${weatherString}\n Adjusting ${plus}${Math.round(sa)}% for weather forecast" // Apply seasonal adjustment on Monday each week or at install - if(getWeekDay() == 1 || state.weekseasonAdj == 0) { - - //get 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) - + if ((getWeekDay() == 1) || (state.weekseasonAdj == 0)) { //get daylight - 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 - 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) + if (wdata.sun_phase) { + int getsunRH = 0 + int getsunRM = 0 + int getsunSH = 0 + int getsunSM = 0 + + if (wdata.sun_phase.sunrise.hour.isNumber()) getsunRH = wdata.sun_phase.sunrise.hour.toInteger() + if (wdata.sun_phase.sunrise.minute.isNumber()) getsunRM = wdata.sun_phase.sunrise.minute.toInteger() + if (wdata.sun_phase.sunset.hour.isNumber()) getsunSH = wdata.sun_phase.sunset.hour.toInteger() + if (wdata.sun_phase.sunset.minute.isNumber()) getsunSM = wdata.sun_phase.sunset.minute.toInteger() - //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() + int daylight = ((getsunSH * 60) + getsunSM)-((getsunRH * 60) + getsunRM) + if (daylight >= 850) daylight = 850 + + //set seasonal adjustment + //seasonal q (fudge) factor + float qFact = 75.0 + + // (Daylight / 11.66 hours) * ( Average of ((Avg Temp / 70F) + ((1/2 of Average Humidity) / 65.46))) * calibration quotient + // Longer days = more water (day length constant = approx USA day length at fall equinox) + // Higher temps = more water + // Lower humidity = more water (humidity constant = USA National Average humidity in July) + float wa = ((daylight / 700.0) * (((avgHigh / 70.0) + (1.5-((avgHum * 0.5) / 65.46))) / 2.0) * qFact) + state.weekseasonAdj = wa + + //apply seasonal time adjustment + plus = '' + if (wa != 0) { + if (wa > 100.0) plus = '+' + String waStr = String.format('%.2f', (wa - 100.0)) + weatherString = "${weatherString}\n Seasonal adjustment of ${waStr}% for the week" + } + setSeason() + } + else { + log.debug 'isWeather(): Unable to get sunrise/set info for today.' + } } } + note('season', weatherString , 'f') + + // if only doing seasonal adjustments, we are done + if (!settings.isRain) return false - 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 > setrainDelay.toFloat()){ - note("raintoday", "is skipping watering, ${qpfTodayIn}in rain today.", "d") - return true - } - else if (qpfTomIn > setrainDelay.toFloat()){ - note("raintom", "is skipping watering, ${qpfTomIn}in rain expected tomorrow.", "d") - return true - } - else if (weeklyRain > setrainDelay.toFloat()){ - note("rainy", "is skipping watering, ${weeklyRain}in average rain over the past week.", "d") - return true - } - 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) + float setrainDelay = 0.2 + if (settings.rainDelay) setrainDelay = settings.rainDelay.toFloat() + + // if we have no sensors, rain causes us to skip watering for the day + if (!anySensors()) { + if (settings.switches.latestValue('rainsensor') == 'rainsensoron'){ + note('raintoday', "${app.label}: skipping, rain sensor is on", 'd') + return true + } + float popRain = qpfTodayIn * (popToday / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintoday', "${app.label}: skipping, ${rainStr}in of rain is probable today", 'd') + return true + } + popRain += qpfTomIn * (popTom / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintom', "${app.label}: skipping, ${rainStr}in of rain is probable today + tomorrow", 'd') + return true + } + if (weeklyRain > setrainDelay){ + String rainStr = String.format('%.2f', weeklyRain) + note('rainy', "${app.label}: skipping, ${rainStr}in weighted average rain over the past week", 'd') + return true + } + } + else { // we have at least one sensor in the schedule + // Ignore rain sensor & historical rain - only skip if more than setrainDelay is expected before midnight tomorrow + float popRain = (qpfTodayIn * (popToday / 100.0)) - TRain // ignore rain that has already fallen so far today - sensors should already reflect that + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintoday', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today", 'd') + return true + } + popRain += qpfTomIn * (popTom / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintom', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today + tomorrow", 'd') + return true + } + } + if (isDebug) log.debug "isWeather() ends" + return false } -def resume(){ - switches.on() - state.fail = 10 +// true if ANY of this schedule's zones are on and using sensors +private boolean anySensors() { + int zone=1 + while (zone <= 16) { + def zoneStr = settings."zone${zone}" + if (zoneStr && (zoneStr != 'Off') && settings."sensor${zone}") return true + zone++ + } + return false } -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] +def getDPWDays(int dpw){ + if (dpw && (dpw.isNumber()) && (dpw >= 1) && (dpw <= 7)) { + return state."DPWDays${dpw}" + } else + return [0,0,0,0,0,0,0] } // Create a map of what days each possible DPW value will run on @@ -1458,152 +2401,159 @@ def createDPWMap() { state.DPWDays5 = [] state.DPWDays6 = [] state.DPWDays7 = [] - def NDAYS = 7 + //def NDAYS = 7 // day Distance[NDAYS][NDAYS], easier to just define than calculate everytime def int[][] dayDistance = [[0,1,2,3,3,2,1],[1,0,1,2,3,3,2],[2,1,0,1,2,3,3],[3,2,1,0,1,2,3],[3,3,2,1,0,1,2],[2,3,3,2,1,0,1],[1,2,3,3,2,1,0]] def ndaysAvailable = daysAvailable() - 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}" - } + int i = 0 + + // def int[] daysAvailable = [0,1,2,3,4,5,6] + def int[] daysAvailable = [0,0,0,0,0,0,0] + + if(settings.days) { + if (settings.days.contains('Even') || settings.days.contains('Odd')) { + return + } + if (settings.days.contains('Monday')) { + daysAvailable[i] = 0 + i++ + } + if (settings.days.contains('Tuesday')) { + daysAvailable[i] = 1 + i++ + } + if (settings.days.contains('Wednesday')) { + daysAvailable[i] = 2 + i++ + } + if (settings.days.contains('Thursday')) { + daysAvailable[i] = 3 + i++ + } + if (settings.days.contains('Friday')) { + daysAvailable[i] = 4 + i++ + } + if (settings.days.contains('Saturday')) { + daysAvailable[i] = 5 + i++ + } + if (settings.days.contains('Sunday')) { + daysAvailable[i] = 6 + i++ + } + if(i != ndaysAvailable) { + log.debug 'ERROR: days and daysAvailable do not match in setup - overriding' + log.debug "${i} ${ndaysAvailable}" + ndaysAvailable = i // override incorrect setup execution + state.daysAvailable = i + } + } + else { // all days are available if settings.days == "" + daysAvailable = [0,1,2,3,4,5,6] } //log.debug "Ndays: ${ndaysAvailable} Available Days: ${daysAvailable}" def maxday = -1 def max = -1 - def days = new int[7] + def dDays = 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 - } + // Figure out next day using the dayDistance map, getting the farthest away day (max value) + if(a > 0 && ndaysAvailable >= 2 && a != ndaysAvailable-1) { + if(a == 1) { + for(def c=1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + if(d > max) { + max = d + maxday = daysAvailable[c] + } + } + //log.debug "max: ${max} maxday: ${maxday}" + dDays[0] = maxday + } - // Find successive maxes for the following days - if(a > 1) { - def lmax = max - def lmaxday = maxday - max = -1 - for(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 - } - } + // Find successive maxes for the following days + if(a > 1) { + def lmax = max + def lmaxday = maxday + max = -1 + for(int c = 1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + def t = d > max + if (a % 2 == 0) t = d >= max + if(d < lmax && d >= max) { + if(d == max) { + d = dayDistance[lmaxday][daysAvailable[c]] + if(d > dayDistance[lmaxday][maxday]) { + max = d + maxday = daysAvailable[c] + } + } + else { + max = d + maxday = daysAvailable[c] + } + } + } + lmax = 5 + while(max == -1) { + lmax = lmax -1 + for(int c = 1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + if(d < lmax && d >= max) { + if(d == max) { + d = dayDistance[lmaxday][daysAvailable[c]] + if(d > dayDistance[lmaxday][maxday]) { + max = d + maxday = daysAvailable[c] + } + } + else { + max = d + maxday = daysAvailable[c] + } + } + } + for (def d=0; d< a-2; d++) { + if(maxday == dDays[d]) max = -1 + } + } + //log.debug "max: ${max} maxday: ${maxday}" + dDays[a-1] = maxday + } + } - // Set the runDays map using the calculated maxdays - for(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 - } - } - } - + // Set the runDays map using the calculated maxdays + for(int b=0; b < 7; b++) { + // Runs every day available + if(a == ndaysAvailable-1) { + runDays[a][b] = 0 + for (def c=0; c < ndaysAvailable; c++) { + if(b == daysAvailable[c]) runDays[a][b] = 1 + } + } + else { + // runs weekly, use first available day + if(a == 0) { + if(b == daysAvailable[0]) + runDays[a][b] = 1 + else + runDays[a][b] = 0 + } + else { + // Otherwise, start with first available day + if(b == daysAvailable[0]) + runDays[a][b] = 1 + else { + runDays[a][b] = 0 + for(def c=0; c < a; c++) + if(b == dDays[c]) + runDays[a][b] = 1 + } + } + } + } } //log.debug "DPW: ${runDays}" @@ -1615,3 +2565,70 @@ def createDPWMap() { state.DPWDays6 = runDays[5] state.DPWDays7 = runDays[6] } + +//transition page to populate app state - this is a fix for WP param +def zoneSetPage1(){ + state.app = 1 + zoneSetPage() + } +def zoneSetPage2(){ + state.app = 2 + zoneSetPage() + } +def zoneSetPage3(){ + state.app = 3 + zoneSetPage() + } +def zoneSetPage4(){ + state.app = 4 + zoneSetPage() + } +def zoneSetPage5(){ + state.app = 5 + zoneSetPage() + } +def zoneSetPage6(){ + state.app = 6 + zoneSetPage() + } +def zoneSetPage7(){ + state.app = 7 + zoneSetPage() + } +def zoneSetPage8(){ + state.app = 8 + zoneSetPage() + } +def zoneSetPage9(i){ + state.app = 9 + zoneSetPage() + } +def zoneSetPage10(){ + state.app = 10 + zoneSetPage() + } +def zoneSetPage11(){ + state.app = 11 + zoneSetPage() + } +def zoneSetPage12(){ + state.app = 12 + zoneSetPage() + } +def zoneSetPage13(){ + state.app = 13 + zoneSetPage() + } +def zoneSetPage14(){ + state.app = 14 + zoneSetPage() + } +def zoneSetPage15(){ + state.app = 15 + zoneSetPage() + } +def zoneSetPage16(){ + state.app = 16 + zoneSetPage() + } +