diff --git a/Drivers/aeon-dsc17103-micro-double-switch.src/aeon-dsc17103-micro-double-switch.groovy b/Drivers/aeon-dsc17103-micro-double-switch.src/aeon-dsc17103-micro-double-switch.groovy new file mode 100644 index 0000000..9004813 --- /dev/null +++ b/Drivers/aeon-dsc17103-micro-double-switch.src/aeon-dsc17103-micro-double-switch.groovy @@ -0,0 +1,463 @@ +/** + * + * Aeon DSC17103 Micro Double Switch + * + * Author: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2016-07-06 + * Copyright Eric Maycock + * + * Device Type supports all the feautres of the DSC17103 device including both switches, + * current energy consumption in W and cumulative energy consumption in kWh. + */ + +metadata { +definition (name: "Aeon DSC17103 Micro Double Switch", namespace: "erocm123", author: "Eric Maycock") { +capability "Switch" +capability "Polling" +capability "Configuration" +capability "Refresh" +capability "Energy Meter" +capability "Power Meter" +capability "Health Check" + +attribute "switch1", "string" +attribute "switch2", "string" +attribute "power1", "number" +attribute "energy1", "number" +attribute "power2", "number" +attribute "energy2", "number" + +command "on1" +command "off1" +command "on2" +command "off2" +command "reset" + +fingerprint mfr: "0086", prod: "0003", model: "0011" +fingerprint deviceId: "0x1001", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x25, 0x20, 0x27, 0x71, 0x2B, 0x2C, 0x75, 0x7A, 0x60, 0x32, 0x70" +} + +simulator { +status "on": "command: 2003, payload: FF" +status "off": "command: 2003, payload: 00" + +// reply messages +reply "2001FF,delay 100,2502": "command: 2503, payload: FF" +reply "200100,delay 100,2502": "command: 2503, payload: 00" +} + +tiles(scale: 2){ + + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + standardTile("switch1", "device.switch1",canChangeIcon: true, width: 2, height: 2) { + state "on", label: "switch1", action: "off1", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch1", action: "on1", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("switch2", "device.switch2",canChangeIcon: true, width: 2, height: 2) { + state "on", label: "switch2", action: "off2", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch2", action: "on2", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + 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.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + valueTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy1", "device.energy1", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("power1", "device.power1", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy2", "device.energy2", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("power2", "device.power2", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + + main(["switch","switch1", "switch2"]) + details(["switch", + "switch1", "switch2", "refresh", + "power", "energy", "reset", + "configure"]) +} + preferences { + input name: "parameter120", type: "enum", title: "Switch Type", defaultValue: "3", displayDuringSetup: true, required: false, options: [ + "0":"Momentary", + "1":"Toggle"] + input name: "parameter111", type: "number", title: "Power Meter Report (in seconds)", displayDuringSetup: false, required: false + input name: "parameter112", type: "number", title: "Energy Meter Report (in seconds)", displayDuringSetup: false, required: false + input name: "parameter90", type: "boolean", title: "Enable Selective Reporting?", description: "Only send energy report if below conditions are true.", displayDuringSetup: false, required: false + input name: "parameter91", type: "number", title: "Minimum W change in Wattage for report to be sent", displayDuringSetup: false, required: false + input name: "parameter92", type: "number", title: "Minimum % Change in Wattage for report to be sent", displayDuringSetup: false, required: false + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + + def statusTextmsg = "" + if (device.currentState('power') && device.currentState('energy')) statusTextmsg = "${device.currentState('power').value} W ${device.currentState('energy').value} kWh" + sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + log.debug "BasicReport ${cmd}" + /*def map = [ name: "switch" ] + + switch(cmd.value) { + case 0: + map.value = "off" + createEvent(map) + break + case 255: + map.value = "on" + createEvent(map) + break + }*/ +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd, ep=null) { + def result + def eName + def pName + def cmds = [] + if (ep) { + eName = "energy${ep}" + pName = "power${ep}" + } else { + eName = "energy" + pName = "power" + (1..2).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + } + if (cmd.scale == 0) { + result = createEvent(name: eName, value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + result = createEvent(name: eName, value: cmd.scaledMeterValue, unit: "kVAh") + } else { + result = createEvent(name: pName, value: cmd.scaledMeterValue, unit: "W") + } + cmds ? [result, response(delayBetween(cmds, 1000))] : result +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) +{ + log.debug "multichannelv3.MultiChannelCapabilityReport $cmd" + if (cmd.endPoint == 2 ) { + def currstate = device.currentState("switch2").getValue() + if (currstate == "on") + sendEvent(name: "switch2", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch2", value: "on", isStateChange: true, display: false) + } + else if (cmd.endPoint == 1 ) { + def currstate = device.currentState("switch1").getValue() + if (currstate == "on") + sendEvent(name: "switch1", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch1", value: "on", isStateChange: true, display: false) + } +} + +/*def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def map = [ name: "switch$cmd.sourceEndPoint" ] + + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand && cmd.commandClass == 50) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) + } else { + switch(cmd.commandClass) { + case 32: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + case 37: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + } + } +}*/ + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def map = [ name: "switch$cmd.sourceEndPoint" ] + + switch(cmd.commandClass) { + case 32: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + case 37: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + break + } + def events = [createEvent(map)] + if (map.value == "on") { + events += [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + (1..2).each { n -> + if (n != cmd.sourceEndPoint) { + if (device.currentState("switch${n}").value != "off") allOff = false + } + } + if (allOff) { + events += [createEvent([name: "switch", value: "off"])] + } + } + events +} + + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'" +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + return createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +def refresh() { + def cmds = [] + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + (1..2).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + delayBetween(cmds, 1000) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def poll() { + def cmds = [] + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + delayBetween(cmds, 1000) +} + +def reset() { + delayBetween([ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet().format() + ], 1000) +} + +def configure() { + log.debug "configure() called" + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [] + cmds << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format() + cmds << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format() + cmds << zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 101).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 102).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 80).format() + if (settings."parameter90" != null && settings."parameter90" == "true") { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 90, scaledConfigurationValue: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 90).format() + if (settings."parameter91" != null) + cmds << zwave.configurationV1.configurationSet(parameterNumber: 91, size: 2, scaledConfigurationValue: settings."parameter91".toInteger()).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 91).format() + if (settings."parameter92" != null) + cmds << zwave.configurationV1.configurationSet(parameterNumber: 92, size: 1, scaledConfigurationValue: settings."parameter92".toInteger()).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 92).format() + } else { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 90, scaledConfigurationValue: 0).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 90).format() + } + + if ( settings."parameter111" != null ) { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: (settings."parameter111".toInteger())).format() // Set switch to report values for both Relay1 and Relay2 + cmds << zwave.configurationV1.configurationGet(parameterNumber: 111).format() + } + if ( settings."parameter112" != null ) { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: (settings."parameter112".toInteger())).format() // Set switch to report values for both Relay1 and Relay2 + cmds << zwave.configurationV1.configurationGet(parameterNumber: 112).format() + } + if ( settings."parameter120" != null ) { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 120, size: 1, scaledConfigurationValue: (settings."parameter120".toInteger())).format() // Set switch to report values for both Relay1 and Relay2 + cmds << zwave.configurationV1.configurationGet(parameterNumber: 120).format() + } + + + if ( cmds != [] && cmds != null ) return delayBetween(cmds, 1000) else return +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + log.debug "Preferences have been changed. Attempting configure()" + def cmds = configure() + response(cmds) +} + +def on() { + delayBetween([ + zwave.switchAllV1.switchAllOn().format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} +def off() { + delayBetween([ + zwave.switchAllV1.switchAllOff().format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +def on1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[255]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def off1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[0]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def on2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[255]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +def off2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[0]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private valueCheck(number, value) { + switch (number) { + case 1: + return value / 5 + break + case 2: + return value / 10 + break + case 4: + return value + break + default: + return value + break + } +} + +def cmd2Integer(array) { +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break +} +} \ No newline at end of file diff --git a/Drivers/aeon-multisensor-6-advanced.src/9 Multisensor 6 V1.07 - ES.pdf b/Drivers/aeon-multisensor-6-advanced.src/9 Multisensor 6 V1.07 - ES.pdf new file mode 100644 index 0000000..1edb6a7 Binary files /dev/null and b/Drivers/aeon-multisensor-6-advanced.src/9 Multisensor 6 V1.07 - ES.pdf differ diff --git a/Drivers/aeon-multisensor-6-advanced.src/aeon-multisensor-6-advanced.groovy b/Drivers/aeon-multisensor-6-advanced.src/aeon-multisensor-6-advanced.groovy new file mode 100644 index 0000000..106768f --- /dev/null +++ b/Drivers/aeon-multisensor-6-advanced.src/aeon-multisensor-6-advanced.groovy @@ -0,0 +1,1068 @@ +/** + * + * Aeon Multisensor 6 (Advanced) + * + * github: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2017-03-08 6:45 PM + * Copyright Eric Maycock + * + * Code has elements from other community sources @CyrilPeponnet, @Robert_Vandervoort. Greatly reworked and + * optimized for improved battery life (hopefully) :) and ease of advanced configuration. I tried to get it + * as feature rich as possible. + * + * 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. + * + */ + + metadata { + definition (name: "Aeon Multisensor 6 (Advanced)", namespace: "erocm123", author: "Eric Maycock") { + capability "Motion Sensor" + capability "Acceleration Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Illuminance Measurement" + capability "Ultraviolet Index" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Refresh" + capability "Tamper Alert" + capability "Health Check" + + command "resetBatteryRuntime" + command "resetTamperAlert" + + attribute "needUpdate", "string" + + fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A,0x5A" // 1.07 & 1.08 Secure + fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A,0x5A" // 1.07 & 1.08 + + fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" // 1.06 + + fingerprint mfr:"0086", prod:"0102", model:"0064", deviceJoinName: "Aeon MultiSensor 6" + + } + 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()) + } + simulator { + } + tiles (scale: 2) { + multiAttributeTile(name:"temperature", type:"generic", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState "temperature", label:'${currentValue}°', icon:"st.motion.motion.inactive", backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + standardTile("motion","device.motion", inactiveLabel: false, width: 2, height: 2) { + state "inactive",label:'no motion',icon:"st.motion.motion.inactive",backgroundColor:"#ffffff" + state "active",label:'motion',icon:"st.motion.motion.active",backgroundColor:"#00a0dc" + } + + valueTile("humidity","device.humidity", inactiveLabel: false, width: 2, height: 2) { + state "humidity",label:'${currentValue} % RH' + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { + state "luminosity", label:'${currentValue} LUX', unit:"lux", + backgroundColors:[ + [value: 0, color: "#000000"], + [value: 1, color: "#060053"], + [value: 3, color: "#3E3900"], + [value: 12, color: "#8E8400"], + [value: 24, color: "#C5C08B"], + [value: 36, color: "#DAD7B6"], + [value: 128, color: "#F3F2E9"], + [value: 1000, color: "#FFFFFF"] + ] + } + + valueTile( + "ultravioletIndex","device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) { + state "ultravioletIndex",label:'${currentValue} UV INDEX',unit:"" + } + standardTile("acceleration", "device.acceleration", inactiveLabel: false, width: 2, height: 2) { + state("inactive", label:'clear', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label:'tamper', icon:"st.motion.acceleration.active", backgroundColor:"#f39c12") + } + standardTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state("clear", label:'clear', icon:"st.contact.contact.closed", backgroundColor:"#cccccc", action: "resetTamperAlert") + state("detected", label:'tamper', icon:"st.contact.contact.open", backgroundColor:"#e86d13", action: "resetTamperAlert") + } + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + valueTile("batteryTile", "device.batteryTile", inactiveLabel: false, width: 2, height: 2) { + state "batteryTile", label:'${currentValue}', unit:"" + } + valueTile( + "currentFirmware", "device.currentFirmware", inactiveLabel: false, width: 2, height: 2) { + state "currentFirmware", label:'Firmware: v${currentValue}', unit:"" + } + 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( + "batteryRuntime", "device.batteryRuntime", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "batteryRuntime", label:'Battery: ${currentValue} Double tap to reset counter', unit:"", action:"resetBatteryRuntime" + } + standardTile( + "statusText2", "device.statusText2", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "statusText2", label:'${currentValue}', unit:"", action:"resetBatteryRuntime" + } + + main([ + "temperature", "motion" + ]) + details([ + "temperature", + "humidity","illuminance","ultravioletIndex", + "motion","tamper","batteryTile", + "refresh", "configure", "statusText2", + ]) + } +} + +def parse(String description) +{ + def result = [] + switch(description){ + case ~/Err 106.*/: + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + break + case "updated": + result = createEvent( name: "Inclusion", value: "paired", isStateChange: true, + descriptionText: "Update is hit when the device is paired") + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 3600, nodeid:zwaveHubNodeId).format()) + result << response(zwave.batteryV1.batteryGet().format()) + result << response(zwave.versionV1.versionGet().format()) + result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + result << response(configure()) + break + default: + def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) + if (cmd) { + try { + result += zwaveEvent(cmd) + } catch (e) { + log.debug "error: $e cmd: $cmd description $description" + } + } + break + } + + if(state.batteryRuntimeStart != null){ + sendEvent(name:"batteryRuntime", value:getBatteryRuntime(), displayed:false) + if (device.currentValue('currentFirmware') != null){ + sendEvent(name:"statusText2", value: "Firmware: v${device.currentValue('currentFirmware')} - Battery: ${getBatteryRuntime()} Double tap to reset", displayed:false) + } else { + sendEvent(name:"statusText2", value: "Battery: ${getBatteryRuntime()} Double tap to reset", displayed:false) + } + } else { + state.batteryRuntimeStart = now() + } + + def statusTextmsg = "" + result.each { + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "humidity") { + statusTextmsg = "${it.value}% RH - ${device.currentValue('illuminance')? device.currentValue('illuminance') : "0%"} LUX - ${device.currentValue('ultravioletIndex')? device.currentValue('ultravioletIndex') : "0"} UV" + } + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "illuminance") { + statusTextmsg = "${device.currentValue('humidity')? device.currentValue('humidity') : "0"}% RH - ${it.value} LUX - ${device.currentValue('ultravioletIndex')? device.currentValue('ultravioletIndex') : "0"} UV" + } + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "ultravioletIndex") { + statusTextmsg = "${device.currentValue('humidity')? device.currentValue('humidity') : "0"}% RH - ${device.currentValue('illuminance')? device.currentValue('illuminance') : "0"} LUX - ${it.value} UV" + } + } + if (statusTextmsg != "") sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + if ( result[0] != null ) { result } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) + state.sec = 1 + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + response(configure()) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + if (cmd.parameterNumber.toInteger() == 81 && cmd.configurationValue == [255]) { + update_current_properties([parameterNumber: "81", configurationValue: [1]]) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '1'") + } else { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") + } +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + logging("WakeUpIntervalReport ${cmd.toString()}") + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + logging("Battery Report: $cmd") + def events = [] + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + if(settings."101" == null || settings."101" == "241") { + try { + events << createEvent([name: "batteryTile", value: "Battery ${map.value}%", displayed:false]) + } catch (e) { + logging("$e") + } + } + events << createEvent(map) + + state.lastBatteryReport = now() + return events +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + state.realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(state.realTemperature) + map.unit = getTemperatureScale() + logging("Temperature Report: $map.value") + break; + case 3: + map.name = "illuminance" + state.realLuminance = cmd.scaledSensorValue.toInteger() + map.value = getAdjustedLuminance(cmd.scaledSensorValue.toInteger()) + map.unit = "lux" + logging("Illuminance Report: $map.value") + break; + case 5: + map.name = "humidity" + state.realHumidity = cmd.scaledSensorValue.toInteger() + map.value = getAdjustedHumidity(cmd.scaledSensorValue.toInteger()) + map.unit = "%" + logging("Humidity Report: $map.value") + break; + case 27: + map.name = "ultravioletIndex" + state.realUV = cmd.scaledSensorValue.toInteger() + map.value = getAdjustedUV(cmd.scaledSensorValue.toInteger()) + map.unit = "" + logging("UV Report: $map.value") + break; + default: + map.descriptionText = cmd.toString() + } + + def request = update_needed_settings() + + if(request != []){ + return [response(commands(request)), createEvent(map)] + } else { + return createEvent(map) + } + +} + +def motionEvent(value) { + def map = [name: "motion"] + if (value) { + map.value = "active" + map.descriptionText = "$device.displayName detected motion" + } else { + map.value = "inactive" + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + logging("SensorBinaryReport: $cmd") + motionEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + logging("BasicSet: $cmd") + motionEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + logging("NotificationReport: $cmd") + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + //result << motionEvent(0) + result << createEvent(name: "tamper", value: "clear", descriptionText: "$device.displayName tamper cleared") + result << createEvent(name: "acceleration", value: "inactive", descriptionText: "$device.displayName tamper cleared", displayed:false) + break + case 3: + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered") + result << createEvent(name: "acceleration", value: "active", descriptionText: "$device.displayName was moved", displayed:false) + break + case 7: + //result << motionEvent(1) + break + } + } else { + logging("Need to handle this cmd.notificationType: ${cmd.notificationType}") + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + logging("Device ${device.displayName} woke up") + + def request = update_needed_settings() + + if(request != []){ + response(commands(request) + ["delay 5000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } else { + logging("No commands to send") + response([zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + logging(cmd) + if(cmd.applicationVersion && cmd.applicationSubVersion) { + def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}${location.getTemperatureScale() == 'C' ? 'EU':''}" + state.needfwUpdate = "false" + updateDataValue("firmware", firmware) + createEvent(name: "currentFirmware", value: firmware) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unknown Z-Wave Command: ${cmd.toString()}") +} + +def refresh() { + logging("$device.displayName refresh()") + + def request = [] + if (state.lastRefresh != null && now() - state.lastRefresh < 5000) { + logging("Refresh Double Press") + state.currentProperties."111" = null + state.wakeInterval = null + def configuration = new XmlSlurper().parseText(configuration_model()) + configuration.Value.each + { + if ( "${it.@setting_type}" == "zwave" ) { + request << zwave.configurationV1.configurationGet(parameterNumber: "${it.@index}".toInteger()) + } + } + request << zwave.versionV1.versionGet() + request << zwave.wakeUpV1.wakeUpIntervalGet() + } else { + request << zwave.batteryV1.batteryGet() + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:27, scale:1) + } + + state.lastRefresh = now() + + commands(request) +} + +def ping() { + logging("$device.displayName ping()") + + def request = [] + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:27, scale:1) + + commands(request) +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) commands(cmds) +} + +def updated() +{ + state.enableDebugging = settings.enableDebugging + sendEvent(name: "checkInterval", value: 6 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + logging("updated() is being called") + if(settings."101" != null && settings."101" == "240") { + sendEvent(name:"batteryTile", value: "USB Powered", displayed:false) + } else { + try { + sendEvent(name:"batteryTile", value: "Battery ${(device.currentValue("battery") == null ? '?' : device.currentValue("battery"))}%", displayed:false) + } catch (e) { + logging("$e") + sendEvent(name:"battery", value: "100", displayed:false) + sendEvent(name:"batteryTile", value: "Battery ${(device.currentValue("battery") == null ? '?' : device.currentValue("battery"))}%", displayed:false) + } + } + + state.needfwUpdate = "" + + if (state.realTemperature != null) sendEvent(name:"temperature", value: getAdjustedTemp(state.realTemperature)) + if (state.realHumidity != null) sendEvent(name:"humidity", value: getAdjustedHumidity(state.realHumidity)) + if (state.realLuminance != null) sendEvent(name:"illuminance", value: getAdjustedLuminance(state.realLuminance)) + if (state.realUV != null) sendEvent(name:"ultravioletIndex", value: getAdjustedUV(state.realUV)) + + def cmds = update_needed_settings() + + if (device.currentValue("battery") == null) cmds << zwave.batteryV1.batteryGet() + if (device.currentValue("temperature") == null) cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + if (device.currentValue("humidity") == null) cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1) + if (device.currentValue("illuminance") == null) cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5, scale:1) + if (device.currentValue("ultravioletIndex") == null) cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:27, scale:1) + + //updateStatus() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + response(commands(cmds)) +} + +def resetTamperAlert() { + sendEvent(name: "tamper", value: "clear", descriptionText: "$device.displayName tamper cleared") + sendEvent(name: "acceleration", value: "inactive", descriptionText: "$device.displayName tamper cleared") + sendEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") +} + +def convertParam(number, value) { + switch (number){ + case 41: + //Parameter difference between firmware versions + if (settings."41".toInteger() != null && device.currentValue("currentFirmware") != null) { + if (device.currentValue("currentFirmware") == "1.07" || device.currentValue("currentFirmware") == "1.08" || device.currentValue("currentFirmware") == "1.09") { + (value * 256) + 2 + } else if (device.currentValue("currentFirmware") == "1.10") { + (value * 65536) + 512 + } else if (device.currentValue("currentFirmware") == "1.10EU" || device.currentValue("currentFirmware") == "1.11EU") { + (value * 65536) + 256 + } else if (device.currentValue("currentFirmware") == "1.07EU" || device.currentValue("currentFirmware") == "1.08EU" || device.currentValue("currentFirmware") == "1.09EU") { + (value * 256) + 1 + } else { + value + } + } else { + value + } + break + case 45: + //Parameter difference between firmware versions + if (settings."45".toInteger() != null && device.currentValue("currentFirmware") != null && device.currentValue("currentFirmware") != "1.08") + 2 + else + value + break + case 101: + if (settings."40".toInteger() != null) { + if (settings."40".toInteger() == 1) { + 0 + } else { + value + } + } else { + 241 + } + break + case 201: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 202: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 203: + if (value < 0) + 65536 + value + else if (value > 1000) + value - 65536 + else + value + break + case 204: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + default: + value + break + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam("${cmd.parameterNumber}".toInteger(), settings."${cmd.parameterNumber}".toInteger()) == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(!state.needfwUpdate || state.needfwUpdate == "") { + logging("Requesting device firmware version") + cmds << zwave.versionV1.versionGet() + } + + if (state.currentProperties?."252" != [0]) { + logging("Unlocking configuration.") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(0, 1), parameterNumber: 252, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 252) + } + + if(state.wakeInterval == null || state.wakeInterval != getAdjustedWake()){ + logging("Setting Wake Interval to ${getAdjustedWake()}") + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: getAdjustedWake(), nodeid:zwaveHubNodeId) + cmds << zwave.wakeUpV1.wakeUpIntervalGet() + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + if (device.currentValue("currentFirmware") == null || "${it.@fw}".indexOf(device.currentValue("currentFirmware")) >= 0){ + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if (settings."${it.@index}" != null && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger())) + { + if (device.currentValue("currentFirmware") == null || "${it.@fw}".indexOf(device.currentValue("currentFirmware")) >= 0){ + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger())) + + if (it.@index == "41") { + if (device.currentValue("currentFirmware") == "1.06" || device.currentValue("currentFirmware") == "1.06EU") { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()), 2), parameterNumber: it.@index.toInteger(), size: 2) + } else if (device.currentValue("currentFirmware") == "1.10" || device.currentValue("currentFirmware") == "1.10EU" || device.currentValue("currentFirmware") == "1.11EU") { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()), 4), parameterNumber: it.@index.toInteger(), size: 4) + } else { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()), 3), parameterNumber: it.@index.toInteger(), size: 3) + } + } else { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()), it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + } + + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { +try { +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break +} +}catch (e) { +log.debug "Error: cmd2Integer $e" +} +} + +def integer2Cmd(value, size) { + try{ + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } + } catch (e) { + log.debug "Error: integer2Cmd $e Value: $value" + } +} + +private command(hubitat.zwave.Command cmd) { + + if (state.sec && cmd.toString() != "WakeUpIntervalGet()") { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 + } + } +} + +private getBatteryRuntime() { + def currentmillis = now() - state.batteryRuntimeStart + def days=0 + def hours=0 + def mins=0 + def secs=0 + secs = (currentmillis/1000).toInteger() + mins=(secs/60).toInteger() + hours=(mins/60).toInteger() + days=(hours/24).toInteger() + secs=(secs-(mins*60)).toString().padLeft(2, '0') + mins=(mins-(hours*60)).toString().padLeft(2, '0') + hours=(hours-(days*24)).toString().padLeft(2, '0') + + + if (days>0) { + return "$days days and $hours:$mins:$secs" + } else { + return "$hours:$mins:$secs" + } +} + +private getRoundedInterval(number) { + double tempDouble = (number / 60) + if (tempDouble == tempDouble.round()) + return (tempDouble * 60).toInteger() + else + return ((tempDouble.round() + 1) * 60).toInteger() +} + +private getAdjustedWake(){ + def wakeValue + if (device.currentValue("currentFirmware") != null && settings."101" != null && settings."111" != null){ + if (device.currentValue("currentFirmware") == "1.08"){ + if (settings."101".toInteger() == 241){ + if (settings."111".toInteger() <= 3600){ + wakeValue = getRoundedInterval(settings."111") + } else { + wakeValue = 3600 + } + } else { + wakeValue = 1800 + } + } else { + if (settings."101".toInteger() == 241){ + if (settings."111".toInteger() <= 3600){ + wakeValue = getRoundedInterval(settings."111") + } else { + wakeValue = getRoundedInterval(settings."111".toInteger() / 2) + } + } else { + wakeValue = 240 + } + } + } else { + wakeValue = 3600 + } + return wakeValue.toInteger() +} + +private getAdjustedTemp(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."201") { + return value = value + Math.round(settings."201" * 100) /100 + } else { + return value + } + +} + +private getAdjustedHumidity(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."202") { + return value = value + Math.round(settings."202" * 100) /100 + } else { + return value + } + +} + +private getAdjustedLuminance(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."203") { + return value = value + Math.round(settings."203" * 100) /100 + } else { + return value + } + +} + +private getAdjustedUV(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."204") { + return value = value + Math.round(settings."204" * 100) /100 + } else { + return value + } + +} + +def resetBatteryRuntime() { + if (state.lastReset != null && now() - state.lastReset < 5000) { + logging("Reset Double Press") + state.batteryRuntimeStart = now() + //updateStatus() + } + state.lastReset = now() +} + +private updateStatus(){ + def result = [] + if(state.batteryRuntimeStart != null){ + sendEvent(name:"batteryRuntime", value:getBatteryRuntime(), displayed:false) + if (device.currentValue('currentFirmware') != null){ + sendEvent(name:"statusText2", value: "Firmware: v${device.currentValue('currentFirmware')} - Battery: ${getBatteryRuntime()} Double tap to reset", displayed:false) + } else { + sendEvent(name:"statusText2", value: "Battery: ${getBatteryRuntime()} Double tap to reset", displayed:false) + } + } else { + state.batteryRuntimeStart = now() + } + + String statusText = "" + if(device.currentValue('humidity') != null) + statusText = "RH ${device.currentValue('humidity')}% - " + if(device.currentValue('illuminance') != null) + statusText = statusText + "LUX ${device.currentValue('illuminance')} - " + if(device.currentValue('ultravioletIndex') != null) + statusText = statusText + "UV ${device.currentValue('ultravioletIndex')} - " + + if (statusText != ""){ + statusText = statusText.substring(0, statusText.length() - 2) + sendEvent(name:"statusText", value: statusText, displayed:false) + } +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +def configuration_model() +{ +''' + + + +Is the device powered by battery or usb? + + + + + + +Enable/disable the selective reporting only when measurements reach a certain threshold or percentage set below. This is used to reduce network traffic. +Default: No (Enable for Better Battery Life) + + + + + + +Threshold change in temperature to induce an automatic report. +Range: 1~5000. +Default: 20 +Note: +Only used if selective reporting is enabled. +1. The unit is Fahrenheit for US version, Celsius for EU/AU version. +2. The value contains one decimal point. E.g. if the value is set to 20, the threshold value =2.0 ℃ (EU/AU version) or 2.0 ℉ (US version). When the current temperature gap is more then 2.0, which will induce a temperature report to be sent out. + + + + +Threshold change in humidity to induce an automatic report. +Range: 1~255. +Default: 10 +Note: +Only used if selective reporting is enabled. +1. The unit is %. +2. The default value is 10, which means that if the current humidity gap is more than 10%, it will send out a humidity report. + + + + +Threshold change in luminance to induce an automatic report. +Range: 1~30000. +Default: 100 +Note: +Only used if selective reporting is enabled. + + + + +Threshold change in battery level to induce an automatic report. +Range: 1~99. +Default: 10 +Note: +Only used if selective reporting is enabled. +1. The unit is %. +2. The default value is 10, which means that if the current battery level gap is more than 10%, it will send out a battery report. + + + + +Threshold change in ultraviolet to induce an automatic report. +Range: 1~11. +Default: 2 +Note: Firmware 1.06 and 1.07 only support a value of 2. +Only used if selective reporting is enabled. + + + + +Number of seconds to wait to report motion cleared after a motion event if there is no motion detected. +Range: 10~3600. +Default: 240 (4 minutes) +Note: +(1), The time unit is seconds if the value range is in 10 to 255. +(2), If the value range is in 256 to 3600, the time unit will be minute and its value should follow the below rules: +a), Interval time =Value/60, if the interval time can be divided by 60 and without remainder. +b), Interval time= (Value/60) +1, if the interval time can be divided by 60 and has remainder. + + + + +A value from 0-5, from disabled to high sensitivity +Range: 0~5 +Default: 5 + + + + +The interval time of sending reports in Report group 1 +Range: 30~ +Default: 3600 seconds +Note: +The unit of interval time is in seconds. Minimum interval time is 30 seconds when USB powered and 240 seconds (4 minutes) when battery powered. + + + + +Range: None +Default: 0 +Note: +1. The calibration value = standard value - measure value. +E.g. If measure value =85.3F and the standard value = 83.2F, so the calibration value = 83.2F - 85.3F = -2.1F. +If the measure value =60.1F and the standard value = 63.2F, so the calibration value = 63.2F - 60.1℃ = 3.1F. + + + + +Range: None +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 80RH and the standard value = 75RH, so the calibration value = 75RH – 80RH = -5RH. +If the measure value = 85RH and the standard value = 90RH, so the calibration value = 90RH – 85RH = 5RH. + + + + +Range: None +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 800Lux and the standard value = 750Lux, so the calibration value = 750 – 800 = -50. +If the measure value = 850Lux and the standard value = 900Lux, so the calibration value = 900 – 850 = 50. + + + + +Range: None +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 9 and the standard value = 8, so the calibration value = 8 – 9 = -1. +If the measure value = 7 and the standard value = 9, so the calibration value = 9 – 7 = 2. + + + + +Which command should be sent when the motion sensor is triggered +Default: Basic Set + + + + + + +Choose how the LED functions. (Option 1, 2 firmware v1.08+, Option 1, 2, 3 firmware v1.10+) +Default: Enabled + + + + + + + +Set the timeout of awake after the Wake Up CC is sent out. (Works on Firmware v1.08 only) +Range: 8~255 +Default: 30 +Note: May help if config parameters aren't making it before device goes back to sleep. + + + + + + + + +''' +} diff --git a/Drivers/aeon-multisensor.src/aeon-multisensor.groovy b/Drivers/aeon-multisensor.src/aeon-multisensor.groovy new file mode 100644 index 0000000..b296b21 --- /dev/null +++ b/Drivers/aeon-multisensor.src/aeon-multisensor.groovy @@ -0,0 +1,733 @@ +/* + * + * Uses some original code from @Duncan Aeon Multisensor 6 code for secure configuration, Copyright 2015 SmartThings, modified for setting + * preferences around configuration and the reporting of tampering and ultraviolet index, and reconfiguration after pairing + * + * 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. + * + */ + + metadata { + definition (name: "Aeon Multisensor", namespace: "erocm123", author: "Eric Maycock") { + capability "Motion Sensor" + capability "Acceleration Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Illuminance Measurement" + capability "Ultraviolet Index" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Refresh" + + // CCs supported - 94, 134, 114, 132, 89, 133, 115, 113, 128, 48, 49, 112, 152, 122 + attribute "tamper", "enum", ["detected", "clear"] + attribute "needUpdate", "string" + + fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" + } + preferences { + input description: "Once you change values on this page, the \"Synced\" Status will become \"Pending\" status.\ +You can then force the sync by triple clicking the device button or just wait for the\ +next WakeUp (60 minutes).", + + displayDuringSetup: false, type: "paragraph", element: "paragraph" + + generate_preferences(configuration_model()) + + input "tempOffset", "decimal", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + simulator { + status "no motion" : "command: 9881, payload: 00300300" + status "motion" : "command: 9881, payload: 003003FF" + status "clear" : " command: 9881, payload: 0071050000000007030000" + status "tamper" : "command: 9881, payload: 007105000000FF07030000" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new hubitat.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new hubitat.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, + precision: 1, + sensorType: 1, + scale: 1 + ) + ).incomingMessage() + } + for (int i = 0; i <= 100; i += 20) { + status "RH ${i}%": new hubitat.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new hubitat.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, + sensorType: 5 + ) + ).incomingMessage() + } + for (int i in [0, 1, 2, 8, 12, 16, 20, 24, 30, 64, 82, 100, 200, 500, 1000]) { + status "illuminance ${i} lux": new hubitat.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new hubitat.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, + sensorType: 3 + ) + ).incomingMessage() + } + for (int i = 0; i <= 11; i += 1) { + status "ultravioletultravioletIndex ${i}": new hubitat.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new hubitat.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, + sensorType: 27 + ) + ).incomingMessage() + } + for (int i in [0, 5, 10, 15, 50, 99, 100]) { + status "battery ${i}%": new hubitat.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new hubitat.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i + ) + ).incomingMessage() + } + status "low battery alert": new hubitat.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new hubitat.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: 255 + ) + ).incomingMessage() + status "wake up": "command: 8407, payload:" + } + tiles (scale: 2) { + multiAttributeTile(name:"main", type:"generic", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState "temperature",label:'${currentValue}°', icon:"st.motion.motion.inactive", backgroundColors:[ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState "humidity",label:'RH ${currentValue} %',unit:"" + } + } + standardTile("motion","device.motion", width: 2, height: 2) { + state "active",label:'motion',icon:"st.motion.motion.active",backgroundColor:"#53a7c0" + state "inactive",label:'no motion',icon:"st.motion.motion.inactive",backgroundColor:"#ffffff" + } + valueTile("temperature","device.temperature", width: 2, height: 2) { + state "temperature",label:'${currentValue}°',backgroundColors:[ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] + } + valueTile("humidity","device.humidity", width: 2, height: 2) { + state "humidity",label:'RH ${currentValue} %',unit:"" + } + valueTile( + "illuminance","device.illuminance", width: 2, height: 2) { + state "luminosity",label:'${currentValue} ${unit}', unit:"lux", backgroundColors:[ + [value: 0, color: "#000000"], + [value: 1, color: "#060053"], + [value: 3, color: "#3E3900"], + [value: 12, color: "#8E8400"], + [value: 24, color: "#C5C08B"], + [value: 36, color: "#DAD7B6"], + [value: 128, color: "#F3F2E9"], + [value: 1000, color: "#FFFFFF"] + ] + } + valueTile( + "ultravioletIndex","device.ultravioletIndex", width: 2, height: 2) { + state "ultravioletIndex",label:'${currentValue} UV INDEX',unit:"" + } + standardTile("acceleration", "device.acceleration", width: 2, height: 2) { + state("active", label:'tamper', icon:"st.motion.acceleration.active", backgroundColor:"#f39c12") + state("inactive", label:'clear', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + /*standardTile( + "tamper","device.tamper", width: 2, height: 2) { + state "tamper",label:'tamper',icon:"st.motion.motion.active",backgroundColor:"#ff0000" + state "clear",label:'clear',icon:"st.motion.motion.inactive",backgroundColor:"#00ff00" + }*/ + valueTile( + "battery", "device.battery", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("refresh", "device.switch", decoration: "flat", width: 2, height: 2) { + state "default", label:'Refresh', action:"refresh.refresh", icon:"st.secondary.refresh-icon" + } + standardTile("configure", "device.needUpdate", width: 2, height: 2) { + state("NO" , label:'Synced', action:"configuration.configure", icon:"st.motion.active", backgroundColor:"#8acb47") + state("YES", label:'Pending', action:"configuration.configure", icon:"st.motion.inactive", backgroundColor:"#f39c12") + } + main([ + "main", "motion" + ]) + details([ + "main","humidity","illuminance","ultravioletIndex","motion","acceleration","battery", "refresh", "configure" + ]) + } + +} + +def parse(String description) +{ + def result = [] + switch(description){ + case ~/Err 106.*/: + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + break + case "updated": + log.debug "Update is hit when the device is paired." + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 900, nodeid:zwaveHubNodeId).format()) + result << response(zwave.batteryV1.batteryGet().format()) + result << response(zwave.versionV1.versionGet().format()) + result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + result << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format()) + result << response(configure()) + break + default: + def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + break + } + //log.debug "Parsed '${description}' to ${result.inspect()}" + if ( result[0] != null ) { result } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + //log.debug "${cmd}" + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) + state.sec = 1 + //log.debug "encapsulated: ${encapsulatedCommand}" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + response(configure()) +} + +/** +* This will be called each time we update a paramter. Use it to fill our currents parameters as a callback +*/ +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'" +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "---CONFIGURATION REPORT V1--- ${device.displayName} parameter ${cmd.parameterNumber} with a byte size of ${cmd.size} is set to ${cmd.configurationValue}" +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = now() + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + state.realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(state.realTemperature) + map.unit = getTemperatureScale() + break; + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "lux" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + break; + case 27: + map.name = "ultravioletIndex" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "" + break; + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def motionEvent(value) { + def map = [name: "motion"] + if (value) { + map.value = "active" + map.descriptionText = "$device.displayName detected motion" + } else { + map.value = "inactive" + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + setConfigured() + motionEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + motionEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + result << motionEvent(0) + result << createEvent(name: "acceleration", value: "inactive", descriptionText: "$device.displayName tamper cleared") + break + case 3: + result << createEvent(name: "acceleration", value: "active", descriptionText: "$device.displayName was moved") + break + case 7: + result << motionEvent(1) + break + } + } else { + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +/** +* This is called each time your device will wake up. +*/ +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + //return zwave.configurationV1.configurationGet(parameterNumber: 3).format() + //zwave.wakeUpV1.wakeUpIntervalGet().format() + log.debug "Device ${device.displayName} woke up" + //zwave.wakeUpV2.wakeUpIntervalCapabilitiesGet().format() + def request = sync_properties() + //def commands = [] + request << zwave.wakeUpV2.wakeUpIntervalSet(seconds: 900, nodeid:zwaveHubNodeId) + //commands << zwave.wakeUpV2.wakeUpIntervalGet().format() + sendEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + // check if we need to request battery level (every 48h) + /*if (!state.lastBatteryReport || (now() - state.lastBatteryReport)/60000 >= 60 * 48) + { + commands << zwave.batteryV1.batteryGet().format() + }*/ + // Adding No More infomration needed at the end + //commands << zwave.wakeUpV1.wakeUpNoMoreInformation() + response(commands(request) + ["delay 15000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + log.debug "${cmd.toString()}" +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd) +{ + log.debug "Device ${device.displayName} woke up v2" +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + createEvent(descriptionText: cmd.toString(), isStateChange: false) + log.debug "Unknown Z-Wave Command" +} + +def refresh() { + log.debug "Aeon Multisensor 6 refresh()" + def request = [ + zwave.configurationV1.configurationGet(parameterNumber: 3), + zwave.configurationV1.configurationGet(parameterNumber: 4), + zwave.configurationV1.configurationGet(parameterNumber: 111), + zwave.configurationV1.configurationGet(parameterNumber: 201), + zwave.configurationV1.configurationGet(parameterNumber: 202), + zwave.configurationV1.configurationGet(parameterNumber: 203), + zwave.configurationV1.configurationGet(parameterNumber: 204), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5, scale:1), + ] + log.debug "${request}" + commands(request) + +} + +/** +* Configures the device to settings needed by SmarthThings at device discovery time. +* Need a triple click on B-button to zwave commands to pass +*/ +def configure() { + log.debug "Configuring Device For SmartThings Use" + def cmds = [] + + // Associate Group 3 Device Status (Group 1 is for Basic direct action -switches-, Group 2 for Tamper Alerts System -alarm-) + // Hub need to be Associate to group 3 + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]) + cmds += sync_properties() + commands(cmds) +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + log.debug "updated() is being called" + // Only used to toggle the status if update is needed + update_needed_settings() + //if (state.realTemperature) log.debug "Real Temperature: ${state.realTemperature} Adjusted Temperature: ${getAdjustedTemp(state.realTemperature)}" + if (state.realTemperature) sendEvent(name:"temperature", value: getAdjustedTemp(state.realTemperature)) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) +} + +/** +* Try to sync properties with the device +*/ +def sync_properties() +{ + def currentProperties = state.currentProperties ?: [:] + def configuration = new XmlSlurper().parseText(configuration_model()) + + def cmds = [] + configuration.Value.each + { + if (! currentProperties."${it.@index}" || currentProperties."${it.@index}" == null) + { + log.debug "Looking for current value of parameter ${it.@index}" + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + + if (device.currentValue("needUpdate") == "YES") { cmds += update_needed_settings() } + return cmds +} + +def convertParam(number, value) { + switch (number){ + case 201: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 202: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 203: + if (value < 0) + 65536 + value + else if (value > 1000) + value - 65536 + else + value + break + case 204: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + default: + value + break + } +} + +/** +* Update current cache properties +*/ +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + //log.debug "${cmd.configurationValue}" + def convertedConfigurationValue = convertParam("${cmd.parameterNumber}".toInteger(), cmd2Integer(cmd.configurationValue)) + //log.debug "${convertedConfigurationValue}" + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (settings."${cmd.parameterNumber}".toInteger() == convertedConfigurationValue) + { + sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true) + } + else + { + sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true) + } + } + + state.currentProperties = currentProperties +} + +/** +* Update needed settings +*/ +def update_needed_settings() +{ + def cmds = [] + def currentProperties = state.currentProperties ?: [:] + def configuration = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + configuration.Value.each + { + if (currentProperties."${it.@index}" == null) + { + log.debug "Current value of parameter ${it.@index} is unknown" + isUpdateNeeded = "YES" + } + else if (settings."${it.@index}" != null && convertParam(it.@index.toInteger(), cmd2Integer(currentProperties."${it.@index}")) != settings."${it.@index}".toInteger()) + { + isUpdateNeeded = "YES" + + log.debug "Parameter ${it.@index} will be updated to " + settings."${it.@index}" + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()) + switch(it.@byteSize) + { + case "1": + cmds << zwave.configurationV1.configurationSet(configurationValue: [convertedConfigurationValue], parameterNumber: it.@index.toInteger(), size: 1) + break + case "2": + def short valueLow = convertedConfigurationValue & 0xFF + def short valueHigh = (convertedConfigurationValue >> 8) & 0xFF + def value = [valueHigh, valueLow] + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, 2), parameterNumber: it.@index.toInteger(), size: 2) + break + case "4": + def short value1 = convertedConfigurationValue & 0xFF + def short value2 = (convertedConfigurationValue >> 8) & 0xFF + def short value3 = (convertedConfigurationValue >> 16) & 0xFF + def short value4 = (convertedConfigurationValue >> 24) & 0xFF + def value = [value4, value3, value2, value1] + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, 4), parameterNumber: it.@index.toInteger(), size: 4) + break + } + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + + return cmds +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break +} +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +private setConfigured() { + updateDataValue("configured", "true") +} + +private isConfigured() { + getDataValue("configured") == "true" +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +/** +* This function generate the preferences menu from the XML file +* each input will be accessible from settings map object. +*/ +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + input "${it.@index}", "number", + title:"${it.@index} - ${it.@label}\n" + "${it.Help}", + range: "${it.@min}..${it.@max}", + defaultValue: "${it.@value}" + break + case "list": + def items = [] + it.Item.each { items << ["${it.@value}":"${it.@label}"] } + input "${it.@index}", "enum", + title:"${it.@index} - ${it.@label}\n" + "${it.Help}", + defaultValue: "${it.@value}", + options: items + break + } + } +} + +/** +* Define the Aeon motion sensor model used to generate preference pane. +*/ +def configuration_model() +{ +''' + + + +Number of seconds to wait to report motion cleared after a motion event if there is no motion detected. +Range: 10~3600. +Default: 240 (4 minutes) +Note: +(1), The time unit is seconds if the value range is in 10 to 255. +(2), If the value range is in 256 to 3600, the time unit will be minute and its value should follow the below rules: +a), Interval time =Value/60, if the interval time can be divided by 60 and without remainder. +b), Interval time= (Value/60) +1, if the interval time can be divided by 60 and has remainder. + + + + +A value from 0-5, from disabled to high sensitivity +Range: 0~5 +Default: 5 + + + + +The interval time of sending reports in Report group 1 +Range: 5~ +Default: 3600 seconds +Note: +1. The unit of interval time is second if USB power. +2. If battery power, the minimum interval time is 60 minutes by default, for example, if the value is set to be more than 5 and less than 3600, the interval time is 60 minutes, if the value is set to be more than 3600 and less than 7200, the interval time is 120 minutes. You can also change the minimum interval time to 4 minutes via setting the interval value(3 bytes) to 240 in Wake Up Interval Set CC + + + + +Range: -100~100 +Default: 0 +Note: +1. The value contains one decimal point. E.g. if the value is set to 20, the calibration value is 2.0 F (EU/AU version) or 2.0 ℉(US version) +2. The calibration value = standard value - measure value. +E.g. If measure value =25.3℃ and the standard value = 23.2℃, so the calibration value = 23.2℃ - 25.3℃= -2.1℃. +If the measure value =30.1℃ and the standard value = 33.2℃, so the calibration value = 33.2℃ - 30.1℃=3.1℃. + + + + +Range: -50~50 +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 80RH and the standard value = 75RH, so the calibration value = 75RH – 80RH= -5RH. +If the measure value = 85RH and the standard value = 90RH, so the calibration value = 90RH – 85RH = 5RH. + + + + +Range: -1000~1000 +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 800Lux and the standard value = 750Lux, so the calibration value = 750 – 800 = -50. +If the measure value = 850Lux and the standard value = 900Lux, so the calibration value = 900 – 850 = 50. + + + + +Range: -10~10 +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 9 and the standard value = 8, so the calibration value = 8 – 9 = -1. +If the measure value = 7 and the standard value = 9, so the calibration value = 9 – 7 = 2. + + + +''' +} + +private getAdjustedTemp(value) { + + value = Math.round((value as Double) * 100) / 100 + //log.debug "Adjusted Temp: ${value}" + + if (tempOffset) { + //log.debug "Offset: ${Math.round(tempOffset * 100) /100}" + return value = value + Math.round(tempOffset * 100) /100 + } else { + return value + } + +} \ No newline at end of file diff --git a/Drivers/aeon-rgbw-bulb-advanced.src/aeon-rgbw-bulb-advanced.groovy b/Drivers/aeon-rgbw-bulb-advanced.src/aeon-rgbw-bulb-advanced.groovy new file mode 100644 index 0000000..aab0ec5 --- /dev/null +++ b/Drivers/aeon-rgbw-bulb-advanced.src/aeon-rgbw-bulb-advanced.groovy @@ -0,0 +1,883 @@ +/** + * Copyright 2016 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. + * + * Aeon RGBW Bulb (Advanced) + * + * Author: Eric Maycock (erocm123) + * Date: 2016-10-25 + */ + +metadata { + definition (name: "Aeon RGBW Bulb (Advanced)", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch Level" + capability "Color Control" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Actuator" + capability "Sensor" + capability "Configuration" + capability "Health Check" + + command "reset" + command "refresh" + + (1..6).each { n -> + attribute "switch$n", "enum", ["on", "off"] + command "on$n" + command "off$n" + } + + fingerprint mfr: "0086", prod: "0103", model: "0062", deviceJoinName: "Aeon RGBW Bulb" + fingerprint mfr: "0086", prod: "0103", model: "0079", deviceJoinName: "Aeon RGBW LED Strip" + + fingerprint deviceId: "0x1101", inClusters: "0x5E, 0x26, 0x33, 0x27, 0x2C, 0x2B, 0x70, 0x59, 0x85, 0x72, 0x86, 0x7A, 0x73, 0xEF, 0x5A, 0x82" + + } + + 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()) + + input description: "Create a custom program by modifying the settings below. This program can then be executed by using the \"custom\" button on the device page.", title: "Programs", displayDuringSetup: false, type: "paragraph", element: "paragraph" + + input "transition", "enum", title: "Transition", defaultValue: 0, displayDuringSetup: false, required: false, options: [ + 0:"Smooth", + 1:"Flash", + ] + input "brightness", "number", title: "Brightness (Firmware 1.05)", defaultValue: 1, displayDuringSetup: false, required: false, range: "1..99" + input "count", "number", title: "Cycle Count (0 [unlimited])", defaultValue: 0, displayDuringSetup: false, required: false, range: "0..254" + input "speed", "enum", title: "Color Change Speed", defaultValue: 0, displayDuringSetup: false, required: false, options: [ + 0:"Fast", + 1:"Medium Fast", + 2:"Medium", + 3:"Medium Slow", + 4:"Slow"] + input "speedLevel", "number", title: "Color Residence Time (1 [fastest], 254 [slowest])", defaultValue: "0", displayDuringSetup: true, required: false, range: "0..254" + (1..8).each { i -> + input "color$i", "enum", title: "Color $i", displayDuringSetup: false, required: false, options: [ + 1:"Red", + 2:"Orange", + 3:"Yellow", + 4:"Green", + 5:"Cyan", + 6:"Blue", + 7:"Violet", + 8:"Pink"] + } + } + + simulator { + } + + tiles (scale: 2){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } + + 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" + } + valueTile("colorTempTile", "device.colorTemperature", decoration: "flat", height: 2, width: 2) { + state "colorTemperature", label:'${currentValue}%', backgroundColor:"#ffffff" + } + controlTile("colorTempControl", "device.colorTemperature", "slider", decoration: "flat", height: 2, width: 4, inactiveLabel: false) { + state "colorTemperature", action:"setColorTemperature" + } + standardTile("switch1", "switch1", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "fade", action: "on1", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "fade", action: "off1", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch2", "switch2", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "flash", action: "on2", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "flash", action: "off2", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch3", "switch3", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "random", action: "on3", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "random", action: "off3", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch4", "switch4", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "police", action: "on4", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "police", action: "off4", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch5", "switch5", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "xmas", action: "on5", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "xmas", action: "off5", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch6", "switch6", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "custom", action: "on6", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "custom", action: "off6", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + valueTile( + "currentFirmware", "device.currentFirmware", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "currentFirmware", label:'Firmware: v${currentValue}', unit:"" + } + } + + main(["switch"]) + details(["switch", "levelSliderControl", + "colorTempControl", "colorTempTile", + "switch1", "switch2", "switch3", + "switch4", "switch5", "switch6", + "refresh", "configure", "currentFirmware" ]) +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + state.needfwUpdate = "" + + def cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + if (cmds != []) response(commands(cmds)) +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) commands(cmds) +} + + +def parse(description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3]) + if (cmd) { + result = zwaveEvent(cmd) + logging("'$cmd' parsed to $result") + } else { + logging("Couldn't zwave.parse '$description'") + } + } + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + //dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + if (cmd.value == 0) toggleTiles("all") + return result +} + +def zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) { + response(command(zwave.switchMultilevelV1.switchMultilevelGet())) +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x31: 5, 0x30: 2, 0x84: 1, 0x70: 1]) + state.sec = 1 + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + response(configure()) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + if (cmd.parameterNumber == 37) { + if (cmd.configurationValue[0] == 0) toggleTiles("all") + } else { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'") + } +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd){ + logging("Version Report ${cmd.toString()} ---- ${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2, '0')}") + state.needfwUpdate = "false" + updateDataValue("firmware", "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2, '0')}") + createEvent(name: "currentFirmware", value: "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2, '0')}") +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +private toggleTiles(value) { + def tiles = ["switch1", "switch2", "switch3", "switch4", "switch5", "switch6"] + tiles.each {tile -> + if (tile != value) { sendEvent(name: tile, value: "off") } + else { sendEvent(name:tile, value:"on"); sendEvent(name:"switch", value:"on") } + } +} + +def on() { + toggleTiles("all") + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.basicV1.basicGet(), + ]) +} + +def off() { + toggleTiles("all") + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.basicV1.basicGet(), + ]) +} + +def setLevel(level) { + toggleTiles("all") + setLevel(level, 1) +} + +def setLevel(level, duration) { + if(level > 99) level = 99 + commands([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), + zwave.basicV1.basicGet(), + ], (duration && duration < 12) ? (duration * 1000) : 3500) +} + +def refresh() { + commands([ + zwave.basicV1.basicGet(), + zwave.configurationV1.configurationGet(parameterNumber: 37), + zwave.manufacturerSpecificV2.manufacturerSpecificGet() + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def setSaturation(percent) { + logging("setSaturation($percent)") + setColor(saturation: percent) +} + +def setHue(value) { + logging("setHue($value)") + setColor(hue: value) +} + +private getEnableRandomHue(){ + switch (settings.enableRandom) { + case "0": + return 23 + break + case "1": + return 52 + break + case "2": + return 53 + break + case "3": + return 20 + break + default: + + break + } +} + +private getEnableRandomSat(){ + switch (settings.enableRandom) { + case "0": + return 56 + break + case "1": + return 19 + break + case "2": + return 91 + break + case "3": + return 80 + break + default: + + break + } +} + +def setColor(value) { + def result = [] + def warmWhite = 0 + def coldWhite = 0 + logging("setColor: ${value}") + if (value.hue && value.saturation) { + logging("setting color with hue & saturation") + def hue = (value.hue != null) ? value.hue : 13 + def saturation = (value.saturation != null) ? value.saturation : 13 + def rgb = huesatToRGB(hue as Integer, saturation as Integer) + if ( settings.enableRandom && value.hue == getEnableRandomHue() && value.saturation == getEnableRandomSat() ) { + Random rand = new Random() + int max = 100 + hue = rand.nextInt(max+1) + rgb = huesatToRGB(hue as Integer, saturation as Integer) + } + else if ( value.hue == 23 && value.saturation == 56 ) { + def level = 255 + if ( value.level != null ) level = value.level * 0.01 * 255 + warmWhite = level + coldWhite = 0 + rgb[0] = 0 + rgb[1] = 0 + rgb[2] = 0 + } + else { + if ( value.hue > 5 && value.hue < 100 ) hue = value.hue - 5 else hue = 1 + rgb = huesatToRGB(hue as Integer, saturation as Integer) + } + result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:warmWhite, coldWhite:coldWhite) + if(value.level != null && value.level != 1.0){ + if(value.level > 99) value.level = 99 + result << zwave.switchMultilevelV3.switchMultilevelSet(value: value.level, dimmingDuration: 3500) + result << zwave.switchMultilevelV3.switchMultilevelGet() + } + } + else if (value.hex) { + def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0) + } + result << zwave.basicV1.basicSet(value: 0xFF) + result << zwave.basicV1.basicGet() + if(value.hue) sendEvent(name: "hue", value: value.hue) + if(value.hex) sendEvent(name: "color", value: value.hex) + if(value.switch) sendEvent(name: "switch", value: value.switch) + if(value.saturation) sendEvent(name: "saturation", value: value.saturation) + + toggleTiles("all") + commands(result) +} + +def setColorTemperature(percent) { + if(percent > 99) percent = 99 + int warmValue = percent * 255 / 99 + toggleTiles("all") + sendEvent(name: "colorTemperature", value: percent) + command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite: (warmValue > 127 ? 255 : 0), coldWhite: (warmValue < 128? 255 : 0))) + +} + +def reset() { + logging("reset()") + sendEvent(name: "color", value: "#ffffff") + setColorTemperature(1) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def rgbToHSV(red, green, blue) { + float r = red / 255f + float g = green / 255f + float b = blue / 255f + float max = [r, g, b].max() + float delta = max - [r, g, b].min() + def hue = 13 + def saturation = 0 + if (max && delta) { + saturation = 100 * delta / max + if (r == max) { + hue = ((g - b) / delta) * 100 / 6 + } else if (g == max) { + hue = (2 + (b - r) / delta) * 100 / 6 + } else { + hue = (4 + (r - g) / delta) * 100 / 6 + } + } + [hue: hue, saturation: saturation, value: max * 100] +} + +def huesatToRGB(float hue, float sat) { + while(hue >= 100) hue -= 100 + int h = (int)(hue / 100 * 6) + float f = hue / 100 * 6 - h + int p = Math.round(255 * (1 - (sat / 100))) + int q = Math.round(255 * (1 - (sat / 100) * f)) + int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) + switch (h) { + case 0: return [255, t, p] + case 1: return [q, 255, p] + case 2: return [p, 255, t] + case 3: return [p, q, 255] + case 4: return [t, p, 255] + case 5: return [255, p, q] + } +} + +def on1() { + logging("on1()") + toggleTiles("switch1") + def cmds = [] + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") != "1.05") { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 16781342, parameterNumber: 37, size: 4) + } else { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 50332416, parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(157483073 , 4), parameterNumber: 37, size: 4) + } + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on2() { + logging("on2()") + toggleTiles("switch2") + def cmds = [] + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") != "1.05") { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 1090551813, parameterNumber: 37, size: 4) + } else { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 2560, parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(1097007169, 4), parameterNumber: 37, size: 4) + } + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on3() { + logging("on3()") + toggleTiles("switch3") + def cmds = [] + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") != "1.05") { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 50335754, parameterNumber: 37, size: 4) + } else { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 50332416, parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(191037440 , 4), parameterNumber: 37, size: 4) + } + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on4() { + logging("on4()") + toggleTiles("switch4") + def cmds = [] + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") != "1.05") { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 1633771873, parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 1113784321, parameterNumber: 37, size: 4) + } else { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(256, 4), parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(369098752, 4), parameterNumber: 39, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(1244790848, 4), parameterNumber: 37, size: 4) + } + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on5() { + logging("on5()") + toggleTiles("switch5") + def cmds = [] + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") != "1.05") { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 65, parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 1107300372, parameterNumber: 37, size: 4) + } else { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(768, 4), parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(335544320, 4), parameterNumber: 39, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(1244790784, 4), parameterNumber: 37, size: 4) + } + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on6() { + logging("on6()") + toggleTiles("switch6") + def cmds = [] + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") != "1.04") { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(calculateParameter(38), 4), parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: calculateParameter(37), parameterNumber: 37, size: 4) + } else { + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(calculateParameter(39), 4), parameterNumber: 39, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(calculateParameter(38), 4), parameterNumber: 38, size: 4) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(calculateParameter(37), 4), parameterNumber: 37, size: 4) + } + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def offCmd() { + logging("offCmd()") + def cmds = [] + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") != "1.05") { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 0, parameterNumber: 37, size: 4) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 37) + } else { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 2, parameterNumber: 36, size: 1) + cmds << zwave.basicV1.basicGet() + } + commands(cmds) +} + +def off1() { offCmd() } +def off2() { offCmd() } +def off3() { offCmd() } +def off4() { offCmd() } +def off5() { offCmd() } +def off6() { offCmd() } + +private calculateParameter(number) { + long value = 0 + switch (number){ + case 37: + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") == "1.04"){ + value += settings.transition ? settings.transition.toLong() * 1073741824 : 0 + value += 33554432 // Custom Mode + value += settings.count ? (settings.count.toLong() * 65536) : 0 + value += settings.speed ? (settings.speed.toLong() * 16 * 256) : 0 + value += settings.speedLevel ? settings.speedLevel.toLong() : 0 + } else { + value += settings.transition ? settings.transition.toLong() * 1073741824 : 0 + value += 134217728 // Allow smooth adjustable speed + value += 33554432 // Custom Mode + value += settings.brightness ? (settings.brightness.toLong() * 65536) : 0 + value += settings.count ? (settings.count.toLong() * 256) : 0 + value += settings.speed ? (2 * 32) : 0 + } + break + case 38: + if (device.currentValue("currentFirmware") == null || device.currentValue("currentFirmware") == "1.04"){ + value += settings.color1 ? (settings.color1.toLong() * 1) : 0 + value += settings.color2 ? (settings.color2.toLong() * 16) : 0 + value += settings.color3 ? (settings.color3.toLong() * 256) : 0 + value += settings.color4 ? (settings.color4.toLong() * 4096) : 0 + value += settings.color5 ? (settings.color5.toLong() * 65536) : 0 + value += settings.color6 ? (settings.color6.toLong() * 1048576) : 0 + value += settings.color7 ? (settings.color7.toLong() * 16777216) : 0 + value += settings.color8 ? (settings.color8.toLong() * 268435456) : 0 + } else { + value += settings.transition == "1" ? 0 : (settings.speed.toLong() * 2 * 16777216) // Speed from off to on + value += settings.transition == "1" ? 0 : (settings.speed.toLong() * 2 * 65536) // Speed from on to off + value += settings.speedLevel ? (settings.speedLevel.toLong() * 256) : 5 // Pause time at on + value += 0 // Pause time at off + } + break + case 39: + value += settings.color8 ? (settings.color8.toLong() * 1) : 0 + value += settings.color7 ? (settings.color7.toLong() * 16) : 0 + value += settings.color6 ? (settings.color6.toLong() * 256) : 0 + value += settings.color5 ? (settings.color5.toLong() * 4096) : 0 + value += settings.color4 ? (settings.color4.toLong() * 65536) : 0 + value += settings.color3 ? (settings.color3.toLong() * 1048576) : 0 + value += settings.color2 ? (settings.color2.toLong() * 16777216) : 0 + value += settings.color1 ? (settings.color1.toLong() * 268435456) : 0 + break + } + return value +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 != "" ? "${it.@label}\n" + "${it.Help}" : "" + "${it.Help}", + defaultValue: "${it.@value}", + displayDuringSetup: "${it.@displayDuringSetup}" + break + } + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(!state.needfwUpdate || state.needfwUpdate == ""){ + logging("Requesting device firmware version") + cmds << zwave.versionV1.versionGet() + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + if (device.currentValue("currentFirmware") == null || "${it.@fw}".indexOf(device.currentValue("currentFirmware")) >= 0){ + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if ((settings."${it.@index}" != null || "${it.@type}" == "hidden") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + case 28: + parValue = (value == "true" ? 1 : 0) + parValue += (settings."fc_2" == "true" ? 2 : 0) + parValue += (settings."fc_3" == "true" ? 4 : 0) + parValue += (settings."fc_4" == "true" ? 8 : 0) + break + case 29: + parValue = (value == "true" ? 1 : 0) + parValue += (settings."sc_2" == "true" ? 2 : 0) + parValue += (settings."sc_3" == "true" ? 4 : 0) + parValue += (settings."sc_4" == "true" ? 8 : 0) + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def configuration_model() +{ +''' + + + +Default state of the bulb when power is applied. +Range: 0~2 +Default: 0 (Previous) + + + + + + + +Enable or disable the sending of a report when the bulb color is changed. +Range: 0~1 +Default: 0 (Disabled) + + + + + + +Enable or disable manually toggle the LED bulb ON and OFF using an external switch. +Range: 0~1 +Default: 0 (Disabled) + + + + + + +Enable or disable the sending of a report when the state of the bulb is changed. +Range: 0~2 +Default: 2 (Basic CC Report) + + + + + + + +Range: 0~3 +Default: 0 (Parabolic) + + + + + + + + +If this option is enabled, using the selected color preset in SmartApps such as Smart Lighting will result in a random color. + + + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/aeon-smartstrip-6-switch.src/aeon-smartstrip-6-switch.groovy b/Drivers/aeon-smartstrip-6-switch.src/aeon-smartstrip-6-switch.groovy new file mode 100644 index 0000000..0eab65a --- /dev/null +++ b/Drivers/aeon-smartstrip-6-switch.src/aeon-smartstrip-6-switch.groovy @@ -0,0 +1,422 @@ +/** + * Copyright 2015 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. + * + */ +metadata { + definition (name: "Aeon SmartStrip - 6 Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Actuator" + capability "Sensor" + capability "Temperature Measurement" + + command "reset" + + (1..6).each { n -> + attribute "switch$n", "enum", ["on", "off"] + attribute "power$n", "number" + attribute "energy$n", "number" + command "on$n" + command "off$n" + command "reset$n" + } + + + fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x70,0x85,0x72,0x86,0x60", outClusters: "0x82" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "switch1 on": "command: 600D, payload: 01 00 25 03 FF" + status "switch1 off": "command: 600D, payload: 01 00 25 03 00" + status "switch4 on": "command: 600D, payload: 04 00 25 03 FF" + status "switch4 off": "command: 600D, payload: 04 00 25 03 00" + status "power": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + status "energy": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + status "power1": "command: 600D, payload: 0100" + new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).format() + status "energy2": "command: 600D, payload: 0200" + new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).format() + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + } + + preferences { + input("enableDebugging", "boolean", title:"Enable Debugging", value:false, required:false, displayDuringSetup:false) + } + + // tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + + valueTile("power", "device.power", decoration: "flat") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + valueTile("statusText", "statusText", inactiveLabel: false, width: 2, height: 2) { + state "statusText", label:'${currentValue}', backgroundColor:"#ffffff" + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "temperature", label:'${currentValue}', + backgroundColors: + [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + (1..4).each { n -> + standardTile("switch$n", "switch$n", canChangeIcon: true, width: 2, height: 2) { + state "on", label: "switch$n", action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: "switch$n", action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("power$n", "power$n", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy$n", "energy$n", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + } + (5..6).each { n -> + valueTile("outlet$n", "outlet$n", decoration: "flat", width: 2, height: 2) { + state "default", label:"outlet${n-4}" + } + valueTile("power$n", "power$n", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy$n", "energy$n", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + } + + main(["switch", "switch1", "switch2", "switch3", "switch4"]) + details(["switch", + "switch1","power1","energy1", + "switch2","power2","energy2", + "switch3","power3","energy3", + "switch4","power4","energy4", + "switch5","power5","energy5", + "switch6","power6","energy6", + "temperature", "refresh","reset", "configure"]) + } +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x60: 3, 0x32: 3, 0x25: 1, 0x20: 1]) + //log.debug "Command: ${cmd}" + if (cmd) { + result += zwaveEvent(cmd, null) + } + } + + def statusTextmsg = "" + if (device.currentState('power') && device.currentState('energy')) statusTextmsg = "${device.currentState('power').value} W ${device.currentState('energy').value} kWh" + sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + //log.debug "parsed '${description}' to ${result.inspect()}" + + result +} + +def endpointEvent(endpoint, map) { + logging("endpointEvent($endpoint, $map)") + if (endpoint) { + map.name = map.name + endpoint.toString() + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + if (encapsulatedCommand.commandClassId == 0x32) { + // Metered outlets are numbered differently than switches + Integer endpoint = cmd.sourceEndPoint + if (endpoint > 2) { + zwaveEvent(encapsulatedCommand, endpoint - 2) + } else if (endpoint == 0) { + zwaveEvent(encapsulatedCommand, 0) + } else if (endpoint == 1 || endpoint == 2) { + zwaveEvent(encapsulatedCommand, endpoint + 4) + } else { + log.debug "Ignoring metered outlet ${endpoint} msg: ${encapsulatedCommand}" + [] + } + } else { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, endpoint) { + logging("BasicReport") + def cmds = [] + (1..4).each { n -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), n) + cmds << "delay 1000" + } + + return response(cmds) +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint) { + logging("SwitchBinaryReport") + def map = [name: "switch", value: (cmd.value ? "on" : "off")] + def events = [endpointEvent(endpoint, map)] + def cmds = [] + if (!endpoint && events[0].isStateChange) { + events += (1..4).collect { ep -> endpointEvent(ep, map.clone()) } + cmds << "delay 3000" + cmds += delayBetween((1..4).collect { ep -> encap(zwave.meterV3.meterGet(scale: 2), ep) }) + } else { + if (events[0].value == "on") { + events += [endpointEvent(null, [name: "switch", value: "on"])] + } else { + def allOff = true + (1..4).each { n -> + if (n != endpoint) { + if (device.currentState("switch${n}").value != "off") allOff = false + } + } + if (allOff) { + events += [endpointEvent(null, [name: "switch", value: "off"])] + } + } + + } + if(cmds) events << response(cmds) + events +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd, ep) { + logging("MeterReport") + def event = [:] + def cmds = [] + if (cmd.scale < 2) { + def val = Math.round(cmd.scaledMeterValue*100)/100 + event = endpointEvent(ep, [name: "energy", value: val, unit: ["kWh", "kVAh"][cmd.scale]]) + } else { + event = endpointEvent(ep, [name: "power", value: (Math.round(cmd.scaledMeterValue * 100)/100), unit: "W"]) + } + + // check if we need to request temperature + if (!state.lastTempReport || (now() - state.lastTempReport)/60000 >= 5) + { + cmds << zwave.configurationV1.configurationGet(parameterNumber: 90).format() + cmds << "delay 400" + } + + cmds ? [event, response(cmds)] : event +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, ep) { + updateDataValue("MSR", String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)) + return null +} + +def zwaveEvent(hubitat.zwave.Command cmd, ep) { + logging("${device.displayName}: Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "")) +} + +def onOffCmd(value, endpoint = null) { + logging("onOffCmd($value, $endpoint)") + [ + encap(zwave.basicV1.basicSet(value: value), endpoint), + "delay 500", + encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint), + "delay 3000", + encap(zwave.meterV3.meterGet(scale: 2), endpoint) + ] +} + +def on() { onOffCmd(0xFF) } +def off() { onOffCmd(0x0) } + +def on1() { onOffCmd(0xFF, 1) } +def on2() { onOffCmd(0xFF, 2) } +def on3() { onOffCmd(0xFF, 3) } +def on4() { onOffCmd(0xFF, 4) } + +def off1() { onOffCmd(0, 1) } +def off2() { onOffCmd(0, 2) } +def off3() { onOffCmd(0, 3) } +def off4() { onOffCmd(0, 4) } + +def refresh() { + logging("refresh") + def cmds = [ + zwave.basicV1.basicGet().format(), + zwave.meterV3.meterGet(scale: 0).format(), + zwave.meterV3.meterGet(scale: 2).format(), + encap(zwave.basicV1.basicGet(), 1) // further gets are sent from the basic report handler + ] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), null) + (1..4).each { endpoint -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint) + } + (1..6).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + [90, 101, 102, 111, 112].each { p -> + cmds << zwave.configurationV1.configurationGet(parameterNumber: p).format() + } + delayBetween(cmds, 1000) +} + +def resetCmd(endpoint = null) { + logging("resetCmd($endpoint)") + delayBetween([ + encap(zwave.meterV2.meterReset(), endpoint), + encap(zwave.meterV2.meterGet(scale: 0), endpoint) + ]) +} + +def reset() { + logging("reset()") + delayBetween([resetCmd(null), reset1(), reset2(), reset3(), reset4(), reset5(), reset6()]) +} + +def reset1() { resetCmd(1) } +def reset2() { resetCmd(2) } +def reset3() { resetCmd(3) } +def reset4() { resetCmd(4) } +def reset5() { resetCmd(5) } +def reset6() { resetCmd(6) } + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("configure()") + def cmds = [ + // Configuration of what to include in reports and how often to send them (if the below "change" conditions are met + // Parameter 101 & 111: Send energy reports every 60 seconds (if conditions are met) + // Parameter 102 & 112: Send power reports every 15 seconds (if conditions are met) + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, configurationValue: [0,0,0,127]).format(), + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, configurationValue: [0,0,127,0]).format(), + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 60).format(), + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 15).format(), + ] + [5, 6, 7, 8, 9, 10, 11].each { p -> + // Send power reports at the time interval if they have changed by at least 1 watt + cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 2, scaledConfigurationValue: 1).format() + } + [12, 13, 14, 15, 16, 17, 18].each { p -> + // Send energy reports at the time interval if they have changed by at least 5% + cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 1, scaledConfigurationValue: 5).format() + } + cmds += [ + // Parameter 4: Induce automatic reports at the time interval if the above conditions are met to reduce network traffic + zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: 1).format(), + // Parameter 80: Enable to send automatic reports to devices in association group 1 + zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2).format(), + ] + + delayBetween(cmds, 1000) + "delay 5000" + refresh() +} + +def installed() { + logging("installed()") + configure() +} + +def updated() { + logging("updated()") + configure() +} + +private encap(cmd, endpoint) { + if (endpoint) { + if (cmd.commandClassId == 0x32) { + // Metered outlets are numbered differently than switches + if (endpoint == 5 || endpoint == 6) { + endpoint -= 4 + } + else if (endpoint < 0x80) { + endpoint += 2 + } else { + endpoint = ((endpoint & 0x7F) << 2) | 0x80 + } + } + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd, ep) { + def temperatureEvent + if (cmd.parameterNumber == 90) { + def temperature = convertTemp(cmd.configurationValue) + if(getTemperatureScale() == "C"){ + temperatureEvent = [name:"temperature", value: Math.round(temperature * 100) / 100] + } else { + temperatureEvent = [name:"temperature", value: Math.round(celsiusToFahrenheit(temperature) * 100) / 100] + } + state.lastTempReport = now() + } else { + //log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" + } + if (temperatureEvent) { + createEvent(temperatureEvent) + } +} + +def convertTemp(value) { + def highbit = value[0] + def lowbit = value[1] + + if (highbit > 127) highbit = highbit - 128 - 128 + lowbit = lowbit * 0.00390625 + + return highbit+lowbit +} + +private def logging(message) { + if (state.enableDebugging == "true") log.debug message +} \ No newline at end of file diff --git a/Drivers/aeon-smartstrip.src/aeon-smartstrip.groovy b/Drivers/aeon-smartstrip.src/aeon-smartstrip.groovy new file mode 100644 index 0000000..7f0ecd7 --- /dev/null +++ b/Drivers/aeon-smartstrip.src/aeon-smartstrip.groovy @@ -0,0 +1,433 @@ +/** + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Aeon SmartStrip", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Actuator" + capability "Sensor" + capability "Temperature Measurement" + capability "Health Check" + + command "reset" + + (1..4).each { n -> + attribute "switch$n", "enum", ["on", "off"] + attribute "power$n", "number" + attribute "energy$n", "number" + command "on$n" + command "off$n" + command "reset$n" + } + (5..6).each { n -> + attribute "power$n", "number" + attribute "energy$n", "number" + command "reset$n" + } + fingerprint mfr: "0086", prod: "0003", model: "000B" + fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x70,0x85,0x72,0x86,0x60", outClusters: "0x82" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "switch1 on": "command: 600D, payload: 01 00 25 03 FF" + status "switch1 off": "command: 600D, payload: 01 00 25 03 00" + status "switch4 on": "command: 600D, payload: 04 00 25 03 FF" + status "switch4 off": "command: 600D, payload: 04 00 25 03 00" + status "power": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + status "energy": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + status "power1": "command: 600D, payload: 0100" + new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).format() + status "energy2": "command: 600D, payload: 0200" + new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).format() + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + } + + preferences { + input("enableDebugging", "boolean", title:"Enable Debugging", value:false, required:false, displayDuringSetup:false) + } + + // tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + + valueTile("power", "device.power") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + valueTile("statusText", "statusText", inactiveLabel: false, width: 2, height: 2) { + state "statusText", label:'${currentValue}', backgroundColor:"#ffffff" + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}', + backgroundColors: + [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + (1..4).each { n -> + standardTile("switch$n", "switch$n", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "on", label: "switch$n", action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch$n", action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + } + valueTile("power$n", "power$n", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy$n", "energy$n", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + } + (5..6).each { n -> + standardTile("outlet$n", "outlet$n", width: 2, height: 2) { + state "default", label:"outlet${n-4}", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("power$n", "power$n", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy$n", "energy$n", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + } + + main(["switch", "switch1", "switch2", "switch3", "switch4"]) + details(["switch", + "switch1","power1","energy1", + "switch2","power2","energy2", + "switch3","power3","energy3", + "switch4","power4","energy4", + "outlet5","power5","energy5", + "outlet6","power6","energy6", + "temperature", "refresh","reset", "configure"]) + } +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x60: 3, 0x32: 3, 0x25: 1, 0x20: 1]) + //log.debug "Command: ${cmd}" + if (cmd) { + result += zwaveEvent(cmd, null) + } + } + + def statusTextmsg = "" + if (device.currentState('power') && device.currentState('energy')) statusTextmsg = "${device.currentState('power').value} W ${device.currentState('energy').value} kWh" + sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + //log.debug "parsed '${description}' to ${result.inspect()}" + + result +} + +def endpointEvent(endpoint, map) { + logging("endpointEvent($endpoint, $map)") + if (endpoint) { + map.name = map.name + endpoint.toString() + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + if (encapsulatedCommand.commandClassId == 0x32) { + // Metered outlets are numbered differently than switches + Integer endpoint = cmd.sourceEndPoint + if (endpoint > 2) { + zwaveEvent(encapsulatedCommand, endpoint - 2) + } else if (endpoint == 0) { + zwaveEvent(encapsulatedCommand, 0) + } else if (endpoint == 1 || endpoint == 2) { + zwaveEvent(encapsulatedCommand, endpoint + 4) + } else { + log.debug "Ignoring metered outlet ${endpoint} msg: ${encapsulatedCommand}" + [] + } + } else { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, endpoint) { + logging("BasicReport") + def cmds = [] + (1..4).each { n -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), n) + cmds << "delay 1000" + } + + return response(cmds) +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint) { + logging("SwitchBinaryReport") + def map = [name: "switch", value: (cmd.value ? "on" : "off")] + def events = [endpointEvent(endpoint, map)] + def cmds = [] + if (!endpoint && events[0].isStateChange) { + events += (1..4).collect { ep -> endpointEvent(ep, map.clone()) } + cmds << "delay 3000" + cmds += delayBetween((1..4).collect { ep -> encap(zwave.meterV3.meterGet(scale: 2), ep) }) + } else { + if (events[0].value == "on") { + events += [endpointEvent(null, [name: "switch", value: "on"])] + } else { + def allOff = true + (1..4).each { n -> + if (n != endpoint) { + if (device.currentState("switch${n}").value != "off") allOff = false + } + } + if (allOff) { + events += [endpointEvent(null, [name: "switch", value: "off"])] + } + } + + } + if(cmds) events << response(cmds) + events +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd, ep) { + logging("MeterReport") + def event = [:] + def cmds = [] + if (cmd.scale < 2) { + def val = Math.round(cmd.scaledMeterValue*100)/100 + event = endpointEvent(ep, [name: "energy", value: val, unit: ["kWh", "kVAh"][cmd.scale]]) + } else { + event = endpointEvent(ep, [name: "power", value: (Math.round(cmd.scaledMeterValue * 100)/100), unit: "W"]) + } + + // check if we need to request temperature + if (!state.lastTempReport || (now() - state.lastTempReport)/60000 >= 5) + { + cmds << zwave.configurationV1.configurationGet(parameterNumber: 90).format() + cmds << "delay 400" + } + + cmds ? [event, response(cmds)] : event +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, ep) { + updateDataValue("MSR", String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)) + return null +} + +def zwaveEvent(hubitat.zwave.Command cmd, ep) { + logging("${device.displayName}: Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "")) +} + +def onOffCmd(value, endpoint = null) { + logging("onOffCmd($value, $endpoint)") + [ + encap(zwave.basicV1.basicSet(value: value), endpoint), + "delay 500", + encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint), + "delay 3000", + encap(zwave.meterV3.meterGet(scale: 2), endpoint) + ] +} + +def on() { onOffCmd(0xFF) } +def off() { onOffCmd(0x0) } + +def on1() { onOffCmd(0xFF, 1) } +def on2() { onOffCmd(0xFF, 2) } +def on3() { onOffCmd(0xFF, 3) } +def on4() { onOffCmd(0xFF, 4) } + +def off1() { onOffCmd(0, 1) } +def off2() { onOffCmd(0, 2) } +def off3() { onOffCmd(0, 3) } +def off4() { onOffCmd(0, 4) } + +def refresh() { + logging("refresh") + def cmds = [ + zwave.basicV1.basicGet().format(), + zwave.meterV3.meterGet(scale: 0).format(), + zwave.meterV3.meterGet(scale: 2).format(), + encap(zwave.basicV1.basicGet(), 1) // further gets are sent from the basic report handler + ] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), null) + (1..4).each { endpoint -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint) + } + (1..6).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + [90, 101, 102, 111, 112].each { p -> + cmds << zwave.configurationV1.configurationGet(parameterNumber: p).format() + } + delayBetween(cmds, 1000) +} + +def ping() { + logging("ping") + return zwave.basicV1.basicGet().format() +} + +def resetCmd(endpoint = null) { + logging("resetCmd($endpoint)") + delayBetween([ + encap(zwave.meterV2.meterReset(), endpoint), + encap(zwave.meterV2.meterGet(scale: 0), endpoint) + ]) +} + +def reset() { + logging("reset()") + delayBetween([resetCmd(null), reset1(), reset2(), reset3(), reset4(), reset5(), reset6()]) +} + +def reset1() { resetCmd(1) } +def reset2() { resetCmd(2) } +def reset3() { resetCmd(3) } +def reset4() { resetCmd(4) } +def reset5() { resetCmd(5) } +def reset6() { resetCmd(6) } + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("configure()") + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [ + // Configuration of what to include in reports and how often to send them (if the below "change" conditions are met + // Parameter 101 & 111: Send energy reports every 60 seconds (if conditions are met) + // Parameter 102 & 112: Send power reports every 15 seconds (if conditions are met) + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, configurationValue: [0,0,0,127]).format(), + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, configurationValue: [0,0,127,0]).format(), + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 60).format(), + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 15).format(), + ] + [5, 6, 7, 8, 9, 10, 11].each { p -> + // Send power reports at the time interval if they have changed by at least 1 watt + cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 2, scaledConfigurationValue: 1).format() + } + [12, 13, 14, 15, 16, 17, 18].each { p -> + // Send energy reports at the time interval if they have changed by at least 5% + cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 1, scaledConfigurationValue: 5).format() + } + cmds += [ + // Parameter 4: Induce automatic reports at the time interval if the above conditions are met to reduce network traffic + zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: 1).format(), + // Parameter 80: Enable to send automatic reports to devices in association group 1 + zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2).format(), + ] + + delayBetween(cmds, 1000) + "delay 5000" + refresh() +} + +def installed() { + logging("installed()") + configure() +} + +def updated() { + logging("updated()") + configure() +} + +private encap(cmd, endpoint) { + if (endpoint) { + if (cmd.commandClassId == 0x32) { + // Metered outlets are numbered differently than switches + if (endpoint == 5 || endpoint == 6) { + endpoint -= 4 + } + else if (endpoint < 0x80) { + endpoint += 2 + } else { + endpoint = ((endpoint & 0x7F) << 2) | 0x80 + } + } + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd, ep) { + def temperatureEvent + if (cmd.parameterNumber == 90) { + def temperature = convertTemp(cmd.configurationValue) + if(getTemperatureScale() == "C"){ + temperatureEvent = [name:"temperature", value: Math.round(temperature * 100) / 100] + } else { + temperatureEvent = [name:"temperature", value: Math.round(celsiusToFahrenheit(temperature) * 100) / 100] + } + state.lastTempReport = now() + } else { + //log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" + } + if (temperatureEvent) { + createEvent(temperatureEvent) + } +} + +def convertTemp(value) { + def highbit = value[0] + def lowbit = value[1] + + if (highbit > 127) highbit = highbit - 128 - 128 + lowbit = lowbit * 0.00390625 + + return highbit+lowbit +} + +private def logging(message) { + if (state.enableDebugging == "true") log.debug message +} \ No newline at end of file diff --git a/Drivers/aeon-wallmote.src/aeon-wallmote.groovy b/Drivers/aeon-wallmote.src/aeon-wallmote.groovy new file mode 100644 index 0000000..3036657 --- /dev/null +++ b/Drivers/aeon-wallmote.src/aeon-wallmote.groovy @@ -0,0 +1,570 @@ + /** + * Copyright 2016 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. + * + * Aeon WallMote Dual/Quad + * + * Author: Eric Maycock (erocm123) + * Date: 2017-06-19 + * + * 2017-06-19: Added check to only send color change config for three wakeups. Editing preferences + * and hitting "done" will reset the counter. This is an attempt to prevent freezing + * caused by updating preferences. + */ + +metadata { + definition (name: "Aeon WallMote", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "PushableButton" + capability "HoldableButton" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Health Check" + + attribute "sequenceNumber", "number" + attribute "numberOfButtons", "number" + attribute "needUpdate", "string" + + fingerprint mfr: "0086", prod: "0102", model: "0082", deviceJoinName: "Aeon WallMote" + + fingerprint deviceId: "0x1801", inClusters: "0x5E,0x73,0x98,0x86,0x85,0x59,0x8E,0x60,0x72,0x5A,0x84,0x5B,0x71,0x70,0x80,0x7A", outClusters: "0x25,0x26" // secure inclusion + fingerprint deviceId: "0x1801", inClusters: "0x5E,0x85,0x59,0x8E,0x60,0x86,0x70,0x72,0x5A,0x73,0x84,0x80,0x5B,0x71,0x7A", outClusters: "0x25,0x26" + + } + 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()) + } + + simulator { + + } + tiles (scale: 2) { + multiAttributeTile(name:"button", type:"generic", width:6, height:4) { + tileAttribute("device.button", key: "PRIMARY_CONTROL"){ + attributeState "default", label:'', backgroundColor:"#ffffff", icon: "st.unknown.zwave.remote-controller" + } + tileAttribute ("device.battery", key: "SECONDARY_CONTROL") { + attributeState "battery", label:'${currentValue} % battery' + } + + } + valueTile( + "battery", "device.battery", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}%', unit:"" + } + valueTile( + "sequenceNumber", "device.sequenceNumber", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}', unit:"" + } + 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" + } + main "button" + details(["button", "battery", "sequenceNumber", "configure"]) + } +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1]) + if(cmd) results += zwaveEvent(cmd) + if(!results) results = [ descriptionText: cmd, displayed: false ] + } + + return results +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd) { + logging("upDown: $cmd.upDown") + + switch (cmd.upDown) { + case 0: // Up + buttonEvent(device.currentValue("numberOfButtons"), "pushed") + break + case 1: // Down + buttonEvent(device.currentValue("numberOfButtons"), "held") + break + default: + logging("Unhandled SwitchMultilevelStartLevelChange: ${cmd}") + break + } +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging("keyAttributes: $cmd.keyAttributes") + logging("sceneNumber: $cmd.sceneNumber") + logging("sequenceNumber: $cmd.sequenceNumber") + + sendEvent(name: "sequenceNumber", value: cmd.sequenceNumber, displayed:false) + switch (cmd.keyAttributes) { + case 0: + buttonEvent(cmd.sceneNumber, "pushed") + break + case 1: // released + if (!settings.holdMode || settings.holdMode == "2") buttonEvent(cmd.sceneNumber, "held") + break + case 2: // held + if (settings.holdMode == "1") buttonEvent(cmd.sceneNumber, "held") + break + default: + logging("Unhandled CentralSceneNotification: ${cmd}") + break + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x5B: 1, 0x20: 1, 0x31: 5, 0x30: 2, 0x84: 1, 0x70: 1]) + state.sec = 1 + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + response(configure()) +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + logging("WakeUpIntervalReport ${cmd.toString()}") + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + logging("Device ${device.displayName} woke up") + + def request = update_needed_settings() + + request << zwave.versionV1.versionGet() + + if (!state.lastBatteryReport || (now() - state.lastBatteryReport) / 60000 >= 60 * 24) + { + logging("Over 24hr since last battery report. Requesting report") + request << zwave.batteryV1.batteryGet() + } + + state.wakeCount? (state.wakeCount = state.wakeCount + 1) : (state.wakeCount = 2) + + if(request != []){ + response(commands(request) + ["delay 5000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } else { + logging("No commands to send") + response([zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } +} + +def buttonEvent(button, value) { + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true) +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + logging("Battery Report: $cmd") + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastBatteryReport = now() + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + logging("AssociationReport $cmd") + state."association${cmd.groupingIdentifier}" = cmd.nodeId[0] +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + if (state.MSR == "003B-6341-5044") { + updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}") + } + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: false) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unhandled zwaveEvent: ${cmd}") +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def installed() { + logging("installed()") + configure() +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + logging("updated() is being called") + state.wakeCount = 1 + def cmds = update_needed_settings() + sendEvent(name: "checkInterval", value: 2 * 60 * 12 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "numberOfButtons", value: settings.buttons ? (settings."3" == "1" ? settings.buttons.toInteger() + 1 : settings.buttons) : (settings."3" ? 4 + 1 : 4), displayed: true) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(cmds)) +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + cmds = update_needed_settings() + sendEvent(name: "numberOfButtons", value: settings.buttons ? (settings."3" == "1" ? settings.buttons.toInteger() + 1 : settings.buttons) : (settings."3" ? 4 + 1 : 4), displayed: true) + if (cmds != []) commands(cmds) +} + +def ping() { + logging("ping()") + logging("Battery Device - Not sending ping commands") +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 != "" ? "${it.@label}\n" + "${it.Help}" : "" + "${it.Help}", + defaultValue: "${it.@value}", + displayDuringSetup: "${it.@displayDuringSetup}" + break + } + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(state.wakeInterval == null || state.wakeInterval != 86400){ + logging("Setting Wake Interval to 86400") + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: 86400, nodeid:zwaveHubNodeId) + cmds << zwave.wakeUpV1.wakeUpIntervalGet() + } + + if(settings."3" == "1"){ + if(!state.association3 || state.association3 == "" || state.association3 == "1"){ + logging("Setting association group 3") + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:3) + } + if(!state.association5 || state.association5 == "" || state.association5 == "1"){ + logging("Setting association group 5") + cmds << zwave.associationV2.associationSet(groupingIdentifier:5, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:5) + } + if(!state.association7 || state.association7 == "" || state.association7 == "1"){ + logging("Setting association group 7") + cmds << zwave.associationV2.associationSet(groupingIdentifier:7, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:7) + } + if(!state.association9 || state.association9 == "" || state.association9 == "1"){ + logging("Setting association group 9") + cmds << zwave.associationV2.associationSet(groupingIdentifier:9, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:9) + } + } + + if(state.MSR == null){ + logging("Getting Manufacturer Specific Info") + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet() + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + if (it.@setonly == "true"){ + if (it.@index == 5) { + if (state.wakeCount <= 3) { + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } else { + logging ("Parameter has already sent. Will not send again until updated() gets called") + } + } else { + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } else { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if (settings."${it.@index}" != null && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}")) + { + isUpdateNeeded = "YES" + + if (it.@index == 5) { + if (state.wakeCount <= 3) { + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } else { + logging ("Parameter has already sent. Will not send again until updated() gets called") + } + } else { + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + long parValue + switch (number){ + case 5: + switch (value) { + case "1": + parValue = 4278190080 + break + case "2": + parValue = 16711680 + break + case "3": + parValue = 65280 + break + default: + parValue = value + break + } + break + default: + parValue = value.toLong() + break + } + return parValue +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { +long value + if (array != [255, 0, 0, 0]){ + switch(array.size()) { + case 1: + value = array[0] + break + case 2: + value = ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + value = ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + value = ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } + } else { + value = 4278190080 + } + return value +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value.toInteger()] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2.toInteger(), value1.toInteger()] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3.toInteger(), value2.toInteger(), value1.toInteger()] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4.toInteger(), value3.toInteger(), value2.toInteger(), value1.toInteger()] + break + } +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def configuration_model() +{ +''' + + + +Which model of WallMote is this? + + + + + + +Enable/disable the touch sound. +Default: Enable + + + + + + +Enable/disable the touch vibration. +Default: Enable + + + + + + +Enable/disable the function of button slide. +Default: Enable + + + + + + +To configure which color will be displayed when the button is pressed. +Default: Blue + + + + + + + +Multiple "held" events on botton hold? With this option, the controller will send a "held" event about every second while holding down a button. If set to No it will send a "held" event a single time when the button is released. +Default: No + + + + + + + + + +''' +} diff --git a/Drivers/alarm-com-smart-thermostat.src/alarm-com-smart-thermostat.groovy b/Drivers/alarm-com-smart-thermostat.src/alarm-com-smart-thermostat.groovy new file mode 100644 index 0000000..df58c88 --- /dev/null +++ b/Drivers/alarm-com-smart-thermostat.src/alarm-com-smart-thermostat.groovy @@ -0,0 +1,1230 @@ +/** + * Copyright 2016 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. + * + * Alarm.com Smart Thermostat ADC-T2000 / Building 36 Intelligent Thermostat B36-T10 + * + * Author: Eric Maycock (erocm123) + * Date: 2016-11-12 + * + * 2017-10-20: Removed parameter 26 "Power Source" as this seems to be read only. + * + */ + +metadata { + definition (name: "Alarm.com Smart Thermostat", namespace: "erocm123", author: "Eric Maycock") + { + capability "Refresh" + capability "Actuator" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Thermostat" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Health Check" + + command "setTemperature" + command "heatLevelUp" + command "heatLevelDown" + command "coolLevelUp" + command "coolLevelDown" + command "quickSetCool" + command "quickSetHeat" + command "modeoff" + command "modeheat" + command "modecool" + command "modeauto" + command "fanauto" + command "fanon" + command "fancir" + + attribute "thermostatFanState", "string" + attribute "currentState", "string" + attribute "currentMode", "string" + attribute "currentfanMode", "string" + attribute "needUpdate", "string" + + fingerprint mfr: "0190", prod: "0001", model: "0001" + } + + tiles(scale: 2) { + multiAttributeTile(name:"temperature", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal", backgroundColors:[ + // Celsius + [value: 0, color: "#153591"], + [value: 7, color: "#1e9cbb"], + [value: 15, color: "#90d2a7"], + [value: 23, color: "#44b621"], + [value: 28, color: "#f1d801"], + [value: 35, color: "#d04e00"], + [value: 37, color: "#bc2323"], + // Fahrenheit + [value: 40, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ]) + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("default", action: "setTemperature", label: "") + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}%', unit:"%") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + attributeState("cooling", backgroundColor:"#269bd2") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label:'${currentValue}') + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label:'${currentValue}') + } + + } + standardTile("thermostatOperatingState", "device.currentState", canChangeIcon: false, inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state ("default", label:'${currentValue}', icon:"st.tesla.tesla-hvac") + } + standardTile("thermostatFanState", "device.thermostatFanState", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "running", label:'Fan is On', icon:"st.Appliances.appliances11" + state "idle", label:'Fan is Off', icon:"st.Appliances.appliances11" + } + + standardTile("modeoff", "device.thermostatMode", width: 3, height: 2, inactiveLabel: false, decoration: "flat") { + state "off", label: '', action:"modeoff", icon:"st.thermostat.heating-cooling-off" + } + standardTile("modeheat", "device.thermostatMode", width: 3, height: 2, inactiveLabel: false, decoration: "flat") { + state "heat", label:'', action:"modeheat", icon:"st.thermostat.heat" + } + standardTile("modecool", "device.thermostatMode", width: 3, height: 2, inactiveLabel: false, decoration: "flat") { + state "cool", label:'', action:"modecool", icon:"st.thermostat.cool" + } + standardTile("modeauto", "device.thermostatMode", width: 3, height: 2, inactiveLabel: false, decoration: "flat") { + state "cool", label:'', action:"modeauto", icon:"st.thermostat.auto" + } + + standardTile("heatLevelUp", "device.heatingSetpoint", width: 1, height: 1, inactiveLabel: false, decoration: "flat") { + state "heatLevelUp", label:'', action:"heatLevelUp", icon:"st.thermostat.thermostat-up", backgroundColor:"#d04e00" + } + standardTile("heatLevelDown", "device.heatingSetpoint", width: 1, height: 1, inactiveLabel: false, decoration: "flat") { + state "heatLevelDown", label:'', action:"heatLevelDown", icon:"st.thermostat.thermostat-down", backgroundColor:"#d04e00" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false) { + state "heat", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 40, color: "#f49b88"], + [value: 50, color: "#f28770"], + [value: 60, color: "#f07358"], + [value: 70, color: "#ee5f40"], + [value: 80, color: "#ec4b28"], + [value: 90, color: "#ea3811"] + ] + } + controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 2, width: 4, inactiveLabel: false, range:"(60..90)") { + state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00" + } + + standardTile("coolLevelUp", "device.coolingSetpoint", width: 1, height: 1, inactiveLabel: false, decoration: "flat") { + state "coolLevelUp", label:'', action:"coolLevelUp", icon:"st.thermostat.thermostat-up", backgroundColor: "#1e9cbb" + } + standardTile("coolLevelDown", "device.coolingSetpoint", width: 1, height: 1, inactiveLabel: false, decoration: "flat") { + state "coolLevelDown", label:'', action:"coolLevelDown", icon:"st.thermostat.thermostat-down", backgroundColor: "#1e9cbb" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false) { + state "cool", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 40, color: "#88e1f4"], + [value: 50, color: "#70dbf2"], + [value: 60, color: "#58d5f0"], + [value: 70, color: "#40cfee"], + [value: 80, color: "#28c9ec"], + [value: 90, color: "#11c3ea"] + ] + } + controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 2, width: 4, inactiveLabel: false, range:"(60..90)") { + state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb" + } + + standardTile("fanauto", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "fanauto", label:'', action:"fanauto", icon:"st.thermostat.fan-auto" + } + standardTile("fanon", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "fanon", label:'', action:"fanon", icon:"st.thermostat.fan-on" + } + standardTile("fancir", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "fancir", label:'', action:"fancir", icon:"st.thermostat.fan-circulate" + } + + standardTile("modefan", "device.currentfanMode", width: 2, height: 2, canChangeIcon: false, inactiveLabel: false, decoration: "flat") { + state ("default", label:'${currentValue}', icon:"st.Appliances.appliances11") + } + standardTile("refresh", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", 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" + } + + valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + 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()) + } + + main "temperature" + details(["temperature", + "heatSliderControl", "heatingSetpoint", + "coolSliderControl", "coolingSetpoint", + "fanon", "fanauto", "fancir", + "modeoff", "modeheat", + "modecool", "modeauto", + "battery", "refresh", "configure"]) + } +} + +def parse(String description) +{ + def map = createEvent(zwaveEvent(zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3]))) + if (!map) { + return null + } + + if (map.name == "thermostatFanMode"){ + if (map.value == "fanAuto") { + sendEvent(name: "currentfanMode", value: "Auto Mode" as String) + } + if (map.value == "fanOn") { + sendEvent(name: "currentfanMode", value: "On Mode" as String) + } + if (map.value == "fanCirculate") { + sendEvent(name: "currentfanMode", value: "Cycle Mode" as String) + } + } + + def result = [] + result += map + if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) { + def map2 = [ + name: "thermostatSetpoint", + unit: getTemperatureScale() + ] + if (map.name == "thermostatMode") { + state.lastTriedMode = map.value + if (map.value == "cool") { + map2.value = device.latestValue("coolingSetpoint") + log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}" + } + else { + map2.value = device.latestValue("heatingSetpoint") + log.info "THERMOSTAT, latest heating setpoint = ${map2.value}" + } + } + else { + def mode = device.latestValue("thermostatMode") + log.info "THERMOSTAT, latest mode = ${mode}" + if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) { + map2.value = map.value + map2.unit = map.unit + } + } + if (map2.value != null) { + logging("THERMOSTAT, adding setpoint event: $map") + result += createEvent(map2) + } + } else if (map.name == "thermostatFanMode" && map.isStateChange) { + state.lastTriedFanMode = map.value + } + + if (!state.lastBatteryReport || (now() - state.lastBatteryReport) / 60000 >= 60 * 24) + { + logging("Over 24hr since last battery report. Requesting report") + result += response(commands(zwave.batteryV1.batteryGet())) + } + + logging("Parse returned $result") + return result +} + +def zwaveEvent(hubitat.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) +{ + def cmdScale = cmd.scale == 1 ? "F" : "C" + def map = [:] + map.value = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.displayed = false + switch (cmd.setpointType) { + case 1: + map.name = "heatingSetpoint" + break; + case 2: + map.name = "coolingSetpoint" + break; + default: + return [:] + } + // So we can respond with same format + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision + map +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) +{ + def map = [:] + if (cmd.sensorType == 1) { + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + } else if (cmd.sensorType == 5) { + map.value = cmd.scaledSensorValue + map.unit = "%" + map.name = "humidity" + } + map +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + logging("Battery Report: $cmd") + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastBatteryReport = now() + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) +{ + def map = [:] + switch (cmd.operatingState) { + case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: + map.value = "idle" + sendEvent(name: "currentState", value: "Idle" as String) + def mode = device.latestValue("thermostatMode") + if (mode == "off") { + sendEvent(name: "currentState", value: "Off" as String) + } + if (mode == "aux") { + sendEvent(name: "currentState", value: "in AUX/EM Mode and is idle" as String) + } + if (mode == "heat") { + sendEvent(name: "currentState", value: "in Heat Mode and is idle" as String) + } + if (mode == "cool") { + sendEvent(name: "currentState", value: "in A/C Mode and is idle" as String) + } + if (mode == "auto") { + sendEvent(name: "currentState", value: "in Auto Mode and is idle" as String) + } + break + case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING: + map.value = "heating" + sendEvent(name: "currentState", value: "Heating and is running" as String) + break + case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_COOLING: + map.value = "cooling" + sendEvent(name: "currentState", value: "Cooling and is running" as String) + break + case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY: + map.value = "fan only" + sendEvent(name: "currentState", value: "Fan Only Mode" as String) + break + case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT: + map.value = "pending heat" + sendEvent(name: "currentState", value: "Pending Heat Mode" as String) + break + case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL: + map.value = "pending cool" + sendEvent(name: "currentState", value: "Pending A/C Mode" as String) + break + case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER: + map.value = "vent economizer" + sendEvent(name: "currentState", value: "Vent Eco Mode" as String) + break + } + map.name = "thermostatOperatingState" + map +} + +def zwaveEvent(hubitat.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { + def map = [name: "thermostatFanState", unit: ""] + switch (cmd.fanOperatingState) { + case 0: + map.value = "idle" + break + case 1: + map.value = "running" + break + case 2: + map.value = "running high" + break + } + map +} + +def zwaveEvent(hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def map = [:] + switch (cmd.mode) { + case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: + map.value = "off" + sendEvent(name: "currentMode", value: "off" as String) + break + case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT: + map.value = "heat" + sendEvent(name: "currentMode", value: "heat" as String) + break + case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT: + map.value = "emergencyHeat" + sendEvent(name: "currentMode", value: "aux" as String) + break + case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: + map.value = "cool" + sendEvent(name: "currentMode", value: "cool" as String) + def displayMode = map.value + break + case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO: + map.value = "auto" + sendEvent(name: "currentMode", value: "auto" as String) + break + } + map.name = "thermostatMode" + map +} + +def zwaveEvent(hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { + def map = [:] + switch (cmd.fanMode) { + case hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: + map.value = "fanAuto" + sendEvent(name: "currentfanMode", value: "Auto Mode" as String) + break + case hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: + map.value = "fanOn" + sendEvent(name: "currentfanMode", value: "On Mode" as String) + break + case hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: + map.value = "fanCirculate" + sendEvent(name: "currentfanMode", value: "Cycle Mode" as String) + break + } + map.name = "thermostatFanMode" + map.displayed = false + map +} + +def zwaveEvent(hubitat.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { + def supportedModes = "" + if(cmd.off) { supportedModes += "off " } + if(cmd.heat) { supportedModes += "heat " } + if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergencyHeat " } + if(cmd.cool) { supportedModes += "cool " } + if(cmd.auto) { supportedModes += "auto " } + state.supportedModes = supportedModes +} + +def zwaveEvent(hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { + def supportedFanModes = "fanAuto fanOn fanCirculate " + state.supportedFanModes = supportedFanModes +} + +def updateState(String name, String value) { + state[name] = value + device.updateDataValue(name, value) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + logging("Zwave event received: $cmd") +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.warn "Unhandled zwave command $cmd" +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def heatLevelUp(){ + int nextLevel = device.currentValue("heatingSetpoint") + 1 + + if( nextLevel > 90){ + nextLevel = 90 + } + logging("Setting heat set point up to: ${nextLevel}") + setHeatingSetpoint(nextLevel) +} + +def heatLevelDown(){ + int nextLevel = device.currentValue("heatingSetpoint") - 1 + + if( nextLevel < 40){ + nextLevel = 40 + } + logging("Setting heat set point down to: ${nextLevel}") + setHeatingSetpoint(nextLevel) +} + +def quickSetHeat(degrees) { + setHeatingSetpoint(degrees, 2000) +} + +def setHeatingSetpoint(degrees, delay = 2000) { + setHeatingSetpoint(degrees.toDouble(), delay) +} + +def setHeatingSetpoint(Double degrees, Integer delay = 2000) { + log.trace "setHeatingSetpoint($degrees, $delay)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + def convertedDegrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } else { + convertedDegrees = degrees + } + state.heat = convertedDegrees + sendEvent(name:"heatingSetpoint", value: convertedDegrees) + if (device.currentValue("thermostatMode") == null || device.currentValue("thermostatMode") == "heat") { + commands([ + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) + ], delay) + } +} + +def coolLevelUp(){ + int nextLevel = device.currentValue("coolingSetpoint") + 1 + + if( nextLevel > 99){ + nextLevel = 99 + } + logging("Setting cool set point up to: ${nextLevel}") + setCoolingSetpoint(nextLevel) +} + +def coolLevelDown(){ + int nextLevel = device.currentValue("coolingSetpoint") - 1 + + if( nextLevel < 50){ + nextLevel = 50 + } + logging("Setting cool set point down to: ${nextLevel}") + setCoolingSetpoint(nextLevel) +} + +def quickSetCool(degrees) { + setCoolingSetpoint(degrees, 2000) +} + +def setCoolingSetpoint(degrees, delay = 2000) { + setCoolingSetpoint(degrees.toDouble(), delay) +} + +def setCoolingSetpoint(Double degrees, Integer delay = 2000) { + log.trace "setCoolingSetpoint($degrees, $delay)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + def convertedDegrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } else { + convertedDegrees = degrees + } + state.cool = convertedDegrees + sendEvent(name:"coolingSetpoint", value: convertedDegrees) + if (device.currentValue("thermostatMode") == null || device.currentValue("thermostatMode") == "cool") { + commands([ + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) + ], delay) + } +} + +def modeoff() { + logging("Setting thermostat mode to OFF.") + commands([ + zwave.thermostatModeV2.thermostatModeSet(mode: 0), + zwave.thermostatModeV2.thermostatModeGet() + ]) +} + +def modeheat() { + logging("Setting thermostat mode to HEAT.") + commands([ + zwave.thermostatModeV2.thermostatModeSet(mode: 1), + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: 1, precision: 1, scaledValue: state.heat), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1), + zwave.thermostatModeV2.thermostatModeGet() + ]) +} + +def modecool() { + logging("Setting thermostat mode to COOL.") + commands([ + zwave.thermostatModeV2.thermostatModeSet(mode: 2), + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: 1, precision: 1, scaledValue: state.cool), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2), + zwave.thermostatModeV2.thermostatModeGet() + ]) +} + +def modeauto() { + logging("Setting thermostat mode to AUTO.") + commands([ + zwave.thermostatModeV2.thermostatModeSet(mode: 3), + zwave.thermostatModeV2.thermostatModeGet() + ]) +} + +def modeemgcyheat() { + commands([ + zwave.thermostatModeV2.thermostatModeSet(mode: 4), + zwave.thermostatModeV2.thermostatModeGet() + ]) +} +def fanon() { + commands([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1), + zwave.thermostatFanModeV3.thermostatFanModeGet() + ]) +} + +def fanauto() { + commands([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0), + zwave.thermostatFanModeV3.thermostatFanModeGet() + ]) +} + +def fancir() { + commands([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6), + zwave.thermostatFanModeV3.thermostatFanModeGet() + ]) +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + if (cmds != []) response(commands(cmds, 2000)) +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + cmds << zwave.thermostatModeV2.thermostatModeSupportedGet() + cmds << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]) + + if(!device.currentValue("temperature") || device.currentValue("humidity")) cmds << zwave.sensorMultilevelV3.sensorMultilevelGet() // current temperature + if(!device.currentValue("coolingSetPoint")) cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) + if(!device.currentValue("heatingSetPoint")) cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) + if(!device.currentValue("thermostatMode")) cmds << zwave.thermostatModeV2.thermostatModeGet() + if(!device.currentValue("thermostatFanState")) cmds << zwave.thermostatFanStateV1.thermostatFanStateGet() + if(!device.currentValue("thermostatFanMode")) cmds << zwave.thermostatFanModeV3.thermostatFanModeGet() + if(!device.currentValue("thermostatOperatingState")) zwave.thermostatOperatingStateV1.thermostatOperatingStateGet() + if(!device.currentValue("battery")) cmds << zwave.batteryV1.batteryGet() + + if (cmds != []) commands(cmds, 2000) +} + +def refresh() { + logging("refresh()") + commands([ + zwave.sensorMultilevelV3.sensorMultilevelGet(), // current temperature + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2), + zwave.thermostatModeV2.thermostatModeGet(), + zwave.thermostatFanStateV1.thermostatFanStateGet(), + zwave.thermostatFanModeV3.thermostatFanModeGet(), + zwave.thermostatOperatingStateV1.thermostatOperatingStateGet(), + zwave.batteryV1.batteryGet(), + ], 2000) +} + +def ping() { + logging("ping()") + return commands(zwave.sensorMultilevelV3.sensorMultilevelGet()) +} + +private commands(commands, delay=2000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private getStandardDelay() { + 1000 +} + +def tempUp() { + log.debug "tempUp()" + def operMode = device.currentValue("thermostatMode") + def curTemp = device.currentValue("temperature").toInteger() + switch (operMode) { + case "heat": + setHeatingSetpoint(getHeatTemp().toInteger() + 1) + break; + case "cool": + setCoolingSetpoint(getCoolTemp().toInteger() + 1) + break; + case "auto": + if (settings.threshold == null || settings.threshold == "") settings.threshold = 70 + if (curTemp < settings.threshold) { + setHeatingSetpoint(getHeatTemp().toInteger() + 1) + } else { + setCoolingSetpoint(getCoolTemp().toInteger() + 1) + } + break; + default: + break; + } +} + +def tempDown() { + log.debug "tempDown" + def operMode = device.currentValue("thermostatMode") + def curTemp = device.currentValue("temperature").toInteger() + switch (operMode) { + case "heat": + setHeatingSetpoint(getHeatTemp().toInteger() - 1) + break; + case "cool": + setCoolingSetpoint(getCoolTemp().toInteger() - 1) + break; + case "auto": + if (settings.threshold == null || settings.threshold == "") settings.threshold = 70 + if (curTemp < settings.threshold) { + setHeatingSetpoint(getHeatTemp().toInteger() - 1) + } else { + setCoolingSetpoint(getCoolTemp().toInteger() - 1) + } + break; + default: + break; + } +} + +def setTemperature(value) { + def operMode = device.currentValue("thermostatMode") + def curTemp = device.currentValue("temperature").toInteger() + def newCTemp + def newHTemp + switch (operMode) { + case "heat": + (value < curTemp) ? (newHTemp = getHeatTemp().toInteger() - 1) : (newHTemp = getHeatTemp().toInteger() + 1) + setHeatingSetpoint(newHTemp.toInteger()) + break; + case "cool": + (value < curTemp) ? (newCTemp = getCoolTemp().toInteger() - 1) : (newCTemp = getCoolTemp().toInteger() + 1) + setCoolingSetpoint(newCTemp.toInteger()) + break; + case "auto": + if (settings.threshold == null || settings.threshold == "") settings.threshold = 70 + if (curTemp < settings.threshold) { + (value < curTemp) ? (newHTemp = getHeatTemp().toInteger() - 1) : (newHTemp = getHeatTemp().toInteger() + 1) + setHeatingSetpoint(newHTemp.toInteger()) + } else { + (value < curTemp) ? (newCTemp = getCoolTemp().toInteger() - 1) : (newCTemp = getCoolTemp().toInteger() + 1) + setCoolingSetpoint(newCTemp.toInteger()) + } + + /*def cmds = [] + cmds << setHeatingSetpoint(newHTemp.toInteger()) + cmds << "delay 1000" + cmds << setCoolingSetpoint(newCTemp.toInteger()) + return cmds*/ + break; + default: + break; + } +} + + +def getHeatTemp() { + try { return device.latestValue("heatingSetpoint") } + catch (e) { return 0 } +} + +def getCoolTemp() { + try { return device.latestValue("coolingSetpoint") } + catch (e) { return 0 } +} + +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 ["byte","short","four","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 "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 != "" ? "${it.@label}\n" + "${it.Help}" : "" + "${it.Help}", + defaultValue: "${it.@value}", + displayDuringSetup: "${it.@displayDuringSetup}" + break + } + } + } +} + +def convertParam(number, value) { + switch (number){ + case [5, 6, 7, 10, 15, 16, 17, 18, 31, 32, 33]: + (value * 256 + 704643072).toInteger() + break + default: + value.toInteger() + break + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + if (device.currentValue("currentFirmware") == null || "${it.@fw}".indexOf(device.currentValue("currentFirmware")) >= 0){ + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if ((settings."${it.@index}" != null || "${it.@type}" == "hidden") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def configuration_model() +{ +''' + + + +Range: 0 to 1 +Default: 0 (Normal) + + + + + + +Number of Heat Stages +Range: 0 to 3 +Default: 2 + + + + +Cool Stages +Range: 0 to 2 +Default: 2 + + + + +Range: 0-1 +Default: 1 (Electric) + + + + + + +Calibration Temperature Range (in deg. F) Precision is tenths of a degree. +Range: -100 to 100 +Default: 0 + + + + +Overshoot Range (in deg. F) Precision is tenths of a degree. +Range: 0 to 30 +Default: 5 + + + + +Swing Range (in deg. F) Precision is tenths of a degree. +Range: 0 to 30 +Default: 0 + + + + +Heat Staging Delay (in min) +Range: 1 to 60 +Default: 10 + + + + +Cool Staging Delay (in min) +Range: 1 to 60 +Default: 10 + + + + +Balance Setpont Range (in deg. F) Precision is tenths of a degree. +Range: 0 to 950 +Default: 350 + + + + +Range: 0 to 1 +Default: 0 (Comfort) + + + + + + +Fan Circulation Period (in min) +Range: 10 to 240 +Default: 20 + + + + +Duty Cycle (percentage) +Range: 0 to 100 +Default: 25 + + + + +Purge Time (in s) +Range: 1 to 3600 +Default: 60 + + + + +Max Heat Setpoint Range (in deg. F) Precision is tenths of a degree. +Range: 350 to 950 +Default: 950 + + + + +Min Heat Setpoint Range (in deg. F) Precision is tenths of a degree. +Range: 350 to 950 +Default: 350 + + + + +Max Cool Setpoint Range(in deg. F) Precision is tenths of a degree. +Range: 500 to 950 +Default: 950 + + + + +Min Cool Setpoint (in deg. F) Precision is tenths of a degree. +Range: 500 to 950 +Default: 500 + + + + +Range: 0 to 1 +Default: 0 (Disabled) + + + + + + +Compressor Delay (in min) +Range: 0 to 60 +Default: 5 + + + + +Demand Response Period (in min) +Range: 10 to 240 +Default: 10 + + + + +Demand Response Duty Cycle (percentage) +Range: 0 to 100 +Default: 25 + + + + +Range: 0 to 1 +Default: 1 (Farenheit) + + + + + + +Range: 3, 5, 7, 15, 31, 23, 19 +Default: 15 (Off, Heat, Cool, Auto) + + + + + + + + + + + +Range: 0 to 3 +Default: 0 (Disabled) + + + + + + + + +Range: 0 to 1 +Default: 0 (Battery) + + + + + + +Battery Alert Range (percentage) +Range: 0 to 100 +Default: 30 + + + + +Very Low Battery Alert Range (percentage) +Range: 0 to 100 +Default: 15 + + + + +Range: 0 to 1 +Default: 0 (Disabled) + + + + + + +Heat Differential (in deg. F) Precision is tenths of a degree. +Range: 10 to 100 +Default: 30 + + + + +Cool Differential (in deg. F) Precision is tenths of a degree. +Range: 10 to 100 +Default: 30 + + + + +Temperature Reporting Range (in deg. F) Precision is tenths of a degree. +Range: 5 to 20 +Default: 10 + + + + +Range: 0 to 1 +Default: 1 (O/B Terminal acts as O terminal, closed when heating) + + + + + + +Range: 0 to 1 +Default: 0 (Disabled) + + + + + + +Temperature in which you do not want your thermostat to heat above or cool below. Used for auto mode adjustments + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/carbon-dioxide-detector-child-device.src/carbon-dioxide-detector-child-device.groovy b/Drivers/carbon-dioxide-detector-child-device.src/carbon-dioxide-detector-child-device.groovy new file mode 100644 index 0000000..914e033 --- /dev/null +++ b/Drivers/carbon-dioxide-detector-child-device.src/carbon-dioxide-detector-child-device.groovy @@ -0,0 +1,31 @@ +/** + * Carbon Dioxide Detector Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Carbon Dioxide Detector Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Carbon Dioxide Measurement" + capability "Sensor" + } + + tiles() { + multiAttributeTile(name:"smoke", type: "generic", width: 6, height: 4){ + tileAttribute ("device.alarmState", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("carbonMonoxide", label:"dioxide", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13") + } + } + } + +} diff --git a/Drivers/carbon-monoxide-detector-child-device.src/carbon-monoxide-detector-child-device.groovy b/Drivers/carbon-monoxide-detector-child-device.src/carbon-monoxide-detector-child-device.groovy new file mode 100644 index 0000000..8e62bc0 --- /dev/null +++ b/Drivers/carbon-monoxide-detector-child-device.src/carbon-monoxide-detector-child-device.groovy @@ -0,0 +1,31 @@ +/** + * Carbon Monoxide Detector Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Carbon Monoxide Detector Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Carbon Monoxide Detector" + capability "Sensor" + } + + tiles() { + multiAttributeTile(name:"carbonMonoxide", type: "generic", width: 6, height: 4){ + tileAttribute ("device.carbonMonoxide", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:"monoxide", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13") + } + } + } + +} diff --git a/Drivers/contact-sensor-child-device.src/contact-sensor-child-device.groovy b/Drivers/contact-sensor-child-device.src/contact-sensor-child-device.groovy new file mode 100644 index 0000000..51b8f64 --- /dev/null +++ b/Drivers/contact-sensor-child-device.src/contact-sensor-child-device.groovy @@ -0,0 +1,31 @@ +/** + * Contact Sensor Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Contact Sensor Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Contact Sensor" + capability "Sensor" + } + + tiles() { + multiAttributeTile(name:"contact", type: "generic"){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + } + } + } + +} \ No newline at end of file diff --git a/Drivers/ecolink-firefighter.src/ecolink-firefighter.groovy b/Drivers/ecolink-firefighter.src/ecolink-firefighter.groovy new file mode 100644 index 0000000..2363f39 --- /dev/null +++ b/Drivers/ecolink-firefighter.src/ecolink-firefighter.groovy @@ -0,0 +1,303 @@ +/** + * 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. + * + * 2018-02-12: Added temperature capability and reading. Increased checkInterval to 6 hours. + */ + +metadata { + definition (name: "Ecolink Firefighter", namespace: "erocm123", author: "Eric Maycock") { + capability "Smoke Detector" + capability "Carbon Monoxide Detector" + capability "Sensor" + capability "Battery" + capability "Tamper Alert" + capability "Health Check" + capability "Temperature Measurement" + + attribute "alarmState", "string" + + fingerprint mfr:"014A", prod:"0005", model:"000F", deviceJoinName: "Ecolink Firefighter" + } + + simulator { + } + + preferences { + input "tempReportInterval", "enum", title: "Temperature Report Interval\n\nHow often you would like temperature reports to be sent from the sensor. More frequent reports will have a negative impact on battery life.\n", description: "Tap to set", required: false, options:[60: "1 Hour", 120: "2 Hours", 180: "3 Hours", 240: "4 Hours", 300: "5 Hours", 360: "6 Hours", 720: "12 Hours", 1440: "24 Hours"], defaultValue: 240 + input "tempOffset", "decimal", title: "Temperature Offset\n\nCalibrate reported temperature by applying a negative or positive offset\nRange: -10.0 to 10.0", description: "Tap to set", required: false, range: "-10..10" + } + + tiles (scale: 2){ + multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){ + tileAttribute ("device.alarmState", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("smoke", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + attributeState("carbonMonoxide", label:"MONOXIDE", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13") + attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + attributeState("detected", label:"TAMPERED", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + } + tileAttribute("device.temperature", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}°',icon: "") + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "smoke" + details(["smoke", "battery"]) + } +} + +def installed() { + sendEvent(name: "checkInterval", value: 24 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [] + createSmokeOrCOEvents("allClear", cmds) // allClear to set inital states for smoke and CO + cmds.each { cmd -> sendEvent(cmd) } +} + +def updated() { + sendEvent(name: "checkInterval", value: 24 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + if (state.realTemperature != null) sendEvent(name:"temperature", value: getAdjustedTemp(state.realTemperature)) +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results << createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + if (cmd) { + zwaveEvent(cmd, results) + } + } + //log.debug "'$description' parsed to ${results.inspect()}" + return results +} + +def createSmokeOrCOEvents(name, results) { + def text = null + switch (name) { + case "smoke": + text = "$device.displayName smoke was detected!" + // these are displayed:false because the composite event is the one we want to see in the app + results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false) + break + case "carbonMonoxide": + text = "$device.displayName carbon monoxide was detected!" + results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) + break + case "tested": + text = "$device.displayName was tested" + results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) + break + case "smokeClear": + text = "$device.displayName smoke is clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + case "carbonMonoxideClear": + text = "$device.displayName carbon monoxide is clear" + results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + case "allClear": + text = "$device.displayName all clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) + name = "clear" + break + case "testClear": + text = "$device.displayName test cleared" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) + name = "clear" + break + case "detected": + text = "$device.displayName covering was removed" + results << createEvent(name: "tamper", value: "detected", descriptionText: text, displayed: false) + name = "detected" + break + case "clear": + text = "$device.displayName covering was restored" + results << createEvent(name: "tamper", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + } + // This composite event is used for updating the tile + results << createEvent(name: "alarmState", value: name, descriptionText: text) +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd, results) +{ + log.debug cmd + state.wakeInterval = cmd.seconds + return results +} + +def zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd, results) { + log.debug cmd + if (cmd.zwaveAlarmType == hubitat.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_SMOKE) { + if (cmd.zwaveAlarmEvent == 3) { + createSmokeOrCOEvents("tested", results) + } else { + createSmokeOrCOEvents((cmd.zwaveAlarmEvent == 1 || cmd.zwaveAlarmEvent == 2) ? "smoke" : "smokeClear", results) + } + } else if (cmd.zwaveAlarmType == hubitat.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_CO) { + createSmokeOrCOEvents((cmd.zwaveAlarmEvent == 1 || cmd.zwaveAlarmEvent == 2) ? "carbonMonoxide" : "carbonMonoxideClear", results) + } else if (cmd.zwaveAlarmType == hubitat.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_BURGLAR) { + if (cmd.zwaveAlarmEvent == 0x00) { + createSmokeOrCOEvents("clear", results) + } else if (cmd.zwaveAlarmEvent == 0x03) { + createSmokeOrCOEvents("detected", results) + } + } else if (cmd.zwaveAlarmType == hubitat.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_POWER_MANAGEMENT) { + if (cmd.zwaveAlarmEvent == 0x0A) { + results << createEvent(descriptionText: "Replace Battery Soon", displayed: true) + } else if (cmd.zwaveAlarmEvent == 0x0B) { + results << createEvent(descriptionText: "Replace Battery Now", displayed: true) + } + } else switch(cmd.alarmType) { + case 1: + createSmokeOrCOEvents(cmd.alarmLevel ? "smoke" : "smokeClear", results) + break + case 2: + createSmokeOrCOEvents(cmd.alarmLevel ? "carbonMonoxide" : "carbonMonoxideClear", results) + break + case 12: // test button pressed + createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results) + break + case 13: // sent every hour -- not sure what this means, just a wake up notification? + if (cmd.alarmLevel == 255) { + results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false) + } else { + results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", isStateChange:true, displayed:false) + } + + // Clear smoke in case they pulled batteries and we missed the clear msg + if(device.currentValue("smoke") != "clear") { + createSmokeOrCOEvents("smokeClear", results) + } + + // Check battery if we don't have a recent battery event + if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) { + results << response(zwave.batteryV1.batteryGet()) + } + break + default: + results << createEvent(displayed: true, descriptionText: "Alarm $cmd.alarmType ${cmd.alarmLevel == 255 ? 'activated' : cmd.alarmLevel ?: 'deactivated'}".toString()) + break + } +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd, results) { + if (cmd.sensorType == hubitat.zwave.commandclasses.SensorBinaryV2.SENSOR_TYPE_SMOKE) { + createSmokeOrCOEvents(cmd.sensorValue ? "smoke" : "smokeClear", results) + } else if (cmd.sensorType == hubitat.zwave.commandclasses.SensorBinaryV2.SENSOR_TYPE_CO) { + createSmokeOrCOEvents(cmd.sensorValue ? "carbonMonoxide" : "carbonMonoxideClear", results) + } +} + +def zwaveEvent(hubitat.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, results) { + if (cmd.sensorType == 1) { + createSmokeOrCOEvents(cmd.sensorState ? "smoke" : "smokeClear", results) + } else if (cmd.sensorType == 2) { + createSmokeOrCOEvents(cmd.sensorState ? "carbonMonoxide" : "carbonMonoxideClear", results) + } + +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { + results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + results << response(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1).format()) + + if(state.wakeInterval == null || state.wakeInterval != (tempReportInterval? tempReportInterval.toInteger()*60:14400)){ + log.debug "Setting Wake Interval to ${tempReportInterval? tempReportInterval.toInteger()*60:14400}" + results << response([ + zwave.wakeUpV1.wakeUpIntervalSet(seconds: tempReportInterval? tempReportInterval.toInteger()*60:14400, nodeid:zwaveHubNodeId).format(), + "delay 1000", + zwave.wakeUpV1.wakeUpIntervalGet().format() + ]) + } + + if (!state.lastbatt || (now() - state.lastbatt) >= 24*60*60*1000) { + results << response([ + zwave.batteryV1.batteryGet().format(), + "delay 2000", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ]) + } else { + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd, results) +{ + log.debug cmd + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + state.realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(state.realTemperature) + map.unit = getTemperatureScale() + log.debug "Temperature Report: $map.value" + break; + default: + map.descriptionText = cmd.toString() + } + results << createEvent(map) +} + +private getAdjustedTemp(value) { + value = Math.round((value as Double) * 100) / 100 + if (tempOffset) { + return value = value + Math.round(tempOffset * 100) /100 + } else { + return value + } +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd, results) { + def map = [ name: "battery", unit: "%", isStateChange: true ] + state.lastbatt = now() + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName battery is low!" + } else { + map.value = cmd.batteryLevel + } + results << createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, results) { + def encapsulatedCommand = cmd.encapsulatedCommand([ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + state.sec = 1 + log.debug "encapsulated: ${encapsulatedCommand}" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, results) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + results << createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd, results) { + log.debug cmd + def event = [ displayed: false ] + event.linkText = device.label ?: device.name + event.descriptionText = "$event.linkText: $cmd" + results << createEvent(event) +} \ No newline at end of file diff --git a/Drivers/enerwave-8-button-scene-controller-zwn-sc8.src/enerwave-8-button-scene-controller-zwn-sc8.groovy b/Drivers/enerwave-8-button-scene-controller-zwn-sc8.src/enerwave-8-button-scene-controller-zwn-sc8.groovy new file mode 100644 index 0000000..9c8bac7 --- /dev/null +++ b/Drivers/enerwave-8-button-scene-controller-zwn-sc8.src/enerwave-8-button-scene-controller-zwn-sc8.groovy @@ -0,0 +1,213 @@ +/** + * Enerwave 8-Button Scene Controller ZWN-SC8 + * Copyright 2015 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. + * + */ + +metadata { + definition (name: "Enerwave 8-Button Scene Controller ZWN-SC8", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "PushableButton" + capability "Configuration" + capability "Sensor" + capability "Switch" + capability "Refresh" + + attribute "numberOfButtons", "number" + + (1..8).each { n -> + attribute "switch$n", "enum", ["on", "off"] + command "on$n" + command "off$n" + } + + fingerprint deviceId: "0x1801", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x85,0x59,0x5B,0x87,0x20,0x7A" + } + + simulator { + } + + tiles (scale: 2) { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + (1..8).each { n -> + standardTile("switch$n", "switch$n", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "switch$n", action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: "switch$n", action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + } + + main "button" + details(["switch1", "switch2", "button", + "switch3", "switch4", "configure", + "switch5", "switch6", "refresh", + "switch7", "switch8"]) + } + + preferences { + input name: "sendScene", type: "boolean", title:"Send button event when activating switch (1-8)", required:false, displayDuringSetup:true + input name: "enableDebugging", type: "boolean", title: "Enable Debug?", defaultValue: false, displayDuringSetup: false, required: false + } +} + +def parse(String description) { + def results = [] + //logging("${description}") + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1]) + if(cmd) results += zwaveEvent(cmd) + if(!results) results = [ descriptionText: cmd, displayed: false ] + } + + if(state.isConfigured != "true") configure() + + return results +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging("keyAttributes: $cmd.keyAttributes") + logging("sceneNumber: $cmd.sceneNumber") + logging("sequenceNumber: $cmd.sequenceNumber") + + sendEvent(name: "sequenceNumber", value: cmd.sequenceNumber, displayed:false) + buttonEvent(cmd.sceneNumber, "pushed") +} + +def zwaveEvent(hubitat.zwave.commands.indicatorv1.IndicatorReport cmd) { + logging("IndicatorReport: $cmd") + switch (cmd.value) { + case 1: + toggleTiles("switch1") + break + case 2: + toggleTiles("switch2") + break + case 4: + toggleTiles("switch3") + break + case 8: + toggleTiles("switch4") + break + case 16: + toggleTiles("switch5") + break + case 32: + toggleTiles("switch6") + break + case 64: + toggleTiles("switch7") + break + case 128: + toggleTiles("switch8") + break + default: + logging("Unhandled IndicatorReport: ${cmd}") + break + } +} + +def buttonEvent(button, value) { + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unhandled zwaveEvent: ${cmd}") +} + +private toggleTiles(value) { + def tiles = ["switch1", "switch2", "switch3", "switch4", "switch5", "switch6", "switch7", "switch8"] + tiles.each {tile -> + if (tile != value) { sendEvent(name: tile, value: "off") } + else { sendEvent(name:tile, value:"on"); sendEvent(name:"switch", value:"on") } + } +} + +def installed() { + logging("installed()") + configure() +} + +def updated() { + state.enableDebugging = settings.enableDebugging + logging("updated()") + configure() +} + +def configure() { + logging("configure()") + sendEvent(name: "numberOfButtons", value: 8, displayed: true) + state.isConfigured = "true" +} + +def onCmd(endpoint = null) { + logging("onCmd($endpoint)") + toggleTiles("switch$endpoint") + if (endpoint != null) { + if (sendScene == "true") sendEvent(name: "button", value: "pushed", data: [buttonNumber: endpoint], descriptionText: "$device.displayName button $endpoint was pushed", isStateChange: true) + zwave.indicatorV1.indicatorSet(value:(2.power(endpoint - 1))).format() + } else { + zwave.indicatorV1.indicatorSet(value:255).format() + } +} + +def offCmd(endpoint = null) { + logging("offCmd($value, $endpoint)") + if (endpoint != null) { + if (sendScene == "true") { + sendEvent(name: "button", value: "pushed", data: [buttonNumber: endpoint], descriptionText: "$device.displayName button $endpoint was pushed", isStateChange: true) + sendEvent(name: "switch$endpoint", value: "on", isStateChange: true) + } + zwave.indicatorV1.indicatorSet(value:(2.power(endpoint - 1))).format() + } else { + zwave.indicatorV1.indicatorSet(value:0).format() + } +} + +def on() { onCmd() } +def off() { offCmd() } + +def on1() { onCmd(1) } +def on2() { onCmd(2) } +def on3() { onCmd(3) } +def on4() { onCmd(4) } +def on5() { onCmd(5) } +def on6() { onCmd(6) } +def on7() { onCmd(7) } +def on8() { onCmd(8) } + +def off1() { offCmd(1) } +def off2() { offCmd(2) } +def off3() { offCmd(3) } +def off4() { offCmd(4) } +def off5() { offCmd(5) } +def off6() { offCmd(6) } +def off7() { offCmd(7) } +def off8() { offCmd(8) } + +def refresh() { + logging("refresh()") + zwave.indicatorV1.indicatorGet().format() +} + +private def logging(message) { + if (state.enableDebugging == "true") log.debug message +} \ No newline at end of file diff --git a/Drivers/enerwave-ceiling-mounted-motion-sensor.src/enerwave-ceiling-mounted-motion-sensor.groovy b/Drivers/enerwave-ceiling-mounted-motion-sensor.src/enerwave-ceiling-mounted-motion-sensor.groovy new file mode 100644 index 0000000..8d81e47 --- /dev/null +++ b/Drivers/enerwave-ceiling-mounted-motion-sensor.src/enerwave-ceiling-mounted-motion-sensor.groovy @@ -0,0 +1,256 @@ +/** + * + * 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. + * + * Enerwave Ceiling Mounted Motion Sensor + * + * Author: erocm123 (Eric Maycock) + * Date: 2017-06-18 + */ + +metadata { + definition (name: "Enerwave Ceiling Mounted Motion Sensor", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Motion Sensor" + capability "Sensor" + capability "Battery" + capability "Health Check" + + fingerprint mfr: "011A", prod: "0601", model: "0901", deviceJoinName: "Enerwave Motion Sensor" + } + + simulator { + } + + preferences { + input "parameter1", "enum", title: "Motion Inactivity Timeout", description: "Time (in seconds) that must elapse before the sensor reports inactivity", value:1, displayDuringSetup: false, options: [1:60, 2:120, 3:180, 4:240] + } + + tiles (scale: 2) { + multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ + tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00a0dc" + attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + } + tileAttribute ("battery", key: "SECONDARY_CONTROL") { + attributeState "battery", label:'${currentValue}% battery' + } + } + + main "motion" + details(["motion"]) + } +} + +def installed() { + log.debug "installed()" +// Device wakes up every 4 minutes (at most), this interval allows us to miss 4 wakeup notification before marking offline + sendEvent(name: "checkInterval", value: 4 * 4 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + return response(commands(updateConfiguration())) +} + +def updated() { + log.debug "updated()" +// Device wakes up every 60 seconds, this interval allows us to miss ten wakeup notification before marking offline + sendEvent(name: "checkInterval", value: 4 * 4 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def parse(String description) { + def result = null + log.debug description + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description) + } else { + def cmd = zwave.parse(description, [0x20: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + log.debug cmd + if (cmd) { + result = zwaveEvent(cmd) + } else { + result = createEvent(value: description, descriptionText: description, isStateChange: false) + } + } + return result +} + +def sensorValueEvent(value) { + if (value) { + createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") + } else { + createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + state."parameter${cmd.parameterNumber}" = cmd.configurationValue[0].toString() +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) +{ + def result = [] + if (cmd.notificationType == 0x07) { + if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice ensors + result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x01 || cmd.event == 0x02 || cmd.event == 0x07 || cmd.event == 0x08) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x00) { + result << sensorValueEvent(0) + } else if (cmd.event == 0x03) { + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true) + result << response(zwave.batteryV1.batteryGet()) + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, isStateChange: true, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + log.debug "WakeUpIntervalReport ${cmd.toString()}" + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + log.debug "Enerwave Motion Sensor Woke Up" + def cmds = updateConfiguration() + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + if (cmds) { + result += response(commands(updateConfiguration()) + "delay 3000") + } + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + result +} + +def updateConfiguration() { + def cmds = [] + if (state.parameter1 != (settings.parameter1? settings.parameter1 : "2")) { + log.debug "Updating Configuration Parameter 1 to ${settings.parameter1? settings.parameter1.toInteger() : 2}" + cmds << zwave.configurationV1.configurationSet(parameterNumber: 1, configurationValue:[(settings.parameter1? settings.parameter1.toInteger() : 2)]) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + } + if (state.wakeInterval != (settings.parameter1? settings.parameter1.toInteger() * 60 : 120)) { + log.debug "Updating Wake Interval to ${settings.parameter1? settings.parameter1.toInteger() * 60 : 120}" + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: settings.parameter1? settings.parameter1.toInteger() * 60 : 120, nodeid:zwaveHubNodeId) + cmds << zwave.wakeUpV1.wakeUpIntervalGet() + } + if(!state.association1){ + log.debug "Setting Association Group 1" + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + } + if (!state.lastbat || (new Date().time) - state.lastbat > 1000 * 60 * 60 * 6) { + log.debug "Battery Report Not Received for 6 hours. Requesting One Now" + cmds << zwave.batteryV1.batteryGet() + } + return cmds +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = new Date().time + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + map.unit = cmd.scale == 1 ? "F" : "C" + break; + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = cmd.scale == 0 ? "%" : "" + break; + case 0x1E: + map.name = "loudness" + map.unit = cmd.scale == 1 ? "dBA" : "dB" + break; + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + if (zwaveHubNodeId in cmd.nodeId) state."association${cmd.groupingIdentifier}" = true + else state."association${cmd.groupingIdentifier}" = false +} + +private command(hubitat.zwave.Command cmd) { + + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} \ No newline at end of file diff --git a/Drivers/enerwave-metering-switch-zw15rm-plus.src/enerwave-metering-switch-zw15rm-plus.groovy b/Drivers/enerwave-metering-switch-zw15rm-plus.src/enerwave-metering-switch-zw15rm-plus.groovy new file mode 100644 index 0000000..fe91847 --- /dev/null +++ b/Drivers/enerwave-metering-switch-zw15rm-plus.src/enerwave-metering-switch-zw15rm-plus.groovy @@ -0,0 +1,136 @@ +metadata { + // Automatically generated. Make future change here. + definition (name: "Enerwave Metering Switch ZW15RM-Plus", namespace: "erocm123", author: "Enerwave") { + capability "Energy Meter" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Polling" + capability "Refresh" + capability "Sensor" + + command "reset" + + fingerprint inClusters: "0x25,0x32" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("power", "device.power", decoration: "flat") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","power","energy","reset","configure","refresh"]) + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x20: 1, 0x32: 1]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.meterv1.MeterReport cmd) { + if (cmd.scale == 0) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } + else { + [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + [ + name: "switch", value: cmd.value ? "on" : "off", type: "physical" + ] +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + [ + name: "switch", value: cmd.value ? "on" : "off", type: "digital" + ] +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def poll() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def reset() { + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ] +} \ No newline at end of file diff --git a/Drivers/enerwave-rsm1-plus.src/enerwave-rsm1-plus.groovy b/Drivers/enerwave-rsm1-plus.src/enerwave-rsm1-plus.groovy new file mode 100644 index 0000000..f880a34 --- /dev/null +++ b/Drivers/enerwave-rsm1-plus.src/enerwave-rsm1-plus.groovy @@ -0,0 +1,171 @@ +/** + * + * 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. + * + */ +metadata { + definition (name: "Enerwave RSM1-Plus", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Health Check" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Configuration" + + fingerprint mfr: "011A", prod: "0101", model: "5605", deviceJoinName: "Enerwave RSM1-Plus" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + } + + // tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + } + + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + standardTile("configure", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + + main "switch" + details(["switch","refresh","configure"]) + } +} + +def updated(){ +// Device-Watch simply pings if no device events received for checkInterval duration of 122min = 2 * 60min + 2min lag time + sendEvent(name: "checkInterval", value: 15, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + response(configure()) +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x20: 1, 0x70: 1]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + log.debug "Was hailed: requesting state update" + } else { + log.debug "Parse returned ${result?.descriptionText}" + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "physical"] +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "physical"] +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "digital"] +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug cmd +} + +def zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) { + [name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false] +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + updateDataValue("manufacturer", cmd.manufacturerName) + createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + log.debug("AssociationReport $cmd") + state."association${cmd.groupingIdentifier}" = cmd.nodeId[0] + return +} + +def configure(){ + sendEvent(name: "checkInterval", value: 15, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [] + cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, configurationValue: [1]).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + if(!state.association2 || state.association2 == "" || state.association2 == "2"){ + log.debug("Setting association group 2") + cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() + cmds << zwave.associationV2.associationGet(groupingIdentifier:2).format() + } + if (cmds != []) return cmds +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def poll() { + log.debug "poll()" + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + ]) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping()" + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + ]) +} \ No newline at end of file diff --git a/Drivers/enerwave-rsm2-dual-relay-switch.src/enerwave-rsm2-dual-relay-switch.groovy b/Drivers/enerwave-rsm2-dual-relay-switch.src/enerwave-rsm2-dual-relay-switch.groovy new file mode 100644 index 0000000..fb57baf --- /dev/null +++ b/Drivers/enerwave-rsm2-dual-relay-switch.src/enerwave-rsm2-dual-relay-switch.groovy @@ -0,0 +1,273 @@ +/** + * + * Enerwave RSM2 Dual Relay Switch Device Type + * + * Author: Eric Maycock (erocm123) + * Date: 2016-03-25 + * + * Device Type supports all the feautres of the Enerwave RSM2 device including both switches, + * and the AllOn/AllOff functionality. + */ + +metadata { +definition (name: "Enerwave RSM2 Dual Relay Switch", namespace: "erocm123", author: "Eric Maycock") { +capability "Switch" +capability "Polling" +capability "Configuration" +capability "Refresh" +capability "Zw Multichannel" +capability "Health Check" + +attribute "switch1", "string" +attribute "switch2", "string" + +command "on1" +command "off1" +command "on2" +command "off2" + +fingerprint mfr: "011A", prod: "0101", model: "5606" +fingerprint deviceId: "0x1001", inClusters:"0x86, 0x72, 0x85, 0x60, 0x8E, 0x25, 0x20, 0x70, 0x27" + +} + +simulator { +status "on": "command: 2003, payload: FF" +status "off": "command: 2003, payload: 00" + +// reply messages +reply "2001FF,delay 100,2502": "command: 2503, payload: FF" +reply "200100,delay 100,2502": "command: 2503, payload: 00" +} + +tiles(scale: 2){ + + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + } + standardTile("switch1", "device.switch1",canChangeIcon: true, width: 2, height: 2) { + state "on", label: "switch1", action: "off1", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch1", action: "on1", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("switch2", "device.switch2",canChangeIcon: true, width: 2, height: 2) { + state "on", label: "switch2", action: "off2", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch2", action: "on2", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + 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.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + + main(["switch","switch1", "switch2"]) + details(["switch","switch1","switch2","refresh","configure"]) +} +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + // Not used +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd) { + def result + if (cmd.scale == 0) { + result = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + result = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else { + result = createEvent(name: "power", value: cmd.scaledMeterValue, unit: "W") + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) +{ + log.debug "multichannelv3.MultiChannelCapabilityReport $cmd" + if (cmd.endPoint == 2 ) { + def currstate = device.currentState("switch2").getValue() + if (currstate == "on") + sendEvent(name: "switch2", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch2", value: "on", isStateChange: true, display: false) + } + else if (cmd.endPoint == 1 ) { + def currstate = device.currentState("switch1").getValue() + if (currstate == "on") + sendEvent(name: "switch1", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch1", value: "on", isStateChange: true, display: false) + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def map = [ name: "switch$cmd.sourceEndPoint" ] + + switch(cmd.commandClass) { + case 32: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + case 37: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + break + } + def events = [createEvent(map)] + if (map.value == "on") { + events += [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + (1..2).each { n -> + if (n != cmd.sourceEndPoint) { + if (device.currentState("switch${n}").value != "off") allOff = false + } + } + if (allOff) { + events += [createEvent([name: "switch", value: "off"])] + } + } + events +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + return createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +def zwaveEvent(hubitat.zwave.commands.switchallv1.SwitchAllReport cmd) { + log.debug "SwitchAllReport $cmd" +} + +def refresh() { + def cmds = [] + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + delayBetween(cmds, 1000) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def poll() { + def cmds = [] + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + delayBetween(cmds, 1000) +} + +def configure() { + log.debug "configure() called" + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [] + cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, configurationValue: [1]).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + + return delayBetween(cmds, 2000) +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + log.debug "Preferences have been changed. Attempting configure()" + def cmds = configure() + response(cmds) +} + +def on() { + delayBetween([ + zwave.switchAllV1.switchAllOn().format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} +def off() { + delayBetween([ + zwave.switchAllV1.switchAllOff().format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +def on1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[255]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def off1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[0]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def on2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[255]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +def off2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[0]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} diff --git a/Drivers/enerwave-rsm2-plus-v5-11.src/enerwave-rsm2-plus-v5-11.groovy b/Drivers/enerwave-rsm2-plus-v5-11.src/enerwave-rsm2-plus-v5-11.groovy new file mode 100644 index 0000000..9f93fc5 --- /dev/null +++ b/Drivers/enerwave-rsm2-plus-v5-11.src/enerwave-rsm2-plus-v5-11.groovy @@ -0,0 +1,555 @@ +/** + * + * Enerwave RSM2-Plus v5.11 + * + * github: Eric Maycock (erocm123) + * Date: 2017-05-29 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ + +metadata { + definition (name: "Enerwave RSM2-Plus v5.11", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + fingerprint deviceId: "0x1001", inClusters:"0x5E,0x86,0x72,0x5A,0x73,0x20,0x27,0x25,0x32,0x60,0x85,0x8E,0x59,0x70", outClusters:"0x20" + } + + 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{ + 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" + } + } + 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" + } + + main(["switch"]) + details(["switch", + childDeviceTiles("all"), + "refresh","configure","reset" + ]) + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + logging("Parsed ${cmd} to ${result.inspect()}", 1) + } else { + logging("Non-parsed event: ${description}", 2) + } + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep=null) +{ + logging("BasicReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + childDevices.each { childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + logging("BasicSet ${cmd}", 2) + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=null) +{ + logging("SwitchBinaryReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + def childDevice = childDevices.find{it.deviceNetworkId == "$device.deviceNetworkId-ep$ep"} + if (childDevice) + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { n -> + if (n.currentState("switch")?.value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + logging("MultiChannelCmdEncap ${cmd}", 2) + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport ${cmd}", 2) + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr", 2) + updateDataValue("MSR", msr) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + logging("Unhandled Event: ${cmd}", 2) +} + +def on() { + logging("on()", 1) + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +def off() { + logging("off()", 1) + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +void childOn(String dni) { + logging("childOn($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childOff(String dni) { + logging("childOff($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childRefresh(String dni) { + logging("childRefresh($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 0), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 2), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +def poll() { + logging("poll()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def refresh() { + logging("refresh()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + zwave.meterV2.meterGet(scale: 0), + zwave.meterV2.meterGet(scale: 2), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ]) +} + +def reset() { + logging("reset()", 1) + commands([ + zwave.meterV2.meterReset(), + zwave.meterV2.meterGet() + ]) +} + +def ping() { + logging("ping()", 1) + refresh() +} + +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) commands(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: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(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}", + 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 "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.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true) + } + else + { + sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true) + } + } + + state.currentProperties = currentProperties +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport $cmd" + if (zwaveHubNodeId in cmd.nodeId) state."association${cmd.groupingIdentifier}" = true + else state."association${cmd.groupingIdentifier}" = false +} + +def zwaveEvent(hubitat.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + log.debug "MultiChannelAssociationReport $cmd" + if (cmd.groupingIdentifier == 1) { + if ([0,zwaveHubNodeId,1] == cmd.nodeId) state."associationMC${cmd.groupingIdentifier}" = true + else state."associationMC${cmd.groupingIdentifier}" = false + } +} + +def update_needed_settings() +{ + def cmds = [] + def currentProperties = state.currentProperties ?: [:] + + def configuration = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(state.association2){ + logging("Setting association group 2", 1) + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 2, nodeId: []) + cmds << zwave.associationV2.associationGet(groupingIdentifier:2) + } + if(state.association3){ + logging("Setting association group 3", 1) + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 3, nodeId: []) + cmds << zwave.associationV2.associationGet(groupingIdentifier:3) + } + if(!state.associationMC1) { + logging("Adding MultiChannel association group 1", 1) + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,1]) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1) + } + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave" && it.@disabled != "true"){ + if (currentProperties."${it.@index}" == null) + { + if (it.@setonly == "true"){ + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + } else { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown", 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if ((settings."${it.@index}" != null || "${it.@type}" == "hidden") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + case 110: + if (value < 0) + parValue = value * -1 + 1000 + else + parValue = value + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +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 + } + } +} + +/** +* Convert byte values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd) + } else { + cmd + } +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 1..2) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, + [completedSetup: true, label: "${device.displayName} (R${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Relay $i"]) + } + } catch (e) { + runIn(2, "sendAlert") + } +} + +private sendAlert() { + sendEvent( + descriptionText: "Child device creation failed. Please make sure that the \"Metering Switch Child Device\" is installed and published.", + eventType: "ALERT", + name: "childDeviceCreation", + value: "failed", + displayed: true, + ) +} + +def configuration_model() +{ +''' + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/enerwave-rsm2-plus.src/enerwave-rsm2-plus.groovy b/Drivers/enerwave-rsm2-plus.src/enerwave-rsm2-plus.groovy new file mode 100644 index 0000000..658f115 --- /dev/null +++ b/Drivers/enerwave-rsm2-plus.src/enerwave-rsm2-plus.groovy @@ -0,0 +1,585 @@ +/** + * + * Enerwave RSM2-Plus + * + * github: Eric Maycock (erocm123) + * Date: 2017-06-16 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + * 2017-06-16: Added support for firmware 5.11 in this handler. + */ + +metadata { + definition (name: "Enerwave RSM2-Plus", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + fingerprint mfr: "011A", prod: "0111", model: "0605", deviceJoinName: "Enerwave RSM2-Plus" + + fingerprint deviceId: "0x1001", inClusters:"0x5E,0x86,0x72,0x5A,0x73,0x20,0x27,0x25,0x32,0x60,0x85,0x8E,0x59,0x70", outClusters:"0x20" + fingerprint deviceId: "0x1001", inClusters:"0x5E,0x86,0x72,0x5A,0x73,0x25,0x60,0x8E,0x85,0x59,0x27" + } + + 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{ + 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" + } + } + 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" + } + + main(["switch"]) + details(["switch", + childDeviceTiles("all"), + "refresh","configure","reset" + ]) + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + logging("Parsed ${cmd} to ${result.inspect()}", 1) + } else { + logging("Non-parsed event: ${description}", 2) + } + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep=null) +{ + logging("BasicReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + childDevices.each { childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + logging("BasicSet ${cmd}", 2) + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=null) +{ + logging("SwitchBinaryReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + def childDevice = childDevices.find{it.deviceNetworkId == "$device.deviceNetworkId-ep$ep"} + if (childDevice) + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + logging("MultiChannelCmdEncap ${cmd}", 2) + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport ${cmd}", 2) + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr", 2) + updateDataValue("MSR", msr) +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + if (state.MSR == "003B-6341-5044") { + updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}") + } + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: false) +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport $cmd" + if (zwaveHubNodeId in cmd.nodeId) state."association${cmd.groupingIdentifier}" = true + else state."association${cmd.groupingIdentifier}" = false +} + +def zwaveEvent(hubitat.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + log.debug "MultiChannelAssociationReport $cmd" + if (cmd.groupingIdentifier == 1) { + if ([0,zwaveHubNodeId,1] == cmd.nodeId) state."associationMC${cmd.groupingIdentifier}" = true + else state."associationMC${cmd.groupingIdentifier}" = false + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + logging("Unhandled Event: ${cmd}", 2) +} + +def on() { + logging("on()", 1) + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +def off() { + logging("off()", 1) + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +void childOn(String dni) { + logging("childOn($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childOff(String dni) { + logging("childOff($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childRefresh(String dni) { + logging("childRefresh($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 0), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 2), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +def poll() { + logging("poll()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def refresh() { + logging("refresh()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + zwave.meterV2.meterGet(scale: 0), + zwave.meterV2.meterGet(scale: 2), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ]) +} + +def reset() { + logging("reset()", 1) + commands([ + zwave.meterV2.meterReset(), + zwave.meterV2.meterGet() + ]) +} + +def ping() { + logging("ping()", 1) + refresh() +} + +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) commands(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: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(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}", + 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 "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.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + cmds << zwave.versionV1.versionGet() + + if (state.fw == "5.11") { + if(state.association2){ + logging("Setting association group 2", 1) + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 2, nodeId: []) + cmds << zwave.associationV2.associationGet(groupingIdentifier:2) + } + if(state.association3){ + logging("Setting association group 3", 1) + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 3, nodeId: []) + cmds << zwave.associationV2.associationGet(groupingIdentifier:3) + } + if(!state.associationMC1) { + logging("Adding MultiChannel association group 1", 1) + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,1]) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1) + } + } else { + if(!state.association2){ + logging("Setting association group 2", 1) + cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:2) + } + if(!state.association3){ + logging("Setting association group 3", 1) + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:3) + } + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave" && it.@disabled != "true"){ + if (currentProperties."${it.@index}" == null) + { + if (it.@setonly == "true"){ + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + } else { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown", 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if ((settings."${it.@index}" != null || "${it.@type}" == "hidden") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + case 110: + if (value < 0) + parValue = value * -1 + 1000 + else + parValue = value + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +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 + } + } +} + +/** +* Convert byte values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd) + } else { + cmd + } +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 1..2) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, + [completedSetup: true, label: "${device.displayName} (R${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Relay $i"]) + } + } catch (e) { + runIn(2, "sendAlert") + } +} + +private sendAlert() { + sendEvent( + descriptionText: "Child device creation failed. Please make sure that the \"Switch Child Device\" is installed and published.", + eventType: "ALERT", + name: "childDeviceCreation", + value: "failed", + displayed: true, + ) +} + +def configuration_model() +{ +''' + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/enerwave-zigbee-dimmer-255.src/enerwave-zigbee-dimmer-255.groovy b/Drivers/enerwave-zigbee-dimmer-255.src/enerwave-zigbee-dimmer-255.groovy new file mode 100644 index 0000000..f69716c --- /dev/null +++ b/Drivers/enerwave-zigbee-dimmer-255.src/enerwave-zigbee-dimmer-255.groovy @@ -0,0 +1,148 @@ +metadata { + // Automatically generated. Make future change here. + definition (name: "Enerwave Zigbee Dimmer 255", namespace: "erocm123", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Sensor" + capability "Switch Level" + capability "Refresh" + + fingerprint profileId: "0104", inClusters: "0000,0003,0006,0008", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff" + } + + main "switch" + details (["switch","refresh","level","levelSliderControl"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + /*if (description?.startsWith("catchall: 0104 000A")) { + log.debug "Dropping catchall for SmartPower Outlet" + return [] + }*/ + log.trace description + if (description?.startsWith("catchall:")) { + def msg = zigbee.parse(description) + log.trace msg + log.trace "data: $msg.data" + if(msg.command == 0x01){ + if(msg.clusterId == 0x0006 && msg.data[0] == 0x00 && msg.data[1] == 0x00){ + def name = "switch" + def value = (msg.data[4] != 0 ? "on" : "off") + log.debug"name:$name,value:$value" + def result = createEvent(name: name, value: value) + return result + } + }else if(msg.command == 0x0b && msg.clusterId == 0x0006){ + def name = "switch" + def value = (msg.data[0] != 0 ? "on" : "off") + log.debug"name:$name,value:$value" + def result = createEvent(name: name, value: value) + return result + } + }else if(description?.startsWith("read attr")){ + def descMap = parseDescriptionAsMap(description) + log.debug "Read attr: $description" + if(descMap.cluster == "0008" && descMap.attrId == "0000"){ + def name = "level" + def value = Integer.parseInt(descMap.value, 16) + value = Math.round(value * 100/255) + log.debug"name:$name,value:$value" + def result = createEvent(name: name, value: value) + return result + } + }/*else { + log.debug "parse description: $description" + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + }*/ +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def refresh() { + log.debug "refresh()" + def cmds = [] + cmds << "st rattr 0x${device.deviceNetworkId} 1 0x0006 0x0000" + cmds << "delay 200" + cmds << "st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000" +} + +// Commands to device +def on() { + log.debug "on()" + def cmds = [] + cmds << "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" + cmds << "delay 5000" + cmds << "st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000" +} + +def off() { + log.debug "off()" + def cmds = [] + cmds << "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" + cmds << "delay 5000" + cmds << "st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000" +} + +def setLevel(value) { + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + //cmds << "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + //cmds << "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" + + } + //def transitionTime = Math.round(Math.abs(((device.currentValue("level")?:0) as int) - (value as int))*0.3) + //def transitionTime2 = hexString(transitionTime>>8) + //def transitionTime1 = hexString(transitionTime%256) + sendEvent(name: "level", value: value) + def level = hexString(Math.round(value * 255/100)) + cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} FFFF}" + cmds << "delay 5000" + cmds << "st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000" + + log.debug cmds + cmds +} \ No newline at end of file diff --git a/Drivers/enerwave-zigbee-outlet-switch-zb15r-zb333-zb15s.src/enerwave-zigbee-outlet-switch-zb15r-zb333-zb15s.groovy b/Drivers/enerwave-zigbee-outlet-switch-zb15r-zb333-zb15s.src/enerwave-zigbee-outlet-switch-zb15r-zb333-zb15s.groovy new file mode 100644 index 0000000..be75eb2 --- /dev/null +++ b/Drivers/enerwave-zigbee-outlet-switch-zb15r-zb333-zb15s.src/enerwave-zigbee-outlet-switch-zb15r-zb333-zb15s.groovy @@ -0,0 +1,87 @@ +metadata { + // Automatically generated. Make future change here. + definition (name: "Enerwave Zigbee Outlet/Switch ZB15R/ZB333/ZB15S", namespace: "erocm123", author: "Enerwave Home Automation") { + capability "Actuator" + capability "Switch" + capability "Sensor" + capability "Refresh" + + fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details (["switch","refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + /*if (description?.startsWith("catchall: 0104 000A")) { + log.debug "Dropping catchall for SmartPower Outlet" + return [] + }*/ + //log.debug description + if (description?.startsWith("catchall:")) { + log.trace description + def msg = zigbee.parse(description) + log.trace msg + log.trace "data: $msg.data" + if(msg.command == 0x01 && msg.clusterId == 0x0006 && msg.data[0] == 0x00 && msg.data[1] == 0x00){ + def name = "switch" + def value = (msg.data[4] != 0 ? "on" : "off") + log.debug"name:$name,value:$value" + def result = createEvent(name: name, value: value) + return result + }else if(msg.command == 0x0b && msg.clusterId == 0x0006){ + def name = "switch" + def value = (msg.data[0] != 0 ? "on" : "off") + log.debug"name:$name,value:$value" + def result = createEvent(name: name, value: value) + return result + } + }else { + log.debug "parse description: $description" + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } +} + +def refresh() { + log.debug "refresh()" + "st rattr 0x${device.deviceNetworkId} 1 0x0006 0x0000" +} + +// Commands to device +def on() { + log.debug "on()" + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +} + +def off() { + log.debug "off()" + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +} \ No newline at end of file diff --git a/Drivers/enerwave-zw15sm-metering-switch.src/enerwave-zw15sm-metering-switch.groovy b/Drivers/enerwave-zw15sm-metering-switch.src/enerwave-zw15sm-metering-switch.groovy new file mode 100644 index 0000000..49b48fb --- /dev/null +++ b/Drivers/enerwave-zw15sm-metering-switch.src/enerwave-zw15sm-metering-switch.groovy @@ -0,0 +1,242 @@ +/** + * Copyright 2015 SmartThings + * + * 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. + * + */ +metadata { + definition (name: "Enerwave ZW15SM Metering Switch", namespace: "erocm123", author: "SmartThings", ocfDeviceType: "oic.d.switch") { + capability "Energy Meter" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Polling" + capability "Refresh" + capability "Configuration" + capability "Sensor" + capability "Light" + capability "Health Check" + + command "reset" + + fingerprint mfr:"011A", prod:"0111", model:"0102", deviceJoinName: "Enerwave Metering Switch" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new hubitat.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + + } + + // tile definitions + tiles { + 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" + } + } + valueTile("power", "device.power", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch","power","energy"]) + details(["switch","power","energy","refresh","reset"]) + } +} + +def installed() { + // Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 15, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def updated() { + // Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 15, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + try { + if (!state.MSR) { + response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + } + } catch (e) { log.debug e } +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x32: 1, // SwitchMultilevel + 0x56: 1, // Crc16Encap + 0x72: 2, // ManufacturerSpecific + ] +} + +def parse(String description) { + def result = null + if(description == "updated") return + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.meterv1.MeterReport cmd) { + if (cmd.scale == 0) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else if (cmd.scale == 2) { + createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + def evt = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") + if (evt.isStateChange) { + [evt, response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()])] + } else { + evt + } +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + // retypeBasedOnMSR() + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + + if (msr.startsWith("0086") && !state.aeonconfig) { // Aeon Labs meter + state.aeonconfig = 1 + result << response(delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // report power in watts + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // report energy in kWh + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report + //zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format(), + ])) + } else { + result << response(delayBetween([ + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format(), + ])) + } + + result +} + +def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = commandClassVersions + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "$device.displayName: Unhandled: $cmd" + [:] +} + +def on() { + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def off() { + [ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def poll() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def configure() { + zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() +} + +def reset() { + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ] +} \ No newline at end of file diff --git a/Drivers/ezmultipli.src/ezmultipli.groovy b/Drivers/ezmultipli.src/ezmultipli.groovy new file mode 100644 index 0000000..cb934c3 --- /dev/null +++ b/Drivers/ezmultipli.src/ezmultipli.groovy @@ -0,0 +1,409 @@ +// Express Controls EZMultiPli Multi-sensor +// Motion Sensor - Temperature - Light level - 8 Color Indicator LED - Z-Wave Range Extender - Wall Powered +// driver for SmartThings +// The EZMultiPli is also known as the HSM200 from HomeSeer.com +// +// 2016-01-28 - erocm1231 - Changed the configuration method to use scaledConfiguration so that it properly formatted negative numbers. +// Also, added configurationGet and a configurationReport method so that config values can be verified. +// 2015-12-04 - erocm1231 - added range value to preferences as suggested by @Dela-Rick. +// 2015-11-26 - erocm1231 - Fixed null condition error when adding as a new device. +// 2015-11-24 - erocm1231 - Added refresh command. Made a few changes to how the handler maps colors to the LEDs. Fixed +// the device not having its on/off status updated when colors are changed. +// 2015-11-23 - erocm1231 - Changed the look to match SmartThings v2 devices. +// 2015-11-21 - erocm1231 - Made code much more efficient. Also made it compatible when setColor is passed a hex value. +// Mapping of special colors: Soft White - Default - Yellow, White - Concentrate - White, +// Daylight - Energize - Teal, Warm White - Relax - Yellow +// 2015-11-19 - erocm1231 - Fixed a couple incorrect colors, changed setColor to be more compatible with other apps +// 2015-11-18 - erocm1231 - Added to setColor for compatibility with Smart Lighting +// v0.1.0 - DrZWave - chose better icons, Got color LED to work - first fully functional version +// v0.0.9 - jrs - got the temp and luminance to work. Motion works. Debugging the color wheel. +// v0.0.8 - DrZWave 2/25/2015 - change the color control to be tiles since there are only 8 colors. +// v0.0.7 - jrs - 02/23/2015 - Jim Sulin + +metadata { + definition (name: "EZmultiPli", namespace: "erocm123", author: "Eric Maycock", oauth: true) { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Illuminance Measurement" + capability "Switch" + capability "Color Control" + capability "Configuration" + capability "Refresh" + + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x71, 0x31, 0x33, 0x72, 0x86, 0x59, 0x85, 0x70, 0x77, 0x5A, 0x7A, 0x73, 0xEF, 0x20" + + } // end definition + + simulator { + // messages the device returns in response to commands it receives + status "motion" : "command: 7105000000FF07, payload: 07" + status "no motion" : "command: 7105000000FF07, payload: 00" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new hubitat.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + for (int i = 0; i <= 100; i += 20) { + status "luminance ${i} %": new hubitat.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() + } + + } //end simulator + + + tiles (scale: 2){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL", icon: "st.Lighting.light18") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', icon:"st.switches.light.on", backgroundColor:"#79b821" + attributeState "turningOff", label:'${name}', icon:"st.switches.light.off", backgroundColor:"#ffffff" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + + standardTile("motion", "device.motion", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true) { + state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" + state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state "temperature", label:'${currentValue}°', unit:"F", icon:"", // would be better if the units would switch to the desired units of the system (imperial or metric) + backgroundColors:[ + [value: 0, color: "#1010ff"], // blue=cold + [value: 65, color: "#a0a0f0"], + [value: 70, color: "#e0e050"], + [value: 75, color: "#f0d030"], // yellow + [value: 80, color: "#fbf020"], + [value: 85, color: "#fbdc01"], + [value: 90, color: "#fb3a01"], + [value: 95, color: "#fb0801"] // red=hot + ] + } + + // icons to use would be st.Weather.weather2 or st.alarm.temperature.normal - see http://scripts.3dgo.net/smartthings/icons/ for a list of icons + valueTile("illuminance", "device.illuminance", width: 2, height: 2, inactiveLabel: false) { +// jrs 4/7/2015 - Null on display + //state "luminosity", label:'${currentValue} ${unit}' + state "luminosity", label:'${currentValue}', unit:'${currentValue}', icon:"", + backgroundColors:[ + [value: 25, color: "#404040"], + [value: 50, color: "#808080"], + [value: 75, color: "#a0a0a0"], + [value: 90, color: "#e0e0e0"], + //lux measurement values + [value: 150, color: "#404040"], + [value: 300, color: "#808080"], + [value: 600, color: "#a0a0a0"], + [value: 900, color: "#e0e0e0"] + ] + } + 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.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main (["temperature","motion", "switch"]) + details(["switch", "motion", "temperature", "illuminance", "refresh", "configure"]) + } // end tiles + + preferences { + input("lum", "enum", title:"Illuminance Measurement", description: "Percent or Lux", defaultValue: 1 ,required: true, displayDuringSetup: true, options: + [1:"Percent", + 2:"Lux"]) + input "OnTime", "number", title: "No Motion Interval", description: "N minutes lights stay on after no motion detected [0, 1-127]", range: "0..127", defaultValue: 10, displayDuringSetup: true, required: false + input "OnLevel", "number", title: "Dimmer Onlevel", description: "Dimmer OnLevel for associated node 2 lights [-1, 0, 1-99]", range: "-1..99", defaultValue: -1, displayDuringSetup: true, required: false + input "LiteMin", "number", title: "Luminance Report Frequency", description: "Luminance report sent every N minutes [0-127]", range: "0..127", defaultValue: 10, displayDuringSetup: true, required: false + input "TempMin", "number", title: "Temperature Report Frequency", description: "Temperature report sent every N minutes [0-127]", range: "0..127", defaultValue: 10, displayDuringSetup: true, required: false + input "TempAdj", "number", title: "Temperature Calibration", description: "Adjust temperature up/down N tenths of a degree F [(-127)-(+128)]", range: "-127..128", defaultValue: 0, displayDuringSetup: true, required: false + } + +} // end metadata + + +// Parse incoming device messages from device to generate events +def parse(String description){ + //log.debug "==> New Zwave Event: ${description}" + def result = [] + def cmd = zwave.parse(description, [0x31: 5]) // 0x31=SensorMultilevel which we force to be version 5 + if (cmd) { + result << createEvent(zwaveEvent(cmd)) + } + + def statusTextmsg = "" + if (device.currentState('temperature') != null && device.currentState('illuminance') != null) { + statusTextmsg = "Temperature is ${device.currentState('temperature').value} °F - Relative Luminance is ${device.currentState('illuminance').value}%" + sendEvent("name":"statusText", "value":statusTextmsg, displayed:false) + //log.debug statusTextmsg + } + if (result != [null]) log.debug "Parse returned ${result}" + + + return result +} + + +// Event Generation +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ + def map = [:] + switch (cmd.sensorType) { + case 0x01: // SENSOR_TYPE_TEMPERATURE_VERSION_1 + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + log.debug "Temperature report" + break; + case 0x03 : // SENSOR_TYPE_LUMINANCE_VERSION_1 + map.value = cmd.scaledSensorValue.toInteger().toString() + if(lum == "" || lum == null || lum == 1) map.unit = "%" + else map.unit = "lux" + map.name = "illuminance" + log.debug "Luminance report" + break; + } + return map +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + def map = [:] + if (cmd.notificationType==0x07) { // NOTIFICATION_TYPE_BURGLAR + if (cmd.event==0x07 || cmd.event==0x08) { + map.name = "motion" + map.value = "active" + map.descriptionText = "$device.displayName motion detected" + log.debug "motion recognized" + } else if (cmd.event==0) { + map.name = "motion" + map.value = "inactive" + map.descriptionText = "$device.displayName no motion detected" + log.debug "No motion recognized" + } + } + if (map.name != "motion") { + log.debug "unmatched parameters for cmd: ${cmd.toString()}}" + } + return map +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "digital"] +} + + +def on() { + log.debug "Turning Light 'on'" + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.basicV1.basicGet().format() + ], 500) +} + +def off() { + log.debug "Turning Light 'off'" + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.basicV1.basicGet().format() + ], 500) +} + + +def setColor(value) { + log.debug "setColor() : ${value}" + def myred + def mygreen + def myblue + def hexValue + def cmds = [] + + if ( value.level == 1 && value.saturation > 20) { + def rgb = huesatToRGB(value.hue as Integer, 100) + myred = rgb[0] >=128 ? 255 : 0 + mygreen = rgb[1] >=128 ? 255 : 0 + myblue = rgb[2] >=128 ? 255 : 0 + } + else if ( value.level > 1 ) { + def rgb = huesatToRGB(value.hue as Integer, value.saturation as Integer) + myred = rgb[0] >=128 ? 255 : 0 + mygreen = rgb[1] >=128 ? 255 : 0 + myblue = rgb[2] >=128 ? 255 : 0 + } + else if (value.hex) { + def rgb = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + myred = rgb[0] >=128 ? 255 : 0 + mygreen = rgb[1] >=128 ? 255 : 0 + myblue = rgb[2] >=128 ? 255 : 0 + } + else { + myred=value.red >=128 ? 255 : 0 // the EZMultiPli has just on/off for each of the 3 channels RGB so convert the 0-255 value into 0 or 255. + mygreen=value.green >=128 ? 255 : 0 + myblue=value.blue>=128 ? 255 : 0 + } + //log.debug "Red: ${myred} Green: ${mygreen} Blue: ${myblue}" + //cmds << zwave.colorControlV1.stateSet(stateDataLength: 3, VariantGroup1: [0x02, myred], VariantGroup2:[ 0x03, mygreen], VariantGroup3:[0x04,myblue]).format() // ST support for this command as of 2015/02/23 does not support the color IDs so this command cannot be used. + // So instead we'll use these commands to hack around the lack of support of the above command + cmds << zwave.basicV1.basicSet(value: 0x00).format() // As of 2015/02/23 ST is not supporting stateSet properly but found this hack that works. + if (myred!=0) { + cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x02, startState: myred, ignoreStartState: True, updown: True).format() + cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x02).format() + } + if (mygreen!=0) { + cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x03, startState: mygreen, ignoreStartState: True, updown: True).format() + cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x03).format() + } + if (myblue!=0) { + cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x04, startState: myblue, ignoreStartState: True, updown: True).format() + cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x04).format() + } + cmds << zwave.basicV1.basicGet().format() + hexValue = rgbToHex([r:myred, g:mygreen, b:myblue]) + if(hexValue) sendEvent(name: "color", value: hexValue, displayed: true) + delayBetween(cmds, 100) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +// ensure we are passing acceptable param values for LiteMin & TempMin configs +def checkLiteTempInput(value) { + if (value == null) { + value=60 + } + def liteTempVal = value.toInteger() + switch (liteTempVal) { + case { it < 0 }: + return 60 // bad value, set to default + break + case { it > 127 }: + return 127 // bad value, greater then MAX, set to MAX + break + default: + return liteTempVal // acceptable value + } +} + +// ensure we are passing acceptable param value for OnTime config +def checkOnTimeInput(value) { + if (value == null) { + value=10 + } + def onTimeVal = value.toInteger() + switch (onTimeVal) { + case { it < 0 }: + return 10 // bad value set to default + break + case { it > 127 }: + return 127 // bad value, greater then MAX, set to MAX + break + default: + return onTimeVal // acceptable value + } +} + +// ensure we are passing acceptable param value for OnLevel config +def checkOnLevelInput(value) { + if (value == null) { + value=99 + } + def onLevelVal = value.toInteger() + switch (onLevelVal) { + case { it < -1 }: + return -1 // bad value set to default + break + case { it > 99 }: + return 99 // bad value, greater then MAX, set to MAX + break + default: + return onLevelVal // acceptable value + } +} + + +// ensure we are passing an acceptable param value for TempAdj configs +def checkTempAdjInput(value) { + if (value == null) { + value=0 + } + def tempAdjVal = value.toInteger() + switch (tempAdjVal) { + case { it < -127 }: + return 0 // bad value, set to default + break + case { it > 128 }: + return 128 // bad value, greater then MAX, set to MAX + break + default: + return tempAdjVal // acceptable value + } +} + +def refresh() { + def cmd = [] + cmd << zwave.switchColorV3.switchColorGet().format() + cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1).format() + cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1).format() + cmd << zwave.basicV1.basicGet().format() + delayBetween(cmd, 1000) +} + +def configure() { + log.debug "OnTime=${settings.OnTime} OnLevel=${settings.OnLevel} TempAdj=${settings.TempAdj}" + def cmd = delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, scaledConfigurationValue: checkOnTimeInput(settings.OnTime)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: checkOnLevelInput(settings.OnLevel)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: checkLiteTempInput(settings.LiteMin)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: checkLiteTempInput(settings.TempMin)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: checkTempAdjInput(settings.TempAdj)).format(), + zwave.configurationV1.configurationGet(parameterNumber: 1).format(), + zwave.configurationV1.configurationGet(parameterNumber: 2).format(), + zwave.configurationV1.configurationGet(parameterNumber: 3).format(), + zwave.configurationV1.configurationGet(parameterNumber: 4).format(), + zwave.configurationV1.configurationGet(parameterNumber: 5).format() + ], 100) + //log.debug cmd + cmd +} + +def huesatToRGB(float hue, float sat) { + while(hue >= 100) hue -= 100 + int h = (int)(hue / 100 * 6) + float f = hue / 100 * 6 - h + int p = Math.round(255 * (1 - (sat / 100))) + int q = Math.round(255 * (1 - (sat / 100) * f)) + int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) + switch (h) { + case 0: return [255, t, p] + case 1: return [q, 255, p] + case 2: return [p, 255, t] + case 3: return [p, q, 255] + case 4: return [t, p, 255] + case 5: return [255, p, q] + } +} +def rgbToHex(rgb) { + def r = hex(rgb.r) + def g = hex(rgb.g) + def b = hex(rgb.b) + def hexColor = "#${r}${g}${b}" + + hexColor +} +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} \ No newline at end of file diff --git a/Drivers/fibaro-dimmer-2.src/fibaro-dimmer-2.groovy b/Drivers/fibaro-dimmer-2.src/fibaro-dimmer-2.groovy new file mode 100644 index 0000000..c1d44f3 --- /dev/null +++ b/Drivers/fibaro-dimmer-2.src/fibaro-dimmer-2.groovy @@ -0,0 +1,893 @@ +/** + * + * Fibaro Dimmer 2 (US) + * + * github: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2016-07-31 8:03PM + * Copyright 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. + * + */ + +metadata { + definition (name: "Fibaro Dimmer 2", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + capability "Switch Level" + capability "Refresh" + capability "Configuration" + capability "Sensor" + capability "Polling" + capability "Energy Meter" + capability "Power Meter" + capability "PushableButton" + capability "Health Check" + + attribute "needUpdate", "string" + + fingerprint mfr: "010F", prod: "0102", model: "2000", deviceJoinName: "Fibaro Dimmer 2" + + fingerprint deviceId: "0x1101", inClusters: "0x72,0x86,0x70,0x85,0x8E,0x26,0x7A,0x27,0x73,0xEF,0x26,0x2B" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x20,0x86,0x72,0x26,0x5A,0x59,0x85,0x73,0x98,0x7A,0x56,0x70,0x31,0x32,0x8E,0x60,0x75,0x71,0x27" + + } + + 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()) + } + + simulator { + + } + + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + 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" + } + + + main "switch" + details (["switch", "power", "energy", "refresh", "configure", "reset"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + + def statusTextmsg = "" + if (device.currentState('power') && device.currentState('energy')) statusTextmsg = "${device.currentState('power').value} W ${device.currentState('energy').value} kWh" + sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + logging("BasicReport: $cmd") + def events = [] + if (cmd.value == 0) { + events << createEvent(name: "switch", value: "off") + } else if (cmd.value == 255) { + events << createEvent(name: "switch", value: "on") + } else { + events << createEvent(name: "switch", value: "on") + events << createEvent(name: "switchLevel", value: cmd.value) + } + + def request = update_needed_settings() + + if(request != []){ + return [response(commands(request)), events] + } else { + return events + } +} + +def zwaveEvent(hubitat.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { + logging("SceneActivationSet: $cmd") + logging("sceneId: $cmd.sceneId") + logging("dimmingDuration: $cmd.dimmingDuration") + logging("Configuration for preference \"Switch Type\" is set to ${settings."20"}") + + if (settings."20" == "2") { + logging("Switch configured as Roller blinds") + switch (cmd.sceneId) { + // Roller blinds S1 + case 10: // Turn On (1x click) + buttonEvent(1, "pushed") + break + case 13: // Release + buttonEvent(1, "held") + break + case 14: // 2x click + buttonEvent(2, "pushed") + break + case 17: // Brightening + buttonEvent(2, "held") + break + // Roller blinds S2 + case 11: // Turn Off + buttonEvent(3, "pushed") + break + case 13: // Release + buttonEvent(3, "held") + break + case 14: // 2x click + buttonEvent(4, "pushed") + break + case 15: // 3x click + buttonEvent(5, "pushed") + break + case 18: // Dimming + buttonEvent(4, "held") + break + default: + logging("Unhandled SceneActivationSet: ${cmd}") + break + } + } else if (settings."20" == "1") { + logging("Switch configured as Toggle") + switch (cmd.sceneId) { + // Toggle S1 + case 10: // Off to On + buttonEvent(1, "pushed") + break + case 11: // On to Off + buttonEvent(1, "held") + break + case 14: // 2x click + buttonEvent(2, "pushed") + break + // Toggle S2 + case 20: // Off to On + buttonEvent(3, "pushed") + break + case 21: // On to Off + buttonEvent(3, "held") + break + case 24: // 2x click + buttonEvent(4, "pushed") + break + case 25: // 3x click + buttonEvent(5, "pushed") + break + default: + logging("Unhandled SceneActivationSet: ${cmd}") + break + + } + } else { + if (settings."20" == "0") logging("Switch configured as Momentary") else logging("Switch type not configured") + switch (cmd.sceneId) { + // Momentary S1 + case 16: // 1x click + buttonEvent(1, "pushed") + break + case 14: // 2x click + buttonEvent(2, "pushed") + break + case 12: // held + buttonEvent(1, "held") + break + case 13: // release + buttonEvent(2, "held") + break + // Momentary S2 + case 26: // 1x click + buttonEvent(3, "pushed") + break + case 24: // 2x click + buttonEvent(4, "pushed") + break + case 25: // 3x click + buttonEvent(5, "pushed") + break + case 22: // held + buttonEvent(3, "held") + break + case 23: // release + buttonEvent(4, "held") + break + default: + logging("Unhandled SceneActivationSet: ${cmd}") + break + } + } +} + +def buttonEvent(button, value) { + logging("buttonEvent() Button:$button, Value:$value") + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + logging(cmd) + dimmerEvents(cmd) +} + +def dimmerEvents(hubitat.zwave.Command cmd) { + logging(cmd) + def result = [] + def value = (cmd.value ? "on" : "off") + def switchEvent = createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value") + result << switchEvent + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + if (switchEvent.isStateChange) { + result << response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()]) + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + logging("AssociationReport $cmd") + state."association${cmd.groupingIdentifier}" = cmd.nodeId[0] +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof hubitat.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + +def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = [0x31: 5, 0x30: 1, 0x9C: 1, 0x70: 2, 0x85: 2] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unhandled Z-Wave Event: $cmd") +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd) { + logging(cmd) + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + return createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + return createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else if (cmd.scale == 2) { + return createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } else { + return createEvent(name: "electric", value: cmd.scaledMeterValue, unit: ["pulses", "V", "A", "R/Z", ""][cmd.scale - 3]) + } + } +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ + logging("SensorMultilevelReport: $cmd") + def map = [:] + switch (cmd.sensorType) { + case 4: + map.name = "power" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = cmd.scale == 1 ? "Btu/h" : "W" + break + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def on() { + commands([zwave.basicV1.basicSet(value: 0xFF), zwave.basicV1.basicGet()]) +} + +def off() { + commands([zwave.basicV1.basicSet(value: 0x00), zwave.basicV1.basicGet()]) +} + +def refresh() { + logging("$device.displayName refresh()") + + def cmds = [] + if (state.lastRefresh != null && now() - state.lastRefresh < 5000) { + logging("Refresh Double Press") + def configuration = new XmlSlurper().parseText(configuration_model()) + configuration.Value.each + { + if ( "${it.@setting_type}" == "zwave" ) { + cmds << zwave.configurationV1.configurationGet(parameterNumber: "${it.@index}".toInteger()) + } + } + cmds << zwave.firmwareUpdateMdV2.firmwareMdGet() + } else { + cmds << zwave.meterV2.meterGet(scale: 0) + cmds << zwave.meterV2.meterGet(scale: 2) + cmds << zwave.basicV1.basicGet() + } + + state.lastRefresh = now() + + commands(cmds) +} + +def ping() { + logging("$device.displayName ping()") + + def cmds = [] + + cmds << zwave.meterV2.meterGet(scale: 0) + cmds << zwave.meterV2.meterGet(scale: 2) + cmds << zwave.basicV1.basicGet() + + commands(cmds) +} + +def setLevel(level) { + if(level > 99) level = 99 + if(level < 1) level = 1 + def cmds = [] + cmds << zwave.basicV1.basicSet(value: level) + cmds << zwave.switchMultilevelV1.switchMultilevelGet() + + commands(cmds) +} + +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + state.needfwUpdate = "" + + def cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + response(commands(cmds)) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 + } + } +} + + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (settings."${cmd.parameterNumber}".toInteger() == convertParam("${cmd.parameterNumber}".toInteger(), cmd2Integer(cmd.configurationValue))) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(!state.needfwUpdate || state.needfwUpdate == ""){ + logging("Requesting device firmware version") + cmds << zwave.firmwareUpdateMdV2.firmwareMdGet() + } + if(!state.association1 || state.association1 == "" || state.association1 == "1"){ + logging("Setting association group 1") + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + } + if(!state.association2 || state.association2 == "" || state.association1 == "2"){ + logging("Setting association group 2") + cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:2) + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + if (device.currentValue("currentFirmware") == null || "${it.@fw}".indexOf(device.currentValue("currentFirmware")) >= 0){ + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if (settings."${it.@index}" != null && convertParam(it.@index.toInteger(), cmd2Integer(currentProperties."${it.@index}")) != settings."${it.@index}".toInteger()) + { + if (device.currentValue("currentFirmware") == null || "${it.@fw}".indexOf(device.currentValue("currentFirmware")) >= 0){ + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + settings."${it.@index}") + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def zwaveEvent(hubitat.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd){ + logging("Firmware Report ${cmd.toString()}") + def firmwareVersion + switch(cmd.checksum){ + case "3281": + firmwareVersion = "3.08" + break; + default: + firmwareVersion = cmd.checksum + } + state.needfwUpdate = "false" + updateDataValue("firmware", firmwareVersion.toString()) + createEvent(name: "currentFirmware", value: firmwareVersion) +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) commands(cmds) +} + +def convertParam(number, value) { + switch (number){ + case 201: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 202: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 203: + if (value < 0) + 65536 + value + else if (value > 1000) + value - 65536 + else + value + break + case 204: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + default: + value + break + } +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + + +def configuration_model() +{ +''' + + + +(parameter is set automatically during the calibration process) +The parameter can be changed manually after the calibration. +Range: 1~98 +Default: 1 + + + + +(parameter is set automatically during the calibration process) +The parameter can be changed manually after the calibration. +Range: 2~99 +Default: 99 + + + + +This parameter defines the percentage value of dimming step during the automatic control. +Range: 1~99 +Default: 1 + + + + +This parameter defines the time of single dimming step set in parameter 5 during the automatic control. +Range: 0~255 +Default: 1 + + + + +This parameter defines the percentage value of dimming step during the manual control. +Range: 1~99 +Default: 1 + + + + +This parameter defines the time of single dimming step set in parameter 7 during the manual control. +Range: 0~255 +Default: 5 + + + + +The Dimmer 2 will return to the last state before power failure. +0 - the Dimmer 2 does not save the state before a power failure, it returns to the "off" position +1 - the Dimmer 2 restores its state before power failure +Range: 0~1 +Default: 1 + + + + + + +This parameter allows to automatically switch off the device after specified time (seconds) from switching on the light source. +Range: 1~32767 +Default: 0 + + + + +If the parameter is active, switching on the Dimmer 2 (S1 single click) will always set this brightness level. +Range: 0~99 +Default: 0 + + + + +Choose between momentary, toggle and roller blind switch. +Range: 0~2 +Default: 0 + + + + + + + +By default each change of toggle switch position results in action of Dimmer 2 (switch on/off) regardless the physical connection of contacts. +0 - device changes status on switch status change +1 - device status is synchronized with switch status +Range: 0~1 +Default: 0 + + + + + + +set the brightness level to MAX +Range: 0~1 +Default: 1 + + + + + + +Switch no. 2 controls the Dimmer 2 additionally (in 3-way switch mode). Function disabled for parameter 20 set to 2 (roller blind switch). +Range: 0~1 +Default: 0 + + + + + + +SCENE ID depends on the switch type configurations. +Range: 0~1 +Default: 0 + + + + + + +This parameter allows for switching the role of keys connected to S1 and S2 without changes in connection. +Range: 0~1 +Default: 0 + + + + + + +This parameter determines the trigger of auto-calibration procedure, e.g. power on, load error, etc. +0 - No auto-calibration of the load after power on +1 - Auto-calibration performed after first power on +2 - Auto-calibration performed after each power on +3 - Auto-calibration performed after first power on or after each LOAD ERROR alarm (no load, load failure, burnt out bulb), if parameter 37 is set to 1 also after alarms: SURGE (Dimmer 2 output overvoltage) and OVERCURRENT (Dimmer 2 output overcurrent) +4 - Auto-calibration performed after each power on or after each LOAD ERROR alarm (no load, load failure, burnt out bulb), if parameter 37 is set to 1 also after alarms: SURGE (Dimmer 2 output overvoltage) and OVERCURRENT (Dimmer 2 output overcurrent) +Range: 0~4 +Default: 1 + + + + + + + + + +This parameter defines the maximum load for a dimmer. +Range: 0~350 +Default: 250 + + + + +This parameter determines how the device will react to General Alarm frame. +Range: 0~3 +Default: 3 (Flash) + + + + + + + + +This parameter determines how the device will react to Flood Alarm frame. +Range: 0~3 +Default: 2 (OFF) + + + + + + + + +This parameter determines how the device will react to CO, CO2 or Smoke frame. +Range: 0~3 +Default: 3 (Flash) + + + + + + + + +This parameter determines how the device will react to Heat Alarm frame. +Range: 0~3 +Default: 1 (ON) + + + + + + + + +This parameter allows to set duration of flashing alarm mode. +Range: 1~32000 (1s-32000s) +Default: 600 (10 min) + + + + +The parameter defines the power level change that will result in a new power report being sent. The value is a percentage of the previous report. +Range: 0~100 +Default: 10 + + + + +Parameter 52 defines a time period between consecutive reports. Timer is reset and counted from zero after each report. +Range: 0~32767 +Default: 3600 + + + + +Energy level change which will result in sending a new energy report. +Range: 0~255 +Default: 10 + + + + +The Dimmer 2 may include active power and energy consumed by itself in reports sent to the main controller. +Range: 0~1 +Default: 0 + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/fibaro-double-switch-2-fgs-223.src/fibaro-double-switch-2-fgs-223.groovy b/Drivers/fibaro-double-switch-2-fgs-223.src/fibaro-double-switch-2-fgs-223.groovy new file mode 100644 index 0000000..e3c52c9 --- /dev/null +++ b/Drivers/fibaro-double-switch-2-fgs-223.src/fibaro-double-switch-2-fgs-223.groovy @@ -0,0 +1,948 @@ +/** + * Note: This handler requires the "Metering Switch Child Device" to be installed. + * + * Copyright 2016 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. + * + * Fibaro FGS-223 Dual Relay + * + * Author: Eric Maycock (erocm123) + * + * 04/25/2017 - Fix for combined energy & power reports & switch endpoints showing correct info. + * 04/18/2017 - This handler requires the Metering Switch Child device to create the multiple switch endpoints. + */ + +metadata { +definition (name: "Fibaro Double Switch 2 FGS-223", namespace: "erocm123", author: "Eric Maycock") { + capability "Sensor" + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Configuration" + capability "Refresh" + capability "Zw Multichannel" + capability "Energy Meter" + capability "Power Meter" + capability "Health Check" + capability "PushableButton" + capability "HoldableButton" + + command "reset" + + fingerprint mfr: "010F", prod: "0203", model: "2000", deviceJoinName: "Fibaro Double Switch 2" + fingerprint mfr: "010F", prod: "0203", model: "1000", deviceJoinName: "Fibaro Double Switch 2" + + fingerprint deviceId: "0x1001", inClusters:"0x5E,0x86,0x72,0x59,0x73,0x22,0x56,0x32,0x71,0x98,0x7A,0x25,0x5A,0x85,0x70,0x8E,0x60,0x75,0x5B" +} + +simulator { +} + +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 ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + 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("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch", childDeviceTiles("all"), + "refresh","reset","configure"]) + +} + 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()) + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + //log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + + def statusTextmsg = "" + + result.each { + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "power") { + statusTextmsg = "${it.value} W ${device.currentValue('energy')? device.currentValue('energy') : "0"} kWh" + } + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "energy") { + statusTextmsg = "${device.currentValue('power')? device.currentValue('power') : "0"} W ${it.value} kWh" + } + } + if (statusTextmsg != "") sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + log.debug "BasicReport $cmd" +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd, ep=null) { + logging("BasicSet: $cmd : Endpoint: $ep") + if (ep) { + def event + childDevices.each { childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=null) +{ + logging("SwitchBinaryReport: $cmd : Endpoint: $ep") + if (ep) { + def event + childDevices.each { childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } else { + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return response(secureSequence(cmds)) // returns the result of reponse() + } +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd, ep=null) { + logging("MeterReport: $cmd : Endpoint: $ep") + def result + def cmds = [] + if (cmd.scale == 0) { + result = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + result = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } else { + result = [name: "power", value: cmd.scaledMeterValue, unit: "W"] + } + if (ep) { + def childDevice = childDevices.find{it.deviceNetworkId == "$device.deviceNetworkId-ep$ep"} + if (childDevice) + childDevice.sendEvent(result) + def combinedValue = 0.00 + childDevices.each { + if(it.currentValue(result.name)) combinedValue += it.currentValue(result.name) + } + return createEvent([name: result.name, value: combinedValue]) + } else { + (1..2).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + return response(secureSequence(cmds)) + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) +{ + //log.debug "multichannelv3.MultiChannelCapabilityReport $cmd" + if (cmd.endPoint == 2 ) { + def currstate = device.currentState("switch2").getValue() + if (currstate == "on") + sendEvent(name: "switch2", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch2", value: "on", isStateChange: true, display: false) + } + else if (cmd.endPoint == 1 ) { + def currstate = device.currentState("switch1").getValue() + if (currstate == "on") + sendEvent(name: "switch1", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch1", value: "on", isStateChange: true, display: false) + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + //logging("MultiChannelCmdEncap ${cmd}") + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport $cmd" + if (zwaveHubNodeId in cmd.nodeId) state."association${cmd.groupingIdentifier}" = true + else state."association${cmd.groupingIdentifier}" = false +} + +def zwaveEvent(hubitat.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + log.debug "MultiChannelAssociationReport $cmd" + if (cmd.groupingIdentifier == 1) { + if ([0,zwaveHubNodeId,1] == cmd.nodeId) state."associationMC${cmd.groupingIdentifier}" = true + else state."associationMC${cmd.groupingIdentifier}" = false + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled event $cmd" + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + return createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +def zwaveEvent(hubitat.zwave.commands.switchallv1.SwitchAllReport cmd) { + log.debug "SwitchAllReport $cmd" +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def refresh() { + def cmds = [] + (1..2).each { endpoint -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + secureSequence(cmds, 1000) +} + +def reset() { + logging("reset()") + def cmds = [] + (1..2).each { endpoint -> + cmds << encap(zwave.meterV2.meterReset(), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + secureSequence(cmds, 1000) +} + +def ping() { + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + secureSequence(cmds, 1000) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def poll() { + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + secureSequence(cmds, 1000) +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) secureSequence(cmds) +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging("CentralSceneNotification: $cmd") + logging("sceneNumber: $cmd.sceneNumber") + logging("sequenceNumber: $cmd.sequenceNumber") + logging("keyAttributes: $cmd.keyAttributes") + + buttonEvent(cmd.keyAttributes + 1, (cmd.sceneNumber == 1? "pushed" : "held")) + +} + +def buttonEvent(button, value) { + logging("buttonEvent() Button:$button, Value:$value") + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true) +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + if (!childDevices) { + createChildDevices() + } + else if (device.label != state.oldLabel) { + childDevices.each { + if (it.label == "${state.oldLabel} (S${channelNumber(it.deviceNetworkId)})") { + def newLabel = "${device.displayName} (S${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + } + state.oldLabel = device.label + } + sendEvent(name: "checkInterval", value: 2 * 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + if (cmds != []) response(secureSequence(cmds)) +} + +def on() { + secureSequence([ + encap(zwave.basicV1.basicSet(value: 0xFF), 1), + encap(zwave.basicV1.basicSet(value: 0xFF), 2) + ]) +} +def off() { + secureSequence([ + encap(zwave.basicV1.basicSet(value: 0x00), 1), + encap(zwave.basicV1.basicSet(value: 0x00), 2) + ]) +} + +void childOn(String dni) { + logging("childOn($dni)") + def cmds = [] + cmds << new hubitat.device.HubAction(secure(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childOff(String dni) { + logging("childOff($dni)") + def cmds = [] + cmds << new hubitat.device.HubAction(secure(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childRefresh(String dni) { + logging("childRefresh($dni)") + def cmds = [] + cmds << new hubitat.device.HubAction(secure(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(secure(encap(zwave.meterV2.meterGet(scale: 0), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(secure(encap(zwave.meterV2.meterGet(scale: 2), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childReset(String dni) { + logging("childReset($dni)") + def cmds = [] + cmds << new hubitat.device.HubAction(secure(encap(zwave.meterV2.meterReset(), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(secure(encap(zwave.meterV2.meterGet(scale: 0), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(secure(encap(zwave.meterV2.meterGet(scale: 2), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +private secure(hubitat.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=1500) { + delayBetween(commands.collect{ secure(it) }, delay) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd) + } else { + cmd + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x32: 3, 0x25: 1, 0x98: 1, 0x70: 2, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1, 0x2B: 1]) // can specify command class versions here like in zwave.parse + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 != "" ? "${it.@label}\n" + "${it.Help}" : "" + "${it.Help}", + defaultValue: "${it.@value}", + displayDuringSetup: "${it.@displayDuringSetup}" + break + case "paragraph": + input title: "${it.@label}", + description: "${it.Help}", + type: "paragraph", + element: "paragraph" + break + } + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + sendEvent(name:"numberOfButtons", value:"5") + + if(!state.associationMC1) { + logging("Adding MultiChannel association group 1") + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,1]) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1) + } + if(state.association2){ + logging("Removing association group 2") + cmds << zwave.associationV2.associationRemove(groupingIdentifier:2, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:2) + } + if(state.association4){ + logging("Removing association group 4") + cmds << zwave.associationV2.associationRemove(groupingIdentifier:4, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:4) + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + else if (settings."${it.@index}" != null && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}")) + { + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + case 28: + parValue = (value == "true" ? 1 : 0) + parValue += (settings."fc_2" == "true" ? 2 : 0) + parValue += (settings."fc_3" == "true" ? 4 : 0) + parValue += (settings."fc_4" == "true" ? 8 : 0) + break + case 29: + parValue = (value == "true" ? 1 : 0) + parValue += (settings."sc_2" == "true" ? 2 : 0) + parValue += (settings."sc_3" == "true" ? 4 : 0) + parValue += (settings."sc_4" == "true" ? 8 : 0) + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +private command(hubitat.zwave.Command cmd) { + + if (state.sec && cmd.toString() != "WakeUpIntervalGet()") { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = [0x31: 5, 0x30: 1, 0x9C: 1, 0x70: 2, 0x85: 2] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 1..2) { + addChildDevice("Metering Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, + [completedSetup: true, label: "${device.displayName} (S${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Switch $i"]) + } + } catch (e) { + runIn(2, "sendAlert") + } +} + +private sendAlert() { + sendEvent( + descriptionText: "Child device creation failed. Please make sure that the \"Metering Switch Child Device\" is installed and published.", + eventType: "ALERT", + name: "childDeviceCreation", + value: "failed", + displayed: true, + ) +} + +def configuration_model() +{ +''' + + + +The device will return to the last state before power failure. +0 - the Dimmer 2 does not save the state before a power failure, it returns to the "off" position +1 - the Dimmer 2 restores its state before power failure +Range: 0~1 +Default: 1 (Previous State) + + + + + + +This parameter allows you to choose the operating mode for the 1st channel controlled by the S1 switch. +Range: 0~5 +Default: 0 (Standard) + + + + + + + + + + +This parameter determines how the device in timed mode reacts to pushing the switch connected to the S1 terminal. +Range: 0~2 +Default: 0 (Cancel) + + + + + + + +This parameter allows to set time parameter used in timed modes. +Range: 0~32000 (0.1s, 1-32000s) +Default: 50 + + + + +This parameter allows to set time of switching to opposite state in flashing mode. +Range: 1~32000 (0.1s-3200.0s) +Default: 5 (0.5s) + + + + +This parameter allows you to choose the operating mode for the 2nd channel controlled by the S2 switch. +Range: 0~5 +Default: 0 (Standard) + + + + + + + + + + +This parameter determines how the device in timed mode reacts to pushing the switch connected to the S2 terminal. +Range: 0~2 +Default: 0 (Cancel) + + + + + + + +This parameter allows to set time parameter used in timed modes. +Range: 0~32000 (0.1s, 1-32000s) +Default: 50 + + + + +This parameter allows to set time of switching to opposite state in flashing mode. +Range: 1~32000 (0.1s-3200.0s) +Default: 5 (0.5s) + + + + +Choose between momentary and toggle switch. +Range: 0~2 +Default: 2 (Toggle) + + + + + + + +This parameter determines how the device will react to General Alarm frame. +Range: 0~3 +Default: 3 (Flash) + + + + + + + + +This parameter determines how the device will react to Flood Alarm frame. +Range: 0~3 +Default: 2 (OFF) + + + + + + + + +This parameter determines how the device will react to CO, CO2 or Smoke frame. +Range: 0~3 +Default: 3 (Flash) + + + + + + + + +This parameter determines how the device will react to Heat Alarm frame. +Range: 0~3 +Default: 1 (ON) + + + + + + + + +This parameter allows to set duration of flashing alarm mode. +Range: 1~32000 (1s-32000s) +Default: 600 (10 min) + + + + +The parameter defines the power level change that will result in a new power report being sent. The value is a percentage of the previous report. +Range: 0~100 (1-100%) +Default: 10 + + + + +Parameter 51 defines a time period between consecutive reports. Timer is reset and counted from zero after each report. +Range: 0~120 (1-120s) +Default: 10 + + + + +Energy level change which will result in sending a new energy report. +Range: 0~32000 (0.01-320 kWh) +Default: 100 + + + + +The parameter defines the power level change that will result in a new power report being sent. The value is a percentage of the previous report. +Range: 0~100 (1-100%) +Default: 10 + + + + +Parameter 55 defines a time period between consecutive reports. Timer is reset and counted from zero after each report. +Range: 0~120 (1-120s) +Default: 10 + + + + +Energy level change which will result in sending a new energy report. +Range: 0~32000 (0.01-320 kWh) +Default: 100 + + + + +This parameter determines in what time interval the periodic power reports are sent to the main controller. +Range: 0~32000 (1-32000s) +Default: 3600 + + + + +This parameter determines in what time interval the periodic energy reports are sent to the main controller. +Range: 0~32000 (1-32000s) +Default: 3600 + + + + +Send scene ID on single press + + + + +Send scene ID on double press + + + + +Send scene ID on tripple press + + + + +Send scene ID on hold and release + + + + +Send scene ID on single press + + + + +Send scene ID on double press + + + + +Send scene ID on tripple press + + + + +Send scene ID on hold and release + + + + +Toggle Mode +1 pushed - S1 1x toggle +4 pushed - S1 2x toggle +5 pushed - S1 3x toggle + +1 held - S2 1x toggle +4 held - S2 2x toggle +5 held - S2 3x toggle + +Momentary Mode +1 pushed - S1 1x click +2 pushed - S1 release +3 pushed - S1 hold +4 pushed - S1 2x click +5 pushed - S1 3x click + +1 held - S2 1x click +2 held - S2 release +3 held - S2 hold +4 held - S2 2x click +5 held - S2 3x click + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/fibaro-flood-sensor-advanced.src/fibaro-flood-sensor-advanced.groovy b/Drivers/fibaro-flood-sensor-advanced.src/fibaro-flood-sensor-advanced.groovy new file mode 100644 index 0000000..fc228a0 --- /dev/null +++ b/Drivers/fibaro-flood-sensor-advanced.src/fibaro-flood-sensor-advanced.groovy @@ -0,0 +1,617 @@ +/** + * Fibaro Flood Sensor ZW5 + * + * Copyright 2016 Fibar Group S.A. + * + * 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. + * + */ +metadata { + definition (name: "Fibaro Flood Sensor (Advanced)", namespace: "erocm123", author: "Fibar Group S.A.") { + capability "Battery" + capability "Configuration" + capability "Sensor" + capability "Tamper Alert" + capability "Temperature Measurement" + capability "Water Sensor" + capability "Health Check" + + attribute "needUpdate", "string" + + fingerprint mfr: "010F", prod: "0B01", model: "2002", deviceJoinName: "Fibaro Flood Sensor" + + // Wall Powered + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x22, 0x85, 0x59, 0x20, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x9C, 0x31, 0x86" + // Battery Powered + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x22, 0x85, 0x59, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x9C, 0x31, 0x86", outClusters: "" + } + + 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()) + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"FGFS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + tileAttribute("device.water", key:"PRIMARY_CONTROL") { + attributeState("dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff") + attributeState("wet", icon:"st.alarm.water.wet", backgroundColor:"#00a0dc") + } + + tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { + attributeState("active", label:'tamper active', backgroundColor:"#e86d13") + attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff") + } + } + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + 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" + } + + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "FGFS" + details(["FGFS","battery", "temperature", "configure"]) + } +} + +// parse events into attributes +def parse(String description) { + //logging("Parsing '${description}'") + def result = [] + + if (description.startsWith("Err 106")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + state.sec = 0 + result = createEvent( + descriptionText: "FGFS failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else if (description == "updated") { + return null + } else { + def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72:2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + + if (cmd) { + //logging("Parsed '${cmd}'") + zwaveEvent(cmd) + } + } +} + +//security +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + if (encapsulatedCommand) { + state.sec = 1 + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +//crc16 +def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x31: 5, 0x30: 1, 0x9C: 1, 0x70: 2, 0x85: 2] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (!encapsulatedCommand) { + logging("Could not extract command from $cmd") + } else { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + logging("WakeUpIntervalReport ${cmd.toString()}") + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd) +{ + logging("Device ${device.displayName} woke up") + + def request = update_needed_settings() + + if (!state.lastBatteryReport || (now() - state.lastBatteryReport) / 60000 >= 60 * 24) + { + logging("Over 24hr since last battery report. Requesting report") + request << zwave.batteryV1.batteryGet() + } + + if (!state.lastTempReport || (now() - state.lastTempReport) / 60000 >= 60 * 3) + { + logging("Over 3hr since last temperature report. Requesting report") + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + } + + if(request != []){ + response(encapSequence(request, 500) + ["delay 5000", encap(zwave.wakeUpV1.wakeUpNoMoreInformation())]) + } else { + logging("No commands to send") + response(encap(zwave.wakeUpV1.wakeUpNoMoreInformation())) + } +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + state.MSR = 1 + logging("manufacturerId: ${cmd.manufacturerId}") + logging("manufacturerName: ${cmd.manufacturerName}") + logging("productId: ${cmd.productId}") + logging("productTypeId: ${cmd.productTypeId}") +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { + logging("deviceIdData: ${cmd.deviceIdData}") + logging("deviceIdDataFormat: ${cmd.deviceIdDataFormat}") + logging("deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}") + logging("deviceIdType: ${cmd.deviceIdType}") + + if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format + String serialNumber = "h'" + + cmd.deviceIdData.each{ data -> + serialNumber += "${String.format("%02X", data)}" + } + + updateDataValue("serialNumber", serialNumber) + logging("${device.displayName} - serial number: ${serialNumber}") + } +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") + logging("applicationVersion: ${cmd.applicationVersion}") + logging("applicationSubVersion: ${cmd.applicationSubVersion}") + logging("zWaveLibraryType: ${cmd.zWaveLibraryType}") + logging("zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}") + logging("zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}") +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() + map.unit = "%" + map.displayed = true + state.lastBatteryReport = now() + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + def map = [:] + if (cmd.notificationType == 5) { + switch (cmd.event) { + case 2: + map.name = "water" + map.value = "wet" + map.descriptionText = "${device.displayName} is ${map.value}" + break + + case 0: + map.name = "water" + map.value = "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + map.name = "tamper" + map.value = "inactive" + map.descriptionText = "${device.displayName}: tamper alarm has been deactivated" + break + + case 3: + map.name = "tamper" + map.value = "active" + map.descriptionText = "${device.displayName}: tamper alarm activated" + break + } + } + + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + if (cmd.sensorType == 1) { + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + map.displayed = true + } + state.lastTempReport = now() + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def zwaveEvent(hubitat.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "${device.displayName}: received command: $cmd - device has reset itself" +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unhandled event $cmd") + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + return createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 12 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = update_needed_settings() + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(encapSequence(cmds, 500)) +} + +def ping() { + logging("ping()") + logging("Battery Device - Not sending ping commands") +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + cmds = update_needed_settings() + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() + if (cmds != []) encapSequence(cmds, 500) +} + +private secure(hubitat.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crc16(hubitat.zwave.Command cmd) { + //zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() + "5601${cmd.format()}0000" +} + +private encapSequence(commands, delay=500) { + delayBetween(commands.collect{ encap(it) }, delay) +} + +private encap(hubitat.zwave.Command cmd) { + def secureClasses = [0x20, 0x5A, 0x71, 0x85, 0x8E, 0x9C, 0x70] + + //todo: check if secure inclusion was successful + //if not do not send security-encapsulated command + if (state.sec != 0) { + logging("Sending Secure $cmd") + secure(cmd) + } else { + logging("Sending crc16 $cmd") + crc16(cmd) + } +} + + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 != "" ? "${it.@label}\n" + "${it.Help}" : "" + "${it.Help}", + defaultValue: "${it.@value}", + displayDuringSetup: "${it.@displayDuringSetup}" + break + } + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(state.wakeInterval == null || state.wakeInterval != 21600){ + logging("Setting Wake Interval to 21600") + cmds << zwave.wakeUpV2.wakeUpIntervalSet(seconds: 21600, nodeid:zwaveHubNodeId) + cmds << zwave.wakeUpV1.wakeUpIntervalGet() + } + + if(state.MSR == null){ + logging("Getting Manufacturer Specific Info") + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet() + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV2.configurationGet(parameterNumber: it.@index.toInteger()) + } + else if (settings."${it.@index}" != null && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}")) + { + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue.toInteger(), it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + return value.toInteger() +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def configuration_model() +{ +''' + + + +in seconds +Range: 0 to 3600 +Default: 0 (No Delay) + + + + +In seconds +Range: 1 to 65535 +Default: 300 + + + + +Each .01 +Determines a minimum temperature change value (insensitivity level), resulting in a temperature report being sent to the main controller. +Range: 1 to 1000 +Default: 50 + + + + +Range: 0 to 3 +Default: 3 (Acoustic and visual alarms active) + + + + + + + + +each 0.01 +Range: -1000 to 10000 +Default: 1500 + + + + +each 0.01 +Range: -10000 to 10000 +Default: 3500 + + + + +Range: 0 to 16777215 +Default: Blue + + + + + + + + + + + + +Range: 0 to 16777215 +Default: Red + + + + + + + + + + + + +Visual indicator indicates the temperature (blink) every Temperature Measurement Interval +Range: 0 to 2 +Default: + + + + + + + +Parameter stores a temperature value to be added to or deducted from the current temperature measured by internal temperature sensor in order to compensate the difference between air temperature and temperature at the floor level. +Range: -10000 to 10000(Default) +Default: 0 + + + + +The user can silence the Flood Sensor. Because the Sensor’s alarm may last for a long time, it’s possible to turn off visual and audible alarm signaling to save battery. +Range: 0 to 65535 +Default: 0 + + + + +Range: 0 to 1 +Default: On + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/fibaro-rgbw-controller-advanced.src/fibaro-rgbw-controller-advanced.groovy b/Drivers/fibaro-rgbw-controller-advanced.src/fibaro-rgbw-controller-advanced.groovy new file mode 100644 index 0000000..5e3acf4 --- /dev/null +++ b/Drivers/fibaro-rgbw-controller-advanced.src/fibaro-rgbw-controller-advanced.groovy @@ -0,0 +1,479 @@ +/** + * Copyright 2016 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. + * + * Fibaro RGBW Controller (Advanced) + * + * Author: Eric Maycock (erocm123) + * Date: 2016-11-02 + */ + +metadata { + definition (name: "Fibaro RGBW Controller (Advanced)", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch Level" + capability "Color Control" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Actuator" + capability "Sensor" + + command "reset" + command "refresh" + + (1..5).each { n -> + attribute "switch$n", "enum", ["on", "off"] + command "on$n" + command "off$n" + } + + fingerprint deviceId: "0x1101", inClusters: "0x5E, 0x26, 0x33, 0x27, 0x2C, 0x2B, 0x70, 0x59, 0x85, 0x72, 0x86, 0x7A, 0x73, 0xEF, 0x5A, 0x82" + + } + + preferences { + input description: "Create a custom program by modifying the settings below. This program can then be executed by using the \"custom\" button on the device page.", displayDuringSetup: false, type: "paragraph", element: "paragraph" + + input "transition", "enum", title: "Transition", defaultValue: 0, displayDuringSetup: false, required: false, options: [ + 0:"Smooth", + 1073741824:"Flash", + //3221225472:"Fade Out Fade In" + ] + input "count", "number", title: "Cycle Count (0 [unlimited])", defaultValue: 0, displayDuringSetup: false, required: false, range: "0..254" + input "speed", "enum", title: "Color Change Speed", defaultValue: 0, displayDuringSetup: false, required: false, options: [ + 0:"Fast", + 16:"Medium Fast", + 32:"Medium", + 64:"Medium Slow", + 128:"Slow"] + input "speedLevel", "number", title: "Color Residence Time (1 [fastest], 254 [slowest])", defaultValue: "0", displayDuringSetup: true, required: false, range: "0..254" + (1..8).each { i -> + input "color$i", "enum", title: "Color $i", displayDuringSetup: false, required: false, options: [ + 1:"Red", + 2:"Orange", + 3:"Yellow", + 4:"Green", + 5:"Cyan", + 6:"Blue", + 7:"Violet", + 8:"Pink"] + } + } + + simulator { + } + + tiles (scale: 2){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } + + 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.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + valueTile("colorTempTile", "device.colorTemperature", decoration: "flat", height: 2, width: 2) { + state "colorTemperature", label:'${currentValue}%', backgroundColor:"#FFFFFF" + } + controlTile("colorTempControl", "device.colorTemperature", "slider", decoration: "flat", height: 2, width: 4, inactiveLabel: false) { + state "colorTemperature", action:"setColorTemperature" + } + standardTile("switch1", "switch1", canChangeIcon: true, width: 2, height: 2) { + state "off", label: "fireplace", action: "on1", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState:"on" + state "on", label: "fireplace", action: "off1", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState:"off" + } + standardTile("switch2", "switch2", canChangeIcon: true, width: 2, height: 2) { + state "off", label: "storm", action: "on2", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState:"on" + state "on", label: "storm", action: "off2", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState:"off" + } + standardTile("switch3", "switch3", canChangeIcon: true, width: 2, height: 2) { + state "off", label: "deepfade", action: "on3", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState:"on" + state "on", label: "deepfade", action: "off3", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState:"off" + } + standardTile("switch4", "switch4", canChangeIcon: true, width: 2, height: 2) { + state "off", label: "litefade", action: "on4", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState:"on" + state "on", label: "litefade", action: "off4", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState:"off" + } + standardTile("switch5", "switch5", canChangeIcon: true, width: 2, height: 2) { + state "off", label: "police", action: "on5", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState:"on" + state "on", label: "police", action: "off5", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState:"off" + } + } + + main(["switch"]) + details(["switch", "levelSliderControl", + "colorTempControl", "colorTempTile", + "switch1", "switch2", "switch3", + "switch4", "switch5", "switch6", + "refresh", "configure" ]) +} + +def updated() { + response(refresh()) +} + +def parse(description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$cmd' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + //dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + if (cmd.value == 0) toggleTiles("all") + return result +} + +def zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) { + response(command(zwave.switchMultilevelV1.switchMultilevelGet())) +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof hubitat.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + if (cmd.parameterNumber == 37) { + if (cmd.configurationValue[0] == 0) toggleTiles("all") + } else { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" + } +} + + +def zwaveEvent(hubitat.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +private toggleTiles(value) { + def tiles = ["switch1", "switch2", "switch3", "switch4", "switch5", "switch6"] + tiles.each {tile -> + if (tile != value) { sendEvent(name: tile, value: "off") } + else { sendEvent(name:tile, value:"on"); sendEvent(name:"switch", value:"on") } + } +} + +def on() { + toggleTiles("all") + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 2000) +} + +def off() { + toggleTiles("all") + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 2000) +} + +def setLevel(level) { + toggleTiles("all") + setLevel(level, 1) +} + +def setLevel(level, duration) { + if(level > 99) level = 99 + commands([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], (duration && duration < 12) ? (duration * 1000) : 3500) +} + +def refresh() { + commands([ + zwave.switchMultilevelV3.switchMultilevelGet(), + zwave.configurationV1.configurationGet(parameterNumber: 37), + ], 1000) +} + +def setSaturation(percent) { + log.debug "setSaturation($percent)" + setColor(saturation: percent) +} + +def setHue(value) { + log.debug "setHue($value)" + setColor(hue: value) +} + +def setColor(value) { + def result = [] + def warmWhite = 0 + def coldWhite = 0 + log.debug "setColor: ${value}" + if (value.hue && value.saturation) { + log.debug "setting color with hue & saturation" + def hue = value.hue ?: device.currentValue("hue") + def saturation = value.saturation ?: device.currentValue("saturation") + if(hue == null) hue = 13 + if(saturation == null) saturation = 13 + def rgb = huesatToRGB(hue as Integer, saturation as Integer) + if ( value.hue == 53 && value.saturation == 91 ) { + Random rand = new Random() + int max = 100 + hue = rand.nextInt(max+1) + rgb = huesatToRGB(hue as Integer, saturation as Integer) + } + else if ( value.hue == 23 && value.saturation == 56 ) { + def level = 255 + if ( value.level != null ) level = value.level * 0.01 * 255 + warmWhite = level + coldWhite = 0 + rgb[0] = 0 + rgb[1] = 0 + rgb[2] = 0 + } + else { + if ( value.hue > 5 && value.hue < 100 ) hue = value.hue - 5 else hue = 1 + rgb = huesatToRGB(hue as Integer, saturation as Integer) + } + result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:warmWhite, coldWhite:coldWhite) + if(value.level != null && value.level > 1){ + if(value.level > 99) value.level = 99 + result << zwave.switchMultilevelV3.switchMultilevelSet(value: value.level, dimmingDuration: 3500) + result << zwave.switchMultilevelV3.switchMultilevelGet() + } + } + else if (value.hex) { + def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0) + } + result << zwave.basicV1.basicSet(value: 0xFF) + if(value.hue) sendEvent(name: "hue", value: value.hue) + if(value.hex) sendEvent(name: "color", value: value.hex) + if(value.switch) sendEvent(name: "switch", value: value.switch) + if(value.saturation) sendEvent(name: "saturation", value: value.saturation) + + toggleTiles("all") + commands(result) +} + +def setColorTemperature(percent) { + if(percent > 99) percent = 99 + int warmValue = percent * 255 / 99 + toggleTiles("all") + sendEvent(name: "colorTemperature", value: percent) + command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite:warmValue, coldWhite:(255 - warmValue))) +} + +def reset() { + log.debug "reset()" + sendEvent(name: "color", value: "#ffffff") + setColorTemperature(99) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def rgbToHSV(red, green, blue) { + float r = red / 255f + float g = green / 255f + float b = blue / 255f + float max = [r, g, b].max() + float delta = max - [r, g, b].min() + def hue = 13 + def saturation = 0 + if (max && delta) { + saturation = 100 * delta / max + if (r == max) { + hue = ((g - b) / delta) * 100 / 6 + } else if (g == max) { + hue = (2 + (b - r) / delta) * 100 / 6 + } else { + hue = (4 + (r - g) / delta) * 100 / 6 + } + } + [hue: hue, saturation: saturation, value: max * 100] +} + +def huesatToRGB(float hue, float sat) { + while(hue >= 100) hue -= 100 + int h = (int)(hue / 100 * 6) + float f = hue / 100 * 6 - h + int p = Math.round(255 * (1 - (sat / 100))) + int q = Math.round(255 * (1 - (sat / 100) * f)) + int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) + switch (h) { + case 0: return [255, t, p] + case 1: return [q, 255, p] + case 2: return [p, 255, t] + case 3: return [p, q, 255] + case 4: return [t, p, 255] + case 5: return [255, p, q] + } +} + +def on1() { + log.debug "on1()" + toggleTiles("switch1") + commands([ + zwave.configurationV1.configurationSet(scaledConfigurationValue: 6, parameterNumber: 72, size: 1), + ], 1500) +} + +def on2() { + log.debug "on2()" + toggleTiles("switch2") + commands([ + zwave.configurationV1.configurationSet(scaledConfigurationValue: 7, parameterNumber: 72, size: 1), + ], 1500) +} + +def on3() { + log.debug "on3()" + toggleTiles("switch3") + commands([ + zwave.configurationV1.configurationSet(scaledConfigurationValue: 8, parameterNumber: 72, size: 1), + ], 1500) +} + +def on4() { + log.debug "on4()" + toggleTiles("switch4") + commands([ + zwave.configurationV1.configurationSet(scaledConfigurationValue: 9, parameterNumber: 72, size: 1), + ], 1500) +} + +def on5() { + log.debug "on5()" + toggleTiles("switch5") + commands([ + zwave.configurationV1.configurationSet(scaledConfigurationValue: 10, parameterNumber: 72, size: 1), + ], 1500) +} + +def offCmd() { + log.debug "offCmd()" + commands([ + zwave.configurationV1.configurationSet(scaledConfigurationValue: 0, parameterNumber: 72, size: 1), + ], 1500) +} + +def off1() { offCmd() } +def off2() { offCmd() } +def off3() { offCmd() } +def off4() { offCmd() } +def off5() { offCmd() } +def off6() { offCmd() } + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + + +private calculateParameter(number) { + long value = 0 + switch (number){ + case 37: + value += settings.transition ? settings.transition.toLong() : 0 + value += 33554432 // Custom Mode + value += settings.count ? (settings.count.toLong() * 65536) : 0 + value += settings.speed ? (settings.speed.toLong() * 256) : 0 + value += settings.speedLevel ? settings.speedLevel.toLong() : 0 + break + case 38: + value += settings.color1 ? (settings.color1.toLong() * 1) : 0 + value += settings.color2 ? (settings.color2.toLong() * 16) : 0 + value += settings.color3 ? (settings.color3.toLong() * 256) : 0 + value += settings.color4 ? (settings.color4.toLong() * 4096) : 0 + value += settings.color5 ? (settings.color5.toLong() * 65536) : 0 + value += settings.color6 ? (settings.color6.toLong() * 1048576) : 0 + value += settings.color7 ? (settings.color7.toLong() * 16777216) : 0 + value += settings.color8 ? (settings.color8.toLong() * 268435456) : 0 + break + } + return value +} \ No newline at end of file diff --git a/Drivers/fibaro-single-switch-2-fgs-213.src/fibaro-single-switch-2-fgs-213.groovy b/Drivers/fibaro-single-switch-2-fgs-213.src/fibaro-single-switch-2-fgs-213.groovy new file mode 100644 index 0000000..6faba3a --- /dev/null +++ b/Drivers/fibaro-single-switch-2-fgs-213.src/fibaro-single-switch-2-fgs-213.groovy @@ -0,0 +1,707 @@ +/** + * + * Copyright 2017 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. + * + * Fibaro Single Switch 2 FGS-213 + * + * Author: Eric Maycock (erocm123) + * + */ + +metadata { + definition (name: "Fibaro Single Switch 2 FGS-213", namespace: "erocm123", author: "Eric Maycock") { + capability "Sensor" + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Configuration" + capability "Refresh" + capability "Energy Meter" + capability "Power Meter" + capability "Health Check" + capability "PushableButton" + capability "HoldableButton" + + command "reset" + + fingerprint mfr: "010F", prod: "0403", model: "2000", deviceJoinName: "Fibaro Single Switch 2" + fingerprint mfr: "010F", prod: "0403", model: "1000", deviceJoinName: "Fibaro Single Switch 2" + + fingerprint deviceId: "0x1001", inClusters:"0x5E,0x86,0x72,0x59,0x73,0x22,0x56,0x32,0x71,0x98,0x7A,0x25,0x5A,0x85,0x70,0x8E,0x60,0x75,0x5B" +} + +simulator { +} + +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 ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + 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("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch", + "power","energy","reset", + "refresh","configure"]) + +} + 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()) + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + //log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + + def statusTextmsg = "" + + result.each { + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "power") { + statusTextmsg = "${it.value} W ${device.currentValue('energy')? device.currentValue('energy') : "0"} kWh" + } + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "energy") { + statusTextmsg = "${device.currentValue('power')? device.currentValue('power') : "0"} W ${it.value} kWh" + } + } + if (statusTextmsg != "") sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + log.debug "BasicReport $cmd" +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd, ep=null) { + logging("BasicSet: $cmd : Endpoint: $ep") + def event + if (!ep) { + event = [createEvent([name: "switch", value: cmd.value? "on":"off"])] + } + return event +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=null) +{ + logging("SwitchBinaryReport: $cmd : Endpoint: $ep") + def event + if (!ep) { + event = [createEvent([name: "switch", value: cmd.value? "on":"off"])] + } + return event +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd, ep=null) { + logging("MeterReport: $cmd : Endpoint: $ep") + def result + def cmds = [] + if (cmd.scale == 0) { + result = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + result = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } else { + result = [name: "power", value: cmd.scaledMeterValue, unit: "W"] + } + if (!ep) { + return createEvent(result) + } +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport $cmd" + if (zwaveHubNodeId in cmd.nodeId) state."association${cmd.groupingIdentifier}" = true + else state."association${cmd.groupingIdentifier}" = false +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled event $cmd" + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + return createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def refresh() { + def cmds = [] + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + cmds << zwave.associationV2.associationGet(groupingIdentifier:2) + cmds << zwave.associationV2.associationGet(groupingIdentifier:3) + cmds << zwave.associationV2.associationGet(groupingIdentifier:4) + cmds << zwave.associationV2.associationGet(groupingIdentifier:5) + cmds << zwave.switchBinaryV1.switchBinaryGet() + cmds << zwave.meterV2.meterGet(scale: 0) + cmds << zwave.meterV2.meterGet(scale: 2) + secureSequence(cmds, 1000) +} + +def reset() { + def cmds = [] + cmds << zwave.meterV2.meterReset() + cmds << zwave.meterV2.meterGet(scale: 0) + cmds << zwave.meterV2.meterGet(scale: 2) + secureSequence(cmds, 1000) +} + +def ping() { + refresh() +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def poll() { + refresh() +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) secureSequence(cmds) +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging("CentralSceneNotification: $cmd") + logging("sceneNumber: $cmd.sceneNumber") + logging("sequenceNumber: $cmd.sequenceNumber") + logging("keyAttributes: $cmd.keyAttributes") + + buttonEvent(cmd.keyAttributes + 1, (cmd.sceneNumber == 1? "pushed" : "held")) + +} + +def buttonEvent(button, value) { + logging("buttonEvent() Button:$button, Value:$value") + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true) +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + if (cmds != []) response(secureSequence(cmds)) +} + +def on() { + secureSequence([ + zwave.basicV1.basicSet(value: 0xFF) + ]) +} +def off() { + secureSequence([ + zwave.basicV1.basicSet(value: 0x00) + ]) +} + +private secure(hubitat.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=1500) { + delayBetween(commands.collect{ secure(it) }, delay) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd) + } else { + cmd + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x32: 3, 0x25: 1, 0x98: 1, 0x70: 2, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1, 0x2B: 1]) // can specify command class versions here like in zwave.parse + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 != "" ? "${it.@label}\n" + "${it.Help}" : "" + "${it.Help}", + defaultValue: "${it.@value}", + displayDuringSetup: "${it.@displayDuringSetup}" + break + case "paragraph": + input title: "${it.@label}", + description: "${it.Help}", + type: "paragraph", + element: "paragraph" + break + } + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(!state.association1){ + logging("Setting association group 1") + cmds << zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []) + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + } + + sendEvent(name:"numberOfButtons", value:"5") + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + else if (settings."${it.@index}" != null && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}")) + { + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + case 28: + parValue = (value == "true" ? 1 : 0) + parValue += (settings."fc_2" == "true" ? 2 : 0) + parValue += (settings."fc_3" == "true" ? 4 : 0) + parValue += (settings."fc_4" == "true" ? 8 : 0) + break + case 29: + parValue = (value == "true" ? 1 : 0) + parValue += (settings."sc_2" == "true" ? 2 : 0) + parValue += (settings."sc_3" == "true" ? 4 : 0) + parValue += (settings."sc_4" == "true" ? 8 : 0) + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +private command(hubitat.zwave.Command cmd) { + + if (state.sec && cmd.toString() != "WakeUpIntervalGet()") { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = [0x31: 5, 0x30: 1, 0x9C: 1, 0x70: 2, 0x85: 2] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def configuration_model() +{ +''' + + + +The device will return to the last state before power failure. +0 - the Dimmer 2 does not save the state before a power failure, it returns to the "off" position +1 - the Dimmer 2 restores its state before power failure +Range: 0~1 +Default: 1 (Previous State) + + + + + + +This parameter allows you to choose the operating mode for the 1st channel controlled by the S1 switch. +Range: 0~5 +Default: 0 (Standard) + + + + + + + + + + +This parameter determines how the device in timed mode reacts to pushing the switch connected to the S1 terminal. +Range: 0~2 +Default: 0 (Cancel) + + + + + + + +This parameter allows to set time parameter used in timed modes. +Range: 0~32000 (0.1s, 1-32000s) +Default: 50 + + + + +This parameter allows to set time of switching to opposite state in flashing mode. +Range: 1~32000 (0.1s-3200.0s) +Default: 5 (0.5s) + + + + +Choose between momentary and toggle switch. +Range: 0~2 +Default: 2 (Toggle) + + + + + + + +This parameter determines how the device will react to General Alarm frame. +Range: 0~3 +Default: 3 (Flash) + + + + + + + + +This parameter determines how the device will react to Flood Alarm frame. +Range: 0~3 +Default: 2 (OFF) + + + + + + + + +This parameter determines how the device will react to CO, CO2 or Smoke frame. +Range: 0~3 +Default: 3 (Flash) + + + + + + + + +This parameter determines how the device will react to Heat Alarm frame. +Range: 0~3 +Default: 1 (ON) + + + + + + + + +This parameter allows to set duration of flashing alarm mode. +Range: 1~32000 (1s-32000s) +Default: 600 (10 min) + + + + +The parameter defines the power level change that will result in a new power report being sent. The value is a percentage of the previous report. +Range: 0~100 (1-100%) +Default: 10 + + + + +Parameter 51 defines a time period between consecutive reports. Timer is reset and counted from zero after each report. +Range: 0~120 (1-120s) +Default: 10 + + + + +Energy level change which will result in sending a new energy report. +Range: 0~32000 (0.01-320 kWh) +Default: 100 + + + + +This parameter determines in what time interval the periodic power reports are sent to the main controller. +Range: 0~32000 (1-32000s) +Default: 3600 + + + + +This parameter determines in what time interval the periodic energy reports are sent to the main controller. +Range: 0~32000 (1-32000s) +Default: 3600 + + + + +Send scene ID on single press + + + + +Send scene ID on double press + + + + +Send scene ID on tripple press + + + + +Send scene ID on hold and release + + + + +Send scene ID on single press + + + + +Send scene ID on double press + + + + +Send scene ID on tripple press + + + + +Send scene ID on hold and release + + + + +Toggle Mode +1 pushed - S1 1x toggle +4 pushed - S1 2x toggle +5 pushed - S1 3x toggle + +1 held - S2 1x toggle +4 held - S2 2x toggle +5 held - S2 3x toggle + +Momentary Mode +1 pushed - S1 1x click +2 pushed - S1 release +3 pushed - S1 hold +4 pushed - S1 2x click +5 pushed - S1 3x click + +1 held - S2 1x click +2 held - S2 release +3 held - S2 hold +4 held - S2 2x click +5 held - S2 3x click + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/fosbaby-lullaby-control.src/fosbaby-lullaby-control.groovy b/Drivers/fosbaby-lullaby-control.src/fosbaby-lullaby-control.groovy new file mode 100644 index 0000000..27e1d7a --- /dev/null +++ b/Drivers/fosbaby-lullaby-control.src/fosbaby-lullaby-control.groovy @@ -0,0 +1,484 @@ +/** + * Copyright 2016 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. + * + * Fosbaby Lullaby Control + * + * Author: Eric Maycock (erocm123) + * Date: 2016-01-27 + */ + +import groovy.util.XmlSlurper + +metadata { + definition (name: "Fosbaby Lullaby Control", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Indicator" + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Sensor" + capability "Music Player" + capability "Temperature Measurement" + capability "Health Check" + + command "playOne" + command "playTwo" + command "playThree" + command "playFour" + command "playFive" + command "playRandom" + command "mode" + command "timer" + } + + simulator { + } + + preferences { + input("ip", "string", title:"IP Address", description: "192.168.1.150", required: true, displayDuringSetup: true) + input("port", "string", title:"Port", description: "88", required: true, displayDuringSetup: true) + input("userName", "string", title:"User Name", required:true, displayDuringSetup:true) + input("password", "password", title:"Password", required:false, displayDuringSetup:true) + input("enableDebugging", "boolean", title:"Enable Debugging", value:false, required:false, displayDuringSetup:false) + } + + tiles (scale: 2){ + multiAttributeTile(name:"main", type:"generic", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState "temperature",label:'${currentValue}°', icon:"st.Entertainment.entertainment2", backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + } + + standardTile("play", "device.status", inactiveLabel:false, decoration:"flat", width: 2, height: 2) { + state "stopped", label:'', icon:"st.sonos.play-btn", nextState:"playing", action:"Music Player.play" + state "playing", label:'', icon:"st.sonos.stop-btn", nextState:"stopped", action:"Music Player.stop" + } + + standardTile("nextTrack", "device.status", inactiveLabel:false, decoration:"flat", width: 2, height: 2) { + state "default", label:'', icon:"st.sonos.next-btn", action:"Music Player.nextTrack" + } + + standardTile("previousTrack", "device.status", inactiveLabel:false, decoration:"flat", width: 2, height: 2) { + state "default", label:'', icon:"st.sonos.previous-btn", action:"music Player.previousTrack" + } + + controlTile("volume", "device.level", "slider", height:1, width:6, inactiveLabel:false) { + state "level", action:"Music Player.setLevel" + } + + 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.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on", nextState:"never" + state "never", action:"indicator.indicatorWhenOn", icon:"st.indicators.never-lit", nextState:"when on" + } + standardTile("mode", "device.mode", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "order", label:"In Order", icon:"", nextState:"loopsong", action:"mode" + state "loopsong", label:"Loop Song", icon:"", nextState:"looplist", action:"mode" + state "looplist", label:"Loop List", icon:"", nextState:"order", action:"mode" + } + + standardTile("timer", "device.timer", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "off", label:"No Timer", icon:"", nextState:"10", action:"timer" + state "10", label:"10 Minutes", icon:"", nextState:"20", action:"timer" + state "20", label:"20 Minutes", icon:"", nextState:"30", action:"timer" + state "30", label:"30 Minutes", icon:"", nextState:"off", action:"timer" + } + + standardTile("playOne", "device.playOne", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "off", label:"Song 1", action:"playOne", icon:"", backgroundColor:"#ffffff" + state "on", label:"Song 1", action:"playOne", icon:"", backgroundColor:"#00a0dc" + } + standardTile("playTwo", "device.playTwo", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "off", label:"Song 2", action:"playTwo", icon:"", backgroundColor:"#ffffff" + state "on", label:"Song 2", action:"playTwo", icon:"", backgroundColor:"#00a0dc" + } + standardTile("playThree", "device.playThree", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "off", label:"Song 3", action:"playThree", icon:"", backgroundColor:"#ffffff" + state "on", label:"Song 3", action:"playThree", icon:"", backgroundColor:"#00a0dc" + } + standardTile("playFour", "device.playFour", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "off", label:"Song 4", action:"playFour", icon:"", backgroundColor:"#ffffff" + state "on", label:"Song 4", action:"playFour", icon:"", backgroundColor:"#00a0dc" + } + standardTile("playFive", "device.playFive", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "off", label:"Song 5", action:"playFive", icon:"", backgroundColor:"#ffffff" + state "on", label:"Song 5", action:"playFive", icon:"", backgroundColor:"#00a0dc" + } + standardTile("playRandom", "device.playRandom", decoration: "flat", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false) { + state "off", label:"Random", action:"playRandom", icon:"", backgroundColor:"#ffffff" + state "on", label:"Random", action:"playRandom", icon:"", backgroundColor:"#00a0dc" + } + + } + + main "main" + details(["main", "volume", + "previousTrack", "play", "nextTrack", + "mode", "timer", "temperature", + "playOne", "playTwo", "playThree", + "playFour", "playFive", "playRandom", + "indicator", "refresh",]) +} + +def installed() { + logging("installed()") + configure() +} + +def updated() { + logging("updated()") + configure() +} + +def configure() { + logging("configure()") + logging("Configuring Device For SmartThings Use") + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) + state.enableDebugging = settings.enableDebugging + if (state.MAC != null) state.dni = setDeviceNetworkId(state.MAC) + else if (ip != null && port != null) state.dni = setDeviceNetworkId(ip, port) +} + +def parse(Map description) { + def eventMap + eventMap = [name:"$description.name", value:"$description.value"] + [createEvent(eventMap), response(refresh())] +} + +def parse(description) { + def events = [] + def descMap = parseDescriptionAsMap(description) + + if (!state.MAC || state.MAC != descMap["mac"]) { + logging("Mac address of device found ${descMap["mac"]}") + updateDataValue("MAC", descMap["mac"]) + state.dni = state.MAC + } + + def body = new String(descMap["body"].decodeBase64()) + + def result = new XmlSlurper().parseText(body) + def descriptionText = "$device.displayName is playing $currentTrackDescription" + if (result.index != "") { + logging("Current song is ${result.index}") + toggleTiles(getSongName(result.index)) + events << createEvent(name: getSongName(result.index), value: "on", displayed:false) + events << createEvent(name: "trackDescription", value: "Song $result.index", descriptionText: "$device.displayName is playing song $result.index") + } + if (result.dormantTime != "") { + logging("Timer is currently set to ${ result.dormantTime == -1 ? 'off' : result.dormantTime}") + events << createEvent(name:"timer", value: (result.dormantTime == -1 ? 'off' : result.dormantTime)) + } + if (result.degree == "" && result.state != "") { + logging("The device is currently ${ result.state == 1 ? 'playing' : 'stopped' }") + events << createEvent(name:"status", value: (result.state == 1 ? 'playing' : 'stopped')) + } + if (result.mode != "") { + logging("The device mode is currently \"$mode\"") + def mode + switch (result.mode.toInteger()) { + case 1: + mode = "order" + break + case 2: + mode = "loopsong" + break + case 3: + mode = "looplist" + break + } + events << createEvent(name:"mode", value:mode) + } + if (result.degree != "") { + logging("Current temperature is ${result.degree}") + if(getTemperatureScale() == "C"){ + events << createEvent([name:"temperature", value: Math.round(result.degree.toInteger() * 100) / 100]) + } else { + events << createEvent([name:"temperature", value: Math.round(celsiusToFahrenheit(result.degree.toInteger()) * 100) / 100]) + } + } + if (result.humidity != "") { + logging("Current humidity is ${result.humidity}") + events << createEvent(name:'humidity', value:result.humidity) + } + if (result.volume != "") { + logging("Current volume is ${result.volume}") + events << createEvent(name:'level', value:result.volume) + } + if (result.isEnable != "") { + logging("Indicator light is ${(result.isEnable == 1 ? "on" : "off")}") + events << createEvent(name:'indicatorStatus', value: (result.isEnable == 1 ? "when on" : "never")) + } + + if (events) return events +} + +private toggleTiles(value) { + def tiles = ["playOne", "playTwo", "playThree", "playFour", "playFive", "playRandom"] + tiles.each {tile -> + if (tile != value) sendEvent(name: tile, value: "off", displayed:false) + } +} + +private getSongName(song) { + switch (song.toInteger()) { + case 1: return "playOne"; break + case 2: return "playTwo"; break + case 3: return "playThree"; break + case 4: return "playFour"; break + case 5: return "playFive"; break + case 6: return "playEnd"; break + } +} + +def parseDescriptionAsMap(description) { + description.split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def on() { + play() +} + +def off() { + stop() +} + +def play() { + logging("play()") + playTrack(1) +} + +def stop() { + logging("stop()") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayStop") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicPlayState") + return cmds +} + +def pause() { + logging("pause()") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayStop") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicPlayState") + return cmds +} + +def nextTrack() { + logging("nextTrack()") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayNext") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicPlayState") + return cmds +} + +def previousTrack() { + logging("previousTrack()") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayPre") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicPlayState") + return cmds +} + +def setLevel(number) { + logging("setLevel(${number})") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setAudioVolume&volume=${number}") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getAudioVolume") + return cmds +} + +def refresh() { + logging("refresh()") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicPlayState") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getTemperatureState") + return delayBetween(cmds, 1000) +} + +def ping() { + logging("ping()") + return postAction("/cgi-bin/CGIProxy.fcgi?cmd=getTemperatureState") +} + +def indicatorWhenOn() { + logging("indicatorWhenOn()") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setLedEnableState&isEnable=1") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getLedEnableState") + return delayBetween(cmds, 1000) +} + +def indicatorNever() { + logging("indicatorNever()") + def cmds = [] + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setLedEnableState&isEnable=0") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getLedEnableState") + return delayBetween(cmds, 1000) +} + +def poll() { + logging("poll()") + return refresh() +} + +private postAction(uri){ + logging("uri ${uri}") + if (userName && password) uri = uri + "&usr=$userName&pwd=$password" + updateDNI() + def headers = getHeader() + def hubAction = new hubitat.device.HubAction( + method: "GET", + path: uri, + headers: headers + ) + 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" + } + log.debug "Device Network Id set to ${myDNI}" + return myDNI +} + +private updateDNI() { + if (device.deviceNetworkId != state.dni) { + device.deviceNetworkId = state.dni + } +} + +private getHostAddress() { + return "${ip}:${port}" +} + +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 getHeader(){ + def headers = [:] + headers.put("Host", getHostAddress()) + return headers +} + +private def logging(message) { + if (state.enableDebugging == "true") log.debug message +} + +def playTrack(number) { + def cmds = [] + cmds << stop() + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayStart&mode=1&index=${number - 1}&name=default") + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicPlayState") + return delayBetween(cmds, 1000) +} + +def playOne() { + logging("playOne()") + playTrack(1) +} +def playTwo() { + logging("playTwo()") + playTrack(2) +} +def playThree() { + logging("playThree()") + playTrack(3) +} +def playFour() { + logging("playFour()") + playTrack(4) +} +def playFive() { + logging("playFive()") + playTrack(5) +} +def playRandom() { + logging("playRandom()") + Random rand = new Random() + int max = 4 + playTrack(rand.nextInt(max+1) + 1) +} + +def mode() { + logging("mode()") + def cmds = [] + switch (device.currentValue('mode')) { + case "order": + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayMode&mode=2") + break + case "loopsong": + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayMode&mode=3") + break + case "looplist": + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayMode&mode=1") + break + default: + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicPlayMode&mode=1") + break + } + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicPlayState") + return cmds +} + +def timer() { + logging("timer()") + def cmds = [] + switch (device.currentValue('timer')) { + case "off": + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicDormantTime&minutes=10") + break + case "10": + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicDormantTime&minutes=20") + break + case "20": + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicDormantTime&minutes=30") + break + case "30": + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicDormantTime&minutes=-1") + break + default: + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=setMusicDormantTime&minutes=-1") + break + } + cmds << postAction("/cgi-bin/CGIProxy.fcgi?cmd=getMusicDormantTime") + return cmds +} \ No newline at end of file diff --git a/Drivers/generic-dual-relay.src/generic-dual-relay.groovy b/Drivers/generic-dual-relay.src/generic-dual-relay.groovy new file mode 100644 index 0000000..75870a9 --- /dev/null +++ b/Drivers/generic-dual-relay.src/generic-dual-relay.groovy @@ -0,0 +1,286 @@ +/** + * + * Generic Dual Relay Device Type + * + * Author: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2015-10-29 + * + * 2016-01-13: Fixed an error in the MultiChannelCmdEncap method that was stopping the instant status + * update from working correctly. Also removed some unnecessary code. + * 2015-11-17: Added the ability to change config parameters through the device preferences + * + * + * Device Type supports all the feautres of the Pan04 device including both switches, + * current energy consumption in W and cumulative energy consumption in kWh. + */ + +metadata { +definition (name: "Generic Dual Relay", namespace: "erocm123", author: "Eric Maycock") { +capability "Switch" +capability "Polling" +capability "Configuration" +capability "Refresh" +capability "Energy Meter" +capability "Zw Multichannel" + +attribute "switch1", "string" +attribute "switch2", "string" + + +command "on1" +command "off1" +command "on2" +command "off2" +command "reset" + +//fingerprint deviceId: "0x1001", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x25, 0x20, 0x27, 0x71, 0x2B, 0x2C, 0x75, 0x7A, 0x60, 0x32, 0x70" +} + +simulator { +status "on": "command: 2003, payload: FF" +status "off": "command: 2003, payload: 00" + +// reply messages +reply "2001FF,delay 100,2502": "command: 2503, payload: FF" +reply "200100,delay 100,2502": "command: 2503, payload: 00" +} + +tiles { + + standardTile("switch1", "device.switch1",canChangeIcon: true) { + state "on", label: "switch1", action: "off1", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: "switch1", action: "on1", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("switch2", "device.switch2",canChangeIcon: true) { + state "on", label: "switch2", action: "off2", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: "switch2", action: "on2", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + standardTile("configure", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + valueTile("power", "device.power", decoration: "flat") { + state "default", label:'${currentValue} W' + } + + main(["switch1", "switch2"]) + details(["switch1","switch2","refresh","energy","power","reset","configure"]) +} + preferences { + input "deviceType", "enum", title: "Switch Model", defaultValue: "1", displayDuringSetup: true, required: false, options: [ + "1":"Philio", + "2":"Enerwave", + "3":"Monoprice"] + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + /*def result + if (cmd.value == 0) { + result = createEvent(name: "switch", value: "off") + } else { + result = createEvent(name: "switch", value: "on") + } + return result*/ +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd) { + def result + if (cmd.scale == 0) { + result = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + result = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else { + result = createEvent(name: "power", value: cmd.scaledMeterValue, unit: "W") + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) +{ + log.debug "multichannelv3.MultiChannelCapabilityReport $cmd" + if (cmd.endPoint == 2 ) { + def currstate = device.currentState("switch2").getValue() + if (currstate == "on") + sendEvent(name: "switch2", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch2", value: "on", isStateChange: true, display: false) + } + else if (cmd.endPoint == 1 ) { + def currstate = device.currentState("switch1").getValue() + if (currstate == "on") + sendEvent(name: "switch1", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch1", value: "on", isStateChange: true, display: false) + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def map = [ name: "switch$cmd.sourceEndPoint" ] + + switch(cmd.commandClass) { + case 32: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + case 37: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + return createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +def refresh() { + def cmds = [] + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + delayBetween(cmds, 1000) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def poll() { + def cmds = [] + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + delayBetween(cmds, 1000) +} + +def reset() { + delayBetween([ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet().format() + ], 1000) +} + +def configure() { + log.debug "configure() called" + def cmds = [] + //if (deviceType.value == deviceType.value) log.debug "Statement True" + if (deviceType != null && deviceType.value != null) { + switch (deviceType.value as String) { + case "1": + log.debug "Configuring device as Philio" + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, configurationValue: [3]).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + break + case "2": + log.debug "Configuring device as Enerwave" + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, configurationValue: [1]).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + break + case "3": + log.debug "Configuring device as Monoprice" + break + default: + log.debug "No valid device type chosen" + break + } + } + + if ( cmds != [] && cmds != null ) return delayBetween(cmds, 2000) else return +} +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + log.debug "Preferences have been changed. Attempting configure()" + def cmds = configure() + response(cmds) +} + +def on1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[255]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def off1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[0]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def on2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[255]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +def off2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[0]).format(), + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} \ No newline at end of file diff --git a/Drivers/generic-zwave-association-handler.src/generic-zwave-association-handler.groovy b/Drivers/generic-zwave-association-handler.src/generic-zwave-association-handler.groovy new file mode 100644 index 0000000..5666f75 --- /dev/null +++ b/Drivers/generic-zwave-association-handler.src/generic-zwave-association-handler.groovy @@ -0,0 +1,336 @@ +/** + * + * Generic Z-Wave Association Handler + * + * github: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2016-10-05 + * Copyright 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. + * + */ + +metadata { + definition (name: "Generic Z-Wave Association Handler", namespace: "erocm123", author: "Eric Maycock") { + capability "Contact Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Refresh" + + command "associateGroup" + command "associateGroup", ["number", "list"] + + //fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x80,0x71,0x30,0x85,0x59,0x84,0x70" + + } + + simulator { + } + + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "contact" + details(["contact", "battery", "refresh"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err 106")) { + if (state.sec) { + log.debug description + } else { + result = createEvent( + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + isStateChange: true, + ) + } + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "parsed '$description' to $result" + return result +} + +def updated() { + def cmds = [] + if (!state.MSR) { + cmds = [ + command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()), + "delay 1200", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ] + } else if (!state.lastbat) { + cmds = [] + } else { + cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + } + response(cmds) +} + +def configure() { + commands([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet(), + zwave.batteryV1.batteryGet() + ], 6000) +} + +def refresh() { + def cmds = [] + if (!state.MSR) { + cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 1200" + } + if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { + cmds << command(zwave.batteryV1.batteryGet()) + } else { + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } + cmds += processAssociations() + [event, cmds] +} + +def sensorValueEvent(value) { + if (value) { + createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") + } else { + createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) +{ + def result = [] + if (cmd.notificationType == 0x06 && cmd.event == 0x16) { + result << sensorValueEvent(1) + } else if (cmd.notificationType == 0x06 && cmd.event == 0x17) { + result << sensorValueEvent(0) + } else if (cmd.notificationType == 0x07) { + if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice door/window sensors + result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } else if (cmd.event == 0x07) { + if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion") + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + def cmds = [] + if (!state.MSR) { + cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 1200" + } + if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { + cmds << command(zwave.batteryV1.batteryGet()) + } else { + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } + cmds += processAssociations() + [event, response(cmds)] +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = now() + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + retypeBasedOnMSR() + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + + if (msr == "011A-0601-0901") { // Enerwave motion doesn't always get the associationSet that the hub sends on join + result << response(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + } else if (!device.currentState("battery")) { + if (msr == "0086-0102-0059") { + result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) + } else { + result << response(command(zwave.batteryV1.batteryGet())) + } + } + + result +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + // log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec == 1) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def retypeBasedOnMSR() { + switch (state.MSR) { + case "0086-0002-002D": + log.debug "Changing device type to Z-Wave Water Sensor" + setDeviceType("Z-Wave Water Sensor") + break + case "011F-0001-0001": // Schlage motion + case "014A-0001-0001": // Ecolink motion + case "014A-0004-0001": // Ecolink motion + + case "0060-0001-0002": // Everspring SP814 + case "0060-0001-0003": // Everspring HSP02 + case "011A-0601-0901": // Enerwave ZWN-BPC + log.debug "Changing device type to Z-Wave Motion Sensor" + setDeviceType("Z-Wave Motion Sensor") + break + case "013C-0002-000D": // Philio multi + + log.debug "Changing device type to 3-in-1 Multisensor Plus (SG)" + setDeviceType("3-in-1 Multisensor Plus (SG)") + break + case "0109-2001-0106": // Vision door/window + log.debug "Changing device type to Z-Wave Plus Door/Window Sensor" + setDeviceType("Z-Wave Plus Door/Window Sensor") + break + case "0109-2002-0205": // Vision Motion + log.debug "Changing device type to Z-Wave Plus Motion/Temp Sensor" + setDeviceType("Z-Wave Plus Motion/Temp Sensor") + break + } +} + +def associateGroup(group, nodes){ + state."desiredAssociation${group}" = nodes + log.debug state."desiredAssociation${group}" +} + +private processAssociations(){ + def cmds = [] + for (int i = 1; i <=5; i++){ + if(state."actualAssociation${i}" != null){ + if(state."desiredAssociation${i}" != null) { + def refreshGroup = false + (state."desiredAssociation${i}" - state."actualAssociation${i}"*.toString()).each { + log.debug "Adding node $it to group $i" + cmds << zwave.associationV2.associationSet(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)).format() + refreshGroup = true + } + (state."actualAssociation${i}"*.toString() - state."desiredAssociation${i}").each { + if(it.toInteger() != 1) { + log.debug "Removing node $it from group $i" + cmds << zwave.associationV2.associationRemove(groupingIdentifier:i, nodeId:it.toInteger()).format() + refreshGroup = true + } + } + if (refreshGroup == true) cmds << zwave.associationV2.associationGet(groupingIdentifier:i).format() + else log.debug "There are no association actions to complete" + } + } else { + log.debug "Association info not known for group $i. Requesting info from device." + cmds << zwave.associationV2.associationGet(groupingIdentifier:i).format() + } + } + return cmds +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport $cmd" + def temp = [] + if (cmd.nodeId != []) { + cmd.nodeId.each { + temp += it.toString().format( '%02x', it.toInteger() ) + } + } + state."actualAssociation${cmd.groupingIdentifier}" = temp +} \ No newline at end of file diff --git a/Drivers/homeseer-hs-wd100-dimmer-switch.src/homeseer-hs-wd100-dimmer-switch.groovy b/Drivers/homeseer-hs-wd100-dimmer-switch.src/homeseer-hs-wd100-dimmer-switch.groovy new file mode 100644 index 0000000..6dcfeb2 --- /dev/null +++ b/Drivers/homeseer-hs-wd100-dimmer-switch.src/homeseer-hs-wd100-dimmer-switch.groovy @@ -0,0 +1,262 @@ +/** + * HomeSeer HS-WD100+ Dimmer Switch + * Copyright 2016 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. + * + */ +metadata { + definition (name: "HomeSeer HS-WD100+ Dimmer Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch Level" + capability "Actuator" + capability "Indicator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "PushableButton" + capability "HoldableButton" + + fingerprint mfr: "000C", prod: "4447", model: "3034" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x73,0x26,0x27,0x70,0x2C,0x2B,0x5B,0x7A,0xEF,0x5B" + + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "09%": "command: 2003, payload: 09" + status "10%": "command: 2003, payload: 0A" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + + // reply messages + reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" + reply "200100,delay 5000,2602": "command: 2603, payload: 00" + reply "200119,delay 5000,2602": "command: 2603, payload: 19" + reply "200132,delay 5000,2602": "command: 2603, payload: 32" + reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" + reply "200163,delay 5000,2602": "command: 2603, payload: 63" + } + + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", 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" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + } + + standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off" + state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on" + state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit" + } + + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff" + } + + main(["switch"]) + details(["switch", "level", "indicator", "refresh"]) + + } +} + +def parse(String description) { + def result = null + log.debug "description: $description" + if (description != "updated") { + log.debug "parse() >> zwave.parse($description)" + def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1]) + log.debug "cmd: $cmd" + if (cmd) { + result = zwaveEvent(cmd) + log.debug "result: $result" + } + } + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + log.debug "Was hailed: requesting state update" + } else { + log.debug "Parse returned ${result?.descriptionText}" + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + //sendEvent(name: "sequenceNumber", value: cmd.sequenceNumber, displayed:false) + switch (cmd.keyAttributes) { + case 0: + // Single tap + break + case 2: + // Button hold + buttonEvent(cmd.sceneNumber + 4, "pushed") + break + case 3: + // Double tap + buttonEvent(cmd.sceneNumber, "pushed") + break + case 4: + // Tripple tap + buttonEvent(cmd.sceneNumber + 2, "pushed") + break + default: + log.debug "Unhandled CentralSceneNotification: ${cmd}" + break + } +} + +def buttonEvent(button, value) { + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value)] + if (cmd.value && cmd.value <= 100) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "ConfigurationReport $cmd" + def value = "when off" + if (cmd.configurationValue[0] == 1) {value = "when on"} + if (cmd.configurationValue[0] == 2) {value = "never"} + createEvent([name: "indicatorStatus", value: value]) +} + +def zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) { + createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false]) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) { + [createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())] +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) +} + +def setLevel(value) { + log.debug "setLevel >> value: $value" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + if (level > 0) { + sendEvent(name: "switch", value: "on") + } else { + sendEvent(name: "switch", value: "off") + } + sendEvent(name: "level", value: level, unit: "%") + delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value, duration) { + log.debug "setLevel >> value: $value, duration: $duration" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000 + delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay) +} + +def poll() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def refresh() { + log.debug "refresh() is called" + def commands = [] + commands << zwave.switchMultilevelV1.switchMultilevelGet().format() + if (getDataValue("MSR") == null) { + commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + delayBetween(commands,100) +} + +def indicatorWhenOn() { + sendEvent(name: "indicatorStatus", value: "when on") + zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() +} + +def indicatorWhenOff() { + sendEvent(name: "indicatorStatus", value: "when off") + zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() +} + +def indicatorNever() { + sendEvent(name: "indicatorStatus", value: "never") + zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format() +} + +def invertSwitch(invert=true) { + if (invert) { + zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 4, size: 1).format() + } + else { + zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format() + } +} \ No newline at end of file diff --git a/Drivers/inovelli-1-channel-outdoor-smart-plug-nzw96.src/inovelli-1-channel-outdoor-smart-plug-nzw96.groovy b/Drivers/inovelli-1-channel-outdoor-smart-plug-nzw96.src/inovelli-1-channel-outdoor-smart-plug-nzw96.groovy new file mode 100644 index 0000000..8fe9940 --- /dev/null +++ b/Drivers/inovelli-1-channel-outdoor-smart-plug-nzw96.src/inovelli-1-channel-outdoor-smart-plug-nzw96.groovy @@ -0,0 +1,167 @@ +/** + * Inovelli 1-Channel Outdoor Smart Plug NZW96 + * Author: Eric Maycock (erocm123) + * Date: 2018-02-15 + * + * 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. + * + */ + +metadata { + definition (name: "Inovelli 1-Channel Outdoor Smart Plug NZW96", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + capability "Health Check" + + attribute "lastActivity", "String" + + fingerprint mfr: "015D", prod: "6000", model: "6000", deviceJoinName: "Inovelli Outdoor Smart Plug" + fingerprint mfr: "0312", prod: "6000", model: "6000", deviceJoinName: "Inovelli Outdoor Smart Plug" + fingerprint deviceId: "0x1001", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x73,0x25,0x27,0x70,0x71,0x8E,0x55,0x6C,0x7A" + } + + simulator { + } + + preferences { + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when switch is:\n", description: "Tap to set", required: false, options:[[0: "On"], [1: "Off"], [2: "Disable"]], defaultValue: 0 + } + + tiles { + 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" + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def installed() { + refresh() +} + +def updated() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 1, displayed: true) + def cmds = [] + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [ledIndicator? ledIndicator.toInteger() : 0], parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff? autoOff.toInteger() : 0, parameterNumber: 2, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + response(commands(cmds)) +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def ping() { + refresh() +} + +def poll() { + refresh() +} + +def refresh() { + commands(zwave.switchBinaryV1.switchBinaryGet()) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} \ No newline at end of file diff --git a/Drivers/inovelli-1-channel-smart-plug-nzw36-w-scene.src/inovelli-1-channel-smart-plug-nzw36-w-scene.groovy b/Drivers/inovelli-1-channel-smart-plug-nzw36-w-scene.src/inovelli-1-channel-smart-plug-nzw36-w-scene.groovy new file mode 100644 index 0000000..0c92472 --- /dev/null +++ b/Drivers/inovelli-1-channel-smart-plug-nzw36-w-scene.src/inovelli-1-channel-smart-plug-nzw36-w-scene.groovy @@ -0,0 +1,198 @@ + /** + * Inovelli 1-Channel Smart Plug NZW36 w/Scene + * Author: Eric Maycock (erocm123) + * Date: 2017-09-19 + * + * Copyright 2017 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. + * + */ + +metadata { + definition (name: "Inovelli 1-Channel Smart Plug NZW36 w/Scene", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + capability "Health Check" + capability "PushableButton" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "pressUpX2" + + fingerprint mfr: "015D", prod: "0221", model: "241C", deviceJoinName: "Inovelli Smart Plug" + fingerprint mfr: "015D", prod: "2400", model: "2400", deviceJoinName: "Inovelli Smart Plug" + fingerprint mfr: "0312", prod: "2400", model: "2400", deviceJoinName: "Inovelli Smart Plug" + + } + + simulator { + } + + preferences { + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when switch is:\n", description: "Tap to set", required: false, options:[[0: "On"], [1: "Off"], [2: "Disable"]], defaultValue: 0 + input description: "1 pushed - Button 2x click", title: "Button Mappings", displayDuringSetup: false, type: "paragraph", element: "paragraph" + } + + tiles { + 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.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}',icon: "st.unknown.zwave.remote-controller") + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + standardTile("pressUpX2", "device.button", width: 4, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲", backgroundColor: "#ffffff", action: "pressUpX2" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'Tap on the ▲▲ button above to test your scene' + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def installed() { + refresh() +} + +def updated() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 1, displayed: true) + def cmds = [] + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [ledIndicator? ledIndicator.toInteger() : 0], parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff? autoOff.toInteger() : 0, parameterNumber: 2, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + response(commands(cmds)) +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + createEvent(buttonEvent(cmd.sceneNumber, (cmd.sceneNumber == 2? "held" : "pushed"), "physical")) +} + +def buttonEvent(button, value, type = "digital") { + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Tap '.padRight(button+1+5, '▼'):' Tap '.padRight(button+1+5, '▲')}", displayed:false) + [name: value, value: button, isStateChange:true] +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def ping() { + refresh() +} + +def poll() { + refresh() +} + +def refresh() { + commands(zwave.switchBinaryV1.switchBinaryGet()) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def pressUpX2() { + sendEvent(buttonEvent(1, "pushed")) +} \ No newline at end of file diff --git a/Drivers/inovelli-2-channel-outdoor-smart-plug-nzw97.src/inovelli-2-channel-outdoor-smart-plug-nzw97.groovy b/Drivers/inovelli-2-channel-outdoor-smart-plug-nzw97.src/inovelli-2-channel-outdoor-smart-plug-nzw97.groovy new file mode 100644 index 0000000..dd1cb34 --- /dev/null +++ b/Drivers/inovelli-2-channel-outdoor-smart-plug-nzw97.src/inovelli-2-channel-outdoor-smart-plug-nzw97.groovy @@ -0,0 +1,304 @@ +/** + * Inovelli 2-Channel Outdoor Smart Plug NZW97 + * Author: Eric Maycock (erocm123) + * Date: 2017-11-14 + * + * Copyright 2017 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. + * + */ + +metadata { + definition(name: "Inovelli 2-Channel Outdoor Smart Plug NZW97", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Health Check" + capability "PushableButton" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + fingerprint manufacturer: "015D", prod: "6100", model: "6100", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" + fingerprint manufacturer: "0312", prod: "6100", model: "6100", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" + fingerprint manufacturer: "015D", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" + fingerprint manufacturer: "0312", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" + } + + simulator {} + + preferences { + input "autoOff1", "number", title: "Auto Off Channel 1\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "autoOff2", "number", title: "Auto Off Channel 2\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when switch is:\n", description: "Tap to set", required: false, options:[[0: "On"], [1: "Off"], [2: "Disable"]], defaultValue: 0 + } + + tiles { + 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" + } + } + + childDeviceTiles("all") + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep = null) { + log.debug "BasicReport ${cmd} - ep ${ep}" + if (ep) { + def event + childDevices.each { + childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + log.debug "BasicSet ${cmd}" + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + log.debug "SwitchBinaryReport ${cmd} - ep ${ep}" + if (ep) { + def event + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId-ep$ep" + } + if (childDevice) childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n-> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + log.debug "MultiChannelCmdEncap ${cmd}" + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "ManufacturerSpecificReport ${cmd}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + log.debug "Unhandled Event: ${cmd}" +} + +def on() { + log.debug "on()" + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +def off() { + log.debug "off()" + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +void childOn(String dni) { + log.debug "childOn($dni)" + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childOff(String dni) { + log.debug "childOff($dni)" + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childRefresh(String dni) { + log.debug "childRefresh($dni)" + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +def poll() { + log.debug "poll()" + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def refresh() { + log.debug "refresh()" + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def installed() { + log.debug "installed()" + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def updated() { + log.debug "updated()" + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + if (it.label == "${state.oldLabel} (CH${channelNumber(it.deviceNetworkId)})") { + def newLabel = "${device.displayName} (CH${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + } + state.oldLabel = device.label + } + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 1, displayed: true) + def cmds = [] + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [ledIndicator? ledIndicator.toInteger() : 0], parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff1? autoOff1.toInteger() : 0, parameterNumber: 2, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff2? autoOff2.toInteger() : 0, parameterNumber: 3, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3) + response(commands(cmds)) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } else { + cmd + } +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay = 1000) { + delayBetween(commands.collect { + command(it) + }, delay) +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} +private void createChildDevices() { + state.oldLabel = device.label + for (i in 1..2) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", + isComponent: true, componentName: "ep$i", componentLabel: "Channel $i" + ]) + } +} \ No newline at end of file diff --git a/Drivers/inovelli-2-channel-smart-plug-alternate.src/inovelli-2-channel-smart-plug-alternate.groovy b/Drivers/inovelli-2-channel-smart-plug-alternate.src/inovelli-2-channel-smart-plug-alternate.groovy new file mode 100644 index 0000000..c2824ef --- /dev/null +++ b/Drivers/inovelli-2-channel-smart-plug-alternate.src/inovelli-2-channel-smart-plug-alternate.groovy @@ -0,0 +1,289 @@ +/** + * + * Inovelli 2-Channel Smart Plug + * + * github: Eric Maycock (erocm123) + * Date: 2017-04-27 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ +metadata { + definition(name: "Inovelli 2-Channel Smart Plug (Alternate)", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Health Check" + + fingerprint mfr: "015D", prod: "0221", model: "251C" + fingerprint mfr: "0312", prod: "B221", model: "251C" + fingerprint deviceId: "0x1001", inClusters: "0x5E,0x85,0x59,0x5A,0x72,0x60,0x8E,0x73,0x27,0x25,0x86" + } + simulator {} + preferences {} + tiles { + 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" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + main(["switch"]) + details(["switch", + childDeviceTiles("all"), "refresh" + ]) + } +} +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + logging("Parsed ${cmd} to ${result.inspect()}", 1) + } else { + logging("Non-parsed event: ${description}", 2) + } + return result +} +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep = null) { + logging("BasicReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + childDevices.each { + childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } +} +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + logging("BasicSet ${cmd}", 2) + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + logging("SwitchBinaryReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId-ep$ep" + } + if (childDevice) childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n-> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + logging("MultiChannelCmdEncap ${cmd}", 2) + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport ${cmd}", 2) + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr", 2) + updateDataValue("MSR", msr) +} +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + logging("Unhandled Event: ${cmd}", 2) +} +def on() { + logging("on()", 1) + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} +def off() { + logging("off()", 1) + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} +void childOn(String dni) { + logging("childOn($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} +void childOff(String dni) { + logging("childOff($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} +void childRefresh(String dni) { + logging("childRefresh($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} +def poll() { + logging("poll()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} +def refresh() { + logging("refresh()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} +def ping() { + logging("ping()", 1) + refresh() +} +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} +def updated() { + logging("updated()", 1) + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + if (it.label == "${state.oldLabel} (CH${channelNumber(it.deviceNetworkId)})") { + def newLabel = "${device.displayName} (CH${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + } + state.oldLabel = device.label + } + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "needUpdate", value: device.currentValue("needUpdate"), displayed: false, isStateChange: true) +} +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } else { + cmd + } +} +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} +private commands(commands, delay = 1000) { + delayBetween(commands.collect { + command(it) + }, delay) +} +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 1..2) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Channel $i" + ]) + } + } catch (e) { + runIn(2, "sendAlert") + } +} +private sendAlert() { + sendEvent(descriptionText: "Child device creation failed. Please make sure that the \"Switch Child Device\" is installed and published.", eventType: "ALERT", name: "childDeviceCreation", value: "failed", displayed: true, ) +} +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 configuration_model() +{ +''' + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/inovelli-2-channel-smart-plug-nzw37-w-scene.src/inovelli-2-channel-smart-plug-nzw37-w-scene.groovy b/Drivers/inovelli-2-channel-smart-plug-nzw37-w-scene.src/inovelli-2-channel-smart-plug-nzw37-w-scene.groovy new file mode 100644 index 0000000..726caf2 --- /dev/null +++ b/Drivers/inovelli-2-channel-smart-plug-nzw37-w-scene.src/inovelli-2-channel-smart-plug-nzw37-w-scene.groovy @@ -0,0 +1,326 @@ + /** + * Inovelli 2-Channel Smart Plug NZW37 w/Scene + * Author: Eric Maycock (erocm123) + * Date: 2017-10-17 + * + * Copyright 2017 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. + * + */ + +metadata { + definition(name: "Inovelli 2-Channel Smart Plug NZW37 w/Scene", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Health Check" + capability "PushableButton" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "pressUpX2" + + fingerprint manufacturer: "015D", prod: "2500", model: "2500", deviceJoinName: "Inovelli 2-Channel Smart Plug" + fingerprint manufacturer: "0312", prod: "2500", model: "2500", deviceJoinName: "Inovelli 2-Channel Smart Plug" + } + + simulator {} + + preferences { + input "autoOff1", "number", title: "Auto Off Channel 1\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "autoOff2", "number", title: "Auto Off Channel 2\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when switch is:\n", description: "Tap to set", required: false, options:[[0: "On"], [1: "Off"], [2: "Disable"]], defaultValue: 0 + input description: "1 pushed - Button 2x click", title: "Button Mappings", displayDuringSetup: false, type: "paragraph", element: "paragraph" + } + + tiles { + 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" + } + } + + 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("pressUpX2", "device.button", width: 4, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲", backgroundColor: "#ffffff", action: "pressUpX2" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'Tap on the ▲▲ button above to test your scene' + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep = null) { + log.debug "BasicReport ${cmd} - ep ${ep}" + if (ep) { + def event + childDevices.each { + childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + log.debug "BasicSet ${cmd}" + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + log.debug "SwitchBinaryReport ${cmd} - ep ${ep}" + if (ep) { + def event + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId-ep$ep" + } + if (childDevice) childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n-> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + log.debug "MultiChannelCmdEncap ${cmd}" + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "ManufacturerSpecificReport ${cmd}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + log.debug "Unhandled Event: ${cmd}" +} + +def on() { + log.debug "on()" + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +def off() { + log.debug "off()" + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +void childOn(String dni) { + log.debug "childOn($dni)" + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childOff(String dni) { + log.debug "childOff($dni)" + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +void childRefresh(String dni) { + log.debug "childRefresh($dni)" + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +def poll() { + log.debug "poll()" + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def refresh() { + log.debug "refresh()" + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def installed() { + log.debug "installed()" + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def updated() { + log.debug "updated()" + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + if (it.label == "${state.oldLabel} (CH${channelNumber(it.deviceNetworkId)})") { + def newLabel = "${device.displayName} (CH${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + } + state.oldLabel = device.label + } + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 1, displayed: true) + def cmds = [] + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [ledIndicator? ledIndicator.toInteger() : 0], parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff1? autoOff1.toInteger() : 0, parameterNumber: 2, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff2? autoOff2.toInteger() : 0, parameterNumber: 3, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3) + response(commands(cmds)) +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + createEvent(buttonEvent(cmd.sceneNumber, (cmd.sceneNumber == 2? "held" : "pushed"), "physical")) +} + +def buttonEvent(button, value, type = "digital") { + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Tap '.padRight(button+1+5, '▼'):' Tap '.padRight(button+1+5, '▲')}", displayed:false) + [name: value, value: button, isStateChange:true] +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } else { + cmd + } +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay = 1000) { + delayBetween(commands.collect { + command(it) + }, delay) +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} +private void createChildDevices() { + state.oldLabel = device.label + for (i in 1..2) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Channel $i" + ]) + } +} + +def pressUpX2() { + sendEvent(buttonEvent(1, "pushed")) +} \ No newline at end of file diff --git a/Drivers/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy b/Drivers/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy new file mode 100644 index 0000000..d7af2ed --- /dev/null +++ b/Drivers/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy @@ -0,0 +1,273 @@ +/** + * + * Inovelli 2-Channel Smart Plug + * + * github: Eric Maycock (erocm123) + * Date: 2017-04-27 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ +metadata { + definition(name: "Inovelli 2-Channel Smart Plug", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Health Check" + + fingerprint manufacturer: "015D", prod: "0221", model: "251C", deviceJoinName: "Inovelli 2-Channel Smart Plug" + fingerprint manufacturer: "0312", prod: "0221", model: "251C", deviceJoinName: "Inovelli 2-Channel Smart Plug" + fingerprint manufacturer: "0312", prod: "B221", model: "251C", deviceJoinName: "Inovelli 2-Channel Smart Plug" + fingerprint manufacturer: "0312", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" + fingerprint manufacturer: "015D", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" + fingerprint manufacturer: "015D", prod: "6100", model: "6100", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" + fingerprint manufacturer: "015D", prod: "2500", model: "2500", deviceJoinName: "Inovelli 2-Channel Smart Plug w/Scene" + } + simulator {} + preferences {} + tiles { + 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" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + main(["switch"]) + details(["switch", + childDeviceTiles("all"), "refresh" + ]) + } +} +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + logging("Parsed ${cmd} to ${result.inspect()}", 1) + } else { + logging("Non-parsed event: ${description}", 2) + } + return result +} +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep = null) { + logging("BasicReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + childDevices.each { + childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } + } + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n -> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } +} +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + logging("BasicSet ${cmd}", 2) + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + logging("SwitchBinaryReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId-ep$ep" + } + if (childDevice) childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + childDevices.each { + n-> + if (n.currentState("switch").value != "off") allOff = false + } + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + return event + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + logging("MultiChannelCmdEncap ${cmd}", 2) + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport ${cmd}", 2) + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr", 2) + updateDataValue("MSR", msr) +} +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + logging("Unhandled Event: ${cmd}", 2) +} +def on() { + logging("on()", 1) + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} +def off() { + logging("off()", 1) + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} +void childOn(String dni) { + logging("childOn($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} +void childOff(String dni) { + logging("childOff($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} +void childRefresh(String dni) { + logging("childRefresh($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} +def poll() { + logging("poll()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} +def refresh() { + logging("refresh()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} +def ping() { + logging("ping()", 1) + refresh() +} +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} +def updated() { + logging("updated()", 1) + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + if (it.label == "${state.oldLabel} (CH${channelNumber(it.deviceNetworkId)})") { + def newLabel = "${device.displayName} (CH${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + } + state.oldLabel = device.label + } + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "needUpdate", value: device.currentValue("needUpdate"), displayed: false, isStateChange: true) +} +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } else { + cmd + } +} +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} +private commands(commands, delay = 1000) { + delayBetween(commands.collect { + command(it) + }, delay) +} +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} +private void createChildDevices() { + state.oldLabel = device.label + for (i in 1..2) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", + isComponent: true, componentName: "ep$i", componentLabel: "Channel $i" + ]) + } +} + +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 + } + } +} diff --git a/Drivers/inovelli-dimmer-nzw31-w-scene.src/inovelli-dimmer-nzw31-w-scene.groovy b/Drivers/inovelli-dimmer-nzw31-w-scene.src/inovelli-dimmer-nzw31-w-scene.groovy new file mode 100644 index 0000000..10f747e --- /dev/null +++ b/Drivers/inovelli-dimmer-nzw31-w-scene.src/inovelli-dimmer-nzw31-w-scene.groovy @@ -0,0 +1,699 @@ + /** + * Inovelli Dimmer NZW31/NZW31T w/Scene + * Author: Eric Maycock (erocm123) + * Date: 2018-04-11 + * + * 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. + * + * 2018-04-23: Added configuration parameters for association group 3. + * + * 2018-04-11: No longer deleting child devices when user toggles the option off. SmartThings was throwing errors. + * User will have to manually delete them. + * + * 2018-03-08: Added support for local protection to disable local control. Requires firmware 1.03+. + * Also merging handler from NZW31T as they are identical other than the LED indicator. + * Child device creation option added for local control setting. Child device must be installed: + * https://github.com/erocm123/SmartThingsPublic/blob/master/devicetypes/erocm123/switch-level-child-device.src + * + * 2018-03-01: Added support for additional configuration options (default level - local & z-wave) and child devices + * for adjusting configuration options from other SmartApps. Requires firmware 1.02+. + * + * 2018-02-26: Added support for Z-Wave Association Tool SmartApp. Associations require firmware 1.02+. + * https://github.com/erocm123/SmartThingsPublic/tree/master/smartapps/erocm123/parent/zwave-association-tool.src + */ + +metadata { + definition (name: "Inovelli Dimmer NZW31 w/Scene", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + capability "Health Check" + capability "PushableButton" + capability "HoldableButton" + capability "Switch Level" + capability "Configuration" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "pressUpX1" + command "pressDownX1" + command "pressUpX2" + command "pressDownX2" + command "pressUpX3" + command "pressDownX3" + command "pressUpX4" + command "pressDownX4" + command "pressUpX5" + command "pressDownX5" + command "holdUp" + command "holdDown" + + command "setAssociationGroup", ["number", "enum", "number", "number"] // group number, nodes, action (0 - remove, 1 - add), multi-channel endpoint (optional) + + fingerprint mfr: "015D", prod: "B111", model: "251C", deviceJoinName: "Inovelli Dimmer" + fingerprint mfr: "051D", prod: "B111", model: "251C", deviceJoinName: "Inovelli Dimmer" + fingerprint mfr: "015D", prod: "1F00", model: "1F00", deviceJoinName: "Inovelli Dimmer" + fingerprint mfr: "0312", prod: "1F00", model: "1F00", deviceJoinName: "Inovelli Dimmer" + fingerprint mfr: "0312", prod: "1F02", model: "1F02", deviceJoinName: "Inovelli Dimmer" // Toggle version NZW31T + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x26,0x27,0x70,0x5B,0x75,0x22,0x85,0x8E,0x59,0x55,0x86,0x72,0x5A,0x73,0x6C,0x7A" + + } + + simulator { + } + + preferences { + input "minimumLevel", "number", title: "Minimum Level\n\nMinimum dimming level for attached light\nRange: 1 to 99", description: "Tap to set", required: false, range: "1..99" + input "dimmingStep", "number", title: "Dimming Step Size\n\nPercentage of step when switch is dimming up or down\nRange: 0 to 99 (0 - Instant)", description: "Tap to set", required: false, range: "0..99" + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when light is: (Paddle Switch Only)", description: "Tap to set", required: false, options:[[1: "On"], [0: "Off"], [2: "Disable"], [3: "Always On"]], defaultValue: 1 + input "invert", "enum", title: "Invert Switch\n\nInvert on & off on the physical switch", description: "Tap to set", required: false, options:[[0: "No"], [1: "Yes"]], defaultValue: 0 + input "defaultLocal", "number", title: "Default Level (Local)\n\nDefault level when light is turned on at the switch\nRange: 0 to 99\nNote: 0 = Previous Level\n(Firmware 1.02+)", description: "Tap to set", required: false, range: "0..99" + input "defaultZWave", "number", title: "Default Level (Z-Wave)\n\nDefault level when light is turned on via Z-Wave command\nRange: 0 to 99\nNote: 0 = Previous Level\n(Firmware 1.02+)", description: "Tap to set", required: false, range: "0..99" + input "disableLocal", "enum", title: "Disable Local Control\n\nDisable ability to control switch from the wall\n(Firmware 1.03+)", description: "Tap to set", required: false, options:[[2: "Yes"], [0: "No"]], defaultValue: 1 + input description: "Use the below options to enable child devices for the specified settings. This will allow you to adjust these settings using SmartApps such as Smart Lighting. If any of the options are enabled, make sure you have the appropriate child device handlers installed.\n(Firmware 1.02+)", title: "Child Devices", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "enableDefaultLocalChild", "bool", title: "Default Local Level", description: "", required: false + input "enableDefaultZWaveChild", "bool", title: "Default Z-Wave Level", description: "", required: false + input "enableDisableLocalChild", "bool", title: "Disable Local Control", description: "", required: false + input description: "1 pushed - Up 1x click\n2 pushed - Up 2x click\n3 pushed - Up 3x click\n4 pushed - Up 4x click\n5 pushed - Up 5x click\n6 pushed - Up held\n\n1 held - Down 1x click\n2 held - Down 2x click\n3 held - Down 3x click\n4 held - Down 4x click\n5 held - Down 5x click\n6 held - Down held", title: "Button Mappings", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input description: "Use the \"Z-Wave Association Tool\" SmartApp to set device associations.\n(Firmware 1.02+)\n\nGroup 2: Sends on/off commands to associated devices when switch is pressed (BASIC_SET).\n\nGroup 3: Sends dim/brighten commands to associated devices when switch is pressed (SWITCH_MULTILEVEL_SET).", title: "Associations", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "group3Setting", "enum", title: "Association Group 3 Behavior\n\nChange how devices respond when associated in group 3", description: "Tap to set", required: false, options:[[1: "Keep in Sync"], [0: "Dim up/down"]], defaultValue: 0 + input description: "When should the switch send commands to associated devices?", title: "Association Behavior", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "group3local", "bool", title: "Send command on local action", description: "", required: false, value: true + input "group3remote", "bool", title: "Send command on z-wave action", description: "", required: false + input "group3way", "bool", title: "Send command on 3-way action", description: "", required: false + input "group3timer", "bool", title: "Send command on auto off timer", description: "", required: false + } + + tiles { + 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" + } + tileAttribute("device.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}',icon: "st.unknown.zwave.remote-controller") + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + standardTile("pressUpX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲", backgroundColor: "#ffffff", action: "pressUpX2" + } + + standardTile("pressUpX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲", backgroundColor: "#ffffff", action: "pressUpX3" + } + + standardTile("pressDownX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼", backgroundColor: "#ffffff", action: "pressDownX2" + } + + standardTile("pressDownX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼", backgroundColor: "#ffffff", action: "pressDownX3" + } + + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: '${currentValue}%', icon: "" + } + + standardTile("pressUpX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX4" + } + + standardTile("pressUpX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX5" + } + + standardTile("pressDownX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX4" + } + + standardTile("pressDownX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX5" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("status", "device.status", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: '${currentValue}', icon: "" + } + + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'Tap on the buttons above to test scenes (ie: Tap ▲ 1x, ▲▲ 2x, etc depending on the button)' + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +private sendAlert(data) { + sendEvent( + descriptionText: data.message, + eventType: "ALERT", + name: "failedOperation", + value: "failed", + displayed: true, + ) +} + +void childSetLevel(String dni, value) { + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + def cmds = [] + switch (channelNumber(dni)) { + case 8: + cmds << new hubitat.device.HubAction(command(zwave.configurationV1.configurationSet(scaledConfigurationValue: value, parameterNumber: channelNumber(dni), size: 1) )) + cmds << new hubitat.device.HubAction(command(zwave.configurationV1.configurationGet(parameterNumber: channelNumber(dni) ))) + break + case 9: + cmds << new hubitat.device.HubAction(command(zwave.configurationV1.configurationSet(scaledConfigurationValue: value, parameterNumber: channelNumber(dni), size: 1) )) + cmds << new hubitat.device.HubAction(command(zwave.configurationV1.configurationGet(parameterNumber: channelNumber(dni) ))) + break + case 101: + cmds << new hubitat.device.HubAction(command(zwave.protectionV2.protectionSet(localProtectionState : level > 0 ? 2 : 0, rfProtectionState: 0) )) + cmds << new hubitat.device.HubAction(command(zwave.protectionV2.protectionGet() )) + break + } + sendHubCommand(cmds, 1000) +} + +void childOn(String dni) { + log.debug "childOn($dni)" + childSetLevel(dni, 99) +} + +void childOff(String dni) { + log.debug "childOff($dni)" + childSetLevel(dni, 0) +} + +void childRefresh(String dni) { + log.debug "childRefresh($dni)" +} + +def childExists(ep) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith(ep)} + if (childDevice) + return true + else + return false +} + +def installed() { + log.debug "installed()" + refresh() +} + +def configure() { + log.debug "configure()" + def cmds = initialize() + commands(cmds) +} + +def updated() { + if (!state.lastRan || now() >= state.lastRan + 2000) { + log.debug "updated()" + state.lastRan = now() + def cmds = initialize() + response(commands(cmds)) + } else { + log.debug "updated() ran within the last 2 seconds. Skipping execution." + } +} + +def initialize() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 6, displayed: true) + + if (enableDefaultLocalChild && !childExists("ep8")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep8", null, + [completedSetup: true, label: "${device.displayName} (Default Local Level)", + isComponent: true, componentName: "ep8", componentLabel: "Default Local Level"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDefaultLocalChild && childExists("ep8")) { + log.debug "Trying to delete child device ep8. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep8")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (enableDefaultZWaveChild && !childExists("ep9")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep9", null, + [completedSetup: true, label: "${device.displayName} (Default Z-Wave Level)", + isComponent: true, componentName: "ep9", componentLabel: "Default Z-Wave Level"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDefaultLocalChild && childExists("ep9")) { + log.debug "Trying to delete child device ep9. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep9")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (enableDisableLocalChild && !childExists("ep101")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep101", null, + [completedSetup: true, label: "${device.displayName} (Disable Local Control)", + isComponent: true, componentName: "ep101", componentLabel: "Disable Local Control"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDisableLocalChild && childExists("ep101")) { + log.debug "Trying to delete child device ep101. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (device.label != state.oldLabel) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep8")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Default Local Level)") + childDevice = children.find{it.deviceNetworkId.endsWith("ep9")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Default Z-Wave Level)") + childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Disable Local Control)") + state.oldLabel = device.label + } + + def cmds = processAssociations() + cmds << zwave.versionV1.versionGet() + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: dimmingStep!=null? dimmingStep.toInteger() : 1, parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: minimumLevel!=null? minimumLevel.toInteger() : 1, parameterNumber: 2, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: ledIndicator!=null? ledIndicator.toInteger() : 1, parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: invert!=null? invert.toInteger() : 0, parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff!=null? autoOff.toInteger() : 0, parameterNumber: 5, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5) + if (state.defaultLocal != settings.defaultLocal) { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: defaultLocal!=null? defaultLocal.toInteger() : 0, parameterNumber: 8, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 8) + } + if (state.defaultZWave != settings.defaultZWave) { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: defaultZWave!=null? defaultZWave.toInteger() : 0, parameterNumber: 9, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 9) + } + if (state.disableLocal != settings.disableLocal) { + cmds << zwave.protectionV2.protectionSet(localProtectionState : disableLocal!=null? disableLocal.toInteger() : 0, rfProtectionState: 0) + cmds << zwave.protectionV2.protectionGet() + } + + //Calculate group 3 configuration parameter + def group3value = 0 + group3value += group3local? 1 : 0 + group3value += group3way? 2 : 0 + group3value += group3remote? 4 : 0 + group3value += group3timer? 8 : 0 + + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: group3value, parameterNumber: 6, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 6) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: group3Setting!=null? group3Setting.toInteger() : 0, parameterNumber: 7, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 7) + + state.defaultLocal = settings.defaultLocal + state.defaultZWave = settings.defaultZWave + state.disableLocal = settings.disableLocal + return cmds +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'" + def integerValue = cmd2Integer(cmd.configurationValue) + switch (cmd.parameterNumber) { + case 8: + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep8")} + if (childDevice) { + childDevice.sendEvent(name: "switch", value: integerValue > 0 ? "on" : "off") + childDevice.sendEvent(name: "level", value: integerValue) + } + break + case 9: + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep9")} + if (childDevice) { + childDevice.sendEvent(name: "switch", value: integerValue > 0 ? "on" : "off") + childDevice.sendEvent(name: "level", value: integerValue) + } + break + } +} + +def cmd2Integer(array) { + switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + // Since SmartThings isn't filtering duplicate events, we are skipping these + // Switch is sending BasicReport as well (which we will use) + //dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value)] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + switch (cmd.keyAttributes) { + case 0: + createEvent(buttonEvent(cmd.keyAttributes + 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 1: + createEvent(buttonEvent(6, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 2: + null + break + default: + createEvent(buttonEvent(cmd.keyAttributes - 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + } +} + +def buttonEvent(button, value, type = "digital") { + if(button != 6) + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Tap '.padRight(button+5, '▼'):' Tap '.padRight(button+5, '▲')}", displayed:false) + else + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Hold ▼':' Hold ▲'}", displayed:false) + [name: value, value: button, isStateChange:true] +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def setLevel(value) { + commands([ + zwave.basicV1.basicSet(value: value < 100 ? value : 99), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def setLevel(value, duration) { + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + commands([ + zwave.switchMultilevelV2.switchMultilevelSet(value: value < 100 ? value : 99, dimmingDuration: dimmingDuration), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def poll() { + log.debug "poll()" + refresh() +} + +def refresh() { + log.debug "refresh()" + commands([zwave.switchBinaryV1.switchBinaryGet(), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def pressUpX1() { + sendEvent(buttonEvent(1, "pushed")) +} + +def pressDownX1() { + sendEvent(buttonEvent(1, "held")) +} + +def pressUpX2() { + sendEvent(buttonEvent(2, "pushed")) +} + +def pressDownX2() { + sendEvent(buttonEvent(2, "held")) +} + +def pressUpX3() { + sendEvent(buttonEvent(3, "pushed")) +} + +def pressDownX3() { + sendEvent(buttonEvent(3, "held")) +} + +def pressUpX4() { + sendEvent(buttonEvent(4, "pushed")) +} + +def pressDownX4() { + sendEvent(buttonEvent(4, "held")) +} + +def pressUpX5() { + sendEvent(buttonEvent(5, "pushed")) +} + +def pressDownX5() { + sendEvent(buttonEvent(5, "held")) +} + +def holdUp() { + sendEvent(buttonEvent(6, "pushed")) +} + +def holdDown() { + sendEvent(buttonEvent(6, "held")) +} + +def setDefaultAssociations() { + def smartThingsHubID = zwaveHubNodeId.toString().format( '%02x', zwaveHubNodeId ) + state.defaultG1 = [smartThingsHubID] + state.defaultG2 = [] + state.defaultG3 = [] +} + +def setAssociationGroup(group, nodes, action, endpoint = null){ + if (!state."desiredAssociation${group}") { + state."desiredAssociation${group}" = nodes + } else { + switch (action) { + case 0: + state."desiredAssociation${group}" = state."desiredAssociation${group}" - nodes + break + case 1: + state."desiredAssociation${group}" = state."desiredAssociation${group}" + nodes + break + } + } +} + +def processAssociations(){ + def cmds = [] + setDefaultAssociations() + def associationGroups = 5 + if (state.associationGroups) { + associationGroups = state.associationGroups + } else { + log.debug "Getting supported association groups from device" + cmds << zwave.associationV2.associationGroupingsGet() + } + for (int i = 1; i <= associationGroups; i++){ + if(state."actualAssociation${i}" != null){ + if(state."desiredAssociation${i}" != null || state."defaultG${i}") { + def refreshGroup = false + ((state."desiredAssociation${i}"? state."desiredAssociation${i}" : [] + state."defaultG${i}") - state."actualAssociation${i}").each { + log.debug "Adding node $it to group $i" + cmds << zwave.associationV2.associationSet(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + ((state."actualAssociation${i}" - state."defaultG${i}") - state."desiredAssociation${i}").each { + log.debug "Removing node $it from group $i" + cmds << zwave.associationV2.associationRemove(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + if (refreshGroup == true) cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + else log.debug "There are no association actions to complete for group $i" + } + } else { + log.debug "Association info not known for group $i. Requesting info from device." + cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + } + } + return cmds +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + def temp = [] + if (cmd.nodeId != []) { + cmd.nodeId.each { + temp += it.toString().format( '%02x', it.toInteger() ).toUpperCase() + } + } + state."actualAssociation${cmd.groupingIdentifier}" = temp + log.debug "Associations for Group ${cmd.groupingIdentifier}: ${temp}" + updateDataValue("associationGroup${cmd.groupingIdentifier}", "$temp") +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) { + sendEvent(name: "groups", value: cmd.supportedGroupings) + log.debug "Supported association groups: ${cmd.supportedGroupings}" + state.associationGroups = cmd.supportedGroupings +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + log.debug cmd + if(cmd.applicationVersion && cmd.applicationSubVersion) { + def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}" + state.needfwUpdate = "false" + sendEvent(name: "status", value: "fw: ${firmware}") + updateDataValue("firmware", firmware) + } +} + +def zwaveEvent(hubitat.zwave.commands.protectionv2.ProtectionReport cmd) { + log.debug cmd + def integerValue = cmd.localProtectionState + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + if (childDevice) { + childDevice.sendEvent(name: "switch", value: integerValue > 0 ? "on" : "off") + } +} diff --git a/Drivers/inovelli-dimmer-nzw31.src/inovelli-dimmer-nzw31.groovy b/Drivers/inovelli-dimmer-nzw31.src/inovelli-dimmer-nzw31.groovy new file mode 100644 index 0000000..02b10eb --- /dev/null +++ b/Drivers/inovelli-dimmer-nzw31.src/inovelli-dimmer-nzw31.groovy @@ -0,0 +1,461 @@ +/** + * Inovelli Dimmer NZW31 + * Author: Eric Maycock (erocm123) + * Date: 2018-04-11 + * + * 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. + * + * 2018-04-23: Added configuration parameters for association group 3. + * + * 2018-04-11: No longer deleting child devices when user toggles the option off. SmartThings was throwing errors. + * User will have to manually delete them. + * + * 2018-03-08: Added support for local protection to disable local control. Requires firmware 1.03+. + * Also merging handler from NZW31T as they are identical other than the LED indicator. + * Child device creation option added for local control setting. Child device must be installed: + * https://github.com/erocm123/SmartThingsPublic/blob/master/devicetypes/erocm123/switch-level-child-device.src + * + * 2018-03-01: Added support for additional configuration options (default level - local & z-wave) and child devices + * for adjusting configuration options from other SmartApps. Requires firmware 1.02+. + * + * 2018-02-26: Added support for Z-Wave Association Tool SmartApp. Associations require firmware 1.02+. + * https://github.com/erocm123/SmartThingsPublic/tree/master/smartapps/erocm123/parent/zwave-association-tool.src + */ + +metadata { + definition (name: "Inovelli Dimmer NZW31", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + capability "Health Check" + capability "Switch Level" + capability "Configuration" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "setAssociationGroup", ["number", "enum", "number", "number"] // group number, nodes, action (0 - remove, 1 - add), multi-channel endpoint (optional) + + fingerprint mfr: "0312", prod: "0118", model: "1E1C", deviceJoinName: "Inovelli Dimmer" + fingerprint mfr: "015D", prod: "0118", model: "1E1C", deviceJoinName: "Inovelli Dimmer" + fingerprint mfr: "015D", prod: "1F01", model: "1F01", deviceJoinName: "Inovelli Dimmer" + fingerprint mfr: "0312", prod: "1F01", model: "1F01", deviceJoinName: "Inovelli Dimmer" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x26,0x27,0x70,0x75,0x22,0x85,0x8E,0x59,0x55,0x86,0x72,0x5A,0x73,0x6C,0x7A" + } + + simulator { + } + + preferences { + input "minimumLevel", "number", title: "Minimum Level\n\nMinimum dimming level for attached light\nRange: 1 to 99", description: "Tap to set", required: false, range: "1..99" + input "dimmingStep", "number", title: "Dimming Step Size\n\nPercentage of step when switch is dimming up or down\nRange: 0 to 99 (0 - Instant)", description: "Tap to set", required: false, range: "0..99" + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when light is: (Paddle Switch Only)", description: "Tap to set", required: false, options:[[1: "On"], [0: "Off"], [2: "Disable"], [3: "Always On"]], defaultValue: 1 + input "invert", "enum", title: "Invert Switch\n\nInvert on & off on the physical switch", description: "Tap to set", required: false, options:[[0: "No"], [1: "Yes"]], defaultValue: 0 + input "defaultLocal", "number", title: "Default Level (Local)\n\nDefault level when light is turned on at the switch\nRange: 0 to 99\nNote: 0 = Previous Level\n(Firmware 1.02+)", description: "Tap to set", required: false, range: "0..99" + input "defaultZWave", "number", title: "Default Level (Z-Wave)\n\nDefault level when light is turned on via Z-Wave command\nRange: 0 to 99\nNote: 0 = Previous Level\n(Firmware 1.02+)", description: "Tap to set", required: false, range: "0..99" + input "disableLocal", "enum", title: "Disable Local Control\n\nDisable ability to control switch from the wall\n(Firmware 1.03+)", description: "Tap to set", required: false, options:[[2: "Yes"], [0: "No"]], defaultValue: 1 + input description: "Use the below options to enable child devices for the specified settings. This will allow you to adjust these settings using SmartApps such as Smart Lighting. If any of the options are enabled, make sure you have the appropriate child device handlers installed.\n(Firmware 1.02+)", title: "Child Devices", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "enableDefaultLocalChild", "bool", title: "Default Local Level", description: "", required: false + input "enableDefaultZWaveChild", "bool", title: "Default Z-Wave Level", description: "", required: false + input "enableDisableLocalChild", "bool", title: "Disable Local Control", description: "", required: false + input description: "Use the \"Z-Wave Association Tool\" SmartApp to set device associations.\n(Firmware 1.02+)\n\nGroup 2: Sends on/off commands to associated devices when switch is pressed (BASIC_SET).\n\nGroup 3: Sends dim/brighten commands to associated devices when switch is pressed (SWITCH_MULTILEVEL_SET).", title: "Associations", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "group3Setting", "enum", title: "Association Group 3 Behavior\n\nChange how devices respond when associated in group 3", description: "Tap to set", required: false, options:[[1: "Keep in Sync"], [0: "Dim up/down"]], defaultValue: 0 + input description: "When should the switch send commands to associated devices?", title: "Association Behavior", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "group3local", "bool", title: "Send command on local action", description: "", required: false, value: true + input "group3remote", "bool", title: "Send command on z-wave action", description: "", required: false + input "group3way", "bool", title: "Send command on 3-way action", description: "", required: false + input "group3timer", "bool", title: "Send command on auto off timer", description: "", required: false + } + + tiles { + 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" + } + tileAttribute("device.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}') + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def installed() { + log.debug "installed()" + refresh() +} + +def configure() { + log.debug "configure()" + def cmds = initialize() + commands(cmds) +} + +def updated() { + if (!state.lastRan || now() >= state.lastRan + 2000) { + log.debug "updated()" + state.lastRan = now() + def cmds = initialize() + response(commands(cmds)) + } else { + log.debug "updated() ran within the last 2 seconds. Skipping execution." + } +} + +def initialize() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + if (enableDefaultLocalChild && !childExists("ep8")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep8", null, + [completedSetup: true, label: "${device.displayName} (Default Local Level)", + isComponent: true, componentName: "ep8", componentLabel: "Default Local Level"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDefaultLocalChild && childExists("ep8")) { + log.debug "Trying to delete child device ep8. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep8")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (enableDefaultZWaveChild && !childExists("ep9")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep9", null, + [completedSetup: true, label: "${device.displayName} (Default Z-Wave Level)", + isComponent: true, componentName: "ep9", componentLabel: "Default Z-Wave Level"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDefaultLocalChild && childExists("ep9")) { + log.debug "Trying to delete child device ep9. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep9")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (enableDisableLocalChild && !childExists("ep101")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep101", null, + [completedSetup: true, label: "${device.displayName} (Disable Local Control)", + isComponent: true, componentName: "ep101", componentLabel: "Disable Local Control"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDisableLocalChild && childExists("ep101")) { + log.debug "Trying to delete child device ep101. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (device.label != state.oldLabel) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep8")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Default Local Level)") + childDevice = children.find{it.deviceNetworkId.endsWith("ep9")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Default Z-Wave Level)") + childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Disable Local Control)") + state.oldLabel = device.label + } + + def cmds = processAssociations() + cmds << zwave.versionV1.versionGet() + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: dimmingStep!=null? dimmingStep.toInteger() : 1, parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: minimumLevel!=null? minimumLevel.toInteger() : 1, parameterNumber: 2, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: ledIndicator!=null? ledIndicator.toInteger() : 1, parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: invert!=null? invert.toInteger() : 0, parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff!=null? autoOff.toInteger() : 0, parameterNumber: 5, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5) + if (state.defaultLocal != settings.defaultLocal) { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: defaultLocal!=null? defaultLocal.toInteger() : 0, parameterNumber: 8, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 8) + } + if (state.defaultZWave != settings.defaultZWave) { + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: defaultZWave!=null? defaultZWave.toInteger() : 0, parameterNumber: 9, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 9) + } + if (state.disableLocal != settings.disableLocal) { + cmds << zwave.protectionV2.protectionSet(localProtectionState : disableLocal!=null? disableLocal.toInteger() : 0, rfProtectionState: 0) + cmds << zwave.protectionV2.protectionGet() + } + + //Calculate group 3 configuration parameter + def group3value = 0 + group3value += group3local? 1 : 0 + group3value += group3way? 2 : 0 + group3value += group3remote? 4 : 0 + group3value += group3timer? 8 : 0 + + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: group3value, parameterNumber: 6, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 6) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: group3Setting!=null? group3Setting.toInteger() : 0, parameterNumber: 7, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 7) + + state.defaultLocal = settings.defaultLocal + state.defaultZWave = settings.defaultZWave + state.disableLocal = settings.disableLocal + return cmds +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + // Since SmartThings isn't filtering duplicate events, we are skipping these + // Switch is sending BasicReport as well (which we will use) + //dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value)] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def setLevel(value) { + commands([ + zwave.basicV1.basicSet(value: value < 100 ? value : 99), + ]) +} + +def setLevel(value, duration) { + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + commands([zwave.switchMultilevelV2.switchMultilevelSet(value: value < 100 ? value : 99, dimmingDuration: dimmingDuration), + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def poll() { + log.debug "poll()" + refresh() +} + +def refresh() { + log.debug "refresh()" + commands([zwave.switchBinaryV1.switchBinaryGet(), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def setDefaultAssociations() { + def smartThingsHubID = zwaveHubNodeId.toString().format( '%02x', zwaveHubNodeId ) + state.defaultG1 = [smartThingsHubID] + state.defaultG2 = [] + state.defaultG3 = [] +} + +def setAssociationGroup(group, nodes, action, endpoint = null){ + if (!state."desiredAssociation${group}") { + state."desiredAssociation${group}" = nodes + } else { + switch (action) { + case 0: + state."desiredAssociation${group}" = state."desiredAssociation${group}" - nodes + break + case 1: + state."desiredAssociation${group}" = state."desiredAssociation${group}" + nodes + break + } + } +} + +def processAssociations(){ + def cmds = [] + setDefaultAssociations() + def associationGroups = 5 + if (state.associationGroups) { + associationGroups = state.associationGroups + } else { + log.debug "Getting supported association groups from device" + cmds << zwave.associationV2.associationGroupingsGet() + } + for (int i = 1; i <= associationGroups; i++){ + if(state."actualAssociation${i}" != null){ + if(state."desiredAssociation${i}" != null || state."defaultG${i}") { + def refreshGroup = false + ((state."desiredAssociation${i}"? state."desiredAssociation${i}" : [] + state."defaultG${i}") - state."actualAssociation${i}").each { + log.debug "Adding node $it to group $i" + cmds << zwave.associationV2.associationSet(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + ((state."actualAssociation${i}" - state."defaultG${i}") - state."desiredAssociation${i}").each { + log.debug "Removing node $it from group $i" + cmds << zwave.associationV2.associationRemove(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + if (refreshGroup == true) cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + else log.debug "There are no association actions to complete for group $i" + } + } else { + log.debug "Association info not known for group $i. Requesting info from device." + cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + } + } + return cmds +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + def temp = [] + if (cmd.nodeId != []) { + cmd.nodeId.each { + temp += it.toString().format( '%02x', it.toInteger() ).toUpperCase() + } + } + state."actualAssociation${cmd.groupingIdentifier}" = temp + log.debug "Associations for Group ${cmd.groupingIdentifier}: ${temp}" + updateDataValue("associationGroup${cmd.groupingIdentifier}", "$temp") +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) { + sendEvent(name: "groups", value: cmd.supportedGroupings) + log.debug "Supported association groups: ${cmd.supportedGroupings}" + state.associationGroups = cmd.supportedGroupings +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + log.debug cmd + if(cmd.applicationVersion && cmd.applicationSubVersion) { + def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}" + state.needfwUpdate = "false" + sendEvent(name: "status", value: "fw: ${firmware}") + updateDataValue("firmware", firmware) + } +} + +def zwaveEvent(hubitat.zwave.commands.protectionv2.ProtectionReport cmd) { + log.debug cmd + def integerValue = cmd.localProtectionState + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + if (childDevice) { + childDevice.sendEvent(name: "switch", value: integerValue > 0 ? "on" : "off") + } +} diff --git a/Drivers/inovelli-dimmer-nzw31t-w-scene.src/inovelli-dimmer-nzw31t-w-scene.groovy b/Drivers/inovelli-dimmer-nzw31t-w-scene.src/inovelli-dimmer-nzw31t-w-scene.groovy new file mode 100644 index 0000000..5400b15 --- /dev/null +++ b/Drivers/inovelli-dimmer-nzw31t-w-scene.src/inovelli-dimmer-nzw31t-w-scene.groovy @@ -0,0 +1,351 @@ +/** + * Inovelli Dimmer NZW31T w/Scene + * Author: Eric Maycock (erocm123) + * Date: 2017-12-28 + * + * Copyright 2017 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. + * + */ + +metadata { + definition (name: "Inovelli Dimmer NZW31T w/Scene", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + //capability "Health Check" + capability "PushableButton" + capability "HoldableButton" + capability "Indicator" + capability "Switch Level" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "pressUpX1" + command "pressDownX1" + command "pressUpX2" + command "pressDownX2" + command "pressUpX3" + command "pressDownX3" + command "pressUpX4" + command "pressDownX4" + command "pressUpX5" + command "pressDownX5" + command "holdUp" + command "holdDown" + + fingerprint mfr: "0312", prod: "1F02", model: "1F02", deviceJoinName: "Inovelli Dimmer" + } + + simulator { + } + + preferences { + input "minimumLevel", "number", title: "Minimum Level\n\nMinimum dimming level for attached light\nRange: 1 to 99", description: "Tap to set", required: false, range: "1..99" + input "dimmingStep", "number", title: "Dimming Step Size\n\nPercentage of step when switch is dimming up or down\nRange: 1 to 99", description: "Tap to set", required: false, range: "1..99" + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "invert", "enum", title: "Invert Switch", description: "Tap to set", required: false, options:[0: "No", 1: "Yes"], defaultValue: 0 + input description: "1 pushed - Up 1x click\n2 pushed - Up 2x click\n3 pushed - Up 3x click\n4 pushed - Up 4x click\n5 pushed - Up 5x click\n6 pushed - Up held\n\n1 held - Down 1x click\n2 held - Down 2x click\n3 held - Down 3x click\n4 held - Down 4x click\n5 held - Down 5x click\n6 held - Down held", title: "Button Mappings", displayDuringSetup: false, type: "paragraph", element: "paragraph" + } + + tiles { + 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" + } + tileAttribute("device.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}',icon: "st.unknown.zwave.remote-controller") + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + standardTile("pressUpX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲", backgroundColor: "#ffffff", action: "pressUpX2" + } + + standardTile("pressUpX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲", backgroundColor: "#ffffff", action: "pressUpX3" + } + + standardTile("pressDownX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼", backgroundColor: "#ffffff", action: "pressDownX2" + } + + standardTile("pressDownX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼", backgroundColor: "#ffffff", action: "pressDownX3" + } + + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: '${currentValue}%', icon: "" + } + + standardTile("pressUpX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX4" + } + + standardTile("pressUpX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX5" + } + + standardTile("pressDownX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX4" + } + + standardTile("pressDownX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX5" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("blank", "device.blank", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: '', icon: "" + } + + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'Tap on the buttons above to test scenes (ie: Tap ▲ 1x, ▲▲ 2x, etc depending on the button)' + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def installed() { + refresh() +} + +def updated() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 6, displayed: true) + def cmds = [] + cmds << zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [dimmingStep? dimmingStep.toInteger() : 1], parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [minimumLevel? minimumLevel.toInteger() : 1], parameterNumber: 2, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + cmds << zwave.configurationV1.configurationSet(configurationValue: [invert? invert.toInteger() : 0], parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff? autoOff.toInteger() : 0, parameterNumber: 5, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5) + response(commands(cmds)) +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + switch (cmd.keyAttributes) { + case 0: + createEvent(buttonEvent(cmd.keyAttributes + 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 1: + createEvent(buttonEvent(6, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 2: + null + break + default: + createEvent(buttonEvent(cmd.keyAttributes - 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + } +} + +def buttonEvent(button, value, type = "digital") { + if(button != 6) + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Tap '.padRight(button+5, '▼'):' Tap '.padRight(button+5, '▲')}", displayed:false) + else + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Hold ▼':' Hold ▲'}", displayed:false) + [name: value, value: button, isStateChange:true] +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def setLevel(value) { + commands([ + zwave.basicV1.basicSet(value: value < 100 ? value : 99), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def setLevel(value, duration) { + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + commands([ + zwave.switchMultilevelV2.switchMultilevelSet(value: value < 100 ? value : 99, dimmingDuration: dimmingDuration), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def poll() { + log.debug "poll()" + refresh() +} + +def refresh() { + log.debug "refresh()" + commands([zwave.switchBinaryV1.switchBinaryGet(), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def pressUpX1() { + sendEvent(buttonEvent(1, "pushed")) +} + +def pressDownX1() { + sendEvent(buttonEvent(1, "held")) +} + +def pressUpX2() { + sendEvent(buttonEvent(2, "pushed")) +} + +def pressDownX2() { + sendEvent(buttonEvent(2, "held")) +} + +def pressUpX3() { + sendEvent(buttonEvent(3, "pushed")) +} + +def pressDownX3() { + sendEvent(buttonEvent(3, "held")) +} + +def pressUpX4() { + sendEvent(buttonEvent(4, "pushed")) +} + +def pressDownX4() { + sendEvent(buttonEvent(4, "held")) +} + +def pressUpX5() { + sendEvent(buttonEvent(5, "pushed")) +} + +def pressDownX5() { + sendEvent(buttonEvent(5, "held")) +} + +def holdUp() { + sendEvent(buttonEvent(6, "pushed")) +} + +def holdDown() { + sendEvent(buttonEvent(6, "held")) +} \ No newline at end of file diff --git a/Drivers/inovelli-dimmer-smart-plug-nzw39-w-scene.src/inovelli-dimmer-smart-plug-nzw39-w-scene.groovy b/Drivers/inovelli-dimmer-smart-plug-nzw39-w-scene.src/inovelli-dimmer-smart-plug-nzw39-w-scene.groovy new file mode 100644 index 0000000..66aa0c7 --- /dev/null +++ b/Drivers/inovelli-dimmer-smart-plug-nzw39-w-scene.src/inovelli-dimmer-smart-plug-nzw39-w-scene.groovy @@ -0,0 +1,240 @@ +/** + * Inovelli Dimmer Smart Plug NZW39 w/Scene + * Author: Eric Maycock (erocm123) + * Date: 2017-10-18 + * + * Copyright 2017 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. + * + */ + +metadata { + definition (name: "Inovelli Dimmer Smart Plug NZW39 w/Scene", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + capability "Health Check" + capability "PushableButton" + capability "Switch Level" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "pressUpX2" + + fingerprint mfr: "015D", prod: "2700", model: "2700", deviceJoinName: "Inovelli Dimmer Smart Plug" + fingerprint mfr: "0312", prod: "2700", model: "2700", deviceJoinName: "Inovelli Dimmer Smart Plug" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x26,0x27,0x70,0x5B,0x85,0x8E,0x59,0x55,0x86,0x72,0x5A,0x73,0x6C,0x7A" + } + + simulator { + } + + preferences { + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when light is:\n", description: "Tap to set", required: false, options:[[1: "On"], [0: "Off"], [2: "Disable"]], defaultValue: 1 + input description: "1 pushed - Button 2x click", title: "Button Mappings", displayDuringSetup: false, type: "paragraph", element: "paragraph" + } + + tiles { + 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" + } + tileAttribute("device.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}',icon: "st.unknown.zwave.remote-controller") + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + standardTile("pressUpX2", "device.button", width: 4, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲", backgroundColor: "#ffffff", action: "pressUpX2" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'Tap on the ▲▲ button above to test your scene' + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def installed() { + refresh() +} + +def updated() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 6, displayed: true) + def cmds = [] + cmds << zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [ledIndicator? ledIndicator.toInteger() : 1], parameterNumber: 1, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff? autoOff.toInteger() : 0, parameterNumber: 2, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + response(commands(cmds)) +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + createEvent(buttonEvent(cmd.sceneNumber, (cmd.sceneNumber == 2? "held" : "pushed"), "physical")) +} + +def buttonEvent(button, value, type = "digital") { + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Tap '.padRight(button+1+5, '▼'):' Tap '.padRight(button+1+5, '▲')}", displayed:false) + [name: value, value: button, isStateChange:true] +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def setLevel(value) { + commands([ + zwave.basicV1.basicSet(value: value < 100 ? value : 99), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def setLevel(value, duration) { + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + commands([ + zwave.switchMultilevelV2.switchMultilevelSet(value: value < 100 ? value : 99, dimmingDuration: dimmingDuration), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def poll() { + log.debug "poll()" + refresh() +} + +def refresh() { + log.debug "refresh()" + commands([zwave.switchBinaryV1.switchBinaryGet(), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def pressUpX2() { + sendEvent(buttonEvent(2, "pushed")) +} diff --git a/Drivers/inovelli-dimmer-smart-plug-nzw39.src/inovelli-dimmer-smart-plug-nzw39.groovy b/Drivers/inovelli-dimmer-smart-plug-nzw39.src/inovelli-dimmer-smart-plug-nzw39.groovy new file mode 100644 index 0000000..6f9d206 --- /dev/null +++ b/Drivers/inovelli-dimmer-smart-plug-nzw39.src/inovelli-dimmer-smart-plug-nzw39.groovy @@ -0,0 +1,285 @@ +/** + * Copyright 2015 SmartThings + * + * 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. + * + */ +metadata { + definition (name: "Inovelli Dimmer Smart Plug NZW39", namespace: "erocm123", author: "SmartThings", ocfDeviceType: "oic.d.light") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Health Check" + capability "Light" + + fingerprint mfr: "0312", prod: "B212", model: "271C", deviceJoinName: "Inovelli Dimmer Smart Plug" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x85,0x59,0x26,0x27,0x70" + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "09%": "command: 2003, payload: 09" + status "10%": "command: 2003, payload: 0A" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + + // reply messages + reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" + reply "200100,delay 5000,2602": "command: 2603, payload: 00" + reply "200119,delay 5000,2602": "command: 2603, payload: 19" + reply "200132,delay 5000,2602": "command: 2603, payload: 32" + reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" + reply "200163,delay 5000,2602": "command: 2603, payload: 63" + } + + preferences { + input "ledIndicator", "enum", title: "LED Indicator", description: "Turn LED indicator... ", required: false, options:[["on": "When On"], ["off": "When Off"], ["never": "Never"]], defaultValue: "off" + } + + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", 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" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + } + + standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off" + state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on" + state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit" + } + + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff" + } + + main(["switch"]) + details(["switch", "level", "refresh"]) + + } +} + +def installed() { + // Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + response(refresh()) +} + +def updated(){ + // Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + switch (ledIndicator) { + case "on": + indicatorWhenOn() + break + case "off": + indicatorWhenOff() + break + case "never": + indicatorNever() + break + default: + indicatorWhenOn() + break + } +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // SwitchMultilevel + 0x56: 1, // Crc16Encap + 0x70: 1, // Configuration + ] +} + +def parse(String description) { + def result = null + if (description != "updated") { + log.debug "parse() >> zwave.parse($description)" + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + log.debug "Was hailed: requesting state update" + } else { + log.debug "Parse returned ${result?.descriptionText}" + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value)] + if (cmd.value && cmd.value <= 100) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "ConfigurationReport $cmd" + def value = "when off" + if (cmd.configurationValue[0] == 1) {value = "when on"} + if (cmd.configurationValue[0] == 2) {value = "never"} + createEvent([name: "indicatorStatus", value: value]) +} + +def zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) { + createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false]) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + updateDataValue("manufacturer", cmd.manufacturerName) + createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) { + [createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())] +} + +def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = commandClassVersions + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) +} + +def setLevel(value) { + log.debug "setLevel >> value: $value" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + if (level > 0) { + sendEvent(name: "switch", value: "on") + } else { + sendEvent(name: "switch", value: "off") + } + sendEvent(name: "level", value: level, unit: "%") + delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value, duration) { + log.debug "setLevel >> value: $value, duration: $duration" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000 + delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay) +} + +def poll() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def refresh() { + log.debug "refresh() is called" + def commands = [] + commands << zwave.switchMultilevelV1.switchMultilevelGet().format() + if (getDataValue("MSR") == null) { + commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + delayBetween(commands,100) +} + +void indicatorWhenOn() { + sendEvent(name: "indicatorStatus", value: "when on", displayed: false) + sendHubCommand(new hubitat.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 2, size: 1).format())) +} + +void indicatorWhenOff() { + sendEvent(name: "indicatorStatus", value: "when off", displayed: false) + sendHubCommand(new hubitat.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 2, size: 1).format())) +} + +void indicatorNever() { + sendEvent(name: "indicatorStatus", value: "never", displayed: false) + sendHubCommand(new hubitat.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 2, size: 1).format())) +} + +def invertSwitch(invert=true) { + if (invert) { + zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 4, size: 1).format() + } + else { + zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format() + } +} diff --git a/Drivers/inovelli-door-window-sensor-nzw1201.src/inovelli-door-window-sensor-nzw1201.groovy b/Drivers/inovelli-door-window-sensor-nzw1201.src/inovelli-door-window-sensor-nzw1201.groovy new file mode 100644 index 0000000..01cc94f --- /dev/null +++ b/Drivers/inovelli-door-window-sensor-nzw1201.src/inovelli-door-window-sensor-nzw1201.groovy @@ -0,0 +1,382 @@ +/** + * Inovelli Door/Window Sensor NZW1201 + * Author: Eric Maycock (erocm123) + * Date: 2018-02-26 + * + * Copyright 2017 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. + * + * 2018-02-26: Added support for Z-Wave Association Tool SmartApp. + * https://github.com/erocm123/SmartThingsPublic/tree/master/smartapps/erocm123/parent/zwave-association-tool.src + * + */ + +metadata { + definition (name: "Inovelli Door/Window Sensor NZW1201", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "x.com.st.d.sensor.contact") { + capability "Contact Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Temperature Measurement" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "setAssociationGroup", ["number", "enum", "number", "number"] // group number, nodes, action (0 - remove, 1 - add), multi-channel endpoint (optional) + + fingerprint mfr:"015D", prod:"2003", model:"B41C", deviceJoinName: "Inovelli Door/Window Sensor" + fingerprint mfr:"0312", prod:"2003", model:"C11C", deviceJoinName: "Inovelli Door/Window Sensor" + fingerprint mfr:"015D", prod:"2003", model:"C11C", deviceJoinName: "Inovelli Door/Window Sensor" + fingerprint mfr:"015D", prod:"C100", model:"C100", deviceJoinName: "Inovelli Door/Window Sensor" + fingerprint mfr:"0312", prod:"C100", model:"C100", deviceJoinName: "Inovelli Door/Window Sensor" + fingerprint deviceId: "0x0701", inClusters:"0x5E,0x86,0x72,0x5A,0x73,0x80,0x85,0x59,0x71,0x30,0x31,0x70,0x84" + } + + simulator { + } + + preferences { + input "tempReportInterval", "enum", title: "Temperature Report Interval\n\nHow often you would like temperature reports to be sent from the sensor. More frequent reports will have a negative impact on battery life.\n", description: "Tap to set", required: false, options:[[10: "10 Minutes"], [30: "30 Minutes"], [60: "1 Hour"], [120: "2 Hours"], [180: "3 Hours"], [240: "4 Hours"], [300: "5 Hours"], [360: "6 Hours"], [720: "12 Hours"], [1440: "24 Hours"]], defaultValue: 180 + input "tempOffset", "number", title: "Temperature Offset\n\nCalibrate reported temperature by applying a negative or positive offset\nRange: -10 to 10", description: "Tap to set", required: false, range: "-10..10" + } + + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + } + tileAttribute("device.temperature", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}°',icon: "") + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 1) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'After adjusting the Temperature Report Interval, open the sensor and press the small white button' + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err 106")) { + if (state.sec) { + log.debug description + } else { + result = createEvent( + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + isStateChange: true, + ) + } + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + } + + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + return result +} + +def installed() { + log.debug "installed()" + def cmds = [zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 10)] + commands(cmds) +} + +def configure() { + log.debug "configure()" + def cmds = initialize() + commands(cmds) +} + +def updated() { + if (!state.lastRan || now() >= state.lastRan + 2000) { + log.debug "updated()" + state.lastRan = now() + state.needfwUpdate = "" + def cmds = initialize() + response(commands(cmds)) + } else { + log.debug "updated() ran within the last 2 seconds. Skipping execution." + } +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "0"]) + def cmds = processAssociations() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + if (state.realTemperature != null) sendEvent(name:"temperature", value: getAdjustedTemp(state.realTemperature)) + if(!state.needfwUpdate || state.needfwUpdate == "") { + log.debug "Requesting device firmware version" + cmds << zwave.versionV1.versionGet() + } + if (!state.lastbat || now() - state.lastbat > 24*60*60*1000) { + log.debug "Battery report not received in 24 hours. Requesting one now." + cmds << zwave.batteryV1.batteryGet() + } + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation() + return cmds +} + +private getAdjustedTemp(value) { + value = Math.round((value as Double) * 100) / 100 + if (tempOffset) { + return value = value + Math.round(tempOffset * 100) /100 + } else { + return value + } +} + +def sensorValueEvent(value) { + if (value) { + createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") + } else { + createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +void zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + log.debug "WakeUpIntervalReport ${cmd.toString()}" + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) +{ + log.debug cmd + def result = [] + if (cmd.notificationType == 0x06 && cmd.event == 0x16) { + result << sensorValueEvent(1) + } else if (cmd.notificationType == 0x06 && cmd.event == 0x17) { + result << sensorValueEvent(0) + } else if (cmd.notificationType == 0x07) { + if (cmd.event == 0x00) { + result << createEvent(descriptionText: "$device.displayName covering was restored", isStateChange: true) + result << response(command(zwave.batteryV1.batteryGet())) + } else if (cmd.event == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + log.debug "${device.displayName} woke up" + def cmds = processAssociations() + + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + + if(!state.wakeInterval || state.wakeInterval != (tempReportInterval? tempReportInterval.toInteger()*60:10800)){ + log.debug "Setting Wake Interval to ${tempReportInterval? tempReportInterval.toInteger()*60:10800}" + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: tempReportInterval? tempReportInterval.toInteger()*60:10800, nodeid:zwaveHubNodeId) + cmds << zwave.wakeUpV1.wakeUpIntervalGet() + } + if (!state.lastbat || now() - state.lastbat > 24*60*60*1000) { + log.debug "Battery report not received in 24 hours. Requesting one now." + cmds << zwave.batteryV1.batteryGet() + } + if(!state.needfwUpdate || state.needfwUpdate == "") { + log.debug "Requesting device firmware version" + cmds << zwave.versionV1.versionGet() + } + + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation() + + response(commands(cmds)) +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + log.debug "SensorMultilevelReport: $cmd" + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + state.realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(state.realTemperature) + map.unit = getTemperatureScale() + log.debug "Temperature Report: $map.value" + break; + default: + map.descriptionText = cmd.toString() + } + return createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = now() + createEvent(map) +} + +void zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + log.debug cmd + if(cmd.applicationVersion && cmd.applicationSubVersion) { + def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}" + state.needfwUpdate = "false" + updateDataValue("firmware", firmware) + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def setDefaultAssociations() { + state.associationGroups = 3 + def smartThingsHubID = zwaveHubNodeId.toString().format( '%02x', zwaveHubNodeId ) + state.defaultG1 = [smartThingsHubID] + state.defaultG2 = [] + state.defaultG3 = [] +} + +def setAssociationGroup(group, nodes, action, endpoint = null){ + if (!state."desiredAssociation${group}") { + state."desiredAssociation${group}" = nodes + } else { + switch (action) { + case 0: + state."desiredAssociation${group}" = state."desiredAssociation${group}" - nodes + break + case 1: + state."desiredAssociation${group}" = state."desiredAssociation${group}" + nodes + break + } + } +} + +def processAssociations(){ + def cmds = [] + setDefaultAssociations() + def supportedGroupings = 5 + if (state.supportedGroupings) { + supportedGroupings = state.supportedGroupings + } else { + log.debug "Getting supported association groups from device" + cmds << zwave.associationV2.associationGroupingsGet() + } + for (int i = 1; i <= supportedGroupings; i++){ + if(state."actualAssociation${i}" != null){ + if(state."desiredAssociation${i}" != null || state."defaultG${i}") { + def refreshGroup = false + ((state."desiredAssociation${i}"? state."desiredAssociation${i}" : [] + state."defaultG${i}") - state."actualAssociation${i}").each { + log.debug "Adding node $it to group $i" + cmds << zwave.associationV2.associationSet(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + ((state."actualAssociation${i}" - state."defaultG${i}") - state."desiredAssociation${i}").each { + log.debug "Removing node $it from group $i" + cmds << zwave.associationV2.associationRemove(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + if (refreshGroup == true) cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + else log.debug "There are no association actions to complete for group $i" + } + } else { + log.debug "Association info not known for group $i. Requesting info from device." + cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + } + } + return cmds +} + +void zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + def temp = [] + if (cmd.nodeId != []) { + cmd.nodeId.each { + temp += it.toString().format( '%02x', it.toInteger() ).toUpperCase() + } + } + state."actualAssociation${cmd.groupingIdentifier}" = temp + log.debug "Associations for Group ${cmd.groupingIdentifier}: ${temp}" + updateDataValue("associationGroup${cmd.groupingIdentifier}", "$temp") +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) { + log.debug "Supported association groups: ${cmd.supportedGroupings}" + state.supportedGroupings = cmd.supportedGroupings + return createEvent(name: "groups", value: cmd.supportedGroupings) +} \ No newline at end of file diff --git a/Drivers/inovelli-door-window-sensor.src/inovelli-door-window-sensor.groovy b/Drivers/inovelli-door-window-sensor.src/inovelli-door-window-sensor.groovy new file mode 100644 index 0000000..d466a86 --- /dev/null +++ b/Drivers/inovelli-door-window-sensor.src/inovelli-door-window-sensor.groovy @@ -0,0 +1,295 @@ + /** + * Inovelli Door/Window Sensor + * Author: Eric Maycock (erocm123) + * Date: 2017-08-17 + * + * Copyright 2017 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. + * + */ + +metadata { + definition (name: "Inovelli Door/Window Sensor", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "x.com.st.d.sensor.contact") { + capability "Contact Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Temperature Measurement" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + fingerprint mfr:"015D", prod:"2003", model:"B41C" + } + + simulator { + } + + preferences { + input "tempReportInterval", "enum", title: "Temperature Report Interval\n\nHow often you would like temperature reports to be sent from the sensor. More frequent reports will have a negative impact on battery life.\nRange: 1 to 3600", description: "", required: false, options:[10: "10 Minutes", 30: "30 Minutes", 60: "1 Hour", 120: "2 Hours", 180: "3 Hours", 240: "4 Hours", 300: "5 Hours", 360: "6 Hours", 720: "12 Hours", 1440: "24 Hours"], defaultValue: 180 + input "tempOffset", "number", title: "Temperature Offset\n\nCalibrate reported temperature by applying a negative or positive offset\nRange: -10 to 10", description: "", required: false, range: "-10..10" + } + + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + } + tileAttribute("device.temperature", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}°',icon: "") + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 1) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'After adjusting the Temperature Report Interval, open the sensor and press the small white button' + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err 106")) { + if (state.sec) { + log.debug description + } else { + result = createEvent( + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + isStateChange: true, + ) + } + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + return result +} + +def installed() { + // Device-Watch simply pings if no device events received for 482min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "0"]) +} + +def updated() { + // Device-Watch simply pings if no device events received for 482min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + if (state.realTemperature != null) sendEvent(name:"temperature", value: getAdjustedTemp(state.realTemperature)) + def cmds = [] + if (!state.MSR) { + cmds = [ + command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()), + "delay 1200", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ] + } else if (!state.lastbat) { + cmds = [] + } else { + cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + } + response(cmds) +} + +private getAdjustedTemp(value) { + value = Math.round((value as Double) * 100) / 100 + if (tempOffset) { + return value = value + Math.round(tempOffset * 100) /100 + } else { + return value + } +} + +def configure() { + commands([ + zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW), + zwave.manufacturerSpecificV2.manufacturerSpecificGet() + ], 1000) +} + +def sensorValueEvent(value) { + if (value) { + createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") + } else { + createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + log.debug "WakeUpIntervalReport ${cmd.toString()}" + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) +{ + log.debug cmd + def result = [] + if (cmd.notificationType == 0x06 && cmd.event == 0x16) { + result << sensorValueEvent(1) + } else if (cmd.notificationType == 0x06 && cmd.event == 0x17) { + result << sensorValueEvent(0) + } else if (cmd.notificationType == 0x07) { + if (cmd.event == 0x00) { + result << createEvent(descriptionText: "$device.displayName covering was restored", isStateChange: true) + result << response(command(zwave.batteryV1.batteryGet())) + } else if (cmd.event == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + def cmds = [] + if (!state.MSR) { + cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 1200" + } + + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1).format() + + if(state.wakeInterval == null || state.wakeInterval != (tempReportInterval? tempReportInterval.toInteger()*60:10800)){ + log.debug "Setting Wake Interval to ${tempReportInterval? tempReportInterval.toInteger()*60:10800}" + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: tempReportInterval? tempReportInterval.toInteger()*60:10800, nodeid:zwaveHubNodeId).format() + cmds << zwave.wakeUpV1.wakeUpIntervalGet().format() + } + + if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { + cmds << command(zwave.batteryV1.batteryGet()) + } else { // If we check the battery state we will send NoMoreInfo in the handler for BatteryReport so that we definitely get the report + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } + + [event, response(cmds)] +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + log.debug "SensorMultilevelReport: $cmd" + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + state.realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(state.realTemperature) + map.unit = getTemperatureScale() + log.debug "Temperature Report: $map.value" + break; + default: + map.descriptionText = cmd.toString() + } + return createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = now() + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result << response(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + if (!device.currentState("battery")) { + result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) + } else { + result << response(command(zwave.batteryV1.batteryGet())) + } + + result +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec == 1) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} \ No newline at end of file diff --git a/Drivers/inovelli-switch-nzw30-w-scene.src/inovelli-switch-nzw30-w-scene.groovy b/Drivers/inovelli-switch-nzw30-w-scene.src/inovelli-switch-nzw30-w-scene.groovy new file mode 100644 index 0000000..f83ac17 --- /dev/null +++ b/Drivers/inovelli-switch-nzw30-w-scene.src/inovelli-switch-nzw30-w-scene.groovy @@ -0,0 +1,532 @@ + /** + * Inovelli Switch NZW30/NZW30T w/Scene + * Author: Eric Maycock (erocm123) + * Date: 2018-04-11 + * + * 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. + * + * 2018-04-11: No longer deleting child devices when user toggles the option off. SmartThings was throwing errors. + * User will have to manually delete them. + * + * 2018-03-08: Added support for local protection to disable local control. Requires firmware 1.03+. + * Also merging handler from NZW30T as they are identical other than the LED indicator. + * Child device creation option added for local control setting. Child device must be installed: + * https://github.com/erocm123/SmartThingsPublic/blob/master/devicetypes/erocm123/switch-level-child-device.src + * + * 2018-02-26: Added support for Z-Wave Association Tool SmartApp. Associations require firmware 1.02+. + * https://github.com/erocm123/SmartThingsPublic/tree/master/smartapps/erocm123/parent/zwave-association-tool.src + */ + +metadata { + definition (name: "Inovelli Switch NZW30 w/Scene", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + capability "Health Check" + capability "PushableButton" + capability "HoldableButton" + capability "Configuration" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "pressUpX1" + command "pressDownX1" + command "pressUpX2" + command "pressDownX2" + command "pressUpX3" + command "pressDownX3" + command "pressUpX4" + command "pressDownX4" + command "pressUpX5" + command "pressDownX5" + command "holdUp" + command "holdDown" + + command "setAssociationGroup", ["number", "enum", "number", "number"] // group number, nodes, action (0 - remove, 1 - add), multi-channel endpoint (optional) + + fingerprint mfr: "015D", prod: "B111", model: "1E1C", deviceJoinName: "Inovelli Switch" + fingerprint mfr: "015D", prod: "1E00", model: "1E00", deviceJoinName: "Inovelli Switch" + fingerprint mfr: "0312", prod: "1E00", model: "1E00", deviceJoinName: "Inovelli Switch" + fingerprint mfr: "0312", prod: "1E02", model: "1E02", deviceJoinName: "Inovelli Switch" // Toggle version NZW30T + fingerprint deviceId: "0x1001", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x73,0x25,0x27,0x70,0x5B,0x75,0x22,0x8E,0x55,0x6C,0x7A" + } + + simulator { + } + + preferences { + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when light is: (Paddle Switch Only)\n", description: "Tap to set", required: false, options:[[1: "On"], [0: "Off"], [2: "Disable"], [3: "Always On"]], defaultValue: 1 + input "invert", "enum", title: "Invert Switch\n\nInvert on & off on the physical switch", description: "Tap to set", required: false, options:[[0: "No"], [1: "Yes"]], defaultValue: 0 + input "disableLocal", "enum", title: "Disable Local Control\n\nDisable ability to control switch from the wall\n(Firmware 1.02+)", description: "Tap to set", required: false, options:[[2: "Yes"], [0: "No"]], defaultValue: 1 + input description: "Use the below options to enable child devices for the specified settings. This will allow you to adjust these settings using SmartApps such as Smart Lighting. If any of the options are enabled, make sure you have the appropriate child device handlers installed.\n(Firmware 1.02+)", title: "Child Devices", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "enableDisableLocalChild", "bool", title: "Disable Local Control", description: "", required: false + input description: "1 pushed - Up 1x click\n2 pushed - Up 2x click\n3 pushed - Up 3x click\n4 pushed - Up 4x click\n5 pushed - Up 5x click\n6 pushed - Up held\n\n1 held - Down 1x click\n2 held - Down 2x click\n3 held - Down 3x click\n4 held - Down 4x click\n5 held - Down 5x click\n6 held - Down held", title: "Button Mappings", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input description: "Use the \"Z-Wave Association Tool\" SmartApp to set device associations. (Firmware 1.02+)\n\nGroup 2: Sends on/off commands to associated devices when switch is pressed (BASIC_SET).", title: "Associations", displayDuringSetup: false, type: "paragraph", element: "paragraph" + } + + tiles { + 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.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}',icon: "st.unknown.zwave.remote-controller") + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + standardTile("pressUpX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲", backgroundColor: "#ffffff", action: "pressUpX2" + } + standardTile("pressUpX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲", backgroundColor: "#ffffff", action: "pressUpX3" + } + standardTile("pressDownX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼", backgroundColor: "#ffffff", action: "pressDownX2" + } + standardTile("pressDownX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼", backgroundColor: "#ffffff", action: "pressDownX3" + } + standardTile("pressUpX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX4" + } + standardTile("pressUpX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX5" + } + standardTile("holdUp", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Hold ▲", backgroundColor: "#ffffff", action: "holdUp" + } + + standardTile("pressDownX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX4" + } + standardTile("pressDownX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX5" + } + standardTile("holdDown", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Hold ▼", backgroundColor: "#ffffff", action: "holdDown" + } + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + valueTile("status", "device.status", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: '${currentValue}', icon: "" + } + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'Tap on the buttons above to test scenes (ie: Tap ▲ 1x, ▲▲ 2x, etc depending on the button)' + } + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +private sendAlert(data) { + sendEvent( + descriptionText: data.message, + eventType: "ALERT", + name: "failedOperation", + value: "failed", + displayed: true, + ) +} + +void childSetLevel(String dni, value) { + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + def cmds = [] + switch (channelNumber(dni)) { + case 101: + cmds << new hubitat.device.HubAction(command(zwave.protectionV2.protectionSet(localProtectionState : level > 0 ? 2 : 0, rfProtectionState: 0) )) + cmds << new hubitat.device.HubAction(command(zwave.protectionV2.protectionGet() )) + break + } + sendHubCommand(cmds, 1000) +} + +void childOn(String dni) { + log.debug "childOn($dni)" + childSetLevel(dni, 99) +} + +void childOff(String dni) { + log.debug "childOff($dni)" + childSetLevel(dni, 0) +} + +void childRefresh(String dni) { + log.debug "childRefresh($dni)" +} + +def childExists(ep) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith(ep)} + if (childDevice) + return true + else + return false +} + +def installed() { + log.debug "installed()" + refresh() +} + +def configure() { + log.debug "configure()" + def cmds = initialize() + commands(cmds) +} + +def updated() { + if (!state.lastRan || now() >= state.lastRan + 2000) { + log.debug "updated()" + state.lastRan = now() + def cmds = initialize() + response(commands(cmds)) + } else { + log.debug "updated() ran within the last 2 seconds. Skipping execution." + } +} + + +def initialize() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 6, displayed: true) + if (enableDisableLocalChild && !childExists("ep101")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep101", null, + [completedSetup: true, label: "${device.displayName} (Disable Local Control)", + isComponent: true, componentName: "ep101", componentLabel: "Disable Local Control"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDisableLocalChild && childExists("ep101")) { + log.debug "Trying to delete child device ep101. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (device.label != state.oldLabel) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("e101")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Disable Local Control)") + state.oldLabel = device.label + } + + def cmds = processAssociations() + cmds << zwave.versionV1.versionGet() + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: ledIndicator!=null? ledIndicator.toInteger() : 1, parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: invert!=null? invert.toInteger() : 0, parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff!=null? autoOff.toInteger() : 0, parameterNumber: 5, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5) + if (state.disableLocal != settings.disableLocal) { + cmds << zwave.protectionV2.protectionSet(localProtectionState : disableLocal!=null? disableLocal.toInteger() : 0, rfProtectionState: 0) + cmds << zwave.protectionV2.protectionGet() + } + state.disableLocal = settings.disableLocal + return cmds +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + //log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + switch (cmd.keyAttributes) { + case 0: + createEvent(buttonEvent(cmd.keyAttributes + 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 1: + createEvent(buttonEvent(6, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 2: + null + break + default: + createEvent(buttonEvent(cmd.keyAttributes - 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + } +} + +def buttonEvent(button, value, type = "digital") { + if(button != 6) + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Tap '.padRight(button+5, '▼'):' Tap '.padRight(button+5, '▲')}", displayed:false) + else + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Hold ▼':' Hold ▲'}", displayed:false) + [name: value, value: button, isStateChange:true] +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'" +} + +def cmd2Integer(array) { + switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + //zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + //zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def ping() { + refresh() +} + +def poll() { + refresh() +} + +def refresh() { + commands(zwave.switchBinaryV1.switchBinaryGet()) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def pressUpX1() { + sendEvent(buttonEvent(1, "pushed")) +} + +def pressDownX1() { + sendEvent(buttonEvent(1, "held")) +} + +def pressUpX2() { + sendEvent(buttonEvent(2, "pushed")) +} + +def pressDownX2() { + sendEvent(buttonEvent(2, "held")) +} + +def pressUpX3() { + sendEvent(buttonEvent(3, "pushed")) +} + +def pressDownX3() { + sendEvent(buttonEvent(3, "held")) +} + +def pressUpX4() { + sendEvent(buttonEvent(4, "pushed")) +} + +def pressDownX4() { + sendEvent(buttonEvent(4, "held")) +} + +def pressUpX5() { + sendEvent(buttonEvent(5, "pushed")) +} + +def pressDownX5() { + sendEvent(buttonEvent(5, "held")) +} + +def holdUp() { + sendEvent(buttonEvent(6, "pushed")) +} + +def holdDown() { + sendEvent(buttonEvent(6, "held")) +} + +def setDefaultAssociations() { + def smartThingsHubID = zwaveHubNodeId.toString().format( '%02x', zwaveHubNodeId ) + state.defaultG1 = [smartThingsHubID] + state.defaultG2 = [] +} + +def setAssociationGroup(group, nodes, action, endpoint = null){ + if (!state."desiredAssociation${group}") { + state."desiredAssociation${group}" = nodes + } else { + switch (action) { + case 0: + state."desiredAssociation${group}" = state."desiredAssociation${group}" - nodes + break + case 1: + state."desiredAssociation${group}" = state."desiredAssociation${group}" + nodes + break + } + } +} + +def processAssociations(){ + def cmds = [] + setDefaultAssociations() + def associationGroups = 5 + if (state.associationGroups) { + associationGroups = state.associationGroups + } else { + log.debug "Getting supported association groups from device" + cmds << zwave.associationV2.associationGroupingsGet() + } + for (int i = 1; i <= associationGroups; i++){ + if(state."actualAssociation${i}" != null){ + if(state."desiredAssociation${i}" != null || state."defaultG${i}") { + def refreshGroup = false + ((state."desiredAssociation${i}"? state."desiredAssociation${i}" : [] + state."defaultG${i}") - state."actualAssociation${i}").each { + log.debug "Adding node $it to group $i" + cmds << zwave.associationV2.associationSet(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + ((state."actualAssociation${i}" - state."defaultG${i}") - state."desiredAssociation${i}").each { + log.debug "Removing node $it from group $i" + cmds << zwave.associationV2.associationRemove(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + if (refreshGroup == true) cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + else log.debug "There are no association actions to complete for group $i" + } + } else { + log.debug "Association info not known for group $i. Requesting info from device." + cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + } + } + return cmds +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + def temp = [] + if (cmd.nodeId != []) { + cmd.nodeId.each { + temp += it.toString().format( '%02x', it.toInteger() ).toUpperCase() + } + } + state."actualAssociation${cmd.groupingIdentifier}" = temp + log.debug "Associations for Group ${cmd.groupingIdentifier}: ${temp}" + updateDataValue("associationGroup${cmd.groupingIdentifier}", "$temp") +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) { + sendEvent(name: "groups", value: cmd.supportedGroupings) + log.debug "Supported association groups: ${cmd.supportedGroupings}" + state.associationGroups = cmd.supportedGroupings +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + log.debug cmd + if(cmd.applicationVersion && cmd.applicationSubVersion) { + def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}" + state.needfwUpdate = "false" + sendEvent(name: "status", value: "fw: ${firmware}") + updateDataValue("firmware", firmware) + } +} + +def zwaveEvent(hubitat.zwave.commands.protectionv2.ProtectionReport cmd) { + log.debug cmd + def integerValue = cmd.localProtectionState + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + if (childDevice) { + childDevice.sendEvent(name: "switch", value: integerValue > 0 ? "on" : "off") + } +} diff --git a/Drivers/inovelli-switch-nzw30.src/inovelli-switch-nzw30.groovy b/Drivers/inovelli-switch-nzw30.src/inovelli-switch-nzw30.groovy new file mode 100644 index 0000000..8e5d741 --- /dev/null +++ b/Drivers/inovelli-switch-nzw30.src/inovelli-switch-nzw30.groovy @@ -0,0 +1,334 @@ +/** + * Inovelli Switch NZW30 + * Author: Eric Maycock (erocm123) + * Date: 2018-04-11 + * 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. + * + * 2018-04-11: No longer deleting child devices when user toggles the option off. SmartThings was throwing errors. + * User will have to manually delete them. + * + * 2018-03-08: Added support for local protection to disable local control. Requires firmware 1.03+. + * Also merging handler from NZW30T as they are identical other than the LED indicator. + * Child device creation option added for local control setting. Child device must be installed: + * https://github.com/erocm123/SmartThingsPublic/blob/master/devicetypes/erocm123/switch-level-child-device.src + * + * 2018-02-26: Added support for Z-Wave Association Tool SmartApp. Associations require firmware 1.02+. + * https://github.com/erocm123/SmartThingsPublic/tree/master/smartapps/erocm123/parent/zwave-association-tool.src + */ + +metadata { + definition (name: "Inovelli Switch NZW30", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + capability "Health Check" + capability "Configuration" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "setAssociationGroup", ["number", "enum", "number", "number"] // group number, nodes, action (0 - remove, 1 - add), multi-channel endpoint (optional) + + fingerprint mfr: "0312", prod: "0117", model: "1E1C", deviceJoinName: "Inovelli Switch" + fingerprint mfr: "015D", prod: "0117", model: "1E1C", deviceJoinName: "Inovelli Switch" + fingerprint mfr: "015D", prod: "1E01", model: "1E01", deviceJoinName: "Inovelli Switch" + fingerprint mfr: "0312", prod: "1E01", model: "1E01", deviceJoinName: "Inovelli Switch" + fingerprint deviceId: "0x1001", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x73,0x25,0x27,0x70,0x75,0x22,0x8E,0x55,0x6C,0x7A" + } + + simulator { + } + + preferences { + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when light is: (Paddle Switch Only)\n", description: "Tap to set", required: false, options:[[1: "On"], [0: "Off"], [2: "Disable"], [3: "Always On"]], defaultValue: 1 + input "invert", "enum", title: "Invert Switch\n\nInvert on & off on the physical switch", description: "Tap to set", required: false, options:[[0: "No"], [1: "Yes"]], defaultValue: 0 + input "disableLocal", "enum", title: "Disable Local Control\n\nDisable ability to control switch from the wall\n(Firmware 1.02+)", description: "Tap to set", required: false, options:[[2: "Yes"], [0: "No"]], defaultValue: 1 + input description: "Use the below options to enable child devices for the specified settings. This will allow you to adjust these settings using SmartApps such as Smart Lighting. If any of the options are enabled, make sure you have the appropriate child device handlers installed.\n(Firmware 1.02+)", title: "Child Devices", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "enableDisableLocalChild", "bool", title: "Disable Local Control", description: "", required: false + input description: "Use the \"Z-Wave Association Tool\" SmartApp to set device associations. (Firmware 1.02+)\n\nGroup 2: Sends on/off commands to associated devices when switch is pressed (BASIC_SET).", title: "Associations", displayDuringSetup: false, type: "paragraph", element: "paragraph" + } + + tiles { + 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.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}') + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def installed() { + log.debug "installed()" + refresh() +} + +def configure() { + log.debug "configure()" + def cmds = initialize() + commands(cmds) +} + +def updated() { + if (!state.lastRan || now() >= state.lastRan + 2000) { + log.debug "updated()" + state.lastRan = now() + def cmds = initialize() + response(commands(cmds)) + } else { + log.debug "updated() ran within the last 2 seconds. Skipping execution." + } +} + +def initialize() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + if (enableDisableLocalChild && !childExists("ep101")) { + try { + addChildDevice("Switch Level Child Device", "${device.deviceNetworkId}-ep101", null, + [completedSetup: true, label: "${device.displayName} (Disable Local Control)", + isComponent: true, componentName: "ep101", componentLabel: "Disable Local Control"]) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Child device creation failed. Make sure the device handler for \"Switch Level Child Device\" is installed"]]) + } + } else if (!enableDisableLocalChild && childExists("ep101")) { + log.debug "Trying to delete child device ep101. If this fails it is likely that there is a SmartApp using the child device in question." + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + try { + log.debug "SmartThings has issues trying to delete the child device when it is in use. Need to manually delete them." + //if(childDevice) deleteChildDevice(childDevice.deviceNetworkId) + } catch (e) { + runIn(3, "sendAlert", [data: [message: "Failed to delete child device. Make sure the device is not in use by any SmartApp."]]) + } + } + if (device.label != state.oldLabel) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("e101")} + if (childDevice) + childDevice.setLabel("${device.displayName} (Disable Local Control)") + state.oldLabel = device.label + } + + def cmds = processAssociations() + cmds << zwave.versionV1.versionGet() + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: ledIndicator!=null? ledIndicator.toInteger() : 1, parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: invert!=null? invert.toInteger() : 0, parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff!=null? autoOff.toInteger() : 0, parameterNumber: 5, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5) + if (state.disableLocal != settings.disableLocal) { + cmds << zwave.protectionV2.protectionSet(localProtectionState : disableLocal!=null? disableLocal.toInteger() : 0, rfProtectionState: 0) + cmds << zwave.protectionV2.protectionGet() + } + state.disableLocal = settings.disableLocal + return cmds +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def ping() { + refresh() +} + +def poll() { + refresh() +} + +def refresh() { + commands(zwave.switchBinaryV1.switchBinaryGet()) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def setDefaultAssociations() { + def smartThingsHubID = zwaveHubNodeId.toString().format( '%02x', zwaveHubNodeId ) + state.defaultG1 = [smartThingsHubID] + state.defaultG2 = [] +} + +def setAssociationGroup(group, nodes, action, endpoint = null){ + if (!state."desiredAssociation${group}") { + state."desiredAssociation${group}" = nodes + } else { + switch (action) { + case 0: + state."desiredAssociation${group}" = state."desiredAssociation${group}" - nodes + break + case 1: + state."desiredAssociation${group}" = state."desiredAssociation${group}" + nodes + break + } + } +} + +def processAssociations(){ + def cmds = [] + setDefaultAssociations() + def associationGroups = 5 + if (state.associationGroups) { + associationGroups = state.associationGroups + } else { + log.debug "Getting supported association groups from device" + cmds << zwave.associationV2.associationGroupingsGet() + } + for (int i = 1; i <= associationGroups; i++){ + if(state."actualAssociation${i}" != null){ + if(state."desiredAssociation${i}" != null || state."defaultG${i}") { + def refreshGroup = false + ((state."desiredAssociation${i}"? state."desiredAssociation${i}" : [] + state."defaultG${i}") - state."actualAssociation${i}").each { + log.debug "Adding node $it to group $i" + cmds << zwave.associationV2.associationSet(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + ((state."actualAssociation${i}" - state."defaultG${i}") - state."desiredAssociation${i}").each { + log.debug "Removing node $it from group $i" + cmds << zwave.associationV2.associationRemove(groupingIdentifier:i, nodeId:Integer.parseInt(it,16)) + refreshGroup = true + } + if (refreshGroup == true) cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + else log.debug "There are no association actions to complete for group $i" + } + } else { + log.debug "Association info not known for group $i. Requesting info from device." + cmds << zwave.associationV2.associationGet(groupingIdentifier:i) + } + } + return cmds +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + def temp = [] + if (cmd.nodeId != []) { + cmd.nodeId.each { + temp += it.toString().format( '%02x', it.toInteger() ).toUpperCase() + } + } + state."actualAssociation${cmd.groupingIdentifier}" = temp + log.debug "Associations for Group ${cmd.groupingIdentifier}: ${temp}" + updateDataValue("associationGroup${cmd.groupingIdentifier}", "$temp") +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) { + sendEvent(name: "groups", value: cmd.supportedGroupings) + log.debug "Supported association groups: ${cmd.supportedGroupings}" + state.associationGroups = cmd.supportedGroupings +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + log.debug cmd + if(cmd.applicationVersion && cmd.applicationSubVersion) { + def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}" + state.needfwUpdate = "false" + sendEvent(name: "status", value: "fw: ${firmware}") + updateDataValue("firmware", firmware) + } +} + +def zwaveEvent(hubitat.zwave.commands.protectionv2.ProtectionReport cmd) { + log.debug cmd + def integerValue = cmd.localProtectionState + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep101")} + if (childDevice) { + childDevice.sendEvent(name: "switch", value: integerValue > 0 ? "on" : "off") + } +} diff --git a/Drivers/inovelli-switch-nzw30t-w-scene.src/inovelli-switch-nzw30t-w-scene.groovy b/Drivers/inovelli-switch-nzw30t-w-scene.src/inovelli-switch-nzw30t-w-scene.groovy new file mode 100644 index 0000000..0e9adca --- /dev/null +++ b/Drivers/inovelli-switch-nzw30t-w-scene.src/inovelli-switch-nzw30t-w-scene.groovy @@ -0,0 +1,296 @@ +/** + * Inovelli Switch NZW30T w/Scene + * Author: Eric Maycock (erocm123) + * Date: 2017-12-28 + * + * Copyright 2017 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. + * + */ + +metadata { + definition (name: "Inovelli Switch NZW30T w/Scene", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + //capability "Health Check" + capability "PushableButton" + capability "HoldableButton" + capability "Indicator" + + attribute "lastActivity", "String" + attribute "lastEvent", "String" + + command "pressUpX1" + command "pressDownX1" + command "pressUpX2" + command "pressDownX2" + command "pressUpX3" + command "pressDownX3" + command "pressUpX4" + command "pressDownX4" + command "pressUpX5" + command "pressDownX5" + command "holdUp" + command "holdDown" + + fingerprint mfr: "0312", prod: "1E02", model: "1E02", deviceJoinName: "Inovelli Switch" + } + + simulator { + } + + preferences { + input "autoOff", "number", title: "Auto Off\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767" + input "invert", "enum", title: "Invert Switch", description: "Tap to set", required: false, options:[0: "No", 1: "Yes"], defaultValue: 0 + + input description: "1 pushed - Up 1x click\n2 pushed - Up 2x click\n3 pushed - Up 3x click\n4 pushed - Up 4x click\n5 pushed - Up 5x click\n6 pushed - Up held\n\n1 held - Down 1x click\n2 held - Down 2x click\n3 held - Down 3x click\n4 held - Down 4x click\n5 held - Down 5x click\n6 held - Down held", title: "Button Mappings", displayDuringSetup: false, type: "paragraph", element: "paragraph" + } + + tiles { + 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.lastEvent", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}',icon: "st.unknown.zwave.remote-controller") + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + standardTile("pressUpX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲", backgroundColor: "#ffffff", action: "pressUpX2" + } + standardTile("pressUpX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲", backgroundColor: "#ffffff", action: "pressUpX3" + } + standardTile("pressDownX2", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼", backgroundColor: "#ffffff", action: "pressDownX2" + } + standardTile("pressDownX3", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼", backgroundColor: "#ffffff", action: "pressDownX3" + } + standardTile("pressUpX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX4" + } + standardTile("pressUpX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▲▲▲▲▲", backgroundColor: "#ffffff", action: "pressUpX5" + } + standardTile("holdUp", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Hold ▲", backgroundColor: "#ffffff", action: "holdUp" + } + + standardTile("pressDownX4", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX4" + } + standardTile("pressDownX5", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Tap ▼▼▼▼▼", backgroundColor: "#ffffff", action: "pressDownX5" + } + standardTile("holdDown", "device.button", width: 2, height: 1, decoration: "flat") { + state "default", label: "Hold ▼", backgroundColor: "#ffffff", action: "holdDown" + } + + valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9" + } + valueTile("blank", "device.blank", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: '', icon: "" + } + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: 'Tap on the buttons above to test scenes (ie: Tap ▲ 1x, ▲▲ 2x, etc depending on the button)' + } + valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png" + } + } +} + +def installed() { + refresh() +} + +def updated() { + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "numberOfButtons", value: 6, displayed: true) + def cmds = [] + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + cmds << zwave.configurationV1.configurationSet(configurationValue: [invert? invert.toInteger() : 0], parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: autoOff? autoOff.toInteger() : 0, parameterNumber: 5, size: 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5) + response(commands(cmds)) +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + def now + if(location.timeZone) + now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + else + now = new Date().format("yyyy MMM dd EEE h:mm:ss a") + sendEvent(name: "lastActivity", value: now, displayed:false) + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + switch (cmd.keyAttributes) { + case 0: + createEvent(buttonEvent(cmd.keyAttributes + 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 1: + createEvent(buttonEvent(6, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + case 2: + null + break + default: + createEvent(buttonEvent(cmd.keyAttributes - 1, (cmd.sceneNumber == 2? "pushed" : "held"), "physical")) + break + } +} + +def buttonEvent(button, value, type = "digital") { + if(button != 6) + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Tap '.padRight(button+5, '▼'):' Tap '.padRight(button+5, '▲')}", displayed:false) + else + sendEvent(name:"lastEvent", value: "${value != 'pushed'?' Hold ▼':' Hold ▲'}", displayed:false) + [name: value, value: button, isStateChange:true] +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def ping() { + refresh() +} + +def poll() { + refresh() +} + +def refresh() { + commands(zwave.switchBinaryV1.switchBinaryGet()) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def pressUpX1() { + sendEvent(buttonEvent(1, "pushed")) +} + +def pressDownX1() { + sendEvent(buttonEvent(1, "held")) +} + +def pressUpX2() { + sendEvent(buttonEvent(2, "pushed")) +} + +def pressDownX2() { + sendEvent(buttonEvent(2, "held")) +} + +def pressUpX3() { + sendEvent(buttonEvent(3, "pushed")) +} + +def pressDownX3() { + sendEvent(buttonEvent(3, "held")) +} + +def pressUpX4() { + sendEvent(buttonEvent(4, "pushed")) +} + +def pressDownX4() { + sendEvent(buttonEvent(4, "held")) +} + +def pressUpX5() { + sendEvent(buttonEvent(5, "pushed")) +} + +def pressDownX5() { + sendEvent(buttonEvent(5, "held")) +} + +def holdUp() { + sendEvent(buttonEvent(6, "pushed")) +} + +def holdDown() { + sendEvent(buttonEvent(6, "held")) +} \ No newline at end of file diff --git a/Drivers/lock-child-device.src/lock-child-device.groovy b/Drivers/lock-child-device.src/lock-child-device.groovy new file mode 100644 index 0000000..aa62898 --- /dev/null +++ b/Drivers/lock-child-device.src/lock-child-device.groovy @@ -0,0 +1,31 @@ +/** + * Lock Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Lock Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Lock" + capability "Sensor" + } + + tiles() { + multiAttributeTile(name:"lock", type: "generic"){ + tileAttribute ("device.lock", key: "PRIMARY_CONTROL") { + attributeState "locked", label:'locked', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC", nextState:"unlocking" + attributeState "unlocked", label:'unlocked', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" + } + } + } + +} \ No newline at end of file diff --git a/Drivers/lockable-door-window-child-device.src/lockable-door-window-child-device.groovy b/Drivers/lockable-door-window-child-device.src/lockable-door-window-child-device.groovy new file mode 100644 index 0000000..d231aa3 --- /dev/null +++ b/Drivers/lockable-door-window-child-device.src/lockable-door-window-child-device.groovy @@ -0,0 +1,42 @@ +/** + * Lockable Door/Window Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Lockable Door/Window Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Contact Sensor" + capability "Lock" + capability "Sensor" + } + + tiles() { + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + } + tileAttribute ("lock", key: "SECONDARY_CONTROL") { + attributeState "locked", label:'locked', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC", nextState:"unlocking" + attributeState "unlocked", label:'unlocked', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" + } + } + + + main "contact" + details(["contact", childDeviceTiles("all")]) + } + } + +} \ No newline at end of file diff --git a/Drivers/metering-switch-child-device.src/metering-switch-child-device.groovy b/Drivers/metering-switch-child-device.src/metering-switch-child-device.groovy new file mode 100644 index 0000000..ca39448 --- /dev/null +++ b/Drivers/metering-switch-child-device.src/metering-switch-child-device.groovy @@ -0,0 +1,66 @@ +/** + * Metering Switch Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Metering Switch Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Actuator" + capability "Sensor" + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + + command "reset" + } + + tiles { + multiAttributeTile(name:"switch", type: "lighting", width: 3, 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 "turningOn", 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" + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + } +} + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} + +void refresh() { + parent.childRefresh(device.deviceNetworkId) +} + +void reset() { + parent.childReset(device.deviceNetworkId) +} \ No newline at end of file diff --git a/Drivers/mipow-playbulb.src/find_handle.sh b/Drivers/mipow-playbulb.src/find_handle.sh new file mode 100644 index 0000000..8d5e861 --- /dev/null +++ b/Drivers/mipow-playbulb.src/find_handle.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +candle=$1 + +for i in 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F +do +result=$(gatttool -b "$candle" --char-read -a 0x00$i | sed 's_Characteristic value/descriptor: __g' | egrep '^[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s$') 2>/dev/null +if [ "$result" != "" ] +then + possible_color=$(echo -e "$possible_color\n0x00$i $result") +fi +result=$(gatttool -b "$candle" --char-read -a 0x00$i | sed 's_Characteristic value/descriptor: __g' | egrep '^[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s[0-9a-fA-F][0-9a-fA-F]\s$' 2>/dev/null) +if [ "$result" != "" ] +then + possible_effect=$(echo -e "$possible_effect\n0x00$i $result") +fi +done + +echo "" +echo "" +echo "Possible color handles found" +echo "$possible_color" +echo "" +echo "" +echo "Possible effect handles found" +echo "$possible_effect" diff --git a/Drivers/mipow-playbulb.src/mipow-playbulb.groovy b/Drivers/mipow-playbulb.src/mipow-playbulb.groovy new file mode 100644 index 0000000..fbb4e25 --- /dev/null +++ b/Drivers/mipow-playbulb.src/mipow-playbulb.groovy @@ -0,0 +1,609 @@ +/** + * Copyright 2015 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. + * + * MiPow PlayBulb + * + * Author: Eric Maycock (erocm123) + * Date: 2016-01-27 + */ + +import groovy.json.JsonSlurper + +metadata { + definition (name: "MiPow PlayBulb", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Switch" + capability "Refresh" + capability "Sensor" + + command "setWhiteLevel" + command "fadeOn" + command "flashOn" + command "rainbowfadeOn" + command "rainbowflashOn" + command "candleOn" + command "fadeOff" + command "flashOff" + command "rainbowfadeOff" + command "rainbowflashOff" + command "candleOff" + + command "reset" + + } + + simulator { + } + + preferences { + input("ip", "string", title:"IP Address", description: "10.37.20.150", defaultValue: "10.37.20.150" ,required: true, displayDuringSetup: true) + input("port", "string", title:"Port", description: "88", defaultValue: "88" , required: true, displayDuringSetup: true) + input("deviceId", "string", title:"Device ID", description: "99", defaultValue: "99" , required: true, displayDuringSetup: true) + } + + tiles (scale: 2){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } + + 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.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + controlTile("whiteSliderControl", "device.whiteLevel", "slider", height: 2, width: 4, inactiveLabel: false) { + state "whiteLevel", action:"setWhiteLevel" + } + valueTile("whiteValueTile", "device.whiteLevel", decoration: "flat", height: 2, width: 2) { + state "whiteLevel", label:'${currentValue}%', backgroundColor:"#FFFFFF" + } + standardTile("fade", "device.fade", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false, decoration: "flat") { + state "off", label:"Fade", action:"fadeOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"Fade", action:"fadeOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00a0dc" + } + standardTile("flash", "device.flash", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false, decoration: "flat") { + state "off", label:"Flash", action:"flashOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"Flash", action:"flashOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00a0dc" + } + standardTile("candle", "device.candle", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false, decoration: "flat") { + state "off", label:"Candle", action:"candleOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"Candle", action:"candleOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00a0dc" + } + standardTile("rainbowfade", "device.rainbowfade", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false, decoration: "flat") { + state "off", label:"Rainbow\nFade", action:"rainbowfadeOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"Rainbow\nFade", action:"rainbowfadeOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00a0dc" + } + standardTile("rainbowflash", "device.rainbowflash", height: 2, width: 2, inactiveLabel: false, canChangeIcon: false, decoration: "flat") { + state "off", label:"Rainbow\nFlash", action:"rainbowflashOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"Rainbow \nFlash", action:"rainbowflashOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00a0dc" + } + } + + main(["switch"]) + details(["switch", "levelSliderControl", + "whiteSliderControl", "whiteValueTile", + "fade", "flash", "candle", + "rainbowflash", "rainbowfade", "refresh" ]) +} + +def installed() { + log.debug "installed()" + configure() +} + +def updated() { + log.debug "updated()" + configure() +} + +def configure() { + log.debug "configure()" + log.debug "Configuring Device For SmartThings Use" + state.previousColor="00ffffff" + state.previousEffect="00000000" + if (ip != null && port != null) state.dni = setDeviceNetworkId(ip, port) +} + +def parse(description) { + //log.debug "Parsing: ${description}" + def map = [:] + def descMap = parseDescriptionAsMap(description) + //log.debug "descMap: ${descMap}" + + if (!state.MAC || state.MAC != descMap["mac"]) { + log.debug "Mac address of device found ${descMap["mac"]}" + updateDataValue("MAC", descMap["mac"]) + } + + def body = new String(descMap["body"].decodeBase64()) + + def slurper = new JsonSlurper() + def result = slurper.parseText(body) + + log.debug "result: ${result}" + + if (result.containsKey("power")) { + sendEvent(name: "switch", value: result.power) + } + if (result.containsKey("color")) { + if (result.color.substring(0,2) != "00") { + sendEvent(name: "color", value: "#ffffff") + sendEvent(name: "whiteLevel", value: Integer.parseInt(result.color.substring(0,2),16)/255 * 100 as Integer) + } else { + sendEvent(name: "color", value: getScaledColor(result.color.substring(2))) + sendEvent(name: "whiteLevel", value: 0) + } + //toggleTiles("all") + } + if (result.containsKey("effect")) { + if (result.effect == "00000000") { + toggleTiles("all") + } + else { + toggleTiles(getEffectName(result.effect)) + sendEvent(name: getEffectName(result.effect), value: "on") + } + if (result.effect == "ff000100") { + state.previousEffect="00000000" + } else { + state.previousEffect="${result.effect}" + } + } +} + +private toggleTiles(value) { + def tiles = ["fade", "flash", "rainbowfade", "rainbowflash", "candle"] + tiles.each {tile -> + if (tile != value) sendEvent(name: tile, value: "off") + } +} + +private getScaledColor(color) { + def rgb = color.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + def maxNumber = 1 + for (int i = 0; i < 3; i++){ + if (rgb[i] > maxNumber) { + maxNumber = rgb[i] + } + } + def scale = 255/maxNumber + for (int i = 0; i < 3; i++){ + rgb[i] = rgb[i] * scale + } + def myred = rgb[0] + def mygreen = rgb[1] + def myblue = rgb[2] + return rgbToHex([r:myred, g:mygreen, b:myblue]) +} + +private getEffectName(effect) { + switch (effect) { + case "01000f0f": return "fade"; break + case "00000f0f": return "flash"; break + case "03000f0f": return "rainbowfade"; break + case "02000f0f": return "rainbowflash"; break + case "04000100": return "candle"; break + } +} + +def on() { + log.debug "on()" + if (state.previousColor != "00000000" || state.previousEffect != "00000000") { + if (state.previousEffect != "00000000") { + setColor(effect: state.previousEffect) + } + else { + setColor(pow: state.previousColor) + //setColor(pow: "ff000000") + } + } + else { + setColor(white: "ff") + } +} + +def off() { + log.debug "off()" + def uri = "/playbulb.php?device=${deviceId}&setting=off" + postAction(uri) +} + +def setLevel(level) { + setLevel(level, 1) +} +def setLevel(level, duration) { + log.debug "setLevel() level = ${level}" + if(level > 100) level = 100 + if (level == 0) { off() } + else if (device.latestValue("switch") == "off") { on() } + sendEvent(name: "level", value: level) + sendEvent(name: "setLevel", value: level, displayed: false) + setColor(aLevel: level) +} +def setSaturation(percent) { + log.debug "setSaturation($percent)" + setColor(saturation: percent) +} +def setHue(value) { + log.debug "setHue($value)" + setColor(hue: value) +} +def getWhite(value) { + log.debug "getWhite($value)" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + return hex(level) +} +def setColor(value) { + log.debug "setColor being called with ${value}" + def uri + + + if ( !(value.hex) && (value.saturation) && (value.hue)) { + def rgb = huesatToRGB(value.hue as Integer, value.saturation as Integer) + value.hex = rgbToHex([r:rgb[0], g:rgb[1], b:rgb[2]]) + } + if ( value.level > 1 ) { + sendEvent("name":"level", "value": value.level) + } + + if (value.hue == 5 && value.saturation == 4) { + log.debug "setting color Soft White" + def whiteLevel = getWhite(value.level) + if (state.previousEffect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000${state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000" + } + state.previousColor = "${whiteLevel}000000" + } + else if (value.hue == 79 && value.saturation == 7) { + log.debug "setting color White" + def whiteLevel = getWhite(value.level) + if (state.previousEffect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000${state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000" + } + state.previousColor = "${whiteLevel}000000" + } + else if (value.hue == 53 && value.saturation == 91) { + log.debug "setting color Daylight" + def whiteLevel = getWhite(value.level) + if (state.previousEffect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000${state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000" + } + state.previousColor = "${whiteLevel}000000" + } + else if (value.hue == 20 && value.saturation == 80) { + log.debug "setting color Warm White" + def whiteLevel = getWhite(value.level) + if (state.previousEffect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000${state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000" + } + state.previousColor = "${whiteLevel}000000" + } + else if (value.colorTemperature) { + log.debug "setting color with color temperature" + def whiteLevel = getWhite(value.level) + if (state.previousEffect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000${state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=${whiteLevel}000000" + } + state.previousColor = "${whiteLevel}000000" + } + else if (value.pow) { + log.debug "setting color with MiPow setting" + uri = "/playbulb.php?device=${deviceId}&setting=${value.pow}" + state.previousColor = "${value.pow}" + } + else if (value.hex) { + log.debug "setting color with hex" + def rgb = hexToRgb(getScaledColor(value.hex.substring(1))) + def myred = rgb.r + def mygreen = rgb.g + def myblue = rgb.b + def dimmedColor = getDimmedColor(rgbToHex([r:myred, g:mygreen, b:myblue])) + if (state.previousEffect == "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=00${dimmedColor.substring(1)}" + } else { + if (state.previousEffect != "01000f0f") { + uri = "/playbulb.php?device=${deviceId}&setting=00${dimmedColor.substring(1) + state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=00${rgbToHex([r:myred, g:mygreen, b:myblue]).substring(1) + state.previousEffect}" + } + } + + state.previousColor = "00${dimmedColor.substring(1)}" + } + else if (value.white) { + if (state.previousEffect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=${value.white}000000${state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=${value.white}000000" + } + state.previousColor = "${value.white}000000" + } + else if (value.effect) { + log.debug "setting effect with ${value.effect}" + def effectColor = state.previousColor + log.debug "${effectColor}" + if (value.effect == "01000f0f") { + //Fade function only works with set colors + if (state.previousColor.substring(0,2) == "00") { + def rgb = getScaledColor(state.previousColor.substring(2)).findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + def myred = rgb[0] >=128 ? 255 : 0 + def mygreen = rgb[1] >=128 ? 255 : 0 + def myblue = rgb[2] >=128 ? 255 : 0 + effectColor = "00${rgbToHex([r:myred, g:mygreen, b:myblue]).substring(1)}" + } else { + effectColor = "ff000000" + } + } + if (value.effect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=${effectColor + value.effect}" + } else { + if (state.previousEffect == "04000100") { + uri = "/playbulb.php?device=${deviceId}&setting=${state.previousColor}ff000100" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=${state.previousColor}" + } + } + } + else if (value.aLevel) { + if (state.previousColor.substring(2) == "000000") { + state.previousColor = "00ffffff" + } + if (state.previousEffect != "00000000") { + uri = "/playbulb.php?device=${deviceId}&setting=00${getDimmedColor(state.previousColor.substring(2)).substring(1) + state.previousEffect}" + } else { + uri = "/playbulb.php?device=${deviceId}&setting=00${getDimmedColor(state.previousColor.substring(2)).substring(1)}" + } + state.previousColor = "00${getDimmedColor(state.previousColor.substring(2)).substring(1)}" + } + else { + // A valid color was not chosen. Setting to white + uri = "/playbulb.php?device=${deviceId}&setting=ff000000" + state.previousColor = "ff000000" + } + + if (uri != null) postAction(uri) + +} + +private getDimmedColor(color) { + if (device.latestValue("level")) { + def newLevel = device.latestValue("level") + def colorHex = getScaledColor(color) + def rgb = colorHex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + def myred = rgb[0] + def mygreen = rgb[1] + def myblue = rgb[2] + + colorHex = rgbToHex([r:myred, g:mygreen, b:myblue]) + def c = hexToRgb(colorHex) + + def r = hex(c.r * (newLevel/100)) + def g = hex(c.g * (newLevel/100)) + def b = hex(c.b * (newLevel/100)) + + return "#${r + g + b}" + } else { + return color + } +} + +def reset() { + log.debug "reset()" + setColor(white: "ff") +} +def refresh() { + log.debug "refresh()" + def uri = "/playbulb.php?device=${deviceId}&refresh=true" + postAction(uri) +} + +def setWhiteLevel(value) { + log.debug "setwhiteLevel: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + if ( value > 0 ) { + if (device.latestValue("switch") == "off") { on() } + sendEvent(name: "white", value: "on") + } else { + sendEvent(name: "white", value: "off") + } + def whiteLevel = hex(level) + setColor(white: whiteLevel) +} + +def hexToRgb(colorHex) { + def rrInt = Integer.parseInt(colorHex.substring(1,3),16) + def ggInt = Integer.parseInt(colorHex.substring(3,5),16) + def bbInt = Integer.parseInt(colorHex.substring(5,7),16) + + def colorData = [:] + colorData = [r: rrInt, g: ggInt, b: bbInt] + colorData +} + +def huesatToRGB(float hue, float sat) { + while(hue >= 100) hue -= 100 + int h = (int)(hue / 100 * 6) + float f = hue / 100 * 6 - h + int p = Math.round(255 * (1 - (sat / 100))) + int q = Math.round(255 * (1 - (sat / 100) * f)) + int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) + switch (h) { + case 0: return [255, t, p] + case 1: return [q, 255, p] + case 2: return [p, 255, t] + case 3: return [p, q, 255] + case 4: return [t, p, 255] + case 5: return [255, p, q] + } +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} +def rgbToHex(rgb) { + def r = hex(rgb.r) + def g = hex(rgb.g) + def b = hex(rgb.b) + def hexColor = "#${r}${g}${b}" + + hexColor +} + +private postAction(uri){ + log.debug "uri ${uri}" + updateDNI() + def headers = getHeader() + //log.debug("headders: " + headers) + + def hubAction = new hubitat.device.HubAction( + method: "GET", + path: uri, + headers: headers + ) + 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" + } + log.debug "Device Network Id set to ${myDNI}" + return myDNI +} + +private updateDNI() { + if (device.deviceNetworkId != state.dni) { + device.deviceNetworkId = state.dni + } +} + +private getHostAddress() { + return "${ip}:${port}" +} + +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 +} + +def parseDescriptionAsMap(description) { + description.split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +private getHeader(){ + //log.debug "Getting headers" + def headers = [:] + headers.put("HOST", getHostAddress()) + //log.debug "Headers are ${headers}" + return headers +} + +def toAscii(s){ + StringBuilder sb = new StringBuilder(); + String ascString = null; + long asciiInt; + for (int i = 0; i < s.length(); i++){ + sb.append((int)s.charAt(i)); + sb.append("|"); + char c = s.charAt(i); + } + ascString = sb.toString(); + asciiInt = Long.parseLong(ascString); + return asciiInt; +} + +def fadeOn() { + log.debug "fadeOn()" + setColor(effect: "01000f0f") +} +def fadeOff() { + log.debug "fadeOff()" + setColor(effect: "00000000") +} +def flashOn() { + log.debug "flashOn()" + setColor(effect: "00000f0f") +} +def flashOff() { + log.debug "flashOff()" + setColor(effect: "00000000") +} +def candleOn() { + log.debug "candleOn()" + setColor(effect: "04000100") +} +def candleOff() { + log.debug "candleOff()" + setColor(effect: "00000000") +} +def rainbowfadeOn() { + log.debug "rainbowfadeOn()" + setColor(effect: "03000f0f") +} +def rainbowfadeOff() { + log.debug "rainbowfadeOff()" + setColor(effect: "00000000") +} +def rainbowflashOn() { + log.debug "fainbowflashOn()" + setColor(effect: "02000f0f") +} +def rainbowflashOff() { + log.debug "fainbowflashOff()" + setColor(effect: "00000000") +} \ No newline at end of file diff --git a/Drivers/mipow-playbulb.src/playbulb.php b/Drivers/mipow-playbulb.src/playbulb.php new file mode 100644 index 0000000..9ff1076 --- /dev/null +++ b/Drivers/mipow-playbulb.src/playbulb.php @@ -0,0 +1,227 @@ + $n[0], 'color_handle' => $n[1], 'effect_handle' => $n[2] ); + } + echo json_encode( $return ); + +} + +if ($device && $refresh) { + + $devices = explode(",", $device); + + for ($x = 0; $x < count($candles); $x++) { + if ("$devices[0]" == "candle" . ($x+1)) + { + $candle=$candles[$x][0]; + $color_handle=$candles[$x][1]; + $effect_handle=$candles[$x][2]; + } + } + + $output = shell_exec("gatttool -b $candle --char-read -a $effect_handle | sed 's_Characteristic value/descriptor: __g'"); + $effect_value = explode(" ", $output); + $effect_value = $effect_value[4] . $effect_value[5] . $effect_value[6] . $effect_value[7]; + $effect_color = explode(" ", $output); + $effect_color = $effect_color[0] . $effect_color[1] . $effect_color[2] . $effect_color[3]; + $output = shell_exec("gatttool -b $candle --char-read -a $color_handle | sed 's_Characteristic value/descriptor: __g'"); + $candle_color = trim(str_replace(" ", "", $output)); + + if (($candle_color === "00000000" && $effect_color === "00000000") || $effect_color === "00000000") { + //echo $device . " is off"; + $return = array( 'device' => $device, 'power' => 'off' ); + echo json_encode( $return ); + } + else { + //echo $device . " is on"; + $return = array( 'device' => $device, 'power' => 'on' ); + if ($candle_color !== "00000000") { + $return['color'] = $candle_color; + $return['effe'] = $effect_value; + $return['ecol'] = $effect_color; + } + echo json_encode( $return ); + } +} + +if ($device && $setting) { + +$devices = explode(",", $device); +$all_devices = []; + +for ($x = 0; $x < count($candles); $x++) { +for ($y = 0; $y < count($devices); $y++) { + if ("$devices[$y]" == "candle" . ($x+1)) + { + array_push ( $all_devices, array($candles[$x][0],$candles[$x][1],$candles[$x][2]) ); + } +} +} + +switch ($setting) { + case off: + $handle=$color_handle; + $color="0000000001000f0f"; + break; + case red: + $handle=$color_handle; + $color="00ff0000"; + break; + case green: + $handle=$color_handle; + $color="0000ff00"; + break; + case blue: + $handle=$color_handle; + $color="000000ff"; + break; + case white: + $handle=$color_handle; + $color="ff000000"; + break; + case yellow: + $handle=$color_handle; + $color="00ffff00"; + break; + case magenta: + $handle=$color_handle; + $color="00ff00ff"; + break; + case cyan: + $handle=$color_handle; + $color="0000ffff"; + break; + case flashred: + $handle=$effect_handle; + $color="00ff000000000f0f"; + break; + case flashgreen: + $handle=$effect_handle; + $color="0000ff0000000f0f"; + break; + case flashblue: + $handle=$effect_handle; + $color="000000ff00000f0f"; + break; + case flashwhite: + $handle=$effect_handle; + $color="ff00000000000f0f"; + break; + case flashyellow: + $handle=$effect_handle; + $color="00ffff0000000f0f"; + break; + case flashmagenta: + $handle=$effect_handle; + $color="00ff00ff00000f0f"; + break; + case flashcyan: + $handle=$effect_handle; + $color="0000ffff00000f0f"; + break; + case fadered: + $handle=$effect_handle; + $color="00ff000001000f0f"; + break; + case fadegreen: + $handle=$effect_handle; + $color="0000ff0001000f0f"; + break; + case fadeblue: + $handle=$effect_handle; + $color="000000ff01000f0f"; + break; + case fadewhite: + $handle=$effect_handle; + $color="ff00000001000f0f"; + break; + case fadeyellow: + $handle=$effect_handle; + $color="00ffff0001000f0f"; + break; + case fademagenta: + $handle=$effect_handle; + $color="00ff00ff01000f0f"; + break; + case fadecyan: + $handle=$effect_handle; + $color="0000ffff00000f0f"; + break; + case faderainbow: + $handle=$effect_handle; + $color="00ff000003000f0f"; + break; + case flashrainbow: + $handle=$effect_handle; + $color="00ff000002000f0f"; + break; + default: + if (strlen($setting) == 8) + { + $handle=$color_handle; + $color=$setting; + } + elseif (strlen($setting) == 16) + { + $handle=$effect_handle; + $color=$setting; + } + else + { + // A valid option was not chosen" + } +} + +if ( $color === "00000000" || substr($color, 0, 8) === "00000000" ) +{ + $return = array( 'device' => $device, 'power' => 'off' ); +} else { + if (strlen($color) == 16) + { + $return = array( 'device' => $device, 'power' => 'on', 'color' => substr($color, 0, 8), 'effect' => substr($color, 8, 8) ); + } + else if (strlen($color) == 8) { + $return = array( 'device' => $device, 'power' => 'on', 'color' => $color, 'effect' => '00000000' ); + } + else { + $return = array( 'error' => 'A valid option was not chosen' ); + } +} +echo json_encode( $return ); + +foreach ($all_devices as $x) +{ + //echo $x[0] . " " . $x[1] . " " . $x[2]; + if (strlen($color) == 16) + { + $output = shell_exec("gatttool -b $x[0] --char-write -a $x[2] -n $color" ); + } else { + $output = shell_exec("gatttool -b $x[0] --char-write -a $x[1] -n $color" ); + } + usleep(500000); +} +} + +?> diff --git a/Drivers/mipow-playbulb.src/playbulb.sh b/Drivers/mipow-playbulb.src/playbulb.sh new file mode 100644 index 0000000..de938a1 --- /dev/null +++ b/Drivers/mipow-playbulb.src/playbulb.sh @@ -0,0 +1,140 @@ +#!/bin/sh + +if [ "$1" = "candle1" ] +then +candle=xx:xx:xx:xx:xx:xx +color_handle="0x0019" +effect_handle="0x0017" +fi +if [ "$1" = "candle2" ] +then +candle=xx:xx:xx:xx:xx:xx +color_handle="0x0019" +effect_handle="0x0017" +fi +if [ "$1" = "candle3" ] +then +candle=xx:xx:xx:xx:xx:xx +color_handle="0x0019" +effect_handle="0x0017" +fi +if [ "$1" = "candle4" ] +then +candle=xx:xx:xx:xx:xx:xx +color_handle="0x001B" +effect_handle="0x0019" +fi + +if [ "$2" = "red" ] +then + handle=$color_handle + color="00ff0000" +elif [ "$2" = "blue" ] +then + handle=$color_handle + color="000000ff" +elif [ "$2" = "green" ] +then + handle=$color_handle + color="0000ff00" +elif [ "$2" = "magenta" ] +then + handle=$color_handle + color="00ff00ff" +elif [ "$2" = "yellow" ] +then + handle=$color_handle + color="00ffff00" +elif [ "$2" = "white" ] +then + handle=$color_handle + color="ff000000" +elif [ "$2" = "cyan" ] +then + handle=$color_handle + color="0000ffff" +elif [ "$2" = "off" ] +then + handle=$color_handle + color="00000000" +elif [ "$2" = "fadewhite" ] +then + handle=$effect_handle + color="ff00000001000f0f" +elif [ "$2" = "fadered" ] +then + handle=$effect_handle + color="00ff000001000f0f" +elif [ "$2" = "fadegreen" ] +then + handle=$effect_handle + color="0000ff0001000f0f" +elif [ "$2" = "fadeblue" ] +then + handle=$effect_handle + color="000000ff01000f0f" +elif [ "$2" = "fadeyellow" ] +then + handle=$effect_handle + color="00ffff0001000f0f" +elif [ "$2" = "fademagenta" ] +then + handle=$effect_handle + color="00ff00ff01000f0f" +elif [ "$2" = "fadecyan" ] +then + handle=$effect_handle + color="0000ffff00000f0f" +elif [ "$2" = "flashwhite" ] +then + handle=$effect_handle + color="ff00000000000f0f" +elif [ "$2" = "flashred" ] +then + handle=$effect_handle + color="00ff000000000f0f" +elif [ "$2" = "flashgreen" ] +then + handle=$effect_handle + color="0000ff0000000f0f" +elif [ "$2" = "flashblue" ] +then + handle=$effect_handle + color="000000ff00000f0f" +elif [ "$2" = "flashyellow" ] +then + handle=$effect_handle + color="00ffff0000000f0f" +elif [ "$2" = "flashmagenta" ] +then + handle=$effect_handle + color="00ff00ff00000f0f" +elif [ "$2" = "flashcyan" ] +then + handle=$effect_handle + color="0000ffff00000f0f" +elif [ "$2" = "flashrainbow" ] +then + handle=$effect_handle + color="00ff000002000f0f" +elif [ "$2" = "faderainbow" ] +then + handle=$effect_handle + color="00ff000003000f0f" +else + if [ ${#2} = 8 ] + then + handle=$color_handle + color=$2 + elif [ ${#2} = 16 ] + then + handle=$effect_handle + color=$2 + else + echo "A valid option was not chosen" + fi +fi + +gatttool -b $candle --char-write -a $handle -n $color +gatttool -b $candle --char-write -a $handle -n $color +gatttool -b $candle --char-write -a $handle -n $color diff --git a/Drivers/motion-sensor-child-device.src/motion-sensor-child-device.groovy b/Drivers/motion-sensor-child-device.src/motion-sensor-child-device.groovy new file mode 100644 index 0000000..d5d6534 --- /dev/null +++ b/Drivers/motion-sensor-child-device.src/motion-sensor-child-device.groovy @@ -0,0 +1,31 @@ +/** + * Motion Sensor Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Motion Sensor Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Motion Sensor" + capability "Sensor" + } + + tiles() { + multiAttributeTile(name:"motion", type: "generic"){ + tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#00A0DC" + attributeState "inactive", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#cccccc" + } + } + } + +} diff --git a/Drivers/philio-pan04-dual-relay.src/philio-pan04-dual-relay.groovy b/Drivers/philio-pan04-dual-relay.src/philio-pan04-dual-relay.groovy new file mode 100644 index 0000000..f9db931 --- /dev/null +++ b/Drivers/philio-pan04-dual-relay.src/philio-pan04-dual-relay.groovy @@ -0,0 +1,386 @@ +/** + * + * Philio Pan04 Dual Relay Device Type + * + * Author: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2015-10-29 + * + * NOTE: As of the 2016-02-23 update, this handler is no longer recommended for the Enerwave and Monoprice relays. + * It has been optimized for performance and functionality with the Philio. + * You can still get excellent results with my Generic Dual Relay if you are using the Enerwave or Monoprice: + * https://github.com/erocm123/SmartThingsPublic/blob/master/devicetypes/erocm123/generic-dual-relay.src/generic-dual-relay.groovy + * + * 2016-02-23: Complete redesign and support for energy and power on both circuits. + * 2016-01-13: Fixed an error in the MultiChannelCmdEncap method that was stopping the instant status + * update from working correctly. Also removed some unnecessary code. + * 2015-11-17: Added the ability to change config parameters through the device preferences + * + * + * Device Type supports all the feautres of the Pan04 device including both switches, + * current energy consumption in W and cumulative energy consumption in kWh. + */ + +metadata { +definition (name: "Philio PAN04 Dual Relay", namespace: "erocm123", author: "Eric Maycock") { +capability "Switch" +capability "Polling" +capability "Configuration" +capability "Refresh" +capability "Energy Meter" +capability "Power Meter" + +attribute "switch1", "string" +attribute "switch2", "string" +attribute "power1", "number" +attribute "energy1", "number" +attribute "power2", "number" +attribute "energy2", "number" + +command "on1" +command "off1" +command "on2" +command "off2" +command "reset" + +fingerprint mfr: "013C", prod: "0001", model: "0003" +fingerprint deviceId: "0x1001", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x25, 0x20, 0x27, 0x71, 0x2B, 0x2C, 0x75, 0x7A, 0x60, 0x32, 0x70" +} + +simulator { +status "on": "command: 2003, payload: FF" +status "off": "command: 2003, payload: 00" + +// reply messages +reply "2001FF,delay 100,2502": "command: 2503, payload: FF" +reply "200100,delay 100,2502": "command: 2503, payload: 00" +} + +tiles(scale: 2){ + + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + standardTile("switch1", "device.switch1",canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "on", label: "switch1", action: "off1", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch1", action: "on1", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + } + standardTile("switch2", "device.switch2",canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "on", label: "switch2", action: "off2", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch2", action: "on2", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + } + 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.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy1", "device.energy1", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("power1", "device.power1", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy2", "device.energy2", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("power2", "device.power2", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + + main(["switch","switch1", "switch2"]) + details(["switch", + "switch1","energy1","power1", + "switch2","energy2","power2", + "refresh","reset","configure"]) +} + preferences { + //input "paragraph", "paragraph", description: "Input a parameter to change. Watch the debug logs to verify change", displayDuringSetup: false + input name: "parameter1", type: "number", title: "Power Meter Report (in seconds)", defaultValue: 3600, displayDuringSetup: false, required: false + input name: "parameter2", type: "number", title: "Energy Meter Report (in minutes)", defaultValue: 60, displayDuringSetup: false, required: false + input name: "parameter4", type: "enum", title: "Switch Type", defaultValue: "3", displayDuringSetup: true, required: false, options: [ + "1":"Toggle w/Memory", + "2":"Momentary", + "3":"Toggle"] + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + + def statusTextmsg = "" + if (device.currentState('power') && device.currentState('energy')) statusTextmsg = "${device.currentState('power').value} W ${device.currentState('energy').value} kWh" + sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + log.debug "BasicReport ${cmd}" +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sendEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def result = [] + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + //result << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + response(delayBetween(result, 1000)) // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd, ep=null) { + def result + def eName + def pName + def cmds = [] + if (ep) { + eName = "energy${ep}" + pName = "power${ep}" + } else { + eName = "energy" + pName = "power" + (1..2).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + } + if (cmd.scale == 0) { + result = createEvent(name: eName, value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + result = createEvent(name: eName, value: cmd.scaledMeterValue, unit: "kVAh") + } else { + result = createEvent(name: pName, value: cmd.scaledMeterValue, unit: "W") + } + cmds ? [result, response(delayBetween(cmds, 1000))] : result +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) +{ + log.debug "multichannelv3.MultiChannelCapabilityReport $cmd" + if (cmd.endPoint == 2 ) { + def currstate = device.currentState("switch2").getValue() + if (currstate == "on") + sendEvent(name: "switch2", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch2", value: "on", isStateChange: true, display: false) + } + else if (cmd.endPoint == 1 ) { + def currstate = device.currentState("switch1").getValue() + if (currstate == "on") + sendEvent(name: "switch1", value: "off", isStateChange: true, display: false) + else if (currstate == "off") + sendEvent(name: "switch1", value: "on", isStateChange: true, display: false) + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def map = [ name: "switch$cmd.sourceEndPoint" ] + + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand && cmd.commandClass == 50) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) + } else { + switch(cmd.commandClass) { + case 32: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + case 37: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + createEvent(map) + break + } + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'" +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + return createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +def refresh() { + def cmds = [] + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + (1..2).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + delayBetween(cmds, 1000) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) +} + +def poll() { + def cmds = [] + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:2, commandClass:37, command:2).format() + cmds << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + delayBetween(cmds, 1000) +} + +def reset() { + delayBetween([ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet().format() + ], 1000) +} + +def configure() { + log.debug "configure() called" + def cmds = [] + [1, 2, 4].each { n -> + if ( settings."parameter${n}" != null ) { + if ( settings."parameter${n}".value != "" ){ + log.debug "Setting parameter: ${n} to value: ${settings."parameter${n}".value}" + log.debug "The converted value for this parameter is: ${valueCheck(n, (settings."parameter${n}".value as String).toInteger())}" + cmds << zwave.configurationV1.configurationSet(parameterNumber: n, scaledConfigurationValue: valueCheck(n, (settings."parameter${n}".value as String).toInteger())).format() // Set switch to report values for both Relay1 and Relay2 + cmds << zwave.configurationV1.configurationGet(parameterNumber: n).format() + } + } + } + if ( cmds != [] && cmds != null ) return delayBetween(cmds, 1000) else return +} +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + log.debug "Preferences have been changed. Attempting configure()" + def cmds = configure() + response(cmds) +} + +def on() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:1, parameter:[255]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + ], 1000) +} +def off() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:1, parameter:[0]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:3, commandClass:37, command:2).format() + ], 1000) +} + +def on1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[255]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def off1() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:1, parameter:[0]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format() + ], 1000) +} + +def on2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[255]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +def off2() { + delayBetween([ + zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:1, parameter:[0]).format(), + //zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format() + ], 1000) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private valueCheck(number, value) { + switch (number) { + case 1: + return value / 5 + break + case 2: + return value / 10 + break + case 4: + return value + break + default: + return value + break + } +} + +def cmd2Integer(array) { +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break +} +} \ No newline at end of file diff --git a/Drivers/qubino-flush-1-relay.src/qubino-flush-1-relay.groovy b/Drivers/qubino-flush-1-relay.src/qubino-flush-1-relay.groovy new file mode 100644 index 0000000..a3fd492 --- /dev/null +++ b/Drivers/qubino-flush-1-relay.src/qubino-flush-1-relay.groovy @@ -0,0 +1,800 @@ +/** + * + * Qubino Flush 1 Relay + * + * github: Eric Maycock (erocm123) + * Date: 2017-04-19 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ + +metadata { + + definition (name: "Qubino Flush 1 Relay", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Relay Switch" + capability "Configuration" + capability "Energy Meter" + capability "Power Meter" + capability "Temperature Measurement" + capability "Health Check" + + command "reset" + + fingerprint mfr: "0159", prod: "0002", model: "0052" + fingerprint deviceId: "0x1001", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x20,0x27,0x25,0x32,0x31,0x60,0x85,0x8E,0x59,0x70", outClusters: "0x20" + + } + + 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{ + 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 ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + childDeviceTiles("all") + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + standardTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors: + [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + 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" + } + } +} + +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def parse(String description) { + def result = [] + if (description != "updated") { + logging("description: $description", 1) + def cmd = zwave.parse(description, [0x20: 1, 0x70: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + } + + def statusTextmsg = "" + result.each { + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "power") { + statusTextmsg = "${it.value} W ${device.currentValue('energy')? device.currentValue('energy') : "0"} kWh" + } + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "energy") { + statusTextmsg = "${device.currentValue('power')? device.currentValue('power') : "0"} W ${it.value} kWh" + } + } + if (statusTextmsg != "") sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + logging("BasicReport: $cmd", 2) + def result = [] + def value = (cmd.value ? "on" : "off") + def switchEvent = createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value") + result << switchEvent + if (switchEvent.isStateChange) { + result << response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()]) + } + + return result +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd) { + logging("MeterReport $cmd", 2) + def event + if (cmd.scale == 0) { + event = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 2) { + event = createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } + return event +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + logging("SwitchBinaryReport: $cmd", 2) + if (cmd.value != 254) { + createEvent(name: "switch", value: cmd.value? "on" : "off", type: "digital") + } +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + logging("SensorMultilevelReport: $cmd", 2) + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + logging("Temperature Report: $map.value", 2) + break; + default: + map.descriptionText = cmd.toString() + } + + return createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport: $cmd", 2) + if (state.manufacturer != cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + + createEvent(name: "manufacturer", value: cmd.manufacturerName) +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + logging("AssociationReport $cmd", 2) + state."association${cmd.groupingIdentifier}" = cmd.nodeId[0] +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + logging("NotificationReport: $cmd", 2) + def result + + if (cmd.notificationType == 3) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("-i2")} + switch (cmd.event) { + case 0: + switch(settings."i2") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "active") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "detected") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "detected") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "wet") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "detected") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "open") + break + } + break + case 2: + switch(settings."i2") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "inactive") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "clear") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "clear") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "dry") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "clear") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "closed") + break + } + break + } + } + else if (cmd.notificationType == 5) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("-i3")} + switch (cmd.event) { + case 0: + switch(settings."i3") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "active") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "detected") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "detected") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "wet") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "detected") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "open") + break + } + break + case 2: + switch(settings."i3") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "inactive") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "clear") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "clear") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "dry") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "clear") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "closed") + break + } + break + } + }else { + logging("Need to handle this cmd.notificationType: ${cmd.notificationType}", 2) + result = createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("$device.displayName: Unhandled: $cmd", 2) + [:] +} + +def on() { + logging("on()", 1) + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + logging("off()", 1) + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def poll() { + logging("poll()", 1) + command(zwave.switchBinaryV1.switchBinaryGet()) +} + +def refresh() { + logging("refresh()", 1) + commands([ + zwave.switchBinaryV1.switchBinaryGet(), + zwave.meterV2.meterGet(scale: 0), + zwave.meterV2.meterGet(scale: 2), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ]) +} + +def reset() { + logging("reset()", 1) + commands([ + zwave.meterV2.meterReset(), + zwave.meterV2.meterGet() + ]) +} + +def ping() { + logging("ping()", 1) + refresh() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) commands(cmds) +} + +def updated() +{ + logging("updated()", 1) + if (!childDevices) { + createChildDevices() + } + else if (device.label != state.oldLabel) { + childDevices.each { + def newLabel = "${device.displayName} (i${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + state.oldLabel = device.label + } + if (childDevices) { + def childDevice = childDevices.find{it.deviceNetworkId.endsWith("-i2")} + if (childDevice && settings."i2" && settings."i2" != "Disabled" && childDevice.typeName != settings."i2") { + childDevice.setDeviceType(settings."i2") + } + childDevice = childDevices.find{it.deviceNetworkId.endsWith("-i3")} + if (childDevice && settings."i3" && settings."i3" != "Disabled" && childDevice.typeName != settings."i3") { + childDevice.setDeviceType(settings."i3") + } + } + def cmds = [] + cmds = update_needed_settings() + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(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}", + 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 "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.parameterNumber}" = cmd.configurationValue + + def parameterSettings = new XmlSlurper().parseText(configuration_model()).Value.find{it.@index == "${cmd.parameterNumber}"} + + if (settings."${cmd.parameterNumber}" != null || parameterSettings.@hidden == "true") + { + if (convertParam(cmd.parameterNumber, parameterSettings.@hidden != "true"? settings."${cmd.parameterNumber}" : parameterSettings.@value) == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(!state.association4 || state.association4 == "" || state.association4 != 1){ + logging("Setting association group 4", 1) + cmds << zwave.associationV2.associationSet(groupingIdentifier:4, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:4) + } + if(!state.association7 || state.association7 == "" || state.association7 != 1){ + logging("Setting association group 7", 1) + cmds << zwave.associationV2.associationSet(groupingIdentifier:7, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:7) + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave" && it.@disabled != "true"){ + if (currentProperties."${it.@index}" == null) + { + if (it.@setonly == "true"){ + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + } else { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown", 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if ((settings."${it.@index}" != null || "${it.@hidden}" == "true") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}")) + { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + case 110: + if (value < 0) + parValue = value * -1 + 1000 + else + parValue = value + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +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 + } + } +} + +/** +* Convert byte values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private channelNumber(String dni) { + dni.split("-i")[-1] as Integer +} + +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 2..3) { + addChildDevice("erocm123", "Contact Sensor Child Device", "${device.deviceNetworkId}-i${i}", null, + [completedSetup: true, label: "${device.displayName} (i${i})", + isComponent: true, componentName: "i$i", componentLabel: "Input $i"]) + } + } catch (e) { + runIn(2, "sendAlert") + } +} + +private sendAlert() { + sendEvent( + descriptionText: "Child device creation failed. Please make sure that the \"Contact Sensor Child Device\" is installed and published.", + eventType: "ALERT", + name: "childDeviceCreation", + value: "failed", + displayed: true, + ) +} + +def configuration_model() +{ +''' + + + +Range: 0 to 1 +Default: Bi-stable switch type (Toggle) + + + + + + +Range: 0 to 1 +Default: 0 (NO - Normally Open) + + + + + + +Range: 0 to 1 +Default: 0 (NO - Normally Open) + + + + + + +ALL ON active +Range: 0, 1, 2, 255 +Default: ALL ON active, ALL OFF active + + + + + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 1 +Default: 0 (Seconds) + + + + + + +Range: 0 to 1 +Default: 0 (Previous State) + + + + + + +Range: 0 (Disabled) to 100 +Default: 5 (%) + + + + +Range: 0 to 32767 +Default: 0 (Disabled) + + + + +Range: 0 to 1 +Default: 0 (When system is turned off the output is 0V (NC)) + + + + + + + + +Range: 0 to 6, 9 +Default: Home Security; Motion Detection, unknown location + + + + + + + + + + + +Range: 0 to 6, 9 +Default: Home Security; Motion Detection, unknown location + + + + + + + + + + + +In tenths. i.e. 1 = 0.1C, -15 = -1.5C +Range: -100 to 100 +Default: 0 + + + + +Range: 0 to 127 +Default: 5 (0.5°C change) + + + + +Range: 0 to 1 +Default: Disabled + + + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/qubino-flush-1d-relay.src/configure.png b/Drivers/qubino-flush-1d-relay.src/configure.png new file mode 100644 index 0000000..c357187 Binary files /dev/null and b/Drivers/qubino-flush-1d-relay.src/configure.png differ diff --git a/Drivers/qubino-flush-1d-relay.src/configure@2x.png b/Drivers/qubino-flush-1d-relay.src/configure@2x.png new file mode 100644 index 0000000..7d82fc3 Binary files /dev/null and b/Drivers/qubino-flush-1d-relay.src/configure@2x.png differ diff --git a/Drivers/qubino-flush-1d-relay.src/qubino-flush-1d-relay.groovy b/Drivers/qubino-flush-1d-relay.src/qubino-flush-1d-relay.groovy new file mode 100644 index 0000000..c5708a6 --- /dev/null +++ b/Drivers/qubino-flush-1d-relay.src/qubino-flush-1d-relay.groovy @@ -0,0 +1,590 @@ +/** + * + * Qubino Flush 1D Relay + * + * github: Eric Maycock (erocm123) + * Date: 2017-02-20 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ + +metadata { + definition (name: "Qubino Flush 1D Relay", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Relay Switch" + capability "Configuration" + capability "Temperature Measurement" + capability "Health Check" + + fingerprint mfr: "0159", prod: "0002", model: "0053" + fingerprint deviceId: "0x1001", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x20,0x27,0x25,0x85,0x8E,0x59,0x70", outClusters: "0x20" + } + + 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 { + multiAttributeTile(name:"switch", type: "generic", 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 ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + childDeviceTiles("all") + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors: + [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + 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" + } + } +} + +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description, [0x20: 1, 0x70: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + logging("Was hailed: requesting state update", 2) + } else { + logging("Parse returned ${result?.descriptionText}", 1) + } + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + logging("BasicReport ${cmd}", 2) + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + logging("SwitchBinaryReport ${cmd}", 2) + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + logging("SensorMultilevelReport: $cmd", 2) + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + logging("Temperature Report: $map.value", 2) + break; + default: + map.descriptionText = cmd.toString() + } + + return createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport ${cmd}", 2) + if (state.manufacturer != cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + + createEvent(name: "manufacturer", value: cmd.manufacturerName) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + logging("SensorBinaryReport: $cmd", 2) + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("ep2")} + switch (cmd.sensorValue) { + case 0: + switch(settings."i2") { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "active") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "detected") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "detected") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "wet") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "detected") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "open") + break + } + break + case 255: + switch(settings."i2") { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "inactive") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "clear") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "clear") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "dry") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "clear") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "closed") + break + } + break + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("$device.displayName: Unhandled: $cmd", 2) + [:] +} + +def on() { + logging("on()", 1) + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + logging("off()", 1) + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def poll() { + logging("poll()", 1) + command(zwave.switchBinaryV1.switchBinaryGet()) +} + +def ping() { + logging("ping()", 1) + refresh() +} + +def refresh() { + logging("refresh()", 1) + commands([ + zwave.switchBinaryV1.switchBinaryGet(), + zwave.manufacturerSpecificV1.manufacturerSpecificGet(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ]) +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) commands(cmds) +} + +def updated() { + logging("updated()", 1) + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + def newLabel = "${device.displayName} (i${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + state.oldLabel = device.label + } + if (childDevices) { + def childDevice = childDevices.find{it.deviceNetworkId.endsWith("-i2")} + if (childDevice && settings."i2" && settings."i2" != "Disabled" && childDevice.typeName != settings."i2") { + childDevice.setDeviceType(settings."i2") + } + } + def cmds = [] + cmds = update_needed_settings() + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(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}", + 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 "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.parameterNumber}" = cmd.configurationValue + + def parameterSettings = new XmlSlurper().parseText(configuration_model()).Value.find{it.@index == "${cmd.parameterNumber}"} + + if (settings."${cmd.parameterNumber}" != null || parameterSettings.@hidden == "true") { + if (convertParam(cmd.parameterNumber, parameterSettings.@hidden != "true"? settings."${cmd.parameterNumber}" : parameterSettings.@value) == cmd2Integer(cmd.configurationValue)) { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + configuration.Value.each { + if ("${it.@setting_type}" == "zwave" && it.@disabled != "true") { + if (currentProperties."${it.@index}" == null) { + if (it.@setonly == "true") { + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + } else { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown", 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } else if ((settings."${it.@index}" != null || "${it.@hidden}" == "true") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}")) { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number) { + case 110: + if (value < 0) + parValue = value * -1 + 1000 + else + parValue = value + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +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 + } + } +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 2..2) { + addChildDevice("Contact Sensor Child Device", "${device.deviceNetworkId}-i${i}", null, + [completedSetup: true, label: "${device.displayName} (i${i})", + isComponent: true, componentName: "i$i", componentLabel: "Input $i"]) + } + } catch (e) { + runIn(2, "sendAlert") + } +} + +private channelNumber(String dni) { + dni.split("-i")[-1] as Integer +} + +private sendAlert() { + sendEvent( + descriptionText: "Child device creation failed. Please make sure that the \"Contact Sensor Child Device\" is installed and published.", + eventType: "ALERT", + name: "childDeviceCreation", + value: "failed", + displayed: true, + ) +} + +def configuration_model() { +''' + + + +Range: 0 to 1 +Default: Bi-stable switch type (Toggle) + + + + + + +Range: 0 to 1 +Default: 0 (NO - Normally Open) + + + + + + +ALL ON active +Range: 0, 1, 2, 255 +Default: ALL ON active, ALL OFF active + + + + + + + + +Range: 0 to 1 +Default: 0 (Seconds) + + + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 1 +Default: 0 (Previous State) + + + + + + +Range: 0 to 1 +Default: 0 (NC - Normally Closed) + + + + + + + +Range: 0 to 6, 9 +Default: Home Security; Motion Detection, unknown location + + + + + + + + + + + +In tenths. i.e. 1 = 0.1C, -15 = -1.5C +Range: -100 to 100 +Default: 0 + + + + +Range: 0 to 127 +Default: 0 (Disabled) + + + + + + + + + + +''' +} diff --git a/Drivers/qubino-flush-2-relays.src/qubino-flush-2-relays.groovy b/Drivers/qubino-flush-2-relays.src/qubino-flush-2-relays.groovy new file mode 100644 index 0000000..5fb0b5f --- /dev/null +++ b/Drivers/qubino-flush-2-relays.src/qubino-flush-2-relays.groovy @@ -0,0 +1,694 @@ +/** + * + * Qubino Flush 2 Relays + * + * github: Eric Maycock (erocm123) + * Date: 2017-02-22 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ + +metadata { + definition (name: "Qubino Flush 2 Relays", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Configuration" + capability "Refresh" + capability "Energy Meter" + capability "Power Meter" + capability "Temperature Measurement" + capability "Health Check" + + command "reset" + + fingerprint mfr: "0159", prod: "0002", model: "0051" + fingerprint deviceId: "0x1001", inClusters:"0x5E,0x86,0x72,0x5A,0x73,0x20,0x27,0x25,0x32,0x60,0x85,0x8E,0x59,0x70", outClusters:"0x20" + } + + 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 { + 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 ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + 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" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors: + [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + main(["switch","switch1", "switch2"]) + details(["switch", + childDeviceTiles("all"), + "temperature","refresh","configure", + "reset" + ]) + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + logging("Parsed ${cmd} to ${result.inspect()}", 1) + } else { + logging("Non-parsed event: ${description}", 2) + } + + def statusTextmsg = "" + + result.each { + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "power") { + statusTextmsg = "${it.value} W ${device.currentValue('energy')? device.currentValue('energy') : "0"} kWh" + } + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "energy") { + statusTextmsg = "${device.currentValue('power')? device.currentValue('power') : "0"} W ${it.value} kWh" + } + } + if (statusTextmsg != "") sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + logging("BasicReport ${cmd}", 2) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + logging("BasicSet ${cmd}", 2) + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=null) { + logging("SwitchBinaryReport ${cmd} , ${ep}", 2) + if (ep) { + def childDevice = childDevices.find{it.deviceNetworkId == "$device.deviceNetworkId-ep$ep"} + if (childDevice) + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd, ep=null) { + logging("MeterReport $cmd : Endpoint: $ep", 2) + def result + def cmds = [] + if (cmd.scale == 0) { + result = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + result = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } else { + result = [name: "power", value: cmd.scaledMeterValue, unit: "W"] + } + if (ep) { + def childDevice = childDevices.find{it.deviceNetworkId == "$device.deviceNetworkId-ep$ep"} + if (childDevice) + childDevice.sendEvent(result) + } else { + (1..2).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << encap(zwave.meterV2.meterGet(scale: 2), endpoint) + } + return [createEvent(result), response(commands(cmds))] + } +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + logging("MultiChannelCmdEncap ${cmd}", 2) + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + logging("SensorMultilevelReport: $cmd", 2) + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + logging("Temperature Report: $map.value", 2) + break; + default: + map.descriptionText = cmd.toString() + } + + return createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport ${cmd}", 2) + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr", 2) + updateDataValue("MSR", msr) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + logging("Unhandled Event: ${cmd}", 2) +} + +def on() { + logging("on()", 1) + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +def off() { + logging("off()", 1) + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +void childOn(String dni) { + logging("childOn($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds) +} + +void childOff(String dni) { + logging("childOff($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds) +} + +void childRefresh(String dni) { + logging("childRefresh($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 0), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 2), channelNumber(dni)))) + sendHubCommand(cmds) +} + +void childReset(String dni) { + logging("childReset($dni)", 1) + def cmds = [] + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterReset(), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 0), channelNumber(dni)))) + cmds << new hubitat.device.HubAction(command(encap(zwave.meterV2.meterGet(scale: 2), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} + +def poll() { + logging("poll()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def refresh() { + logging("refresh()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + zwave.meterV2.meterGet(scale: 0), + zwave.meterV2.meterGet(scale: 2), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ]) +} + +def reset() { + logging("reset()", 1) + commands([ + zwave.meterV2.meterReset(), + zwave.meterV2.meterGet() + ]) +} + +def ping() { + logging("ping()", 1) + refresh() +} + +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) commands(cmds) +} + +def updated() { + logging("updated()", 1) + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + if (it.label == "${state.oldLabel} (Q${channelNumber(it.deviceNetworkId)})") { + def newLabel = "${device.displayName} (Q${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: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(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}", + 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 "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.parameterNumber}" = cmd.configurationValue + + def parameterSettings = new XmlSlurper().parseText(configuration_model()).Value.find{it.@index == "${cmd.parameterNumber}"} + + if (settings."${cmd.parameterNumber}" != null || parameterSettings.@hidden == "true") { + if (convertParam(cmd.parameterNumber, parameterSettings.@hidden != "true"? settings."${cmd.parameterNumber}" : parameterSettings.@value) == cmd2Integer(cmd.configurationValue)) { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + //cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,1]) + //cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1) + cmds << zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier: 1) + + configuration.Value.each { + if ("${it.@setting_type}" == "zwave" && it.@disabled != "true") { + if (currentProperties."${it.@index}" == null) { + if (it.@setonly == "true") { + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + } else { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown", 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } else if ((settings."${it.@index}" != null || "${it.@hidden}" == "true") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}")) { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number) { + case 110: + if (value < 0) + parValue = value * -1 + 1000 + else + parValue = value + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +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 + } + } +} + +/** +* Convert byte values to integer +*/ +def cmd2Integer(array) { + switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd) + } else { + cmd + } +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 1..2) { + addChildDevice("Metering Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, + [completedSetup: true, label: "${device.displayName} (Q${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Output $i"]) + } + } catch (e) { + log.debug e + runIn(2, "sendAlert") + } +} + +private sendAlert() { + sendEvent( + descriptionText: "Child device creation failed. Please make sure that the \"Metering Switch Child Device\" is installed and published.", + eventType: "ALERT", + name: "childDeviceCreation", + value: "failed", + displayed: true, + ) +} + +def configuration_model() { +''' + + + +Range: 0 to 1 +Default: Bi-stable switch type (Toggle) + + + + + + +Range: 0 to 1 +Default: Bi-stable switch type (Toggle) + + + + + + +ALL ON active +Range: 0, 1, 2, 255 +Default: ALL ON active, ALL OFF active + + + + + + + + +Range: 0 to 1 +Default: 0 (Seconds) + + + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 1 +Default: 0 (Previous State) + + + + + + +Range: 0 (Disabled) to 100 +Default: 5 (%) + + + + +Range: 0 to 32767 +Default: 0 (Disabled) + + + + +Range: 0 (Disabled) to 100 +Default: 5 (%) + + + + +Range: 0 to 32767 +Default: 0 (Disabled) + + + + +Range: 0 to 1 +Default: 0 (NC - Normally Closed) + + + + + + +Range: 0 to 1 +Default: 0 (NC - Normally Closed) + + + + + + +1 to 100 value from 0.1C to 10.0C is added to actual measured temperature. +1001 to 1100 value from -0.1C to -10.0C is subtracted to actual measured temperature. +Range: 1 to 32536 +Default: 32536 (0.0) + + + + +Range: 0 to 127 +Default: 0 (Disabled) + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/qubino-flush-dimmer.src/qubino-flush-dimmer.groovy b/Drivers/qubino-flush-dimmer.src/qubino-flush-dimmer.groovy new file mode 100644 index 0000000..936a00c --- /dev/null +++ b/Drivers/qubino-flush-dimmer.src/qubino-flush-dimmer.groovy @@ -0,0 +1,882 @@ +/** + * + * Qubino Flush Dimmer + * + * github: Eric Maycock (erocm123) + * Date: 2017-02-21 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ + +metadata { + + definition (name: "Qubino Flush Dimmer", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + capability "Switch Level" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Relay Switch" + capability "Configuration" + capability "Energy Meter" + capability "Power Meter" + capability "Temperature Measurement" + capability "Health Check" + + command "reset" + + fingerprint mfr: "0159", prod: "0001", model: "0051" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x20,0x27,0x25,0x26,0x32,0x85,0x8E,0x59,0x70", outClusters: "0x20,0x26" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x20,0x27,0x25,0x26,0x30,0x32,0x60,0x85,0x8E,0x59,0x70", outClusters: "0x20,0x26" + + } + + 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{ + 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" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + childDeviceTiles("all") + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + standardTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors: + [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + 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" + } + } +} + +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + createChildDevices() +} + +def parse(String description) { + def result = [] + if (description != "updated") { + logging("description: $description", 1) + def cmd = zwave.parse(description, [0x20: 1, 0x70: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + } + + def statusTextmsg = "" + result.each { + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "power") { + statusTextmsg = "${it.value} W ${device.currentValue('energy')? device.currentValue('energy') : "0"} kWh" + } + if ((it instanceof Map) == true && it.find{ it.key == "name" }?.value == "energy") { + statusTextmsg = "${device.currentValue('power')? device.currentValue('power') : "0"} W ${it.value} kWh" + } + } + if (statusTextmsg != "") sendEvent(name:"statusText", value:statusTextmsg, displayed:false) + + return result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + logging("BasicReport: $cmd", 2) + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd) { + logging("MeterReport $cmd", 2) + def event + if (cmd.scale == 0) { + event = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 2) { + event = createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } + return event +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + logging("SwitchBinaryReport: $cmd", 2) + if (cmd.value != 254) { + createEvent(name: "switch", value: cmd.value? "on" : "off", type: "digital") + } +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + logging("SwitchMultilevelReport: $cmd", 2) + dimmerEvents(cmd) +} + +def dimmerEvents(hubitat.zwave.Command cmd) { + logging("dimmerEvents: $cmd", 1) + def result = [] + def value = (cmd.value ? "on" : "off") + def switchEvent = createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value") + result << switchEvent + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + if (switchEvent.isStateChange) { + result << response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()]) + } + + return result +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + logging("SensorMultilevelReport: $cmd", 2) + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + logging("Temperature Report: $map.value", 2) + break; + default: + map.descriptionText = cmd.toString() + } + + return createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport: $cmd", 2) + if (state.manufacturer != cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + + createEvent(name: "manufacturer", value: cmd.manufacturerName) +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + logging("AssociationReport $cmd", 2) + state."association${cmd.groupingIdentifier}" = cmd.nodeId[0] +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + logging("SensorBinaryReport: $cmd", 2) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + logging("NotificationReport: $cmd", 2) + def result + + + if (cmd.notificationType == 2) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("-i2")} + switch (cmd.event) { + case 0: + switch(settings."i2") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "active") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "detected") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "detected") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "wet") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "detected") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "open") + break + } + break + case 2: + switch(settings."i2") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "inactive") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "clear") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "clear") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "dry") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "clear") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "closed") + break + } + break + } + } + else if (cmd.notificationType == 5) { + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("-i3")} + switch (cmd.event) { + case 0: + switch(settings."i3") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "active") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "detected") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "detected") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "wet") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "detected") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "open") + break + } + break + case 2: + switch(settings."i3") + { + case "Motion Sensor Child Device": + childDevice.sendEvent(name: "motion", value: "inactive") + break + case "Carbon Monoxide Detector Child Device": + childDevice.sendEvent(name: "carbonMonoxide", value: "clear") + break + case "Carbon Dioxide Detector Child Device": + childDevice.sendEvent(name: "carbonDioxide", value: "clear") + break + case "Water Sensor Child Device": + childDevice.sendEvent(name: "water", value: "dry") + break + case "Smoke Detector Child Device": + childDevice.sendEvent(name: "smoke", value: "clear") + break + case "Contact Sensor Child Device": + childDevice.sendEvent(name: "contact", value: "closed") + break + } + break + } + }else { + logging("Need to handle this cmd.notificationType: ${cmd.notificationType}", 2) + result = createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("$device.displayName: Unhandled: $cmd", 2) + [:] +} + +def on() { + logging("on()", 1) + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + logging("off()", 1) + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def setLevel(level) { + logging("setLevel($level)", 1) + if(level > 99) level = 99 + if(level < 1) level = 1 + commands([ + zwave.basicV1.basicSet(value: level), + ]) +} + +def poll() { + logging("poll()", 1) + command(zwave.switchBinaryV1.switchBinaryGet()) +} + +def refresh() { + logging("refresh()", 1) + commands([ + zwave.switchBinaryV1.switchBinaryGet(), + zwave.meterV2.meterGet(scale: 0), + zwave.meterV2.meterGet(scale: 2), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ]) +} + +def reset() { + logging("reset()", 1) + commands([ + zwave.meterV2.meterReset(), + zwave.meterV2.meterGet() + ]) +} + +def ping() { + logging("ping()", 1) + refresh() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) commands(cmds) +} + +def updated() +{ + logging("updated()", 1) + if (!childDevices) { + createChildDevices() + } + else if (device.label != state.oldLabel) { + childDevices.each { + def newLabel = "${device.displayName} (i${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + state.oldLabel = device.label + } + if (childDevices) { + def childDevice = childDevices.find{it.deviceNetworkId.endsWith("-i2")} + if (childDevice && settings."i2" && settings."i2" != "Disabled" && childDevice.typeName != settings."i2") { + childDevice.setDeviceType(settings."i2") + } + childDevice = childDevices.find{it.deviceNetworkId.endsWith("-i3")} + if (childDevice && settings."i3" && settings."i3" != "Disabled" && childDevice.typeName != settings."i3") { + childDevice.setDeviceType(settings."i3") + } + } + def cmds = [] + cmds = update_needed_settings() + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(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}", + 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 "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.parameterNumber}" = cmd.configurationValue + + def parameterSettings = new XmlSlurper().parseText(configuration_model()).Value.find{it.@index == "${cmd.parameterNumber}"} + + if (settings."${cmd.parameterNumber}" != null || parameterSettings.@hidden == "true") + { + if (convertParam(cmd.parameterNumber, parameterSettings.@hidden != "true"? settings."${cmd.parameterNumber}" : parameterSettings.@value) == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(!state.association9 || state.association9 == "" || state.association9 != 1){ + logging("Setting association group 9", 1) + cmds << zwave.associationV2.associationSet(groupingIdentifier:9, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:9) + } + if(!state.association6 || state.association6 == "" || state.association6 != 1){ + logging("Setting association group 6", 1) + cmds << zwave.associationV2.associationSet(groupingIdentifier:6, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:6) + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave" && it.@disabled != "true"){ + if (currentProperties."${it.@index}" == null) + { + if (it.@setonly == "true"){ + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + } else { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown", 2) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + else if ((settings."${it.@index}" != null || "${it.@hidden}" == "true") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}")) + { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}"), 2) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), "${it.@hidden}" != "true"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + case 110: + if (value < 0) + parValue = value * -1 + 1000 + else + parValue = value + break + default: + parValue = value + break + } + return parValue.toInteger() +} + +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 + } + } +} + +/** +* Convert byte values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private channelNumber(String dni) { + dni.split("-i")[-1] as Integer +} + +private void createChildDevices() { + state.oldLabel = device.label + try { + for (i in 2..3) { + addChildDevice("erocm123", "Contact Sensor Child Device", "${device.deviceNetworkId}-i${i}", null, + [completedSetup: true, label: "${device.displayName} (i${i})", + isComponent: true, componentName: "i$i", componentLabel: "Input $i"]) + } + } catch (e) { + runIn(2, "sendAlert") + } +} + +private sendAlert() { + sendEvent( + descriptionText: "Child device creation failed. Please make sure that the \"Contact Sensor Child Device\" is installed and published.", + eventType: "ALERT", + name: "childDeviceCreation", + value: "failed", + displayed: true, + ) +} + +def configuration_model() +{ +''' + + + +Range: 0 to 1 +Default: Bi-stable switch type (Toggle) + + + + + + +Range: 0 to 1 +Default: Bi-stable switch type (Toggle) + + + + + + +Range: 0 to 1 +Default: 0 (NO - Normally Open) + + + + + + +Range: 0 to 1 +Default: 0 (NO - Normally Open) + + + + + + +ALL ON active +Range: 0, 1, 2, 255 +Default: ALL ON active, ALL OFF active + + + + + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Range: 0 to 32535 +Default: 0 (Disabled) + + + + +Dimming is done by push button or switch connected to I1 (by default). Enabling 3way switch, dimming can be controlled by push button or switch connected to I1 and I2. +Range: 0 to 2 +Default: Disabled + + + + + + + +If Double click function is enabled, a fast double click on the push button will set dimming power at maximum dimming value. +Range: 0 to 1 +Default: Disabled + + + + + + +Range: 0 to 1 +Default: 0 (Previous State) + + + + + + +Range: 0 (Disabled) to 100 +Default: 5 (%) + + + + +Range: 0 to 32767 +Default: 0 (Disabled) + + + + +Range: 1 to 98 +Default: 1 (%) + + + + +Range: 2 to 99 +Default: 99 (%) + + + + +In 10 ms +Range: 50 to 255 +Default: 100 (1000 ms = 1 seconds) + + + + +Range: 1 to 255 +Default: 3 seconds + + + + +Range: 0 to 1 +Default: Disabled + + + + + + +Range: 0 to 127 +Default: 0 (Dimming duration according to parameter 66) + + + + + + +Range: 0 to 6, 9 +Default: Home Security; Motion Detection, unknown location + + + + + + + + + + + +Range: 0 to 6, 9 +Default: Home Security; Motion Detection, unknown location + + + + + + + + + + + +In tenths. i.e. 1 = 0.1C, -15 = -1.5C +Range: -100 to 100 +Default: 0 + + + + +Range: 0 to 127 +Default: 5 (0.5°C change) + + + + +Range: 0 to 1 +Default: Disabled + + + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/remotec-zrc-90-scene-master.src/remotec-zrc-90-scene-master.groovy b/Drivers/remotec-zrc-90-scene-master.src/remotec-zrc-90-scene-master.groovy new file mode 100644 index 0000000..c41418c --- /dev/null +++ b/Drivers/remotec-zrc-90-scene-master.src/remotec-zrc-90-scene-master.groovy @@ -0,0 +1,196 @@ +/** + * Remotec ZRC-90 Scene Master + * Copyright 2015 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. + * + */ +metadata { + definition (name: "Remotec ZRC-90 Scene Master", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "PushableButton" + capability "HoldableButton" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Health Check" + + attribute "sequenceNumber", "number" + attribute "numberOfButtons", "number" + + fingerprint mfr: "5254", prod: "0000", model: "8510" + + fingerprint deviceId: "0x0106", inClusters: "0x5E,0x85,0x72,0x21,0x84,0x86,0x80,0x73,0x59,0x5A,0x5B,0xEF,0x5B,0x84" + } + + simulator { + status "button 1 pushed": "command: 5B03, payload: 23 00 01" + status "button 1 held": "command: 5B03, payload: 2B 02 01" + status "button 1 released": "command: 5B03, payload: 2C 01 01" + status "button 1 double": "command: 5B03, payload: 2F 03 01" + status "button 2 pushed": "command: 5B03, payload: 23 00 02" + status "button 2 held": "command: 5B03, payload: 2B 02 02" + status "button 2 released": "command: 5B03, payload: 2C 01 02" + status "button 2 double": "command: 5B03, payload: 2F 03 02" + status "button 3 pushed": "command: 5B03, payload: 23 00 03" + status "button 3 held": "command: 5B03, payload: 2B 02 03" + status "button 3 released": "command: 5B03, payload: 2C 01 03" + status "button 3 double": "command: 5B03, payload: 2F 03 03" + status "button 4 pushed": "command: 5B03, payload: 23 00 04" + status "button 4 held": "command: 5B03, payload: 2B 02 04" + status "button 4 released": "command: 5B03, payload: 2C 01 04" + status "button 4 double": "command: 5B03, payload: 2F 03 04" + status "button 5 pushed": "command: 5B03, payload: 23 00 05" + status "button 5 held": "command: 5B03, payload: 2B 02 05" + status "button 5 released": "command: 5B03, payload: 2C 01 05" + status "button 5 double": "command: 5B03, payload: 2F 03 05" + status "button 6 pushed": "command: 5B03, payload: 23 00 06" + status "button 6 held": "command: 5B03, payload: 2B 02 06" + status "button 6 released": "command: 5B03, payload: 2C 01 06" + status "button 6 double": "command: 5B03, payload: 2F 03 06" + status "button 7 pushed": "command: 5B03, payload: 23 00 07" + status "button 7 held": "command: 5B03, payload: 2B 02 07" + status "button 7 released": "command: 5B03, payload: 2C 01 07" + status "button 7 double": "command: 5B03, payload: 2F 03 07" + status "button 8 pushed": "command: 5B03, payload: 23 00 08" + status "button 8 held": "command: 5B03, payload: 2B 02 08" + status "button 8 released": "command: 5B03, payload: 2C 01 08" + status "button 8 double": "command: 5B03, payload: 2F 03 08" + status "battery 100%": "command: 8003, payload: 64" + status "wakeup": "command: 8407, payload:" + + } + tiles (scale: 2) { + multiAttributeTile(name:"button", type:"generic", width:6, height:4) { + tileAttribute("device.button", key: "PRIMARY_CONTROL"){ + attributeState "default", label:'', backgroundColor:"#ffffff", icon: "st.unknown.zwave.remote-controller" + } + tileAttribute ("device.battery", key: "SECONDARY_CONTROL") { + attributeState "battery", label:'${currentValue} % battery' + } + + } + valueTile( + "battery", "device.battery", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + valueTile( + "sequenceNumber", "device.sequenceNumber", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}', unit:"" + } + standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + main "button" + details(["button", "battery", "sequenceNumber", "configure"]) + } + + preferences { + input name: "holdMode", type: "enum", title: "Multiple \"held\" events on botton hold? With this option, the controller will send a \"held\" event about every second while holding down a button. If set to No it will send a \"held\" event a single time when the button is released.", defaultValue: "2", displayDuringSetup: true, required: false, options: [ + "1":"Yes", + "2":"No"] + input name: "debug", type: "boolean", title: "Enable Debug?", defaultValue: false, displayDuringSetup: false, required: false + } +} + +def parse(String description) { + def results = [] + logging("${description}") + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1]) + if(cmd) results += zwaveEvent(cmd) + if(!results) results = [ descriptionText: cmd, displayed: false ] + } + + if(state.isConfigured != "true") configure() + + return results +} + +def zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging(cmd) + logging("keyAttributes: $cmd.keyAttributes") + logging("sceneNumber: $cmd.sceneNumber") + logging("sequenceNumber: $cmd.sequenceNumber") + + sendEvent(name: "sequenceNumber", value: cmd.sequenceNumber, displayed:false) + switch (cmd.keyAttributes) { + case 0: + buttonEvent(cmd.sceneNumber, "pushed") + break + case 1: + if (settings.holdMode == "2") buttonEvent(cmd.sceneNumber, "held") + break + case 2: + if (!settings.holdMode || settings.holdMode == "1") buttonEvent(cmd.sceneNumber, "held") + break + case 3: + buttonEvent(cmd.sceneNumber + 8, "pushed") + break + default: + logging("Unhandled CentralSceneNotification: ${cmd}") + break + } +} + +private def logging(message) { + if (settings.debug == "true") log.debug "$message" +} + + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)] + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + return results +} + +def buttonEvent(button, value) { + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true) +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unhandled zwaveEvent: ${cmd}") +} + +def installed() { + logging("installed()") + configure() +} + +def updated() { + logging("updated()") + configure() +} + +def configure() { + logging("configure()") + sendEvent(name: "checkInterval", value: 2 * 60 * 12 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "numberOfButtons", value: 16, displayed: true) + state.isConfigured = "true" +} + +def ping() { + logging("ping()") + logging("Battery Device - Not sending ping commands") +} diff --git a/Drivers/simulated-dimmer.src/simulated-dimmer.groovy b/Drivers/simulated-dimmer.src/simulated-dimmer.groovy new file mode 100644 index 0000000..d0bf257 --- /dev/null +++ b/Drivers/simulated-dimmer.src/simulated-dimmer.groovy @@ -0,0 +1,97 @@ +/** + * Simulated Dimmer + * + * Copyright 2016 Eric Maycock (erocm123) + * + * + * 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. + * + */ + +metadata { + + definition (name: "Simulated Dimmer", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Relay Switch" + capability "Switch Level" + capability "Actuator" + + command "onPhysical" + command "offPhysical" + } + + tiles (scale:2) { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 2, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + } + main "switch" + details(["switch"]) + } +} + +def parse(String description) { + +} + +def parse(Map description) { + def eventMap + if (description.type == null) eventMap = [name:"$description.name", value:"$description.value"] + else eventMap = [name:"$description.name", value:"$description.value", type:"$description.type"] + createEvent(eventMap) +} + +def on() { + log.debug "$version on()" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "$version off()" + sendEvent(name: "switch", value: "off") +} + +def onPhysical() { + log.debug "$version onPhysical()" + sendEvent(name: "switch", value: "on", type: "physical") +} + +def offPhysical() { + log.debug "$version offPhysical()" + sendEvent(name: "switch", value: "off", type: "physical") +} + +def setLevel(value) { + log.debug "setLevel >> value: $value" + def level = Math.max(Math.min(value as Integer, 99), 0) + if (level > 0) { + sendEvent(name: "switch", value: "on") + } else { + sendEvent(name: "switch", value: "off") + } + sendEvent(name: "level", value: level, unit: "%") +} + +def setLevel(value, duration) { + log.debug "setLevel >> value: $value, duration: $duration" + def level = Math.max(Math.min(value as Integer, 99), 0) + setLevel(level) +} + +private getVersion() { + "PUBLISHED" +} \ No newline at end of file diff --git a/Drivers/simulated-energy-switch.src/simulated-energy-switch.groovy b/Drivers/simulated-energy-switch.src/simulated-energy-switch.groovy new file mode 100644 index 0000000..2261c60 --- /dev/null +++ b/Drivers/simulated-energy-switch.src/simulated-energy-switch.groovy @@ -0,0 +1,93 @@ +/** + * Copyright 2015 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. + * + */ +metadata { + + definition (name: "Simulated Energy Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Relay Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Sensor" + capability "Actuator" + + command "onPhysical" + command "offPhysical" + } + + 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.light.off", backgroundColor: "#ffffff", nextState:"turningOn" + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + valueTile("power", "power", decoration: "flat") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + main "switch" + details(["switch"]) + } +} + +def parse(String description) { + +} + +def parse(Map description) { + def eventMap = [] + if (description.type == null) eventMap << createEvent(name:"$description.name", value:"$description.value") + else eventMap << createEvent(name:"$description.name", value:"$description.value", type:"$description.type") + def statusTextmsg = "" + if (description.name == "power") { + if(device.currentState('energy')) statusTextmsg = "${description.value} W ${device.currentState('energy').value} kWh" + else statusTextmsg = "${description.value} W" + } else if (description.name == "energy") { + if(device.currentState('power')) statusTextmsg = "${device.currentState('power').value} W ${description.value} kWh" + else statusTextmsg = "${description.value} kWh" + } + if (statusTextmsg != "") eventMap << createEvent(name:"statusText", value:statusTextmsg, displayed:false) + return eventMap +} + +def on() { + log.debug "$version on()" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "$version off()" + sendEvent(name: "switch", value: "off") +} + +def onPhysical() { + log.debug "$version onPhysical()" + sendEvent(name: "switch", value: "on", type: "physical") +} + +def offPhysical() { + log.debug "$version offPhysical()" + sendEvent(name: "switch", value: "off", type: "physical") +} + +private getVersion() { + "PUBLISHED" +} \ No newline at end of file diff --git a/Drivers/simulated-switch.src/simulated-switch.groovy b/Drivers/simulated-switch.src/simulated-switch.groovy new file mode 100644 index 0000000..1cb2575 --- /dev/null +++ b/Drivers/simulated-switch.src/simulated-switch.groovy @@ -0,0 +1,75 @@ +/** + * Copyright 2015 SmartThings + * + * 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. + * + */ +metadata { + + definition (name: "Simulated Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Relay Switch" + capability "Actuator" + + command "onPhysical" + command "offPhysical" + } + + 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.light.off", backgroundColor: "#ffffff", nextState:"turningOn" + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + } + } + main "switch" + details(["switch"]) + } +} + +def parse(String description) { + //def pair = description.split(":") + //createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def parse(Map description) { + //def pair = description.split(":") + //createEvent(name: pair[0].trim(), value: pair[1].trim()) + def eventMap + if (description.type == null) eventMap = [name:"$description.name", value:"$description.value"] + else eventMap = [name:"$description.name", value:"$description.value", type:"$description.type"] + createEvent(eventMap) +} + +def on() { + log.debug "$version on()" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "$version off()" + sendEvent(name: "switch", value: "off") +} + +def onPhysical() { + log.debug "$version onPhysical()" + sendEvent(name: "switch", value: "on", type: "physical") +} + +def offPhysical() { + log.debug "$version offPhysical()" + sendEvent(name: "switch", value: "off", type: "physical") +} + +private getVersion() { + "PUBLISHED" +} diff --git a/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC01.ino.generic.bin b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC01.ino.generic.bin new file mode 100644 index 0000000..18b51ad Binary files /dev/null and b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC01.ino.generic.bin differ diff --git a/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC02.ino.generic.bin b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC02.ino.generic.bin new file mode 100644 index 0000000..0f8e05a Binary files /dev/null and b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC02.ino.generic.bin differ diff --git a/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC05.ino.generic.bin b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC05.ino.generic.bin new file mode 100644 index 0000000..d49ede1 Binary files /dev/null and b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC05.ino.generic.bin differ diff --git a/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC08.ino.generic.bin b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC08.ino.generic.bin new file mode 100644 index 0000000..ad644e1 Binary files /dev/null and b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC08.ino.generic.bin differ diff --git a/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC10.ino.generic.bin b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC10.ino.generic.bin new file mode 100644 index 0000000..905cb41 Binary files /dev/null and b/Drivers/smartlife-rgbw-controller.src/AriLux_AL-LC10.ino.generic.bin differ diff --git a/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBW.ino b/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBW.ino new file mode 100644 index 0000000..8fa2e14 --- /dev/null +++ b/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBW.ino @@ -0,0 +1,2477 @@ +#include +#include //Local DNS Server used for redirecting all requests to the configuration portal +#include //Local WebServer used to serve the configuration portal +#include +#include +#include +ESP8266HTTPUpdateServer httpUpdater; + +//#define RF_REMOTE +//#define IR_REMOTE + +/* + * r~ r fade + * s~ r flash + * g~ g fade + * h~ g flash + * b~ b fade + * c~ n fade + * d~ rgb fade + * f~ rgb flash + * w~ w1 fade + * x~ w1 flash + * y~ w2 fade + * z~ w2 flash + */ + + +#define H801 +//#define LYT8266 +//#define ARILUX_ALLC01 +//#define ARILUX_ALLC02 +//#define ARILUX_ALLC05 +//#define ARILUX_ALLC08 +//#define ARILUX_ALLC10 + +#ifdef H801 +const char * projectName = "SmartLife RGBW Controller"; +String softwareVersion = "2.0.9"; +#endif +#ifdef LYT8266 +const char * projectName = "SmartLife RGBW Bulb"; +String softwareVersion = "2.0.9"; +#endif +#ifdef ARILUX_ALLC01 +const char * projectName = "AriLux AL-LC01"; +String softwareVersion = "2.0.9"; +#endif +#ifdef ARILUX_ALLC02 +const char * projectName = "AriLux AL-LC02"; +String softwareVersion = "2.0.9"; +#endif +#ifdef ARILUX_ALLC05 +const char * projectName = "AriLux AL-LC05"; +String softwareVersion = "2.0.9"; +#endif +#ifdef ARILUX_ALLC08 +const char * projectName = "AriLux AL-LC08"; +String softwareVersion = "2.0.9"; +#endif +#ifdef ARILUX_ALLC10 +const char * projectName = "AriLux AL-LC10"; +String softwareVersion = "2.0.9"; +#endif + +const char compile_date[] = __DATE__ " " __TIME__; + +int currentRED = 0; +int currentGREEN = 0; +int currentBLUE = 0; +int currentW1 = 0; +int currentW2 = 0; +int lastRED = 0; +int lastGREEN = 0; +int lastBLUE = 0; +int lastW1 = 1023; +int lastW2 = 0; + +boolean needFirmware = true; + +boolean needUpdate = true; +boolean inAutoOff = false; + +unsigned long connectionFailures; + +#define FLASH_EEPROM_SIZE 4096 +extern "C" { +#include "spi_flash.h" +} +extern "C" uint32_t _SPIFFS_start; +extern "C" uint32_t _SPIFFS_end; +extern "C" uint32_t _SPIFFS_page; +extern "C" uint32_t _SPIFFS_block; + +#if defined H801 + #define redPIN 15 + #define greenPIN 13 + #define bluePIN 12 + #define w1PIN 14 + #define w2PIN 4 +#endif +#if defined LYT8266 + #define redPIN 13 + #define greenPIN 12 + #define bluePIN 14 + #define w1PIN 2 + #define w2PIN 4 + #define POWER_ENABLE_LED 15 +#endif +#if defined ARILUX_ALLC02 || defined ARILUX_ALLC01 + #define redPIN 14 + #define greenPIN 5 + #define bluePIN 12 + #define w1PIN 13 + #define w2PIN 15 +#endif +#if defined ARILUX_ALLC05 + #define redPIN 13 + #define greenPIN 12 + #define bluePIN 14 + #define w1PIN 5 + #define w2PIN 15 +#endif +#if defined ARILUX_ALLC08 + #define redPIN 5 + #define greenPIN 4 + #define bluePIN 14 + #define w1PIN 12 + #define w2PIN 13 +#endif +#if defined ARILUX_ALLC10 + #define redPIN 5 + #define greenPIN 14 + #define bluePIN 12 + #define w1PIN 13 + #define w2PIN 15 +#endif + +// onbaord green LED D1 +#define LEDPIN 5 +// onbaord red LED D2 +#define LED2PIN 1 + +#define KEY_PIN 0 + +// note +// TX GPIO2 @Serial1 (Serial ONE) +// RX GPIO3 @Serial + +String program = ""; +String program_off = ""; +boolean run_program; +int program_step = 1; +int program_counter = 1; +int program_wait = 0; +int program_loop = false; +String program_number = "0"; +unsigned long previousMillis = millis(); +unsigned long failureTimeout = millis(); +unsigned long timerwd; +unsigned long autoOffTimer; +unsigned long currentmillis=0; +unsigned long timerUptime; +long debounceDelay = 20; +boolean fade = true; +int repeat = 1; +int repeat_count = 1; + +//stores if the switch was high before at all +int state = LOW; +//stores the time each button went high or low +unsigned long current_high; +unsigned long current_low; + +String s_Current_WIFISSID = ""; +String s_Current_WIFIPW = ""; + +#define DEFAULT_HAIP "0.0.0.0" +#define DEFAULT_HAPORT 39500 +#define DEFAULT_RESETWIFI false +#define DEFAULT_POS 0 +#define DEFAULT_CURRENT STATE "" +#define DEFAULT_IP "0.0.0.0" +#define DEFAULT_GATEWAY "0.0.0.0" +#define DEFAULT_SUBNET "0.0.0.0" +#define DEFAULT_DNS "0.0.0.0" +#define DEFAULT_USE_STATIC false +#define DEFAULT_LONG_PRESS false +#define DEFAULT_REALLY_LONG_PRESS false +#define DEFAULT_USE_PASSWORD false +#define DEFAULT_USE_PASSWORD_CONTROL false +#define DEFAULT_COLOR "" +#define DEFAULT_BAD_BOOT_COUNT 0 +#define DEFAULT_RED 0 +#define DEFAULT_GREEN 0 +#define DEFAULT_BLUE 0 +#define DEFAULT_W1 1023 +#define DEFAULT_W2 0 +#define DEFAULT_DISABLE_J3_RESET false +#define DEFAULT_TRANSITION_SPEED 1 +#define DEFAULT_AUTO_OFF 0 +#define DEFAULT_SWITCH_TYPE 0 +#define DEFAULT_CONTINUE_BOOT false +#define DEFAULT_AUTO_OFF 0 +#define DEFAULT_RESET_TYPE 1 +#define DEFAULT_UREPORT 60 +#define DEFAULT_DEBOUNCE 20 +#define DEFAULT_SWAP_RG false + +#define DEFAULT_PASSWORD "" + + +struct SettingsStruct +{ + byte haIP[4]; + unsigned int haPort; + boolean resetWifi; + int powerOnState; + boolean currentState; + byte IP[4]; + byte Gateway[4]; + byte Subnet[4]; + byte DNS[4]; + boolean useStatic; + boolean longPress; + boolean reallyLongPress; + boolean usePassword; + boolean usePasswordControl; + char defaultColor[10]; + int defaultTransition; + int badBootCount; + boolean disableJ3Reset; + int switchType; + int autoOff; + int transitionSpeed; + boolean continueBoot; + int resetType; + int uReport; + int debounce; + int swapRG; +} Settings; + +struct SecurityStruct +{ + char Password[26]; + int settingsVersion; + char ssid[33]; + char pass[33]; +} SecuritySettings; + +#if defined H801 +#define LEDoff digitalWrite(LEDPIN,HIGH) +#define LEDon digitalWrite(LEDPIN,LOW) + +#define LED2off digitalWrite(LED2PIN,HIGH) +#define LED2on digitalWrite(LED2PIN,LOW) +#else +#define LEDoff +#define LEDon + +#define LED2off +#define LED2on +#endif + +int led_delay_red = 0; +int led_delay_green = 0; +int led_delay_blue = 0; +int led_delay_w1 = 0; +int led_delay_w2 = 0; +#define time_at_colour 1300 +unsigned long TIME_LED_RED = 0; +unsigned long TIME_LED_GREEN = 0; +unsigned long TIME_LED_BLUE = 0; +unsigned long TIME_LED_W1 = 0; +unsigned long TIME_LED_W2 = 0; +int RED, GREEN, BLUE, W1, W2; +int RED_A = 0; +int GREEN_A = 0; +int BLUE_A = 0; +int W1_A = 0; +int W2_A = 0; + +byte mac[6]; + +// Start WiFi Server +std::unique_ptr server; + +void handleRoot() { + server->send(200, "application/json", "{\"message\":\"SmartLife RGBW Controller\"}"); +} + +void handleNotFound() { + String message = "File Not Found\n\n"; + message += "URI: "; + message += server->uri(); + message += "\nMethod: "; + message += (server->method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server->args(); + message += "\n"; + for (uint8_t i = 0; i < server->args(); i++) { + message += " " + server->argName(i) + ": " + server->arg(i) + "\n"; + } + server->send(404, "text/plain", message); +} + +boolean changeColor(String color, int channel, boolean fade, boolean all = false) +{ + boolean success = false; + + if (channel != 0 && !inAutoOff){ + inAutoOff = true; + autoOffTimer = millis(); + } + + switch (channel) + { + case 0: // Off + { + + RED = 0; + GREEN = 0; + BLUE = 0; + W1 = 0; + W2 = 0; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + case 1: //R channel + { + RED = getScaledValue(color.substring(0, 2)); + + if(all == false){ + W1 = 0; + W2 = 0; + } + + lastRED = RED; + lastGREEN = GREEN; + lastBLUE = BLUE; + lastW1 = W1; + lastW2 = W2; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + case 2: //G channel + { + GREEN = getScaledValue(color.substring(0, 2)); + + if(all == false){ + W1 = 0; + W2 = 0; + } + + lastRED = RED; + lastGREEN = GREEN; + lastBLUE = BLUE; + lastW1 = W1; + lastW2 = W2; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + case 3: //B channel + { + BLUE = getScaledValue(color.substring(0, 2)); + + if(all == false){ + W1 = 0; + W2 = 0; + } + + lastRED = RED; + lastGREEN = GREEN; + lastBLUE = BLUE; + lastW1 = W1; + lastW2 = W2; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + case 4: //White1 channel + { + if(all == false){ + RED = 0; + GREEN = 0; + BLUE = 0; + W2 = 0; + } + + W1 = getScaledValue(color.substring(0, 2)); + + lastRED = RED; + lastGREEN = GREEN; + lastBLUE = BLUE; + lastW1 = W1; + lastW2 = W2; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + case 5: //White2 channel + { + if(all == false){ + RED = 0; + GREEN = 0; + BLUE = 0; + W2 = 0; + } + + W2 = getScaledValue(color.substring(0, 2)); + + lastRED = RED; + lastGREEN = GREEN; + lastBLUE = BLUE; + lastW1 = W1; + lastW2 = W2; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + case 6: //RGB channel + { + if (color == "xxxxxx") { + RED = rand_interval(0,1023); + GREEN = rand_interval(0,1023); + BLUE = rand_interval(0,1023); + } else { + RED = getScaledValue(color.substring(0, 2)); + GREEN = getScaledValue(color.substring(2, 4)); + BLUE = getScaledValue(color.substring(4, 6)); + } + if(all == false){ + W1 = 0; + W2 = 0; + } + + lastRED = RED; + lastGREEN = GREEN; + lastBLUE = BLUE; + lastW1 = W1; + lastW2 = W2; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + case 99: // On + { + + RED = lastRED; + GREEN = lastGREEN; + BLUE = lastBLUE; + W1 = lastW1; + W2 = lastW2; + + ::fade = fade; + + change_LED(); + success = true; + break; + } + + } + return success; + +} + +unsigned int rand_interval(unsigned int min, unsigned int max) +{ + int r; + const unsigned int range = 1 + max - min; + const unsigned int buckets = RAND_MAX / range; + const unsigned int limit = buckets * range; + + /* Create equal size buckets all in a row, then fire randomly towards + * the buckets until you land in one of them. All buckets are equally + * likely. If you land off the end of the line of buckets, try again. */ + do + { + r = rand(); + } while (r >= limit); + + return min + (r / buckets); +} + +void relayToggle(){ + if(digitalRead(KEY_PIN) == LOW){ + current_low = millis(); + state = LOW; + } + if(digitalRead(KEY_PIN == HIGH) && state == LOW) + { + current_high = millis(); + if((current_high - current_low) > (Settings.debounce? Settings.debounce : debounceDelay) && (current_high - current_low) < 10000) + { + state = HIGH; + run_program = false; + if(getHex(RED) + getHex(GREEN) + getHex(BLUE) + getHex(W1) + getHex(W2) == "0000000000") { + String hexString(Settings.defaultColor); + if(hexString == "") { + changeColor("0000000000", 99, true); + } else if(hexString == "Previous") { + changeColor("0000000000", 99, true); + } else { + if(hexString.startsWith("w~")) { + String hex = hexString.substring(2, 4); + changeColor(hex, 4, true); + } else if(hexString.startsWith("x~")){ + String hex = hexString.substring(2, 4); + changeColor(hex, 4, false); + } else if(hexString.startsWith("y~")) { + String hex = hexString.substring(2, 4); + changeColor(hex, 5, true); + } else if(hexString.startsWith("z~")){ + String hex = hexString.substring(2, 4); + changeColor(hex, 5, false); + } else if(hexString.startsWith("f~")){ + String hex = hexString.substring(2, 8); + changeColor(hex, 6, true); + }else{ + String hex = hexString.substring(2, 8); + changeColor("0000000000", 99, true); + } + } + } else { + changeColor("0000000000", 0, true); + } + needUpdate = true; + } + else if((current_high - current_low) >= 10000 && (current_high - current_low) < 20000) + { + if (Settings.resetType == 1 || Settings.resetType == 3){ + Settings.longPress = true; + SaveSettings(); + ESP.restart(); + } + } + else if((current_high - current_low) >= 20000 && (current_high - current_low) < 60000) + { + if (Settings.resetType == 1 || Settings.resetType == 3){ + Settings.reallyLongPress = true; + SaveSettings(); + ESP.restart(); + } + } + } +} + +const char * endString(int s, const char *input) { + int length = strlen(input); + if ( s > length ) s = length; + return const_cast(&input[length-s]); +} + +void change_LED() +{ + int diff_red = abs(RED-RED_A); + if(diff_red > 0){ + led_delay_red = time_at_colour / abs(RED-RED_A); + }else{ + led_delay_red = time_at_colour / 1023; + } + + int diff_green = abs(GREEN-GREEN_A); + if(diff_green > 0){ + led_delay_green = time_at_colour / abs(GREEN-GREEN_A); + }else{ + led_delay_green = time_at_colour / 1023; + } + + int diff_blue = abs(BLUE-BLUE_A); + if(diff_blue > 0){ + led_delay_blue = time_at_colour / abs(BLUE-BLUE_A); + }else{ + led_delay_blue = time_at_colour / 1023; + } + + int diff_w1 = abs(W1-W1_A); + if(diff_w1 > 0){ + led_delay_w1 = time_at_colour / abs(W1-W1_A); + }else{ + led_delay_w1 = time_at_colour / 1023; + } + + int diff_w2 = abs(W2-W2_A); + if(diff_w2 > 0){ + led_delay_w2 = time_at_colour / abs(W2-W2_A); + }else{ + led_delay_w2 = time_at_colour / 1023; + } +} + +void LED_RED() +{ + if (fade){ + if((RED_A > RED && (RED_A - Settings.transitionSpeed > RED)) || (RED_A < RED && (RED_A + Settings.transitionSpeed < RED))){ + if(RED_A > RED) RED_A = RED_A - Settings.transitionSpeed; + if(RED_A < RED) RED_A = RED_A + Settings.transitionSpeed; + analogWrite(!Settings.swapRG? redPIN:greenPIN, RED_A); + currentRED=RED_A; + } else { + analogWrite(!Settings.swapRG? redPIN:greenPIN, RED); + } + } else { + RED_A = RED; + analogWrite(!Settings.swapRG? redPIN:greenPIN, RED); + currentRED=RED; + } +} + +void LED_GREEN() +{ + if (fade){ + if((GREEN_A > GREEN && (GREEN_A - Settings.transitionSpeed > GREEN)) || (GREEN_A < GREEN && (GREEN_A + Settings.transitionSpeed < GREEN))){ + if(GREEN_A > GREEN) GREEN_A = GREEN_A - Settings.transitionSpeed; + if(GREEN_A < GREEN) GREEN_A = GREEN_A + Settings.transitionSpeed; + analogWrite(!Settings.swapRG? greenPIN:redPIN, GREEN_A); + currentGREEN=GREEN_A; + } else { + analogWrite(!Settings.swapRG? greenPIN:redPIN, GREEN); + } + } else { + GREEN_A = GREEN; + analogWrite(!Settings.swapRG? greenPIN:redPIN, GREEN); + currentGREEN=GREEN; + } +} + +void LED_BLUE() +{ + if (fade){ + if((BLUE_A > BLUE && (BLUE_A - Settings.transitionSpeed > BLUE)) || (BLUE_A < BLUE && (BLUE_A + Settings.transitionSpeed < BLUE))){ + if(BLUE_A > BLUE) BLUE_A = BLUE_A - Settings.transitionSpeed; + if(BLUE_A < BLUE) BLUE_A = BLUE_A + Settings.transitionSpeed; + analogWrite(bluePIN, BLUE_A); + currentBLUE=BLUE_A; + } else { + analogWrite(bluePIN, BLUE); + } + } else { + BLUE_A = BLUE; + analogWrite(bluePIN, BLUE); + currentBLUE=BLUE; + } +} + +void LED_W1() +{ + if (fade){ + if((W1_A > W1 && (W1_A - Settings.transitionSpeed > W1)) || (W1_A < W1 && (W1_A + Settings.transitionSpeed < W1))){ + if(W1_A > W1) W1_A = W1_A - Settings.transitionSpeed; + if(W1_A < W1) W1_A = W1_A + Settings.transitionSpeed; + analogWrite(w1PIN, W1_A); + currentW1=W1_A; + } else { + analogWrite(w1PIN, W1); + } + } else { + W1_A = W1; + analogWrite(w1PIN, W1); + currentW1=W1; + } +} + +void LED_W2() +{ + if (fade){ + if((W2_A > W2 && (W2_A - Settings.transitionSpeed > W2)) || (W2_A < W2 && (W2_A + Settings.transitionSpeed < W2))){ + if(W2_A > W2) W2_A = W2_A - Settings.transitionSpeed; + if(W2_A < W2) W2_A = W2_A + Settings.transitionSpeed; + analogWrite(w2PIN, W2_A); + currentW2=W2_A; + } else { + analogWrite(w2PIN, W2); + } + } else { + W2_A = W2; + analogWrite(w2PIN, W2); + currentW2=W2; + } +} + +int convertToInt(char upper,char lower) +{ + int uVal = (int)upper; + int lVal = (int)lower; + uVal = uVal >64 ? uVal - 55 : uVal - 48; + uVal = uVal << 4; + lVal = lVal >64 ? lVal - 55 : lVal - 48; + return uVal + lVal; +} + +String getStatus(){ + if(getHex(RED) + getHex(GREEN) + getHex(BLUE) + getHex(W1) + getHex(W2) == "0000000000") { + return "{\"rgb\":\"" + getHex(RED) + getHex(GREEN) + getHex(BLUE) + "\", \"r\":\"" + getHex(RED) + "\", \"g\":\"" + getHex(GREEN) + "\", \"b\":\"" + getHex(BLUE) + "\", \"w1\":\"" + getHex(W1) + "\", \"w2\":\"" + getHex(W2) + "\", \"power\":\"off\", \"running\":\"false\", \"program\":\"" + program_number + "\"}"; + } else if(run_program){ + return "{\"running\":\"true\", \"program\":\"" + program_number + "\", \"power\":\"on\", \"uptime\":\"" + uptime() + "\"}"; + }else{ + return "{\"rgb\":\"" + getHex(RED) + getHex(GREEN) + getHex(BLUE) + "\", \"r\":\"" + getHex(RED) + "\", \"g\":\"" + getHex(GREEN) + "\", \"b\":\"" + getHex(BLUE) + "\", \"w1\":\"" + getHex(W1) + "\", \"w2\":\"" + getHex(W2) + "\", \"power\":\"on\", \"running\":\"false\", \"program\":\"" + program_number + "\"}"; + } +} + +int getScaledValue(String hex){ + hex.toUpperCase(); + char c[2]; + hex.toCharArray(c,3); + long value = convertToInt(c[0],c[1]); + int intValue = map(value,0,255,0,1023); + + return intValue; + +} + +int getInt(String hex){ + hex.toUpperCase(); + char c[2]; + hex.toCharArray(c,3); + return convertToInt(c[0],c[1]); +} + +String getHex(int value){ + if(value > 1018){ + return "ff"; + } else if(value < 4){ + return "00"; + }else{ + int intValue = map(value,0,1023,0,255) + 1; + return padHex(String(intValue, HEX)); + } +} + +String getStandard(int value){ + if(value >= 1020){ + return "255"; + }else{ + return String(round(value*4/16)); + } +} + +String padHex(String hex){ + if(hex.length() == 1){ + hex = "0" + hex; + } + return hex; +} + +/*********************************************************************************************\ + * Tasks each 5 minutes +\*********************************************************************************************/ +void runEach5Minutes() +{ + //timerwd = millis() + 1800000; + timerwd = millis() + 300000; + + sendStatus(); + +} + +boolean sendStatus(int number) { + String authHeader = ""; + boolean success = false; + String message = ""; + char host[20]; + sprintf_P(host, PSTR("%u.%u.%u.%u"), Settings.haIP[0], Settings.haIP[1], Settings.haIP[2], Settings.haIP[3]); + + //client.setTimeout(1000); + if (Settings.haIP[0] + Settings.haIP[1] + Settings.haIP[2] + Settings.haIP[3] == 0) { // HA host is not configured + return false; + } + if (connectionFailures >= 3) { // Too many errors; Trying not to get stuck + if (millis() - failureTimeout < 1800000) { + return false; + } else { + failureTimeout = millis(); + } + } + // Use WiFiClient class to create TCP connections + WiFiClient client; + if (!client.connect(host, Settings.haPort)) + { + connectionFailures++; + return false; + } + if (connectionFailures) + connectionFailures = 0; + + switch(number){ + case 0: { + message = getStatus(); + break; + } + case 98: { + message = "{\"version\":\"" + softwareVersion + "\", \"date\":\"" + compile_date + "\"}"; + break; + } + case 99: { + message = "{\"uptime\":\"" + uptime() + "\"}"; + break; + } + } + + // We now create a URI for the request + String url = F("/"); + //url += event->idx; + + client.print(String("POST ") + url + " HTTP/1.1\r\n" + + "Host: " + host + ":" + Settings.haPort + "\r\n" + authHeader + + "Content-Type: application/json;charset=utf-8\r\n" + + "Server: " + projectName + "\r\n" + + "Connection: close\r\n\r\n" + + message + "\r\n"); + + unsigned long timer = millis() + 200; + while (!client.available() && millis() < timer) + delay(1); + + // Read all the lines of the reply from server and print them to Serial + while (client.available()) { + String line = client.readStringUntil('\n'); + if (line.substring(0, 15) == "HTTP/1.1 200 OK") + { + success = true; + } + delay(1); + } + + client.flush(); + client.stop(); + + return success; +} + +boolean sendStatus(){ + String authHeader = ""; + boolean success = false; + char host[20]; + sprintf_P(host, PSTR("%u.%u.%u.%u"), Settings.haIP[0], Settings.haIP[1], Settings.haIP[2], Settings.haIP[3]); + + //client.setTimeout(1000); + if (Settings.haIP[0] + Settings.haIP[1] + Settings.haIP[2] + Settings.haIP[3] == 0) { // HA host is not configured + return false; + } + if (connectionFailures >= 3) { // Too many errors; Trying not to get stuck + if (millis() - failureTimeout < 1800000) { + return false; + } else { + failureTimeout = millis(); + } + } + // Use WiFiClient class to create TCP connections + WiFiClient client; + if (!client.connect(host, Settings.haPort)) + { + connectionFailures++; + return false; + } + if (connectionFailures) + connectionFailures = 0; + + // We now create a URI for the request + String url = F("/"); + //url += event->idx; + + client.print(String("POST ") + url + " HTTP/1.1\r\n" + + "Host: " + host + ":" + Settings.haPort + "\r\n" + authHeader + + "Content-Type: application/json;charset=utf-8\r\n" + + "Server: " + projectName + "\r\n" + + "Connection: close\r\n\r\n" + + getStatus() + "\r\n"); + + unsigned long timer = millis() + 200; + while (!client.available() && millis() < timer) + delay(1); + + // Read all the lines of the reply from server and print them to Serial + while (client.available()) { + String line = client.readStringUntil('\n'); + if (line.substring(0, 15) == "HTTP/1.1 200 OK") + { + success = true; + } + delay(1); + } + + client.flush(); + client.stop(); + + return success; + +} + +/********************************************************************************************\ + Convert a char string to IP byte array + \*********************************************************************************************/ +boolean str2ip(char *string, byte* IP) +{ + byte c; + byte part = 0; + int value = 0; + + for (int x = 0; x <= strlen(string); x++) + { + c = string[x]; + if (isdigit(c)) + { + value *= 10; + value += c - '0'; + } + + else if (c == '.' || c == 0) // next octet from IP address + { + if (value <= 255) + IP[part++] = value; + else + return false; + value = 0; + } + else if (c == ' ') // ignore these + ; + else // invalid token + return false; + } + if (part == 4) // correct number of octets + return true; + return false; +} + +String deblank(const char* input) +{ + String output = String(input); + output.replace(" ", ""); + return output; +} + +void SaveSettings(void) +{ + SaveToFlash(0, (byte*)&Settings, sizeof(struct SettingsStruct)); + SaveToFlash(32768, (byte*)&SecuritySettings, sizeof(struct SecurityStruct)); +} + +boolean LoadSettings() +{ + LoadFromFlash(0, (byte*)&Settings, sizeof(struct SettingsStruct)); + LoadFromFlash(32768, (byte*)&SecuritySettings, sizeof(struct SecurityStruct)); +} + +/********************************************************************************************\ + Save data to flash + \*********************************************************************************************/ +void SaveToFlash(int index, byte* memAddress, int datasize) +{ + if (index > 33791) // Limit usable flash area to 32+1k size + { + return; + } + uint32_t _sector = ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint8_t* data = new uint8_t[FLASH_EEPROM_SIZE]; + int sectorOffset = index / SPI_FLASH_SEC_SIZE; + int sectorIndex = index % SPI_FLASH_SEC_SIZE; + uint8_t* dataIndex = data + sectorIndex; + _sector += sectorOffset; + + // load entire sector from flash into memory + noInterrupts(); + spi_flash_read(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE); + interrupts(); + + // store struct into this block + memcpy(dataIndex, memAddress, datasize); + + noInterrupts(); + // write sector back to flash + if (spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) + if (spi_flash_write(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE) == SPI_FLASH_RESULT_OK) + { + //Serial.println("flash save ok"); + } + interrupts(); + delete [] data; + //String log = F("FLASH: Settings saved"); + //addLog(LOG_LEVEL_INFO, log); +} + + +/********************************************************************************************\ + Load data from flash + \*********************************************************************************************/ +void LoadFromFlash(int index, byte* memAddress, int datasize) +{ + uint32_t _sector = ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint8_t* data = new uint8_t[FLASH_EEPROM_SIZE]; + int sectorOffset = index / SPI_FLASH_SEC_SIZE; + int sectorIndex = index % SPI_FLASH_SEC_SIZE; + uint8_t* dataIndex = data + sectorIndex; + _sector += sectorOffset; + + // load entire sector from flash into memory + noInterrupts(); + spi_flash_read(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE); + interrupts(); + + // load struct from this block + memcpy(memAddress, dataIndex, datasize); + delete [] data; +} + + + +String uptime() +{ + currentmillis = millis(); + long days=0; + long hours=0; + long mins=0; + long secs=0; + secs = currentmillis/1000; //convect milliseconds to seconds + mins=secs/60; //convert seconds to minutes + hours=mins/60; //convert minutes to hours + days=hours/24; //convert hours to days + secs=secs-(mins*60); //subtract the coverted seconds to minutes in order to display 59 secs max + mins=mins-(hours*60); //subtract the coverted minutes to hours in order to display 59 minutes max + hours=hours-(days*24); //subtract the coverted hours to days in order to display 23 hours max + + + if (days>0) // days will displayed only if value is greater than zero + { + return String(days) + " days and " + String(hours) + ":" + String(mins) + ":" + String(secs); + }else{ + return String(hours) + ":" + String(mins) + ":" + String(secs); + } +} + + + +void EraseFlash() +{ + uint32_t _sectorStart = (ESP.getSketchSize() / SPI_FLASH_SEC_SIZE) + 1; + uint32_t _sectorEnd = _sectorStart + (ESP.getFlashChipRealSize() / SPI_FLASH_SEC_SIZE); + + for (uint32_t _sector = _sectorStart; _sector < _sectorEnd; _sector++) + { + noInterrupts(); + if (spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) + { + interrupts(); + Serial.print(F("FLASH: Erase Sector: ")); + Serial.println(_sector); + delay(10); + } + interrupts(); + } +} + +void ZeroFillFlash() +{ + // this will fill the SPIFFS area with a 64k block of all zeroes. + uint32_t _sectorStart = ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint32_t _sectorEnd = _sectorStart + 16 ; //((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint8_t* data = new uint8_t[FLASH_EEPROM_SIZE]; + + uint8_t* tmpdata = data; + for (int x = 0; x < FLASH_EEPROM_SIZE; x++) + { + *tmpdata = 0; + tmpdata++; + } + + + for (uint32_t _sector = _sectorStart; _sector < _sectorEnd; _sector++) + { + // write sector to flash + noInterrupts(); + if (spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) + if (spi_flash_write(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE) == SPI_FLASH_RESULT_OK) + { + interrupts(); + Serial.print(F("FLASH: Zero Fill Sector: ")); + Serial.println(_sector); + delay(10); + } + } + interrupts(); + delete [] data; +} + +void addHeader(boolean showMenu, String& str) +{ + boolean cssfile = false; + + str += F(""); + str += F(""); + str += projectName; + str += F(""); + + str += F(""); + + + str += F(""); + str += F("
"); + +} + +void addFooter(String& str) +{ + str += F("
smartlife.tech
"); +} + +void addMenu(String& str) +{ + str += F("Main"); + str += F("Advanced"); + str += F("Control"); + str += F("Firmware"); +} + +void setup() +{ + + // Setup console + Serial1.begin(115200); + delay(10); + Serial1.println(); + Serial1.println(); + + LoadSettings(); + + pinMode(LEDPIN, OUTPUT); + pinMode(LED2PIN, OUTPUT); + + pinMode(redPIN, OUTPUT); + pinMode(greenPIN, OUTPUT); + pinMode(bluePIN, OUTPUT); + pinMode(w1PIN, OUTPUT); + pinMode(w2PIN, OUTPUT); + pinMode(KEY_PIN, INPUT_PULLUP); + attachInterrupt(KEY_PIN, relayToggle, CHANGE); + + #ifdef LYT8266 + pinMode(POWER_ENABLE_LED, OUTPUT); // Power Led Enable + digitalWrite(POWER_ENABLE_LED, 1); + #endif + + if (Settings.badBootCount == 0) { + switch (Settings.powerOnState) + { + case 0: //Switch Off on Boot + { + break; + } + case 1: //Switch On on Boot + { + String hexString(Settings.defaultColor); + if(hexString == "") { + changeColor("0000000000", 99, false); + } else if(hexString == "Previous") { + changeColor("0000000000", 99, false); + } else { + if(hexString.startsWith("w~")) { + String hex = hexString.substring(2, 4); + changeColor(hex, 4, false); + } else if(hexString.startsWith("x~")){ + String hex = hexString.substring(2, 4); + changeColor(hex, 4, false); + } else if(hexString.startsWith("y~")) { + String hex = hexString.substring(2, 4); + changeColor(hex, 5, true); + } else if(hexString.startsWith("z~")){ + String hex = hexString.substring(2, 4); + changeColor(hex, 5, false); + } else if(hexString.startsWith("f~")){ + String hex = hexString.substring(2, 8); + changeColor(hex, 6, false); + }else{ + String hex = hexString.substring(2, 8); + changeColor("0000000000", 99, false); + } + } + LED_RED(); + LED_GREEN(); + LED_BLUE(); + LED_W1(); + LED_W2(); + break; + } + case 2: //Saved State on Boot + { + if(Settings.currentState == true){ + + } + else { + + } + break; + } + default : //Optional + { + + } + } + } + + if (Settings.badBootCount == 1){ changeColor("ff", 2, false); LED_GREEN(); } + if (Settings.badBootCount == 2){ changeColor("ff", 3, false); LED_BLUE(); } + if (Settings.badBootCount >= 3){ changeColor("ff", 1, false); LED_RED(); } + + if (Settings.resetType < 3) { + Settings.badBootCount += 1; + SaveSettings(); + } + + delay(5000); + + if (Settings.badBootCount > 3) { + Settings.reallyLongPress = true; + } + + if(Settings.longPress == true){ + for (uint8_t i = 0; i < 3; i++) { + LEDoff; + delay(250); + LEDon; + delay(250); + } + Settings.longPress = false; + Settings.useStatic = false; + Settings.resetWifi = true; + SaveSettings(); + LEDoff; + } + + if(Settings.reallyLongPress == true){ + for (uint8_t i = 0; i < 5; i++) { + LEDoff; + delay(1000); + LEDon; + delay(1000); + } + EraseFlash(); + ZeroFillFlash(); + ESP.restart(); + } + + //analogWrite(greenPIN, 0); + //analogWrite(bluePIN, 0); + //analogWrite(redPIN, 0); + + boolean saveSettings = false; + + if (Settings.badBootCount != 0){ + Settings.badBootCount = 0; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 201){ + str2ip((char*)DEFAULT_HAIP, Settings.haIP); + Settings.haPort = DEFAULT_HAPORT; + Settings.resetWifi = DEFAULT_RESETWIFI; + SecuritySettings.settingsVersion = 201; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 202){ + Settings.powerOnState = DEFAULT_POS; + str2ip((char*)DEFAULT_IP, Settings.IP); + str2ip((char*)DEFAULT_SUBNET, Settings.Subnet); + str2ip((char*)DEFAULT_GATEWAY, Settings.Gateway); + Settings.useStatic = DEFAULT_USE_STATIC; + Settings.usePassword = DEFAULT_USE_PASSWORD; + Settings.usePasswordControl = DEFAULT_USE_PASSWORD_CONTROL; + Settings.longPress = DEFAULT_LONG_PRESS; + Settings.reallyLongPress = DEFAULT_REALLY_LONG_PRESS; + strncpy(SecuritySettings.Password, DEFAULT_PASSWORD, sizeof(SecuritySettings.Password)); + SecuritySettings.settingsVersion = 202; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 203){ + strncpy(Settings.defaultColor, DEFAULT_COLOR, sizeof(Settings.defaultColor)); + SecuritySettings.settingsVersion = 203; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 204){ + Settings.badBootCount = DEFAULT_BAD_BOOT_COUNT; + SecuritySettings.settingsVersion = 204; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 205){ + Settings.disableJ3Reset = DEFAULT_DISABLE_J3_RESET; + SecuritySettings.settingsVersion = 205; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 206){ + Settings.switchType = DEFAULT_SWITCH_TYPE; + Settings.autoOff = DEFAULT_AUTO_OFF; + Settings.transitionSpeed = DEFAULT_TRANSITION_SPEED; + SecuritySettings.settingsVersion = 206; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 207){ + Settings.autoOff = DEFAULT_AUTO_OFF; + Settings.continueBoot = DEFAULT_CONTINUE_BOOT; + SecuritySettings.settingsVersion = 207; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 208){ + Settings.resetType = DEFAULT_RESET_TYPE; + Settings.uReport = DEFAULT_UREPORT; + Settings.debounce = DEFAULT_DEBOUNCE; + SecuritySettings.settingsVersion = 208; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 209){ + Settings.swapRG = DEFAULT_SWAP_RG; + SecuritySettings.settingsVersion = 209; + saveSettings = true; + } + + if (saveSettings == true){ + SaveSettings(); + } + + WiFiManager wifiManager; + + wifiManager.setConnectTimeout(30); + wifiManager.setConfigPortalTimeout(300); + + if(Settings.useStatic == true){ + wifiManager.setSTAStaticIPConfig(Settings.IP, Settings.Gateway, Settings.Subnet); + } + + if (Settings.resetWifi == true){ + wifiManager.resetSettings(); + Settings.resetWifi = false; + SaveSettings(); + } + + if (Settings.continueBoot == true){ + wifiManager.setContinueAfterTimeout(true); + } + + LEDon; + LED2off; + + WiFi.macAddress(mac); + String apSSID = "espRGBW." + String(mac[0],HEX) + String(mac[1],HEX) + String(mac[2],HEX) + String(mac[3],HEX) + String(mac[4],HEX) + String(mac[5],HEX); + + if (!wifiManager.autoConnect(apSSID.c_str(), "configme")) { + Serial.println("failed to connect, we should reset as see if it connects"); + LED2on; + delay(500); + LED2off; + delay(3000); + LED2on; + delay(500); + LED2off; + ESP.restart(); + } + + LED2on; + + Serial1.println(""); + server.reset(new ESP8266WebServer(WiFi.localIP(), 80)); + + //server->on("/", handleRoot); + + server->on("/reset", []() { + server->send(200, "application/json", "{\"message\":\"wifi settings are being removed\"}"); + Settings.reallyLongPress = true; + SaveSettings(); + ESP.restart(); + }); + + + server->on("/description.xml", HTTP_GET, [](){ + SSDP.schema(server->client()); + }); + + server->on("/reboot", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + server->send(200, "application/json", "{\"message\":\"device is rebooting\"}"); + ESP.restart(); + }); + + server->on("/", []() { + + char tmpString[64]; + + String reply = ""; + char str[20]; + addHeader(true, reply); + reply += F(""); + reply += F("
"); + reply += projectName; + reply += F(" Main"); + reply += F("
"); + addMenu(reply); + + reply += F("
Main:"); + + reply += F("Advanced Config
"); + reply += F("Control
"); + reply += F("Firmware Update
"); + reply += F("Documentation
"); + reply += F("Reboot
"); + + reply += F("
JSON Endpoints:"); + + reply += F("status
"); + reply += F("configGet
"); + reply += F("configSet
"); + reply += F("rgb
"); + reply += F("r
"); + reply += F("g
"); + reply += F("b
"); + reply += F("w1
"); + reply += F("w2
"); + reply += F("off
"); + reply += F("program
"); + reply += F("stop
"); + reply += F("info
"); + reply += F("reboot
"); + + reply += F("
"); + addFooter(reply); + server->send(200, "text/html", reply); + }); + + server->on("/info", []() { + server->send(200, "application/json", "{\"version\":\"" + softwareVersion + "\", \"date\":\"" + compile_date + "\", \"mac\":\"" + padHex(String(mac[0],HEX)) + padHex(String(mac[1],HEX)) + padHex(String(mac[2],HEX)) + padHex(String(mac[3],HEX)) + padHex(String(mac[4],HEX)) + padHex(String(mac[5],HEX)) + "\"}"); + }); + + server->on("/program", []() { + program = server->arg("value"); + repeat = server->arg("repeat").toInt(); + program_number = server->arg("number"); + program_off = server->arg("off"); + repeat_count = 1; + program_wait = 0; + run_program = true; + server->send(200, "application/json", "{\"running\":\"true\", \"program\":\"" + program_number + "\", \"power\":\"on\"}"); + }); + + server->on("/stop", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + run_program = false; + server->send(200, "application/json", "{\"program\":\"" + program_number + "\", \"running\":\"false\"}"); + }); + + server->on("/off", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + String transition = server->arg("transition"); + run_program = false; + changeColor("00000000", 0, (transition != "false")); + + server->send(200, "application/json", getStatus()); + }); + + server->on("/on", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + String transition = server->arg("transition"); + run_program = false; + + String hexString(Settings.defaultColor); + if(hexString == "") { + changeColor("0000000000", 99, (transition != "false")); + } else if(hexString == "Previous") { + changeColor("0000000000", 99, (transition != "false")); + } else { + if(hexString.startsWith("w~")) { + String hex = hexString.substring(2, 4); + changeColor(hex, 4, (transition != "false")); + } else if(hexString.startsWith("x~")){ + String hex = hexString.substring(2, 4); + changeColor(hex, 4, (transition != "false")); + } else if(hexString.startsWith("y~")) { + String hex = hexString.substring(2, 4); + changeColor(hex, 5, true); + } else if(hexString.startsWith("z~")){ + String hex = hexString.substring(2, 4); + changeColor(hex, 5, false); + } else if(hexString.startsWith("f~")){ + String hex = hexString.substring(2, 8); + changeColor(hex, 6, (transition != "false")); + }else{ + String hex = hexString.substring(2, 8); + changeColor("0000000000", 99, (transition != "false")); + } + } + + server->send(200, "application/json", getStatus()); + }); + + server->on("/configGet", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePassword == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + char tmpString[64]; + boolean success = false; + String configName = server->arg("name"); + String reply = ""; + char str[20]; + + if (configName == "haip") { + sprintf_P(str, PSTR("%u.%u.%u.%u"), Settings.haIP[0], Settings.haIP[1], Settings.haIP[2], Settings.haIP[3]); + reply += "{\"name\":\"haip\", \"value\":\"" + String(str) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "haport") { + reply += "{\"name\":\"haport\", \"value\":\"" + String(Settings.haPort) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "pos") { + reply += "{\"name\":\"pos\", \"value\":\"" + String(Settings.powerOnState) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "autooff") { + reply += "{\"name\":\"autooff\", \"value\":\"" + String(Settings.autoOff) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "transitionspeed") { + reply += "{\"name\":\"transitionspeed\", \"value\":\"" + String(Settings.transitionSpeed) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "dcolor") { + reply += "{\"name\":\"dcolor\", \"value\":\"" + String(Settings.defaultColor) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "ureport") { + reply += "{\"name\":\"ureport\", \"value\":\"" + String(Settings.uReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "swaprg") { + reply += "{\"name\":\"swaprg\", \"value\":\"" + String(Settings.swapRG) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + +#ifdef H801 + if (configName == "switchtype") { + reply += "{\"name\":\"switchtype\", \"value\":\"" + String(Settings.switchType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "resettype") { + reply += "{\"name\":\"resettype\", \"value\":\"" + String(Settings.resetType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "debounce") { + reply += "{\"name\":\"debounce\", \"value\":\"" + String(Settings.debounce) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } +#endif + + if ( reply != "" ) { + server->send(200, "application/json", reply); + } else { + server->send(200, "application/json", "{\"success\":\"false\", \"type\":\"configuration\"}"); + } + }); + + server->on("/configSet", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePassword == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + char tmpString[64]; + boolean success = false; + String configName = server->arg("name"); + String configValue = server->arg("value"); + String reply = ""; + char str[20]; + + if (configName == "haip") { + if (configValue.length() != 0) + { + if (configValue != String(Settings.haIP[0]) + "." + String(Settings.haIP[1]) + "." + String(Settings.haIP[2]) + "." + String(Settings.haIP[3])) { + needFirmware = true; + } + configValue.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.haIP); + } + reply += "{\"name\":\"haip\", \"value\":\"" + String(tmpString) + "\", \"success\":\"true\"}"; + } + if (configName == "haport") { + if (configValue.length() != 0) + { + Settings.haPort = configValue.toInt(); + } + reply += "{\"name\":\"haport\", \"value\":\"" + String(Settings.haPort) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "pos") { + if (configValue.length() != 0) + { + Settings.powerOnState = configValue.toInt(); + } + reply += "{\"name\":\"pos\", \"value\":\"" + String(Settings.powerOnState) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "autooff") { + if (configValue.length() != 0) + { + Settings.autoOff = configValue.toInt(); + } + reply += "{\"name\":\"autooff\", \"value\":\"" + String(Settings.autoOff) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "transitionspeed") { + if (configValue.length() != 0) + { + Settings.transitionSpeed = configValue.toInt(); + } + reply += "{\"name\":\"transitionspeed\", \"value\":\"" + String(Settings.transitionSpeed) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "dcolor") { + if (configValue.length() != 0) + { + strncpy(Settings.defaultColor, configValue.c_str(), sizeof(Settings.defaultColor)); + } + reply += "{\"name\":\"dcolor\", \"value\":\"" + String(Settings.defaultColor) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "uReport") { + if (configValue.length() != 0) + { + if (configValue.toInt() != Settings.uReport) { + Settings.uReport = configValue.toInt(); + timerUptime = millis() + Settings.uReport * 1000; + } + } + reply += "{\"name\":\"ureport\", \"value\":\"" + String(Settings.uReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "swaprg") { + if (configValue.length() != 0) + { + Settings.swapRG = (configValue == "yes"); + } + reply += "{\"name\":\"swaprg\", \"value\":\"" + String(Settings.swapRG) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + +#ifdef H801 + if (configName == "switchtype") { + if (configValue.length() != 0) + { + Settings.switchType = configValue.toInt(); + } + reply += "{\"name\":\"switchtype\", \"value\":\"" + String(Settings.switchType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "resettype") { + if (configValue.length() != 0) + { + Settings.resetType = configValue.toInt(); + } + reply += "{\"name\":\"resetype\", \"value\":\"" + String(Settings.resetType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "debounce") { + if (configValue.length() != 0) + { + Settings.debounce = configValue.toInt(); + } + reply += "{\"name\":\"debounce\", \"value\":\"" + String(Settings.debounce) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + +#endif + + if ( reply != "" ) { + SaveSettings(); + server->send(200, "application/json", reply); + } else { + server->send(200, "application/json", "{\"success\":\"false\", \"type\":\"configuration\"}"); + } + }); + + + server->on("/status", []() { + server->send(200, "application/json", getStatus()); + }); + + server->on("/advanced", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePassword == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + char tmpString[64]; + String haIP = server->arg("haip"); + String haPort = server->arg("haport"); + String powerOnState = server->arg("pos"); + String ip = server->arg("ip"); + String gateway = server->arg("gateway"); + String subnet = server->arg("subnet"); + String dns = server->arg("dns"); + String usestatic = server->arg("usestatic"); + String usepassword = server->arg("usepassword"); + String usepasswordcontrol = server->arg("usepasswordcontrol"); + String username = server->arg("username"); + String password = server->arg("password"); + String disableJ3reset = server->arg("disableJ3reset"); + String transitionSpeed = server->arg("transitionspeed"); + String continueBoot = server->arg("continueboot"); + String autoOff = server->arg("autooff"); + String resettype = server->arg("resettype"); + String uReport = server->arg("ureport"); + String debounce = server->arg("debounce"); + String swapRG = server->arg("swaprg"); + + if (haPort.length() != 0) + { + Settings.haPort = haPort.toInt(); + } + + if (powerOnState.length() != 0) + { + Settings.powerOnState = powerOnState.toInt(); + } + + if (transitionSpeed.length() != 0) + { + Settings.transitionSpeed = transitionSpeed.toInt(); + } + + if (haIP.length() != 0) + { + if (haIP != String(Settings.haIP[0]) + "." + String(Settings.haIP[1]) + "." + String(Settings.haIP[2]) + "." + String(Settings.haIP[3])) { + needFirmware = true; + } + haIP.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.haIP); + } + + + if (ip.length() != 0 && subnet.length() != 0) + { + ip.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.IP); + subnet.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.Subnet); + } + + if (gateway.length() != 0) + { + gateway.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.Gateway); + } + + if (dns.length() != 0) + { + dns.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.DNS); + } + + if (usestatic.length() != 0) + { + Settings.useStatic = (usestatic == "yes"); + } + + if (usepassword.length() != 0) + { + Settings.usePassword = (usepassword == "yes"); + } + + if (usepasswordcontrol.length() != 0) + { + Settings.usePasswordControl = (usepasswordcontrol == "yes"); + } + + if (password.length() != 0) + { + strncpy(SecuritySettings.Password, password.c_str(), sizeof(SecuritySettings.Password)); + } + + if (disableJ3reset.length() != 0) + { + Settings.disableJ3Reset = (disableJ3reset == "true"); + } + + if (resettype.length() != 0) + { + Settings.resetType = resettype.toInt(); + } + + if (continueBoot.length() != 0) + { + Settings.continueBoot = (continueBoot == "yes"); + } + + if (autoOff.length() != 0) + { + Settings.autoOff = autoOff.toInt(); + } + + if (uReport.length() != 0) + { + if (uReport.toInt() != Settings.uReport) { + Settings.uReport = uReport.toInt(); + timerUptime = millis() + Settings.uReport * 1000; + } + } + + if (debounce.length() != 0) + { + Settings.debounce = debounce.toInt(); + } + + if (swapRG.length() != 0) + { + Settings.swapRG = (swapRG == "yes"); + } + + SaveSettings(); + + String reply = ""; + char str[20]; + addHeader(true, reply); + + reply += F(""); + + reply += F("
"); + reply += F("
"); + reply += projectName; + reply += F(" Settings"); + reply += F("
"); + addMenu(reply); + + reply += F("
Password Protect

Configuration:


"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + reply += F("
Control:"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + reply += F("
\"admin\" Password: Show"); + + reply += F(""); + + reply += F("
Static IP:"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + + reply += F("
IP:
Subnet:
Gateway:
DNS:
HA Controller IP:
HA Controller Port:"); + + byte choice = Settings.powerOnState; + reply += F("
Boot Up State:"); + + choice = Settings.transitionSpeed; + reply += F("
Transition Speed:"); + + #if defined H801 + choice = Settings.resetType; + reply += F("
Reset Type:"); + + reply += F("
Switch Debounce:"); + #endif + reply += F("
Uptime Report Interval:"); + reply += F("
Auto Off:"); + //reply += F("
Disable J3 Reset:"); + + //reply += F("Yes"); + //reply += F(""); + + //reply += F("No"); + //reply += F(""); + + reply += F("
Swap R & G Channels:"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + reply += F("
Continue Boot On Wifi Fail:"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + reply += F("
"); + reply += F("
"); + addFooter(reply); + server->send(200, "text/html", reply); + }); + + server->on("/control", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + String hexR = server->arg("r"); + String hexG = server->arg("g"); + String hexB = server->arg("b"); + String hexW1 = server->arg("w1"); + String hexW2 = server->arg("w2"); + String color = server->arg("color"); + String power = server->arg("power"); + + if (color.length() != 0){ + changeColor(color.substring(1, 7), 6, false, true); + } + else if (power == "off"){ + changeColor("00", 1, false); + changeColor("00", 2, false); + changeColor("00", 3, false); + changeColor("00", 4, false); + changeColor("00", 5, false); + } else { + if (hexR.length() != 0) { + changeColor(padHex(String(hexR.toInt(), HEX)), 1, false, true); + } + if (hexG.length() != 0) { + changeColor(padHex(String(hexG.toInt(), HEX)), 2, false, true); + } + if (hexB.length() != 0) { + changeColor(padHex(String(hexB.toInt(), HEX)), 3, false, true); + } + if (hexW1.length() != 0) { + changeColor(padHex(String(hexW1.toInt(), HEX)), 4, false, true); + } + if (hexW2.length() != 0) { + changeColor(padHex(String(hexW2.toInt(), HEX)), 5, false, true); + } + } + + String reply = ""; + char str[20]; + addHeader(true, reply); + + reply += F(""); + reply += F(""); + reply += F(""); + reply += F(""); + reply += F(""); + reply += F(""); + + reply += F(""); + reply += F(""); + reply += F(""); + reply += F("
"); + reply += projectName; + reply += F(" Control"); + reply += F("
"); + addMenu(reply); + + reply += F("
"); + reply += F("
"); + reply += F(""); + reply += F("
R"); + reply += F("
G"); + reply += F("
B"); + reply += F("
W1"); + reply += F("
W2"); + reply += F("
"); + reply += F(""); + reply += F("
"); + addFooter(reply); + + needUpdate = true; + + server->send(200, "text/html", reply); + }); + + server->on("/rgb", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + run_program = false; + String hexRGB = server->arg("value"); + String channels = server->arg("channels"); + String transition = server->arg("transition"); + + String r, g, b; + + r = hexRGB.substring(0, 2); + g = hexRGB.substring(2, 4); + b = hexRGB.substring(4, 6); + + changeColor(hexRGB, 6, (transition != "false"), (channels != "true")); + + server->send(200, "application/json", getStatus()); + + }); + + + server->on("/w1", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + run_program = false; + String hexW1 = server->arg("value"); + String channels = server->arg("channels"); + String transition = server->arg("transition"); + + changeColor(hexW1, 4, (transition != "false"), (channels != "true")); + + server->send(200, "application/json", getStatus()); + + }); + + server->on("/w2", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + run_program = false; + String hexW2 = server->arg("value"); + String channels = server->arg("channels"); + String transition = server->arg("transition"); + + changeColor(hexW2, 5, (transition != "false"), (channels != "true")); + + server->send(200, "application/json", getStatus()); + + }); + + server->on("/r", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + run_program = false; + String r = server->arg("value"); + String channels = server->arg("channels"); + String transition = server->arg("transition"); + + changeColor(r, 1, (transition != "false"), (channels != "true")); + + server->send(200, "application/json", getStatus()); + + }); + + server->on("/g", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + run_program = false; + String g = server->arg("value"); + String channels = server->arg("channels"); + String transition = server->arg("transition"); + + changeColor(g, 2, (transition != "false"), (channels != "true")); + + server->send(200, "application/json", getStatus()); + + }); + + server->on("/b", []() { + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if(!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + run_program = false; + String b = server->arg("value"); + String channels = server->arg("channels"); + String transition = server->arg("transition"); + + changeColor(b, 3, (transition != "false"), (channels != "true")); + + server->send(200, "application/json", getStatus()); + + }); + + if (ESP.getFlashChipRealSize() > 524288){ + if (Settings.usePassword == true && SecuritySettings.Password[0] != 0){ + httpUpdater.setup(&*server, "/update", "admin", SecuritySettings.Password); + httpUpdater.setProjectName(projectName); + } else { + httpUpdater.setup(&*server); + httpUpdater.setProjectName(projectName); + } + } + + server->onNotFound(handleNotFound); + + server->begin(); + + Serial.printf("Starting SSDP...\n"); + SSDP.setSchemaURL("description.xml"); + SSDP.setHTTPPort(80); + SSDP.setName(projectName); + SSDP.setSerialNumber(ESP.getChipId()); + SSDP.setURL("index.html"); + SSDP.setModelName(projectName); + SSDP.setModelNumber(deblank(projectName) + "_SL"); + SSDP.setModelURL("http://smartlife.tech"); + SSDP.setManufacturer("Smart Life Automated"); + SSDP.setManufacturerURL("http://smartlife.tech"); + SSDP.begin(); + + Serial.println("HTTP server started"); + Serial.println(WiFi.localIP()); + + timerUptime = millis() + Settings.uReport * 1000; + +} + +void loop() +{ + server->handleClient(); + + #ifdef ARILUX_ALLC10 + // handle received RF codes from the remote + handleRFRemote(); + #endif + + if(needFirmware == true){ + sendStatus(98); + needFirmware = false; + } + + if(needUpdate == true && run_program == false){ + sendStatus(); + needUpdate = false; + } + + if(run_program){ + if (millis() - previousMillis >= program_wait) { + char *dup = strdup(program.c_str()); + const char *value = strtok( dup, "_" ); + const char *program_details = ""; + program_counter = 1; + + while(value != NULL) + { + if(program_counter == program_step){ + program_details = value; + } + program_counter = program_counter + 1; + value = strtok(NULL, "_"); + } + String hexString(program_details); + + if(hexString.startsWith("w~")) { + String hexProgram = hexString.substring(2, 4); + if (hexString.indexOf("-",5) >= 0) { + program_wait = rand_interval(hexString.substring(5, hexString.indexOf("-") - 1).toInt(), hexString.substring(hexString.indexOf("-") + 1, hexString.length()).toInt()); + } else { + program_wait = hexString.substring(5, hexString.length()).toInt(); + } + changeColor(hexProgram, 4, true); + }else if(hexString.startsWith("x~")){ + String hexProgram = hexString.substring(2, 4); + if (hexString.indexOf("-",5) >= 0) { + program_wait = rand_interval(hexString.substring(5, hexString.indexOf("-") - 1).toInt(), hexString.substring(hexString.indexOf("-") + 1, hexString.length()).toInt()); + } else { + program_wait = hexString.substring(5, hexString.length()).toInt(); + } + changeColor(hexProgram, 4, false); + }else if(hexString.startsWith("y~")) { + String hexProgram = hexString.substring(2, 4); + if (hexString.indexOf("-",5) >= 0) { + program_wait = rand_interval(hexString.substring(5, hexString.indexOf("-") - 1).toInt(), hexString.substring(hexString.indexOf("-") + 1, hexString.length()).toInt()); + } else { + program_wait = hexString.substring(5, hexString.length()).toInt(); + } + changeColor(hexProgram, 5, true); + }else if(hexString.startsWith("z~")){ + String hexProgram = hexString.substring(2, 4); + if (hexString.indexOf("-",5) >= 0) { + program_wait = rand_interval(hexString.substring(5, hexString.indexOf("-") - 1).toInt(), hexString.substring(hexString.indexOf("-") + 1, hexString.length()).toInt()); + } else { + program_wait = hexString.substring(5, hexString.length()).toInt(); + } + changeColor(hexProgram, 5, false); + }else if(hexString.startsWith("f~")){ + String hexProgram = hexString.substring(2, 8); + if (hexString.indexOf("-",9) >= 0) { + program_wait = rand_interval(hexString.substring(9, hexString.indexOf("-") - 1).toInt(), hexString.substring(hexString.indexOf("-") + 1, hexString.length()).toInt()); + } else { + program_wait = hexString.substring(9, hexString.length()).toInt(); + } + changeColor(hexProgram, 6, true); + }else{ + String hexProgram = hexString.substring(2, 8); + if (hexString.indexOf("-",9) >= 0) { + program_wait = rand_interval(hexString.substring(9, hexString.indexOf("-") - 1).toInt(), hexString.substring(hexString.indexOf("-") + 1, hexString.length()).toInt()); + } else { + program_wait = hexString.substring(9, hexString.length()).toInt(); + } + changeColor(hexProgram, 6, false); + } + + previousMillis = millis(); + program_step = program_step + 1; + + if (program_step >= program_counter && repeat == -1){ + program_step = 1; + }else if(program_step >= program_counter && repeat_count < repeat){ + program_step = 1; + repeat_count = repeat_count + 1; + }else if(program_step > program_counter){ + program_step = 1; + run_program = false; + if(program_off == "true"){ + changeColor("000000", 6, false); + changeColor("00", 4, false); + } + sendStatus(); + } + + free(dup); + } + } + + if(millis() - TIME_LED_RED >= led_delay_red){ + TIME_LED_RED = millis(); + LED_RED(); + } + + if(millis() - TIME_LED_GREEN >= led_delay_green){ + TIME_LED_GREEN = millis(); + LED_GREEN(); + } + + if(millis() - TIME_LED_BLUE >= led_delay_blue){ + TIME_LED_BLUE = millis(); + LED_BLUE(); + } + + if(millis() - TIME_LED_W1 >= led_delay_w1){ + TIME_LED_W1 = millis(); + LED_W1(); + } + + if(millis() - TIME_LED_W2 >= led_delay_w2){ + TIME_LED_W2 = millis(); + LED_W2(); + } + + if (millis() > timerwd) + runEach5Minutes(); + + if (Settings.uReport > 0 && millis() > timerUptime){ + sendStatus(99); + timerUptime = millis() + Settings.uReport * 1000; + } + + if ((Settings.autoOff != 0) && inAutoOff && ((millis() - autoOffTimer) > (1000 * Settings.autoOff))) { + changeColor("00000000", 0, true); + inAutoOff = false; + autoOffTimer = 0; + } + +} diff --git a/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBWBulb.ino.generic.bin b/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBWBulb.ino.generic.bin new file mode 100644 index 0000000..08c7b49 Binary files /dev/null and b/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBWBulb.ino.generic.bin differ diff --git a/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBWController.ino.generic.bin b/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBWController.ino.generic.bin new file mode 100644 index 0000000..a101f4f Binary files /dev/null and b/Drivers/smartlife-rgbw-controller.src/SmartLifeRGBWController.ino.generic.bin differ diff --git a/Drivers/smartlife-rgbw-controller.src/smartlife-rgbw-controller.groovy b/Drivers/smartlife-rgbw-controller.src/smartlife-rgbw-controller.groovy new file mode 100644 index 0000000..79a23f4 --- /dev/null +++ b/Drivers/smartlife-rgbw-controller.src/smartlife-rgbw-controller.groovy @@ -0,0 +1,1168 @@ +/** + * Copyright 2016 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. + * + * SmartLife RGBW Controller + * + * Author: Eric Maycock (erocm123) + * Date: 2016-12-10 + */ + +import groovy.json.JsonSlurper + +metadata { + definition (name: "SmartLife RGBW Controller", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Switch" + capability "Refresh" + capability "Sensor" + capability "Configuration" + capability "Health Check" + + (1..6).each { n -> + attribute "switch$n", "enum", ["on", "off"] + command "on$n" + command "off$n" + } + + command "reset" + command "setProgram" + command "setWhiteLevel" + + command "redOn" + command "redOff" + command "greenOn" + command "greenOff" + command "blueOn" + command "blueOff" + command "white1On" + command "white1Off" + command "white2On" + command "white2Off" + + command "setRedLevel" + command "setGreenLevel" + command "setBlueLevel" + command "setWhite1Level" + command "setWhite2Level" + } + + 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.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } + + 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:"http://cdn.device-icons.smartthings.com/secondary/configure@2x.png" + 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("red", "device.red", height: 1, width: 1, inactiveLabel: false, decoration: "flat", canChangeIcon: false) { + state "off", label:"R", action:"redOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"R", action:"redOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF0000" + } + controlTile("redSliderControl", "device.redLevel", "slider", height: 1, width: 4, inactiveLabel: false) { + state "redLevel", action:"setRedLevel" + } + valueTile("redValueTile", "device.redLevel", height: 1, width: 1) { + state "redLevel", label:'${currentValue}%' + } + + standardTile("green", "device.green", height: 1, width: 1, inactiveLabel: false, decoration: "flat", canChangeIcon: false) { + state "off", label:"G", action:"greenOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"G", action:"greenOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FF00" + } + controlTile("greenSliderControl", "device.greenLevel", "slider", height: 1, width: 4, inactiveLabel: false) { + state "greenLevel", action:"setGreenLevel" + } + valueTile("greenValueTile", "device.greenLevel", height: 1, width: 1) { + state "greenLevel", label:'${currentValue}%' + } + + standardTile("blue", "device.blue", height: 1, width:1, inactiveLabel: false, decoration: "flat", canChangeIcon: false) { + state "off", label:"B", action:"blueOn", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"B", action:"blueOff", icon:"st.illuminance.illuminance.bright", backgroundColor:"#0000FF" + } + controlTile("blueSliderControl", "device.blueLevel", "slider", height: 1, width: 4, inactiveLabel: false) { + state "blueLevel", action:"setBlueLevel" + } + valueTile("blueValueTile", "device.blueLevel", height: 1, width: 1) { + state "blueLevel", label:'${currentValue}%' + } + + standardTile("white1", "device.white1", height: 1, width: 1, inactiveLabel: false, decoration: "flat", canChangeIcon: false) { + state "off", label:"W1", action:"white1On", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"W1", action:"white1Off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + controlTile("white1SliderControl", "device.white1Level", "slider", height: 1, width: 4, inactiveLabel: false) { + state "white1Level", action:"setWhite1Level" + } + valueTile("white1ValueTile", "device.white1Level", height: 1, width: 1) { + state "white1Level", label:'${currentValue}%' + } + standardTile("white2", "device.white2", height: 1, width: 1, inactiveLabel: false, decoration: "flat", canChangeIcon: false) { + state "off", label:"W2", action:"white2On", icon:"st.illuminance.illuminance.dark", backgroundColor:"#cccccc" + state "on", label:"W2", action:"white2Off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + controlTile("white2SliderControl", "device.white2Level", "slider", height: 1, width: 4, inactiveLabel: false) { + state "white2Level", action:"setWhite2Level" + } + valueTile("white2ValueTile", "device.white2Level", height: 1, width: 1) { + state "white2Level", label:'${currentValue}%' + } + valueTile("ip", "ip", width: 2, height: 1) { + state "ip", label:'IP Address\r\n${currentValue}' + } + valueTile("firmware", "firmware", width: 2, height: 1) { + state "firmware", label:'Firmware ${currentValue}' + } + + + (1..6).each { n -> + standardTile("switch$n", "switch$n", canChangeIcon: true, width: 2, height: 2, decoration: "flat") { + state "off", label: "Program\n$n", action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "Program\n$n", action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + } + } + + main(["switch"]) + details(["switch", "levelSliderControl", + "red", "redSliderControl", "redValueTile", + "green", "greenSliderControl", "greenValueTile", + "blue", "blueSliderControl", "blueValueTile", + "white1", "white1SliderControl", "white1ValueTile", + "white2", "white2SliderControl", "white2ValueTile", + "switch1", "switch2", "switch3", + "switch4", "switch5", "switch6", + "refresh", "configure", "ip", "firmware" ]) +} + +def installed() { + log.debug "installed()" + configure() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) cmds +} + +def updated() +{ + logging("updated()", 1) + def cmds = [] + cmds = update_needed_settings() + sendEvent(name: "checkInterval", value: 12 * 60 * 2, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) + 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 getDefault(){ + if(settings.dcolor == "Previous") { + return "Previous" + } else if(settings.dcolor == "Random") { + return "${transition == "false"? "d~" : "f~"}${getHexColor(settings.dcolor)}" + } else if(settings.dcolor == "Custom") { + return "${transition == "false"? "d~" : "f~"}${settings.custom}" + } else if(settings.dcolor == "Soft White" || settings.dcolor == "Warm White") { + if (settings.level == null || settings.level == "0") { + return "${transition == "false"? "x~" : "w~"}${getDimmedColor(getHexColor(settings.dcolor), "100")}" + } else { + return "${transition == "false"? "x~" : "w~"}${getDimmedColor(getHexColor(settings.dcolor), settings.level)}" + } + } else if(settings.dcolor == "W1") { + if (settings.level == null || settings.level == "0") { + return "${transition == "false"? "x~" : "w~"}${getDimmedColor(getHexColor(settings.dcolor), "100")}" + } else { + return "${transition == "false"? "x~" : "w~"}${getDimmedColor(getHexColor(settings.dcolor), settings.level)}" + } + } else if(settings.dcolor == "W2") { + if (settings.level == null || settings.level == "0") { + return "${transition == "false"? "z~" : "y~"}${getDimmedColor(getHexColor(settings.dcolor), "100")}" + } else { + return "${transition == "false"? "z~" : "y~"}${getDimmedColor(getHexColor(settings.dcolor), settings.level)}" + } + } else { + if (settings.level == null || settings.dcolor == null){ + return "Previous" + } else if (settings.level == null || settings.level == "0") { + return "${transition == "false"? "d~" : "f~"}${getDimmedColor(getHexColor(settings.dcolor), "100")}" + } else { + return "${transition == "false"? "d~" : "f~"}${getDimmedColor(getHexColor(settings.dcolor), settings.level)}" + } + } +} + +def parse(description) { + def map = [:] + def events = [] + def cmds = [] + + if(description == "updated") return + def descMap = parseDescriptionAsMap(description) + + if (!state.mac || state.mac != descMap["mac"]) { + log.debug "Mac address of device found ${descMap["mac"]}" + updateDataValue("mac", descMap["mac"]) + } + if (state.mac != null && state.dni != state.mac) state.dni = setDeviceNetworkId(state.mac) + + def body = new String(descMap["body"].decodeBase64()) + log.debug body + + def slurper = new JsonSlurper() + def result = slurper.parseText(body) + + if (result.containsKey("type")) { + if (result.type == "configuration") + events << update_current_properties(result) + } + if (result.containsKey("power")) { + events << createEvent(name: "switch", value: result.power) + toggleTiles("all") + } + if (result.containsKey("rgb")) { + events << createEvent(name:"color", value:"#$result.rgb") + def rgb = hexToRgb("#$result.rgb") + def hsv = rgbwToHSV(rgb) + events << createEvent(name:"hue", value:hsv.hue) + events << createEvent(name:"saturation", value:hsv.saturation) + + // only store the previous value if the response did not come from a power-off command + if (result.power != "off") + state.previousRGB = result.rgb + } + if (result.containsKey("r")) { + events << createEvent(name:"redLevel", value: Integer.parseInt(result.r,16)/255 * 100 as Integer, displayed: false) + if ((Integer.parseInt(result.r,16)/255 * 100 as Integer) > 0 ) { + events << createEvent(name:"red", value: "on", displayed: false) + } else { + events << createEvent(name:"red", value: "off", displayed: false) + } + } + if (result.containsKey("g")) { + events << createEvent(name:"greenLevel", value: Integer.parseInt(result.g,16)/255 * 100 as Integer, displayed: false) + if ((Integer.parseInt(result.g,16)/255 * 100 as Integer) > 0 ) { + events << createEvent(name:"green", value: "on", displayed: false) + } else { + events << createEvent(name:"green", value: "off", displayed: false) + } + } + if (result.containsKey("b")) { + events << createEvent(name:"blueLevel", value: Integer.parseInt(result.b,16)/255 * 100 as Integer, displayed: false) + if ((Integer.parseInt(result.b,16)/255 * 100 as Integer) > 0 ) { + events << createEvent(name:"blue", value: "on", displayed: false) + } else { + events << createEvent(name:"blue", value: "off", displayed: false) + } + } + if (result.containsKey("w1")) { + events << createEvent(name:"white1Level", value: Integer.parseInt(result.w1,16)/255 * 100 as Integer, displayed: false) + if ((Integer.parseInt(result.w1,16)/255 * 100 as Integer) > 0 ) { + events << createEvent(name:"white1", value: "on", displayed: false) + } else { + events << createEvent(name:"white1", value: "off", displayed: false) + } + + // only store the previous value if the response did not come from a power-off command + if (result.power != "off") + state.previousW1 = result.w1 + } + if (result.containsKey("w2")) { + events << createEvent(name:"white2Level", value: Integer.parseInt(result.w2,16)/255 * 100 as Integer, displayed: false) + if ((Integer.parseInt(result.w2,16)/255 * 100 as Integer) > 0 ) { + events << createEvent(name:"white2", value: "on", displayed: false) + } else { + events << createEvent(name:"white2", value: "off", displayed: false) + } + + // only store the previous value if the response did not come from a power-off command + if (result.power != "off") + state.previousW2 = result.w2 + } + if (result.containsKey("version")) { + events << createEvent(name:"firmware", value: result.version + "\r\n" + result.date, displayed: false) + } + + if (result.containsKey("success")) { + if (result.success == "true") state.configSuccess = "true" else state.configSuccess = "false" + } + if (result.containsKey("program")) { + if (result.running == "false") { + toggleTiles("all") + } + else { + toggleTiles("switch$result.program") + events << createEvent(name:"switch$result.program", value: "on") + } + } + + if (!device.currentValue("ip") || (device.currentValue("ip") != getDataValue("ip"))) events << createEvent(name: 'ip', value: getDataValue("ip")) + + return events +} + +private toggleTiles(value) { + def tiles = ["switch1", "switch2", "switch3", "switch4", "switch5", "switch6"] + tiles.each {tile -> + if (tile != value) sendEvent(name: tile, value: "off") + } +} + +private getScaledColor(color) { + def rgb = color.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + def maxNumber = 1 + for (int i = 0; i < 3; i++){ + if (rgb[i] > maxNumber) { + maxNumber = rgb[i] + } + } + def scale = 255/maxNumber + for (int i = 0; i < 3; i++){ + rgb[i] = rgb[i] * scale + } + def myred = rgb[0] + def mygreen = rgb[1] + def myblue = rgb[2] + return rgbToHex([r:myred, g:mygreen, b:myblue]) +} + +def on() { + log.debug "on()" + getAction("/on?transition=$transition") +} + +def off() { + log.debug "off()" + getAction("/off?transition=$transition") +} + +def setLevel(level) { + setLevel(level, 1) +} + +def setLevel(level, duration) { + log.debug "setLevel() level = ${level}" + if(level > 100) level = 100 + if (level == 0) { off() } + else if (device.latestValue("switch") == "off") { on() } + sendEvent(name: "level", value: level) + sendEvent(name: "setLevel", value: level, displayed: false) + setColor(aLevel: level) +} +def setSaturation(percent) { + log.debug "setSaturation($percent)" + setColor(saturation: percent) +} +def setHue(value) { + log.debug "setHue($value)" + setColor(hue: value) +} +def getWhite(value) { + log.debug "getWhite($value)" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + return hex(level) +} +def setColor(value) { + log.debug "setColor being called with ${value}" + def uri + def validValue = true + + if ((value.saturation != null) && (value.hue != null)) { + def hue = (value.hue != null) ? value.hue : 13 + def saturation = (value.saturation != null) ? value.saturation : 13 + def rgb = huesatToRGB(hue as Integer, saturation as Integer) + value.hex = rgbToHex([r:rgb[0], g:rgb[1], b:rgb[2]]) + } + + if (value.hue == 5 && value.saturation == 4) { + log.debug "setting color Soft White - Default" + def whiteLevel = getWhite(value.level) + uri = "/w1?value=${whiteLevel}" + state.previousColor = "${whiteLevel}" + } + /* Letting White - Concentrate adjust RGB values + else if (value.hue == 63 && value.saturation == 28) { + log.debug "setting color White - Concentrate" + def whiteLevel = getWhite(value.level) + uri = "/w1?value=${whiteLevel}" + state.previousColor = "${whiteLevel}" + } */ + else if (value.hue == 63 && value.saturation == 43) { + log.debug "setting color Daylight - Energize" + def whiteLevel = getWhite(value.level) + uri = "/w2?value=${whiteLevel}" + state.previousColor = "${whiteLevel}" + } + else if (value.hue == 79 && value.saturation == 7) { + log.debug "setting color Warm White - Relax" + def whiteLevel = getWhite(value.level) + uri = "/w1?value=${whiteLevel}" + state.previousColor = "${whiteLevel}" + } + else if (value.colorTemperature) { + log.debug "setting color with color temperature" + def whiteLevel = getWhite(value.level) + uri = "/w1?value=${whiteLevel}" + state.previousColor = "${whiteLevel}" + } + else if (value.hex) { + log.debug "setting color with hex" + if (!value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) { + log.debug "$value.hex is not valid" + validValue = false + } else { + def rgb = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + def myred = rgb[0] < 40 ? 0 : rgb[0] + def mygreen = rgb[1] < 40 ? 0 : rgb[1] + def myblue = rgb[2] < 40 ? 0 : rgb[2] + def dimmedColor = getDimmedColor(rgbToHex([r:myred, g:mygreen, b:myblue])) + uri = "/rgb?value=${dimmedColor}" + } + } + else if (value.white) { + uri = "/w1?value=${value.white}" + } + else if (value.aLevel) { + def actions = [] + if (channels == "true") { + def skipColor = false + // Handle white channel dimmers if they're on or were not previously off (excluding power-off command) + if (device.currentValue("white1") == "on" || state.previousW1 != "00") { + actions.push(setWhite1Level(value.aLevel)) + skipColor = true + } + if (device.currentValue("white2") == "on" || state.previousW2 != "00") { + actions.push(setWhite2Level(value.aLevel)) + skipColor = true + } + if (skipColor == false) { + log.debug state.previousRGB + // if the device is currently on, scale the current RGB values; otherwise scale the previous setting + uri = "/rgb?value=${getDimmedColor(device.latestValue("switch") == "on" ? device.currentValue("color").substring(1) : state.previousRGB)}" + actions.push(getAction("$uri&channels=$channels&transition=$transition")) + } + } else { + // Handle white channel dimmers if they're on or were not previously off (excluding power-off command) + if (device.currentValue("white1") == "on" || state.previousW1 != "00") + actions.push(setWhite1Level(value.aLevel)) + if (device.currentValue("white2") == "on" || state.previousW2 != "00") + actions.push(setWhite2Level(value.aLevel)) + + // if the device is currently on, scale the current RGB values; otherwise scale the previous setting + uri = "/rgb?value=${getDimmedColor(device.latestValue("switch") == "on" ? device.currentValue("color").substring(1) : state.previousRGB)}" + actions.push(getAction("$uri&channels=$channels&transition=$transition")) + } + return actions + } + else { + // A valid color was not chosen. Setting to white + uri = "/w1?value=ff" + } + + if (uri != null && validValue != false) getAction("$uri&channels=$channels&transition=$transition") + +} + +private getDimmedColor(color, level) { + if(color.size() > 2){ + def scaledColor = getScaledColor(color) + def rgb = scaledColor.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + + def r = hex(rgb[0] * (level.toInteger()/100)) + def g = hex(rgb[1] * (level.toInteger()/100)) + def b = hex(rgb[2] * (level.toInteger()/100)) + + return "${r + g + b}" + }else{ + color = Integer.parseInt(color, 16) + return hex(color * (level.toInteger()/100)) + } +} + +private getDimmedColor(color) { + if (device.latestValue("level")) { + getDimmedColor(color, device.latestValue("level")) + } else { + return color.replaceAll("#","") + } +} + +def reset() { + log.debug "reset()" + setColor(white: "ff") +} + +def refresh() { + log.debug "refresh()" + getAction("/status") +} + +def ping() { + log.debug "ping()" + refresh() +} + +def setWhiteLevel(value) { + log.debug "setwhiteLevel: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + if ( value > 0 ) { + if (device.latestValue("switch") == "off") { on() } + sendEvent(name: "white", value: "on") + } else { + sendEvent(name: "white", value: "off") + } + def whiteLevel = hex(level) + setColor(white: whiteLevel) +} + +def hexToRgb(colorHex) { + def rrInt = Integer.parseInt(colorHex.substring(1,3),16) + def ggInt = Integer.parseInt(colorHex.substring(3,5),16) + def bbInt = Integer.parseInt(colorHex.substring(5,7),16) + + def colorData = [:] + colorData = [r: rrInt, g: ggInt, b: bbInt] + colorData +} + +// huesatToRGB Changed method provided by daved314 +def huesatToRGB(float hue, float sat) { + if (hue <= 100) { + hue = hue * 3.6 + } + sat = sat / 100 + float v = 1.0 + float c = v * sat + float x = c * (1 - Math.abs(((hue/60)%2) - 1)) + float m = v - c + int mod_h = (int)(hue / 60) + int cm = Math.round((c+m) * 255) + int xm = Math.round((x+m) * 255) + int zm = Math.round((0+m) * 255) + switch(mod_h) { + case 0: return [cm, xm, zm] + case 1: return [xm, cm, zm] + case 2: return [zm, cm, xm] + case 3: return [zm, xm, cm] + case 4: return [xm, zm, cm] + case 5: return [cm, zm, xm] + } +} + +private rgbwToHSV(Map colorMap) { + log.debug "rgbwToHSV(): colorMap: ${colorMap}" + + if (colorMap.containsKey("r") & colorMap.containsKey("g") & colorMap.containsKey("b")) { + + float r = colorMap.r / 255f + float g = colorMap.g / 255f + float b = colorMap.b / 255f + float w = (colorMap.white) ? colorMap.white / 255f : 0.0 + float max = [r, g, b].max() + float min = [r, g, b].min() + float delta = max - min + + float h,s,v = 0 + + if (delta) { + s = delta / max + if (r == max) { + h = ((g - b) / delta) / 6 + } else if (g == max) { + h = (2 + (b - r) / delta) / 6 + } else { + h = (4 + (r - g) / delta) / 6 + } + while (h < 0) h += 1 + while (h >= 1) h -= 1 + } + + v = [max,w].max() + + return colorMap << [ hue: h * 100, saturation: s * 100, level: Math.round(v * 100) ] + } + else { + log.error "rgbwToHSV(): Cannot obtain color information from colorMap: ${colorMap}" + } +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} +def rgbToHex(rgb) { + def r = hex(rgb.r) + def g = hex(rgb.g) + def b = hex(rgb.b) + def hexColor = "#${r}${g}${b}" + + hexColor +} + +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) + } +} + +private encodeCredentials(username, password){ + def userpassascii = "${username}:${password}" + def userpass = "Basic " + userpassascii.encodeAsBase64().toString() + return userpass +} + +private getAction(uri){ + updateDNI() + def userpass + log.debug uri + if(password != null && password != "") + userpass = encodeCredentials("admin", password) + + def headers = getHeader(userpass) + + def hubAction = new hubitat.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 hubitat.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" + } + log.debug "Device Network Id set to ${myDNI}" + return myDNI +} + +private updateDNI() { + if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) { + device.deviceNetworkId = state.dni + } +} + +private getHostAddress() { + 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 +} + +def parseDescriptionAsMap(description) { + description.split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +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 toAscii(s){ + StringBuilder sb = new StringBuilder(); + String ascString = null; + long asciiInt; + for (int i = 0; i < s.length(); i++){ + sb.append((int)s.charAt(i)); + sb.append("|"); + char c = s.charAt(i); + } + ascString = sb.toString(); + asciiInt = Long.parseLong(ascString); + return asciiInt; +} + +def on1() { onOffCmd(1, 1) } +def on2() { onOffCmd(1, 2) } +def on3() { onOffCmd(1, 3) } +def on4() { onOffCmd(1, 4) } +def on5() { onOffCmd(1, 5) } +def on6() { onOffCmd(1, 6) } + +def off1(p=null) { onOffCmd((p == null ? 0 : p), 1) } +def off2(p=null) { onOffCmd((p == null ? 0 : p), 2) } +def off3(p=null) { onOffCmd((p == null ? 0 : p), 3) } +def off4(p=null) { onOffCmd((p == null ? 0 : p), 4) } +def off5(p=null) { onOffCmd((p == null ? 0 : p), 5) } +def off6(p=null) { onOffCmd((p == null ? 0 : p), 6) } + +def onOffCmd(value, program) { + log.debug "onOffCmd($value, $program)" + def uri + if (value == 1){ + if(state."program${program}" != null) { + uri = "/program?value=${state."program${program}"}&number=$program" + } + } else if(value == 0){ + uri = "/stop" + } else { + uri = "/off" + } + if (uri != null) return getAction(uri) +} + +def setProgram(value, program){ + state."program${program}" = value +} + +def hex2int(value){ + return Integer.parseInt(value, 10) +} + +def redOn() { + log.debug "redOn()" + getAction("/r?value=ff&channels=$channels&transition=$transition") +} +def redOff() { + log.debug "redOff()" + getAction("/r?value=00&channels=$channels&transition=$transition") +} + +def setRedLevel(value) { + log.debug "setRedLevel: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + level = hex(level) + getAction("/r?value=$level&channels=$channels&transition=$transition") +} +def greenOn() { + log.debug "greenOn()" + getAction("/g?value=ff&channels=$channels&transition=$transition") +} +def greenOff() { + log.debug "greenOff()" + getAction("/g?value=00&channels=$channels&transition=$transition") +} + +def setGreenLevel(value) { + log.debug "setGreenLevel: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + level = hex(level) + getAction("/g?value=$level&channels=$channels&transition=$transition") +} +def blueOn() { + log.debug "blueOn()" + getAction("/b?value=ff&channels=$channels&transition=$transition") +} +def blueOff() { + log.debug "blueOff()" + getAction("/b?value=00&channels=$channels&transition=$transition") +} + +def setBlueLevel(value) { + log.debug "setBlueLevel: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + level = hex(level) + getAction("/b?value=$level&channels=$channels&transition=$transition") +} +def white1On() { + log.debug "white1On()" + getAction("/w1?value=ff&channels=$channels&transition=$transition") +} +def white1Off() { + log.debug "white1Off()" + getAction("/w1?value=00&channels=$channels&transition=$transition") +} + +def setWhite1Level(value) { + log.debug "setwhite1Level: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + def whiteLevel = hex(level) + getAction("/w1?value=$whiteLevel&channels=$channels&transition=$transition") +} +def white2On() { + log.debug "white2On()" + getAction("/w2?value=ff&channels=$channels&transition=$transition") +} +def white2Off() { + log.debug "white2Off()" + getAction("/w2?value=00&channels=$channels&transition=$transition") +} + +def setWhite2Level(value) { + log.debug "setwhite2Level: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + log.debug "level: ${level}" + def whiteLevel = hex(level) + getAction("/w2?value=$whiteLevel&channels=$channels&transition=$transition") +} + +private getHexColor(value){ +def color = "" + switch(value){ + case "Previous": + color = "Previous" + break; + case "White": + color = "ffffff" + break; + case "Daylight": + color = "ffffff" + break; + case "Soft White": + color = "ff" + break; + case "Warm White": + color = "ff" + break; + case "W1": + color = "ff" + break; + case "W2": + color = "ff" + break; + case "Blue": + color = "0000ff" + break; + case "Green": + color = "00ff00" + break; + case "Yellow": + color = "ffff00" + break; + case "Orange": + color = "ff5a00" + break; + case "Purple": + color = "5a00ff" + break; + case "Pink": + color = "ff00ff" + break; + case "Cyan": + color = "00ffff" + break; + case "Red": + color = "ff0000" + break; + case "Off": + color = "000000" + break; + case "Random": + color = "xxxxxx" + break; +} + return color +} + +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}", + 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 + case "text": + input "${it.@index}", "text", + 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 (convertParam("${cmd.name}", 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 = new XmlSlurper().parseText(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 ${convertParam("${it.@index}", it.@value)}", 2) + cmds << getAction("/configSet?name=${it.@index}&value=${convertParam("${it.@index}", 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}"? convertParam("${it.@index}", settings."${it.@index}".toString()) : convertParam("${it.@index}", "${it.@value}"))) + { + isUpdateNeeded = "YES" + logging("Setting ${it.@index} will be updated to ${convertParam("${it.@index}", settings."${it.@index}")}", 2) + cmds << getAction("/configSet?name=${it.@index}&value=${convertParam("${it.@index}", settings."${it.@index}")}") + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(name, value) { + switch (name){ + case "dcolor": + getDefault() + break + default: + value + break + } +} + +def configuration_model() +{ +''' + + + + + + + +Default: Off + + + + + + +Default: Fade + + + + + + +Default: Previous + + + + + + + + + + + + + + + + + + + + +(ie ffffff) +If \"Custom\" is chosen above as the default color. Default level does not apply if custom hex value is chosen. + + + + + + + + + + + + +Default: Slow + + + + + + + +Automatically turn the switch off after this many seconds. +Range: 0 to 65536 +Default: 0 (Disabled) + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/smartlife-rgbw-virtual-switch.src/smartlife-rgbw-virtual-switch.groovy b/Drivers/smartlife-rgbw-virtual-switch.src/smartlife-rgbw-virtual-switch.groovy new file mode 100644 index 0000000..a6061e6 --- /dev/null +++ b/Drivers/smartlife-rgbw-virtual-switch.src/smartlife-rgbw-virtual-switch.groovy @@ -0,0 +1,75 @@ +/** + * Copyright 2015 SmartThings + * + * 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. + * + */ +metadata { + + definition (name: "SmartLife RGBW Virtual Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Relay Switch" + capability "Actuator" + + command "onPhysical" + command "offPhysical" + } + + 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" + } + } + main "switch" + details(["switch"]) + } +} + +def parse(String description) { + //def pair = description.split(":") + //createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def parse(Map description) { + //def pair = description.split(":") + //createEvent(name: pair[0].trim(), value: pair[1].trim()) + def eventMap + if (description.type == null) eventMap = [name:"$description.name", value:"$description.value"] + else eventMap = [name:"$description.name", value:"$description.value", type:"$description.type"] + createEvent(eventMap) +} + +def on() { + log.debug "$version on()" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "$version off()" + sendEvent(name: "switch", value: "off") +} + +def onPhysical() { + log.debug "$version onPhysical()" + sendEvent(name: "switch", value: "on", type: "physical") +} + +def offPhysical() { + log.debug "$version offPhysical()" + sendEvent(name: "switch", value: "off", type: "physical") +} + +private getVersion() { + "PUBLISHED" +} \ No newline at end of file diff --git a/Drivers/smoke-detector-child-device.src/smoke-detector-child-device.groovy b/Drivers/smoke-detector-child-device.src/smoke-detector-child-device.groovy new file mode 100644 index 0000000..2880812 --- /dev/null +++ b/Drivers/smoke-detector-child-device.src/smoke-detector-child-device.groovy @@ -0,0 +1,31 @@ +/** + * Smoke Detector Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Smoke Detector Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Smoke Detector" + capability "Sensor" + } + + tiles() { + multiAttributeTile(name:"smoke", type: "generic", width: 6, height: 4){ + tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:"smoke", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + } + } + } + +} diff --git a/Drivers/sonoff-4ch-wifi-switch.src/Sonoff4CH.ino.generic.bin b/Drivers/sonoff-4ch-wifi-switch.src/Sonoff4CH.ino.generic.bin new file mode 100644 index 0000000..25b7ceb Binary files /dev/null and b/Drivers/sonoff-4ch-wifi-switch.src/Sonoff4CH.ino.generic.bin differ diff --git a/Drivers/sonoff-4ch-wifi-switch.src/sonoff-4ch-wifi-switch.groovy b/Drivers/sonoff-4ch-wifi-switch.src/sonoff-4ch-wifi-switch.groovy new file mode 100644 index 0000000..721ea98 --- /dev/null +++ b/Drivers/sonoff-4ch-wifi-switch.src/sonoff-4ch-wifi-switch.groovy @@ -0,0 +1,515 @@ +/** + * Copyright 2017 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 4CH Wifi Switch + * + * Author: Eric Maycock (erocm123) + * Date: 2017-04-12 + */ + +import groovy.json.JsonSlurper +import groovy.util.XmlSlurper + +metadata { + definition (name: "Sonoff 4CH Wifi Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + 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: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.on", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.off", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.off", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.on", nextState:"turningOn" + } + } + + 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.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) + } + } else { + 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("/on") + return cmds +} + +def off() { + logging("off()", 1) + def cmds = [] + cmds << getAction("/off") + 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${channelNumber(dni)}") + sendHubCommand(cmds) +} + +void childOff(String dni) { + logging("childOff($dni)", 1) + def cmds = [] + cmds << getAction("/off${channelNumber(dni)}") + 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 hubitat.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 hubitat.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 = 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}", + 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 = new XmlSlurper().parseText(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 { + for (i in 1..4) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, + [completedSetup: true, label: "${device.displayName} (R${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Relay $i"]) + } + } 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) + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/sonoff-dual-wifi-switch.src/SonoffDual.ino.generic.bin b/Drivers/sonoff-dual-wifi-switch.src/SonoffDual.ino.generic.bin new file mode 100644 index 0000000..675e5d7 Binary files /dev/null and b/Drivers/sonoff-dual-wifi-switch.src/SonoffDual.ino.generic.bin differ diff --git a/Drivers/sonoff-dual-wifi-switch.src/sonoff-dual-wifi-switch.groovy b/Drivers/sonoff-dual-wifi-switch.src/sonoff-dual-wifi-switch.groovy new file mode 100644 index 0000000..6f6d569 --- /dev/null +++ b/Drivers/sonoff-dual-wifi-switch.src/sonoff-dual-wifi-switch.groovy @@ -0,0 +1,515 @@ +/** + * Copyright 2017 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 Dual Wifi Switch + * + * Author: Eric Maycock (erocm123) + * Date: 2016-06-02 + */ + +import groovy.json.JsonSlurper +import groovy.util.XmlSlurper + +metadata { + definition (name: "Sonoff Dual Wifi Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + 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: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.on", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.off", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.off", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.on", nextState:"turningOn" + } + } + + 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.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) + } + } else { + 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("/on") + return cmds +} + +def off() { + logging("off()", 1) + def cmds = [] + cmds << getAction("/off") + 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${channelNumber(dni)}") + sendHubCommand(cmds) +} + +void childOff(String dni) { + logging("childOff($dni)", 1) + def cmds = [] + cmds << getAction("/off${channelNumber(dni)}") + 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 hubitat.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 hubitat.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 = 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}", + 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 = new XmlSlurper().parseText(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 { + for (i in 1..2) { + addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, + [completedSetup: true, label: "${device.displayName} (R${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Relay $i"]) + } + } 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) + + + + + + + + + + +''' +} diff --git a/Drivers/sonoff-pow-wifi-switch.src/SonoffPOW.ino.generic.bin b/Drivers/sonoff-pow-wifi-switch.src/SonoffPOW.ino.generic.bin new file mode 100644 index 0000000..ebfebdf Binary files /dev/null and b/Drivers/sonoff-pow-wifi-switch.src/SonoffPOW.ino.generic.bin differ diff --git a/Drivers/sonoff-pow-wifi-switch.src/sonoff-pow-wifi-switch.groovy b/Drivers/sonoff-pow-wifi-switch.src/sonoff-pow-wifi-switch.groovy new file mode 100644 index 0000000..9ab5d4d --- /dev/null +++ b/Drivers/sonoff-pow-wifi-switch.src/sonoff-pow-wifi-switch.groovy @@ -0,0 +1,500 @@ +/** + * Copyright 2016 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 POW Wifi Switch + * + * Author: Eric Maycock (erocm123) + * Date: 2017-06-14 + * + * 2017-06-14: Added option to adjust Uptime report frequency. Made uptime events not show up in "Recently" tab. + * Fixed bug that was making settings appear as if they were not synced even when they were. + * Fixed bug that prevented zero values (0) from being entered into preferences. + * + */ + +import groovy.json.JsonSlurper + +metadata { + definition (name: "Sonoff POW Wifi Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Sensor" + capability "Configuration" + capability "Voltage Measurement" + capability "Power Meter" + capability "Health Check" + + attribute "amperage", "number" + attribute "needUpdate", "string" + command "reboot" + } + + 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: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.on", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.off", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.off", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.on", nextState:"turningOn" + } + } + 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" + } + valueTile("power", "device.power", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("voltage", "device.voltage", width: 2, height: 2) { + state "default", label:'${currentValue} V' + } + valueTile("amperage", "device.amperage", width: 2, height: 2) { + state "default", label:'${currentValue} A' + } + 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}' + } + } + + main(["switch"]) + details(["switch", + "power", "amperage", "voltage", + "refresh","configure","reboot", + "ip", "uptime"]) +} + +def installed() { + log.debug "installed()" + configure() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) cmds +} + +def updated() +{ + logging("updated()", 1) + 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"]) { + log.debug "Mac address of device found ${descMap["mac"]}" + 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) + + log.debug "result: ${result}" + + if (result.containsKey("type")) { + if (result.type == "configuration") + events << update_current_properties(result) + } + if (result.containsKey("power")) { + events << createEvent(name: "switch", value: result.power) + } + if (result.containsKey("uptime")) { + events << createEvent(name: 'uptime', value: result.uptime, displayed: false) + } + if (result.containsKey("V")) { + events << createEvent(name: "voltage", value: result.V, unit: "V") + } + if (result.containsKey("A")) { + events << createEvent(name: "amperage", value: result.A, unit: "A") + } + if (result.containsKey("W")) { + events << createEvent(name: "power", value: result.W, unit: "W") + } + } 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 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("/on") + return cmds +} + +def off() { + log.debug "off()" + def cmds = [] + cmds << getAction("/off") + return cmds +} + +def refresh() { + log.debug "refresh()" + def cmds = [] + cmds << getAction("/status") + return cmds +} + +def ping() { + log.debug "ping()" + refresh() +} + +private getAction(uri){ + + log.debug uri + updateDNI() + + def userpass + + if(password != null && password != "") + userpass = encodeCredentials("admin", password) + + def headers = getHeader(userpass) + + def hubAction = new hubitat.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 hubitat.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" + } + log.debug "Device Network Id set to ${myDNI}" + 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() { + log.debug "reboot()" + 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 = 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}", + 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 (state.settings?."${cmd.name}" != null) + { + if (state.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 ?: [:] + + state.settings = settings + + def configuration = new XmlSlurper().parseText(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}" != null? 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 +} + +def configuration_model() +{ +''' + + + + + + + +Default: Off + + + + + + + +Automatically turn the switch off after this many seconds. +Range: 0 to 65536 +Default: 0 (Disabled) + + + + +In seconds +Range: 0 to 65536 +Default: 60 + + + + +In seconds +Range: 0 to 65536 +Default: 60 + + + + +In seconds +Range: 0 to 65536 +Default: 60 + + + + +Send uptime reports at this interval (in seconds). +Range: 0 (Disabled) to 65536 +Default: 300 + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/sonoff-th-wifi-switch.src/SonoffTH.ino.generic.bin b/Drivers/sonoff-th-wifi-switch.src/SonoffTH.ino.generic.bin new file mode 100644 index 0000000..18161e1 Binary files /dev/null and b/Drivers/sonoff-th-wifi-switch.src/SonoffTH.ino.generic.bin differ diff --git a/Drivers/sonoff-th-wifi-switch.src/sonoff-th-wifi-switch.groovy b/Drivers/sonoff-th-wifi-switch.src/sonoff-th-wifi-switch.groovy new file mode 100644 index 0000000..5120804 --- /dev/null +++ b/Drivers/sonoff-th-wifi-switch.src/sonoff-th-wifi-switch.groovy @@ -0,0 +1,558 @@ +/** + * Copyright 2016 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 TH Wifi Switch + * + * Author: Eric Maycock (erocm123) + * Date: 2017-06-14 + * + * 2017-06-14: Added option to set frequency of temperature and humidity reports. + * Added option to use an external connected switch in place of a temperature/humidity sensor. + * Added option to adjust uptime report frequency. Made uptime events not show up in "Recently" tab. + * Fixed bug that was making settings appear as if they were not synced even when they were. + * Fixed bug that prevented zero values (0) from being entered into preferences. + * + */ + +import groovy.json.JsonSlurper + +metadata { + definition (name: "Sonoff TH Wifi Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Sensor" + capability "Configuration" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + 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: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.on", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.off", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.off", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.on", nextState:"turningOn" + } + } + valueTile("temperature","device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature",label:'${currentValue}°', backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("humidity","device.humidity", inactiveLabel: false, width: 2, height: 2) { + state "humidity",label:'RH ${currentValue} %' + } + 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}' + } + + } + + main(["switch"]) + details(["switch", "temperature", "humidity", + "refresh","configure","reboot", + "ip", "uptime"]) +} + +def installed() { + log.debug "installed()" + configure() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) cmds +} + +def updated() +{ + logging("updated()", 1) + 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 (state.realTemperature != null) sendEvent(name:"temperature", value: getAdjustedTemp(state.realTemperature)) + if (state.realHumidity != null) sendEvent(name:"humidity", value: getAdjustedHumidity(state.realHumidity)) + 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"]) { + log.debug "Mac address of device found ${descMap["mac"]}" + 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 != "") { + //log.debug body + + if(body.startsWith("{") || body.startsWith("[")) { + def slurper = new JsonSlurper() + def result = slurper.parseText(body) + + log.debug "result: ${result}" + + if (result.containsKey("type")) { + if (result.type == "configuration") { + events << update_current_properties(result) + } + } + if (result.containsKey("power")) { + events << createEvent(name: "switch", value: result.power) + } + if (result.containsKey("uptime")) { + events << createEvent(name: 'uptime', value: result.uptime, displayed: false) + } + if (result.containsKey("temperature")) { + if (result.temperature != "nan") { + state.realTemperature = convertTemperatureIfNeeded(result.temperature.toFloat(), result.scale) + events << createEvent(name:"temperature", value:"${getAdjustedTemp(state.realTemperature)}", unit:"${location.temperatureScale}") + } else { + log.debug "The temperature sensor is reporting \"nan\"" + } + } + if (result.containsKey("humidity")) { + if (result.temperature != "nan") { + state.realHumidity = Math.round((result.humidity as Double) * 100) / 100 + events << createEvent(name: "humidity", value:"${getAdjustedHumidity(state.realHumidity)}", unit:"%") + } else { + log.debug "The humidity sensor is reporting \"nan\"" + } + } + } 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 +} + +private getAdjustedTemp(value) { + value = Math.round((value as Double) * 100) / 100 + + if (tempOffset) { + return value = value + Math.round(tempOffset * 100) /100 + } else { + return value + } + +} + +private getAdjustedHumidity(value) { + value = Math.round((value as Double) * 100) / 100 + + if (humidityOffset) { + return value = value + Math.round(humidityOffset * 100) /100 + } else { + return value + } + +} + +def configureInstant(ip, port, pos){ + return getAction("/config?haip=${ip}&haport=${port}&pos=${pos}") +} + + + +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("/on") + return cmds +} + +def off() { + log.debug "off()" + def cmds = [] + cmds << getAction("/off") + return cmds +} + +def refresh() { + log.debug "refresh()" + def cmds = [] + cmds << getAction("/status") + return cmds +} + +def ping() { + log.debug "ping()" + refresh() +} + +private getAction(uri){ + updateDNI() + + def userpass + + if(password != null && password != "") + userpass = encodeCredentials("admin", password) + + def headers = getHeader(userpass) + + def hubAction = new hubitat.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 hubitat.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" + } + log.debug "Device Network Id set to ${myDNI}" + 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() { + log.debug "reboot()" + 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 = 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}", + 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 (state.settings?."${cmd.name}" != null) + { + if (state.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 ?: [:] + + state.settings = settings + + def configuration = new XmlSlurper().parseText(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}" != null? 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 +} + +def configuration_model() +{ +''' + + + + + + + +Default: Off + + + + + + + +Automatically turn the switch off after this many seconds. +Range: 0 to 65536 +Default: 0 (Disabled) + + + + +Default: Disabled + + + + + + + + + +Send temperature reports at this interval (in seconds). +Range: 0 (Disabled) to 65536 +Default: 300 + + + + +Send humidity reports at this interval (in seconds). +Range: 0 (Disabled) to 65536 +Default: 300 + + + + +Send uptime reports at this interval (in seconds). +Range: 0 (Disabled) to 65536 +Default: 300 + + + + +Range: -99 to 99 +Default: 0 + + + + +Range: -50 to 50 +Default: 0 + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/sonoff-wifi-switch.src/ESPEasy_R120.zip b/Drivers/sonoff-wifi-switch.src/ESPEasy_R120.zip new file mode 100644 index 0000000..10cea0b Binary files /dev/null and b/Drivers/sonoff-wifi-switch.src/ESPEasy_R120.zip differ diff --git a/Drivers/sonoff-wifi-switch.src/Sonoff.ino b/Drivers/sonoff-wifi-switch.src/Sonoff.ino new file mode 100644 index 0000000..2c1ec0c --- /dev/null +++ b/Drivers/sonoff-wifi-switch.src/Sonoff.ino @@ -0,0 +1,3220 @@ +#include +#include //Local DNS Server used for redirecting all requests to the configuration portal +#include //Local WebServer used to serve the configuration portal +#include +#include +#include +ESP8266HTTPUpdateServer httpUpdater; +#include + +#define SONOFF +//#define SONOFF_TH +//#define SONOFF_S20 +//#define SONOFF_TOUCH //ESP8285 !!!!!!!!!! +//#define SONOFF_SV +//#define SONOFF_POW +//#define SONOFF_DUAL +//#define SONOFF_4CH //ESP8285 !!!!!!!!!! +//#define ECOPLUG + +#ifdef SONOFF_POW +#include "HLW8012.h" +#endif + +#if defined SONOFF_TH || defined SONOFF +#include +#include +#include +#endif + +String message = ""; + +String inputString = ""; // a string to hold incoming data +boolean stringComplete = false; // whether the string is complete +int currentStatus = LOW; + +boolean needUpdate1 = true; +boolean needUpdate2 = true; +boolean needUpdate3 = true; +boolean needUpdate4 = true; +boolean inAutoOff1 = false; +boolean inAutoOff2 = false; +boolean inAutoOff3 = false; +boolean inAutoOff4 = false; +boolean needReboot = false; +boolean shortPress = false; + +//stores if the switch was high before at all +int state1 = LOW; +int state2 = LOW; +int state3 = LOW; +int state4 = LOW; +int state_ext = LOW; +//stores the time each button went high or low +unsigned long current_high1; +unsigned long current_low1; +unsigned long current_high2; +unsigned long current_low2; +unsigned long current_high3; +unsigned long current_low3; +unsigned long current_high4; +unsigned long current_low4; +unsigned long current_high_ext; +unsigned long current_low_ext; + +#if defined SONOFF || defined ECOPLUG +const char * projectName = "Sonoff"; +String softwareVersion = "2.0.5"; +#endif +#ifdef SONOFF_S20 +const char * projectName = "Sonoff S20"; +String softwareVersion = "2.0.5"; +#endif +#ifdef SONOFF_TOUCH +const char * projectName = "Sonoff Touch"; +String softwareVersion = "2.0.5"; +#endif +#ifdef SONOFF_POW +const char * projectName = "Sonoff POW"; +String softwareVersion = "2.0.5"; +HLW8012 hlw8012; +#endif +#ifdef SONOFF_TH +const char * projectName = "Sonoff TH"; +String softwareVersion = "2.0.5"; +#endif +#ifdef SONOFF_DUAL +const char * projectName = "Sonoff Dual"; +String softwareVersion = "2.0.5"; +#endif +#ifdef SONOFF_4CH +const char * projectName = "Sonoff 4CH"; +String softwareVersion = "2.0.5"; +#endif + +const char compile_date[] = __DATE__ " " __TIME__; + +unsigned long connectionFailures; + +#define FLASH_EEPROM_SIZE 4096 +extern "C" { +#include "spi_flash.h" +} +extern "C" uint32_t _SPIFFS_start; +extern "C" uint32_t _SPIFFS_end; +extern "C" uint32_t _SPIFFS_page; +extern "C" uint32_t _SPIFFS_block; + +unsigned long failureTimeout = millis(); +long debounceDelay = 20; // the debounce time (in ms); increase if false positives +unsigned long timer1s = 0; +unsigned long timer5s = 0; +unsigned long timer5m = 0; +unsigned long timer1m = 0; +unsigned long timerW = 0; +unsigned long timerUptime; +unsigned long autoOffTimer1 = 0; +unsigned long autoOffTimer2 = 0; +unsigned long autoOffTimer3 = 0; +unsigned long autoOffTimer4 = 0; +unsigned long currentmillis = 0; + +#if defined SONOFF_TH || defined SONOFF +unsigned long timerT = 0; +unsigned long timerH = 0; +#endif + +#ifdef SONOFF_POW +unsigned long timerV = 0; +unsigned long timerA = 0; +unsigned long timerVA = 0; +unsigned long timerPF = 0; + +#define REL_PIN1 12 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define LED_PIN1 15 // GPIO 13 = Green Led (0 = On, 1 = Off) +#define KEY_PIN1 0 // GPIO 00 = Button +#define SEL_PIN 5 +#define CF1_PIN 13 +#define CF_PIN 14 + +#define CURRENT_MODE HIGH +#define CURRENT_RESISTOR 0.001 +#define VOLTAGE_RESISTOR_UPSTREAM ( 5 * 470000 ) // Real: 2280k +#define VOLTAGE_RESISTOR_DOWNSTREAM ( 1000 ) // Real 1.009k + +float W; +float V; +float A; +float VA; +float PF; + +// redirect interrupt to HLW library +void ICACHE_RAM_ATTR hlw8012_cf1_interrupt() { + hlw8012.cf1_interrupt(); +} +void ICACHE_RAM_ATTR hlw8012_cf_interrupt() { + hlw8012.cf_interrupt(); +} + +// Library expects an interrupt on both edges +void setInterrupts() { + attachInterrupt(CF1_PIN, hlw8012_cf1_interrupt, CHANGE); + attachInterrupt(CF_PIN, hlw8012_cf_interrupt, CHANGE); +} + +void calibrate(int voltage = 120) { + + // Let some time to register values + unsigned long timeout = millis(); + while ((millis() - timeout) < 10000) { + delay(1); + } + + //hlw8012.resetMultipliers(); + + // Calibrate using a 60W bulb (pure resistive) on a 230V line + //hlw8012.expectedActivePower(60.0); + //hlw8012.expectedVoltage(110.0); + //hlw8012.expectedCurrent(60.0 / 110.0); + + //hlw8012.expectedActivePower(53.0); + hlw8012.expectedVoltage(voltage); + //hlw8012.expectedCurrent(53.0 / 120.0); + + // Show corrected factors + //message += hlw8012.getCurrentMultiplier(); + //message += " "; + //message += hlw8012.getVoltageMultiplier(); + //message += " "; + //message += hlw8012.getPowerMultiplier(); + //Serial.println(); +} +#endif + +#if defined SONOFF || defined SONOFF_TOUCH || defined SONOFF_S20 || defined SONOFF_DUAL +#define REL_PIN1 12 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define LED_PIN1 13 // GPIO 13 = Green Led (0 = On, 1 = Off) +#define KEY_PIN1 0 // GPIO 00 = Button +#define EXT_PIN 14 // GPIO 14 = Fifth pin in flashing header +#endif + +#if defined SONOFF_4CH +#define REL_PIN1 12 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define KEY_PIN1 0 // GPIO 00 = Button +#define REL_PIN2 5 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define KEY_PIN2 9 // GPIO 00 = Button +#define REL_PIN3 4 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define KEY_PIN3 10 // GPIO 00 = Button +#define REL_PIN4 15 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define KEY_PIN4 14 // GPIO 00 = Button + +#define LED_PIN1 13 // GPIO 13 = Green Led (0 = On, 1 = Off) +#endif + +#if defined ECOPLUG +#define REL_PIN1 2 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define LED_PIN1 13 // GPIO 13 = Green Led (0 = On, 1 = Off) +#define KEY_PIN1 0 // GPIO 00 = Button +#define EXT_PIN 14 // GPIO 14 = Fifth pin in flashing header +#endif + +#ifdef SONOFF_TH +#define REL_PIN1 12 // GPIO 12 = Red Led and Relay (0 = Off, 1 = On) +#define LED_PIN1 13 // GPIO 13 = Green Led (0 = On, 1 = Off) +#define KEY_PIN1 0 // GPIO 00 = Button +#endif + +#if defined SONOFF || defined SONOFF_TH +#define EXT_PIN 14 +#define DS_PIN 14 +#define DHTTYPE DHT22 +DHT dht(EXT_PIN, DHTTYPE); +float temperature; +float humidity; + +OneWire oneWire(DS_PIN); +DallasTemperature ds18b20(&oneWire); + +double _dsTemperature = 0; + +double getDSTemperature() { + return _dsTemperature; +} + +void dsSetup() { + ds18b20.begin(); +} +#endif + +#if defined SONOFF || defined SONOFF_S20 || defined SONOFF_TOUCH || defined SONOFF_TH || defined ECOPLUG || defined SONOFF_DUAL || defined SONOFF_4CH +#define LEDoff1 digitalWrite(LED_PIN1,HIGH) +#define LEDon1 digitalWrite(LED_PIN1,LOW) +#else +#define LEDoff1 digitalWrite(LED_PIN1,LOW) +#define LEDon1 digitalWrite(LED_PIN1,HIGH) +#endif + +#if defined SONOFF +#define Relayoff1 {\ + if (Settings.currentState1) needUpdate1 = true; \ + digitalWrite(REL_PIN1,LOW); \ + Settings.currentState1 = false; \ + LEDoff1; \ +} +#define Relayon1 {\ + if (!Settings.currentState1) needUpdate1 = true; \ + digitalWrite(REL_PIN1,HIGH); \ + Settings.currentState1 = true; \ + LEDon1; \ +} +#elif defined SONOFF_DUAL +#define Relayoff1 {\ + byte byteValue; \ + if (Settings.currentState1) needUpdate1 = true; \ + if (Settings.currentState2) byteValue = 0x02; \ + else byteValue = 0x00; \ + Serial.flush(); \ + Serial.write(0xA0); \ + Serial.write(0x04); \ + Serial.write(byteValue); \ + Serial.write(0xA1); \ + Serial.flush(); \ + Settings.currentState1 = false; \ +} +#define Relayon1 {\ + byte byteValue; \ + if (!Settings.currentState1) needUpdate1 = true; \ + if (Settings.currentState2) byteValue = 0x03; \ + else byteValue = 0x01; \ + Serial.flush(); \ + Serial.write(0xA0); \ + Serial.write(0x04); \ + Serial.write(byteValue); \ + Serial.write(0xA1); \ + Serial.flush(); \ + Settings.currentState1 = true; \ +} +#define Relayoff2 {\ + byte byteValue; \ + if (Settings.currentState2) needUpdate2 = true; \ + if (Settings.currentState1) byteValue = 0x01; \ + else byteValue = 0x00; \ + Serial.flush(); \ + Serial.write(0xA0); \ + Serial.write(0x04); \ + Serial.write(byteValue); \ + Serial.write(0xA1); \ + Serial.flush(); \ + Settings.currentState2 = false; \ +} +#define Relayon2 {\ + byte byteValue; \ + if (!Settings.currentState2) needUpdate2 = true; \ + if (Settings.currentState1) byteValue = 0x03; \ + else byteValue = 0x02; \ + Serial.flush(); \ + Serial.write(0xA0); \ + Serial.write(0x04); \ + Serial.write(byteValue); \ + Serial.write(0xA1); \ + Serial.flush(); \ + Settings.currentState2 = true; \ +} +#elif defined SONOFF_4CH +#define Relayoff1 {\ + if (Settings.currentState1) needUpdate1 = true; \ + digitalWrite(REL_PIN1,LOW); \ + Settings.currentState1 = false; \ +} +#define Relayon1 {\ + if (!Settings.currentState1) needUpdate1 = true; \ + digitalWrite(REL_PIN1,HIGH); \ + Settings.currentState1 = true; \ +} +#define Relayoff2 {\ + if (Settings.currentState2) needUpdate2 = true; \ + digitalWrite(REL_PIN2,LOW); \ + Settings.currentState2 = false; \ +} +#define Relayon2 {\ + if (!Settings.currentState2) needUpdate2 = true; \ + digitalWrite(REL_PIN2,HIGH); \ + Settings.currentState2 = true; \ +} +#define Relayoff3 {\ + if (Settings.currentState3) needUpdate3 = true; \ + digitalWrite(REL_PIN3,LOW); \ + Settings.currentState3 = false; \ +} +#define Relayon3 {\ + if (!Settings.currentState3) needUpdate3 = true; \ + digitalWrite(REL_PIN3,HIGH); \ + Settings.currentState3 = true; \ +} +#define Relayoff4 {\ + if (Settings.currentState4) needUpdate4 = true; \ + digitalWrite(REL_PIN4,LOW); \ + Settings.currentState4 = false; \ +} +#define Relayon4 {\ + if (!Settings.currentState4) needUpdate4 = true; \ + digitalWrite(REL_PIN4,HIGH); \ + Settings.currentState4 = true; \ +} +#else +#define Relayoff1 {\ + if (Settings.currentState1) needUpdate1 = true; \ + digitalWrite(REL_PIN1,LOW); \ + Settings.currentState1 = false; \ +} +#define Relayon1 {\ + if (!Settings.currentState1) needUpdate1 = true; \ + digitalWrite(REL_PIN1,HIGH); \ + Settings.currentState1 = true; \ +} +#endif + +byte mac[6]; + +boolean WebLoggedIn = false; +int WebLoggedInTimer = 2; +String printWebString = ""; +boolean printToWeb = false; + +#define DEFAULT_HAIP "0.0.0.0" +#define DEFAULT_HAPORT 39500 +#define DEFAULT_RESETWIFI false +#define DEFAULT_POS 0 +#define DEFAULT_CURRENT STATE "" +#define DEFAULT_IP "0.0.0.0" +#define DEFAULT_GATEWAY "0.0.0.0" +#define DEFAULT_SUBNET "0.0.0.0" +#define DEFAULT_DNS "0.0.0.0" +#define DEFAULT_USE_STATIC false +#define DEFAULT_LONG_PRESS false +#define DEFAULT_REALLY_LONG_PRESS false +#define DEFAULT_USE_PASSWORD false +#define DEFAULT_USE_PASSWORD_CONTROL false +#define DEFAULT_PASSWORD "" +#define DEFAULT_PORT 80 +#define DEFAULT_SWITCH_TYPE 0 +#define DEFAULT_AUTO_OFF1 0 +#define DEFAULT_AUTO_OFF2 0 +#define DEFAULT_AUTO_OFF3 0 +#define DEFAULT_AUTO_OFF4 0 +#define DEFAULT_UREPORT 60 +#define DEFAULT_DEBOUNCE 20 +#define DEFAULT_HOSTNAME "" +#ifdef SONOFF_POW +#define DEFAULT_WREPORT 60 +#define DEFAULT_VREPORT 60 +#define DEFAULT_AREPORT 60 +#define DEFAULT_VAREPORT 120 +#define DEFAULT_PFREPORT 240 +#define DEFAULT_VOLTAGE 120 +#endif +#ifdef SONOFF_TH +#define DEFAULT_SENSOR_TYPE 0 +#define DEFAULT_USE_FAHRENHEIT true +#define DEFAULT_TREPORT 300 +#define DEFAULT_HREPORT 300 +#endif +#define DEFAULT_EXT_TYPE 0 + +struct SettingsStruct +{ + byte haIP[4]; + unsigned int haPort; + boolean resetWifi; + int powerOnState; + boolean currentState1; + byte IP[4]; + byte Gateway[4]; + byte Subnet[4]; + byte DNS[4]; + boolean useStatic; + boolean longPress; + boolean reallyLongPress; + boolean usePassword; + boolean usePasswordControl; +#if defined SONOFF || defined SONOFF_TOUCH || defined SONOFF_S20 || defined ECOPLUG + int usePort; + int switchType; + int autoOff1; + int uReport; + int debounce; + int externalType; + char hostName[26]; +#endif +#ifdef SONOFF_POW + int wReport; + int vReport; + int aReport; + int vaReport; + int pfReport; + int usePort; + int switchType; + int autoOff1; + int uReport; + int debounce; + int externalType; + char hostName[26]; + int voltage; +#endif +#ifdef SONOFF_TH + boolean useFahrenheit; + int usePort; + boolean settingsReboot; + int sensorType; + int switchType; + int autoOff1; + int uReport; + int tReport; + int hReport; + int debounce; + int externalType; + char hostName[26]; +#endif +#if defined SONOFF_DUAL + int usePort; + int switchType; + int autoOff1; + boolean currentState2; + int autoOff2; + int uReport; + int debounce; + int externalType; + char hostName[26]; +#endif +#if defined SONOFF_4CH + int usePort; + int switchType; + int autoOff1; + boolean currentState2; + int autoOff2; + boolean currentState3; + int autoOff3; + boolean currentState4; + int autoOff4; + int uReport; + int debounce; + int externalType; + char hostName[26]; +#endif +} Settings; + +struct SecurityStruct +{ + char Password[26]; + int settingsVersion; +} SecuritySettings; + + + +// Start WiFi Server +std::unique_ptr server; + +String padHex(String hex) { + if (hex.length() == 1) { + hex = "0" + hex; + } + return hex; +} + +void handleRoot() { + server->send(200, "application/json", "{\"message\":\"Sonoff Wifi Switch\"}"); +} + +void handleNotFound() { + String message = "File Not Found\n\n"; + message += "URI: "; + message += server->uri(); + message += "\nMethod: "; + message += (server->method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server->args(); + message += "\n"; + for (uint8_t i = 0; i < server->args(); i++) { + message += " " + server->argName(i) + ": " + server->arg(i) + "\n"; + } + server->send(404, "text/plain", message); +} + +void addHeader(boolean showMenu, String& str) +{ + boolean cssfile = false; + + str += F(""); + str += F(""); + str += projectName; + str += F(""); + + str += F(""); + + + str += F(""); + str += F("
"); + +} + +void addFooter(String& str) +{ + str += F("
smartlife.tech
"); +} + +void addMenu(String& str) +{ + str += F("Main"); + str += F("Advanced"); + str += F("Control"); + str += F("Firmware"); +} + +void addRebootBanner(String& str) +{ + if (needReboot == true) { + str += F("Reboot needed for changes to take effect. Reboot"); + } +} + +void relayControl(int relay, int value) { + switch (relay) + { + case 0: //All Switches + { + if (value == 0) { + Relayoff1; + #if defined SONOFF_DUAL + Relayoff2; + #endif + #if defined SONOFF_4CH + Relayoff2; + Relayoff3; + Relayoff4; + #endif + } + if (value == 1) { + if (!inAutoOff1) { + autoOffTimer1 = millis(); + inAutoOff1 = true; + } + Relayon1; + #if defined SONOFF_DUAL + if (!inAutoOff2) { + autoOffTimer2 = millis(); + inAutoOff2 = true; + } + Relayon2; + #endif + #if defined SONOFF_4CH + if (!inAutoOff2) { + autoOffTimer2 = millis(); + inAutoOff2 = true; + } + Relayon2; + if (!inAutoOff3) { + autoOffTimer3 = millis(); + inAutoOff3 = true; + } + Relayon3; + if (!inAutoOff4) { + autoOffTimer4 = millis(); + inAutoOff4 = true; + } + Relayon4; + #endif + } + if (value == 2) { + if (Settings.currentState1) { + Relayoff1; + } else { + if (!inAutoOff1) { + autoOffTimer1 = millis(); + inAutoOff1 = true; + } + Relayon1; + } + #if defined SONOFF_DUAL + if (Settings.currentState2) { + Relayoff2; + } else { + if (!inAutoOff2) { + autoOffTimer2 = millis(); + inAutoOff2 = true; + } + Relayon2; + } + #endif + #if defined SONOFF_4CH + if (Settings.currentState2) { + Relayoff2; + } else { + if (!inAutoOff2) { + autoOffTimer2 = millis(); + inAutoOff2 = true; + } + Relayon2; + } + if (Settings.currentState3) { + Relayoff3; + } else { + if (!inAutoOff3) { + autoOffTimer3 = millis(); + inAutoOff3 = true; + } + Relayon3; + } + if (Settings.currentState4) { + Relayoff4; + } else { + if (!inAutoOff4) { + autoOffTimer4 = millis(); + inAutoOff4 = true; + } + Relayon4; + } + #endif + } + break; + } + case 1: //Relay 1 + { + if (value == 0) { + Relayoff1; + #ifdef SONOFF + LEDoff1; + #endif + } + if (value == 1) { + if (!inAutoOff1) { + autoOffTimer1 = millis(); + inAutoOff1 = true; + } + Relayon1; + #ifdef SONOFF + LEDon1; + #endif + } + if (value == 2) { + if (Settings.currentState1) { + Relayoff1; + } else { + if (!inAutoOff1) { + autoOffTimer1 = millis(); + inAutoOff1 = true; + } + Relayon1; + } + } + break; + } + case 2: //Relay 2 + { + #if defined SONOFF_DUAL || defined SONOFF_4CH + if (value == 0) { + Relayoff2; + } + if (value == 1) { + if (!inAutoOff2) { + autoOffTimer2 = millis(); + inAutoOff2 = true; + } + Relayon2; + } + if (value == 2) { + if (Settings.currentState2) { + Relayoff2; + } else { + if (!inAutoOff2) { + autoOffTimer2 = millis(); + inAutoOff2 = true; + } + Relayon2; + } + } + #endif + break; + } + case 3: //Relay 3 + { + #if defined SONOFF_4CH + if (value == 0) { + Relayoff3; + } + if (value == 1) { + if (!inAutoOff3) { + autoOffTimer3 = millis(); + inAutoOff3 = true; + } + Relayon3; + } + if (value == 2) { + if (Settings.currentState3) { + Relayoff3; + } else { + if (!inAutoOff3) { + autoOffTimer3 = millis(); + inAutoOff3 = true; + } + Relayon3; + } + } + #endif + break; + } + case 4: //Relay 4 + { + #if defined SONOFF_4CH + if (value == 0) { + Relayoff4; + } + if (value == 1) { + if (!inAutoOff4) { + autoOffTimer4 = millis(); + inAutoOff4 = true; + } + Relayon4; + } + if (value == 2) { + if (Settings.currentState4) { + Relayoff4; + } else { + if (!inAutoOff4) { + autoOffTimer4 = millis(); + inAutoOff4 = true; + } + Relayon4; + } + } + #endif + break; + } + } + if (Settings.powerOnState == 2 || Settings.powerOnState == 3) + { + SaveSettings(); + } + +} + +void relayToggle1() { + int reading = digitalRead(KEY_PIN1); + if (reading == LOW) { + current_low1 = millis(); + state1 = LOW; + } + if (reading == HIGH && state1 == LOW) + { + current_high1 = millis(); + state1 = HIGH; + if ((current_high1 - current_low1) > (Settings.debounce? Settings.debounce : debounceDelay) && (current_high1 - current_low1) < 10000) + { + relayControl(1, 2); + shortPress = true; + } + else if ((current_high1 - current_low1) >= 10000 && (current_high1 - current_low1) < 20000) + { + Settings.longPress = true; + SaveSettings(); + ESP.restart(); + } + else if ((current_high1 - current_low1) >= 20000 && (current_high1 - current_low1) < 60000) + { + Settings.reallyLongPress = true; + SaveSettings(); + ESP.restart(); + } + } +} + +#ifdef SONOFF_4CH +void relayToggle2() { + int reading = digitalRead(KEY_PIN2); + if (reading == LOW) { + current_low2 = millis(); + state2 = LOW; + } + if (reading == HIGH && state2 == LOW) + { + current_high2 = millis(); + state2 = HIGH; + if ((current_high2 - current_low2) > (Settings.debounce? Settings.debounce : debounceDelay) && (current_high2 - current_low2) < 10000) + { + relayControl(2, 2); + } + else if ((current_high2 - current_low2) >= 10000 && (current_high2 - current_low2) < 20000) + { + + } + else if ((current_high2 - current_low2) >= 20000 && (current_high2 - current_low2) < 60000) + { + + } + } +} +void relayToggle3() { + int reading = digitalRead(KEY_PIN3); + if (reading == LOW) { + current_low3 = millis(); + state3 = LOW; + } + if (reading == HIGH && state3 == LOW) + { + current_high3 = millis(); + state3 = HIGH; + if ((current_high3 - current_low3) > (Settings.debounce? Settings.debounce : debounceDelay) && (current_high3 - current_low3) < 10000) + { + relayControl(3, 2); + } + else if ((current_high3 - current_low3) >= 10000 && (current_high3 - current_low3) < 20000) + { + + } + else if ((current_high3 - current_low3) >= 20000 && (current_high3 - current_low3) < 60000) + { + + } + } +} +void relayToggle4() { + int reading = digitalRead(KEY_PIN4); + if (reading == LOW) { + current_low4 = millis(); + state4 = LOW; + } + if (reading == HIGH && state4 == LOW) + { + current_high4 = millis(); + state4 = HIGH; + if ((current_high4 - current_low4) > (Settings.debounce? Settings.debounce : debounceDelay) && (current_high4 - current_low4) < 10000) + { + relayControl(4, 2); + } + else if ((current_high4 - current_low4) >= 10000 && (current_high4 - current_low4) < 20000) + { + + } + else if ((current_high4 - current_low4) >= 20000 && (current_high4 - current_low4) < 60000) + { + + } + } +} +#endif + +#if defined SONOFF || defined SONOFF_TH +void extRelayToggle() { + if (Settings.externalType > 0) { + int reading = digitalRead(EXT_PIN); + if (reading == LOW && state_ext == HIGH) { + current_low_ext = millis(); + state_ext = LOW; + if ((current_low_ext - current_high_ext) > (Settings.debounce? Settings.debounce : debounceDelay)) { + relayControl(1, 2); + } + } + if (reading == HIGH && state_ext == LOW) + { + current_high_ext = millis(); + state_ext = HIGH; + if ((current_high_ext - current_low_ext) > (Settings.debounce? Settings.debounce : debounceDelay)) + { + if (Settings.externalType == 4) { + relayControl(1, 2); + } + } + } + } else { + //External switch has been disabled + } +} +#endif + +const char * endString(int s, const char *input) { + int length = strlen(input); + if ( s > length ) s = length; + return const_cast(&input[length - s]); +} + +#ifdef SONOFF_POW +String getStatus() { + if (digitalRead(REL_PIN1) == 0) { + //return "{\"power\":\"off\", \"uptime\":\"" + uptime() + "\", \"W\":\"" + W + "\", \"V\":\"" + V + "\", \"A\":\"" + A + + "\", \"VA\":\"" + VA + + "\", \"PF\":\"" + PF + "\"}"; + return "{\"power\":\"off\", \"uptime\":\"" + uptime() + "\", \"W\":\"" + W + "\", \"V\":\"" + V + "\", \"A\":\"" + A + "\"}"; + } else { + //return "{\"power\":\"on\", \"uptime\":\"" + uptime() + "\", \"W\":\"" + W + "\", \"V\":\"" + V + "\", \"A\":\"" + A + + "\", \"VA\":\"" + VA + + "\", \"PF\":\"" + PF + "\"}"; + return "{\"power\":\"on\", \"uptime\":\"" + uptime() + "\", \"W\":\"" + W + "\", \"V\":\"" + V + "\", \"A\":\"" + A + "\"}"; + } +} +#elif defined SONOFF_TH +String getStatus() { + if (digitalRead(REL_PIN1) == 0) { + return "{\"power\":\"off\", \"uptime\":\"" + uptime() + "\", \"temperature\":\"" + temperature + "\", \"scale\":\"" + getTempScale() + "\", \"humidity\":\"" + humidity + "\"}"; + } else { + return "{\"power\":\"on\", \"uptime\":\"" + uptime() + "\", \"temperature\":\"" + temperature + "\", \"scale\":\"" + getTempScale() + "\", \"humidity\":\"" + humidity + "\"}"; + } +} +#else +String getStatus() { + if (digitalRead(REL_PIN1) == 0) { + return "{\"power\":\"off\", \"uptime\":\"" + uptime() + "\"}"; + } else { + return "{\"power\":\"on\", \"uptime\":\"" + uptime() + "\"}"; + } +} +#endif + +#ifdef SONOFF_POW +void checkPower() { + W = hlw8012.getActivePower(); + W = hlw8012.getActivePower(); + VA = hlw8012.getApparentPower(); +} + +void checkVoltage() { + V = hlw8012.getVoltage(); +} + +void checkCurrent() { + A = hlw8012.getCurrent(); +} + +void checkPowerFactor() { + PF = (int) (100 * hlw8012.getPowerFactor()); +} +#endif + +#ifdef SONOFF_TH +const char* getTempScale() { + if (Settings.useFahrenheit == true) { + return "F"; + } else { + return "C"; + } +} + +void checkTempAndHumidity() { + // Reading temperature or humidity takes about 250 milliseconds! + // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor) + + if (Settings.externalType == 2) { + ds18b20.requestTemperatures(); + temperature = (!Settings.useFahrenheit) ? ds18b20.getTempCByIndex(0) : ds18b20.getTempFByIndex(0); + } else { + humidity = dht.readHumidity(); + temperature = (!Settings.useFahrenheit) ? dht.readTemperature() : dht.readTemperature(true); + } + + // Check if any reads failed and exit early (to try again). + if (isnan(humidity) || isnan(temperature)) { + //Serial.println("Failed to read from DHT sensor!"); + return; + } +} +#endif + +/*********************************************************************************************\ + Tasks each 5 seconds + \*********************************************************************************************/ +void runEach1Seconds() +{ + timer1s = millis() + 1000; +} + + +/*********************************************************************************************\ + Tasks each 5 seconds + \*********************************************************************************************/ +void runEach5Seconds() +{ + timer5s = millis() + 5000; +#ifdef SONOFF_POW + checkPower(); + checkVoltage(); + checkCurrent(); + //checkPowerFactor(); +#elif defined SONOFF_TH + checkTempAndHumidity(); +#endif +} + +/*********************************************************************************************\ + Tasks each 1 minutes + \*********************************************************************************************/ +void runEach1Minutes() +{ + timer1m = millis() + 60000; + + if (SecuritySettings.Password[0] != 0) + { + if (WebLoggedIn) + WebLoggedInTimer++; + if (WebLoggedInTimer > 2) + WebLoggedIn = false; + } +} + +/*********************************************************************************************\ + Tasks each 5 minutes + \*********************************************************************************************/ +void runEach5Minutes() +{ + timer5m = millis() + 300000; + + //sendStatus(99); + +} + +boolean sendStatus(int number) { + String authHeader = ""; + boolean success = false; + String message = ""; + char host[20]; + sprintf_P(host, PSTR("%u.%u.%u.%u"), Settings.haIP[0], Settings.haIP[1], Settings.haIP[2], Settings.haIP[3]); + + //client.setTimeout(1000); + if (Settings.haIP[0] + Settings.haIP[1] + Settings.haIP[2] + Settings.haIP[3] == 0) { // HA host is not configured + return false; + } + if (connectionFailures >= 3) { // Too many errors; Trying not to get stuck + if (millis() - failureTimeout < 1800000) { + return false; + } else { + failureTimeout = millis(); + } + } + // Use WiFiClient class to create TCP connections + WiFiClient client; + if (!client.connect(host, Settings.haPort)) + { + connectionFailures++; + return false; + } + if (connectionFailures) + connectionFailures = 0; + + switch(number){ + case 0: { + #if defined SONOFF_DUAL + message = "{\"type\":\"relay\", \"number\":\"0\", \"power\":\"" + String(Settings.currentState1 == true || Settings.currentState2 == true? "on" : "off") + "\"}"; + #elif defined SONOFF_4CH + message = "{\"type\":\"relay\", \"number\":\"0\", \"power\":\"" + String(Settings.currentState1 == true || Settings.currentState2 == true || Settings.currentState3 == true || Settings.currentState4 == true? "on" : "off") + "\"}"; + #else + message = "{\"type\":\"relay\", \"number\":\"0\", \"power\":\"" + String(Settings.currentState1 == true? "on" : "off") + "\"}"; + #endif + break; + } + case 1: { + message = "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(Settings.currentState1 == true? "on" : "off") + "\"}"; + break; + } + #if defined SONOFF_DUAL || defined SONOFF_4CH + case 2: { + message = "{\"type\":\"relay\", \"number\":\"2\", \"power\":\"" + String(Settings.currentState2 == true? "on" : "off") + "\"}"; + break; + } + #endif + #if defined SONOFF_4CH + case 3: { + message = "{\"type\":\"relay\", \"number\":\"3\", \"power\":\"" + String(Settings.currentState3 == true? "on" : "off") + "\"}"; + break; + } + case 4: { + message = "{\"type\":\"relay\", \"number\":\"4\", \"power\":\"" + String(Settings.currentState4 == true? "on" : "off") + "\"}"; + break; + } + #endif + case 99: { + message = "{\"uptime\":\"" + uptime() + "\"}"; + break; + } + } + + // We now create a URI for the request + String url = F("/"); + //url += event->idx; + + client.print(String("POST ") + url + " HTTP/1.1\r\n" + + "Host: " + host + ":" + Settings.haPort + "\r\n" + authHeader + + "Content-Type: application/json;charset=utf-8\r\n" + + "Server: " + projectName + "\r\n" + + "Connection: close\r\n\r\n" + + message + "\r\n"); + + unsigned long timer = millis() + 200; + while (!client.available() && millis() < timer) + delay(1); + + // Read all the lines of the reply from server and print them to Serial + while (client.available()) { + String line = client.readStringUntil('\n'); + if (line.substring(0, 15) == "HTTP/1.1 200 OK") + { + success = true; + } + delay(1); + } + + client.flush(); + client.stop(); + + return success; +} + +boolean sendReport(int number) { + String authHeader = ""; + boolean success = false; + char host[20]; + const char* report; + float value; + +#ifdef SONOFF_POW + switch (number) + { + case 1: //W Report + { + report = "W"; + value = W; + timerW = millis() + Settings.wReport * 1000; + break; + } + case 2: //V Report + { + report = "V"; + value = V; + timerV = millis() + Settings.vReport * 1000; + break; + } + case 3: //A Report + { + report = "A"; + value = A; + timerA = millis() + Settings.aReport * 1000; + break; + } case 4: //VA Report + { + report = "VA"; + value = VA; + timerVA = millis() + Settings.vaReport * 1000; + break; + } case 5: //PF Report + { + report = "PF"; + value = PF; + timerPF = millis() + Settings.pfReport * 1000; + break; + } + default : //Optional + { + + } + } +#endif + +#ifdef SONOFF_TH + switch (number) + { + case 1: //T Report + { + report = "temperature"; + value = temperature; + timerT = millis() + Settings.tReport * 1000; + break; + } + case 2: //H Report + { + report = "humidity"; + value = humidity; + timerH = millis() + Settings.hReport * 1000; + break; + } + default : //Optional + { + + } + } +#endif + + sprintf_P(host, PSTR("%u.%u.%u.%u"), Settings.haIP[0], Settings.haIP[1], Settings.haIP[2], Settings.haIP[3]); + + //client.setTimeout(1000); + if (Settings.haIP[0] + Settings.haIP[1] + Settings.haIP[2] + Settings.haIP[3] == 0) { // HA host is not configured + return false; + } + if (connectionFailures >= 3) { // Too many errors; Trying not to get stuck + if (millis() - failureTimeout < 1800000) { + return false; + } else { + failureTimeout = millis(); + } + } + // Use WiFiClient class to create TCP connections + WiFiClient client; + if (!client.connect(host, Settings.haPort)) + { + connectionFailures++; + return false; + } + if (connectionFailures) + connectionFailures = 0; + + // We now create a URI for the request + String url = F("/"); + //url += event->idx; +#ifdef SONOFF_TH + client.print(String("POST ") + url + " HTTP/1.1\r\n" + + "Host: " + host + ":" + Settings.haPort + "\r\n" + authHeader + + "Content-Type: application/json;charset=utf-8\r\n" + + "Server: " + projectName + "\r\n" + + "Connection: close\r\n\r\n" + + "{\"" + report + "\":\"" + value + (report == "temperature"? "\", \"scale\":\"" + String(getTempScale()) : "") + "\"}" + "\r\n"); +#endif + +#if not defined SONOFF_TH + client.print(String("POST ") + url + " HTTP/1.1\r\n" + + "Host: " + host + ":" + Settings.haPort + "\r\n" + authHeader + + "Content-Type: application/json;charset=utf-8\r\n" + + "Server: " + projectName + "\r\n" + + "Connection: close\r\n\r\n" + + "{\"" + report + "\":\"" + value + "\"}" + "\r\n"); +#endif + + unsigned long timer = millis() + 200; + while (!client.available() && millis() < timer) + delay(1); + + // Read all the lines of the reply from server and print them to Serial + while (client.available()) { + String line = client.readStringUntil('\n'); + if (line.substring(0, 15) == "HTTP/1.1 200 OK") + { + success = true; + } + delay(1); + } + + client.flush(); + client.stop(); + + return success; +} + + +/********************************************************************************************\ + Convert a char string to IP byte array + \*********************************************************************************************/ +boolean str2ip(char *string, byte* IP) +{ + byte c; + byte part = 0; + int value = 0; + + for (int x = 0; x <= strlen(string); x++) + { + c = string[x]; + if (isdigit(c)) + { + value *= 10; + value += c - '0'; + } + + else if (c == '.' || c == 0) // next octet from IP address + { + if (value <= 255) + IP[part++] = value; + else + return false; + value = 0; + } + else if (c == ' ') // ignore these + ; + else // invalid token + return false; + } + if (part == 4) // correct number of octets + return true; + return false; +} + +String deblank(const char* input) +{ + String output = String(input); + output.replace(" ", ""); + return output; +} + +void SaveSettings(void) +{ + SaveToFlash(0, (byte*)&Settings, sizeof(struct SettingsStruct)); + SaveToFlash(32768, (byte*)&SecuritySettings, sizeof(struct SecurityStruct)); +} + +boolean LoadSettings() +{ + LoadFromFlash(0, (byte*)&Settings, sizeof(struct SettingsStruct)); + LoadFromFlash(32768, (byte*)&SecuritySettings, sizeof(struct SecurityStruct)); +} + +/********************************************************************************************\ + Save data to flash + \*********************************************************************************************/ +void SaveToFlash(int index, byte* memAddress, int datasize) +{ + if (index > 33791) // Limit usable flash area to 32+1k size + { + return; + } + uint32_t _sector = ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint8_t* data = new uint8_t[FLASH_EEPROM_SIZE]; + int sectorOffset = index / SPI_FLASH_SEC_SIZE; + int sectorIndex = index % SPI_FLASH_SEC_SIZE; + uint8_t* dataIndex = data + sectorIndex; + _sector += sectorOffset; + + // load entire sector from flash into memory + noInterrupts(); + spi_flash_read(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE); + interrupts(); + + // store struct into this block + memcpy(dataIndex, memAddress, datasize); + + noInterrupts(); + // write sector back to flash + if (spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) + if (spi_flash_write(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE) == SPI_FLASH_RESULT_OK) + { + //Serial.println("flash save ok"); + } + interrupts(); + delete [] data; + //String log = F("FLASH: Settings saved"); + //addLog(LOG_LEVEL_INFO, log); +} + +/********************************************************************************************\ + Load data from flash + \*********************************************************************************************/ +void LoadFromFlash(int index, byte* memAddress, int datasize) +{ + uint32_t _sector = ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint8_t* data = new uint8_t[FLASH_EEPROM_SIZE]; + int sectorOffset = index / SPI_FLASH_SEC_SIZE; + int sectorIndex = index % SPI_FLASH_SEC_SIZE; + uint8_t* dataIndex = data + sectorIndex; + _sector += sectorOffset; + + // load entire sector from flash into memory + noInterrupts(); + spi_flash_read(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE); + interrupts(); + + // load struct from this block + memcpy(memAddress, dataIndex, datasize); + delete [] data; +} + +void EraseFlash() +{ + uint32_t _sectorStart = (ESP.getSketchSize() / SPI_FLASH_SEC_SIZE) + 1; + uint32_t _sectorEnd = _sectorStart + (ESP.getFlashChipRealSize() / SPI_FLASH_SEC_SIZE); + + for (uint32_t _sector = _sectorStart; _sector < _sectorEnd; _sector++) + { + noInterrupts(); + if (spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) + { + interrupts(); + //Serial.print(F("FLASH: Erase Sector: ")); + //Serial.println(_sector); + delay(10); + } + interrupts(); + } +} + +void ZeroFillFlash() +{ + // this will fill the SPIFFS area with a 64k block of all zeroes. + uint32_t _sectorStart = ((uint32_t)&_SPIFFS_start - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint32_t _sectorEnd = _sectorStart + 16 ; //((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE; + uint8_t* data = new uint8_t[FLASH_EEPROM_SIZE]; + + uint8_t* tmpdata = data; + for (int x = 0; x < FLASH_EEPROM_SIZE; x++) + { + *tmpdata = 0; + tmpdata++; + } + + + for (uint32_t _sector = _sectorStart; _sector < _sectorEnd; _sector++) + { + // write sector to flash + noInterrupts(); + if (spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) + if (spi_flash_write(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast(data), FLASH_EEPROM_SIZE) == SPI_FLASH_RESULT_OK) + { + interrupts(); + //Serial.print(F("FLASH: Zero Fill Sector: ")); + //Serial.println(_sector); + delay(10); + } + } + interrupts(); + delete [] data; +} + +String uptime() +{ + currentmillis = millis(); + long days = 0; + long hours = 0; + long mins = 0; + long secs = 0; + secs = currentmillis / 1000; //convect milliseconds to seconds + mins = secs / 60; //convert seconds to minutes + hours = mins / 60; //convert minutes to hours + days = hours / 24; //convert hours to days + secs = secs - (mins * 60); //subtract the coverted seconds to minutes in order to display 59 secs max + mins = mins - (hours * 60); //subtract the coverted minutes to hours in order to display 59 minutes max + hours = hours - (days * 24); //subtract the coverted hours to days in order to display 23 hours max + + if (days > 0) // days will displayed only if value is greater than zero + { + return String(days) + " days and " + String(hours) + ":" + String(mins) + ":" + String(secs); + } else { + return String(hours) + ":" + String(mins) + ":" + String(secs); + } +} + +void setup() +{ + pinMode(KEY_PIN1, INPUT_PULLUP); + attachInterrupt(KEY_PIN1, relayToggle1, CHANGE); + pinMode(REL_PIN1, OUTPUT); + + #ifdef SONOFF_POW + //relayControl(1,1); + #endif + + + LoadSettings(); + +#ifdef SONOFF_POW + hlw8012.begin(CF_PIN, CF1_PIN, SEL_PIN, CURRENT_MODE, true); + hlw8012.setResistors(CURRENT_RESISTOR, VOLTAGE_RESISTOR_UPSTREAM, VOLTAGE_RESISTOR_DOWNSTREAM); + setInterrupts(); +#endif +#if defined SONOFF_TH || defined SONOFF + + if (Settings.externalType == 3 || Settings.externalType == 4) { + pinMode(EXT_PIN, INPUT_PULLUP); + attachInterrupt(EXT_PIN, extRelayToggle, CHANGE); + } else if (Settings.externalType == 2) { + dsSetup(); + } else if (Settings.externalType == 1){ + dht.begin(); + } +#endif +#ifdef SONOFF_4CH + pinMode(KEY_PIN2, INPUT_PULLUP); + attachInterrupt(KEY_PIN2, relayToggle2, CHANGE); + pinMode(REL_PIN2, OUTPUT); + pinMode(KEY_PIN3, INPUT_PULLUP); + attachInterrupt(KEY_PIN3, relayToggle3, CHANGE); + pinMode(REL_PIN3, OUTPUT); + pinMode(KEY_PIN4, INPUT_PULLUP); + attachInterrupt(KEY_PIN4, relayToggle4, CHANGE); + pinMode(REL_PIN4, OUTPUT); +#endif + + pinMode(LED_PIN1, OUTPUT); + + // Setup console + Serial.begin(19200); + delay(10); + //Serial1.println(); + //Serial1.println(); + + + #ifdef SONOFF_POW + calibrate(Settings.voltage); + #endif + + if (Settings.longPress == true) { + for (uint8_t i = 0; i < 3; i++) { + LEDoff1; + delay(250); + LEDon1; + delay(250); + } + Settings.longPress = false; + Settings.useStatic = false; + Settings.resetWifi = true; + SaveSettings(); + LEDoff1; + } + //Settings.reallyLongPress = true; + if (Settings.reallyLongPress == true) { + for (uint8_t i = 0; i < 5; i++) { + LEDoff1; + delay(1000); + LEDon1; + delay(1000); + } + EraseFlash(); + ZeroFillFlash(); + ESP.restart(); + } + + switch (Settings.powerOnState) + { + case 0: //Switch Off on Boot + { + relayControl(0, 0); + break; + } + case 1: //Switch On on Boot + { + relayControl(0, 1); + break; + } + case 2: //Saved State on Boot + { + if (Settings.currentState1) relayControl(1, 1); + else relayControl(1, 0); + #if defined SONOFF_DUAL || defined SONOFF_4CH + if (Settings.currentState2) relayControl(2, 1); + else relayControl(2, 0); + #endif + #if defined SONOFF_4CH + if (Settings.currentState3) relayControl(3, 1); + else relayControl(3, 0); + if (Settings.currentState4) relayControl(4, 1); + else relayControl(4, 0); + #endif + break; + } + case 3: //Opposite Saved State on Boot + { + if (!Settings.currentState1) relayControl(1, 1); + else relayControl(1, 0); + #if defined SONOFF_DUAL || defined SONOFF_4CH + if (!Settings.currentState2) relayControl(2, 1); + else relayControl(2, 0); + #endif + #if defined SONOFF_4CH + if (!Settings.currentState3) relayControl(3, 1); + else relayControl(3, 0); + if (!Settings.currentState4) relayControl(4, 1); + else relayControl(4, 0); + #endif + break; + } + default : //Optional + { + relayControl(0, 0); + } + } + + boolean saveSettings = false; + + if (SecuritySettings.settingsVersion < 200) { + str2ip((char*)DEFAULT_HAIP, Settings.haIP); + + Settings.haPort = DEFAULT_HAPORT; + + Settings.resetWifi = DEFAULT_RESETWIFI; + + Settings.powerOnState = DEFAULT_POS; + + str2ip((char*)DEFAULT_IP, Settings.IP); + str2ip((char*)DEFAULT_SUBNET, Settings.Subnet); + + str2ip((char*)DEFAULT_GATEWAY, Settings.Gateway); + + Settings.useStatic = DEFAULT_USE_STATIC; + + Settings.usePassword = DEFAULT_USE_PASSWORD; + + Settings.usePasswordControl = DEFAULT_USE_PASSWORD_CONTROL; + + Settings.longPress = DEFAULT_LONG_PRESS; + Settings.reallyLongPress = DEFAULT_REALLY_LONG_PRESS; + + strncpy(SecuritySettings.Password, DEFAULT_PASSWORD, sizeof(SecuritySettings.Password)); + + SecuritySettings.settingsVersion = 200; + + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 201) { +#ifdef SONOFF_POW + Settings.wReport = DEFAULT_WREPORT; + Settings.vReport = DEFAULT_VREPORT; + Settings.aReport = DEFAULT_AREPORT; + Settings.vaReport = DEFAULT_VAREPORT; + Settings.pfReport = DEFAULT_PFREPORT; +#endif +#ifdef SONOFF_TH + Settings.sensorType = DEFAULT_SENSOR_TYPE; + Settings.useFahrenheit = DEFAULT_USE_FAHRENHEIT; +#endif + SecuritySettings.settingsVersion = 201; + + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 202) { + Settings.usePort = DEFAULT_PORT; + SecuritySettings.settingsVersion = 202; + + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 203) { + Settings.switchType = DEFAULT_SWITCH_TYPE; + Settings.autoOff1 = DEFAULT_AUTO_OFF1; + #if defined SONOFF_DUAL || defined SONOFF_4CH + Settings.autoOff2 = DEFAULT_AUTO_OFF2; + #endif + #if defined SONOFF_4CH + Settings.autoOff3 = DEFAULT_AUTO_OFF3; + Settings.autoOff4 = DEFAULT_AUTO_OFF4; + #endif + SecuritySettings.settingsVersion = 203; + + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 204) { + Settings.uReport = DEFAULT_UREPORT; + SecuritySettings.settingsVersion = 204; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 205) { +#ifdef SONOFF_TH + Settings.tReport = DEFAULT_TREPORT; + Settings.hReport = DEFAULT_HREPORT; + if (Settings.sensorType == 0) Settings.externalType = 1; + if (Settings.sensorType == 1) Settings.externalType = 2; +#endif +#if defined SONOFF + if (Settings.switchType == 0) Settings.externalType = 3; + if (Settings.switchType == 1) Settings.externalType = 4; +#endif + Settings.debounce = DEFAULT_DEBOUNCE; + SecuritySettings.settingsVersion = 205; + saveSettings = true; + } + + if (SecuritySettings.settingsVersion < 206) { + strncpy(Settings.hostName, DEFAULT_HOSTNAME, sizeof(Settings.hostName)); + SecuritySettings.settingsVersion = 206; + saveSettings = true; + } + + if (saveSettings == true) { + SaveSettings(); + } + + WiFiManager wifiManager; + + wifiManager.setConnectTimeout(30); + wifiManager.setConfigPortalTimeout(300); + + if (Settings.useStatic == true) { + wifiManager.setSTAStaticIPConfig(Settings.IP, Settings.Gateway, Settings.Subnet); + } + + if (Settings.hostName[0] != 0) { + wifiManager.setHostName(Settings.hostName); + } + + if (Settings.resetWifi == true) { + wifiManager.resetSettings(); + Settings.resetWifi = false; + SaveSettings(); + } + + WiFi.macAddress(mac); + + String apSSID = deblank(projectName) + "." + String(mac[0], HEX) + String(mac[1], HEX) + String(mac[2], HEX) + String(mac[3], HEX) + String(mac[4], HEX) + String(mac[5], HEX); + + if (!wifiManager.autoConnect(apSSID.c_str(), "configme")) { + //Serial.println("failed to connect, we should reset as see if it connects"); + delay(3000); + ESP.reset(); + delay(5000); + } + +#if not defined SONOFF + LEDon1; +#endif + + //Serial1.println(""); + + if (Settings.usePort > 0 && Settings.usePort < 65535) { + server.reset(new ESP8266WebServer(WiFi.localIP(), Settings.usePort)); + } else { + server.reset(new ESP8266WebServer(WiFi.localIP(), 80)); + } + + //server->on("/", handleRoot); + + server->on("/description.xml", HTTP_GET, []() { + SSDP.schema(server->client()); + }); + + server->on("/reboot", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + server->send(200, "application/json", "{\"message\":\"device is rebooting\"}"); + Relayoff1; + LEDoff1; + delay(2000); + ESP.restart(); + }); + + server->on("/r", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + server->send(200, "text/html", "Device is rebooting..."); + Relayoff1; + LEDoff1; + delay(2000); + ESP.restart(); + }); + + server->on("/reset", []() { + server->send(200, "application/json", "{\"message\":\"wifi settings are being removed\"}"); + Settings.reallyLongPress = true; + SaveSettings(); + ESP.restart(); + }); + + server->on("/status", []() { + server->send(200, "application/json", getStatus()); + }); + + server->on("/info", []() { + server->send(200, "application/json", "{\"version\":\"" + softwareVersion + "\", \"date\":\"" + compile_date + "\", \"mac\":\"" + padHex(String(mac[0], HEX)) + padHex(String(mac[1], HEX)) + padHex(String(mac[2], HEX)) + padHex(String(mac[3], HEX)) + padHex(String(mac[4], HEX)) + padHex(String(mac[5], HEX)) + "\"}"); + }); + + server->on("/advanced", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePassword == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + char tmpString[64]; + String haIP = server->arg("haip"); + String haPort = server->arg("haport"); + String powerOnState = server->arg("pos"); + String ip = server->arg("ip"); + String gateway = server->arg("gateway"); + String subnet = server->arg("subnet"); + String dns = server->arg("dns"); + String usestatic = server->arg("usestatic"); + String usepassword = server->arg("usepassword"); + String usepasswordcontrol = server->arg("usepasswordcontrol"); + String username = server->arg("username"); + String password = server->arg("password"); + String port = server->arg("port"); + String autoOff1 = server->arg("autooff1"); + String uReport = server->arg("ureport"); + String debounce = server->arg("debounce"); + String hostname = server->arg("hostname"); + #if defined SONOFF || defined SONOFF_TH + String switchType = server->arg("switchtype"); + String externalType = server->arg("externaltype"); + #endif + #ifdef SONOFF_POW + String wReport = server->arg("wreport"); + String vReport = server->arg("vreport"); + String aReport = server->arg("areport"); + String voltage = server->arg("voltage"); + #endif + #ifdef SONOFF_TH + String useFahrenheit = server->arg("fahrenheit"); + String sensorType = server->arg("sensortype"); + String tReport = server->arg("treport"); + String hReport = server->arg("hreport"); + #endif + #ifdef SONOFF_DUAL + String autoOff2 = server->arg("autooff2"); + #endif + #ifdef SONOFF_4CH + String autoOff2 = server->arg("autooff2"); + String autoOff3 = server->arg("autooff3"); + String autoOff4 = server->arg("autooff4"); + #endif + + if(server->args() > 0){ + if (haPort.length() != 0) + { + Settings.haPort = haPort.toInt(); + } + if (powerOnState.length() != 0) + { + Settings.powerOnState = powerOnState.toInt(); + } + if (haIP.length() != 0) + { + haIP.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.haIP); + } + + if (ip.length() != 0 && subnet.length() != 0) + { + if (ip != String(Settings.IP[0]) + "." + String(Settings.IP[1]) + "." + String(Settings.IP[2]) + "." + String(Settings.IP[3]) && Settings.useStatic) needReboot = true; + ip.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.IP); + if (subnet != String(Settings.Subnet[0]) + "." + String(Settings.Subnet[1]) + "." + String(Settings.Subnet[2]) + "." + String(Settings.Subnet[3]) && Settings.useStatic) needReboot = true; + subnet.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.Subnet); + } + if (gateway.length() != 0) + { + gateway.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.Gateway); + } + if (dns.length() != 0) + { + dns.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.DNS); + } + if (usestatic.length() != 0) + { + if ((usestatic == "yes") != Settings.useStatic) needReboot = true; + Settings.useStatic = (usestatic == "yes"); + } + if (usepassword.length() != 0) + { + if ((usepassword == "yes") != Settings.usePassword) needReboot = true; + Settings.usePassword = (usepassword == "yes"); + } + if (usepasswordcontrol.length() != 0) + { + Settings.usePasswordControl = (usepasswordcontrol == "yes"); + } + if(password != SecuritySettings.Password && Settings.usePassword) needReboot = true; + strncpy(SecuritySettings.Password, password.c_str(), sizeof(SecuritySettings.Password)); + if (port.length() != 0) + { + //if(port.toInt() != Settings.usePort) needReboot = true; + Settings.usePort = port.toInt(); + } + if (autoOff1.length() != 0) + { + Settings.autoOff1 = autoOff1.toInt(); + } + if (uReport.length() != 0) + { + if (uReport.toInt() != Settings.uReport) { + Settings.uReport = uReport.toInt(); + timerUptime = millis() + Settings.uReport * 1000; + } + } + if (debounce.length() != 0) + { + Settings.debounce = debounce.toInt(); + } + if (hostname != Settings.hostName) needReboot = true; + WiFi.hostname(hostname.c_str()); + strncpy(Settings.hostName, hostname.c_str(), sizeof(Settings.hostName)); + #if defined SONOFF || defined SONOFF_TH + if (externalType.length() != 0) + { + if(externalType.toInt() != Settings.externalType) needReboot = true; + Settings.externalType = externalType.toInt(); + } + #endif + #ifdef SONOFF_POW + if (wReport.length() != 0) + { + if (wReport.toInt() != Settings.wReport) { + Settings.wReport = wReport.toInt(); + timerW = millis() + Settings.wReport * 1000; + } + } + if (vReport.length() != 0) + { + if (vReport.toInt() != Settings.vReport) { + Settings.vReport = vReport.toInt(); + timerV = millis() + Settings.vReport * 1000; + } + } + if (aReport.length() != 0) + { + if (aReport.toInt() != Settings.aReport) { + Settings.aReport = aReport.toInt(); + timerA = millis() + Settings.aReport * 1000; + } + } + if (voltage.length() != 0) + { + if(voltage.toInt() != Settings.voltage) needReboot = true; + Settings.voltage = voltage.toInt(); + } + #endif + #ifdef SONOFF_TH + if (useFahrenheit.length() != 0) + { + Settings.useFahrenheit = (useFahrenheit == "true"); + needUpdate1 = true; + } + if (sensorType.length() != 0) + { + if(sensorType.toInt() != Settings.sensorType) needReboot = true; + Settings.sensorType = sensorType.toInt(); + } + if (tReport.length() != 0) + { + if (tReport.toInt() != Settings.tReport) { + Settings.tReport = tReport.toInt(); + timerT = millis() + Settings.tReport * 1000; + } + } + if (hReport.length() != 0) + { + if (hReport.toInt() != Settings.hReport) { + Settings.hReport = hReport.toInt(); + timerH = millis() + Settings.hReport * 1000; + } + } + #endif + #ifdef SONOFF_DUAL + if (autoOff2.length() != 0) + { + Settings.autoOff2 = autoOff2.toInt(); + } + #endif + #ifdef SONOFF_4CH + if (autoOff2.length() != 0) + { + Settings.autoOff2 = autoOff2.toInt(); + } + if (autoOff3.length() != 0) + { + Settings.autoOff3 = autoOff3.toInt(); + } + if (autoOff4.length() != 0) + { + Settings.autoOff4 = autoOff4.toInt(); + } + #endif + } + + SaveSettings(); + + String reply = ""; + char str[20]; + addHeader(true, reply); + + reply += F(""); + + reply += F("
"); + reply += F("
"); + reply += projectName; + reply += F(" Settings"); + reply += F("
"); + addMenu(reply); + addRebootBanner(reply); + + reply += F("
Host Name:"); + + reply += F("
Password Protect

Configuration:


"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + reply += F("
Control:"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + reply += F("
\"admin\" Password: Show"); + + reply += F(""); + + reply += F("
Static IP:"); + + reply += F("Yes"); + reply += F(""); + + reply += F("No"); + reply += F(""); + + + reply += F("
IP:
Subnet:
Gateway:
DNS:
HA Controller IP:
HA Controller Port:
Boot Up State:"); + #if not defined SONOFF_DUAL || not defined SONOFF_4CH + reply += F("
Auto Off:"); + #else + reply += F("
Auto Off Relay 1:"); + #endif + #ifdef SONOFF_DUAL + reply += F("
Auto Off Relay 2:"); + #endif + #ifdef SONOFF_4CH + reply += F("
Auto Off Relay 2:"); + reply += F("
Auto Off Relay 3:"); + reply += F("
Auto Off Relay 4:"); + #endif + reply += F("
Switch Debounce:"); + #if defined SONOFF || defined SONOFF_TH + choice = Settings.externalType; + reply += F("
External Device Type:"); + #endif + #ifdef SONOFF_TH + reply += F("
Temperature:"); + + reply += F("Fahrenheit"); + reply += F(""); + + reply += F("Celsius"); + reply += F(""); + reply += F("
Temperature Report Interval:
Humidity Report Interval:"); + + #endif + reply += F("
Uptime Report Interval:"); + #ifdef SONOFF_POW + reply += F("
W Report Interval:
V Report Interval:
A Report Interval:"); + choice = Settings.voltage; + reply += F("
Device Voltage:"); + #endif + + reply += F("
"); + reply += F("
"); + addFooter(reply); + server->send(200, "text/html", reply); + }); + + server->on("/control", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + char tmpString[64]; + #if defined SONOFF_DUAL + String value1 = server->arg("relayValue1"); + String value2 = server->arg("relayValue2"); + #elif defined SONOFF_4CH + String value1 = server->arg("relayValue1"); + String value2 = server->arg("relayValue2"); + String value3 = server->arg("relayValue3"); + String value4 = server->arg("relayValue4"); + #else + String value = server->arg("relayValue"); + #endif + + + #if defined SONOFF_DUAL + if (value1 == "on") + { + relayControl(1, 1); + } + if (value1 == "off") + { + relayControl(1, 0); + } + if (value2 == "on") + { + relayControl(2, 1); + } + if (value2 == "off") + { + relayControl(2, 0); + } + #elif defined SONOFF_4CH + if (value1 == "on") + { + relayControl(1, 1); + } + if (value1 == "off") + { + relayControl(1, 0); + } + if (value2 == "on") + { + relayControl(2, 1); + } + if (value2 == "off") + { + relayControl(2, 0); + } + if (value3 == "on") + { + relayControl(3, 1); + } + if (value3 == "off") + { + relayControl(3, 0); + } + if (value4 == "on") + { + relayControl(4, 1); + } + if (value4 == "off") + { + relayControl(4, 0); + } + #else + if (value == "on") + { + relayControl(1, 1); + } + else if (value == "off") + { + relayControl(1, 0); + } + #endif + + String reply = ""; + char str[20]; + addHeader(true, reply); + + reply += F(""); + reply += F("
"); + reply += projectName; + reply += F(" Control"); + reply += F("
"); + addMenu(reply); + addRebootBanner(reply); + + #if defined SONOFF_DUAL + reply += F("
Relay 1
Current State:"); + + if (Settings.currentState1) { + reply += F("ON"); + } else { + reply += F("OFF"); + } + reply += F("
"); + reply += F("
"); + reply += F("
Relay 2
Current State:"); + + if (Settings.currentState2) { + reply += F("ON"); + } else { + reply += F("OFF"); + } + reply += F("
"); + reply += F("
"); + #elif defined SONOFF_4CH + reply += F("
Relay 1
Current State:"); + + if (Settings.currentState1) { + reply += F("ON"); + } else { + reply += F("OFF"); + } + reply += F("
"); + reply += F("
"); + reply += F("
Relay 2
Current State:"); + + if (Settings.currentState2) { + reply += F("ON"); + } else { + reply += F("OFF"); + } + reply += F("
"); + reply += F("
"); + reply += F("
Relay 3
Current State:"); + + if (Settings.currentState3) { + reply += F("ON"); + } else { + reply += F("OFF"); + } + reply += F("
"); + reply += F("
"); + reply += F("
Relay 4
Current State:"); + + if (Settings.currentState4) { + reply += F("ON"); + } else { + reply += F("OFF"); + } + reply += F("
"); + reply += F("
"); + + #else + reply += F("
Current State:"); + + if (Settings.currentState1) { + reply += F("ON"); + } else { + reply += F("OFF"); + } + reply += F("
"); + reply += F("
"); + #endif + + reply += F("
"); + addFooter(reply); + server->send(200, "text/html", reply); + }); + + server->on("/", []() { + + char tmpString[64]; + + String reply = ""; + char str[20]; + addHeader(true, reply); + reply += F(""); + reply += F("
"); + reply += projectName; + reply += F(" Main"); + reply += F("
"); + addMenu(reply); + addRebootBanner(reply); + + reply += F("
Main:"); + + reply += F("Advanced Config
"); + reply += F("Relay Control
"); + reply += F("Firmware Update
"); + reply += F("Documentation
"); + reply += F("Reboot
"); + + reply += F("
JSON Endpoints:"); + + reply += F("status
"); + //reply += F("config
"); + reply += F("configSet
"); + reply += F("configGet
"); + reply += F("on
"); + reply += F("off
"); + #if defined SONOFF_DUAL + reply += F("on1
"); + reply += F("off1
"); + reply += F("on2
"); + reply += F("off2
"); + #endif + #if defined SONOFF_4CH + reply += F("on1
"); + reply += F("off1
"); + reply += F("on2
"); + reply += F("off2
"); + reply += F("on3
"); + reply += F("off3
"); + reply += F("on4
"); + reply += F("off4
"); + #endif + reply += F("info
"); + reply += F("reboot
"); + + reply += F("
"); + addFooter(reply); + server->send(200, "text/html", reply); + }); + + server->on("/configGet", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePassword == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + char tmpString[64]; + boolean success = false; + String configName = server->arg("name"); + String reply = ""; + char str[20]; + + if (configName == "haip") { + sprintf_P(str, PSTR("%u.%u.%u.%u"), Settings.haIP[0], Settings.haIP[1], Settings.haIP[2], Settings.haIP[3]); + reply += "{\"name\":\"haip\", \"value\":\"" + String(str) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "haport") { + reply += "{\"name\":\"haport\", \"value\":\"" + String(Settings.haPort) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "pos") { + reply += "{\"name\":\"pos\", \"value\":\"" + String(Settings.powerOnState) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "autooff1") { + reply += "{\"name\":\"autooff1\", \"value\":\"" + String(Settings.autoOff1) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "debounce") { + reply += "{\"name\":\"debounce\", \"value\":\"" + String(Settings.debounce) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "ureport") { + reply += "{\"name\":\"ureport\", \"value\":\"" + String(Settings.uReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #if defined SONOFF || defined SONOFF_TH + if (configName == "externaltype") { + reply += "{\"name\":\"externaltype\", \"value\":\"" + String(Settings.externalType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #ifdef SONOFF_POW + if (configName == "wreport") { + reply += "{\"name\":\"wreport\", \"value\":\"" + String(Settings.wReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "vreport") { + reply += "{\"name\":\"vreport\", \"value\":\"" + String(Settings.vReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "areport") { + reply += "{\"name\":\"areport\", \"value\":\"" + String(Settings.aReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "vareport") { + reply += "{\"name\":\"vareport\", \"value\":\"" + String(Settings.vaReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "pfreport") { + reply += "{\"name\":\"pfreport\", \"value\":\"" + String(Settings.pfReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #ifdef SONOFF_TH + if ( configName == "usefahrenheit") { + reply += "{\"name\":\"usefahrenheit\", \"value\":\"" + String(Settings.useFahrenheit) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "sensortype") { + reply += "{\"name\":\"sensortype\", \"value\":\"" + String(Settings.sensorType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "treport") { + reply += "{\"name\":\"treport\", \"value\":\"" + String(Settings.tReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "hreport") { + reply += "{\"name\":\"hreport\", \"value\":\"" + String(Settings.hReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #if defined SONOFF_DUAL || defined SONOFF_4CH + if (configName == "autooff2") { + reply += "{\"name\":\"autooff2\", \"value\":\"" + String(Settings.autoOff2) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #if defined SONOFF_4CH + if (configName == "autooff23") { + reply += "{\"name\":\"autooff3\", \"value\":\"" + String(Settings.autoOff3) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "autooff4") { + reply += "{\"name\":\"autooff4\", \"value\":\"" + String(Settings.autoOff4) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + + if ( reply != "" ) { + server->send(200, "application/json", reply); + } else { + server->send(200, "application/json", "{\"success\":\"false\", \"type\":\"configuration\"}"); + } + }); + + server->on("/configSet", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePassword == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + char tmpString[64]; + boolean success = false; + String configName = server->arg("name"); + String configValue = server->arg("value"); + String reply = ""; + char str[20]; + + if (configName == "haip") { + if (configValue.length() != 0) + { + configValue.toCharArray(tmpString, 26); + str2ip(tmpString, Settings.haIP); + } + reply += "{\"name\":\"haip\", \"value\":\"" + String(tmpString) + "\", \"success\":\"true\"}"; + } + if (configName == "haport") { + if (configValue.length() != 0) + { + Settings.haPort = configValue.toInt(); + } + reply += "{\"name\":\"haport\", \"value\":\"" + String(Settings.haPort) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "pos") { + if (configValue.length() != 0) + { + Settings.powerOnState = configValue.toInt(); + } + reply += "{\"name\":\"pos\", \"value\":\"" + String(Settings.powerOnState) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "autooff1") { + if (configValue.length() != 0) + { + Settings.autoOff1 = configValue.toInt(); + } + reply += "{\"name\":\"autooff1\", \"value\":\"" + String(Settings.autoOff1) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "ureport") { + if (configValue.length() != 0) + { + if (configValue.toInt() != Settings.uReport) { + Settings.uReport = configValue.toInt(); + timerUptime = millis() + Settings.uReport * 1000; + } + } + reply += "{\"name\":\"ureport\", \"value\":\"" + String(Settings.uReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "debounce") { + if (configValue.length() != 0) + { + Settings.debounce = configValue.toInt(); + } + reply += "{\"name\":\"debounce\", \"value\":\"" + String(Settings.debounce) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #if defined SONOFF || defined SONOFF_TH + if (configName == "externaltype") { + if (configValue.length() != 0) + { + Settings.externalType = configValue.toInt(); + } + reply += "{\"name\":\"externaltype\", \"value\":\"" + String(Settings.externalType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #ifdef SONOFF_POW + if (configName == "wreport") { + if (configValue.length() != 0) + { + if (configValue.toInt() != Settings.wReport) { + Settings.wReport = configValue.toInt(); + timerW = millis() + Settings.wReport * 1000; + } + } + reply += "{\"name\":\"wreport\", \"value\":\"" + String(Settings.wReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "vreport") { + if (configValue.length() != 0) + { + if (configValue.toInt() != Settings.vReport) { + Settings.vReport = configValue.toInt(); + timerV = millis() + Settings.vReport * 1000; + } + } + reply += "{\"name\":\"vreport\", \"value\":\"" + String(Settings.vReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "areport") { + if (configValue.length() != 0) + { + if (configValue.toInt() != Settings.aReport) { + Settings.aReport = configValue.toInt(); + timerA = millis() + Settings.aReport * 1000; + } + } + reply += "{\"name\":\"areport\", \"value\":\"" + String(Settings.aReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "vareport") { + reply += "{\"name\":\"vareport\", \"value\":\"" + String(Settings.vaReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "pfreport") { + reply += "{\"name\":\"pfreport\", \"value\":\"" + String(Settings.pfReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #ifdef SONOFF_TH + if (configName == "usefahrenheit") { + if (configValue.length() != 0) + { + Settings.useFahrenheit = (configValue == "true"); + } + reply += "{\"name\":\"usefahrenheit\", \"value\":\"" + String(Settings.useFahrenheit) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "sensortype") { + if (configValue.length() != 0) + { + Settings.sensorType = configValue.toInt(); + } + reply += "{\"name\":\"sensortype\", \"value\":\"" + String(Settings.sensorType) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "treport") { + if (configValue.length() != 0) + { + if (configValue.toInt() != Settings.tReport) { + Settings.tReport = configValue.toInt(); + timerT = millis() + Settings.tReport * 1000; + } + } + reply += "{\"name\":\"treport\", \"value\":\"" + String(Settings.tReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "hreport") { + if (configValue.length() != 0) + { + if (configValue.toInt() != Settings.hReport) { + Settings.hReport = configValue.toInt(); + timerH = millis() + Settings.hReport * 1000; + } + } + reply += "{\"name\":\"hreport\", \"value\":\"" + String(Settings.hReport) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #if defined SONOFF_DUAL || defined SONOFF_4CH + if (configName == "autooff2") { + if (configValue.length() != 0) + { + Settings.autoOff2 = configValue.toInt(); + } + reply += "{\"name\":\"autooff2\", \"value\":\"" + String(Settings.autoOff2) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + #if defined SONOFF_4CH + if (configName == "autooff3") { + if (configValue.length() != 0) + { + Settings.autoOff3 = configValue.toInt(); + } + reply += "{\"name\":\"autooff3\", \"value\":\"" + String(Settings.autoOff3) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + if (configName == "autooff4") { + if (configValue.length() != 0) + { + Settings.autoOff4 = configValue.toInt(); + } + reply += "{\"name\":\"autooff4\", \"value\":\"" + String(Settings.autoOff4) + "\", \"success\":\"true\", \"type\":\"configuration\"}"; + } + #endif + + if ( reply != "" ) { + SaveSettings(); + server->send(200, "application/json", reply); + } else { + server->send(200, "application/json", "{\"success\":\"false\", \"type\":\"configuration\"}"); + } + }); + + #if defined SONOFF_DUAL + server->on("/off", []() { + boolean relayStatus = false; + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + relayControl(0, 0); + if (Settings.currentState1 || Settings.currentState2) relayStatus = true; + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(relayStatus? "on" : "off") + "\"}"); + }); + server->on("/off1", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(1, 0); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(Settings.currentState1? "on" : "off") + "\"}"); + }); + server->on("/off2", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(2, 0); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"2\", \"power\":\"" + String(Settings.currentState2? "on" : "off") + "\"}"); + }); + + #elif defined SONOFF_4CH + server->on("/off", []() { + boolean relayStatus = false; + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(0, 0); + if (Settings.currentState1 || Settings.currentState2 || Settings.currentState3 || Settings.currentState4) relayStatus = true; + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"0\", \"power\":\"" + String(relayStatus? "on" : "off") + "\"}"); + }); + server->on("/off1", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(1, 0); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(Settings.currentState1? "on" : "off") + "\"}"); + }); + server->on("/off2", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(2, 0); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"2\", \"power\":\"" + String(Settings.currentState2? "on" : "off") + "\"}"); + }); + server->on("/off3", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(3, 0); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"3\", \"power\":\"" + String(Settings.currentState3? "on" : "off") + "\"}"); + }); + server->on("/off4", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(4, 0); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"4\", \"power\":\"" + String(Settings.currentState4? "on" : "off") + "\"}"); + }); + #else + server->on("/off", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(0, 0); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(Settings.currentState1? "on" : "off") + "\"}"); + }); + + #endif + + #if defined SONOFF_DUAL + server->on("/on", []() { + boolean relayStatus = false; + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + relayControl(0, 1); + if (Settings.currentState1 || Settings.currentState2) relayStatus = true; + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"0\", \"power\":\"" + String(relayStatus? "on" : "off") + "\"}"); + }); + server->on("/on1", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + relayControl(1, 1); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(Settings.currentState1? "on" : "off") + "\"}"); + }); + server->on("/on2", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(2, 1); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"2\", \"power\":\"" + String(Settings.currentState2? "on" : "off") + "\"}"); + }); + #elif defined SONOFF_4CH + server->on("/on", []() { + boolean relayStatus = false; + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + relayControl(0, 1); + if (Settings.currentState1 || Settings.currentState2 || Settings.currentState3 || Settings.currentState4) relayStatus = true; + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"0\", \"power\":\"" + String(relayStatus? "on" : "off") + "\"}"); + }); + server->on("/on1", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(1, 1); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(Settings.currentState1? "on" : "off") + "\"}"); + }); + server->on("/on2", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(2, 1); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"2\", \"power\":\"" + String(Settings.currentState2? "on" : "off") + "\"}"); + }); + server->on("/on3", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(3, 1); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"3\", \"power\":\"" + String(Settings.currentState3? "on" : "off") + "\"}"); + }); + server->on("/on4", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(4, 1); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"4\", \"power\":\"" + String(Settings.currentState4? "on" : "off") + "\"}"); + }); + #else + server->on("/on", []() { + + if (SecuritySettings.Password[0] != 0 && Settings.usePasswordControl == true) + { + if (!server->authenticate("admin", SecuritySettings.Password)) + return server->requestAuthentication(); + } + + relayControl(0, 1); + server->send(200, "application/json", "{\"type\":\"relay\", \"number\":\"1\", \"power\":\"" + String(Settings.currentState1? "on" : "off") + "\"}"); + }); + #endif + + if (ESP.getFlashChipRealSize() > 524288) { + if (Settings.usePassword == true && SecuritySettings.Password[0] != 0) { + httpUpdater.setup(&*server, "/update", "admin", SecuritySettings.Password); + httpUpdater.setProjectName(projectName); + } else { + httpUpdater.setup(&*server); + httpUpdater.setProjectName(projectName); + } + } + + server->onNotFound(handleNotFound); + + server->begin(); + + //Serial.printf("Starting SSDP...\n"); + SSDP.setSchemaURL("description.xml"); + SSDP.setHTTPPort(80); + SSDP.setName(projectName); + SSDP.setSerialNumber(ESP.getChipId()); + SSDP.setURL("index.html"); + SSDP.setModelName(projectName); + SSDP.setModelNumber(deblank(projectName) + "_SL"); + SSDP.setModelURL("http://smartlife.tech"); + SSDP.setManufacturer("Smart Life Automated"); + SSDP.setManufacturerURL("http://smartlife.tech"); + SSDP.begin(); + + //Serial.println("HTTP server started"); + //Serial.println(WiFi.localIP()); + + timerUptime = millis() + Settings.uReport * 1000; + +} + +void loop() +{ + server->handleClient(); + + #if defined SONOFF_DUAL + buttonLoop(); + #endif + + if (needUpdate1 == true || needUpdate2 == true || needUpdate3 == true || needUpdate4 == true) { + sendStatus(0); + } + + if (needUpdate1 == true) { +#ifdef SONOFF_TH + checkTempAndHumidity(); +#endif + sendStatus(1); + needUpdate1 = false; + } + + if (needUpdate2 == true) { + sendStatus(2); + needUpdate2 = false; + } + + if (needUpdate3 == true) { + sendStatus(3); + needUpdate3 = false; + } + + if (needUpdate4 == true) { + sendStatus(4); + needUpdate4 = false; + } + + if (millis() > timer1s) + runEach1Seconds(); + + if (millis() > timer5s) + runEach5Seconds(); + + if (millis() > timer1m) + runEach1Minutes(); + + if (millis() > timer5m) + runEach5Minutes(); + + if (Settings.uReport > 0 && millis() > timerUptime){ + sendStatus(99); + timerUptime = millis() + Settings.uReport * 1000; + } + +#ifdef SONOFF_POW + if (Settings.wReport > 0 && millis() > timerW) + sendReport(1); + + if (Settings.vReport > 0 && millis() > timerV) + sendReport(2); + + if (Settings.aReport > 0 && millis() > timerA) + sendReport(3); + + if (Settings.vaReport > 0 && millis() > timerVA) + sendReport(4); + + if (Settings.pfReport > 0 && millis() > timerPF) + sendReport(5); + +#endif + +#ifdef SONOFF_TH + if (Settings.tReport > 0 && millis() > timerT) + sendReport(1); + + if (Settings.hReport > 0 && millis() > timerH) + sendReport(2); +#endif + + if ((Settings.autoOff1 != 0) && inAutoOff1 && ((millis() - autoOffTimer1) > (1000 * Settings.autoOff1))) { + relayControl(1, 0); + autoOffTimer1 = 0; + inAutoOff1 = false; + } + #if defined SONOFF_DUAL || defined SONOFF_4CH + if ((Settings.autoOff2 != 0) && inAutoOff2 && ((millis() - autoOffTimer2) > (1000 * Settings.autoOff2))) { + relayControl(2, 0); + autoOffTimer2 = 0; + inAutoOff2 = false; + } + #endif + #if defined SONOFF_4CH + if ((Settings.autoOff3 != 0) && inAutoOff3 && ((millis() - autoOffTimer3) > (1000 * Settings.autoOff3))) { + relayControl(3, 0); + autoOffTimer3 = 0; + inAutoOff3 = false; + } + if ((Settings.autoOff4 != 0) && inAutoOff4 && ((millis() - autoOffTimer4) > (1000 * Settings.autoOff4))) { + relayControl(4, 0); + autoOffTimer4 = 0; + inAutoOff4 = false; + } + #endif + +} + +#if defined SONOFF_DUAL +void buttonLoop() { + if (Serial.available() >= 4) { + unsigned char value; + if (Serial.read() == 0xA0) { + if (Serial.read() == 0x04) { + value = Serial.read(); + if (Serial.read() == 0xA1) { + int thisRead = (value > 3? HIGH : LOW); + int lastRead = currentStatus; + currentStatus = (value > 3? HIGH : LOW); + + if (value > 3 || thisRead != lastRead) { + relayControl(0, 2); + return; + } + switch (value) + { + case 0x00: case 0x04: // All Off + { + if (Settings.currentState1) { + needUpdate1 = true; + Settings.currentState1 = false; + } + if (Settings.currentState2) { + needUpdate2 = true; + Settings.currentState2 = false; + } + break; + } + case 0x01: case 0x05: //Switch1 On + { + if (!Settings.currentState1) { + needUpdate1 = true; + Settings.currentState1 = true; + } + if (Settings.currentState2) { + needUpdate2 = true; + Settings.currentState2 = false; + } + break; + } + case 0x02: case 0x06: //Switch2 On + { + if (Settings.currentState1) { + needUpdate1 = true; + Settings.currentState1 = false; + } + if (!Settings.currentState2) { + needUpdate2 = true; + Settings.currentState2 = true; + } + break; + } + case 0x03: case 0x07: //All On + { + if (!Settings.currentState1) { + needUpdate1 = true; + Settings.currentState1 = true; + } + if (!Settings.currentState2) { + needUpdate2 = true; + Settings.currentState2 = true; + } + break; + } + } + } + } + } + } +} +#endif diff --git a/Drivers/sonoff-wifi-switch.src/Sonoff.ino.generic.bin b/Drivers/sonoff-wifi-switch.src/Sonoff.ino.generic.bin new file mode 100644 index 0000000..74e8329 Binary files /dev/null and b/Drivers/sonoff-wifi-switch.src/Sonoff.ino.generic.bin differ diff --git a/Drivers/sonoff-wifi-switch.src/SonoffS20.ino.generic.bin b/Drivers/sonoff-wifi-switch.src/SonoffS20.ino.generic.bin new file mode 100644 index 0000000..9e16639 Binary files /dev/null and b/Drivers/sonoff-wifi-switch.src/SonoffS20.ino.generic.bin differ diff --git a/Drivers/sonoff-wifi-switch.src/SonoffTouch.ino.generic.bin b/Drivers/sonoff-wifi-switch.src/SonoffTouch.ino.generic.bin new file mode 100644 index 0000000..d0abcfa Binary files /dev/null and b/Drivers/sonoff-wifi-switch.src/SonoffTouch.ino.generic.bin differ diff --git a/Drivers/sonoff-wifi-switch.src/firmware_flash.zip b/Drivers/sonoff-wifi-switch.src/firmware_flash.zip new file mode 100644 index 0000000..f1ea1ef Binary files /dev/null and b/Drivers/sonoff-wifi-switch.src/firmware_flash.zip differ diff --git a/Drivers/sonoff-wifi-switch.src/sonoff-wifi-switch.groovy b/Drivers/sonoff-wifi-switch.src/sonoff-wifi-switch.groovy new file mode 100644 index 0000000..02d1f8b --- /dev/null +++ b/Drivers/sonoff-wifi-switch.src/sonoff-wifi-switch.groovy @@ -0,0 +1,450 @@ +/** + * Copyright 2016 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 Wifi Switch + * + * Author: Eric Maycock (erocm123) + * Date: 2016-06-02 + */ + +import groovy.json.JsonSlurper +import groovy.util.XmlSlurper + +metadata { + definition (name: "Sonoff Wifi Switch", namespace: "erocm123", author: "Eric Maycock") { + capability "Actuator" + capability "Switch" + 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: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.on", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.off", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.off", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.on", nextState:"turningOn" + } + } + + 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}' + } + + } + + main(["switch"]) + details(["switch", + "refresh","configure","reboot", + "ip", "uptime"]) +} + +def installed() { + log.debug "installed()" + configure() +} + +def configure() { + logging("configure()", 1) + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) cmds +} + +def updated() +{ + logging("updated()", 1) + 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"]) { + log.debug "Mac address of device found ${descMap["mac"]}" + 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) + + log.debug "result: ${result}" + + if (result.containsKey("type")) { + if (result.type == "configuration") + events << update_current_properties(result) + } + if (result.containsKey("power")) { + events << createEvent(name: "switch", value: result.power) + } + 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 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("/on") + return cmds +} + +def off() { + log.debug "off()" + def cmds = [] + cmds << getAction("/off") + return cmds +} + +def refresh() { + log.debug "refresh()" + def cmds = [] + cmds << getAction("/status") + return cmds +} + +def ping() { + log.debug "ping()" + 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 hubitat.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 hubitat.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" + } + log.debug "Device Network Id set to ${myDNI}" + 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() { + log.debug "reboot()" + 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 = 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}", + 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 = new XmlSlurper().parseText(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 +} + +def configuration_model() +{ +''' + + + + + + + +Default: Off + + + + + + + +Automatically turn the switch off after this many seconds. +Range: 0 to 65536 +Default: 0 (Disabled) + + + + +If a switch is attached to GPIO 14. +Default: Momentary + + + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/switch-child-device.src/switch-child-device.groovy b/Drivers/switch-child-device.src/switch-child-device.groovy new file mode 100644 index 0000000..175455c --- /dev/null +++ b/Drivers/switch-child-device.src/switch-child-device.groovy @@ -0,0 +1,49 @@ +/** + * Switch Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Switch Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Actuator" + capability "Sensor" + capability "Refresh" + } + + tiles { + multiAttributeTile(name:"switch", type: "lighting", width: 3, 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 "turningOn", 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" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } +} + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} + +void refresh() { + parent.childRefresh(device.deviceNetworkId) +} diff --git a/Drivers/switch-level-child-device.src/switch-level-child-device.groovy b/Drivers/switch-level-child-device.src/switch-level-child-device.groovy new file mode 100644 index 0000000..bf8cd5f --- /dev/null +++ b/Drivers/switch-level-child-device.src/switch-level-child-device.groovy @@ -0,0 +1,61 @@ +/** + * 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. + * + */ +metadata { + definition (name: "Switch Level Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Refresh" + } + + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", 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" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + + + } +} + + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} + +void refresh() { + parent.childRefresh(device.deviceNetworkId) +} + +def setLevel(value) { + parent.childSetLevel(device.deviceNetworkId, value) +} \ No newline at end of file diff --git a/Drivers/verilock-translator.src/verilock-translator.groovy b/Drivers/verilock-translator.src/verilock-translator.groovy new file mode 100644 index 0000000..7177289 --- /dev/null +++ b/Drivers/verilock-translator.src/verilock-translator.groovy @@ -0,0 +1,278 @@ +/** + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Verilock Translator", namespace: "erocm123", author: "Eric Maycock") { + capability "Refresh" + capability "Lock" + capability "Contact Sensor" + capability "Configuration" + capability "Sensor" + capability "Zw Multichannel" + + fingerprint mfr: "0178", prod: "5A44", model: "414E" + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + } + tileAttribute ("lock", key: "SECONDARY_CONTROL") { + attributeState "locked", label:'LOCKED', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC", nextState:"unlocking" + attributeState "unlocked", label:'UNLOCKED', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" + } + } + + main "contact" + details(["contact", childDeviceTiles("all")]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug("'$description' parsed to $result") + return result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) { + [ createEvent(descriptionText: "${device.displayName} woke up", isStateChange:true), + response(["delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) ] +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd, ep = null) +{ + def evtName + def evtValue + switch(cmd.event){ + case 1: + evtName = "lock" + evtValue = "locked" + break; + case 2: + evtName = "lock" + evtValue = "unlocked" + break; + case 22: + evtName = "contact" + evtValue = "open" + break; + case 23: + evtName = "contact" + evtValue = "closed" + break; + } + def childDevice = childDevices.find{it.deviceNetworkId == "$device.deviceNetworkId-ep${ep}"} + if (!childDevice) { + log.debug "Child not found for endpoint. Creating one now" + childDevice = addChildDevice("Lockable Door/Window Child Device", "${device.deviceNetworkId}-ep${ep}", null, + [completedSetup: true, label: "${device.displayName} Window ${ep}", + isComponent: false, componentName: "ep$ep", componentLabel: "Window $ep"]) + } + + childDevice.sendEvent(name: evtName, value: evtValue) + + def allLocked = true + def allClosed = true + childDevices.each { n -> + if (n.currentState("contact") && n.currentState("contact").value != "closed") allClosed = false + if (n.currentState("lock") && n.currentState("lock").value != "locked") allLocked = false + } + def events = [] + if (allLocked) { + sendEvent([name: "lock", value: "locked"]) + } else { + sendEvent([name: "lock", value: "unlocked"]) + } + if (allClosed) { + sendEvent([name: "contact", value: "closed"]) + } else { + sendEvent([name: "contact", value: "open"]) + } +} + +private List loadEndpointInfo() { + if (state.endpointInfo) { + state.endpointInfo + } else if (device.currentValue("epInfo")) { + fromJson(device.currentValue("epInfo")) + } else { + [] + } +} + +def updated() { + childDevices.each { + if (it.label == "${state.oldLabel} ${channelNumber(it.deviceNetworkId)}") { + def newLabel = "${device.displayName} ${channelNumber(it.deviceNetworkId)}" + it.setLabel(newLabel) + } + } + state.oldLabel = device.label +} + +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd) { + updateDataValue("endpoints", cmd.endPoints.toString()) + if (!state.endpointInfo) { + state.endpointInfo = loadEndpointInfo() + } + if (state.endpointInfo.size() > cmd.endPoints) { + cmd.endpointInfo + } + state.endpointInfo = [null] * cmd.endPoints + //response(zwave.associationV2.associationGroupingsGet()) + [ createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:""), + response(zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: 1)) ] +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) { + def result = [] + def cmds = [] + if(!state.endpointInfo) state.endpointInfo = [] + state.endpointInfo[cmd.endPoint - 1] = cmd.format()[6..-1] + if (cmd.endPoint < getDataValue("endpoints").toInteger()) { + cmds = zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: cmd.endPoint + 1).format() + } else { + log.debug "endpointInfo: ${state.endpointInfo.inspect()}" + } + result << createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:"") + if(cmds) result << response(cmds) + result +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) { + state.groups = cmd.supportedGroupings + if (cmd.supportedGroupings > 1) { + [response(zwave.associationGrpInfoV1.associationGroupInfoGet(groupingIdentifier:2, listMode:1))] + } +} + +def zwaveEvent(hubitat.zwave.commands.associationgrpinfov1.AssociationGroupInfoReport cmd) { + def cmds = [] + /*for (def i = 0; i < cmd.groupCount; i++) { + def prof = cmd.payload[5 + (i * 7)] + def num = cmd.payload[3 + (i * 7)] + if (prof == 0x20 || prof == 0x31 || prof == 0x71) { + updateDataValue("agi$num", String.format("%02X%02X", *(cmd.payload[(7*i+5)..(7*i+6)]))) + cmds << response(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:num, nodeId:zwaveHubNodeId)) + } + }*/ + for (def i = 2; i <= state.groups; i++) { + cmds << response(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:i, nodeId:zwaveHubNodeId)) + } + cmds +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + if (state.enabledEndpoints.find { it == cmd.sourceEndPoint }) { + def formatCmd = ([cmd.commandClass, cmd.command] + cmd.parameter).collect{ String.format("%02X", it) }.join() + createEvent(name: "epEvent", value: "$cmd.sourceEndPoint:$formatCmd", isStateChange: true, displayed: false, descriptionText: "(fwd to ep $cmd.sourceEndPoint)") + } else { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof hubitat.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + +def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] + // def encapsulatedCommand = cmd.encapsulatedCommand(versions) + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", isStateChange: true) +} + +def configure() { + commands([ + zwave.multiChannelV3.multiChannelEndPointGet() + ], 800) +} + +def epCmd(Integer ep, String cmds) { + def result + if (cmds) { + def header = state.sec ? "988100600D00" : "600D00" + result = cmds.split(",").collect { cmd -> (cmd.startsWith("delay")) ? cmd : String.format("%s%02X%s", header, ep, cmd) } + } + result +} + +def enableEpEvents(enabledEndpoints) { + state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger() + null +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private encap(cmd, endpoint) { + if (endpoint) { + command(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd)) + } else { + command(cmd) + } +} + +private encapWithDelay(commands, endpoint, delay=200) { + delayBetween(commands.collect{ encap(it, endpoint) }, delay) +} \ No newline at end of file diff --git a/Drivers/water-sensor-child-device.src/water-sensor-child-device.groovy b/Drivers/water-sensor-child-device.src/water-sensor-child-device.groovy new file mode 100644 index 0000000..c1f508d --- /dev/null +++ b/Drivers/water-sensor-child-device.src/water-sensor-child-device.groovy @@ -0,0 +1,31 @@ +/** + * Water Sensor Child Device + * + * Copyright 2017 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. + * + */ +metadata { + definition (name: "Water Sensor Child Device", namespace: "erocm123", author: "Eric Maycock") { + capability "Water Sensor" + capability "Sensor" + } + + tiles() { + multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ + tileAttribute ("device.water", key: "PRIMARY_CONTROL") { + attributeState("dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff") + attributeState("wet", icon:"st.alarm.water.wet", backgroundColor:"#00a0dc") + } + } + } + +} diff --git a/Drivers/xiaomi-door-window-sensor.src/xiaomi-door-window-sensor.groovy b/Drivers/xiaomi-door-window-sensor.src/xiaomi-door-window-sensor.groovy new file mode 100644 index 0000000..008c812 --- /dev/null +++ b/Drivers/xiaomi-door-window-sensor.src/xiaomi-door-window-sensor.groovy @@ -0,0 +1,230 @@ +/** + * Xiaomi Door/Window Sensor + * + * Copyright 2015 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. + * + */ +metadata { + definition (name: "Xiaomi Door/Window Sensor", namespace: "erocm123", author: "Eric Maycock") { + capability "Configuration" + capability "Sensor" + capability "Contact Sensor" + capability "Refresh" + + command "enrollResponse" + + //fingerprint endpointId: "01", inClusters: "0000,0001", outClusters: "1234"//, model: "3320-L", manufacturer: "CentraLite" + //fingerprint endpoint: "01", + //profileId: "0104", + //inClusters: "0000,0001" + //outClusters: "1234" + + } + + simulator { + status "closed": "on/off: 0" + status "open": "on/off: 1" + } + + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main (["contact"]) + details(["contact","refresh","configure"]) + } +} + +def parse(String description) { + log.debug "Parsing '${description}'" + def value = zigbee.parse(description)?.text + log.debug "Parse: $value" + Map map = [:] + //def descMap = zigbee.parseDescriptionAsMap(description) + def resultMap = zigbee.getKnownDescription(description) + log.debug "${resultMap}" + if (description?.startsWith('on/off: ')) + map = parseCustomMessage(description) + if (description?.startsWith('catchall:')) + map = parseCatchAllMessage(description) + log.debug "Parse returned $map" + def results = map ? createEvent(map) : null + return results; +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + log.debug rawValue + + def result = [ + name: 'battery', + value: '--' + ] + result.descriptionText = "${linkText} battery was ${rawValue}%" + + def volts = rawValue / 10 + log.debug volts + def descriptionText + + if (rawValue == 0) {} + else { + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else if (volts > 0){ + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + } + + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + log.debug cluster + if (cluster) { + switch(cluster.clusterId) { + case 0x0000: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0xFC02: + log.debug 'ACCELERATION' + break + + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + + +def configure() { + String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) + log.debug "${device.deviceNetworkId}" + def endpointId = 1 + log.debug "${device.zigbeeId}" + log.debug "${zigbeeEui}" + def configCmds = [ + //battery reporting and heartbeat + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500", + + + // Writes CIE attribute on end device to direct reports to the hub's EUID + "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + ] + + log.debug "configure: Write IAS CIE" + return configCmds +} + +def enrollResponse() { + log.debug "Enrolling device into the IAS Zone" + [ + // Enrolling device into the IAS Zone + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + ] +} + +def refresh() { + log.debug "Refreshing Battery" + def endpointId = 0x01 + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0000 0x0000", "delay 200" + ] //+ enrollResponse() +} + +private Map parseCustomMessage(String description) { + def result + if (description?.startsWith('on/off: ')) { + if (description == 'on/off: 0') //contact closed + result = getContactResult("closed") + else if (description == 'on/off: 1') //contact opened + result = getContactResult("open") + return result + } +} + +private Map getContactResult(value) { + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" + return [ + name: 'contact', + value: value, + descriptionText: descriptionText + ] +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + + return array +} \ No newline at end of file diff --git a/Drivers/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy b/Drivers/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy new file mode 100644 index 0000000..a88066a --- /dev/null +++ b/Drivers/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy @@ -0,0 +1,242 @@ +/** + * Xiaomi Motion Sensor + * + * Copyright 2015 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. + * + */ + +metadata { + definition (name: "Xiaomi Motion Sensor", namespace: "erocm123", author: "Eric Maycock") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + capability "Sensor" + + command "reset" + + } + + simulator { + } + + preferences { + input "motionReset", "number", title: "Number of seconds after the last reported activity to report that motion is inactive (in seconds).", description: "", value:120, displayDuringSetup: false + } + + tiles(scale: 2) { + multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ + tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" + attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + } + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action:"reset", label: "Reset Motion" //icon:"st.secondary.refresh" + } + + main(["motion"]) + details(["motion", "reset"]) + } +} + +def parse(String description) { + log.debug "description: $description" + def value = zigbee.parse(description)?.text + log.debug "Parse: $value" + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new hubitat.device.HubAction(it) } + } + + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + log.debug cluster + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0xFC02: + log.debug 'ACCELERATION' + break + + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + //log.debug "Desc Map: $descMap" + + Map resultMap = [:] + + if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + else if (descMap.cluster == "0406" && descMap.attrId == "0000") { + def value = descMap.value.endsWith("01") ? "active" : "inactive" + if (settings.motionReset == null || settings.motionReset == "" ) settings.motionReset = 120 + if (value == "active") runIn(settings.motionReset, stopMotion) + resultMap = getMotionResult(value) + } + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getMotionResult('inactive') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getMotionResult('active') + break + + case '0x0022': // Tamper Alarm + log.debug 'motion with tamper alarm' + resultMap = getMotionResult('active') + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + log.debug 'no motion with tamper alarm' + resultMap = getMotionResult('inactive') + break + + case '0x0025': // Restore Report + break + + case '0x0026': // Trouble/Failure + log.debug 'motion with failure alarm' + resultMap = getMotionResult('active') + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + log.debug rawValue + + def result = [ + name: 'battery', + value: '--' + ] + + def volts = rawValue / 10 + def descriptionText + + if (rawValue == 0) {} + else { + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else if (volts > 0){ + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + } + + return result +} + +private Map getMotionResult(value) { + log.debug 'motion' + String linkText = getLinkText(device) + String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped" + def commands = [ + name: 'motion', + value: value, + descriptionText: descriptionText + ] + return commands +} + +def configure() { + String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) + log.debug "Configuring Reporting, IAS CIE, and Bindings." + def configCmds = [] + return configCmds + refresh() // send refresh cmds as part of config +} + +def enrollResponse() { + log.debug "Sending enroll response" +} + +def stopMotion() { + sendEvent(name:"motion", value:"inactive") +} + +def reset() { + sendEvent(name:"motion", value:"inactive") +} \ No newline at end of file diff --git a/Drivers/xiaomi-smart-button.src/xiaomi-smart-button.groovy b/Drivers/xiaomi-smart-button.src/xiaomi-smart-button.groovy new file mode 100644 index 0000000..ed48bb4 --- /dev/null +++ b/Drivers/xiaomi-smart-button.src/xiaomi-smart-button.groovy @@ -0,0 +1,186 @@ +/** + * Xiaomi Smart Button + * + * Copyright 2015 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. + * + */ +metadata { + definition (name: "Xiaomi Smart Button", namespace: "erocm123", author: "Eric Maycock") { + capability "PushableButton" + capability "Configuration" + capability "Sensor" + capability "Refresh" + + attribute "lastPress", "string" + + } + + simulator { + status "button 1 pressed": "on/off: 0" + status "button 1 released": "on/off: 1" + } + + preferences{ + input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"", + defaultValue: 4, displayDuringSetup: false) + } + + tiles(scale: 2) { + standardTile("button", "device.button", decoration: "flat", width: 2, height: 2) { + state "default", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["button"]) + details(["button","refresh"]) + } +} + +def parse(String description) { + log.debug "Parsing '${description}'" + def value = zigbee.parse(description)?.text + log.debug "Parse: $value" + def descMap = zigbee.parseDescriptionAsMap(description) + def results = [] + if (description?.startsWith('on/off: ')) + results = parseCustomMessage(description) + if (description?.startsWith('catchall:')) + results = parseCatchAllMessage(description) + return results; +} + +def configure(){ + refresh() +} + +def refresh(){ +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + log.debug cluster + if (cluster) { + switch(cluster.clusterId) { + case 0x0000: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0xFC02: + log.debug 'ACCELERATION' + break + + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + log.debug rawValue + + def result = [ + name: 'battery', + value: '--' + ] + result.descriptionText = "${linkText} battery was ${rawValue}%" + def volts = rawValue / 10 + def descriptionText + log.debug volts + + if (rawValue == 0) {} + else { + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else if (volts > 0){ + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + } + + return result +} + +private Map parseCustomMessage(String description) { + if (description?.startsWith('on/off: ')) { + if (description == 'on/off: 0') //button pressed + createPressEvent(1) + else if (description == 'on/off: 1') //button released + createButtonEvent(1) + } +} + +//this method determines if a press should count as a push or a hold and returns the relevant event type +private createButtonEvent(button) { + def currentTime = now() + def startOfPress = device.latestState('lastPress').date.getTime() + def timeDif = currentTime - startOfPress + def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000 + + if (timeDif < 0) + return [] //likely a message sequence issue. Drop this press and wait for another. Probably won't happen... + else if (timeDif < holdTimeMillisec) + return createButtonPushedEvent(button) + else + return createButtonHeldEvent(button) +} + +private createPressEvent(button) { + return createEvent([name: 'lastPress', value: now(), data:[buttonNumber: button], displayed: false]) +} + +private createButtonPushedEvent(button) { + log.debug "Button ${button} pushed" + return createEvent([ + name: "button", + value: "pushed", + data:[buttonNumber: button], + descriptionText: "${device.displayName} button ${button} was pushed", + isStateChange: true, + displayed: true]) +} + +private createButtonHeldEvent(button) { + log.debug "Button ${button} held" + return createEvent([ + name: "button", + value: "held", + data:[buttonNumber: button], + descriptionText: "${device.displayName} button ${button} was held", + isStateChange: true]) +} \ No newline at end of file diff --git a/Drivers/zipato-rgbw-bulb-advanced.src/zipato-rgbw-bulb-advanced.groovy b/Drivers/zipato-rgbw-bulb-advanced.src/zipato-rgbw-bulb-advanced.groovy new file mode 100644 index 0000000..0a75b60 --- /dev/null +++ b/Drivers/zipato-rgbw-bulb-advanced.src/zipato-rgbw-bulb-advanced.groovy @@ -0,0 +1,756 @@ +/** + * Copyright 2016 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. + * + * Zipato RGBW Bulb (Advanced) + * + * Author: Eric Maycock (erocm123) + * Date: 2016-11-09 + */ + +metadata { + definition (name: "Zipato RGBW Bulb (Advanced)", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch Level" + capability "Color Control" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Actuator" + capability "Sensor" + capability "Configuration" + capability "Health Check" + + command "reset" + command "refresh" + + (1..6).each { n -> + attribute "switch$n", "enum", ["on", "off"] + command "on$n" + command "off$n" + } + + fingerprint mfr: "0131", prod: "0002", model: "0002" + fingerprint deviceId: "0x1101", inClusters: "0x5E,0x26,0x85,0x72,0x33,0x70,0x86,0x73,0x59,0x5A,0x7A" + + } + + 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()) + } + + simulator { + } + + tiles (scale: 2){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } + + 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" + } + valueTile("colorTempTile", "device.colorTemperature", height: 2, width: 2) { + state "colorTemperature", label:'${currentValue}%', backgroundColor:"#FFFFFF" + } + controlTile("colorTempControl", "device.colorTemperature", "slider", decoration: "flat", height: 2, width: 4, inactiveLabel: false) { + state "colorTemperature", action:"setColorTemperature" + } + standardTile("switch1", "switch1", canChangeIcon: true, decoration: "flat", width: 2, height: 2) { + state "off", label: "fast", action: "on1", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "fast", action: "off1", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch2", "switch2", canChangeIcon: true, decoration: "flat", width: 2, height: 2) { + state "off", label: "slow", action: "on2", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "slow", action: "off2", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch3", "switch3", canChangeIcon: true, decoration: "flat", width: 2, height: 2) { + state "off", label: "r.fast", action: "on3", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "r.fast", action: "off3", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch4", "switch4", canChangeIcon: true, decoration: "flat", width: 2, height: 2) { + state "off", label: "r.slow", action: "on4", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "r.slow", action: "off4", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch5", "switch5", canChangeIcon: true, decoration: "flat", width: 2, height: 2) { + state "off", label: "custom", action: "on5", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "custom", action: "off5", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + standardTile("switch6", "switch6", canChangeIcon: true, decoration: "flat", width: 2, height: 2) { + state "off", label: "off", action: "on6", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + state "on", label: "off", action: "off6", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + } + valueTile( + "currentFirmware", "device.currentFirmware", inactiveLabel: false, width: 2, height: 2) { + state "currentFirmware", label:'Firmware: v${currentValue}', unit:"" + } + } + + main(["switch"]) + details(["switch", "levelSliderControl", + "colorTempControl", "colorTempTile", + "switch1", "switch2", "switch3", + "switch4", "switch5", "switch6", + "refresh", "configure" ]) +} + +/** +* Triggered when Done button is pushed on Preference Pane +*/ +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 6 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + state.needfwUpdate = "" + + def cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + if (cmds != []) response(commands(cmds)) +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) commands(cmds) +} + + +def parse(description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3]) + if (cmd) { + result = zwaveEvent(cmd) + logging("'$cmd' parsed to $result") + } else { + logging("Couldn't zwave.parse '$description'") + } + } + result +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + //dimmerEvents(cmd) +} + +private dimmerEvents(hubitat.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + if (cmd.value == 0) toggleTiles("all") + return result +} + +def zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) { + response(command(zwave.switchMultilevelV1.switchMultilevelGet())) +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof hubitat.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + if (cmd.parameterNumber == 4) { + if (cmd.configurationValue[0] == 0) toggleTiles("all") + } else { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'") + } + +} + +def zwaveEvent(hubitat.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd){ + logging("Firmware Report ${cmd.toString()}") + def firmwareVersion + switch(cmd.checksum){ + default: + firmwareVersion = cmd.checksum + } + state.needfwUpdate = "false" + updateDataValue("firmware", firmwareVersion.toString()) + createEvent(name: "currentFirmware", value: firmwareVersion) +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unhandled: $cmd") + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +private toggleTiles(value) { + def tiles = ["switch1", "switch2", "switch3", "switch4", "switch5"] + tiles.each {tile -> + if (tile != value) { sendEvent(name: tile, value: "off") } + else { sendEvent(name:tile, value:"on") } + } +} + +def on() { + toggleTiles("all") + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.basicV1.basicGet(), + ]) +} + +def off() { + toggleTiles("all") + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.basicV1.basicGet(), + ]) +} + +def setLevel(level) { + toggleTiles("all") + setLevel(level, 1) +} + +def setLevel(level, duration) { + if(level > 99) level = 99 + commands([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), + zwave.basicV1.basicGet(), + ], (duration && duration < 12) ? (duration * 1000) : 3500) +} + +def refresh() { + commands([ + zwave.basicV1.basicGet(), + zwave.configurationV1.configurationGet(parameterNumber: 4), + ]) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def setSaturation(percent) { + logging("setSaturation($percent)") + setColor(saturation: percent) +} + +def setHue(value) { + logging("setHue($value)") + setColor(hue: value) +} + +private getEnableRandomHue(){ + switch (settings.enableRandom) { + case "0": + return 23 + break + case "1": + return 52 + break + case "2": + return 53 + break + case "3": + return 20 + break + default: + + break + } +} + +private getEnableRandomSat(){ + switch (settings.enableRandom) { + case "0": + return 56 + break + case "1": + return 19 + break + case "2": + return 91 + break + case "3": + return 80 + break + default: + + break + } +} + +def setColor(value) { + def result = [] + def warmWhite = 0 + def coldWhite = 0 + logging("setColor: ${value}") + if (value.hue && value.saturation) { + logging("setting color with hue & saturation") + def hue = (value.hue != null) ? value.hue : 13 + def saturation = (value.saturation != null) ? value.saturation : 13 + def rgb = huesatToRGB(hue as Integer, saturation as Integer) + if ( settings.enableRandom && value.hue == getEnableRandomHue() && value.saturation == getEnableRandomSat() ) { + Random rand = new Random() + int max = 100 + hue = rand.nextInt(max+1) + rgb = huesatToRGB(hue as Integer, saturation as Integer) + } + else if ( value.hue == 23 && value.saturation == 56 ) { + def level = 255 + if ( value.level != null ) level = value.level * 0.01 * 255 + warmWhite = level + coldWhite = 0 + rgb[0] = 0 + rgb[1] = 0 + rgb[2] = 0 + } + else { + rgb = huesatToRGB(hue as Integer, saturation as Integer) + } + result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:warmWhite, coldWhite:coldWhite) + if(value.level != null && value.level != 1.0){ + if(value.level > 99) value.level = 99 + result << zwave.switchMultilevelV3.switchMultilevelSet(value: value.level, dimmingDuration: 3500) + result << zwave.switchMultilevelV3.switchMultilevelGet() + } + } + else if (value.hex) { + def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0) + } + //result << zwave.basicV1.basicSet(value: 0xFF) + result << zwave.basicV1.basicGet() + if(value.hue) sendEvent(name: "hue", value: value.hue) + if(value.hex) sendEvent(name: "color", value: value.hex) + if(value.switch) sendEvent(name: "switch", value: value.switch) + if(value.saturation) sendEvent(name: "saturation", value: value.saturation) + + toggleTiles("all") + commands(result) +} + +def setColorTemperature(percent) { + if(percent > 99) percent = 99 + int warmValue = percent * 255 / 99 + toggleTiles("all") + sendEvent(name: "colorTemperature", value: percent) + command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite: (warmValue > 127 ? 255 : 0), coldWhite: (warmValue < 128? 255 : 0))) + +} + +def reset() { + logging("reset()") + sendEvent(name: "color", value: "#ffffff") + setColorTemperature(1) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def rgbToHSV(red, green, blue) { + float r = red / 255f + float g = green / 255f + float b = blue / 255f + float max = [r, g, b].max() + float delta = max - [r, g, b].min() + def hue = 13 + def saturation = 0 + if (max && delta) { + saturation = 100 * delta / max + if (r == max) { + hue = ((g - b) / delta) * 100 / 6 + } else if (g == max) { + hue = (2 + (b - r) / delta) * 100 / 6 + } else { + hue = (4 + (r - g) / delta) * 100 / 6 + } + } + [hue: hue, saturation: saturation, value: max * 100] +} + +// huesatToRGB Changed method provided by daved314 +def huesatToRGB(float hue, float sat) { + if (hue <= 100) { + hue = hue * 3.6 + } + sat = sat / 100 + float v = 1.0 + float c = v * sat + float x = c * (1 - Math.abs(((hue/60)%2) - 1)) + float m = v - c + int mod_h = (int)(hue / 60) + int cm = Math.round((c+m) * 255) + int xm = Math.round((x+m) * 255) + int zm = Math.round((0+m) * 255) + switch(mod_h) { + case 0: return [cm, xm, zm] + case 1: return [xm, cm, zm] + case 2: return [zm, cm, xm] + case 3: return [zm, xm, cm] + case 4: return [xm, zm, cm] + case 5: return [cm, zm, xm] + } +} + +def on1() { + logging("on1()") + toggleTiles("switch1") + def cmds = [] + if (device.currentValue('switch') != "on") cmds << zwave.basicV1.basicSet(value: 0xFF) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(1, 1), parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(0, 1), parameterNumber: 5, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(255, 1), parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on2() { + logging("on2()") + toggleTiles("switch2") + def cmds = [] + if (device.currentValue('switch') != "on") cmds << zwave.basicV1.basicSet(value: 0xFF) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(25, 1), parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(0, 1), parameterNumber: 5, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(255, 1), parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on3() { + logging("on3()") + toggleTiles("switch3") + def cmds = [] + if (device.currentValue('switch') != "on") cmds << zwave.basicV1.basicSet(value: 0xFF) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(1, 1), parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(1, 1), parameterNumber: 5, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(255, 1), parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on4() { + logging("on4()") + toggleTiles("switch4") + def cmds = [] + if (device.currentValue('switch') != "on") cmds << zwave.basicV1.basicSet(value: 0xFF) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(25, 1), parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(1, 1), parameterNumber: 5, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(255, 1), parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def on5() { + logging("on5()") + toggleTiles("switch5") + def cmds = [] + if (device.currentValue('switch') != "on") cmds << zwave.basicV1.basicSet(value: 0xFF) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(settings."3".toInteger(), 1), parameterNumber: 3, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(settings."5".toInteger(), 1), parameterNumber: 5, size: 1) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(settings."4".toInteger(), 1), parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.switchMultilevelV3.switchMultilevelGet() + commands(cmds) +} + +def on6() { + logging("on6()") + offCmd() +} + +def offCmd() { + logging("offCmd()") + toggleTiles("all") + def cmds = [] + cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: 0, parameterNumber: 4, size: 1) + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4) + cmds << zwave.basicV1.basicGet() + commands(cmds) +} + +def off1() { offCmd() } +def off2() { offCmd() } +def off3() { offCmd() } +def off4() { offCmd() } +def off5() { offCmd() } +def off6() { offCmd() } + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 != "" ? "${it.@label}\n" + "${it.Help}" : "" + "${it.Help}", + defaultValue: "${it.@value}", + displayDuringSetup: "${it.@displayDuringSetup}" + break + } + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (convertParam(cmd.parameterNumber, settings."${cmd.parameterNumber}") == cmd2Integer(cmd.configurationValue)) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + if(!state.needfwUpdate || state.needfwUpdate == ""){ + logging("Requesting device firmware version") + cmds << zwave.firmwareUpdateMdV2.firmwareMdGet() + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + else if ((settings."${it.@index}" != null || "${it.@type}" == "hidden") && cmd2Integer(currentProperties."${it.@index}") != convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + { + isUpdateNeeded = "YES" + logging("Parameter ${it.@index} will be updated to " + convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}")) + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}"? settings."${it.@index}" : "${it.@value}") + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + def parValue + switch (number){ + default: + parValue = value + break + } + return parValue.toInteger() +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def configuration_model() +{ +''' + + + +Adjust color temperature. Values range from 1 to 99 where 1 means full cold white and 99 means full warm white. +Range: 1~99 +Default: 50 + + + + +Adjust shock sensor sensitivity. Values range from 0 to 31 where 0 means minimum sensitivity and 31 means maximum sensitivity +Range: 0~31 +Default: 16 + + + + +Adjust strobe light interval. Values range from 0 to 25 in intervals of 100 milliseconds +Range: 0~25 +Default: 0 + + + + +Adjust strobe light pulse count. Values range from 0 to 250 and a special value 255 which sets infinite flashing. +Range: 0~250, 255 (Infinite) +Default: 0 + + + + +Range: 0~1 +Default: 0 (Disabled) + + + + + + +If this option is enabled, using the selected color preset in SmartApps such as Smart Lighting will result in a random color. + + + + + + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/zooz-4-in-1-sensor-advanced.src/zooz-4-in-1-sensor-advanced.groovy b/Drivers/zooz-4-in-1-sensor-advanced.src/zooz-4-in-1-sensor-advanced.groovy new file mode 100644 index 0000000..66f37f6 --- /dev/null +++ b/Drivers/zooz-4-in-1-sensor-advanced.src/zooz-4-in-1-sensor-advanced.groovy @@ -0,0 +1,785 @@ +/** + * + * zooZ 4-in-1 Sensor (Advanced) + * + * github: Eric Maycock (erocm123) + * Date: 2016-07-09 + * Copyright Eric Maycock + * + * Code has elements from other community source @CyrilPeponnet (Z-Wave Parameter Sync). Includes all + * configuration parameters and ease of advanced configuration. Added software based temp, humidity, + * and light offsets. + * + * 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. + * + */ + + metadata { + definition (name: "zooZ 4-in-1 Sensor (Advanced)", namespace: "erocm123", author: "Eric Maycock") { + capability "Motion Sensor" + capability "Acceleration Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Illuminance Measurement" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Refresh" + capability "Tamper Alert" + capability "Health Check" + + command "resetBatteryRuntime" + + attribute "needUpdate", "string" + + fingerprint mfr: "0109", prod: "2021", model: "2101", deviceJoinName: "zooZ 4-in-1 Sensor" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x31,0x70,0x5A,0x98,0x7A" + } + 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()) + } + simulator { + } + tiles (scale: 2) { + multiAttributeTile(name:"main", type:"generic", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState "temperature",label:'${currentValue}°', icon:"st.motion.motion.inactive", backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + standardTile("motion","device.motion", width: 2, height: 2) { + state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00a0dc" + state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + } + valueTile("humidity","device.humidity", width: 2, height: 2) { + state "humidity",label:'RH ${currentValue}%',unit:"%" + } + valueTile( + "illuminance","device.illuminance", width: 2, height: 2) { + state "luminosity", label:'LUX ${currentValue}%', unit:"%", backgroundColors:[ + [value: 0, color: "#000000"], + [value: 1, color: "#060053"], + [value: 12, color: "#3E3900"], + [value: 24, color: "#8E8400"], + [value: 48, color: "#C5C08B"], + [value: 60, color: "#DAD7B6"], + [value: 84, color: "#F3F2E9"], + [value: 100, color: "#F3F2E9"] + ] + } + standardTile("acceleration", "device.acceleration", width: 2, height: 2) { + state("active", label:'tamper', icon:"st.motion.acceleration.active", backgroundColor:"#f39c12") + state("inactive", label:'clear', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + standardTile("tamper", "device.tamper", width: 2, height: 2) { + state("detected", label:'tamper\nactive', icon:"st.contact.contact.open", backgroundColor:"#e86d13") + state("clear", label:'tamper\nclear', icon:"st.contact.contact.closed", backgroundColor:"#cccccc") + } + valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + valueTile("currentFirmware", "device.currentFirmware", width: 2, height: 2) { + state "currentFirmware", label:'Firmware: v${currentValue}', unit:"" + } + standardTile("refresh", "device.switch", decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh-icon" + } + 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" + } + valueTile( + "batteryRuntime", "device.batteryRuntime", decoration: "flat", width: 2, height: 2) { + state "batteryRuntime", label:'Battery: ${currentValue} Double tap to reset counter', unit:"", action:"resetBatteryRuntime" + } + standardTile( + "statusText2", "device.statusText2", decoration: "flat", width: 2, height: 2) { + state "statusText2", label:'${currentValue}', unit:"", action:"resetBatteryRuntime" + } + + main([ + "main", "motion" + ]) + details([ + "main", + "humidity","illuminance", "battery", + "motion","tamper", "refresh", + "statusText2", "configure", + ]) + } +} + +def parse(String description) +{ + def result = [] + switch(description){ + case ~/Err 106.*/: + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + break + case "updated": + log.debug "Update is hit when the device is paired." + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 43200, nodeid:zwaveHubNodeId).format()) + result << response(zwave.batteryV1.batteryGet().format()) + result << response(zwave.versionV1.versionGet().format()) + result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + result << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format()) + result << response(configure()) + break + default: + def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + break + } + + //log.debug "${description} parsed to ${result}" + + updateStatus() + + if ( result[0] != null ) { result } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) + state.sec = 1 + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + response(configure()) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'" +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + log.debug "WakeUpIntervalReport ${cmd.toString()}" + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + log.debug cmd + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastBatteryReport = now() + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + state.realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(state.realTemperature) + map.unit = getTemperatureScale() + break; + case 3: + map.name = "illuminance" + state.realLuminance = cmd.scaledSensorValue.toInteger() + map.value = getAdjustedLuminance(cmd.scaledSensorValue.toInteger()) + map.unit = "lux" + break; + case 5: + map.name = "humidity" + state.realHumidity = cmd.scaledSensorValue.toInteger() + map.value = getAdjustedHumidity(cmd.scaledSensorValue.toInteger()) + map.unit = "%" + break; + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def motionEvent(value) { + def map = [name: "motion"] + if (value != 0) { + map.value = "active" + map.descriptionText = "$device.displayName detected motion" + } else { + map.value = "inactive" + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + motionEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + motionEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + result << createEvent(name: "tamper", value: "clear", descriptionText: "$device.displayName tamper cleared") + result << createEvent(name: "acceleration", value: "inactive", descriptionText: "$device.displayName tamper cleared") + break + case 3: + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved") + result << createEvent(name: "acceleration", value: "active", descriptionText: "$device.displayName was moved") + break + case 7: + result << motionEvent(1) + break + } + } else { + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + log.debug "Device ${device.displayName} woke up" + + def request = update_needed_settings() + + if (!state.lastBatteryReport || (now() - state.lastBatteryReport) / 60000 >= 60 * 24) + { + log.debug "Over 24hr since last battery report. Requesting report" + request << zwave.batteryV1.batteryGet() + } + + if(request != []){ + response(commands(request) + ["delay 5000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } else { + log.debug "No commands to send" + response([zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } +} + +def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport $cmd" + if (zwaveHubNodeId in cmd.nodeId) state."association${cmd.groupingIdentifier}" = true + else state."association${cmd.groupingIdentifier}" = false +} + +def zwaveEvent(hubitat.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd){ + log.debug "Firmware Report ${cmd.toString()}" +} + +def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { + log.debug cmd + if(cmd.applicationVersion && cmd.applicationSubVersion) { + def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}" + state.needfwUpdate = "false" + updateDataValue("firmware", firmware) + createEvent(name: "currentFirmware", value: firmware) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + log.debug "Unknown Z-Wave Command: ${cmd.toString()}" +} + +def refresh() { + log.debug "$device.displayName - refresh()" + + def request = [] + if (state.lastRefresh != null && now() - state.lastRefresh < 5000) { + log.debug "Refresh Double Press" + def configuration = new XmlSlurper().parseText(configuration_model()) + configuration.Value.each + { + if ( "${it.@setting_type}" == "zwave" ) { + request << zwave.configurationV1.configurationGet(parameterNumber: "${it.@index}".toInteger()) + } + } + request << zwave.wakeUpV1.wakeUpIntervalGet() + } + state.lastRefresh = now() + request << zwave.batteryV1.batteryGet() + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1) + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5, scale:1) + commands(request) +} + +def ping() { + log.debug "$device.displayName - ping()" + return command(zwave.batteryV1.batteryGet()) +} + +def configure() { + log.debug "Configuring Device For SmartThings Use" + def cmds = [] + + cmds += update_needed_settings() + commands(cmds) +} + +def updated() +{ + log.debug "updated() is being called" + sendEvent(name: "checkInterval", value: 2 * 12 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + if (state.realTemperature != null) sendEvent(name:"temperature", value: getAdjustedTemp(state.realTemperature)) + if (state.realHumidity != null) sendEvent(name:"humidity", value: getAdjustedHumidity(state.realHumidity)) + if (state.realLuminance != null) sendEvent(name:"illuminance", value: getAdjustedLuminance(state.realLuminance)) + + updateStatus() + + state.needfwUpdate = "" + + def cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + response(commands(cmds)) +} + +def update_needed_settings() +{ + def currentProperties = state.currentProperties ?: [:] + def configuration = new XmlSlurper().parseText(configuration_model()) + + def isUpdateNeeded = "NO" + + def cmds = [] + + if(!state.needfwUpdate || state.needfwUpdate == "") { + log.debug "Requesting device firmware version" + cmds << zwave.versionV1.versionGet() + } + + if(!state.association1){ + log.debug "Setting association group 1" + cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + cmds << zwave.associationV2.associationGet(groupingIdentifier:1) + } + + if(state.wakeInterval == null || state.wakeInterval != 43200){ + log.debug "Setting Wake Interval to 43200" + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: 43200, nodeid:zwaveHubNodeId) + cmds << zwave.wakeUpV1.wakeUpIntervalGet() + } + + if (device.currentValue("temperature") == null) { + log.debug "Temperature report not yet received. Sending request" + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + } + if (device.currentValue("humidity") == null) { + log.debug "Humidity report not yet received. Sending request" + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5, scale:1) + } + if (device.currentValue("illuminance") == null) { + log.debug "Illuminance report not yet received. Sending request" + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1) + } + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + log.debug "Current value of parameter ${it.@index} is unknown" + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + isUpdateNeeded = "YES" + } + else if (settings."${it.@index}" != null && convertParam(it.@index.toInteger(), cmd2Integer(currentProperties."${it.@index}")) != settings."${it.@index}".toInteger()) + { + isUpdateNeeded = "YES" + + log.debug "Parameter ${it.@index} will be updated to " + settings."${it.@index}" + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + switch (number){ + case 201: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 202: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + case 203: + if (value < 0) + 65536 + value + else if (value > 1000) + value - 65536 + else + value + break + case 204: + if (value < 0) + 256 + value + else if (value > 100) + value - 256 + else + value + break + default: + value + break + } +} + +def update_current_properties(cmd) +{ + + def currentProperties = state.currentProperties ?: [:] + def convertedConfigurationValue = convertParam("${cmd.parameterNumber}".toInteger(), cmd2Integer(cmd.configurationValue)) + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (settings."${cmd.parameterNumber}".toInteger() == convertedConfigurationValue) + { + sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true) + } + else + { + sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true) + } + } + state.currentProperties = currentProperties +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break +} +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +private setConfigured() { + updateDataValue("configured", "true") +} + +private isConfigured() { + getDataValue("configured") == "true" +} + +private command(hubitat.zwave.Command cmd) { + + if (state.sec && cmd.toString()) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + input "${it.@index}", "number", + title:"${it.@label}\n" + "${it.Help}", + range: "${it.@min}..${it.@max}", + defaultValue: "${it.@value}" + 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}", + options: items + break + case "decimal": + input "${it.@index}", "decimal", + title:"${it.@label}\n" + "${it.Help}", + //range: "${it.@min}..${it.@max}", + defaultValue: "${it.@value}" + break + case "boolean": + input "${it.@index}", "boolean", + title:"${it.@label}\n" + "${it.Help}", + //range: "${it.@min}..${it.@max}", + defaultValue: "${it.@value}" + break + } + } +} + +private getBatteryRuntime() { + def currentmillis = now() - state.batteryRuntimeStart + def days=0 + def hours=0 + def mins=0 + def secs=0 + secs = (currentmillis/1000).toInteger() + mins=(secs/60).toInteger() + hours=(mins/60).toInteger() + days=(hours/24).toInteger() + secs=(secs-(mins*60)).toString().padLeft(2, '0') + mins=(mins-(hours*60)).toString().padLeft(2, '0') + hours=(hours-(days*24)).toString().padLeft(2, '0') + + + if (days>0) { + return "$days days and $hours:$mins:$secs" + } else { + return "$hours:$mins:$secs" + } +} + +private getRoundedInterval(number) { + double tempDouble = (number / 60) + if (tempDouble == tempDouble.round()) + return (tempDouble * 60).toInteger() + else + return ((tempDouble.round() + 1) * 60).toInteger() +} + +private getAdjustedTemp(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."302") { + return value = value + Math.round(settings."302" * 100) /100 + } else { + return value + } + +} + +private getAdjustedHumidity(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."303") { + return value = value + Math.round(settings."303" * 100) /100 + } else { + return value + } + +} + +private getAdjustedLuminance(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."304") { + return value = value + Math.round(settings."304" * 100) /100 + } else { + return value + } + +} + +def resetBatteryRuntime() { + if (state.lastReset != null && now() - state.lastReset < 5000) { + log.debug "Reset Double Press" + state.batteryRuntimeStart = now() + updateStatus() + } + state.lastReset = now() +} + +private updateStatus(){ + def result = [] + if(state.batteryRuntimeStart != null){ + sendEvent(name:"batteryRuntime", value:getBatteryRuntime(), displayed:false) + if (device.currentValue('currentFirmware') != null){ + sendEvent(name:"statusText2", value: "Firmware: v${device.currentValue('currentFirmware')} - Battery: ${getBatteryRuntime()} Double tap to reset", displayed:false) + } else { + sendEvent(name:"statusText2", value: "Battery: ${getBatteryRuntime()} Double tap to reset", displayed:false) + } + } else { + state.batteryRuntimeStart = now() + } + + String statusText = "" + if(device.currentValue('humidity') != null) + statusText = "RH ${device.currentValue('humidity')}% - " + if(device.currentValue('illuminance') != null) + statusText = statusText + "LUX ${device.currentValue('illuminance')} - " + + if (statusText != ""){ + statusText = statusText.substring(0, statusText.length() - 2) + sendEvent(name:"statusText", value: statusText, displayed:false) + } +} + +def configuration_model() +{ +''' + + + + + + + + + + +Default: Off for Temp, On with Motion + + + + + + + + +Number of MINUTES to wait to report motion cleared after a motion event if there is no motion detected. +Range: 1~255. +Default: 3 Minutes +Firmware 17.09+: Number of SECONDS to wait to report motion cleared after a motion event if there is no motion detected. +Range: 15~60. +Default: 15 Seconds + + + + +A value from 1-7, from low to high sensitivity +Range: 1~7 +Default: 3 + + + + +Range: 1~50 +Default: 10 +Note: +The amount by which the temperature must change in order for the sensor to send a report. Values are in .10 (tenths) so 1 = .1, 5 = .5, etc. + + + + +Range: 1~50 +Default: 10 +Note: +The amount by which the humidity must change in order for the sensor to send a report. Value is in %. + + + + +Range: 0,5~50 +Default: 10 +Note: +The amount by which the luminance must change in order for the sensor to send a report. Value is in %. + + + + +Range: None +Default: 0 +Note: +1. The calibration value = standard value - measure value. +E.g. If measure value = 85.3F and the standard value = 83.2F, so the calibration value = 83.2F - 85.3F = -2.1F. +If the measure value = 60.1F and the standard value = 63.2F, so the calibration value = 63.2F - 60.1℃ = 3.1F. + + + + +Range: None +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 80RH and the standard value = 75RH, so the calibration value = 75RH – 80RH = -5RH. +If the measure value = 85RH and the standard value = 90RH, so the calibration value = 90RH – 85RH = 5RH. + + + + +Range: None +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 80% Lux and the standard value = 75% Lux, so the calibration value = 75 – 80 = -5. +If the measure value = 85% Lux and the standard value = 90% Lux, so the calibration value = 90 – 85 = 5. + + + +''' +} \ No newline at end of file diff --git a/Drivers/zooz-contact-sensor.src/zooz-contact-sensor.groovy b/Drivers/zooz-contact-sensor.src/zooz-contact-sensor.groovy new file mode 100644 index 0000000..338903e --- /dev/null +++ b/Drivers/zooz-contact-sensor.src/zooz-contact-sensor.groovy @@ -0,0 +1,276 @@ +/** + * + * zooZ Contact Sensor + * + * github: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2016-10-05 + * Copyright 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. + * + */ + +metadata { + definition (name: "zooZ Contact Sensor", namespace: "erocm123", author: "Eric Maycock") { + capability "Contact Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + + fingerprint mfr: "027A", prod: "0003", model: "0082" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x80,0x71,0x30,0x85,0x59,0x84,0x70" + + } + + simulator { + } + + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + } + } + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "contact" + details(["contact", "battery"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err 106")) { + if (state.sec) { + log.debug description + } else { + result = createEvent( + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + isStateChange: true, + ) + } + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "parsed '$description' to $result" + return result +} + +def updated() { + def cmds = [] + sendEvent(name: "checkInterval", value: 2 * 12 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + if (!state.MSR) { + cmds = [ + command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()), + "delay 1200", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ] + } else if (!state.lastbat) { + cmds = [] + } else { + cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + } + response(cmds) +} + +def configure() { + commands([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet(), + zwave.batteryV1.batteryGet() + ], 6000) +} + +def ping() { + log.debug "ping()" + log.debug "Battery Device - Not sending ping commands" +} + +def sensorValueEvent(value) { + if (value) { + createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") + } else { + createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) +{ + def result = [] + if (cmd.notificationType == 0x06 && cmd.event == 0x16) { + //result << sensorValueEvent(1) + } else if (cmd.notificationType == 0x06 && cmd.event == 0x17) { + //result << sensorValueEvent(0) + } else if (cmd.notificationType == 0x07) { + if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice door/window sensors + result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } else if (cmd.event == 0x07) { + if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion") + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + def cmds = [] + if (!state.MSR) { + cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 1200" + } + if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { + cmds << command(zwave.batteryV1.batteryGet()) + } else { + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } + [event, response(cmds)] +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel <= 100? cmd.batteryLevel : 100 + } + state.lastbat = now() + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + retypeBasedOnMSR() + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + + if (msr == "011A-0601-0901") { // Enerwave motion doesn't always get the associationSet that the hub sends on join + result << response(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + } else if (!device.currentState("battery")) { + if (msr == "0086-0102-0059") { + result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) + } else { + result << response(command(zwave.batteryV1.batteryGet())) + } + } + + result +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + // log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec == 1) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def retypeBasedOnMSR() { + switch (state.MSR) { + case "0086-0002-002D": + log.debug "Changing device type to Z-Wave Water Sensor" + setDeviceType("Z-Wave Water Sensor") + break + case "011F-0001-0001": // Schlage motion + case "014A-0001-0001": // Ecolink motion + case "014A-0004-0001": // Ecolink motion + + case "0060-0001-0002": // Everspring SP814 + case "0060-0001-0003": // Everspring HSP02 + case "011A-0601-0901": // Enerwave ZWN-BPC + log.debug "Changing device type to Z-Wave Motion Sensor" + setDeviceType("Z-Wave Motion Sensor") + break + case "013C-0002-000D": // Philio multi + + log.debug "Changing device type to 3-in-1 Multisensor Plus (SG)" + setDeviceType("3-in-1 Multisensor Plus (SG)") + break + case "0109-2001-0106": // Vision door/window + log.debug "Changing device type to Z-Wave Plus Door/Window Sensor" + setDeviceType("Z-Wave Plus Door/Window Sensor") + break + case "0109-2002-0205": // Vision Motion + log.debug "Changing device type to Z-Wave Plus Motion/Temp Sensor" + setDeviceType("Z-Wave Plus Motion/Temp Sensor") + break + } +} \ No newline at end of file diff --git a/Drivers/zooz-mini-plug.src/zooz-mini-plug.groovy b/Drivers/zooz-mini-plug.src/zooz-mini-plug.groovy new file mode 100644 index 0000000..e3f0963 --- /dev/null +++ b/Drivers/zooz-mini-plug.src/zooz-mini-plug.groovy @@ -0,0 +1,507 @@ +/** + * + * zooZ Mini Plug + * + * github: Eric Maycock (erocm123) + * email: erocmail@gmail.com + * Date: 2016-10-05 + * Copyright Eric Maycock + * + * Code has elements from other community source @CyrilPeponnet (Z-Wave Parameter Sync). Includes all + * configuration parameters and ease of advanced configuration. + * + * 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. + * + */ + +metadata { + definition (name: "zooZ Mini Plug", namespace: "erocm123", author: "Eric Maycock") { + capability "Energy Meter" + capability "Voltage Measurement" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Polling" + capability "Refresh" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + command "reset" + + attribute "needUpdate", "string" + attribute "amperage", "number" + + fingerprint mfr: "027A", prod: "0003", model: "0087" + fingerprint deviceId: "0x1001", inClusters: "0x20,0x25,0x27,0x72,0x86,0x70,0x85,0x59,0x5A,0x73,0x71,0x32,0x5E" + + } + + 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()) + } + + simulator { + } + + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", 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" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("power", "device.power", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("voltage", "device.voltage", width: 2, height: 2) { + state "default", label:'${currentValue} V' + } + valueTile("amperage", "device.amperage", width: 2, height: 2) { + state "default", label:'${currentValue} A' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + standardTile("energy", "device.energy", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + 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" + } + + main "switch" + details (["switch", "power", "amperage", "voltage", "energy", "refresh", "configure", "reset"]) + } +} + +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [] + + cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + if (cmds != []) response(commands(cmds)) +} + +def parse(String description) { + def result = null + if(description == "updated") return + def cmd = zwave.parse(description, [0x20: 1, 0x32: 1, 0x72: 2]) + if (cmd) { + result = zwaveEvent(cmd) + } + + return result +} + +def zwaveEvent(hubitat.zwave.commands.meterv1.MeterReport cmd) { + logging("MeterReport $cmd") + def event + if (cmd.scale == 0) { + if (cmd.meterType == 161) { + event = createEvent(name: "voltage", value: cmd.scaledMeterValue, unit: "V") + } else if (cmd.meterType == 33) { + event = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } + } else if (cmd.scale == 1) { + event = createEvent(name: "amperage", value: cmd.scaledMeterValue, unit: "A") + } else if (cmd.scale == 2) { + event = createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } + runIn(1, "updateStatus") + return event +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + def evt = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") + if (evt.isStateChange) { + [evt, response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()])] + } else { + evt + } +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr") + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + + result +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("$device.displayName: Unhandled: $cmd") + [:] +} + +private updateStatus(){ + + String statusText = "" + + if(device.currentValue('power') != null) + statusText = "${device.currentValue('power')} W - " + + if(device.currentValue('amperage') != null) + statusText = statusText + "${device.currentValue('amperage')} A - " + + if(device.currentValue('voltage') != null) + statusText = statusText + "${device.currentValue('voltage')} V - " + + if(device.currentValue('energy') != null) + statusText = statusText + "${device.currentValue('energy')} kWh - " + + if (statusText != ""){ + statusText = statusText.substring(0, statusText.length() - 2) + sendEvent(name:"statusText", value: statusText, displayed:false) + } +} + +def on() { + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def off() { + [ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def poll() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def refresh() { + logging("refresh()") + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def ping() { + logging("ping()") + refresh() +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) commands(cmds) +} + +def reset() { + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ] +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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 + } + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (settings."${cmd.parameterNumber}".toInteger() == convertParam("${cmd.parameterNumber}".toInteger(), cmd2Integer(cmd.configurationValue))) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + else if (settings."${it.@index}" != null && convertParam(it.@index.toInteger(), cmd2Integer(currentProperties."${it.@index}")) != settings."${it.@index}".toInteger()) + { + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + settings."${it.@index}") + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + switch (number){ + case 201: + value + break + default: + value + break + } +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def configuration_model() +{ +''' + + + +Enable or disable the sending of meter reports. +Default: Enable + + + + + + +Number of seconds metering report is sent to controller +Range: 1~65536 +Default: 300 + + + + +Maximum number of Amperes to be accepted by the Mini Plug. +Range: 1~16 +Default: 13 + + + + +Maximum number of Amperes that will trigger overload LED notification on the Mini Plug. +Range: 1~13 +Default: 12 + + + + +Enable or disable LED Notifications +Default: Enable + + + + + + +Percentage of change in power consumption, voltage, electricity, or energy usage to be reported to controller by the Mini Plug +Range: 1~100 +Default: 5 + + + + +Default: Previous State + + + + + + +Enable or disable the auto turn-off timer +Default: Disabled + + + + + + +After turning on the Mini Plug, the device will automatically turn off after this number of minutes (only if "Auto Turn-Off Timer" is enabled). +Range: 1~65535 +Default: 1 + + + + +Enable or disable manual control +Default: Enabled + + + + + + + + + +''' +} diff --git a/Drivers/zooz-mini-sensor.src/zooz-mini-sensor.groovy b/Drivers/zooz-mini-sensor.src/zooz-mini-sensor.groovy new file mode 100644 index 0000000..51c9d99 --- /dev/null +++ b/Drivers/zooz-mini-sensor.src/zooz-mini-sensor.groovy @@ -0,0 +1,631 @@ +/** + * + * zooZ Mini Sensor + * + * github: Eric Maycock (erocm123) + * Date: 2016-10-05 + * Copyright Eric Maycock + * + * Code has elements from other community source @CyrilPeponnet (Z-Wave Parameter Sync). Includes all + * configuration parameters and ease of advanced configuration. Added software based light offsets. + * + * 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. + * + */ + + metadata { + definition (name: "zooZ Mini Sensor", namespace: "erocm123", author: "Eric Maycock") { + capability "Motion Sensor" + capability "Illuminance Measurement" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Acceleration Sensor" + capability "Tamper Alert" + capability "Health Check" + + attribute "needUpdate", "string" + + fingerprint mfr: "027A", prod: "0003", model: "0083" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x5A,0x73,0x80,0x31,0x71,0x30,0x70,0x85,0x59,0x84" + + } + 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()) + } + + simulator { + } + + tiles (scale: 2) { + multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ + tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00a0dc" + attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + valueTile( + "illuminance","device.illuminance", width: 2, height: 2) { + state "luminosity",label:'LUX ${currentValue}', unit:"lux", backgroundColors:[ + [value: 0, color: "#000000"], + [value: 1, color: "#060053"], + [value: 12, color: "#3E3900"], + [value: 24, color: "#8E8400"], + [value: 48, color: "#C5C08B"], + [value: 60, color: "#DAD7B6"], + [value: 84, color: "#F3F2E9"], + [value: 100, color: "#FFFFFF"] + ] + } + valueTile("battery", "device.battery", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + 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" + } + + main([ + "motion" + ]) + details([ + "motion", + "illuminance", "battery", "configure", + ]) + } +} + +def parse(String description) +{ + def result = [] + switch(description){ + case ~/Err 106.*/: + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + break + case "updated": + logging("Update is hit when the device is paired.") + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 43200, nodeid:zwaveHubNodeId).format()) + result << response(zwave.batteryV1.batteryGet().format()) + result << response(zwave.versionV1.versionGet().format()) + result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + result << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format()) + result << response(configure()) + break + default: + def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + break + } + + updateStatus() + + if ( result[0] != null ) { result } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) + state.sec = 1 + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + response(configure()) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) +{ + logging("WakeUpIntervalReport ${cmd.toString()}") + state.wakeInterval = cmd.seconds +} + +def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { + logging("Battery Report: $cmd") + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel <= 100? cmd.batteryLevel : 100 + } + state.lastBatteryReport = now() + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + state.realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(state.realTemperature) + map.unit = getTemperatureScale() + break; + case 3: + map.name = "illuminance" + state.realLuminance = cmd.scaledSensorValue.toInteger() + map.value = getAdjustedLuminance(cmd.scaledSensorValue.toInteger()) + map.unit = "lux" + break; + case 5: + map.name = "humidity" + state.realHumidity = cmd.scaledSensorValue.toInteger() + map.value = getAdjustedHumidity(cmd.scaledSensorValue.toInteger()) + map.unit = "%" + break; + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def motionEvent(value) { + def map = [name: "motion"] + if (value != 0) { + map.value = "active" + map.descriptionText = "$device.displayName detected motion" + } else { + map.value = "inactive" + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + logging("SensorBinaryReport: $cmd") + motionEvent(cmd.sensorValue) +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { + logging("BasicSet: $cmd") + motionEvent(cmd.value) +} + +def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { + logging("NotificationReport: $cmd") + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + result << createEvent(name: "tamper", value: "clear", descriptionText: "$device.displayName tamper cleared") + result << createEvent(name: "acceleration", value: "inactive", descriptionText: "$device.displayName tamper cleared") + break + case 3: + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved") + result << createEvent(name: "acceleration", value: "active", descriptionText: "$device.displayName was moved") + break + case 7: + result << motionEvent(1) + break + } + } else { + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + logging("Device ${device.displayName} woke up") + + def request = sync_properties() + + if (!state.lastBatteryReport || (now() - state.lastBatteryReport) / 60000 >= 60 * 24) + { + logging("Over 24hr since last battery report. Requesting report") + request << zwave.batteryV1.batteryGet() + } + + if(request != []){ + response(commands(request) + ["delay 5000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } else { + logging("No commands to send") + response([zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) + } +} + +def zwaveEvent(hubitat.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd){ + logging("Firmware Report ${cmd.toString()}") +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("Unknown Z-Wave Command: ${cmd.toString()}") +} + +def configure() { + logging("Configuring Device For SmartThings Use") + def cmds = [] + cmds = update_needed_settings() + if (cmds != []) commands(cmds) +} + +def updated() +{ + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 12 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + if (state.realLuminance != null) sendEvent(name:"illuminance", value: getAdjustedLuminance(state.realLuminance)) + updateStatus() + def cmds = [] + cmds = update_needed_settings() + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + if (cmds != []) response(commands(cmds)) +} + +def sync_properties() +{ + def currentProperties = state.currentProperties ?: [:] + def configuration = new XmlSlurper().parseText(configuration_model()) + + def cmds = [] + + if(state.wakeInterval == null || state.wakeInterval != getRoundedInterval(settings.wake)){ + logging("Setting Wake Interval to ${getRoundedInterval(settings.wake)}") + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: getRoundedInterval(settings.wake), nodeid:zwaveHubNodeId) + cmds << zwave.wakeUpV1.wakeUpIntervalGet() + } + + configuration.Value.each + { + if ( "${it.@setting_type}" == "zwave" ) { + if (! currentProperties."${it.@index}" || currentProperties."${it.@index}" == null) + { + logging("Looking for current value of parameter ${it.@index}") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + if (device.currentValue("needUpdate") == "YES") { cmds += update_needed_settings() } + return cmds +} + +def convertParam(number, value) { + switch (number){ + case 201: + value + default: + value + break + } +} + +def update_current_properties(cmd) +{ + def currentProperties = state.currentProperties ?: [:] + def convertedConfigurationValue = convertParam("${cmd.parameterNumber}".toInteger(), cmd2Integer(cmd.configurationValue)) + currentProperties."${cmd.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (settings."${cmd.parameterNumber}".toInteger() == convertedConfigurationValue) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + logging("Current value of parameter ${it.@index} is unknown") + isUpdateNeeded = "YES" + } + else if (settings."${it.@index}" != null && convertParam(it.@index.toInteger(), cmd2Integer(currentProperties."${it.@index}")) != settings."${it.@index}".toInteger()) + { + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + settings."${it.@index}") + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break +} +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +private setConfigured() { + updateDataValue("configured", "true") +} + +private isConfigured() { + getDataValue("configured") == "true" +} + +private command(hubitat.zwave.Command cmd) { + + if (state.sec && cmd.toString()) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1000) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def ping() { + logging("ping()") + logging("Battery Device - Not sending ping commands") +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + input "${it.@index}", "number", + title:"${it.@label}\n" + "${it.Help}", + range: "${it.@min}..${it.@max}", + defaultValue: "${it.@value}" + 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}", + options: items + break + case "decimal": + input "${it.@index}", "decimal", + title:"${it.@label}\n" + "${it.Help}", + //range: "${it.@min}..${it.@max}", + defaultValue: "${it.@value}" + break + case "boolean": + input "${it.@index}", "boolean", + title:"${it.@label}\n" + "${it.Help}", + //range: "${it.@min}..${it.@max}", + defaultValue: "${it.@value}" + break + } + } +} + +private getBatteryRuntime() { + def currentmillis = now() - state.batteryRuntimeStart + def days=0 + def hours=0 + def mins=0 + def secs=0 + secs = (currentmillis/1000).toInteger() + mins=(secs/60).toInteger() + hours=(mins/60).toInteger() + days=(hours/24).toInteger() + secs=(secs-(mins*60)).toString().padLeft(2, '0') + mins=(mins-(hours*60)).toString().padLeft(2, '0') + hours=(hours-(days*24)).toString().padLeft(2, '0') + + + if (days>0) { + return "$days days and $hours:$mins:$secs" + } else { + return "$hours:$mins:$secs" + } +} + +private getRoundedInterval(number) { + double tempDouble = (number / 60) + if (tempDouble == tempDouble.round()) + return (tempDouble * 60).toInteger() + else + return ((tempDouble.round() + 1) * 60).toInteger() +} + +private getAdjustedLuminance(value) { + + value = Math.round((value as Double) * 100) / 100 + + if (settings."304") { + return value = value + Math.round(settings."304" * 100) /100 + } else { + return value + } + +} + +private updateStatus(){ + def result = [] + + String statusText = "" + if(device.currentValue('illuminance') != null) + statusText = statusText + "LUX ${device.currentValue('illuminance')} - " + + if (statusText != ""){ + statusText = statusText.substring(0, statusText.length() - 2) + sendEvent(name:"statusText", value: statusText, displayed:false) + } +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +def configuration_model() +{ +''' + + + +Motion detection sensitivity +Range: 8~255 +Default: 12 + + + + +Number of seconds the associated device to stay ON for after being triggered by the sensor before it automatically turns OFF +Range: 5~600 +Default: 30 + + + + +Associated device will turn ON when triggered by the sensor (255) or Brightness level (percentage) the associated device will turn ON to when triggered by the sensor (1-99). +Range: 1~99,255 +Default: 255 + + + + +Enable or disable motion detection. +Default: Enabled + + + + + + +Light level change (in LUX) to set off light trigger +Range: 0~1000 +Default: 100 + + + + +Number of seconds for motion trigger interval +Range: 1~8 +Default: 8 + + + + +Light polling interval in seconds +Range: 60~360000 +Default: 180 + + + + +Enable or disable the light trigger +Default: Disabled + + + + + + +Light level change (in LUX) to be reported by the sensor to the controller +Range: 0~255 +Default: 20 + + + + +Enable or disable LED notifications +Default: Enabled + + + + + + +Range: None +Default: 0 +Note: +The calibration value = standard value - measure value. +E.g. If measure value = 80% Lux and the standard value = 75% Lux, so the calibration value = 75 – 80 = -5. +If the measure value = 85% Lux and the standard value = 90% Lux, so the calibration value = 90 – 85 = 5. + + + + +Set the wake interval for the device in seconds. Decreasing this value will reduce battery life. +Range: 240~65536 +Default: 43200 (12 Hours) + + + + + + + +''' +} \ No newline at end of file diff --git a/Drivers/zooz-power-strip.src/zooz-power-strip.groovy b/Drivers/zooz-power-strip.src/zooz-power-strip.groovy new file mode 100644 index 0000000..989f177 --- /dev/null +++ b/Drivers/zooz-power-strip.src/zooz-power-strip.groovy @@ -0,0 +1,287 @@ +/** + * + * Adaptation of my Aeon SmartStrip handler that was a heavily modified version of the SmartThing provided Aeon handler + * + * Supports a few things not found in other handlers: + * - Instant status updates when button on power strip is pressed + * - All On & All Off commands controlled by main switch + * + * Copyright 2016 Eric Maycock + * + * zooZ Power Strip ZEN20 + * + * 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. + * + */ +metadata { + definition (name: "zooZ Power Strip", namespace: "erocm123", author: "Eric Maycock") { + capability "Switch" + capability "Refresh" + capability "Configuration" + capability "Actuator" + capability "Sensor" + capability "Health Check" + + (1..5).each { n -> + attribute "switch$n", "enum", ["on", "off"] + command "on$n" + command "off$n" + } + + fingerprint manufacturer: "015D", prod: "0651", model: "F51C" + fingerprint deviceId: "0x1004", inClusters: "0x5E,0x85,0x59,0x5A,0x72,0x60,0x8E,0x73,0x27,0x25,0x86" + } + + simulator { + + } + + preferences { + input("enableDebugging", "boolean", title:"Enable Debugging", value:false, required:false, displayDuringSetup:false) + } + + // tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + valueTile("statusText", "statusText", inactiveLabel: false, width: 2, height: 2) { + state "statusText", label:'${currentValue}', backgroundColor:"#ffffff" + } + + (1..5).each { n -> + standardTile("switch$n", "switch$n", canChangeIcon: true, decoration: "flat", width: 2, height: 2) { + state "on", label: "switch$n", action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" + state "off", label: "switch$n", action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#cccccc" + } + + } + + main(["switch", "switch1", "switch2", "switch3", "switch4", "switch5"]) + details(["switch", + "switch1","switch2","switch3", + "switch4","switch5","refresh", + ]) + } +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x60: 3, 0x32: 3, 0x25: 1, 0x20: 1]) + //log.debug "Command: ${cmd}" + if (cmd) { + result += zwaveEvent(cmd, null) + } + } + + def statusTextmsg = "" + if (device.currentState('power') && device.currentState('energy')) statusTextmsg = "${device.currentState('power').value} W ${device.currentState('energy').value} kWh" + sendEvent("name":"statusText", "value":statusTextmsg) + + //log.debug "parsed '${description}' to ${result.inspect()}" + + result +} + +def endpointEvent(endpoint, map) { + logging("endpointEvent($endpoint, $map)") + if (endpoint) { + map.name = map.name + endpoint.toString() + } + createEvent(map) +} + +def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep) { + logging("MultiChannelCmdEncap") + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + if (encapsulatedCommand.commandClassId == 0x32) { + // Metered outlets are numbered differently than switches + Integer endpoint = cmd.sourceEndPoint + if (endpoint > 2) { + zwaveEvent(encapsulatedCommand, endpoint - 2) + } else if (endpoint == 0) { + zwaveEvent(encapsulatedCommand, 0) + } else if (endpoint == 1 || endpoint == 2) { + zwaveEvent(encapsulatedCommand, endpoint + 4) + } else { + log.debug "Ignoring metered outlet ${endpoint} msg: ${encapsulatedCommand}" + [] + } + } else { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } + } +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, endpoint) { + logging("BasicReport") + def cmds = [] + (1..5).each { n -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), n) + cmds << "delay 1000" + } + + return response(cmds) +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'") +} + +def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'") +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint) { + logging("SwitchBinaryReport") + def map = [name: "switch", value: (cmd.value ? "on" : "off")] + def events = [endpointEvent(endpoint, map)] + def cmds = [] + if (!endpoint && events[0].isStateChange) { + events += (1..4).collect { ep -> endpointEvent(ep, map.clone()) } + cmds << "delay 3000" + cmds += delayBetween((1..4).collect { ep -> encap(zwave.meterV3.meterGet(scale: 2), ep) }) + } else { + if (events[0].value == "on") { + events += [endpointEvent(null, [name: "switch", value: "on"])] + } else { + def allOff = true + (1..5).each { n -> + if (n != endpoint) { + if (device.currentState("switch${n}").value != "off") allOff = false + } + } + if (allOff) { + events += [endpointEvent(null, [name: "switch", value: "off"])] + } + } + + } + if(cmds) events << response(cmds) + events +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, ep) { + updateDataValue("MSR", String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)) + return null +} + +def zwaveEvent(hubitat.zwave.Command cmd, ep) { + logging("${device.displayName}: Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "")) +} + +def onOffCmd(value, endpoint = null) { + logging("onOffCmd($value, $endpoint)") + [ + encap(zwave.basicV1.basicSet(value: value), endpoint), + "delay 500", + encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint), + ] +} + +def on() { + def cmds = [] + cmds << zwave.switchAllV1.switchAllOn().format() + cmds << "delay 1000" + (1..5).each { endpoint -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint) + } + return cmds +} + +def off() { + def cmds = [] + cmds << zwave.switchAllV1.switchAllOff().format() + (1..5).each { endpoint -> + cmds << "delay 1000" + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint) + } + return cmds +} + +def on1() { onOffCmd(0xFF, 1) } +def on2() { onOffCmd(0xFF, 2) } +def on3() { onOffCmd(0xFF, 3) } +def on4() { onOffCmd(0xFF, 4) } +def on5() { onOffCmd(0xFF, 5) } + +def off1() { onOffCmd(0, 1) } +def off2() { onOffCmd(0, 2) } +def off3() { onOffCmd(0, 3) } +def off4() { onOffCmd(0, 4) } +def off5() { onOffCmd(0, 5) } + +def refresh() { + logging("refresh()") + def cmds = [] + + (1..5).each { endpoint -> + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint) + } + + delayBetween(cmds, 1000) +} + +def ping() { + logging("ping()") + refresh() +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("configure()") + sendEvent(name: "checkInterval", value: 2 * 60 * 12 * 60 + 5 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [ + zwave.versionV1.versionGet().format(), + zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(), + zwave.firmwareUpdateMdV2.firmwareMdGet().format(), + ] + + response(delayBetween(cmds, 1000)) +} + +def installed() { + logging("installed()") + configure() +} + +def updated() { + logging("updated()") + configure() +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private def logging(message) { + if (state.enableDebugging == "true") log.debug message +} \ No newline at end of file diff --git a/Drivers/zooz-zwave-smart-plug.src/zooz-zwave-smart-plug.groovy b/Drivers/zooz-zwave-smart-plug.src/zooz-zwave-smart-plug.groovy new file mode 100644 index 0000000..7aad961 --- /dev/null +++ b/Drivers/zooz-zwave-smart-plug.src/zooz-zwave-smart-plug.groovy @@ -0,0 +1,515 @@ +/** + * + * zooZ Z-Wave Smart Plug + * + * github: Eric Maycock (erocm123) + * Date: 2016-12-01 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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. + * + */ + +metadata { + definition (name: "zooZ Z-Wave Smart Plug", namespace: "erocm123", author: "Eric Maycock") { + capability "Energy Meter" + capability "Voltage Measurement" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Polling" + capability "Refresh" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + command "reset" + + attribute "needUpdate", "string" + attribute "amperage", "number" + + fingerprint mfr: "027A", prod: "0101", model: "000A" + + fingerprint deviceId: "0x1001", inClusters: "0x5E,0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x59,0x72,0x86,0x7A,0x73,0x5A" + + } + + 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()) + } + + simulator { + } + + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", 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" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("power", "device.power", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("voltage", "device.voltage", width: 2, height: 2) { + state "default", label:'${currentValue} V' + } + valueTile("amperage", "device.amperage", width: 2, height: 2) { + state "default", label:'${currentValue} A' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset\r\nkWh', action:"reset" + } + standardTile("energy", "device.energy", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + 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" + } + + main "switch" + details (["switch", "power", "amperage", "voltage", "energy", "refresh", "configure", "reset"]) + } +} + +def updated() +{ + state.enableDebugging = settings.enableDebugging + logging("updated() is being called") + sendEvent(name: "checkInterval", value: 2 * 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmds = [] + + cmds = update_needed_settings() + + sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) + + if (cmds != []) response(commands(cmds)) +} + +def parse(String description) { + def result = null + if(description == "updated") return + def cmd = zwave.parse(description, [0x20: 1, 0x32: 1, 0x72: 2]) + if (cmd) { + result = zwaveEvent(cmd) + } + + return result +} + +def zwaveEvent(hubitat.zwave.commands.meterv1.MeterReport cmd) { + logging("MeterReport $cmd") + def event + if (cmd.scale == 0) { + if (cmd.meterType == 161) { + event = createEvent(name: "voltage", value: cmd.scaledMeterValue, unit: "V") + } else if (cmd.meterType == 33) { + event = createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } + } else if (cmd.scale == 1) { + event = createEvent(name: "amperage", value: cmd.scaledMeterValue, unit: "A") + } else if (cmd.scale == 2) { + event = createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } + runIn(1, "updateStatus") + return event +} + +def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) +{ + def evt = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") + if (evt.isStateChange) { + [evt, response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()])] + } else { + evt + } +} + +def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr") + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + + result +} + +def zwaveEvent(hubitat.zwave.Command cmd) { + logging("$device.displayName: Unhandled: $cmd") + [:] +} + +private updateStatus(){ + + String statusText = "" + + if(device.currentValue('power') != null) + statusText = "${device.currentValue('power')} W - " + + if(device.currentValue('amperage') != null) + statusText = statusText + "${device.currentValue('amperage')} A - " + + if(device.currentValue('voltage') != null) + statusText = statusText + "${device.currentValue('voltage')} V - " + + if(device.currentValue('energy') != null) + statusText = statusText + "${device.currentValue('energy')} kWh - " + + if (statusText != ""){ + statusText = statusText.substring(0, statusText.length() - 2) + sendEvent(name:"statusText", value: statusText, displayed:false) + } +} + +def on() { + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def off() { + [ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def poll() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 1).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def refresh() { + logging("refresh()") + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 1).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def ping() { + logging("ping()") + refresh() +} + +def configure() { + state.enableDebugging = settings.enableDebugging + logging("Configuring Device For SmartThings Use") + def cmds = [] + + cmds = update_needed_settings() + + if (cmds != []) commands(cmds) +} + +def reset() { + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ] +} + +def generate_preferences(configuration_model) +{ + def configuration = new XmlSlurper().parseText(configuration_model) + + configuration.Value.each + { + switch(it.@type) + { + case ["byte","short","four"]: + 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 "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.parameterNumber}" = cmd.configurationValue + + if (settings."${cmd.parameterNumber}" != null) + { + if (settings."${cmd.parameterNumber}".toInteger() == convertParam("${cmd.parameterNumber}".toInteger(), cmd2Integer(cmd.configurationValue))) + { + 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 = new XmlSlurper().parseText(configuration_model()) + def isUpdateNeeded = "NO" + + configuration.Value.each + { + if ("${it.@setting_type}" == "zwave"){ + if (currentProperties."${it.@index}" == null) + { + isUpdateNeeded = "YES" + logging("Current value of parameter ${it.@index} is unknown") + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + else if (settings."${it.@index}" != null && convertParam(it.@index.toInteger(), cmd2Integer(currentProperties."${it.@index}")) != settings."${it.@index}".toInteger()) + { + isUpdateNeeded = "YES" + + logging("Parameter ${it.@index} will be updated to " + settings."${it.@index}") + def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()) + cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) + cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) + } + } + } + + sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) + return cmds +} + +def convertParam(number, value) { + switch (number){ + case 201: + value + break + default: + value + break + } +} + +private def logging(message) { + if (state.enableDebugging == null || state.enableDebugging == "true") log.debug "$message" +} + +/** +* Convert 1 and 2 bytes values to integer +*/ +def cmd2Integer(array) { + +switch(array.size()) { + case 1: + array[0] + break + case 2: + ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) + break + case 3: + ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) + break + case 4: + ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) + break + } +} + +def integer2Cmd(value, size) { + switch(size) { + case 1: + [value] + break + case 2: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + [value2, value1] + break + case 3: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + [value3, value2, value1] + break + case 4: + def short value1 = value & 0xFF + def short value2 = (value >> 8) & 0xFF + def short value3 = (value >> 16) & 0xFF + def short value4 = (value >> 24) & 0xFF + [value4, value3, value2, value1] + break + } +} + +def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) { + update_current_properties(cmd) + logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") +} + +private command(hubitat.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=1500) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def configuration_model() +{ +''' + + + +Number of Watts the appliance needs to go over for the change to be reported +Range: 1 to 65535 +Default: 50 + + + + +Percentage in power usage change the appliance needs to go over for the event to be reported +Range: 1 to 255 +Default: 10 + + + + +Number of seconds for the interval the Smart Plug will report power consumption +Range: 0,5 to 2678400 +Default: 30 + + + + +Number of seconds for the interval the Smart Plug will report energy usage +Range: 0,5 to 2678400 +Default: 300 + + + + +Number of seconds for the interval the Smart Plug will report voltage +Range: 0,5 to 2678400 +Default: 0 (Disabled) + + + + +Number of seconds for the interval the Smart Plug will report energy current +Range: 0,5 to 2678400 +Default: 0 (Disabled) + + + + +Range: 0 to 1 +Default: Enabled + + + + + + +Smart Plug remembers the status prior to power outage and turns back to it (default) +Range: 0 to 2 +Default: Previous + + + + + + + +On, Off, or Manual Only. When set to "Manual Only", notifications are only sent when physically pressing the button on the plug +Range: 0 to 2 +Default: On + + + + + + + +LED indicator will display power consumption whenever the device is plugged in (LED stays on at all times) or for 5 Seconds after it is turned on or off +Range: 0 to 1 +Default: Always + + + + + + + + + +''' +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..292c89a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Hubitat