/** * Copyright 2018 Eric Maycock * * 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. * * Sonoff IFan02 Wifi Controller * * Author: Eric Maycock (erocm123) * Date: 2018-09-14 */ import groovy.json.JsonSlurper import groovy.util.XmlSlurper metadata { definition (name: "Sonoff IFan02 Wifi Controller", namespace: "erocm123", author: "Eric Maycock", vid: "generic-dimmer") { capability "Actuator" capability "Switch" capability "Switch Level" capability "Refresh" capability "Sensor" capability "Configuration" capability "Health Check" command "reboot" attribute "needUpdate", "string" } simulator { } preferences { input description: "Once you change values on this page, the corner of the \"configuration\" icon will change orange until all configuration parameters are updated.", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph" generate_preferences(configuration_model()) } tiles (scale: 2){ multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { tileAttribute("device.switch", key: "PRIMARY_CONTROL") { attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState: "turningOff" attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState: "turningOff" } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel" } } childDeviceTiles("all") standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } standardTile("configure", "device.needUpdate", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "NO" , label:'', action:"configuration.configure", icon:"st.secondary.configure" state "YES", label:'', action:"configuration.configure", icon:"https://github.com/erocm123/SmartThingsPublic/raw/master/devicetypes/erocm123/qubino-flush-1d-relay.src/configure@2x.png" } standardTile("reboot", "device.reboot", decoration: "flat", height: 2, width: 2, inactiveLabel: false) { state "default", label:"Reboot", action:"reboot", icon:"", backgroundColor:"#ffffff" } valueTile("ip", "ip", width: 2, height: 1) { state "ip", label:'IP Address\r\n${currentValue}' } valueTile("uptime", "uptime", width: 2, height: 1) { state "uptime", label:'Uptime ${currentValue}' } } } def installed() { logging("installed()", 1) createChildDevices() configure() } def configure() { logging("configure()", 1) def cmds = [] cmds = update_needed_settings() if (cmds != []) cmds } def updated() { logging("updated()", 1) if (!childDevices) { createChildDevices() } else if (device.label != state.oldLabel) { childDevices.each { if (it.label == "${state.oldLabel} (R${channelNumber(it.deviceNetworkId)})") { def newLabel = "${device.displayName} (R${channelNumber(it.deviceNetworkId)})" it.setLabel(newLabel) } } state.oldLabel = device.label } 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: true) if (cmds != []) response(cmds) } private def logging(message, level) { if (logLevel != "0"){ switch (logLevel) { case "1": if (level > 1) log.debug "$message" break case "99": log.debug "$message" break } } } def parse(description) { //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"]}", 2) updateDataValue("mac", descMap["mac"]) } if (state.mac != null && state.dni != state.mac) state.dni = setDeviceNetworkId(state.mac) if (descMap["body"]) body = new String(descMap["body"].decodeBase64()) if (body && body != "") { if(body.startsWith("{") || body.startsWith("[")) { def slurper = new JsonSlurper() def result = slurper.parseText(body) logging("result: ${result}", 2) if (result.containsKey("type")) { if (result.type == "configuration") events << update_current_properties(result) if (result.type == "relay") parseRelay(result) if (result.type == "fan") { switch(result.speed) { case "0": events << createEvent(name: 'switch', value: 'off') events << createEvent(name: 'level', value: 0) break case "1": events << createEvent(name: 'switch', value: 'on') events << createEvent(name: 'level', value: 33) break case "2": events << createEvent(name: 'switch', value: 'on') events << createEvent(name: 'level', value: 66) break case "3": events << createEvent(name: 'switch', value: 'on') events << createEvent(name: 'level', value: 99) break default: log.debug result.speed break } } } if (result.containsKey("uptime")) { events << createEvent(name: 'uptime', value: result.uptime, displayed: false) } } else { //log.debug "Response is not JSON: $body" } } if (!device.currentValue("ip") || (device.currentValue("ip") != getDataValue("ip"))) events << createEvent(name: 'ip', value: getDataValue("ip")) return events } def parseRelay(cmd) { if (cmd.number != "0") { def childDevice = childDevices.find{it.deviceNetworkId == "$device.deviceNetworkId-ep$cmd.number"} if (childDevice) { childDevice.sendEvent(name: "switch", value: cmd.power) } } } 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()):""] } } def on() { log.debug "on()" def cmds = [] cmds << getAction("/fan1") return cmds } def off() { logging("off()", 1) def cmds = [] cmds << getAction("/fan0") return cmds } def setLevel(value) { def cmds = [] if (value <= 0) { cmds << getAction("/fan0") } else if (value > 0 && value < 33) { cmds << getAction("/fan1") } else if (value >= 33 && value < 66) { cmds << getAction("/fan2") } else { cmds << getAction("/fan3") } return cmds } def refresh() { logging("refresh()", 1) def cmds = [] cmds << getAction("/status") return cmds } void childOn(String dni) { logging("childOn($dni)", 1) def cmds = [] cmds << getAction("/on") sendHubCommand(cmds) } void childOff(String dni) { logging("childOff($dni)", 1) def cmds = [] cmds << getAction("/off") sendHubCommand(cmds) } void childRefresh(String dni) { logging("childRefresh($dni)", 1) } def ping() { logging("ping()", 1) refresh() } private getAction(uri){ updateDNI() def userpass //log.debug uri if(password != null && password != "") userpass = encodeCredentials("admin", password) def headers = getHeader(userpass) def hubAction = new physicalgraph.device.HubAction( method: "GET", path: uri, headers: headers ) return hubAction } private postAction(uri, data){ updateDNI() def userpass if(password != null && password != "") userpass = encodeCredentials("admin", password) def headers = getHeader(userpass) def hubAction = new physicalgraph.device.HubAction( method: "POST", path: uri, headers: headers, body: data ) return hubAction } private setDeviceNetworkId(ip, port = null){ def myDNI if (port == null) { myDNI = ip } else { def iphex = convertIPtoHex(ip) def porthex = convertPortToHex(port) myDNI = "$iphex:$porthex" } logging("Device Network Id set to ${myDNI}", 2) return myDNI } private updateDNI() { if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) { device.deviceNetworkId = state.dni } } private getHostAddress() { if (override == "true" && ip != null && ip != ""){ return "${ip}:80" } else if(getDeviceDataByName("ip") && getDeviceDataByName("port")){ return "${getDeviceDataByName("ip")}:${getDeviceDataByName("port")}" }else{ return "${ip}:80" } } private String convertIPtoHex(ipAddress) { String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() return hex } 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.encodeAsBase64().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 reboot() { logging("reboot()", 1) def uri = "/reboot" getAction(uri) } def sync(ip, port) { def existingIp = getDataValue("ip") def existingPort = getDataValue("port") if (ip && ip != existingIp) { updateDataValue("ip", ip) sendEvent(name: 'ip', value: ip) } if (port && port != existingPort) { updateDataValue("port", port) } } def generate_preferences(configuration_model) { def configuration = parseXml(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}", 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}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}", options: items break case ["password"]: input "${it.@index}", "password", title:"${it.@label}\n" + "${it.Help}", displayDuringSetup: "${it.@displayDuringSetup}" break case "decimal": input "${it.@index}", "decimal", title:"${it.@label}\n" + "${it.Help}", range: "${it.@min}..${it.@max}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break case "boolean": input "${it.@index}", "boolean", title:"${it.@label}\n" + "${it.Help}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break } } } } /* Code has elements from other community source @CyrilPeponnet (Z-Wave Parameter Sync). */ def update_current_properties(cmd) { def currentProperties = state.currentProperties ?: [:] currentProperties."${cmd.name}" = cmd.value if (settings."${cmd.name}" != null) { if (settings."${cmd.name}".toString() == cmd.value) { sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true) } else { sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true) } } state.currentProperties = currentProperties } def update_needed_settings() { def cmds = [] def currentProperties = state.currentProperties ?: [:] def configuration = parseXml(configuration_model()) def isUpdateNeeded = "NO" cmds << getAction("/configSet?name=haip&value=${device.hub.getDataValue("localIP")}") cmds << getAction("/configSet?name=haport&value=${device.hub.getDataValue("localSrvPortTCP")}") configuration.Value.each { if ("${it.@setting_type}" == "lan" && it.@disabled != "true"){ if (currentProperties."${it.@index}" == null) { if (it.@setonly == "true"){ logging("Setting ${it.@index} will be updated to ${it.@value}", 2) cmds << getAction("/configSet?name=${it.@index}&value=${it.@value}") } else { isUpdateNeeded = "YES" logging("Current value of setting ${it.@index} is unknown", 2) cmds << getAction("/configGet?name=${it.@index}") } } else if ((settings."${it.@index}" != null || it.@hidden == "true") && currentProperties."${it.@index}" != (settings."${it.@index}"? settings."${it.@index}".toString() : "${it.@value}")) { isUpdateNeeded = "YES" logging("Setting ${it.@index} will be updated to ${settings."${it.@index}"}", 2) cmds << getAction("/configSet?name=${it.@index}&value=${settings."${it.@index}"}") } } } sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) return cmds } private channelNumber(String dni) { dni.split("-ep")[-1] as Integer } private void createChildDevices() { state.oldLabel = device.label if ( device.deviceNetworkId =~ /^([0-9A-F]{2}){6}$/) { try { addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep1", null, [completedSetup: true, label: "${device.displayName} Light", isComponent: false]) } catch (e) { state.alertMessage = "Child device creation failed. Please make sure that the \"Switch Child Device\" is installed and published." runIn(2, "sendAlert") } } else { state.alertMessage = "Device has not yet been fully configured. Hit the configure button device tile and try again." runIn(2, "sendAlert") } } private sendAlert() { sendEvent( descriptionText: state.alertMessage, eventType: "ALERT", name: "childDeviceCreation", value: "failed", displayed: true, ) } def configuration_model() { ''' Default: Off Automatically turn the switch off after this many seconds. Range: 0 to 65536 Default: 0 (Disabled) ''' }