/** * Copyright 2020 Markus Liljergren * * 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. */ /* Default imports */ import groovy.json.JsonSlurper import groovy.json.JsonOutput metadata { definition (name: "Tasmota - RFLink (Parent)", namespace: "tasmota", author: "Markus Liljergren", vid: "generic-switch", importURL: "https://raw.githubusercontent.com/markus-li/Hubitat/release/drivers/expanded/tasmota-rflink-parent-expanded.groovy") { capability "Actuator" capability "Light" capability "Switch" capability "Sensor" // Default Capabilities for Energy Monitor capability "Voltage Measurement" capability "Power Meter" capability "Energy Meter" // Default Capabilities capability "Refresh" capability "Configuration" // Default Attributes for Energy Monitor attribute "current", "string" attribute "apparentPower", "string" attribute "reactivePower", "string" attribute "powerFactor", "string" attribute "energyToday", "string" attribute "energyYesterday", "string" attribute "energyTotal", "string" attribute "voltageWithUnit", "string" attribute "powerWithUnit", "string" //attribute "b0Code", "string" // Default Attributes attribute "needUpdate", "string" //attribute "uptime", "string" // This floods the event log! attribute "ip", "string" attribute "ipLink", "string" attribute "module", "string" attribute "templateData", "string" attribute "driverVersion", "string" // Commands for handling Child Devices command "childOn" command "childOff" command "recreateChildDevices" command "deleteChildren" // Default Commands command "reboot" } simulator { } preferences { // Default Preferences input(name: "runReset", description: "For details and guidance, see the release thread in the Hubitat Forum. For settings marked as ADVANCED, make sure you understand what they do before activating them. If settings are not reflected on the device, press the Configure button in this driver. Also make sure all settings really are saved and correct.
Type RESET and then press 'Save Preferences' to DELETE all Preferences and return to DEFAULTS.
", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph") generate_preferences(configuration_model_debug()) //input(name: "b1Code", type: "string", title: "B1 code (received)", description: "Set this to a B1 code and save and the driver will calculate the B0 code.", displayDuringSetup: true, required: false) //input(name: "b0Code", type: "string", title: "B0 code (command)", description: "Set this to a B0 code or input a B1 code and this will be calculated!", displayDuringSetup: true, required: false) //input(name: "rfRawMode", type: "bool", title: "RF Raw Mode", description: 'Set RF mode to RAW, only works with Portisch. MAY be slower than Standard RF mode, but can handle more signals.', displayDuringSetup: true, required: false) // Default Preferences for Parent Devices input(name: "numSwitches", type: "number", title: "Number of Children", description: "Set the number of children (default 1)", defaultValue: "1", displayDuringSetup: true, required: true) // Default Preferences for Tasmota input(name: "ipAddress", type: "string", title: "Device IP Address", description: "Set this as a default fallback for the auto-discovery feature.", displayDuringSetup: true, required: false) input(name: "port", type: "number", title: "Device Port", description: "The http Port of the Device (default: 80)", displayDuringSetup: true, required: false, defaultValue: 80) input(name: "override", type: "bool", title: "Override IP", description: "Override the automatically discovered IP address and disable auto-discovery.", displayDuringSetup: true, required: false) generate_preferences(configuration_model_tasmota()) input(name: "disableModuleSelection", type: "bool", title: "Disable Automatically Setting Module and Template", description: "ADVANCED: Disable automatically setting the Module Type and Template in Tasmota. Enable for using custom Module or Template settings directly on the device. With this disabled, you need to set these settings manually on the device.", displayDuringSetup: true, required: false) input(name: "moduleNumber", type: "number", title: "Module Number", description: "ADVANCED: Module Number used in Tasmota. If Device Template is set, this value is IGNORED. (default: -1 (use the default for the driver))", displayDuringSetup: true, required: false, defaultValue: -1) input(name: "deviceTemplateInput", type: "string", title: "Device Template", description: "ADVANCED: Set this to a Device Template for Tasmota, leave it EMPTY to use the driver default. Set it to 0 to NOT use a Template. NAME can be maximum 14 characters! (Example: {\"NAME\":\"S120\",\"GPIO\":[0,0,0,0,0,21,0,0,0,52,90,0,0],\"FLAG\":0,\"BASE\":18})", displayDuringSetup: true, required: false) input(name: "useIPAsID", type: "bool", title: "IP as Network ID", description: "ADVANCED: Not needed under normal circumstances. Setting this when not needed can break updates. This requires the IP to be static or set to not change in your DHCP server. It will force the use of IP as network ID. When in use, set Override IP to true and input the correct Device IP Address. See the release thread in the Hubitat forum for details and guidance.", displayDuringSetup: true, required: false) } } def getDeviceInfoByName(infoName) { // DO NOT EDIT: This is generated from the metadata! // TODO: Figure out how to get this from Hubitat instead of generating this? deviceInfo = ['name': 'Tasmota - RFLink (Parent)', 'namespace': 'tasmota', 'author': 'Markus Liljergren', 'vid': 'generic-switch', 'importURL': 'https://raw.githubusercontent.com/markus-li/Hubitat/release/drivers/expanded/tasmota-rflink-parent-expanded.groovy'] return(deviceInfo[infoName]) } /* These functions are unique to each driver */ def installedAdditional() { // This runs from installed() logging("installedAdditional()",50) createChildDevices() } def on() { logging("on()",50) //logging("device.namespace: ${getDeviceInfoByName('namespace')}, device.driverName: ${getDeviceInfoByName('name')}", 50) def cmds = [] cmds << getAction(getCommandString("Power0", "1")) Integer numSwitchesI = numSwitches.toInteger() for (i in 1..numSwitchesI) { cmds << getAction(getCommandString("Power$i", "1")) } //return delayBetween(cmds, 500) return cmds } def off() { logging("off()",50) def cmds = [] cmds << getAction(getCommandString("Power0", "0")) Integer numSwitchesI = numSwitches.toInteger() for (i in 1..numSwitchesI) { cmds << getAction(getCommandString("Power$i", "0")) } //return delayBetween(cmds, 500) return cmds } def updateRFMode() { // If this is called from somewhere it could be repeatedly called, rate-limit this! See RF Bridge for an example def cmds = [] logging("Initializing RFLink...", 100) cmds << getAction(getCommandString("Baudrate", "57600")) cmds << getAction(getCommandString("SerialSend1", "10;PING;")) return cmds } def refreshAdditional() { logging("refreshAdditional()", 10) def cmds = [] cmds << getAction(getCommandString("SerialSend1", "10;PING;")) return cmds } def splitRFLinkData(rawRFLinkData) { parsedData = [] rawRFLinkData.split('20;').each { cmd -> if(cmd != null && cmd != '') { c = cmd.split(';') r = c[2..(c.size()-1)].inject([:]) { map, token -> token.split('=').with { try { map[it[0].trim()] = it[1].trim() } catch(ArrayIndexOutOfBoundsException ex) { // Do nothing, we don't want these values... } } map } d =['counter': c[0], 'etype': c[1]] d << r parsedData << d } } return parsedData } def makeRFLinkDataString(splitRFLinkData) { childData = '' splitRFLinkData.each { if(it.key != 'counter') { childData = childData + "${it.key.toUpperCase()}=${it.value};" } } return(childData) } def parse(description) { // parse() Generic Tasmota-device header BEGINS here //log.debug "Parsing: ${description}" def events = [] def descMap = parseDescriptionAsMap(description) def body //log.debug "descMap: ${descMap}" if (!state.mac || state.mac != descMap["mac"]) { logging("Mac address of device found ${descMap["mac"]}",1) state.mac = descMap["mac"] } prepareDNI() if (descMap["body"] && descMap["body"] != "T04=") body = new String(descMap["body"].decodeBase64()) if (body && body != "") { if(body.startsWith("{") || body.startsWith("[")) { logging("========== Parsing Report ==========",99) def slurper = new JsonSlurper() def result = slurper.parseText(body) logging("result: ${result}",0) // parse() Generic header ENDS here // Standard Basic Data parsing if (result.containsKey("POWER")) { logging("POWER: $result.POWER",99) events << createEvent(name: "switch", value: result.POWER.toLowerCase()) } if (result.containsKey("StatusNET")) { logging("StatusNET: $result.StatusNET",99) result << result.StatusNET //logging("result: ${result}",0) } if (result.containsKey("LoadAvg")) { logging("LoadAvg: $result.LoadAvg",99) } if (result.containsKey("Sleep")) { logging("Sleep: $result.Sleep",99) } if (result.containsKey("SleepMode")) { logging("SleepMode: $result.SleepMode",99) } if (result.containsKey("Vcc")) { logging("Vcc: $result.Vcc",99) } if (result.containsKey("Hostname")) { logging("Hostname: $result.Hostname",99) } if (result.containsKey("IPAddress") && (override == false || override == null)) { logging("IPAddress: $result.IPAddress",99) events << createEvent(name: "ip", value: "$result.IPAddress") //logging("ipLink: $result.IPAddress",10) events << createEvent(name: "ipLink", value: "$result.IPAddress") } if (result.containsKey("WebServerMode")) { logging("WebServerMode: $result.WebServerMode",99) } if (result.containsKey("Version")) { logging("Version: $result.Version",99) } if (result.containsKey("Module") && !result.containsKey("Version")) { // The check for Version is here to avoid using the wrong message logging("Module: $result.Module",50) events << createEvent(name: "module", value: "$result.Module") } // When it is a Template, it looks a bit different if (result.containsKey("NAME") && result.containsKey("GPIO") && result.containsKey("FLAG") && result.containsKey("BASE")) { n = result.toMapString() n = n.replaceAll(', ',',') n = n.replaceAll('\\[','{').replaceAll('\\]','}') n = n.replaceAll('NAME:', '"NAME":"').replaceAll(',GPIO:\\{', '","GPIO":\\[') n = n.replaceAll('\\},FLAG', '\\],"FLAG"').replaceAll('BASE', '"BASE"') // TODO: Learn how to do this the right way in Groovy logging("Template: $n",50) events << createEvent(name: "templateData", value: "${n}") } if (result.containsKey("RestartReason")) { logging("RestartReason: $result.RestartReason",99) } if (result.containsKey("TuyaMCU")) { logging("TuyaMCU: $result.TuyaMCU",99) events << createEvent(name: "tuyaMCU", value: "$result.TuyaMCU") } if (result.containsKey("SetOption81")) { logging("SetOption81: $result.SetOption81",99) } if (result.containsKey("SetOption113")) { logging("SetOption113 (Hubitat enabled): $result.SetOption113",99) } if (result.containsKey("Uptime")) { logging("Uptime: $result.Uptime",99) // Even with "displayed: false, archivable: false" these events still show up under events... There is no way of NOT having it that way... //events << createEvent(name: 'uptime', value: result.Uptime, displayed: false, archivable: false) state.uptime = result.Uptime } if (result.containsKey("RestartReason")) { events << updateRFMode() } // All commands received are separated by 20, then it's the counter (00 to FF, then it wraps around). // After that is the name of the encoding, after which key->data pairs come if (result.containsKey("SerialReceived")) { logging("SerialReceived: $result.SerialReceived", 100) splitRFLinkData = splitRFLinkData(result.SerialReceived) logging("Split RFLink Data: '${splitRFLinkData}'", 100) splitRFLinkData.each { logging("it=${it}", 100) if(it.containsKey('counter')) { //if(it.containsKey('seen') && it['seen'] >= maxActionNumSeen) { // maxActionNumSeen = it['seen'] // frequentData = it['data'] //} it['Data'] = makeRFLinkDataString(it) it['type'] = 'rflink' logging("Split RFLink Data field: '${it['Data']}'", 100) events << sendParseEventToChildren(it) } } } // Standard Wifi Data parsing if (result.containsKey("Wifi")) { if (result.Wifi.containsKey("AP")) { logging("AP: $result.Wifi.AP",99) } if (result.Wifi.containsKey("BSSId")) { logging("BSSId: $result.Wifi.BSSId",99) } if (result.Wifi.containsKey("Channel")) { logging("Channel: $result.Wifi.Channel",99) } if (result.Wifi.containsKey("RSSI")) { logging("RSSI: $result.Wifi.RSSI",99) } if (result.Wifi.containsKey("SSId")) { logging("SSId: $result.Wifi.SSId",99) } } // Standard TuyaSwitch Data parsing Integer numSwitchesI = numSwitches.toInteger() if (numSwitchesI == 1 && result.containsKey("POWER")) { logging("POWER (child): $result.POWER",1) events << childSendState("1", result.POWER.toLowerCase()) } if (result.containsKey("POWER1")) { logging("POWER1: $result.POWER1",1) events << childSendState("1", result.POWER1.toLowerCase()) events << createEvent(name: "switch", value: (areAllChildrenSwitchedOn(result.POWER1.toLowerCase() == "on"?1:0) && result.POWER1.toLowerCase() == "on"? "on" : "off")) } if (result.containsKey("POWER2")) { logging("POWER2: $result.POWER2",1) events << childSendState("2", result.POWER2.toLowerCase()) events << createEvent(name: "switch", value: (areAllChildrenSwitchedOn(result.POWER2.toLowerCase() == "on"?2:0) && result.POWER2.toLowerCase() == "on"? "on" : "off")) } if (result.containsKey("POWER3")) { logging("POWER3: $result.POWER3",1) events << childSendState("3", result.POWER3.toLowerCase()) events << createEvent(name: "switch", value: (areAllChildrenSwitchedOn(result.POWER3.toLowerCase() == "on"?3:0) && result.POWER3.toLowerCase() == "on"? "on" : "off")) } if (result.containsKey("POWER4")) { logging("POWER4: $result.POWER4",1) events << childSendState("4", result.POWER4.toLowerCase()) events << createEvent(name: "switch", value: (areAllChildrenSwitchedOn(result.POWER4.toLowerCase() == "on"?4:0) && result.POWER4.toLowerCase() == "on" ? "on" : "off")) } if (result.containsKey("POWER5")) { logging("POWER5: $result.POWER5",1) events << childSendState("5", result.POWER5.toLowerCase()) events << createEvent(name: "switch", value: (areAllChildrenSwitchedOn(result.POWER5.toLowerCase() == "on"?5:0) && result.POWER5.toLowerCase() == "on" ? "on" : "off")) } if (result.containsKey("POWER6")) { logging("POWER6: $result.POWER6",1) events << childSendState("6", result.POWER6.toLowerCase()) events << createEvent(name: "switch", value: (areAllChildrenSwitchedOn(result.POWER6.toLowerCase() == "on"?6:0) && result.POWER6.toLowerCase() == "on" ? "on" : "off")) } // Standard Energy Monitor Data parsing if (result.containsKey("StatusSNS")) { result << result.StatusSNS } if (result.containsKey("ENERGY")) { //logging("Has ENERGY...", 1) //if (!state.containsKey('energy')) state.energy = {} if (result.ENERGY.containsKey("Total")) { logging("Total: $result.ENERGY.Total kWh",99) events << createEvent(name: "energyTotal", value: "$result.ENERGY.Total kWh") } if (result.ENERGY.containsKey("Today")) { logging("Today: $result.ENERGY.Today kWh",99) events << createEvent(name: "energyToday", value: "$result.ENERGY.Today kWh") } if (result.ENERGY.containsKey("Yesterday")) { logging("Yesterday: $result.ENERGY.Yesterday kWh",99) events << createEvent(name: "energyYesterday", value: "$result.ENERGY.Yesterday kWh") } if (result.ENERGY.containsKey("Current")) { logging("Current: $result.ENERGY.Current A",99) r = (result.ENERGY.Current == null) ? 0 : result.ENERGY.Current events << createEvent(name: "current", value: "$r A") } if (result.ENERGY.containsKey("ApparentPower")) { logging("apparentPower: $result.ENERGY.ApparentPower VA",99) events << createEvent(name: "apparentPower", value: "$result.ENERGY.ApparentPower VA") } if (result.ENERGY.containsKey("ReactivePower")) { logging("reactivePower: $result.ENERGY.ReactivePower VAr",99) events << createEvent(name: "reactivePower", value: "$result.ENERGY.ReactivePower VAr") } if (result.ENERGY.containsKey("Factor")) { logging("powerFactor: $result.ENERGY.Factor",99) events << createEvent(name: "powerFactor", value: "$result.ENERGY.Factor") } if (result.ENERGY.containsKey("Voltage")) { logging("Voltage: $result.ENERGY.Voltage V",99) r = (result.ENERGY.Voltage == null) ? 0 : result.ENERGY.Voltage events << createEvent(name: "voltageWithUnit", value: "$r V") events << createEvent(name: "voltage", value: r, unit: "V") } if (result.ENERGY.containsKey("Power")) { logging("Power: $result.ENERGY.Power W",99) r = (result.ENERGY.Power == null) ? 0 : result.ENERGY.Power events << createEvent(name: "powerWithUnit", value: "$r W") events << createEvent(name: "power", value: r, unit: "W") //state.energy.power = r } } // StatusPTH:[PowerDelta:0, PowerLow:0, PowerHigh:0, VoltageLow:0, VoltageHigh:0, CurrentLow:0, CurrentHigh:0] // parse() Generic Tasmota-device footer BEGINS here } else { //log.debug "Response is not JSON: $body" } } if (!device.currentValue("ip") || (device.currentValue("ip") != getDataValue("ip"))) { curIP = getDataValue("ip") logging("Setting IP: $curIP", 1) events << createEvent(name: 'ip', value: curIP) events << createEvent(name: "ipLink", value: "$curIP") } return events // parse() Generic footer ENDS here } def update_needed_settings() { // updateNeededSettings() Generic header BEGINS here def cmds = [] def currentProperties = state.currentProperties ?: [:] state.settings = settings def configuration = new XmlSlurper().parseText(configuration_model_tasmota()) def isUpdateNeeded = "NO" if(runReset != null && runReset == 'RESET') { for ( e in state.settings ) { logging("Deleting '${e.key}' with value = ${e.value} from Settings", 50) // Not sure which ones are needed, so doing all... device.clearSetting("${e.key}") device.removeSetting("${e.key}") state.settings.remove("${e.key}") } } prepareDNI() // updateNeededSettings() Generic header ENDS here /*logging('Just saved...', 10) if((b0Code == null || b0Code == '') && b1Code != null && b1Code != '') { b0CodeTmp = calculateB0(b1Code, 0) b0Code = b0CodeTmp.replace(' ', '') sendEvent(name: "b0Code", value: b0CodeTmp) state.b0Code = b0Code logging('Calculated b0Code! ', 10) }*/ // Don't send any other commands until AFTER setting the correct Module/Template // Tasmota Module and Template selection command (autogenerated) cmds << getAction(getCommandString("Module", null)) cmds << getAction(getCommandString("Template", null)) if(disableModuleSelection == null) disableModuleSelection = false moduleNumberUsed = moduleNumber if(moduleNumber == null || moduleNumber == -1) moduleNumberUsed = 0 useDefaultTemplate = false defaultDeviceTemplate = '' if(deviceTemplateInput != null && deviceTemplateInput == "0") { useDefaultTemplate = true defaultDeviceTemplate = '' } if(deviceTemplateInput == null || deviceTemplateInput == "") { // We should use the default of the driver useDefaultTemplate = true defaultDeviceTemplate = '{"NAME":"RFLink Bridge","GPIO":[0,149,0,148,0,0,0,0,0,0,0,0,0],"FLAG":0,"BASE":18}' } if(deviceTemplateInput != null) deviceTemplateInput = deviceTemplateInput.replaceAll(' ','') if(disableModuleSelection == false && ((deviceTemplateInput != null && deviceTemplateInput != "") || (useDefaultTemplate && defaultDeviceTemplate != ""))) { if(useDefaultTemplate == false && deviceTemplateInput != null && deviceTemplateInput != "") { usedDeviceTemplate = deviceTemplateInput } else { usedDeviceTemplate = defaultDeviceTemplate } logging("Setting the Template soon...", 10) logging("templateData = ${device.currentValue('templateData')}", 10) if(usedDeviceTemplate != '') moduleNumberUsed = 0 // This activates the Template when set if(usedDeviceTemplate != null && device.currentValue('templateData') != null && device.currentValue('templateData') != usedDeviceTemplate) { logging("The template is NOT set to '${usedDeviceTemplate}', it is set to '${device.currentValue('templateData')}'",10) urlencodedTemplate = URLEncoder.encode(usedDeviceTemplate).replace("+", "%20") // The NAME part of th Device Template can't exceed 14 characters! More than that and they will be truncated. // TODO: Parse and limit the size of NAME cmds << getAction(getCommandString("Template", "${urlencodedTemplate}")) } else if (device.currentValue('module') == null){ // Update our stored value! cmds << getAction(getCommandString("Template", null)) }else if (usedDeviceTemplate != null) { logging("The template is set to '${usedDeviceTemplate}' already!",10) } } else { logging("Can't set the Template...", 10) logging(device.currentValue('templateData'), 10) //logging("deviceTemplateInput: '${deviceTemplateInput}'", 10) //logging("disableModuleSelection: '${disableModuleSelection}'", 10) } if(disableModuleSelection == false && moduleNumberUsed != null && moduleNumberUsed >= 0) { logging("Setting the Module soon...", 10) logging("device.currentValue('module'): '${device.currentValue('module')}'", 10) if(moduleNumberUsed != null && device.currentValue('module') != null && !device.currentValue('module').startsWith("[${moduleNumberUsed}:")) { logging("This DOESN'T start with [${moduleNumberUsed} ${device.currentValue('module')}",10) cmds << getAction(getCommandString("Module", "${moduleNumberUsed}")) } else if (moduleNumberUsed != null && device.currentValue('module') != null){ logging("This starts with [${moduleNumberUsed} ${device.currentValue('module')}",10) } else if (device.currentValue('module') == null){ // Update our stored value! cmds << getAction(getCommandString("Module", null)) } else { logging("Module is set to '${device.currentValue('module')}', and it's set to be null, report this to the creator of this driver!",10) } } else { logging("Setting the Module has been disabled!", 10) } //cmds << getAction(getCommandString("SetOption81", "1")) // Set PCF8574 component behavior for all ports as inverted (default=0) //cmds << getAction(getCommandString("LedPower", "1")) // 1 = turn LED ON and set LedState 8 //cmds << getAction(getCommandString("LedState", "8")) // 8 = LED on when Wi-Fi and MQTT are connected. // updateNeededSettings() TelePeriod setting cmds << getAction(getCommandString("TelePeriod", "300")) // Don't send these types of commands until AFTER setting the correct Module/Template cmds << updateRFMode() // updateNeededSettings() Generic footer BEGINS here cmds << getAction(getCommandString("SetOption113", "1")) // Hubitat Enabled // Disabling Emulation so that we don't flood the logs with upnp traffic //cmds << getAction(getCommandString("Emulation", "0")) // Emulation Disabled cmds << getAction(getCommandString("HubitatHost", device.hub.getDataValue("localIP"))) logging("HubitatPort: ${device.hub.getDataValue("localSrvPortTCP")}", 1) cmds << getAction(getCommandString("HubitatPort", device.hub.getDataValue("localSrvPortTCP"))) cmds << getAction(getCommandString("FriendlyName1", URLEncoder.encode(device.displayName.take(32)))) // Set to a maximum of 32 characters if(override == true) { cmds << sync(ipAddress) } //logging("Cmds: " +cmds,1) sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: false) return cmds // updateNeededSettings() Generic footer ENDS here } /* Default functions go here */ private def getDriverVersion() { logging("getDriverVersion()", 50) def cmds = [] comment = "Functional - Need feedback - Device Model Info" if(comment != "") state.comment = comment sendEvent(name: "driverVersion", value: "v0.9.3 for Tasmota 7.x/8.x (Hubitat version)") return cmds } def getChildDriverName() { childDriverName = 'Tasmota - RF/IR Switch/Toggle/Push (Child)' logging("childDriverName = '$childDriverName'", 1) return(childDriverName) } /* Logging function included in all drivers */ private def logging(message, level) { if (logLevel != "0"){ switch (logLevel) { case "-1": // Insanely verbose if (level >= 0 && level <= 100) log.debug "$message" break case "1": // Very verbose if (level >= 1 && level < 99 || level == 100) log.debug "$message" break case "10": // A little less if (level >= 10 && level < 99 || level == 100) log.debug "$message" break case "50": // Rather chatty if (level >= 50 ) log.debug "$message" break case "99": // Only parsing reports if (level >= 99 ) log.debug "$message" break case "100": // Only special debug messages, eg IR and RF codes if (level == 100 ) log.debug "$message" break } } } /* Helper functions included in all drivers */ def installed() { logging("installed()", 50) configure() try { // In case we have some more to run specific to this driver installedAdditional() } catch (MissingMethodException e) { // ignore } } /* initialize Purpose: initialize the driver Note: also called from updated() in most drivers */ void initialize() { logging("initialize()", 50) unschedule() // disable debug logs after 30 min, unless override is in place if (logLevel != "0") { if(runReset != "DEBUG") { log.warn "Debug logging will be disabled in 30 minutes..." } else { log.warn "Debug logging will NOT BE AUTOMATICALLY DISABLED!" } runIn(1800, logsOff) } } def configure() { logging("configure()", 50) def cmds = [] cmds = update_needed_settings() try { // Run the getDriverVersion() command newCmds = getDriverVersion() if (newCmds != null && newCmds != []) cmds = cmds + newCmds } catch (MissingMethodException e) { // ignore } if (cmds != []) cmds } def generate_preferences(configuration_model) { def configuration = new XmlSlurper().parseText(configuration_model) configuration.Value.each { if(it.@hidden != "true" && it.@disabled != "true"){ switch(it.@type) { case ["number"]: input "${it.@index}", "number", title:"${it.@label}\n" + "${it.Help}", description: "${it.@description}", range: "${it.@min}..${it.@max}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break case "list": def items = [] it.Item.each { items << ["${it.@value}":"${it.@label}"] } input "${it.@index}", "enum", title:"${it.@label}\n" + "${it.Help}", description: "${it.@description}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}", options: items break case ["password"]: input "${it.@index}", "password", title:"${it.@label}\n" + "${it.Help}", description: "${it.@description}", displayDuringSetup: "${it.@displayDuringSetup}" break case "decimal": input "${it.@index}", "decimal", title:"${it.@label}\n" + "${it.Help}", description: "${it.@description}", range: "${it.@min}..${it.@max}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break case "boolean": input "${it.@index}", "boolean", title:"${it.@label}\n" + "${it.Help}", description: "${it.@description}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break } } } } def update_current_properties(cmd) { def currentProperties = state.currentProperties ?: [:] currentProperties."${cmd.name}" = cmd.value if (state.settings?."${cmd.name}" != null) { if (state.settings."${cmd.name}".toString() == cmd.value) { sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: false) } else { sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: false) } } state.currentProperties = currentProperties } /* logsOff Purpose: automatically disable debug logging after 30 mins. Note: scheduled in Initialize() */ void logsOff(){ if(runReset != "DEBUG") { log.warn "Debug logging disabled..." // Setting logLevel to "0" doesn't seem to work, it disables logs, but does not update the UI... //device.updateSetting("logLevel",[value:"0",type:"string"]) //app.updateSetting("logLevel",[value:"0",type:"list"]) // Not sure which ones are needed, so doing all... This works! device.clearSetting("logLevel") device.removeSetting("logLevel") state.settings.remove("logLevel") } else { log.warn "OVERRIDE: Disabling Debug logging will not execute with 'DEBUG' set..." if (logLevel != "0") runIn(1800, logsOff) } } private def getFilteredDeviceDriverName() { deviceDriverName = getDeviceInfoByName('name') if(deviceDriverName.toLowerCase().endsWith(' (parent)')) { deviceDriverName = deviceDriverName.substring(0, deviceDriverName.length()-9) } return deviceDriverName } private def getFilteredDeviceDisplayName() { device_display_name = device.displayName.replace(' (parent)', '').replace(' (Parent)', '') return device_display_name } def configuration_model_debug() { ''' ''' } /* Helper functions included when needing Child devices */ // Get the button number private channelNumber(String dni) { def ch = dni.split("-")[-1] as Integer return ch } def childOn(String dni) { // Make sure to create an onOffCmd that sends the actual command onOffCmd(1, channelNumber(dni)) } def childOff(String dni) { // Make sure to create an onOffCmd that sends the actual command onOffCmd(0, channelNumber(dni)) } private childSendState(String currentSwitchNumber, String state) { def childDevice = childDevices.find{it.deviceNetworkId.endsWith("-${currentSwitchNumber}")} if (childDevice) { logging("childDevice.sendEvent ${currentSwitchNumber} ${state}",1) childDevice.sendEvent(name: "switch", value: state, type: type) } else { logging("childDevice.sendEvent ${currentSwitchNumber} is missing!",1) } } private areAllChildrenSwitchedOn(Integer skip = 0) { def children = getChildDevices() boolean status = true Integer i = 1 children.each {child-> if (i!=skip) { if(child.currentState("switch")?.value == "off") { status = false } } i++ } return status } private sendParseEventToChildren(data) { def children = getChildDevices() children.each {child-> child.parseParentData(data) } return status } private void createChildDevices() { Integer numSwitchesI = numSwitches.toInteger() logging("createChildDevices: creating $numSwitchesI device(s)",1) // If making changes here, don't forget that recreateDevices need to have the same settings set for (i in 1..numSwitchesI) { // https://community.hubitat.com/t/composite-devices-parent-child-devices/1925 try { addChildDevice("${getDeviceInfoByName("namespace")}", "${getChildDriverName()}", "$device.id-$i", [name: "${getFilteredDeviceDriverName()} #$i", label: "${getFilteredDeviceDisplayName()} $i", isComponent: false]) } catch (com.hubitat.app.exception.UnknownDeviceTypeException e) { log.error "'${getChildDriverName()}' driver can't be found! Did you forget to install the child driver?" } } } def recreateChildDevices() { Integer numSwitchesI = numSwitches.toInteger() logging("recreateChildDevices: recreating $numSwitchesI device(s)",1) def childDevice = null for (i in 1..numSwitchesI) { childDevice = childDevices.find{it.deviceNetworkId.endsWith("-$i")} if (childDevice) { // The device exists, just update it childDevice.setName("${getDeviceInfoByName('name')} #$i") childDevice.setDeviceNetworkId("$device.id-$i") // This doesn't work right now... logging(childDevice.getData(), 10) // We leave the device Label alone, since that might be desired by the user to change //childDevice.setLabel("$device.displayName $i") //.setLabel doesn't seem to work on child devices??? } else { // No such device, we should create it try { addChildDevice("${getDeviceInfoByName("namespace")}", "${getChildDriverName()}", "$device.id-$i", [name: "${getFilteredDeviceDriverName()} #$i", label: "${getFilteredDeviceDisplayName()} $i", isComponent: false]) } catch (com.hubitat.app.exception.UnknownDeviceTypeException e) { log.error "'${getChildDriverName()}' driver can't be found! Did you forget to install the child driver?" } } } if (numSwitchesI < 4) { // Check if we should delete some devices for (i in 1..4) { if (i > numSwitchesI) { childDevice = childDevices.find{it.deviceNetworkId.endsWith("-$i")} if (childDevice) { logging("Removing child #$i!", 10) deleteChildDevice(childDevice.deviceNetworkId) } } } } } def deleteChildren() { logging("deleteChildren",1) def children = getChildDevices() children.each {child-> deleteChildDevice(child.deviceNetworkId) } } /* Helper functions included in all Tasmota drivers */ def refresh() { logging("refresh()", 10) def cmds = [] cmds << getAction(getCommandString("Status", "0")) try { // In case we have some more to run specific to this driver refreshAdditional() } catch (MissingMethodException e) { // ignore } return cmds } def reboot() { logging("reboot()", 10) getAction(getCommandString("Restart", "1")) } def updated() { logging("updated()", 10) def cmds = [] cmds = update_needed_settings() //sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID]) sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: false) logging(cmds, 0) try { // Also run initialize(), if it exists... initialize() } catch (MissingMethodException e) { // ignore } if (cmds != [] && cmds != null) cmds } def prepareDNI() { if (useIPAsID) { hexIPAddress = setDeviceNetworkId(ipAddress, true) if(hexIPAddress != null && state.dni != hexIPAddress) { state.dni = hexIPAddress updateDNI() } } else if (state.mac != null && state.dni != state.mac) { state.dni = setDeviceNetworkId(state.mac) updateDNI() } } def getCommandString(command, value) { def uri = "/cm?" if (password) { uri += "user=admin&password=${password}&" } if (value) { uri += "cmnd=${command}%20${value}" } else { uri += "cmnd=${command}" } return uri } def getMultiCommandString(commands) { def uri = "/cm?" if (password) { uri += "user=admin&password=${password}&" } uri += "cmnd=backlog%20" commands.each {cmd-> if(cmd.containsKey("value")) { uri += "${cmd['command']}%20${cmd['value']}%3B%20" } else { uri += "${cmd['command']}%3B%20" } } return uri } def parseDescriptionAsMap(description) { description.split(",").inject([:]) { map, param -> def nameAndValue = param.split(":") if (nameAndValue.length == 2) map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] else map += [(nameAndValue[0].trim()):""] } } private getAction(uri){ logging("Using getAction for '${uri}'...", 0) return httpGetAction(uri) } private httpGetAction(uri){ updateDNI() def headers = getHeader() //logging("Using httpGetAction for '${uri}'...", 0) def hubAction = null try { hubAction = new hubitat.device.HubAction( method: "GET", path: uri, headers: headers ) } catch (e) { log.error "Error in httpGetAction(uri): $e ('$uri')" } return hubAction } private postAction(uri, data){ updateDNI() def headers = getHeader() def hubAction = null try { hubAction = new hubitat.device.HubAction( method: "POST", path: uri, headers: headers, body: data ) } catch (e) { log.error "Error in postAction(uri, data): $e ('$uri', '$data')" } return hubAction } private onOffCmd(value, endpoint) { logging("onOffCmd, value: $value, endpoint: $endpoint", 1) def cmds = [] cmds << getAction(getCommandString("Power$endpoint", "$value")) return cmds } private setDeviceNetworkId(macOrIP, isIP = false){ def myDNI if (isIP == false) { myDNI = macOrIP } else { logging("About to convert ${macOrIP}...", 0) myDNI = convertIPtoHex(macOrIP) } logging("Device Network Id should be set to ${myDNI} from ${macOrIP}", 0) return myDNI } private updateDNI() { if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) { logging("Device Network Id will be set to ${state.dni} from ${device.deviceNetworkId}", 0) device.deviceNetworkId = state.dni } } private getHostAddress() { if (port == null) { port = 80 } if (override == true && ipAddress != null){ return "${ipAddress}:${port}" } else if(getDeviceDataByName("ip") && getDeviceDataByName("port")){ return "${getDeviceDataByName("ip")}:${getDeviceDataByName("port")}" }else{ return "${ip}:80" } } private String convertIPtoHex(ipAddress) { String hex = null if(ipAddress != null) { hex = ipAddress.tokenize( '.' ).collect { String.format( '%02X', it.toInteger() ) }.join() logging("Get this IP in hex: ${hex}", 0) } else { hex = null if (useIPAsID) { logging('ERROR: To use IP as Network ID "Device IP Address" needs to be set and "Override IP" needs to be enabled! If this error persists, consult the release thread in the Hubitat Forum.') } } return hex } private String urlEscape(url) { return(URLEncoder.encode(url).replace("+", "%20")) } private String convertPortToHex(port) { String hexport = port.toString().format( '%04X', port.toInteger() ) return hexport } private encodeCredentials(username, password){ def userpassascii = "${username}:${password}" def userpass = "Basic " + userpassascii.bytes.encodeBase64().toString() return userpass } private getHeader(userpass = null){ def headers = [:] headers.put("Host", getHostAddress()) headers.put("Content-Type", "application/x-www-form-urlencoded") if (userpass != null) headers.put("Authorization", userpass) return headers } def sync(ip, port = null) { def existingIp = getDataValue("ip") def existingPort = getDataValue("port") logging("Running sync()", 1) if (ip && ip != existingIp) { updateDataValue("ip", ip) sendEvent(name: 'ip', value: ip) sendEvent(name: "ipLink", value: "$ip") logging("IP set to ${ip}", 1) } if (port && port != existingPort) { updateDataValue("port", port) logging("Port set to ${port}", 1) } } def configuration_model_tasmota() { ''' ''' }