From 777f8f7e20640f371c9754ed5937e41c905747e9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 16 Jun 2016 15:26:39 -0400 Subject: [PATCH 1/7] PENG-161 - Logitech Harmony don't allow undefined commands (#965) https://smartthings.atlassian.net/browse/PENG-161 extra ) New getCapabilityName() Small fixes Extra colon capName Added .id --- .../logitech-harmony-connect.groovy | 74 +++++++++++++++---- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 83b6cc6..7731c80 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -658,29 +658,73 @@ def updateDevice() { def data = request.JSON def command = data.command def arguments = data.arguments - log.debug "updateDevice, params: ${params}, request: ${data}" if (!command) { render status: 400, data: '{"msg": "command is required"}' } else { def device = allDevices.find { it.id == params.id } - if (device) { - if (device.hasCommand("$command")) { - if (arguments) { - device."$command"(*arguments) - } else { - device."$command"() - } - render status: 204, data: "{}" - } else { - render status: 404, data: '{"msg": "Command not supported by this Device"}' - } - } else { - render status: 404, data: '{"msg": "Device not found"}' - } + if (device) { + if (validateCommand(device, command)) { + if (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + render status: 204, data: "{}" + } else { + render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}' + } + } else { + render status: 404, data: '{"msg": "Device not found"}' + } + } +} + +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +def validateCommand(device, command) { + def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) + def currentDeviceCapability = getCapabilityName(device) + if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) { + return command in capabilityCommands[currentDeviceCapability] ? true : false + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") } } +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(device) { + def capName = "" + if (switches.find{it.id == device.id}) + capName = "Switch" + else if (alarms.find{it.id == device.id}) + capName = "Alarm" + else if (locks.find{it.id == device.id}) + capName = "Lock" + log.trace "Device: $device - Capability Name: $capName" + return capName +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map +} + def listSubscriptions() { log.debug "listSubscriptions()" app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect { From 4d343d9bcfdef1c58d4747fb43c9a973bf53798c Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Fri, 17 Jun 2016 16:37:05 -0400 Subject: [PATCH 2/7] Z-Wave D/W: send requests secure encapsulated when necessary DVCSMP-1826 --- .../zwave-door-window-sensor.groovy | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy index 6011f7e..5b48c64 100644 --- a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy @@ -28,8 +28,7 @@ metadata { fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98" fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82" fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters:"0x20" // Philio multi+ - fingerprint deviceId: "0x0701", inClusters: "0x5E,0x72,0x5A,0x80,0x73,0x86,0x84,0x85,0x59,0x71,0x70,0x7A,0x98" // Vision door/window - fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98,0x86,0x72,0x5A,0x85,0x59,0x73,0x80,0x71,0x31,0x70,0x84,0x7A" // Vision Motion + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x72,0x5A,0x80,0x73,0x84,0x85,0x59,0x71,0x70,0x7A,0x98" // Vision door/window } // simulator metadata @@ -82,22 +81,22 @@ def updated() { def cmds = [] if (!state.MSR) { cmds = [ - zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(), + command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()), "delay 1200", - zwave.wakeUpV1.wakeUpNoMoreInformation().format() + zwave.wakeUpV1.wakeUpNoMoreInformation() ] } else if (!state.lastbat) { cmds = [] } else { - cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation()] } response(cmds) } def configure() { - delayBetween([ - zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(), - batteryGetCommand() + commands([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet(), + zwave.batteryV1.batteryGet() ], 6000) } @@ -148,12 +147,11 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm result << sensorValueEvent(1) } else if (cmd.event == 0x03) { result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) - result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + 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(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion") } } else if (cmd.notificationType) { @@ -171,12 +169,11 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) def cmds = [] if (!state.MSR) { - cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId).format() - cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() + cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) cmds << "delay 1200" } if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { - cmds << batteryGetCommand() + cmds << command(zwave.batteryV1.batteryGet()) } else { cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() } @@ -213,7 +210,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS if (msr == "0086-0102-0059") { result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) } else { - result << response(batteryGetCommand()) + result << response(command(zwave.batteryV1.batteryGet())) } } @@ -221,7 +218,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1]) + 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 @@ -233,12 +230,16 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) } -def batteryGetCommand() { - def cmd = zwave.batteryV1.batteryGet() - if (state.sec) { - cmd = zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd) +private command(physicalgraph.zwave.Command cmd) { + if (state.sec == 1) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() } - cmd.format() +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) } def retypeBasedOnMSR() { @@ -265,8 +266,8 @@ def retypeBasedOnMSR() { setDeviceType("Door / Window Sensor Plus (SG)") break case "0109-2002-0205": // Vision Motion - log.debug "Changing device type to Vision Motion Sensor Plus (SG)" - setDeviceType("Vision Motion Sensor Plus (SG)") + log.debug "Changing device type to Z-Wave Plus Motion/Temp Sensor" + setDeviceType("Z-Wave Plus Motion/Temp Sensor") break } } From 7a44c595812cd59e479ba89c9e87ebc3a5deb955 Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Fri, 17 Jun 2016 21:53:36 -0400 Subject: [PATCH 3/7] Z-Wave Plus Motion/Temp Sensor device type DVCSMP-1826 --- .../zwave-door-window-sensor.groovy | 2 +- .../zwave-plus-motion-temp-sensor.groovy | 322 ++++++++++++++++++ 2 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy diff --git a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy index 5b48c64..3110c91 100644 --- a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy @@ -175,7 +175,7 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { cmds << command(zwave.batteryV1.batteryGet()) } else { - cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation() } [event, response(cmds)] } diff --git a/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy new file mode 100644 index 0000000..a7adfc3 --- /dev/null +++ b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy @@ -0,0 +1,322 @@ +/** +* Copyright 2016 SmartThings +* Copyright 2015 AstraLink +* +* 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. +* +* Z-Wave Plus Motion Sensor with Temperature Measurement, ZP3102*-5 +* +*/ + +metadata { + definition (name: "Z-Wave Plus Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Configuration" + capability "Battery" + capability "Sensor" + + // for Astralink + attribute "ManufacturerCode", "string" + attribute "ProduceTypeCode", "string" + attribute "ProductCode", "string" + attribute "WakeUp", "string" + attribute "WirelessConfig", "string" + + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x31, 0x70, 0x84, 0x7A" + fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,31,71" + fingerprint mfr:"0109", prod:"2002", model:"0205" + } + + tiles { + standardTile("motion", "device.motion", width: 3, 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", inactiveLabel: false) { + 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("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"%" + } + + main(["motion", "temperature"]) + details(["motion", "temperature", "battery"]) + } +} + +def updated() { + if (!device.currentState("ManufacturerCode")) { + response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } +} + +def configure() { + log.debug "configure()" + def cmds = [] + + if (state.sec != 1) { + // secure inclusion may not be complete yet + cmds << "delay 3000" + } + + cmds += secureSequence([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet(), + zwave.batteryV1.batteryGet(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ], 500) + + cmds << "delay 8000" + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + return cmds +} + +private getCommandClassVersions() { + [ + 0x71: 3, // Notification + 0x5E: 2, // ZwaveplusInfo + 0x59: 1, // AssociationGrpInfo + 0x85: 2, // Association + 0x20: 1, // Basic + 0x80: 1, // Battery + 0x70: 1, // Configuration + 0x5A: 1, // DeviceResetLocally + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x98: 1, // Security + 0x31: 5, // SensorMultilevel + 0x84: 2 // WakeUp + ] +} + +// Parse incoming device messages to generate events +def parse(String description) { + def result = [] + def cmd + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT", + 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.") + } else if (description.startsWith("Err")) { + result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true) + } else { + cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + + if (result instanceof List) { + result = result.flatten() + } + + log.debug "Parsed '$description' to $result" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + state.sec = 1 + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + return [createEvent(descriptionText: cmd.toString())] + } +} + +def sensorValueEvent(value) { + def result = [] + if (value) { + log.debug "sensorValueEvent($value) : active" + result << createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") + } else { + log.debug "sensorValueEvent($value) : inactive" + result << createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + return sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { + return sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + if (cmd.notificationType == 0x07) { + 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) + result << response(secure(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) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x08) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x00) { + if (cmd.eventParametersLength && cmd.eventParameter[0] == 3) { + result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false) + } else { + result << sensorValueEvent(0) + } + } else if (cmd.event == 0xFF) { + result << sensorValueEvent(1) + } else { + result << createEvent(descriptionText: "$device.displayName sent event $cmd.event") + } + } 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) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true) // for Astralink + def cmds = [] + + if (!device.currentState("ManufacturerCode")) { + cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 2000" + } + if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) { + event.descriptionText += ", requesting battery" + cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)) + cmds << "delay 800" + cmds << secure(zwave.batteryV1.batteryGet()) + cmds << "delay 2000" + } else { + log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago" + } + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + + return [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def result = [] + 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() + result << createEvent(map) + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def result = [] + def map = [:] + switch (cmd.sensorType) { + case 1: + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.name = "temperature" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + 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" + map.value = cmd.scaledSensorValue.toString() + break; + default: + map.descriptionText = cmd.toString() + } + result << createEvent(map) + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + def manufacturerCode = String.format("%04X", cmd.manufacturerId) + def productTypeCode = String.format("%04X", cmd.productTypeId) + def productCode = String.format("%04X", cmd.productId) + def wirelessConfig = "ZWP" + log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}" + + result << createEvent(name: "ManufacturerCode", value: manufacturerCode) + result << createEvent(name: "ProduceTypeCode", value: productTypeCode) + result << createEvent(name: "ProductCode", value: productCode) + result << createEvent(name: "WirelessConfig", value: wirelessConfig) + + if (manufacturerCode == "0109" && productTypeCode == "2002") { + result << response(secureSequence([ + // Change re-trigger duration to 1 minute + zwave.configurationV1.configurationSet(parameterNumber: 1, configurationValue: [1], size: 1), + zwave.batteryV1.batteryGet(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ], 400)) + } + + return result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)] +} + +private secure(physicalgraph.zwave.Command cmd) { + if (state.sec == 0) { // default to secure + cmd.format() + } else { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } +} + +private secureSequence(commands, delay=200) { + delayBetween(commands.collect{ secure(it) }, delay) +} From 23f66e3caaca9afbffd4b37225b2e0531f3eaff5 Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Mon, 20 Jun 2016 17:24:17 -0400 Subject: [PATCH 4/7] Z-Wave Plus Door/Window Sensor DVCSMP-1831 --- .../zwave-door-window-sensor.groovy | 4 +- .../zwave-plus-door-window-sensor.groovy | 270 ++++++++++++++++++ .../zwave-plus-motion-temp-sensor.groovy | 13 +- 3 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy diff --git a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy index 3110c91..870f106 100644 --- a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy @@ -262,8 +262,8 @@ def retypeBasedOnMSR() { setDeviceType("3-in-1 Multisensor Plus (SG)") break case "0109-2001-0106": // Vision door/window - log.debug "Changing device type to Door / Window Sensor Plus (SG)" - setDeviceType("Door / Window Sensor Plus (SG)") + 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" diff --git a/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy b/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy new file mode 100644 index 0000000..1da4557 --- /dev/null +++ b/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy @@ -0,0 +1,270 @@ +/** +* Copyright 2016 SmartThings +* Copyright 2015 AstraLink +* +* 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. +* +* Z-Wave Plus Door/Window Sensor, ZD2102*-5 +* +*/ + +metadata { + definition (name: "Z-Wave Plus Door/Window Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Contact Sensor" + capability "Configuration" + capability "Battery" + capability "Sensor" + + // for Astralink + attribute "ManufacturerCode", "string" + attribute "ProduceTypeCode", "string" + attribute "ProductCode", "string" + attribute "WakeUp", "string" + attribute "WirelessConfig", "string" + + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x70, 0x84, 0x7A" + fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,71" + fingerprint mfr:"0109", prod:"2001", model:"0106" // not using deviceJoinName because it's sold under different brand names + } + + 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" + } + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main (["contact"]) + details(["contact","battery"]) + } + + simulator { + // messages the device returns in response to commands it receives + status "open (basic)" : "command: 9881, payload: 00 20 01 FF" + status "closed (basic)" : "command: 9881 payload: 00 20 01 00" + status "open (notification)" : "command: 9881, payload: 00 71 05 06 FF 00 FF 06 16 00 00" + status "closed (notification)" : "command: 9881, payload: 00 71 05 06 00 00 FF 06 17 00 00" + status "tamper: enclosure opened" : "command: 9881, payload: 00 71 05 07 FF 00 FF 07 03 00 00" + status "tamper: enclosure replaced" : "command: 9881, payload: 00 71 05 07 00 00 FF 07 00 00 00" + status "wake up" : "command: 9881, payload: 00 84 07" + status "battery (100%)" : "command: 9881, payload: 00 80 03 64" + status "battery low" : "command: 9881, payload: 00 80 03 FF" + } +} + +def configure() { + log.debug "configure()" + def cmds = [] + + if (state.sec != 1) { + // secure inclusion may not be complete yet + cmds << "delay 3000" + } + + cmds += secureSequence([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet(), + zwave.batteryV1.batteryGet(), + ], 500) + + cmds << "delay 8000" + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + return cmds +} + +private getCommandClassVersions() { + [ + 0x71: 3, // Notification + 0x5E: 2, // ZwaveplusInfo + 0x59: 1, // AssociationGrpInfo + 0x85: 2, // Association + 0x20: 1, // Basic + 0x80: 1, // Battery + 0x70: 1, // Configuration + 0x5A: 1, // DeviceResetLocally + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x98: 1, // Security + 0x84: 2, // WakeUp + 0x86: 1, // Version + ] +} + +// Parse incoming device messages to generate events +def parse(String description) { + def result = [] + def cmd + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT", + 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.") + } else if (description.startsWith("Err")) { + result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true) + } else { + cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + + if (result instanceof List) { + result = result.flatten() + } + + log.debug "Parsed '$description' to $result" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + state.sec = 1 + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + return [createEvent(descriptionText: cmd.toString())] + } +} + +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(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + return sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { + return sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.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.event == 0x00) { + if (cmd.eventParametersLength == 0 || cmd.eventParameter[0] != 3) { + result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false) + } else { + result << sensorValueEvent(0) + } + } 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 (!device.currentState("ManufacturerCode")) { + result << response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } else { + result << createEvent(descriptionText: "$device.displayName event $cmd.event ${cmd.eventParameter.inspect()}", isStateChange: true, displayed: false) + } + } else if (cmd.notificationType) { + result << createEvent(descriptionText: "$device.displayName notification $cmd.notificationType event $cmd.event ${cmd.eventParameter.inspect()}", 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) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink + def cmds = [] + + if (!device.currentState("ManufacturerCode")) { + cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 2000" + } + if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) { + event.descriptionText += ", requesting battery" + cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)) + cmds << "delay 800" + cmds << secure(zwave.batteryV1.batteryGet()) + cmds << "delay 2000" + } else { + log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago" + } + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + + return [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.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 + } + def event = createEvent(map) + + // Save at least one battery report in events list every few days + if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) { + map.isStateChange = true + } + state.lastbat = now() + return [event] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + def manufacturerCode = String.format("%04X", cmd.manufacturerId) + def productTypeCode = String.format("%04X", cmd.productTypeId) + def productCode = String.format("%04X", cmd.productId) + def wirelessConfig = "ZWP" + log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}" + + result << createEvent(name: "ManufacturerCode", value: manufacturerCode) + result << createEvent(name: "ProduceTypeCode", value: productTypeCode) + result << createEvent(name: "ProductCode", value: productCode) + result << createEvent(name: "WirelessConfig", value: wirelessConfig) + + return result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)] +} + +private secure(physicalgraph.zwave.Command cmd) { + if (state.sec == 0) { // default to secure + cmd.format() + } else { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } +} + +private secureSequence(commands, delay=200) { + delayBetween(commands.collect{ secure(it) }, delay) +} diff --git a/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy index a7adfc3..7fcc0ac 100644 --- a/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy +++ b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy @@ -32,7 +32,7 @@ metadata { fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x31, 0x70, 0x84, 0x7A" fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,31,71" - fingerprint mfr:"0109", prod:"2002", model:"0205" + fingerprint mfr:"0109", prod:"2002", model:"0205" // not using deviceJoinName because it's sold under different brand names } tiles { @@ -212,7 +212,7 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm } def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { - def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true) // for Astralink + def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink def cmds = [] if (!device.currentState("ManufacturerCode")) { @@ -243,9 +243,14 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { } else { map.value = cmd.batteryLevel } + def event = createEvent(map) + + // Save at least one battery report in events list every few days + if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) { + map.isStateChange = true + } state.lastbat = now() - result << createEvent(map) - return result + return [event] } def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { From 9b285ec93b80e0d98349e8907244377f5031c60f Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Tue, 21 Jun 2016 13:19:53 -0400 Subject: [PATCH 5/7] PRP-151 - Harmony Cloud (#1003) - Added isIP() to check if a local or cloud callback URL - Added try around Integer.parseInt --- .../logitech-harmony-connect.groovy | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 7731c80..e3dda08 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -824,17 +824,51 @@ def deviceHandler(evt) { def sendToHarmony(evt, String callbackUrl) { def callback = new URI(callbackUrl) - def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host - def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path - sendHubCommand(new physicalgraph.device.HubAction( - method: "POST", - path: path, - headers: [ - "Host": host, - "Content-Type": "application/json" - ], - body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] - )) + if(isIP(callback.host)){ + def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host + def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path + sendHubCommand(new physicalgraph.device.HubAction( + method: "POST", + path: path, + headers: [ + "Host": host, + "Content-Type": "application/json" + ], + body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] + )) + } else { + def params = [ + uri: callbackUrl, + body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] + ] + try { + log.debug "Sending data to Harmony Cloud: $params" + httpPostJson(params) { resp -> + log.debug "Harmony Cloud - Response: ${resp.status}" + } + } catch (e) { + log.error "Harmony Cloud - Something went wrong: $e" + } + } +} + +public static boolean isIP(String str) { + try { + String[] parts = str.split("\\."); + if (parts.length != 4) return false; + for (int i = 0; i < 4; ++i) { + int p + try { + p = Integer.parseInt(parts[i]); + } catch (Exception e) { + return false; + } + if (p > 255 || p < 0) return false; + } + return true; + } catch (Exception e) { + return false; + } } def listHubs() { From 7d6f37d98fe0d25d241165e1971674c9eb7d786d Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Wed, 22 Jun 2016 11:12:10 -0400 Subject: [PATCH 6/7] Z-Wave Plus Secure Sensors: reduce configure() delay for join v2 --- .../zwave-plus-door-window-sensor.groovy | 2 +- .../zwave-plus-motion-temp-sensor.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy b/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy index 1da4557..93df68a 100644 --- a/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy +++ b/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy @@ -69,7 +69,7 @@ def configure() { if (state.sec != 1) { // secure inclusion may not be complete yet - cmds << "delay 3000" + cmds << "delay 1000" } cmds += secureSequence([ diff --git a/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy index 7fcc0ac..02d327c 100644 --- a/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy +++ b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy @@ -74,7 +74,7 @@ def configure() { if (state.sec != 1) { // secure inclusion may not be complete yet - cmds << "delay 3000" + cmds << "delay 1000" } cmds += secureSequence([ From dd63e76dfb55041ec3d75421383b186df5426233 Mon Sep 17 00:00:00 2001 From: jackchi Date: Wed, 22 Jun 2016 10:19:40 -0700 Subject: [PATCH 7/7] [CHF-131] Adding Health Check to V1 SmartSense Open/Close Sensor --- .../smartsense-open-closed-sensor.groovy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy index e879148..47d930d 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -15,12 +15,13 @@ */ metadata { - definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") { capability "Battery" capability "Configuration" capability "Contact Sensor" capability "Refresh" capability "Temperature Measurement" + capability "Health Check" command "enrollResponse" @@ -273,7 +274,8 @@ def refresh() { } def configure() { - + sendEvent(name: "checkInterval", value: 7200, displayed: false) + String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) log.debug "Configuring Reporting, IAS CIE, and Bindings." def configCmds = [