From 55765f67b7f76f7d63d5509b7af870a92987a655 Mon Sep 17 00:00:00 2001 From: Bruce Ravenel Date: Wed, 25 Nov 2015 08:54:21 -0600 Subject: [PATCH] MSA-708: This app is a parent-child app with the parent being Rule Machine and the child Rule. Extensive documentation is located on the Community Forum, https://community.smartthings.com/t/release-rule-machine/28730 --- .../rule-machine.src/rule-machine.groovy | 52 + smartapps/bravenel/rule.src/rule.groovy | 1005 +++++++++++++++++ 2 files changed, 1057 insertions(+) create mode 100644 smartapps/bravenel/rule-machine.src/rule-machine.groovy create mode 100644 smartapps/bravenel/rule.src/rule.groovy diff --git a/smartapps/bravenel/rule-machine.src/rule-machine.groovy b/smartapps/bravenel/rule-machine.src/rule-machine.groovy new file mode 100644 index 0000000..543a202 --- /dev/null +++ b/smartapps/bravenel/rule-machine.src/rule-machine.groovy @@ -0,0 +1,52 @@ +/** + * Rule + * + * Copyright 2015 Bruce Ravenel + * + * Version 1.1.0 25 Nov 2015 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Rule Machine", + singleInstance: true, + namespace: "bravenel", + author: "Bruce Ravenel", + description: "Rule Machine", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic@3x.png" +) + +preferences { + page(name: "mainPage", title: "Rules", install: true, uninstall: true,submitOnChange: true) { + section { + app(name: "childRules", appName: "Rule", namespace: "bravenel", title: "Create New Rule...", multiple: true) + } + } +} + +def installed() { + initialize() +} + +def updated() { + unsubscribe() + initialize() +} + +def initialize() { + childApps.each {child -> + log.info "Installed Rules: ${child.label}" + } +} \ No newline at end of file diff --git a/smartapps/bravenel/rule.src/rule.groovy b/smartapps/bravenel/rule.src/rule.groovy new file mode 100644 index 0000000..85f16a5 --- /dev/null +++ b/smartapps/bravenel/rule.src/rule.groovy @@ -0,0 +1,1005 @@ +/** + * Rule + * + * Copyright 2015 Bruce Ravenel + * + * Version 1.2.2 24 Nov 2015 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Rule", + namespace: "bravenel", + author: "Bruce Ravenel", + description: "Rule", + category: "Convenience", + parent: "bravenel:Rule Machine", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png" +) + +preferences { + page(name: "selectRule") + page(name: "selectConditions") + page(name: "defineRule") + page(name: "certainTime") + page(name: "atCertainTime") + page(name: "selectActionsTrue") + page(name: "selectActionsFalse") + page(name: "selectMsgTrue") + page(name: "selectMsgFalse") +} + +def selectRule() { + dynamicPage(name: "selectRule", title: "Select Conditions, Rule and Results", uninstall: true, install: true) { + section() { + label title: "Name the Rule", required: true + def condLabel = conditionLabel() + if (condLabel) condLabel = condLabel[0..-2] + href "selectConditions", title: "Define Conditions", description: condLabel ? (condLabel) : "Tap to set", required: true, state: condLabel ? "complete" : null, submitOnChange: true + href "defineRule", title: "Define the Rule", description: state.str ? (state.str) : "Tap to set", state: state.str ? "complete" : null + href "selectActionsTrue", title: "Select the Actions for True", description: state.actsTrue ? state.actsTrue : "Tap to set", state: state.actsTrue ? "complete" : null + href "selectActionsFalse", title: "Select the Actions for False", description: state.actsFalse ? state.actsFalse : "Tap to set", state: state.actsFalse ? "complete" : null + } + section(title: "More options", hidden: hideOptionsSection(), hideable: true) { + input "modesZ", "mode", title: "Evaluate only when mode is", multiple: true, required: false + paragraph "Advanced Rule Input allows for parenthesized sub-rules." + input "advanced", "bool", title: "Advanced Rule Input", required: false + input "disabled", "capability.switch", title: "Switch to disable rule", required: false, multiple: false + } + } +} + +// Condition input code follows + +def selectConditions() { + def ct = settings.findAll{it.key.startsWith("rCapab")} + state.howMany = ct.size() + 1 + def howMany = state.howMany + dynamicPage(name: "selectConditions", title: "Select Conditions", uninstall: false) { + if(howMany) { + for (int i = 1; i <= howMany; i++) { + def thisCapab = "rCapab$i" + section("Condition #$i") { + getCapab(thisCapab) + def myCapab = settings.find {it.key == thisCapab} + if(myCapab) { + def xCapab = myCapab.value // removed , "Certain Time" + if(!(xCapab in ["Time of day", "Days of week", "Mode"])) { + def thisDev = "rDev$i" + getDevs(xCapab, thisDev) + def myDev = settings.find {it.key == thisDev} + if(myDev) if(myDev.value.size() > 1) getAnyAll(thisDev) + if(xCapab in ["Temperature", "Humidity", "Illuminance", "Dimmer level", "Energy meter", "Power meter", "Battery"]) getRelational(thisDev) + } + getState(xCapab, i) + } + } + } + } + } +} + +def getDevs(myCapab, dev) { + def thisName = "" + def thisCapab = "" + switch(myCapab) { + case "Switch": + thisName = "Switches" + thisCapab = "switch" + break + case "Motion": + thisName = "Motion sensors" + thisCapab = "motionSensor" + break + case "Acceleration": + thisName = "Acceleration sensors" + thisCapab = "accelerationSensor" + break + case "Contact": + thisName = "Contact sensors" + thisCapab = "contactSensor" + break + case "Presence": + thisName = "Presence sensors" + thisCapab = "presenceSensor" + break + case "Lock": + thisName = "Locks" + thisCapab = "lock" + break + case "Dimmer level": + thisName = "Dimmers" + thisCapab = "switchLevel" + break + case "Temperature": + thisName = "Temperature sensors" + thisCapab = "temperatureMeasurement" + break + case "Humidity": + thisName = "Humidity sensors" + thisCapab = "relativeHumidityMeasurement" + break + case "Illuminance": + thisName = "Illuminance sensors" + thisCapab = "illuminanceMeasurement" + break + case "Energy meter": + thisName = "Energy meters" + thisCapab = "energyMeter" + break + case "Power meter": + thisName = "Power meters" + thisCapab = "powerMeter" + break + case "Water sensor": + thisName = "Water sensors" + thisCapab = "waterSensor" + break + case "Battery": + thisName = "Batteries" + thisCapab = "battery" + } + def result = input dev, "capability.$thisCapab", title: thisName, required: true, multiple: true, submitOnChange: true +} + +def getAnyAll(myDev) { + def result = input "All$myDev", "bool", title: "All of these?", defaultValue: false +} + +def getRelational(myDev) { + def result = input "Rel$myDev", "enum", title: "Choose comparison", required: true, options: ["=", "!=", "<", ">", "<=", ">="] +} + +def getCapab(myCapab) { // removed , "Valve" to avoid confusion, and , "Certain Time" + def myOptions = ["Switch", "Motion", "Acceleration", "Contact", "Presence", "Lock", "Temperature", "Humidity", "Illuminance", "Time of day", + "Days of week", "Mode", "Dimmer level", "Energy meter", "Power meter", "Water sensor", "Battery"] + def result = input myCapab, "enum", title: "Select capability", required: false, options: myOptions.sort(), submitOnChange: true +} + +def getState(myCapab, n) { + def result = null + def days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (myCapab == "Switch") result = input "state$n", "enum", title: "Switch state", options: ["on", "off"] + else if(myCapab == "Motion") result = input "state$n", "enum", title: "Motion state", options: ["active", "inactive"], defaultValue: "active" + else if(myCapab == "Acceleration") result = input "state$n", "enum", title: "Acceleration state", options: ["active", "inactive"] + else if(myCapab == "Contact") result = input "state$n", "enum", title: "Contact state", options: ["open", "closed"] + else if(myCapab == "Presence") result = input "state$n", "enum", title: "Presence state", options: ["present", "not present"], defaultValue: "present" + else if(myCapab == "Lock") result = input "state$n", "enum", title: "Lock state", options: ["locked", "unlocked"] + else if(myCapab == "Water sensor") result = input "state$n", "enum", title: "Water state", options: ["dry", "wet"] + else if(myCapab == "Dimmer level") result = input "state$n", "number", title: "Dimmer level", range: "0..100" + else if(myCapab == "Temperature") result = input "state$n", "decimal", title: "Temperature", range: "*..*" + else if(myCapab == "Humidity") result = input "state$n", "number", title: "Humidity", range: "0..100" + else if(myCapab == "Illuminance") result = input "state$n", "number", title: "Illuminance" + else if(myCapab == "Energy meter") result = input "state$n", "number", title: "Energy level" + else if(myCapab == "Power meter") result = input "state$n", "number", title: "Power level", range: "*..*" + else if(myCapab == "Battery") result = input "state$n", "number", title: "Battery level" + else if(myCapab == "Days of week") result = input "days", "enum", title: "On certain days of the week", multiple: true, required: false, options: days + else if(myCapab == "Mode") { + def myModes = [] + location.modes.each {myModes << "$it"} + result = input "modes", "enum", title: "Select mode(s)", multiple: true, required: false, options: myModes.sort() + } else if(myCapab == "Time of day") { + def timeLabel = timeIntervalLabel() + href "certainTime", title: "During a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : null + } +} + +def certainTime() { + dynamicPage(name: "certainTime", title: "Only during a certain time", uninstall: false) { + section() { + input "startingX", "enum", title: "Starting at", options: ["A specific time", "Sunrise", "Sunset"], defaultValue: "A specific time", submitOnChange: true + if(startingX in [null, "A specific time"]) input "starting", "time", title: "Start time", required: false + else { + if(startingX == "Sunrise") input "startSunriseOffset", "number", range: "*..*", title: "Offset in minutes (+/-)", required: false + else if(startingX == "Sunset") input "startSunsetOffset", "number", range: "*..*", title: "Offset in minutes (+/-)", required: false + } + } + section() { + input "endingX", "enum", title: "Ending at", options: ["A specific time", "Sunrise", "Sunset"], defaultValue: "A specific time", submitOnChange: true + if(endingX in [null, "A specific time"]) input "ending", "time", title: "End time", required: false + else { + if(endingX == "Sunrise") input "endSunriseOffset", "number", range: "*..*", title: "Offset in minutes (+/-)", required: false + else if(endingX == "Sunset") input "endSunsetOffset", "number", range: "*..*", title: "Offset in minutes (+/-)", required: false + } + } + } +} + +def conditionLabel() { + def howMany = state.howMany + def result = "" + if(howMany) { + for (int i = 1; i <= howMany; i++) { + result = result + conditionLabelN(i) + if((i + 1) <= howMany) result = result + "\n" + } + if(howMany == 2) { + state.str = result[0..-2] + state.eval = [1] + } + } + return result +} + +def conditionLabelN(i) { + def result = "" + def thisCapab = settings.find {it.key == "rCapab$i"} + if(!thisCapab) return result + if(thisCapab.value == "Time of day") result = "Time between " + timeIntervalLabel() + else if(thisCapab.value == "Days of week") result = "Day i" + (days.size() > 1 ? "n " + days : "s " + days[0]) + else if(thisCapab.value == "Mode") result = "Mode i" + (modes.size() > 1 ? "n " + modes : "s " + modes[0]) + else { + def thisDev = settings.find {it.key == "rDev$i"} + if(!thisDev) return result + def thisAll = settings.find {it.key == "AllrDev$i"} + def myAny = thisAll ? "any " : "" + if (thisCapab.value == "Temperature") result = "Temperature of " + else if(thisCapab.value == "Humidity") result = "Humidity of " + else if(thisCapab.value == "Illuminance") result = "Illuminance of " + else if(thisCapab.value == "Dimmer level") result = "Dimmer level of " + else if(thisCapab.value == "Energy meter") result = "Energy level of " + else if(thisCapab.value == "Power meter") result = "Power level of " + else if(thisCapab.value == "Battery") result = "Battery level of " + result = result + (myAny ? thisDev.value : thisDev.value[0]) + " " + ((thisAll ? thisAll.value : false) ? "all " : myAny) + def thisRel = settings.find {it.key == "RelrDev$i"} + if(thisCapab.value in ["Temperature", "Humidity", "Illuminance", "Dimmer level", "Energy meter", "Power meter", "Battery"]) result = result + " " + thisRel.value + " " + def thisState = settings.find {it.key == "state$i"} + result = result + thisState.value + } + return result +} + +// Rule definition code follows + +def defineRule() { + dynamicPage(name: "defineRule", title: "Define the Rule", uninstall: false) { + state.n = 0 + state.str = "" + state.eval = [] + section() {inputLeftAndRight(false)} + } +} + +def inputLeft(sub) { + def howMany = state.howMany - 1 + def conds = [] + for (int i = 1; i <= howMany; i++) conds << conditionLabelN(i) + if(advanced) input "subCondL$state.n", "bool", title: "Enter subrule for left?", submitOnChange: true + if(settings["subCondL$state.n"]) { + state.str = state.str + "(" + state.eval << "(" + paragraph(state.str) + inputLeftAndRight(true) + input "moreConds$state.n", "bool", title: "More conditions on left?", submitOnChange: true + if(settings["moreConds$state.n"]) inputRight(sub) + } else { + input "condL$state.n", "enum", title: "Which condition?", options: conds, submitOnChange: true + if(settings["condL$state.n"]) { + state.str = state.str + settings["condL$state.n"] + def myCond = 0 + for (int i = 1; i <= howMany; i++) if(conditionLabelN(i) == settings["condL$state.n"]) myCond = i + state.eval << myCond + paragraph(state.str) + } + } +} + +def inputRight(sub) { + def howMany = state.howMany - 1 + state.n = state.n + 1 + input "operator$state.n", "enum", title: "AND or OR", options: ["AND", "OR"], submitOnChange: true, required: false + if(settings["operator$state.n"]) { + state.str = state.str + " " + settings["operator$state.n"] + " " + state.eval << settings["operator$state.n"] + paragraph(state.str) + def conds = [] + for (int i = 1; i <= howMany; i++) conds << conditionLabelN(i) + if(advanced) input "subCondR$state.n", "bool", title: "Enter subrule for right?", submitOnChange: true + if(settings["subCondR$state.n"]) { + state.str = state.str + "(" + state.eval << "(" + paragraph(state.str) + inputLeftAndRight(true) + inputRight(sub) + } else { + input "condR$state.n", "enum", title: "Which condition?", options: conds, submitOnChange: true + if(settings["condR$state.n"]) { + state.str = state.str + settings["condR$state.n"] + def myCond = 0 + for (int i = 1; i <= howMany; i++) if(conditionLabelN(i) == settings["condR$state.n"]) myCond = i + state.eval << myCond + paragraph(state.str) + } + if(sub) { + input "endOfSub$state.n", "bool", title: "End of sub-rule?", submitOnChange: true + if(settings["endOfSub$state.n"]) { + state.str = state.str + ")" + state.eval << ")" + paragraph(state.str) + return + } + } + inputRight(sub) + } + } +} + +def inputLeftAndRight(sub) { + state.n = state.n + 1 + inputLeft(sub) + inputRight(sub) +} + +def stripBrackets(str) { + def i = str.indexOf('[') + def j = str.indexOf(']') + def result = str.substring(0, i) + str.substring(i + 1, j) + str.substring(j + 1) + return result +} + +// Action selection code follows + +def setActTrue(dev, str) { + if(dev) state.actsTrue = state.actsTrue + stripBrackets("$str") + "\n" +} + +def addToActTrue(str) { + state.actsTrue = state.actsTrue + str + "\n" +} + +def buildActTrue(str, brackets) { + state.actsTrue = state.actsTrue + (brackets ? stripBrackets("$str") : str) +} + +def setActFalse(dev, str) { + if(dev) state.actsFalse = state.actsFalse + stripBrackets("$str") + "\n" +} + +def addToActFalse(str) { + state.actsFalse = state.actsFalse + str + "\n" +} + +def buildActFalse(str, brackets) { + state.actsFalse = state.actsFalse + (brackets ? stripBrackets("$str") : str) +} + +def selectActionsTrue() { + dynamicPage(name: "selectActionsTrue", title: "Select Actions for True", uninstall: false) { + state.actsTrue = "" + section("") { + input "onSwitchTrue", "capability.switch", title: "Turn on these switches", multiple: true, required: false, submitOnChange: true + setActTrue(onSwitchTrue, "On: $onSwitchTrue") + input "offSwitchTrue", "capability.switch", title: "Turn off these switches", multiple: true, required: false, submitOnChange: true + setActTrue(offSwitchTrue, "Off: $offSwitchTrue") + input "delayedOffTrue", "capability.switch", title: "Turn on/off these switches after a delay (default is OFF)", multiple: true, required: false, submitOnChange: true + if(delayedOffTrue) { + input "delayOnOffTrue", "bool", title: "Turn ON after the delay?", multiple: false, required: false, defaultValue: false, submitOnChange: true + input "delayMinutesTrue", "number", title: "Minutes of delay", required: true, range: "1..*", submitOnChange: true + if(delayMinutesTrue) { + def delayStrTrue = "Delayed " + (delayOnOffTrue ? "On:" : "Off:") + " $delayedOffTrue: $delayMinutesTrue minute" + if(delayMinutesTrue > 1) delayStrTrue = delayStrTrue + "s" + setActTrue(delayedOffTrue, delayStrTrue) + } + } + input "pendedOffTrue", "capability.switch", title: "Turn on/off these switches after a delay, pending cancellation (default is OFF)", multiple: true, required: false, submitOnChange: true + if(pendedOffTrue) { + input "pendOnOffTrue", "bool", title: "Turn ON after the delay?", multiple: false, required: false, defaultValue: false, submitOnChange: true + input "pendMinutesTrue", "number", title: "Minutes of delay", required: true, range: "1..*", submitOnChange: true + if(pendMinutesTrue) { + def pendStrTrue = "Pending "+ (pendOnOffTrue ? "On:" : "Off:") + " $pendedOffTrue: $pendMinutesTrue minute" + if(pendMinutesTrue > 1) pendStrTrue = pendStrTrue + "s" + setActTrue(pendedOffTrue, pendStrTrue) + } + } + input "dimATrue", "capability.switchLevel", title: "Set these dimmers", multiple: true, submitOnChange: true, required: false + if(dimATrue) input "dimLATrue", "number", title: "To this level", range: "0..100", required: true, submitOnChange: true + if(dimLATrue) setActTrue(dimATrue, "Dim: $dimATrue: $dimLATrue") + input "dimBTrue", "capability.switchLevel", title: "Set these other dimmers", multiple: true, submitOnChange: true, required: false + if(dimBTrue) input "dimLBTrue", "number", title: "To this level", range: "0..100", required: true, submitOnChange: true + if(dimLBTrue) setActTrue(dimBTrue, "Dim: $dimBTrue: $dimLBTrue") + input "bulbsTrue", "capability.colorControl", title: "Set color for these bulbs", multiple: true, required: false, submitOnChange: true + if(bulbsTrue) { + input "colorTrue", "enum", title: "Bulb color?", required: true, multiple: false, submitOnChange: true, + options: ["Soft White", "White", "Daylight", "Warm White", "Red", "Green", "Blue", "Yellow", "Orange", "Purple", "Pink"] + input "colorLevelTrue", "number", title: "Bulb level?", required: false, submitOnChange: true, range: "0..100" + buildActTrue("Color: $bulbsTrue ", true) + if(colorTrue) buildActTrue("$colorTrue ", false) + if(colorLevelTrue) addToActTrue("Level: $colorLevelTrue") + } + input "lockTrue", "capability.lock", title: "Lock these locks", multiple: true, required: false, submitOnChange: true + setActTrue(lockTrue, "Lock: $lockTrue") + input "unlockTrue", "capability.lock", title: "Unlock these locks", multiple: true, required: false, submitOnChange: true + setActTrue(unlockTrue, "Unlock: $unlockTrue") + input "openValveTrue", "capability.valve", title: "Open these valves", multiple: true, required: false, submitOnChange: true + setActTrue(openValveTrue, "Open: $openValveTrue") + input "closeValveTrue", "capability.valve", title: "Close these valves", multiple: true, required: false, submitOnChange: true + setActTrue(closeValveTrue, "Close: $closeValveTrue") + input "thermoTrue", "capability.thermostat", title: "Set these thermostats", multiple: true, required: false, submitOnChange: true + if(thermoTrue) { + input "thermoModeTrue", "enum", title: "Select thermostate mode", multiple: false, required: false, options: ["auto", "heat", "cool", "off"], submitOnChange: true + input "thermoSetHeatTrue", "decimal", title: "Set heating point", multiple: false, required: false, submitOnChange: true + input "thermoSetCoolTrue", "decimal", title: "Set cooling point", multiple: false, required: false, submitOnChange: true + input "thermoFanTrue", "enum", title: "Fan setting", multiple: false, required: false, submitOnChange: true, options: ["fanOn", "fanAuto"] + buildActTrue("$thermoTrue: ", true) + if(thermoModeTrue) buildActTrue("Mode: " + thermoModeTrue + " ", false) + if(thermoSetHeatTrue) buildActTrue("Heat to $thermoSetHeatTrue ", false) + if(thermoSetCoolTrue) buildActTrue("Cool to $thermoSetCoolTrue ", false) + if(thermoFanTrue) buildActTrue("Fan setting $thermoFanTrue", false) + addToActTrue("") + } + input "modeTrue", "mode", title: "Set the mode", multiple: false, required: false, submitOnChange: true + if(modeTrue) addToActTrue("Mode: $modeTrue") + def phrases = location.helloHome?.getPhrases()*.label + input "myPhraseTrue", "enum", title: "Routine to run", required: false, options: phrases.sort(), submitOnChange: true + if(myPhraseTrue) addToActTrue("Routine: $myPhraseTrue") + href "selectMsgTrue", title: "Send message", description: state.msgTrue ? state.msgTrue : "Tap to set", state: state.msgTrue ? "complete" : null + if(state.msgTrue) addToActTrue(state.msgTrue) + input "delayTrue", "number", title: "Delay the effect of this rule by this many minutes", required: false, submitOnChange: true + if(delayTrue) { + def delayStr = "Delay Rule: $delayTrue minute" + if(delayTrue > 1) delayStr = delayStr + "s" + addToActTrue(delayStr) + } + } + if(state.actsTrue) state.actsTrue = state.actsTrue[0..-2] + } +} + +def selectActionsFalse() { + dynamicPage(name: "selectActionsFalse", title: "Select Actions for False", uninstall: false) { + state.actsFalse = "" + section("") { + input "onSwitchFalse", "capability.switch", title: "Turn on these switches", multiple: true, required: false, submitOnChange: true + setActFalse(onSwitchFalse, "On: $onSwitchFalse") + input "offSwitchFalse", "capability.switch", title: "Turn off these switches", multiple: true, required: false, submitOnChange: true + setActFalse(offSwitchFalse, "Off: $offSwitchFalse") + input "delayedOffFalse", "capability.switch", title: "Turn on/off these switches after a delay (default is OFF)", multiple: true, required: false, submitOnChange: true + if(delayedOffFalse) { + input "delayOnOffFalse", "bool", title: "Turn ON after the delay?", multiple: false, required: false, defaultValue: false, submitOnChange: true + input "delayMinutesFalse", "number", title: "Minutes of delay", required: true, range: "1..*", submitOnChange: true + if(delayMinutesFalse) { + def delayStrFalse = "Delayed " + (delayOnOffFalse ? "On:" : "Off:") + " $delayedOffFalse: $delayMinutesFalse minute" + if(delayMinutesFalse > 1) delayStrFalse = delayStrFalse + "s" + setActFalse(delayedOffFalse, delayStrFalse) + } + } + input "pendedOffFalse", "capability.switch", title: "Turn on/off these switches after a delay, pending cancellation (default is OFF)", multiple: true, required: false, submitOnChange: true + if(pendedOffFalse) { + input "pendOnOffFalse", "bool", title: "Turn ON after the delay?", multiple: false, required: false, defaultValue: false, submitOnChange: true + input "pendMinutesFalse", "number", title: "Minutes of delay", required: true, range: "1..*", submitOnChange: true + if(pendMinutesFalse) { + def pendStrFalse = "Pending "+ (pendOnOffFalse ? "On:" : "Off:") + " $pendedOffFalse: $pendMinutesFalse minute" + if(pendMinutesFalse > 1) pendStrFalse = pendStrFalse + "s" + setActFalse(pendedOffFalse, pendStrFalse) + } + } + input "dimAFalse", "capability.switchLevel", title: "Set these dimmers", multiple: true, submitOnChange: true, required: false + if(dimAFalse) input "dimLAFalse", "number", title: "To this level", range: "0..100", required: true, submitOnChange: true + if(dimLAFalse) setActFalse(dimAFalse, "Dim: $dimAFalse: $dimLAFalse") + input "dimBFalse", "capability.switchLevel", title: "Set these other dimmers", multiple: true, submitOnChange: true, required: false + if(dimBFalse) input "dimLBFalse", "number", title: "To this level", range: "0..100", required: true, submitOnChange: true + if(dimLBFalse) setActFalse(dimBFalse, "Dim: $dimBFalse: $dimLBFalse") + input "bulbsFalse", "capability.colorControl", title: "Set color for these bulbs", multiple: true, required: false, submitOnChange: true + if(bulbsFalse) { + input "colorFalse", "enum", title: "Bulb color?", required: true, multiple: false, submitOnChange: true, + options: ["Soft White", "White", "Daylight", "Warm White", "Red", "Green", "Blue", "Yellow", "Orange", "Purple", "Pink"] + input "colorLevelFalse", "number", title: "Bulb level?", required: false, submitOnChange: true, range: "0..100" + buildActFalse("Color: $bulbsFalse ", true) + if(colorFalse) buildActFalse("$colorFalse ", false) + if(colorLevelFalse) addToActFalse("Level: $colorLevelFalse") + } + input "lockFalse", "capability.lock", title: "Lock these locks", multiple: true, required: false, submitOnChange: true + setActFalse(lockFalse, "Lock: $lockFalse") + input "unlockFalse", "capability.lock", title: "Unlock these locks", multiple: true, required: false, submitOnChange: true + setActFalse(unlockFalse, "Unlock: $unlockFalse") + input "openValveFalse", "capability.valve", title: "Open these valves", multiple: true, required: false, submitOnChange: true + setActTrue(openValveFalse, "Open: $openValveFalse") + input "closeValveFalse", "capability.valve", title: "Close these valves", multiple: true, required: false, submitOnChange: true + setActTrue(closeValveFalse, "Close: $closeValveFalse") + input "thermoFalse", "capability.thermostat", title: "Set these thermostats", multiple: true, required: false, submitOnChange: true + if(thermoFalse) { + input "thermoModeFalse", "enum", title: "Select thermostate mode", multiple: false, required: false, options: ["auto", "heat", "cool", "off"], submitOnChange: true + input "thermoSetHeatFalse", "decimal", title: "Set heating point", multiple: false, required: false, submitOnChange: true + input "thermoSetCoolFalse", "decimal", title: "Set cooling point", multiple: false, required: false, submitOnChange: true + input "thermoFanFalse", "enum", title: "Fan setting", multiple: false, required: false, submitOnChange: true, options: ["fanOn", "fanAuto"] + buildActFalse("$thermoFalse: ", true) + if(thermoModeFalse) buildActFalse("Mode: " + thermoModeFalse + " ", false) + if(thermoSetHeatFalse) buildActFalse("Heat to $thermoSetHeatFalse ", false) + if(thermoSetCoolFalse) buildActFalse("Cool to $thermoSetCoolFalse ", false) + if(thermoFanFalse) buildActFalse("Fan setting $thermoFanFalse", false) + addToActFalse("") + } + input "modeFalse", "mode", title: "Set the mode", multiple: false, required: false, submitOnChange: true + if(modeFalse) addToActFalse("Mode: $modeFalse") + def phrases = location.helloHome?.getPhrases()*.label + input "myPhraseFalse", "enum", title: "Routine to run", required: false, options: phrases.sort(), submitOnChange: true + if(myPhraseFalse) addToActFalse("Routine: $myPhraseFalse") + href "selectMsgFalse", title: "Send message", description: state.msgFalse ? state.msgFalse : "Tap to set", state: state.msgFalse ? "complete" : null + if(state.msgFalse) addToActFalse(state.msgFalse) + input "delayFalse", "number", title: "Delay the effect of this rule by this many minutes", required: false, submitOnChange: true + if(delayFalse) { + def delayStr = "Delay Rule: $delayFalse minute" + if(delayFalse > 1) delayStr = delayStr + "s" + addToActFalse(delayStr) + } + } + if(state.actsFalse) state.actsFalse = state.actsFalse[0..-2] + } +} + +def selectMsgTrue() { + dynamicPage(name: "selectMsgTrue", title: "Select Message and Destination", uninstall: false) { + section("") { + input "pushTrue", "bool", title: "Send Push Notification?", required: false, submitOnChange: true + input "msgTrue", "text", title: "Custom message to send", required: false, submitOnChange: true + input "phoneTrue", "phone", title: "Phone number for SMS", required: false, submitOnChange: true + } + state.msgTrue = (pushTrue ? "Push" : "") + (msgTrue ? " '$msgTrue'" : "") + (phoneTrue ? " to $phoneTrue" : "") + } +} + +def selectMsgFalse() { + dynamicPage(name: "selectMsgFalse", title: "Select Message and Destination", uninstall: false) { + section("") { + input "pushFalse", "bool", title: "Send Push Notification?", required: false, submitOnChange: true + input "msgFalse", "text", title: "Custom message to send", required: false, submitOnChange: true + input "phoneFalse", "phone", title: "Phone number for SMS", required: false, submitOnChange: true + } + state.msgFalse = (pushFalse ? "Push" : "") + (msgFalse ? " '$msgFalse'" : "") + (phoneFalse ? " to $phoneFalse" : "") + } +} + +// initialization code follows + +def scheduleTimeOfDay() { + def start = null + def stop = null + def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: startSunriseOffset, sunsetOffset: startSunsetOffset) + if(startingX == "Sunrise") start = s.sunrise.time + else if(startingX == "Sunset") start = s.sunset.time + else if(starting) start = timeToday(starting,location.timeZone).time + s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: endSunriseOffset, sunsetOffset: endSunsetOffset) + if(endingX == "Sunrise") stop = s.sunrise.time + else if(endingX == "Sunset") stop = s.sunset.time + else if(ending) stop = timeToday(ending,location.timeZone).time + schedule(start, "startHandler") + schedule(stop, "stopHandler") + if(startingX in ["Sunrise", "Sunset"] || endingX in ["Sunrise", "Sunset"]) + schedule("2015-01-09T00:00:29.000-0700", "scheduleTimeOfDay") // in case sunset/sunrise; change daily +} + +def installed() { + initialize() +} + +def updated() { + unschedule() + unsubscribe() + initialize() +} + +def initialize() { + def howMany = state.howMany - 1 + for (int i = 1; i <= howMany; i++) { + def capab = (settings.find {it.key == "rCapab$i"}).value + if (capab == "Mode") subscribe(location, "mode", allHandler) + else if(capab == "Time of day") scheduleTimeOfDay() + else if(capab == "Days of week") schedule("2015-01-09T00:00:10.000-0700", "runRule") + else if(capab == "Dimmer level") subscribe((settings.find{it.key == "rDev$i"}).value, "level", allHandler) + else if(capab == "Energy meter") subscribe((settings.find{it.key == "rDev$i"}).value, "energy", allHandler) + else if(capab == "Power meter") subscribe((settings.find{it.key == "rDev$i"}).value, "power", allHandler) + else if(capab == "Water sensor") subscribe((settings.find{it.key == "rDev$i"}).value, "water", allHandler) + else subscribe((settings.find{it.key == "rDev$i"}).value, capab.toLowerCase(), allHandler) + } + state.success = null + subscribe(disabled, "switch", disabledHandler) + if(disabled) state.disabled = disabled.currentSwitch == "on" + else state.disabled = false + runRule(false) +} + +// Main rule evaluation code follows + +def compare(a, rel, b) { + def result = true + if (rel == "=") result = a == b + else if(rel == "!=") result = a != b + else if(rel == ">") result = a > b + else if(rel == "<") result = a < b + else if(rel == ">=") result = a >= b + else if(rel == "<=") result = a <= b + return result +} + +def checkCondAny(dev, state, cap, rel) { + def result = false + if (cap == "Temperature") dev.currentTemperature.each {result = result || compare(it, rel, state)} + else if(cap == "Humidity") dev.currentHumidity.each {result = result || compare(it, rel, state)} + else if(cap == "Illuminance") dev.currentIlluminance.each {result = result || compare(it, rel, state)} + else if(cap == "Dimmer level") dev.currentLevel.each {result = result || compare(it, rel, state)} + else if(cap == "Energy meter") dev.currentEnergy.each {result = result || compare(it, rel, state)} + else if(cap == "Power meter") dev.currentPower.each {result = result || compare(it, rel, state)} + else if(cap == "Battery") dev.currentBattery.each {result = result || compare(it, rel, state)} + else if(cap == "Water sensor") result = state in dev.currentWater + else if(cap == "Switch") result = state in dev.currentSwitch + else if(cap == "Motion") result = state in dev.currentMotion + else if(cap == "Acceleration") result = state in dev.currentAcceleration + else if(cap == "Contact") result = state in dev.currentContact + else if(cap == "Presence") result = state in dev.currentPresence + else if(cap == "Lock") result = state in dev.currentLock +// log.debug "CheckAny $cap $result" + return result +} + +def checkCondAll(dev, state, cap, rel) { + def flip = ["on": "off", + "off": "on", + "active": "inactive", + "inactive": "active", + "open": "closed", + "closed": "open", + "wet": "dry", + "dry": "wet", + "present": "not present", + "not present": "present", + "locked": "unlocked", + "unlocked": "locked"] + def result = true + if (cap == "Temperature") dev.currentTemperature.each {result = result && compare(it, rel, state)} + else if(cap == "Humidity") dev.currentHumidity.each {result = result && compare(it, rel, state)} + else if(cap == "Illuminance") dev.currentIlluminance.each {result = result && compare(it, rel, state)} + else if(cap == "Dimmer level") dev.currentLevel.each {result = result && compare(it, rel, state)} + else if(cap == "Energy meter") dev.currentEnergy.each {result = result && compare(it, rel, state)} + else if(cap == "Power meter") dev.currentPower.each {result = result && compare(it, rel, state)} + else if(cap == "Battery") dev.currentBattery.each {result = result && compare(it, rel, state)} + else if(cap == "Water sensor") result = !(flip[state] in dev.currentSwitch) + else if(cap == "Switch") result = !(flip[state] in dev.currentSwitch) + else if(cap == "Motion") result = !(flip[state] in dev.currentMotion) + else if(cap == "Acceleration") result = !(flip[state] in dev.currentAcceleration) + else if(cap == "Contact") result = !(flip[state] in dev.currentContact) + else if(cap == "Presence") result = !(flip[state] in dev.currentPresence) + else if(cap == "Lock") result = !(flip[state] in dev.currentLock) +// log.debug "CheckAll $cap $result" + return result +} + +def getOperand(i) { + def result = true + def capab = (settings.find {it.key == "rCapab$i"}).value + if (capab == "Mode") result = modeOk + else if(capab == "Time of day") result = timeOk + else if(capab == "Days of week") result = daysOk + else { + def myDev = settings.find {it.key == "rDev$i"} + def myState = settings.find {it.key == "state$i"} + def myRel = settings.find {it.key == "RelrDev$i"} + def myAll = settings.find {it.key == "AllrDev$i"} + if(myAll) { + if(myAll.value) result = checkCondAll(myDev.value, myState.value, capab, myRel ? myRel.value : 0) + else result = checkCondAny(myDev.value, myState.value, capab, myRel ? myRel.value : 0) + } else result = checkCondAny(myDev.value, myState.value, capab, myRel ? myRel.value : 0) + } +// log.debug "operand is $result" + return result +} + +def findRParen() { + def noMatch = true + while(noMatch) { + if(state.eval[state.token] == ")") { + if(state.parenLev == 0) return + else state.parenLev = state.parenLev - 1 + } else if(state.eval[state.token] == "(") state.parenLev = state.parenLev + 1 + state.token = state.token + 1 + if(state.token >= state.eval.size) return + } +} + +def disEval() { + if(state.eval[state.token] == "(") { + state.parenLev = 0 + findRParen() + } + if(state.token >= state.eval.size) return + state.token = state.token + 1 +} + +def evalTerm() { + def result = true + def thisTok = state.eval[state.token] + if (thisTok == "(") { + state.token = state.token + 1 + result = eval() + } else result = getOperand(thisTok) + state.token = state.token + 1 + return result +} + +def eval() { + def result = evalTerm() + while(true) { + if(state.token >= state.eval.size) return result + def thisTok = state.eval[state.token] + if (thisTok == "OR") { + if(result) { + disEval() + return true + } + } else if (thisTok == "AND") { + if(!result) { + disEval() + return false + } + } else if (thisTok == ")") return result + state.token = state.token + 1 + result = evalTerm() + } +} + +// Run the evaluation and take action code follows + +def doDelayTrue(time) { + runIn(time * 60, delayRuleTrue) + def delayStr = "minute" + if(time > 1) delayStr = delayStr + "s" + log.info ("$app.label is True, but delayed by $time $delayStr") + state.success = success +} + +def doDelayFalse(time) { + runIn(time * 60, delayRuleFalse) + def delayStr = "minute" + if(time > 1) delayStr = delayStr + "s" + log.info ("$app.label is False, but delayed by $time $delayStr") + state.success = success +} + +def runRule(delay) { + if(!allOk) return + state.token = 0 + def success = eval() + if((success != state.success) || delay) { + unschedule(delayRuleTrue) + unschedule(delayRuleFalse) + if (delayTrue > 0 && !delay && success) doDelayTrue(delayTrue) + else if(delayFalse > 0 && !delay && !success) doDelayFalse(delayFalse) + else { + if(success) { + if(onSwitchTrue) onSwitchTrue.on() + if(offSwitchTrue) offSwitchTrue.off() + if(delayedOffTrue) runIn(delayMinutesTrue * 60, delayOffTrue) + if(pendedOffTrue) runIn(pendMinutesTrue * 60, pendingOffTrue) + if(pendedOffFalse) unschedule(pendingOffFalse) + if(dimATrue) dimATrue.setLevel(dimLATrue) + if(dimBTrue) dimBTrue.setLevel(dimLBTrue) + if(bulbsTrue) setColor(true) + if(lockTrue) lockTrue.lock() + if(unlockTrue) unlockTrue.unlock() + if(openValveTrue) openValveTrue.open() + if(closeValveTrue) closeValveTrue.close() + if(thermoTrue) { if(thermoModeTrue) thermoTrue.setThermostatMode(thermoModeTrue) + if(thermoSetHeatTrue) thermoTrue.setHeatingSetpoint(thermoSetHeatTrue) + if(thermoSetCoolTrue) thermoTrue.setCoolingSetpoint(thermoSetCoolTrue) + if(thermoFanTrue) thermoTrue.setThermostatFanMode(thermoFanTrue) } + if(modeTrue) setLocationMode(modeTrue) + if(myPhraseTrue) location.helloHome.execute(myPhraseTrue) + if(pushTrue) sendPush(msgTrue ?: "Rule $app.label True") + if(phoneTrue) sendSms(phoneTrue, msgTrue ?: "Rule $app.label True") + } else { + if(onSwitchFalse) onSwitchFalse.on() + if(offSwitchFalse) offSwitchFalse.off() + if(delayedOffFalse) runIn(delayMinutesFalse * 60, delayOffFalse) + if(pendedOffFalse) runIn(pendMinutesFalse * 60, pendingOffFalse) + if(pendedOffTrue) unschedule(pendingOffTrue) + if(dimAFalse) dimAFalse.setLevel(dimLAFalse) + if(dimBFalse) dimBFalse.setLevel(dimLBFalse) + if(bulbsFalse) setColor(false) + if(lockFalse) lockFalse.lock() + if(unlockFalse) unlockFalse.unlock() + if(openValveFalse) openValveFalse.open() + if(closeValveFalse) closeValveFalse.close() + if(thermoFalse) { if(thermoModeFalse) thermoFalse.setThermostatMode(thermoModeFalse) + if(thermoSetHeatFalse) thermoFalse.setHeatingSetpoint(thermoSetHeatFalse) + if(thermoSetCoolFalse) thermoFalse.setCoolingSetpoint(thermoSetCoolFalse) + if(thermoFanFalse) thermoFalse.setThermostatFanMode(thermoFanFalse) } + if(modeFalse) setLocationMode(modeFalse) + if(myPhraseFalse) location.helloHome.execute(myPhraseFalse) + if(pushFalse) sendPush(msgFalse ?: "Rule $app.label False") + if(phoneFalse) sendSms(phoneFalse, msgFalse ?: "Rule $app.label False") + } + state.success = success + log.info (success ? "$app.label is True" : "$app.label is False") + } + } +} + +def allHandler(evt) { + log.info "$app.label: $evt.displayName $evt.name $evt.value" + runRule(false) +} + +def startHandler() { + runRule(false) +} + +def stopHandler() { + runRule(false) +} + +def timeHandler() { + runRule(false) +} + +def delayOffTrue() { + if(delayOnOffTrue) delayedOffTrue.on() else delayedOffTrue.off() +} + +def pendingOffTrue() { + if(pendOnOffTrue) pendedOffTrue.on() else pendedOffTrue.off() +} + +def delayOffFalse() { + if(delayOnOffFalse) delayedOffFalse.on() else delayedOffFalse.off() +} + +def pendingOffFalse() { + if(pendOnOffFalse) pendedOffFalse.on() else pendedOffFalse.off() +} + +def delayRuleTrue() { + runRule(true) +} + +def delayRuleFalse() { + runRule(true) +} + +def disabledHandler() { + state.disabled = evt.value == "on" +} + +// private execution filter methods follow + +private atTimeLabel() { + def result = '' + if (timeX == "Sunrise") result = "Sunrise" + offset(atSunriseOffset) + else if(timeX == "Sunset") result = "Sunset" + offset(atSunsetOffset) + else if(atTime) result = hhmm(atTime) +} + +private hhmm(time, fmt = "h:mm a") { + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private offset(value) { + def result = value ? ((value > 0 ? "+" : "") + value + " min") : "" +} + +private timeIntervalLabel() { + def result = "" + if (startingX == "Sunrise" && endingX == "Sunrise") result = "Sunrise" + offset(startSunriseOffset) + " and Sunrise" + offset(endSunriseOffset) + else if (startingX == "Sunrise" && endingX == "Sunset") result = "Sunrise" + offset(startSunriseOffset) + " and Sunset" + offset(endSunsetOffset) + else if (startingX == "Sunset" && endingX == "Sunrise") result = "Sunset" + offset(startSunsetOffset) + " and Sunrise" + offset(endSunriseOffset) + else if (startingX == "Sunset" && endingX == "Sunset") result = "Sunset" + offset(startSunsetOffset) + " and Sunset" + offset(endSunsetOffset) + else if (startingX == "Sunrise" && ending) result = "Sunrise" + offset(startSunriseOffset) + " and " + hhmm(ending, "h:mm a z") + else if (startingX == "Sunset" && ending) result = "Sunset" + offset(startSunsetOffset) + " and " + hhmm(ending, "h:mm a z") + else if (starting && endingX == "Sunrise") result = hhmm(starting) + " and Sunrise" + offset(endSunriseOffset) + else if (starting && endingX == "Sunset") result = hhmm(starting) + " and Sunset" + offset(endSunsetOffset) + else if (starting && ending) result = hhmm(starting) + " and " + hhmm(ending, "h:mm a z") +} + +private getAllOk() { + modeZOk && !state.disabled //&& daysOk && timeOk +} + +private hideOptionsSection() { + (modesZ || physicalOverride || advanced) ? false : true +} + +private getModeZOk() { + def result = !modesZ || modesZ.contains(location.mode) +// log.trace "modeZOk = $result" + return result +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) +// log.trace "modeOk = $result" + return result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) df.setTimeZone(location.timeZone) + else df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + def day = df.format(new Date()) + result = days.contains(day) + } +// log.trace "daysOk = $result" + return result +} + +private getTimeOk() { + def result = true + if((starting && ending) || + (starting && endingX in ["Sunrise", "Sunset"]) || + (startingX in ["Sunrise", "Sunset"] && ending) || + (startingX in ["Sunrise", "Sunset"] && endingX in ["Sunrise", "Sunset"])) { + def currTime = now() + def start = null + def stop = null + def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: startSunriseOffset, sunsetOffset: startSunsetOffset) + if(startingX == "Sunrise") start = s.sunrise.time + else if(startingX == "Sunset") start = s.sunset.time + else if(starting) start = timeToday(starting, location.timeZone).time + s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: endSunriseOffset, sunsetOffset: endSunsetOffset) + if(endingX == "Sunrise") stop = s.sunrise.time + else if(endingX == "Sunset") stop = s.sunset.time + else if(ending) stop = timeToday(ending,location.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } +// log.trace "getTimeOk = $result" + return result +} + +private setColor(trufal) { + def hueColor = 0 + def saturation = 100 + switch(trufal ? colorTrue : colorFalse) { + case "White": + hueColor = 52 + saturation = 19 + break; + case "Daylight": + hueColor = 53 + saturation = 91 + break; + case "Soft White": + hueColor = 23 + saturation = 56 + break; + case "Warm White": + hueColor = 20 + saturation = 80 //83 + break; + case "Blue": + hueColor = 70 + break; + case "Green": + hueColor = 39 + break; + case "Yellow": + hueColor = 25 + break; + case "Orange": + hueColor = 10 + break; + case "Purple": + hueColor = 75 + break; + case "Pink": + hueColor = 83 + break; + case "Red": + hueColor = 100 + break; + } + def lightLevel = trufal ? colorLevelTrue : colorLevelFalse + def newValue = [hue: hueColor, saturation: saturation, level: lightLevel as Integer ?: 100] + if(trufal) bulbsTrue.setColor(newValue) else bulbsFalse.setColor(newValue) + if(lightLevel == 0) {if(trufal) bulbsTrue.off() else bulbsFalse.off()} +} \ No newline at end of file