From c1f91f42db6816d0a02df79c6a5e859ae601cfff Mon Sep 17 00:00:00 2001 From: Matt Miskinnis Date: Wed, 28 Dec 2016 08:54:33 -0800 Subject: [PATCH] MSA-1677: Not sure what to put here... --- .../rule-machine.src/rule-machine.groovy | 623 ++++++++++++++++++ 1 file changed, 623 insertions(+) create mode 100644 smartapps/bravenel/rule-machine.src/rule-machine.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..fa7d4bf --- /dev/null +++ b/smartapps/bravenel/rule-machine.src/rule-machine.groovy @@ -0,0 +1,623 @@ +/** + * Rule Machine + * + * Copyright 2015 Bruce Ravenel and Mike Maxwell + * + * Version 1.6.5 1 Jan 2016 + * + * Version History + * + * 1.6.5 1 Jan 2016 Added version numbers to main page + * 1.6.4 30 Dec 2015 Multi-commands + * 1.6.3 26 Dec 2015 UI improvements and icon per Michael Struck + * 1.6.2 25 Dec 2015 null parameter value patch in expert, maxwell + * 1.6.1 24 Dec 2015 UI improvement + * 1.6 23 Dec 2015 Added expert commands per Mike Maxwell + * + * 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 and Mike Maxwell", + description: "Rule Machine", + category: "My Apps", + iconUrl: "https://raw.githubusercontent.com/bravenel/Rule-Trigger/master/smartapps/bravenel/RuleMachine.png", + iconX2Url: "https://raw.githubusercontent.com/bravenel/Rule-Trigger/master/smartapps/bravenel/RuleMachine%402x.png", + iconX3Url: "https://raw.githubusercontent.com/bravenel/Rule-Trigger/master/smartapps/bravenel/RuleMachine%402x.png" +) + +preferences { + page(name: "mainPage") + page(name: "removePage") + //expert pages + page(name: "expert") + page(name: "generalApprovalPAGE") + page(name: "customCommandsPAGE") + page(name: "addCustomCommandPAGE") + page(name: "customParamsPAGE") +} + +def mainPage() { + dynamicPage(name: "mainPage", title: "Installed Rules", install: true, uninstall: false, submitOnChange: true) { + if(!state.setup) initialize(true) + section { + app(name: "childRules", appName: "Rule", namespace: "bravenel", title: "Create New Rule...", multiple: true) + } + section ("Expert Features") { + href( "expert", title: "", description: "Tap to create custom commands", state: "") + } + section ("Remove Rule Machine"){ + href "removePage", description: "Tap to remove Rule Machine ", title: "" + } + if(state.ver) section ("Version 1.6.5/" + state.ver) { } + } +} + +def removePage() { + dynamicPage(name: "removePage", title: "Remove Rule Machine And All Rules", install: false, uninstall: true) { + section ("WARNING!\n\nRemoving Rule Machine also removes all Rules\n") { + } + } +} + +def installed() { + if(!state.setup) initialize(true) else initialize(false) +} + +def updated() { + initialize(false) +} + +def initialize(first) { + if(first) { + state.ruleState = [:] + state.ruleSubscribers = [:] + } + childApps.each {child -> + if(child.name == "Rule") { + log.info "Installed Rules and Triggers: ${child.label}" + if(first) { + state.ruleState[child.label] = null + state.ruleSubscribers[child.label] = [:] + } + } + } + state.setup = true +} + +def ruleList(appLabel) { + def result = [] + childApps.each { child -> + if(child.name == "Rule" && child.label != appLabel && state.ruleState[child.label] != null) result << child.label + } + return result +} + +def subscribeRule(appLabel, ruleName, ruleTruth, childMethod) { +// log.debug "subscribe: $appLabel, $ruleName, $ruleTruth, $childMethod" + ruleName.each {name -> + state.ruleSubscribers[name].each {if(it == appLabel) return} + if(state.ruleSubscribers[name] == null) state.ruleSubscribers[name] = ["$appLabel":ruleTruth] + else state.ruleSubscribers[name] << ["$appLabel":ruleTruth] + } +} + +def setRuleTruth(appLabel, ruleTruth) { +// log.debug "setRuleTruth1: $appLabel, $ruleTruth" + state.ruleState[appLabel] = ruleTruth + def thisList = state.ruleSubscribers[appLabel] + thisList.each { + if(it.value == null || "$it.value" == "$ruleTruth") { + childApps.each { child -> + if(child.label == it.key) child.ruleHandler(appLabel, ruleTruth) + } + } + } +} + +def currentRule(appLabel) { +// log.debug "currentRule: $appLabel, ${state.ruleState[appLabel]}" + def result = state.ruleState[appLabel] +} + +def childUninstalled() { +// log.debug "childUninstalled called" +} + +def removeChild(appLabel) { +// log.debug "removeChild: $appLabel" + unSubscribeRule(appLabel) + if(state.ruleState[appLabel] != null) state.ruleState.remove(appLabel) + if(state.ruleSubscribers[appLabel] != null) state.ruleSubscribers.remove(appLabel) +} + +def unSubscribeRule(appLabel) { +// log.debug "unSubscribeRule: $appLabel" + state.ruleSubscribers.each { rule -> + def newList = [:] + rule.value.each {list -> + if(list.key != appLabel) newList << list + } + rule.value = newList + } +} + +def runRule(rule, appLabel) { +// log.debug "runRule: $rule, $appLabel" + childApps.each { child -> + rule.each { + if(child.label == it) child.ruleEvaluator(appLabel) + } + } +} + +/*****custom command specific pages *****/ +def expert(){ + dynamicPage(name: "expert", title: "Expert Features", uninstall: false, install: false) { + section(){ + paragraph "${expertText()}" + //expert hrefs... + href( "customCommandsPAGE" + ,title : "Configure Custom Commands..." + ,description: "" + ,state : anyCustom() + ) + } + } +} + +def generalApprovalPAGE(params){ + def title = params.title + def method = params.method + def result + dynamicPage(name: "generalApprovalPAGE", title: title ){ + section() { + if (method) { + result = app."${method}"() + paragraph "${result}" + } + } + } +} + +def customCommandsPAGE() { + if (!state.lastCmdIDX) state.lastCmdIDX = 0 + def savedCommands = getCommands() + dynamicPage(name: "customCommandsPAGE", title: "Custom Commands", uninstall: false, install: false) { + section(){ + input( + name : "devices" + ,title : "Test device" + ,multiple : false + ,required : false + ,type : "capability.actuator" + ,submitOnChange : true + ) + if (settings.devices && savedCommands.size() != 0){ + input( + name : "testCmd" + ,title : "Select saved command to test" + ,multiple : false + ,required : false + ,type : "enum" + ,options : savedCommands + ,submitOnChange : true + ) + } + } + def result = execCommand(settings.testCmd) + if (result) { + section("${result}"){ + } + } + section(){ + if (devices){ + href( "addCustomCommandPAGE" + ,title : "New custom command..." + ,description: "" + ,state : null + ) + } + } + if (getCommands()){ + section(){ + input( + name : "deleteCmds" + ,title : "Delete custom commands..." + ,multiple : true + ,required : false + ,description : "" + ,type : "enum" + ,options : getCommands() + ,submitOnChange : true + ) + if (isValidCommand(deleteCmds)){ + href( "generalApprovalPAGE" + ,title : "Delete command(s) now" + ,description : "" + ,state : null + ,params : [method:"deleteCommands",title:"Delete Command"] + ,submitOnChange : true + ) + } + } + } + } +} + +def addCustomCommandPAGE(){ + def cmdLabel = getCmdLabel() + def complete = "" + def test = false + def pageTitle = "Create new custom command for:\n${devices}" + if (cmdLabel){ + complete = "complete" + test = true + } + dynamicPage(name: "addCustomCommandPAGE", title: pageTitle, uninstall: false, install: false) { + section(){ + input( + name : "cCmd" + ,title : "Available device commands" + ,multiple : false + ,required : false + ,type : "enum" + ,options : getDeviceCommands() + ,submitOnChange : true + ) + href( "customParamsPAGE" + ,title: "Parameters" + ,description: parameterLabel() + ,state: null + ) + } + if (test){ + def result = execTestCommand() + section("Configured command: ${cmdLabel}\n${result}"){ + if (result == "succeeded"){ + if (!commandExists(cmdLabel)){ + href( "generalApprovalPAGE" + ,title : "Save command now" + ,description: "" + ,state : null + ,params : [method:"addCommand",title:"Add Command"] + ) + } + } + } + } + } +} + +def customParamsPAGE(p){ + def ct = settings.findAll{it.key.startsWith("cpType_")} + state.howManyP = ct.size() + 1 + def howMany = state.howManyP + dynamicPage(name: "customParamsPAGE", title: "Select parameters", uninstall: false) { + if(howMany) { + for (int i = 1; i <= howMany; i++) { + def thisParam = "cpType_" + i + def myParam = ct.find {it.key == thisParam} + section("Parameter #${i}") { + getParamType(thisParam, i != howMany) + if(myParam) { + def pType = myParam.value + getPvalue(pType, i) + } + } + } + } + } +} + +/***** child specific methods *****/ + +def getCommandMap(cmdID){ + return state.customCommands["${cmdID}"] +} + + +/***** local custom command specific methods *****/ +def anyCustom(){ + def result = null + if (getCommands()) result = "complete" + return result +} + +def getParamType(myParam,isLast){ + def myOptions = ["string", "number", "decimal"] + def result = input ( + name : myParam + ,type : "enum" + ,title : "parameter type" + ,required : isLast + ,options : myOptions + ,submitOnChange : true + ) + return result +} + +def getPvalue(myPtype, n){ + def myVal = "cpVal_" + n + def result = null + if (myPtype == "string"){ + result = input( + name : myVal + ,title : "string value" + ,type : "text" + ,required : false + ) + } else if (myPtype == "number"){ + result = input( + name : myVal + ,title : "integer value" + ,type : "number" + ,required : false + ) + } else if (myPtype == "decimal"){ + result = input( + name : myVal + ,title : "decimal value" + ,type : "decimal" + ,required : false + ) + } + return result +} + +def getCmdLabel(){ + def cmd + if (settings.cCmd) cmd = settings.cCmd.value + def cpTypes = settings.findAll{it.key.startsWith("cpType_")}.sort() + def result = null + if (cmd) { + result = "${cmd}(" + if (cpTypes.size() == 0){ + result = result + ")" + } else { + def r = getParams(cpTypes) + if (r == "") result = r + else result = "${result}${r})" + } + } + return result +} + +def getParams(cpTypes){ + def result = "" + def cpValue + def badParam = false + cpTypes.each{ cpType -> + def i = cpType.key.replaceAll("cpType_","") + def cpVal = settings.find{it.key == "cpVal_${i}"} + if (cpVal){ + cpValue = cpVal.value + if (cpType.value == "string"){ + result = result + "'${cpValue}'," + } else { + if (cpValue.isNumber()){ + result = result + "${cpValue}," + } else { + result = result + "[${cpValue}]: is not a number," + } + } + } else { + badParam = true + } + } + if (badParam) result = "" + else result = result[0..-2] + return result +} + +def parameterLabel(){ + def howMany = (state.howManyP ?: 1) - 1 + def result = "" + if (howMany) { + for (int i = 1; i <= howMany; i++) { + result = result + parameterLabelN(i) + "\n" + } + result = result[0..-2] + } + return result +} + +def parameterLabelN(i){ + def result = "" + def cpType = settings.find{it.key == "cpType_${i}"} + def cpVal = settings.find{it.key == "cpVal_${i}"} + def cpValue + if (cpVal) cpValue = cpVal.value + else cpValue = "missing value" + if (cpType){ + result = "p${i} - type:${cpType.value}, value:${cpValue}" + } + return result +} + +def getParamsAsList(cpTypes){ + def result = [] + cpTypes.each{ cpType -> + def i = cpType.key.replaceAll("cpType_","") + def cpVal = settings.find{it.key == "cpVal_${i}"} + if (cpVal){ + if (cpType.value == "string"){ + result << "${cpVal.value}" + } else if (cpType.value == "decimal"){ + result << cpVal.value.toBigDecimal() + } else { + result << cpVal.value.toInteger() + } + } else { + result << "missing value" + } + } + return result +} + +def getCommands(){ + def result = [] + def cmdMaps = state.customCommands ?: [] + cmdMaps.each{ cmd -> + def option = [(cmd.key):(cmd.value.text)] + result.push(option) + } + return result +} + +def isValidCommand(cmdIDS){ + def result = false + cmdIDS.each{ cmdID -> + def cmd = state.customCommands["${cmdID}"] + if (cmd) result = true + } + return result +} + +def deleteCommands(){ + def result + def cmdMaps = state.customCommands + if (deleteCmds.size == 1) result = "Command removed" + else result = "Commands removed" + deleteCmds.each{ it -> + cmdMaps.remove(it) + } + return result +} +def commandExists(cmd){ + def result = false + if (state.customCommands){ + result = state.customCommands.find{ it.value.text == "${cmd}" } + } + return result +} +def addCommand(){ + def result + def newCmd = getCmdLabel() + def found = commandExists(newCmd) + def cmdMaps = state.customCommands + //only update if not found... + if (!found) { + state.lastCmdIDX = state.lastCmdIDX + 1 + def nextIDX = state.lastCmdIDX + def cmd = [text:"${newCmd}",cmd:"${cCmd}"] + def params = [:] + def cpTypes = settings.findAll{it.key.startsWith("cpType_")}.sort() + cpTypes.each{ cpType -> + def i = cpType.key.replaceAll("cpType_","") + def cpVal = settings.find{it.key == "cpVal_${i}"} + def param = ["type":"${cpType.value}","value":"${cpVal.value}"] + params.put(i, param) + } + cmd.put("params",params) + if (cmdMaps) cmdMaps.put((nextIDX),cmd) + else state.customCommands = [(nextIDX):cmd] + result = "command: ${newCmd} was added" + } else { + result = "command: ${newCmd} was not added, it already exists." + } + return result +} + +def execTestCommand(){ + def result + def cTypes = settings.findAll{it.key.startsWith("cpType_")} + def p = getParamsAsList(cTypes.sort()) as Object[] + devices.each { device -> + try { + device."${cCmd}"(p) + result = "succeeded" + } + catch (IllegalArgumentException e){ + def em = e as String + def ems = em.split(":") + ems = ems[2].replace(" [","").replace("]","") + ems = ems.replaceAll(", ","\n") + result = "failed, valid commands:\n${ems}" + } + catch (e){ + result = "failed with:\n${e}" + } + } + return result +} + +def execCommand(cmdID){ + def result = "" + def pList = [] + if (cmdID){ + def cmdMap = state.customCommands["${cmdID}"] + if (testCmd && cmdMap) { + def params = cmdMap.params.sort() + params.each{ p -> + if (p.value.type == "string"){ + pList << "${p.value.value}" + } else if (p.value.type == "decimal"){ + pList << p.value.value.toBigDecimal() + } else { + pList << p.value.value.toInteger() + } + } + def p = pList as Object[] + devices.each { device -> + try { + device."${cmdMap.cmd}"(p) + result = "Command succeeded" + } + catch (IllegalArgumentException e){ + def em = e as String + def ems = em.split(":") + ems = ems[2].replace(" [","").replace("]","") + ems = ems.replaceAll(", ","\n") + result = "Command failed, valid commands:\n${ems}" + } + catch (e){ + result = "failed with:\n${e}" + } + } + return result + } + } +} + +def getDeviceCommands(){ + def result = "" + devices.each { device -> + try { + device."xxx"() + result = "Command succeeded" + } + catch (IllegalArgumentException e){ + def em = e as String + def ems = em.split(":") + ems = ems[2].replace(" [","").replace("]","") + result = ems.split(", ").collect{it as String} + } + } + return result +} + +def isExpert(ver){ + state.ver = ver + return getCommands().size() > 0 +} + +def expertText() { + def text = + "Custom commands allows Rule Machine to control devices with custom capabilities. " + + "This includes dual dimmers and switches, ThingShields, FGBW controllers or any device you might build a " + + "custom SmartApp to utilize.\n\nCustom commands that are created and saved here will become available for use " + + "in any new or existing rules.\n\nAfter saving at least one command, look for 'Run custom device command' in your " + + "'Select Actions' sections." +} \ No newline at end of file