From 848678482d44c9e5806ca9b3c70c2fb79fd4e029 Mon Sep 17 00:00:00 2001 From: Mark-C-uk Date: Tue, 13 Feb 2018 12:58:30 +0000 Subject: [PATCH 1/5] Update zwave-light-switch-multichannel.groovy --- .../zwave-light-switch-multichannel.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devicetypes/hongtat/zwave-light-switch-multichannel.src/zwave-light-switch-multichannel.groovy b/devicetypes/hongtat/zwave-light-switch-multichannel.src/zwave-light-switch-multichannel.groovy index 68748ba..cadbb38 100644 --- a/devicetypes/hongtat/zwave-light-switch-multichannel.src/zwave-light-switch-multichannel.groovy +++ b/devicetypes/hongtat/zwave-light-switch-multichannel.src/zwave-light-switch-multichannel.groovy @@ -35,7 +35,7 @@ */ metadata { - definition (name: "Z-Wave Light Switch Multichannel", namespace: "hongtat", author: "HongTat Tan") { + definition (name: "NEO Coolcam/MCOHome Z-Wave Light Switch Multichannel", namespace: "hongtat", author: "HongTat Tan") { capability "Actuator" capability "Switch" capability "Refresh" From 187a4a9070104b6e89126f1fd0d176544e08726f Mon Sep 17 00:00:00 2001 From: Mark-C-uk Date: Tue, 13 Feb 2018 12:59:30 +0000 Subject: [PATCH 2/5] Update zwave-light-switch-multichannel-child-device.groovy --- .../zwave-light-switch-multichannel-child-device.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devicetypes/hongtat/zwave-light-switch-multichannel-child-device.src/zwave-light-switch-multichannel-child-device.groovy b/devicetypes/hongtat/zwave-light-switch-multichannel-child-device.src/zwave-light-switch-multichannel-child-device.groovy index 0a9683b..60a9948 100644 --- a/devicetypes/hongtat/zwave-light-switch-multichannel-child-device.src/zwave-light-switch-multichannel-child-device.groovy +++ b/devicetypes/hongtat/zwave-light-switch-multichannel-child-device.src/zwave-light-switch-multichannel-child-device.groovy @@ -20,7 +20,7 @@ */ metadata { - definition (name: "Z-Wave Light Switch Multichannel Child Device", namespace: "hongtat", author: "HongTat Tan") { + definition (name: "NEO Coolcam/MCOHome Z-Wave Light Switch Multichannel Child Device", namespace: "hongtat", author: "HongTat Tan") { capability "Switch" capability "Actuator" capability "Sensor" From 10915c0020c5aa5cbb788ddf9050fb716012fa24 Mon Sep 17 00:00:00 2001 From: Mark Cockcroft Date: Thu, 22 Feb 2018 12:17:00 +0000 Subject: [PATCH 3/5] upload --- ...ome-zwave-light-switch-multichannel.groovy | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy diff --git a/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy b/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy new file mode 100644 index 0000000..cadbb38 --- /dev/null +++ b/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy @@ -0,0 +1,456 @@ +/** + * Z-Wave Light Switch Multichannel - Device Handler (parent) + * + * Copyright 2017 HongTat Tan + * + * + * Requires ** Z-Wave Light Switch Multichannel Child Device ** + * + * Tested on the following Z-Wave light switches: + * NEO Coolcam Z-Wave Light Switch (EU-1/2 Gang) + * MCOHome Z-Wave Light Switch (MH-312-EU) + * + * + * Version history: + * 1.0.3 (7/10/2017) - Add 2-way status reporting + * 1.0.2 (25/09/2017) - Add Health Check & Configure for child device + * 1.0.1 (23/09/2017) - Bug fix + * 1.0 (23/09/2017) - Initial Release + * + * Incorporates code from: + * Peter Major - https://github.com/petermajor/ + * Omar Shahbal - https://github.com/omarshahbal/ + * Eric Maycock - https://github.com/erocm123/ + * David Lomas - https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker + * + * 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: "NEO Coolcam/MCOHome Z-Wave Light Switch Multichannel", namespace: "hongtat", author: "HongTat Tan") { + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Health Check" + capability "Polling" + capability "Configuration" + capability "Sensor" + capability "Zw Multichannel" + + attribute "lastCheckin", "String" + + preferences { + section(title: "Check-in Interval") { + paragraph "Run a Check-in procedure every so often." + input "checkin", "enum", title: "Run Check-in procedure", options: ["Every 1 minute", "Every 5 minutes", "Every 10 minutes", "Every 15 minutes", "Every 30 minutes", "Every 1 hour"], description: "Allows check-in procedure to run every so often", defaultValue: "Every 1 minute", required: true + } + section(title: "Check-in Info") { + paragraph "Display check-in info" + input "checkinInfo", "enum", title: "Show last Check-in info", options: ["Hide", "MM/dd/yyyy h:mm", "MM-dd-yyyy h:mm", "dd/MM/yyyy h:mm", "dd-MM-yyyy h:mm"], description: "Show last check-in info.", defaultValue: "dd/MM/yyyy h:mm", required: true + } + } + + //fingerprint inClusters: "0x25", deviceJoinName: "Z-Wave Light Switch" + fingerprint mfr:"0258", prod:"0003", model:"108C", deviceJoinName: "NEO Coolcam Light Switch (1-CH)" + fingerprint mfr:"0258", prod:"0003", model:"108B", deviceJoinName: "NEO Coolcam Light Switch (2-CH)" + fingerprint mfr:"015F", prod:"3102", model:"0201", deviceJoinName: "MCOHome Light Switch S311 (1-CH)" + fingerprint mfr:"015F", prod:"3102", model:"0202", deviceJoinName: "MCOHome Light Switch S312 (2-CH)" + fingerprint mfr:"015F", prod:"3102", model:"0204", deviceJoinName: "MCOHome Light Switch S314 (4-CH)" + fingerprint mfr:"015F", prod:"4102", model:"0201", deviceJoinName: "MCOHome Light Switch S411 (1-CH)" + fingerprint mfr:"015F", prod:"4102", model:"0202", deviceJoinName: "MCOHome Light Switch S412 (2-CH)" + } + simulator { + // TODO: define status and reply messages here + } + 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 "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.lastCheckin", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}') + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + standardTile("configure", "device.default", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + main(["switch"]) + details(["switch", "refresh", "configure"]) + } +} + +private getCommandClassVersions() { + [ + 0x20: 1, // Basic (v1) + 0x25: 1, // Switch Binary (v1) + 0x27: 1, // Switch All (v1) + 0x59: 1, // Association Grp Info (v1) + 0x5A: 1, // Device Reset Locally (v1) + 0x5E: 1, // ZwaveplusInfo + 0x60: 3, // Multi Channel (v3) + 0x70: 2, // Configuration (v1,2) + 0x72: 2, // Manufacturer Specific (v1,2) + 0x73: 1, // Powerlevel (v1) + 0x85: 2, // Association (v1,2) + 0x86: 1, // Version (v1) + 0x8E: 2 // Multi Channel Association (v2) + ] +} +def parse(String description) { + def checkinInfoFormat = (settings.checkinInfo ?: 'dd/MM/yyyy h:mm') + def now = '' + if (checkinInfoFormat != 'Hide') { + try { + now = 'Last Check-in: ' + new Date().format("${checkinInfoFormat}a", location.timeZone) + } catch (all) { } + } + sendEvent(name: "lastCheckin", value: now, displayed: false) + def channels = getDataValue("endpoints")?.toInteger() + if (channels > 0) { + def epevents = "1" + if (channels > 1) { + for (i in 2..channels) { + def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$channels" } + if (childDevice) { + childDevice.sendEvent(name: "lastCheckin", value: now, displayed: false) + } + } + } + } + log.debug "Event: ${description}" + def result = null + def cmd = zwave.parse(description, getCommandClassVersions()) + if (cmd) { + result = zwaveEvent(cmd) + log.debug "Parsed ${description} to ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + result +} +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.debug "BasicReport() called - ${cmd.inspect()}" + createEvent(name:"switch", value: cmd.value ? "on" : "off") +} +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + log.debug "SwitchBinaryReport() called - ${cmd.inspect()}" + createEvent(name:"switch", value: cmd.value ? "on" : "off") +} +def zwaveEvent(physicalgraph.zwave.commands.powerlevelv1.PowerlevelReport cmd) { + log.debug "PowerlevelReport() called - ${cmd.inspect()}" +} +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport() called - Group #${cmd.groupingIdentifier} contains nodes: ${toHexString(cmd.nodeId)} (hexadecimal format)" +} +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + log.debug "MultiChannelAssociationReport() called - Group #${cmd.groupingIdentifier} contains destinations: ${toHexString(cmd.nodeId)} (hexadecimal format)" +} +def zwaveEvent(physicalgraph.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 +} +private List loadEndpointInfo() { + if (state.endpointInfo) { + state.endpointInfo + } else if (device.currentValue("epInfo")) { + fromJson(device.currentValue("epInfo")) + } else { + [] + } +} +def zwaveEvent(physicalgraph.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 + [ createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:""), + response(zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: 1)) ] +} +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv1.ManufacturerSpecificReport cmd) { + log.debug("ManufacturerSpecificReport ${cmd.inspect()}") +} +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + + def map = [ name: "switch${cmd.sourceEndPoint}" ] + + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + log.debug "MultiChannelCmdEncap called - ${cmd.inspect()}" + if (encapsulatedCommand && cmd.commandClass == 50) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) + } else { + switch(cmd.commandClass) { + case 32: + case 37: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + log.debug "MultiChannelCmdEncap mapped - ${map}" + + if (cmd.sourceEndPoint == 1) { + sendEvent(name: "switch", value: map.value) + } else { + def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$cmd.sourceEndPoint" } + if (childDevice) { + childDevice.sendEvent(name: "switch", value: map.value) + } + } + break + } + } +} +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug("Command () called - ${device.displayName} ${cmd}") + // Handles all Z-Wave commands we aren't interested in + [:] +} +// Devices that support the Security command class can send messages in an +// encrypted form; they arrive wrapped in a SecurityMessageEncapsulation +// command and must be unencapsulated +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x98: 1, 0x20: 1]) + + // can specify command class versions here like in zwave.parse + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } +} +def on() { + onOffCmd(device.deviceNetworkId, 0xFF) +} +def off() { + onOffCmd(device.deviceNetworkId, 0) +} +void onOffCmd(dni, value) { + def channels = getDataValue("endpoints").toInteger() + if (channels > 0) { + log.debug("onOffCmd called - ${channelNumber(dni)} ${value}") + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:channelNumber(dni), destinationEndPoint:channelNumber(dni), commandClass:37, command:1, parameter:[value]).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:channelNumber(dni), destinationEndPoint:channelNumber(dni), commandClass:37, command:2).format()) + sendHubCommand(cmds, 1000) + } else { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.basicV1.basicSet(value: value).format()) + cmds << new physicalgraph.device.HubAction(zwave.switchBinaryV1.switchBinaryGet().format()) + sendHubCommand(cmds, 500) + } +} + +def installed() { + log.debug "installed() called" + initialize() +} +def uninstalled() { + log.debug "uninstalled() called" + sendEvent(name: "epEvent", value: "delete all", isStateChange: true, displayed: false, descriptionText: "Delete endpoint devices") +} +def updated() { + log.debug "updated() called" + initialize() + if (childDevices) { + 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 + } + } +} +def refresh() { + def lastRefreshed = state.lastRefreshed + if (lastRefreshed && (now() - lastRefreshed < 5000)) return + state.lastRefreshed = now() + + log.debug "refresh() called" + def channels = getDataValue("endpoints").toInteger() + if (channels > 0) { + def cmds = (1..channels).collect { endpoint -> + new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:endpoint, destinationEndPoint:endpoint, commandClass:37, command:2).format()) + } + sendHubCommand(cmds, 500) + } else { + sendHubCommand([ + new physicalgraph.device.HubAction(zwave.basicV1.basicGet().format()), + new physicalgraph.device.HubAction(zwave.switchBinaryV1.switchBinaryGet().format()) + ], 500) + } +} +// If you add the Configuration capability to your device type, this +// command will be called right after the device joins to set +// device-specific configuration commands. +def configure() { + log.debug "configure() called" + initialize() +} +// PING is used by Device-Watch in attempt to reach the Device +def ping() { + log.debug "ping() called" + refresh() +} +// If you add the Polling capability to your device type, this command +// will be called approximately every 5 minutes to check the device's state +def poll() { + log.debug "poll() called" + refresh() +} +def initialize() { + def lastInitialized = state.lastInitialized + if (lastInitialized && (now() - lastInitialized < 2000)) return + state.lastInitialized = now() + log.debug "initialize() called" + + def checkinMethod = (settings.checkin ?: 'Every 1 minute').replace('Every ', 'Every').replace(' minute', 'Minute').replace(' hour', 'Hour') + try { + "run$checkinMethod"(refresh) + } catch (all) { } + + device?.updateSetting("refreshinterval", null) + + // Parse fingerprint for supported command classes: + def ccIds = [] + if (getZwaveInfo()?.cc) { + ccIds = getZwaveInfo()?.cc + getZwaveInfo()?.sec + } + else { + ccIds = device.rawDescription.findAll(/0x\p{XDigit}+/) + if (ccIds.size() > 0) { ccIds.remove(0) } + } + ccIds.removeAll([null]) + ccIds = ccIds.sort().collect { Integer.parseInt(it.replace("0x",""),16) } + if (!ccIds.find( {it == 0x60 }) ) { + log.debug "initialize() - Device does not support MULTI_CHANNEL command classes" + updateDataValue("endpoints", "0") + if (ccIds.find( {it == 0x27 }) ) { + } + } else { + sendHubCommand([new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelEndPointGet().format())], 500) + } + // Powerlevel + /* + if (ccIds.find( {it == 0x73 }) ) { + updateDataValue("powerlevel", "1") + } else { + updateDataValue("powerlevel", "0") + } + */ + def channels = getDataValue("endpoints")?.toInteger() + if (channels > 0) { + def epevents = "1" + if ("${channels}" > 1) { + for (i in 2..channels) { + epevents = "${epevents},${i}" + } + } + enableEpEvents(epevents) + } + createChildDevices() + sendEvent(name: "checkInterval", value: (60 * 60), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} +def enableEpEvents(enabledEndpoints) { + log.debug "enableEpEvents() called - ${enabledEndpoints}" + state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger() + null +} +private void createChildDevices() { + state.oldLabel = device.label + def channels = getDataValue("endpoints")?.toInteger() + log.debug "createChildDevices() called - Channels: ${channels}" + if (channels > 0) { + // Multi-channel Association + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1, nodeId: []).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format()) + if (channels > 1) { + try { + for (i in 2..channels) { + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: i, nodeId: []).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: i, nodeId: [zwaveHubNodeId]).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: i).format()) + def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$i" } + if (!childDevice) { + addChildDevice("Z-Wave Light Switch Multichannel Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Channel $i" + ]) + } else { + childDevice.sendEvent(name: "checkInterval", value: (60 * 60), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } + } + } catch (e) { + runIn(2, "sendAlert") + } + } + sendHubCommand(cmds, 500) + } else { + // Single-channel Association + sendHubCommand([ + new physicalgraph.device.HubAction(zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationGet(groupingIdentifier: 1).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationRemove(groupingIdentifier: 2, nodeId: []).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationSet(groupingIdentifier: 2, nodeId: [zwaveHubNodeId]).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationGet(groupingIdentifier: 2).format()) + ], 500) + } +} +private channelNumber(String dni) { + if (dni.indexOf("-ep") >= 0) { + dni.split("-ep")[-1] as Integer + } else { + "1" as Integer + } +} +/** + * toHexString() + * + * Convert a list of integers to a list of hex strings. + **/ +private toHexString(input, size = 2, usePrefix = false) { + def pattern = (usePrefix) ? "0x%0${size}X" : "%0${size}X" + + if (input instanceof Collection) { + def hex = [] + input.each { hex.add(String.format(pattern, it)) } + return hex.toString() + } + else { + return String.format(pattern, input) + } +} +private sendAlert() { + sendEvent(descriptionText: "Child device creation failed. Please make sure that the \"Z-Wave Light Switch Multichannel Child Device\" is installed and published.", eventType: "ALERT", name: "childDeviceCreation", value: "failed", displayed: true, ) +} From d3bfa20717ce783b3e818dac6d46c37489f46fc3 Mon Sep 17 00:00:00 2001 From: Mark-C-uk Date: Thu, 22 Feb 2018 12:19:29 +0000 Subject: [PATCH 4/5] Delete neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy --- ...ome-zwave-light-switch-multichannel.groovy | 456 ------------------ 1 file changed, 456 deletions(-) delete mode 100644 devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy diff --git a/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy b/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy deleted file mode 100644 index cadbb38..0000000 --- a/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy +++ /dev/null @@ -1,456 +0,0 @@ -/** - * Z-Wave Light Switch Multichannel - Device Handler (parent) - * - * Copyright 2017 HongTat Tan - * - * - * Requires ** Z-Wave Light Switch Multichannel Child Device ** - * - * Tested on the following Z-Wave light switches: - * NEO Coolcam Z-Wave Light Switch (EU-1/2 Gang) - * MCOHome Z-Wave Light Switch (MH-312-EU) - * - * - * Version history: - * 1.0.3 (7/10/2017) - Add 2-way status reporting - * 1.0.2 (25/09/2017) - Add Health Check & Configure for child device - * 1.0.1 (23/09/2017) - Bug fix - * 1.0 (23/09/2017) - Initial Release - * - * Incorporates code from: - * Peter Major - https://github.com/petermajor/ - * Omar Shahbal - https://github.com/omarshahbal/ - * Eric Maycock - https://github.com/erocm123/ - * David Lomas - https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker - * - * 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: "NEO Coolcam/MCOHome Z-Wave Light Switch Multichannel", namespace: "hongtat", author: "HongTat Tan") { - capability "Actuator" - capability "Switch" - capability "Refresh" - capability "Health Check" - capability "Polling" - capability "Configuration" - capability "Sensor" - capability "Zw Multichannel" - - attribute "lastCheckin", "String" - - preferences { - section(title: "Check-in Interval") { - paragraph "Run a Check-in procedure every so often." - input "checkin", "enum", title: "Run Check-in procedure", options: ["Every 1 minute", "Every 5 minutes", "Every 10 minutes", "Every 15 minutes", "Every 30 minutes", "Every 1 hour"], description: "Allows check-in procedure to run every so often", defaultValue: "Every 1 minute", required: true - } - section(title: "Check-in Info") { - paragraph "Display check-in info" - input "checkinInfo", "enum", title: "Show last Check-in info", options: ["Hide", "MM/dd/yyyy h:mm", "MM-dd-yyyy h:mm", "dd/MM/yyyy h:mm", "dd-MM-yyyy h:mm"], description: "Show last check-in info.", defaultValue: "dd/MM/yyyy h:mm", required: true - } - } - - //fingerprint inClusters: "0x25", deviceJoinName: "Z-Wave Light Switch" - fingerprint mfr:"0258", prod:"0003", model:"108C", deviceJoinName: "NEO Coolcam Light Switch (1-CH)" - fingerprint mfr:"0258", prod:"0003", model:"108B", deviceJoinName: "NEO Coolcam Light Switch (2-CH)" - fingerprint mfr:"015F", prod:"3102", model:"0201", deviceJoinName: "MCOHome Light Switch S311 (1-CH)" - fingerprint mfr:"015F", prod:"3102", model:"0202", deviceJoinName: "MCOHome Light Switch S312 (2-CH)" - fingerprint mfr:"015F", prod:"3102", model:"0204", deviceJoinName: "MCOHome Light Switch S314 (4-CH)" - fingerprint mfr:"015F", prod:"4102", model:"0201", deviceJoinName: "MCOHome Light Switch S411 (1-CH)" - fingerprint mfr:"015F", prod:"4102", model:"0202", deviceJoinName: "MCOHome Light Switch S412 (2-CH)" - } - simulator { - // TODO: define status and reply messages here - } - 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 "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.lastCheckin", key: "SECONDARY_CONTROL") { - attributeState("default", label:'${currentValue}') - } - } - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" - } - standardTile("configure", "device.default", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:"", action:"configure", icon:"st.secondary.configure" - } - main(["switch"]) - details(["switch", "refresh", "configure"]) - } -} - -private getCommandClassVersions() { - [ - 0x20: 1, // Basic (v1) - 0x25: 1, // Switch Binary (v1) - 0x27: 1, // Switch All (v1) - 0x59: 1, // Association Grp Info (v1) - 0x5A: 1, // Device Reset Locally (v1) - 0x5E: 1, // ZwaveplusInfo - 0x60: 3, // Multi Channel (v3) - 0x70: 2, // Configuration (v1,2) - 0x72: 2, // Manufacturer Specific (v1,2) - 0x73: 1, // Powerlevel (v1) - 0x85: 2, // Association (v1,2) - 0x86: 1, // Version (v1) - 0x8E: 2 // Multi Channel Association (v2) - ] -} -def parse(String description) { - def checkinInfoFormat = (settings.checkinInfo ?: 'dd/MM/yyyy h:mm') - def now = '' - if (checkinInfoFormat != 'Hide') { - try { - now = 'Last Check-in: ' + new Date().format("${checkinInfoFormat}a", location.timeZone) - } catch (all) { } - } - sendEvent(name: "lastCheckin", value: now, displayed: false) - def channels = getDataValue("endpoints")?.toInteger() - if (channels > 0) { - def epevents = "1" - if (channels > 1) { - for (i in 2..channels) { - def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$channels" } - if (childDevice) { - childDevice.sendEvent(name: "lastCheckin", value: now, displayed: false) - } - } - } - } - log.debug "Event: ${description}" - def result = null - def cmd = zwave.parse(description, getCommandClassVersions()) - if (cmd) { - result = zwaveEvent(cmd) - log.debug "Parsed ${description} to ${cmd} to ${result.inspect()}" - } else { - log.debug "Non-parsed event: ${description}" - } - result -} -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - log.debug "BasicReport() called - ${cmd.inspect()}" - createEvent(name:"switch", value: cmd.value ? "on" : "off") -} -def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { - log.debug "SwitchBinaryReport() called - ${cmd.inspect()}" - createEvent(name:"switch", value: cmd.value ? "on" : "off") -} -def zwaveEvent(physicalgraph.zwave.commands.powerlevelv1.PowerlevelReport cmd) { - log.debug "PowerlevelReport() called - ${cmd.inspect()}" -} -def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { - log.debug "AssociationReport() called - Group #${cmd.groupingIdentifier} contains nodes: ${toHexString(cmd.nodeId)} (hexadecimal format)" -} -def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { - log.debug "MultiChannelAssociationReport() called - Group #${cmd.groupingIdentifier} contains destinations: ${toHexString(cmd.nodeId)} (hexadecimal format)" -} -def zwaveEvent(physicalgraph.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 -} -private List loadEndpointInfo() { - if (state.endpointInfo) { - state.endpointInfo - } else if (device.currentValue("epInfo")) { - fromJson(device.currentValue("epInfo")) - } else { - [] - } -} -def zwaveEvent(physicalgraph.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 - [ createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:""), - response(zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: 1)) ] -} -def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv1.ManufacturerSpecificReport cmd) { - log.debug("ManufacturerSpecificReport ${cmd.inspect()}") -} -def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { - - def map = [ name: "switch${cmd.sourceEndPoint}" ] - - def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) - log.debug "MultiChannelCmdEncap called - ${cmd.inspect()}" - if (encapsulatedCommand && cmd.commandClass == 50) { - zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) - } else { - switch(cmd.commandClass) { - case 32: - case 37: - if (cmd.parameter == [0]) { - map.value = "off" - } - if (cmd.parameter == [255]) { - map.value = "on" - } - log.debug "MultiChannelCmdEncap mapped - ${map}" - - if (cmd.sourceEndPoint == 1) { - sendEvent(name: "switch", value: map.value) - } else { - def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$cmd.sourceEndPoint" } - if (childDevice) { - childDevice.sendEvent(name: "switch", value: map.value) - } - } - break - } - } -} -def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug("Command () called - ${device.displayName} ${cmd}") - // Handles all Z-Wave commands we aren't interested in - [:] -} -// Devices that support the Security command class can send messages in an -// encrypted form; they arrive wrapped in a SecurityMessageEncapsulation -// command and must be unencapsulated -def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x98: 1, 0x20: 1]) - - // can specify command class versions here like in zwave.parse - if (encapsulatedCommand) { - return zwaveEvent(encapsulatedCommand) - } -} -def on() { - onOffCmd(device.deviceNetworkId, 0xFF) -} -def off() { - onOffCmd(device.deviceNetworkId, 0) -} -void onOffCmd(dni, value) { - def channels = getDataValue("endpoints").toInteger() - if (channels > 0) { - log.debug("onOffCmd called - ${channelNumber(dni)} ${value}") - def cmds = [] - cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:channelNumber(dni), destinationEndPoint:channelNumber(dni), commandClass:37, command:1, parameter:[value]).format()) - cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:channelNumber(dni), destinationEndPoint:channelNumber(dni), commandClass:37, command:2).format()) - sendHubCommand(cmds, 1000) - } else { - def cmds = [] - cmds << new physicalgraph.device.HubAction(zwave.basicV1.basicSet(value: value).format()) - cmds << new physicalgraph.device.HubAction(zwave.switchBinaryV1.switchBinaryGet().format()) - sendHubCommand(cmds, 500) - } -} - -def installed() { - log.debug "installed() called" - initialize() -} -def uninstalled() { - log.debug "uninstalled() called" - sendEvent(name: "epEvent", value: "delete all", isStateChange: true, displayed: false, descriptionText: "Delete endpoint devices") -} -def updated() { - log.debug "updated() called" - initialize() - if (childDevices) { - 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 - } - } -} -def refresh() { - def lastRefreshed = state.lastRefreshed - if (lastRefreshed && (now() - lastRefreshed < 5000)) return - state.lastRefreshed = now() - - log.debug "refresh() called" - def channels = getDataValue("endpoints").toInteger() - if (channels > 0) { - def cmds = (1..channels).collect { endpoint -> - new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:endpoint, destinationEndPoint:endpoint, commandClass:37, command:2).format()) - } - sendHubCommand(cmds, 500) - } else { - sendHubCommand([ - new physicalgraph.device.HubAction(zwave.basicV1.basicGet().format()), - new physicalgraph.device.HubAction(zwave.switchBinaryV1.switchBinaryGet().format()) - ], 500) - } -} -// If you add the Configuration capability to your device type, this -// command will be called right after the device joins to set -// device-specific configuration commands. -def configure() { - log.debug "configure() called" - initialize() -} -// PING is used by Device-Watch in attempt to reach the Device -def ping() { - log.debug "ping() called" - refresh() -} -// If you add the Polling capability to your device type, this command -// will be called approximately every 5 minutes to check the device's state -def poll() { - log.debug "poll() called" - refresh() -} -def initialize() { - def lastInitialized = state.lastInitialized - if (lastInitialized && (now() - lastInitialized < 2000)) return - state.lastInitialized = now() - log.debug "initialize() called" - - def checkinMethod = (settings.checkin ?: 'Every 1 minute').replace('Every ', 'Every').replace(' minute', 'Minute').replace(' hour', 'Hour') - try { - "run$checkinMethod"(refresh) - } catch (all) { } - - device?.updateSetting("refreshinterval", null) - - // Parse fingerprint for supported command classes: - def ccIds = [] - if (getZwaveInfo()?.cc) { - ccIds = getZwaveInfo()?.cc + getZwaveInfo()?.sec - } - else { - ccIds = device.rawDescription.findAll(/0x\p{XDigit}+/) - if (ccIds.size() > 0) { ccIds.remove(0) } - } - ccIds.removeAll([null]) - ccIds = ccIds.sort().collect { Integer.parseInt(it.replace("0x",""),16) } - if (!ccIds.find( {it == 0x60 }) ) { - log.debug "initialize() - Device does not support MULTI_CHANNEL command classes" - updateDataValue("endpoints", "0") - if (ccIds.find( {it == 0x27 }) ) { - } - } else { - sendHubCommand([new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelEndPointGet().format())], 500) - } - // Powerlevel - /* - if (ccIds.find( {it == 0x73 }) ) { - updateDataValue("powerlevel", "1") - } else { - updateDataValue("powerlevel", "0") - } - */ - def channels = getDataValue("endpoints")?.toInteger() - if (channels > 0) { - def epevents = "1" - if ("${channels}" > 1) { - for (i in 2..channels) { - epevents = "${epevents},${i}" - } - } - enableEpEvents(epevents) - } - createChildDevices() - sendEvent(name: "checkInterval", value: (60 * 60), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) -} -def enableEpEvents(enabledEndpoints) { - log.debug "enableEpEvents() called - ${enabledEndpoints}" - state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger() - null -} -private void createChildDevices() { - state.oldLabel = device.label - def channels = getDataValue("endpoints")?.toInteger() - log.debug "createChildDevices() called - Channels: ${channels}" - if (channels > 0) { - // Multi-channel Association - def cmds = [] - cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1, nodeId: []).format()) - cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format()) - cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format()) - if (channels > 1) { - try { - for (i in 2..channels) { - cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: i, nodeId: []).format()) - cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: i, nodeId: [zwaveHubNodeId]).format()) - cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: i).format()) - def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$i" } - if (!childDevice) { - addChildDevice("Z-Wave Light Switch Multichannel Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", - isComponent: false, componentName: "ep$i", componentLabel: "Channel $i" - ]) - } else { - childDevice.sendEvent(name: "checkInterval", value: (60 * 60), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - } - } - } catch (e) { - runIn(2, "sendAlert") - } - } - sendHubCommand(cmds, 500) - } else { - // Single-channel Association - sendHubCommand([ - new physicalgraph.device.HubAction(zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []).format()), - new physicalgraph.device.HubAction(zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format()), - new physicalgraph.device.HubAction(zwave.associationV2.associationGet(groupingIdentifier: 1).format()), - new physicalgraph.device.HubAction(zwave.associationV2.associationRemove(groupingIdentifier: 2, nodeId: []).format()), - new physicalgraph.device.HubAction(zwave.associationV2.associationSet(groupingIdentifier: 2, nodeId: [zwaveHubNodeId]).format()), - new physicalgraph.device.HubAction(zwave.associationV2.associationGet(groupingIdentifier: 2).format()) - ], 500) - } -} -private channelNumber(String dni) { - if (dni.indexOf("-ep") >= 0) { - dni.split("-ep")[-1] as Integer - } else { - "1" as Integer - } -} -/** - * toHexString() - * - * Convert a list of integers to a list of hex strings. - **/ -private toHexString(input, size = 2, usePrefix = false) { - def pattern = (usePrefix) ? "0x%0${size}X" : "%0${size}X" - - if (input instanceof Collection) { - def hex = [] - input.each { hex.add(String.format(pattern, it)) } - return hex.toString() - } - else { - return String.format(pattern, input) - } -} -private sendAlert() { - sendEvent(descriptionText: "Child device creation failed. Please make sure that the \"Z-Wave Light Switch Multichannel Child Device\" is installed and published.", eventType: "ALERT", name: "childDeviceCreation", value: "failed", displayed: true, ) -} From 98e9b237b5615ce3b04f3bf30d76611ac6efc8db Mon Sep 17 00:00:00 2001 From: Mark Cockcroft Date: Fri, 1 Jun 2018 09:26:15 +0100 Subject: [PATCH 5/5] updated to show device name, reducted logging --- ...ht-switch-multichannel-child-device.groovy | 73 +++ ...ome-zwave-light-switch-multichannel.groovy | 450 ++++++++++++++++++ 2 files changed, 523 insertions(+) create mode 100644 devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.src/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.groovy create mode 100644 devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy diff --git a/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.src/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.groovy b/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.src/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.groovy new file mode 100644 index 0000000..0bd1130 --- /dev/null +++ b/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.src/neo-coolcam-mcohome-zwave-light-switch-multichannel-child-device.groovy @@ -0,0 +1,73 @@ +/** + * Z-Wave Light Switch Multichannel - Device Handler (child) + * + * Copyright 2017 HongTat Tan + * + * + * Version history: + * 1.0 (23/09/2017) - Initial Release + * + * + * 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: "NEO Coolcam/MCOHome Z-Wave Light Switch Multichannel Child Device", namespace: "hongtat", author: "HongTat Tan") { + capability "Switch" + capability "Actuator" + capability "Sensor" + capability "Refresh" + capability "Health Check" + capability "Configuration" + + attribute "lastCheckin", "String" + } + + 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 "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.lastCheckin", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}') + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.default", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + } +} + +void on() { + log.info "ON - $device (Child device)" + parent.onOffCmd(device.deviceNetworkId, 0xFF) +} + +void off() { + log.info "OFF - $device (Child device)" + parent.onOffCmd(device.deviceNetworkId, 0) +} + +void refresh() { + log.debug "REFRESH - $device (Child device)" + parent.refresh() +} + +void configure() { + log.info "CONFIGURE - $device (Child device)" + parent.configure() +} diff --git a/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy b/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy new file mode 100644 index 0000000..cbe9f99 --- /dev/null +++ b/devicetypes/hongtat/neo-coolcam-mcohome-zwave-light-switch-multichannel.src/neo-coolcam-mcohome-zwave-light-switch-multichannel.groovy @@ -0,0 +1,450 @@ +/** + * Z-Wave Light Switch Multichannel - Device Handler (parent) + * + * Copyright 2017 HongTat Tan + * + * + * Requires ** Z-Wave Light Switch Multichannel Child Device ** + * + * Tested on the following Z-Wave light switches: + * NEO Coolcam Z-Wave Light Switch (EU-1/2 Gang) + * MCOHome Z-Wave Light Switch (MH-312-EU) + * + * + * Version history: + * 1.0.3 (7/10/2017) - Add 2-way status reporting + * 1.0.2 (25/09/2017) - Add Health Check & Configure for child device + * 1.0.1 (23/09/2017) - Bug fix + * 1.0 (23/09/2017) - Initial Release + * + * Incorporates code from: + * Peter Major - https://github.com/petermajor/ + * Omar Shahbal - https://github.com/omarshahbal/ + * Eric Maycock - https://github.com/erocm123/ + * David Lomas - https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker + * + * 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: "NEO Coolcam/MCOHome Z-Wave Light Switch Multichannel", namespace: "hongtat", author: "HongTat Tan") { + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Health Check" + capability "Polling" + capability "Configuration" + capability "Sensor" + capability "Zw Multichannel" + + attribute "lastCheckin", "String" + + preferences { + input "checkin", "enum", title: "Run Check-in procedure", options: ["Every 1 minute", "Every 5 minutes", "Every 10 minutes", "Every 15 minutes", "Every 30 minutes", "Every 1 hour"], description: "Allows check-in procedure to run every so often", defaultValue: "Every 1 minute", required: true + input "checkinInfo", "enum", title: "Show last Check-in info", options: ["Hide", "MM/dd/yyyy h:mm", "MM-dd-yyyy h:mm", "dd/MM/yyyy h:mm", "dd-MM-yyyy h:mm"], description: "Show last check-in info.", defaultValue: "dd/MM/yyyy h:mm", required: true + } + + //fingerprint inClusters: "0x25", deviceJoinName: "Z-Wave Light Switch" + fingerprint mfr:"0258", prod:"0003", model:"108C", deviceJoinName: "NEO Coolcam Light Switch (1-CH)" + fingerprint mfr:"0258", prod:"0003", model:"108B", deviceJoinName: "NEO Coolcam Light Switch (2-CH)" + fingerprint mfr:"015F", prod:"3102", model:"0201", deviceJoinName: "MCOHome Light Switch S311 (1-CH)" + fingerprint mfr:"015F", prod:"3102", model:"0202", deviceJoinName: "MCOHome Light Switch S312 (2-CH)" + fingerprint mfr:"015F", prod:"3102", model:"0204", deviceJoinName: "MCOHome Light Switch S314 (4-CH)" + fingerprint mfr:"015F", prod:"4102", model:"0201", deviceJoinName: "MCOHome Light Switch S411 (1-CH)" + fingerprint mfr:"015F", prod:"4102", model:"0202", deviceJoinName: "MCOHome Light Switch S412 (2-CH)" + } + simulator { + // TODO: define status and reply messages here + } + 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 "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.lastCheckin", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}') + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + standardTile("configure", "device.default", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"configure", icon:"st.secondary.configure" + } + main(["switch"]) + details(["switch", "refresh", "configure"]) + } +} + +private getCommandClassVersions() { + [ + 0x20: 1, // Basic (v1) + 0x25: 1, // Switch Binary (v1) + 0x27: 1, // Switch All (v1) + 0x59: 1, // Association Grp Info (v1) + 0x5A: 1, // Device Reset Locally (v1) + 0x5E: 1, // ZwaveplusInfo + 0x60: 3, // Multi Channel (v3) + 0x70: 2, // Configuration (v1,2) + 0x72: 2, // Manufacturer Specific (v1,2) + 0x73: 1, // Powerlevel (v1) + 0x85: 2, // Association (v1,2) + 0x86: 1, // Version (v1) + 0x8E: 2 // Multi Channel Association (v2) + ] +} +def parse(String description) { + def checkinInfoFormat = (settings.checkinInfo ?: 'dd/MM/yyyy h:mm') + def now = '' + if (checkinInfoFormat != 'Hide') { + try { + now = 'Last Check-in: ' + new Date().format("${checkinInfoFormat}a", location.timeZone) + } catch (all) { } + } + sendEvent(name: "lastCheckin", value: now, displayed: false) + def channels = getDataValue("endpoints")?.toInteger() + if (channels > 0) { + def epevents = "1" + if (channels > 1) { + for (i in 2..channels) { + def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$channels" } + if (childDevice) { + childDevice.sendEvent(name: "lastCheckin", value: now, displayed: false) + } + } + } + } +// log.debug "Event: ${description}" + def result = null + def cmd = zwave.parse(description, getCommandClassVersions()) + if (cmd) { + result = zwaveEvent(cmd) +///log.debug "Parsed ${description} to ${cmd} to ${result.inspect()}" + } else { + log.warn "Non-parsed event: ${description}" + } + result +} +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.info "BasicReport() called - ${cmd.inspect()}" + createEvent(name:"switch", value: cmd.value ? "on" : "off") +} +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + log.info "SwitchBinaryReport() called - ${cmd.inspect()}" + createEvent(name:"switch", value: cmd.value ? "on" : "off") +} +def zwaveEvent(physicalgraph.zwave.commands.powerlevelv1.PowerlevelReport cmd) { + log.debug "PowerlevelReport() called - ${cmd.inspect()}" +} +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + log.debug "AssociationReport() called - Group #${cmd.groupingIdentifier} contains nodes: ${toHexString(cmd.nodeId)} (hexadecimal format)" +} +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + log.debug "MultiChannelAssociationReport() called - Group #${cmd.groupingIdentifier} contains destinations: ${toHexString(cmd.nodeId)} (hexadecimal format)" +} +def zwaveEvent(physicalgraph.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 +} +private List loadEndpointInfo() { + if (state.endpointInfo) { + state.endpointInfo + } else if (device.currentValue("epInfo")) { + fromJson(device.currentValue("epInfo")) + } else { + [] + } +} +def zwaveEvent(physicalgraph.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 + [ createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:""), + response(zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: 1)) ] +} +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv1.ManufacturerSpecificReport cmd) { + log.debug("ManufacturerSpecificReport ${cmd.inspect()}") +} +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + + def map = [ name: "switch${cmd.sourceEndPoint}" ] + + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) +//log.debug "MultiChannelCmdEncap called - ${cmd.inspect()}" + if (encapsulatedCommand && cmd.commandClass == 50) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) + } else { + switch(cmd.commandClass) { + case 32: + case 37: + if (cmd.parameter == [0]) { + map.value = "off" + } + if (cmd.parameter == [255]) { + map.value = "on" + } + if (cmd.sourceEndPoint == 1) { + sendEvent(name: "switch", value: map.value) + log.info "REFRESH '$device' - '${map.value}' ---- PARENT '${map}'" + } else { + def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$cmd.sourceEndPoint" } + if (childDevice) { + childDevice.sendEvent(name: "switch", value: map.value) + log.info "REFRESH '$childDevice' - '${map.value}' ---- Child '${map}' parent='$device'" + } + } + break + } + } +} +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug("Command () called - ${device.displayName} ${cmd}") + // Handles all Z-Wave commands we aren't interested in + [:] +} +// Devices that support the Security command class can send messages in an +// encrypted form; they arrive wrapped in a SecurityMessageEncapsulation +// command and must be unencapsulated +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x98: 1, 0x20: 1]) + + // can specify command class versions here like in zwave.parse + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } +} +def on() { + onOffCmd(device.deviceNetworkId, 0xFF) +} +def off() { + onOffCmd(device.deviceNetworkId, 0) +} +void onOffCmd(dni, value) { + def channels = getDataValue("endpoints").toInteger() + if (channels > 0) { + log.info("onOffCmd called - ${channelNumber(dni)} ${value}") + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:channelNumber(dni), destinationEndPoint:channelNumber(dni), commandClass:37, command:1, parameter:[value]).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:channelNumber(dni), destinationEndPoint:channelNumber(dni), commandClass:37, command:2).format()) + sendHubCommand(cmds, 1000) + } else { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.basicV1.basicSet(value: value).format()) + cmds << new physicalgraph.device.HubAction(zwave.switchBinaryV1.switchBinaryGet().format()) + sendHubCommand(cmds, 500) + } +} + +def installed() { + log.debug "installed() called" + initialize() +} +def uninstalled() { + log.debug "uninstalled() called" + sendEvent(name: "epEvent", value: "delete all", isStateChange: true, displayed: false, descriptionText: "Delete endpoint devices") +} +def updated() { + log.debug "updated() called" + initialize() + if (childDevices) { + 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 + } + } +} +def refresh() { + def lastRefreshed = state.lastRefreshed + if (lastRefreshed && (now() - lastRefreshed < 5000)) return + state.lastRefreshed = now() + + //log.debug "REFRESH '$device' called" + def channels = getDataValue("endpoints").toInteger() + if (channels > 0) { + def cmds = (1..channels).collect { endpoint -> + new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:endpoint, destinationEndPoint:endpoint, commandClass:37, command:2).format()) + } + sendHubCommand(cmds, 500) + } else { + sendHubCommand([ + new physicalgraph.device.HubAction(zwave.basicV1.basicGet().format()), + new physicalgraph.device.HubAction(zwave.switchBinaryV1.switchBinaryGet().format()) + ], 500) + } +} +// If you add the Configuration capability to your device type, this +// command will be called right after the device joins to set +// device-specific configuration commands. +def configure() { + log.debug "configure() called" + initialize() +} +// PING is used by Device-Watch in attempt to reach the Device +def ping() { + log.debug "ping() called" + refresh() +} +// If you add the Polling capability to your device type, this command +// will be called approximately every 5 minutes to check the device's state +def poll() { + log.debug "poll() called" + refresh() +} +def initialize() { + def lastInitialized = state.lastInitialized + if (lastInitialized && (now() - lastInitialized < 2000)) return + state.lastInitialized = now() + log.debug "initialize() called" + + def checkinMethod = (settings.checkin ?: 'Every 1 minute').replace('Every ', 'Every').replace(' minute', 'Minute').replace(' hour', 'Hour') + try { + "run$checkinMethod"(refresh) + } catch (all) { } + + device?.updateSetting("refreshinterval", null) + + // Parse fingerprint for supported command classes: + def ccIds = [] + if (getZwaveInfo()?.cc) { + ccIds = getZwaveInfo()?.cc + getZwaveInfo()?.sec + } + else { + ccIds = device.rawDescription.findAll(/0x\p{XDigit}+/) + if (ccIds.size() > 0) { ccIds.remove(0) } + } + ccIds.removeAll([null]) + ccIds = ccIds.sort().collect { Integer.parseInt(it.replace("0x",""),16) } + if (!ccIds.find( {it == 0x60 }) ) { + log.warn "initialize() - Device does not support MULTI_CHANNEL command classes" + updateDataValue("endpoints", "0") + if (ccIds.find( {it == 0x27 }) ) { + } + } else { + sendHubCommand([new physicalgraph.device.HubAction(zwave.multiChannelV3.multiChannelEndPointGet().format())], 500) + } + // Powerlevel + /* + if (ccIds.find( {it == 0x73 }) ) { + updateDataValue("powerlevel", "1") + } else { + updateDataValue("powerlevel", "0") + } + */ + def channels = getDataValue("endpoints")?.toInteger() + if (channels > 0) { + def epevents = "1" + if ("${channels}" > 1) { + for (i in 2..channels) { + epevents = "${epevents},${i}" + } + } + enableEpEvents(epevents) + } + createChildDevices() + sendEvent(name: "checkInterval", value: (60 * 60), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} +def enableEpEvents(enabledEndpoints) { + log.debug "enableEpEvents() called - ${enabledEndpoints}" + state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger() + null +} +private void createChildDevices() { + state.oldLabel = device.label + def channels = getDataValue("endpoints")?.toInteger() + log.info "createChildDevices() called - Channels: ${channels}" + if (channels > 0) { + // Multi-channel Association + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1, nodeId: []).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format()) + if (channels > 1) { + try { + for (i in 2..channels) { + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: i, nodeId: []).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: i, nodeId: [zwaveHubNodeId]).format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: i).format()) + def childDevice = childDevices.find{ it.deviceNetworkId == "$device.deviceNetworkId-ep$i" } + if (!childDevice) { + addChildDevice("Z-Wave Light Switch Multichannel Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", + isComponent: false, componentName: "ep$i", componentLabel: "Channel $i" + ]) + } else { + childDevice.sendEvent(name: "checkInterval", value: (60 * 60), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } + } + } catch (e) { + runIn(2, "sendAlert") + } + } + sendHubCommand(cmds, 500) + } else { + // Single-channel Association + sendHubCommand([ + new physicalgraph.device.HubAction(zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationGet(groupingIdentifier: 1).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationRemove(groupingIdentifier: 2, nodeId: []).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationSet(groupingIdentifier: 2, nodeId: [zwaveHubNodeId]).format()), + new physicalgraph.device.HubAction(zwave.associationV2.associationGet(groupingIdentifier: 2).format()) + ], 500) + } +} +private channelNumber(String dni) { + if (dni.indexOf("-ep") >= 0) { + dni.split("-ep")[-1] as Integer + } else { + "1" as Integer + } +} +/** + * toHexString() + * + * Convert a list of integers to a list of hex strings. + **/ +private toHexString(input, size = 2, usePrefix = false) { + def pattern = (usePrefix) ? "0x%0${size}X" : "%0${size}X" + + if (input instanceof Collection) { + def hex = [] + input.each { hex.add(String.format(pattern, it)) } + return hex.toString() + } + else { + return String.format(pattern, input) + } +} +private sendAlert() { + sendEvent(descriptionText: "Child device creation failed. Please make sure that the \"Z-Wave Light Switch Multichannel Child Device\" is installed and published.", eventType: "ALERT", name: "childDeviceCreation", value: "failed", displayed: true, ) +}