From aff8dec3ceea971d50924b74c3ea0daf51cd0a20 Mon Sep 17 00:00:00 2001 From: Nowak Date: Fri, 26 Feb 2016 13:12:26 +0100 Subject: [PATCH 01/14] MSA-904: Device Handler for Fibaro Flood Sensor ZW5 --- .../fibaro-flood-sensor-zw5.groovy | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy diff --git a/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy new file mode 100644 index 0000000..693f767 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy @@ -0,0 +1,269 @@ +/** + * Fibaro Flood Sensor ZW5 + * + * Copyright 2016 Fibar Group S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Fibaro Flood Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.") { + capability "Battery" + capability "Configuration" + capability "Sensor" + capability "Tamper Alert" + capability "Temperature Measurement" + capability "Water Sensor" + + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x22, 0x85, 0x59, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x9C, 0x31, 0x86", outClusters: "" + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"FGFS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + tileAttribute("device.water", key:"PRIMARY_CONTROL") { + attributeState("wet", icon:"st.alarm.water.wet", backgroundColor:"#ffa81e") + attributeState("dry", icon:"st.alarm.water.dry", backgroundColor:"#79b821") + } + + tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { + attributeState("active", label:'tamper active', backgroundColor:"#53a7c0") + attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff") + } + } + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "FGFS" + details(["FGFS","battery", "temperature"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + def result = [] + + if (description.startsWith("Err 106")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "FGFS failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else if (description == "updated") { + return null + } else { + def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72:2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + + if (cmd) { + log.debug "Parsed '${cmd}'" + zwaveEvent(cmd) + } + } +} + +//security +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +//crc16 +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x31: 5, 0x72: 2, 0x80: 1] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (!encapsulatedCommand) { + log.debug "Could not extract command from $cmd" + } else { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) +{ + def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) + def cmds = [] + cmds << encap(zwave.batteryV1.batteryGet()) + cmds << "delay 500" + cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)) + cmds << "delay 1200" + cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation()) + [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { + log.debug "deviceIdData: ${cmd.deviceIdData}" + log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}" + log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}" + log.debug "deviceIdType: ${cmd.deviceIdType}" + + if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format + String serialNumber = "h'" + + cmd.deviceIdData.each{ data -> + serialNumber += "${String.format("%02X", data)}" + } + + updateDataValue("serialNumber", serialNumber) + log.debug "${device.displayName} - serial number: ${serialNumber}" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() + map.unit = "%" + map.displayed = true + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def map = [:] + if (cmd.notificationType == 5) { + switch (cmd.event) { + case 2: + map.name = "water" + map.value = "wet" + map.descriptionText = "${device.displayName} is ${map.value}" + break + + case 0: + map.name = "water" + map.value = "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + map.name = "tamper" + map.value = "inactive" + map.descriptionText = "${device.displayName}: tamper alarm has been deactivated" + break + + case 3: + map.name = "tamper" + map.value = "active" + map.descriptionText = "${device.displayName}: tamper alarm activated" + break + } + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + if (cmd.sensorType == 1) { + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + map.displayed = true + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "${device.displayName}: received command: $cmd - device has reset itself" +} + +def configure() { + log.debug "Executing 'configure'" + + def cmds = [] + + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGFS' default wake up interval + cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet() + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.versionV1.versionGet() + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]) + cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() + + encapSequence(cmds, 500) +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crc16(physicalgraph.zwave.Command cmd) { + //zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() + "5601${cmd.format()}0000" +} + +private encapSequence(commands, delay=200) { + delayBetween(commands.collect{ encap(it) }, delay) +} + +private encap(physicalgraph.zwave.Command cmd) { + def secureClasses = [0x20, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C] + + //todo: check if secure inclusion was successful + //if not do not send security-encapsulated command + if (secureClasses.find{ it == cmd.commandClassId }) { + secure(cmd) + } else { + crc16(cmd) + } +} \ No newline at end of file From a46f09a84acc2501a34fae1406f9004f15237d14 Mon Sep 17 00:00:00 2001 From: Nowak Date: Fri, 26 Feb 2016 13:14:39 +0100 Subject: [PATCH 02/14] MSA-905: Device Handler for Fibaro Motion Sensor ZW5 --- .../fibaro-motion-sensor-zw5.groovy | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy diff --git a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy new file mode 100644 index 0000000..cd7966f --- /dev/null +++ b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy @@ -0,0 +1,282 @@ +/** + * Fibaro Motion Sensor ZW5 + * + * Copyright 2016 Fibar Group S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Fibaro Motion Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.") { + capability "Battery" + capability "Configuration" + capability "Illuminance Measurement" + capability "Motion Sensor" + capability "Sensor" + capability "Tamper Alert" + capability "Temperature Measurement" + + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x20, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x30, 0x9C, 0x98, 0x7A", outClusters: "" + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + tileAttribute("device.motion", key:"PRIMARY_CONTROL") { + attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e") + attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821") + } + + tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { + attributeState("active", label:'tamper active', backgroundColor:"#53a7c0") + attributeState("inactive", label:'tamper 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("illuminance", "device.illuminance", inactiveLabel: false) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + + valueTile("battery", "device.battery", inactiveLabel: false, + decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "FGMS" + details(["FGMS","battery","temperature","illuminance"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + def result = [] + + if (description.startsWith("Err 106")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "FGK failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else if (description == "updated") { + return null + } else { + def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + + if (cmd) { + log.debug "Parsed '${cmd}'" + zwaveEvent(cmd) + } + } +} + +//security +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +//crc16 +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x31: 5, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (!encapsulatedCommand) { + log.debug "Could not extract command from $cmd" + } else { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [ displayed: true ] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + map.unit = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, map.unit, cmd.precision) + break + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + break + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def map = [:] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + if (cmd.eventParameter[0] == 3) { + map.name = "tamper" + map.value = "inactive" + map.descriptionText = "${device.displayName}: tamper alarm has been deactivated" + } + if (cmd.eventParameter[0] == 8) { + map.name = "motion" + map.value = "inactive" + map.descriptionText = "${device.displayName}: motion has stopped" + } + break + + case 3: + map.name = "tamper" + map.value = "active" + map.descriptionText = "${device.displayName}: tamper alarm activated" + break + + case 8: + map.name = "motion" + map.value = "active" + map.descriptionText = "${device.displayName}: motion detected" + break + } + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() + map.unit = "%" + map.displayed = true + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) +{ + def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) + def cmds = [] + cmds << encap(zwave.batteryV1.batteryGet()) + cmds << "delay 500" + cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)) + cmds << "delay 500" + cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1)) + cmds << "delay 1200" + cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation()) + [event, response(cmds)] + +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { + log.debug "deviceIdData: ${cmd.deviceIdData}" + log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}" + log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}" + log.debug "deviceIdType: ${cmd.deviceIdType}" + + if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format + String serialNumber = "h'" + + cmd.deviceIdData.each{ data -> + serialNumber += "${String.format("%02X", data)}" + } + + updateDataValue("serialNumber", serialNumber) + log.debug "${device.displayName} - serial number: ${serialNumber}" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "${device.displayName}: received command: $cmd - device has reset itself" +} + +def configure() { + log.debug "Executing 'configure'" + + def cmds = [] + + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds: 7200, nodeid: zwaveHubNodeId)//FGMS' default wake up interval + cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet() + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.versionV1.versionGet() + cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]) + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1) + cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() + + encapSequence(cmds, 500) +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crc16(physicalgraph.zwave.Command cmd) { + //zwave.crc16encapV1.crc16Encap().encapsulate(cmd).format() + "5601${cmd.format()}0000" +} + +private encapSequence(commands, delay=200) { + delayBetween(commands.collect{ encap(it) }, delay) +} + +private encap(physicalgraph.zwave.Command cmd) { + def secureClasses = [0x20, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C] + + //todo: check if secure inclusion was successful + //if not do not send security-encapsulated command + if (secureClasses.find{ it == cmd.commandClassId }) { + secure(cmd) + } else { + crc16(cmd) + } +} \ No newline at end of file From 23a76fa72b0759f5db13cdc0b7bc3d8600ca8e8d Mon Sep 17 00:00:00 2001 From: Nowak Date: Fri, 26 Feb 2016 13:18:11 +0100 Subject: [PATCH 03/14] MSA-906: Device Handlers for Fibaro Door/Window Sensor ZW5 (with and without DS18B20 connected) --- ...-window-sensor-zw5-with-temperature.groovy | 272 ++++++++++++++++++ .../fibaro-door-window-sensor-zw5.groovy | 240 ++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy create mode 100644 devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy new file mode 100644 index 0000000..808b360 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy @@ -0,0 +1,272 @@ +/** + * Fibaro Door/Window Sensor ZW5 + * + * Copyright 2016 Fibar Group S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Fibaro Door/Window Sensor ZW5 with Temperature", namespace: "fibargroup", author: "Fibar Group S.A.") { + capability "Battery" + capability "Contact Sensor" + capability "Sensor" + capability "Configuration" + + capability "Temperature Measurement" + + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86", outClusters: "" + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86, 0x84", outClusters: ""//actual NIF + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + tileAttribute("device.motion", key:"PRIMARY_CONTROL") { + attributeState("open", icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + attributeState("closed", icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + + tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { + attributeState("active", label:'tamper active', backgroundColor:"#53a7c0") + attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff") + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, + decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + 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"] + ] + } + + main "FGK" + details(["FGK","battery", "temperature"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + def result = [] + + if (description.startsWith("Err 106")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "FGK failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else if (description == "updated") { + return null + } else { + def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + + if (cmd) { + log.debug "Parsed '${cmd}'" + zwaveEvent(cmd) + } + } +} + +//security +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x98: 1]) + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +//crc16 +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x31: 5, 0x72: 2, 0x80: 1, 0x86: 1] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (!encapsulatedCommand) { + log.debug "Could not extract command from $cmd" + } else { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + //it is assumed that default notification events are used + //(parameter 20 was not changed before device's re-inclusion) + def map = [:] + if (cmd.notificationType == 6) { + switch (cmd.event) { + case 22: + map.name = "contact" + map.value = "open" + map.descriptionText = "${device.displayName}: is open" + break + + case 23: + map.name = "contact" + map.value = "closed" + map.descriptionText = "${device.displayName}: is closed" + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + map.name = "tamper" + map.value = "inactive" + map.descriptionText = "${device.displayName}: tamper alarm has been deactivated" + break + + case 3: + map.name = "tamper" + map.value = "active" + map.descriptionText = "${device.displayName}: tamper alarm activated" + break + } + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() + map.unit = "%" + map.displayed = true + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) + def cmds = [] + cmds << encap(zwave.batteryV1.batteryGet()) + cmds << "delay 500" + cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)) + cmds << "delay 1200" + cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation()) + [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { + log.debug "deviceIdData: ${cmd.deviceIdData}" + log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}" + log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}" + log.debug "deviceIdType: ${cmd.deviceIdType}" + + if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format + String serialNumber = "h'" + + cmd.deviceIdData.each{ data -> + serialNumber += "${String.format("%02X", data)}" + } + + updateDataValue("serialNumber", serialNumber) + log.debug "${device.displayName} - serial number: ${serialNumber}" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + if (cmd.sensorType == 1) { + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + map.displayed = true + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "${device.displayName}: received command: $cmd - device has reset itself" +} + +def configure() { + log.debug "Executing 'configure'" + + def cmds = [] + + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGK's default wake up interval + cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet() + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.versionV1.versionGet() + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]) + cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() + + encapSequence(cmds, 500) +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crc16(physicalgraph.zwave.Command cmd) { + //zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() + "5601${cmd.format()}0000" +} + +private encapSequence(commands, delay=200) { + delayBetween(commands.collect{ encap(it) }, delay) +} + +private encap(physicalgraph.zwave.Command cmd) { + def secureClasses = [0x20, 0x2B, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C] + + //todo: check if secure inclusion was successful + //if not do not send security-encapsulated command + if (secureClasses.find{ it == cmd.commandClassId }) { + secure(cmd) + } else { + crc16(cmd) + } +} \ No newline at end of file diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy new file mode 100644 index 0000000..aebf359 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy @@ -0,0 +1,240 @@ +/** + * Fibaro Door/Window Sensor ZW5 + * + * Copyright 2016 Fibar Group S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Fibaro Door/Window Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.") { + capability "Battery" + capability "Contact Sensor" + capability "Sensor" + capability "Configuration" + capability "Tamper Alert" + + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x86, 0x84", outClusters: "" + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + tileAttribute("device.motion", key:"PRIMARY_CONTROL") { + attributeState("open", icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + attributeState("closed", icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + + tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { + attributeState("active", label:'tamper active', backgroundColor:"#53a7c0") + attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff") + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, + decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "FGK" + details(["FGK","battery"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + def result = [] + + if (description.startsWith("Err 106")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "FGK failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else if (description == "updated") { + return null + } else { + def cmd = zwave.parse(description, [0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + + if (cmd) { + log.debug "Parsed '${cmd}'" + zwaveEvent(cmd) + } + } +} + +//security +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x98: 1]) + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +//crc16 +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x72: 2, 0x80: 1, 0x86: 1] + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (!encapsulatedCommand) { + log.debug "Could not extract command from $cmd" + } else { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + //it is assumed that default notification events are used + //(parameter 20 was not changed before device's re-inclusion) + def map = [:] + if (cmd.notificationType == 6) { + switch (cmd.event) { + case 22: + map.name = "contact" + map.value = "open" + map.descriptionText = "${device.displayName}: is open" + break + + case 23: + map.name = "contact" + map.value = "closed" + map.descriptionText = "${device.displayName}: is closed" + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + map.name = "tamper" + map.value = "inactive" + map.descriptionText = "${device.displayName}: tamper alarm has been deactivated" + break + + case 3: + map.name = "tamper" + map.value = "active" + map.descriptionText = "${device.displayName}: tamper alarm activated" + break + } + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() + map.unit = "%" + map.displayed = true + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) + def cmds = [] + cmds << encap(zwave.batteryV1.batteryGet()) + cmds << "delay 1200" + cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation()) + [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { + log.debug "deviceIdData: ${cmd.deviceIdData}" + log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}" + log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}" + log.debug "deviceIdType: ${cmd.deviceIdType}" + + if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format + String serialNumber = "h'" + + cmd.deviceIdData.each{ data -> + serialNumber += "${String.format("%02X", data)}" + } + + updateDataValue("serialNumber", serialNumber) + log.debug "${device.displayName} - serial number: ${serialNumber}" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "${device.displayName}: received command: $cmd - device has reset itself" +} + +def configure() { + log.debug "Executing 'configure'" + + def cmds = [] + + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGK's default wake up interval + cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet() + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.versionV1.versionGet() + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]) + cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() + + encapSequence(cmds, 500) +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crc16(physicalgraph.zwave.Command cmd) { + //zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() + "5601${cmd.format()}0000" +} + +private encapSequence(commands, delay=200) { + delayBetween(commands.collect{ encap(it) }, delay) +} + +private encap(physicalgraph.zwave.Command cmd) { + def secureClasses = [0x20, 0x2B, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C] + + //todo: check if secure inclusion was successful + //if not do not send security-encapsulated command + if (secureClasses.find{ it == cmd.commandClassId }) { + secure(cmd) + } else { + crc16(cmd) + } +} \ No newline at end of file From b211b298c0dc81fba77b597dc21474b25754f8ec Mon Sep 17 00:00:00 2001 From: Nowak Date: Mon, 29 Feb 2016 15:10:02 +0100 Subject: [PATCH 04/14] updated tiles --- .../fibaro-motion-sensor-zw5.groovy | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy index cd7966f..69ef3ce 100644 --- a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy @@ -43,7 +43,7 @@ metadata { } } - valueTile("temperature", "device.temperature", inactiveLabel: false) { + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { state "temperature", label:'${currentValue}°', backgroundColors:[ [value: 31, color: "#153591"], @@ -56,13 +56,12 @@ metadata { ] } - valueTile("illuminance", "device.illuminance", inactiveLabel: false) { + valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { state "luminosity", label:'${currentValue} ${unit}', unit:"lux" } - valueTile("battery", "device.battery", inactiveLabel: false, - decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" } main "FGMS" From 1af43681a5a36de08e540f091f731afe81369931 Mon Sep 17 00:00:00 2001 From: Nowak Date: Mon, 29 Feb 2016 15:10:26 +0100 Subject: [PATCH 05/14] updated tiles --- ...fibaro-door-window-sensor-zw5-with-temperature.groovy | 9 ++++----- .../fibaro-door-window-sensor-zw5.groovy | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy index 808b360..e14bba3 100644 --- a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy @@ -32,7 +32,7 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app - tileAttribute("device.motion", key:"PRIMARY_CONTROL") { + tileAttribute("device.contact", key:"PRIMARY_CONTROL") { attributeState("open", icon:"st.contact.contact.open", backgroundColor:"#ffa81e") attributeState("closed", icon:"st.contact.contact.closed", backgroundColor:"#79b821") } @@ -43,12 +43,11 @@ metadata { } } - valueTile("battery", "device.battery", inactiveLabel: false, - decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" + valueTile("battery", "device.battery", inactiveLabel: false, , width: 2, height: 2, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" } - valueTile("temperature", "device.temperature", inactiveLabel: false) { + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { state "temperature", label:'${currentValue}°', backgroundColors:[ [value: 31, color: "#153591"], diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy index aebf359..f65f838 100644 --- a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy @@ -27,10 +27,10 @@ metadata { simulator { } - + tiles(scale: 2) { multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app - tileAttribute("device.motion", key:"PRIMARY_CONTROL") { + tileAttribute("device.contact", key:"PRIMARY_CONTROL") { attributeState("open", icon:"st.contact.contact.open", backgroundColor:"#ffa81e") attributeState("closed", icon:"st.contact.contact.closed", backgroundColor:"#79b821") } @@ -41,9 +41,8 @@ metadata { } } - valueTile("battery", "device.battery", inactiveLabel: false, - decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" + valueTile("battery", "device.battery", inactiveLabel: false, , width: 2, height: 2, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" } main "FGK" From 3824ccb5e180b6c86d0eb539b5c82fd5bf8bfe10 Mon Sep 17 00:00:00 2001 From: Nowak Date: Wed, 9 Mar 2016 15:31:52 +0100 Subject: [PATCH 06/14] updated initial sensor status (dry) --- .../fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy index 693f767..4ab0479 100644 --- a/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy @@ -32,8 +32,8 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"FGFS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app tileAttribute("device.water", key:"PRIMARY_CONTROL") { + attributeState("dry", icon:"st.alarm.water.dry", backgroundColor:"#79b821") attributeState("wet", icon:"st.alarm.water.wet", backgroundColor:"#ffa81e") - attributeState("dry", icon:"st.alarm.water.dry", backgroundColor:"#79b821") } tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { From e2ab965e89807bb9fc8ab5f0b4bb5961ac1cb087 Mon Sep 17 00:00:00 2001 From: Nowak Date: Wed, 9 Mar 2016 15:32:22 +0100 Subject: [PATCH 07/14] updated initial sensor status (no motion) --- .../fibaro-motion-sensor-zw5.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy index 69ef3ce..864420f 100644 --- a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy @@ -33,8 +33,8 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app tileAttribute("device.motion", key:"PRIMARY_CONTROL") { - attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e") - attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821") + attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821") + attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e") } tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { From 8a5f0af0e250f18cc32434d1debfd7d7a96cc637 Mon Sep 17 00:00:00 2001 From: Nowak Date: Thu, 17 Mar 2016 16:02:14 +0100 Subject: [PATCH 08/14] added missing capability --- .../fibaro-door-window-sensor-zw5-with-temperature.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy index e14bba3..56f12ac 100644 --- a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy @@ -19,6 +19,7 @@ metadata { capability "Contact Sensor" capability "Sensor" capability "Configuration" + capability "Tamper Alert" capability "Temperature Measurement" From 805b87044766b1d4b119c29e9eedba7879828580 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Fri, 25 Mar 2016 10:12:48 -0700 Subject: [PATCH 09/14] DVCSMP-1667 Authentication needs to be updated for Philips Hue --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 1a8829a..0da5905 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -161,7 +161,7 @@ private sendDeveloperReq() { headers: [ HOST: host ], - body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}")) + body: [devicetype: "$token-0"]], "${selectedHue}")) } private discoverHueBulbs() { From c6818c8c2b1651cc3771a79b84214b9489ae2497 Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Mon, 28 Mar 2016 13:29:51 -0400 Subject: [PATCH 10/14] Add new Ecolink Motion MSR to door/window retyping code --- .../zwave-door-window-sensor.src/zwave-door-window-sensor.groovy | 1 + 1 file changed, 1 insertion(+) 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 293be99..ab1853d 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 @@ -245,6 +245,7 @@ def retypeBasedOnMSR() { break case "011F-0001-0001": // Schlage motion case "014A-0001-0001": // Ecolink motion + case "014A-0004-0001": // Ecolink motion + case "0060-0001-0002": // Everspring SP814 case "0060-0001-0003": // Everspring HSP02 case "011A-0601-0901": // Enerwave ZWN-BPC From a6ee53641f74664148fbe2cf78bdc9058485188c Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Mon, 28 Mar 2016 20:51:32 -0700 Subject: [PATCH 11/14] DVCSMP-1672 Phillips HUE: Mismatched in on button light color for hue lux -Changed color from green to blue to match other Hue lights --- .../smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index 408c2e2..5779cc0 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -23,10 +23,10 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel", range:"(0..100)" From 0c5840087b163507a7681860ffbbba6e7e7290ab Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Mon, 28 Mar 2016 09:32:19 -0700 Subject: [PATCH 12/14] DVCSMP-1597 Philips HUE: Bloom and Strip lights need temperature control removed -Added new device type for Hue lights that have color control but no temperature control (Bloom/Strip) -Add missing event to setLevel --- .../hue-bloom.src/hue-bloom.groovy | 227 ++++++++++++++++++ .../smartthings/hue-bulb.src/hue-bulb.groovy | 6 +- .../hue-lux-bulb.src/hue-lux-bulb.groovy | 7 +- .../hue-connect.src/hue-connect.groovy | 53 +++- 4 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy diff --git a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy new file mode 100644 index 0000000..eedd244 --- /dev/null +++ b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy @@ -0,0 +1,227 @@ +/** + * Hue Bloom + * + * Philips Hue Type "Color Light" + * + * Author: SmartThings + */ + +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue Bloom", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Switch" + capability "Refresh" + capability "Sensor" + + command "setAdjustedColor" + command "reset" + command "refresh" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles (scale: 2){ + multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel", range:"(0..100)" + } + tileAttribute ("device.level", key: "SECONDARY_CONTROL") { + attributeState "level", label: 'Level ${currentValue}%' + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } + + standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" + } + + standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["rich-control"]) + details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) + } +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + def map = description + if (description instanceof String) { + log.debug "Hue Bulb stringToMap - ${map}" + map = stringToMap(description) + } + + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + results +} + +// handle commands +void on() { + log.trace parent.on(this) + sendEvent(name: "switch", value: "on") +} + +void off() { + log.trace parent.off(this) + sendEvent(name: "switch", value: "off") +} + +void nextLevel() { + def level = device.latestValue("level") as Integer ?: 0 + if (level <= 100) { + level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer + } + else { + level = 25 + } + setLevel(level) +} + +void setLevel(percent) { + log.debug "Executing 'setLevel'" + if (verifyPercent(percent)) { + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%") + sendEvent(name: "switch", value: "on") + } +} + +void setSaturation(percent) { + log.debug "Executing 'setSaturation'" + if (verifyPercent(percent)) { + parent.setSaturation(this, percent) + sendEvent(name: "saturation", value: percent, displayed: false) + } +} + +void setHue(percent) { + log.debug "Executing 'setHue'" + if (verifyPercent(percent)) { + parent.setHue(this, percent) + sendEvent(name: "hue", value: percent, displayed: false) + } +} + +void setColor(value) { + log.debug "setColor: ${value}, $this" + def events = [] + def validValues = [:] + + if (verifyPercent(value.hue)) { + events << createEvent(name: "hue", value: value.hue, displayed: false) + validValues.hue = value.hue + } + if (verifyPercent(value.saturation)) { + events << createEvent(name: "saturation", value: value.saturation, displayed: false) + validValues.saturation = value.saturation + } + if (value.hex != null) { + if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) { + events << createEvent(name: "color", value: value.hex) + validValues.hex = value.hex + } else { + log.warn "$value.hex is not a valid color" + } + } + if (verifyPercent(value.level)) { + events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%") + validValues.level = value.level + } + if (value.switch == "off" || (value.level != null && value.level <= 0)) { + events << createEvent(name: "switch", value: "off") + validValues.switch = "off" + } else { + events << createEvent(name: "switch", value: "on") + validValues.switch = "on" + } + if (!events.isEmpty()) { + parent.setColor(this, validValues) + } + events.each { + sendEvent(it) + } +} + +void reset() { + log.debug "Executing 'reset'" + def value = [level:100, saturation:56, hue:23] + setAdjustedColor(value) + parent.poll() +} + +void setAdjustedColor(value) { + if (value) { + log.trace "setAdjustedColor: ${value}" + def adjusted = value + [:] + adjusted.hue = adjustOutgoingHue(value.hue) + // Needed because color picker always sends 100 + adjusted.level = null + setColor(adjusted) + } else { + log.warn "Invalid color input" + } +} + +void setColorTemperature(value) { + if (value) { + log.trace "setColorTemperature: ${value}k" + parent.setColorTemperature(this, value) + sendEvent(name: "colorTemperature", value: value) + sendEvent(name: "switch", value: "on") + } else { + log.warn "Invalid color temperature" + } +} + +void refresh() { + log.debug "Executing 'refresh'" + parent.manualRefresh() +} + +def adjustOutgoingHue(percent) { + def adjusted = percent + if (percent > 31) { + if (percent < 63.0) { + adjusted = percent + (7 * (percent -30 ) / 32) + } + else if (percent < 73.0) { + adjusted = 69 + (5 * (percent - 62) / 10) + } + else { + adjusted = percent + (2 * (100 - percent) / 28) + } + } + log.info "percent: $percent, adjusted: $adjusted" + adjusted +} + +def verifyPercent(percent) { + if (percent == null) + return false + else if (percent >= 0 && percent <= 100) { + return true + } else { + log.warn "$percent is not 0-100" + return false + } +} diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 99b529b..93a0130 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -1,6 +1,8 @@ /** * Hue Bulb * + * Philips Hue Type "Extended Color Light" + * * Author: SmartThings */ @@ -69,11 +71,13 @@ metadata { def parse(description) { log.debug "parse() - $description" def results = [] + def map = description if (description instanceof String) { log.debug "Hue Bulb stringToMap - ${map}" map = stringToMap(description) } + if (map?.name && map?.value) { results << createEvent(name: "${map?.name}", value: "${map?.value}") } @@ -229,4 +233,4 @@ def verifyPercent(percent) { log.warn "$percent is not 0-100" return false } -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index 408c2e2..a1d7f33 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -1,6 +1,8 @@ /** * Hue Lux Bulb * + * Philips Hue Type "Dimmable Light" + * * Author: SmartThings */ // for the UI @@ -68,12 +70,12 @@ def parse(description) { // handle commands void on() { - parent.on(this) + log.trace parent.on(this) sendEvent(name: "switch", value: "on") } void off() { - parent.off(this) + log.trace parent.off(this) sendEvent(name: "switch", value: "off") } @@ -82,6 +84,7 @@ void setLevel(percent) { if (percent != null && percent >= 0 && percent <= 100) { parent.setLevel(this, percent) sendEvent(name: "level", value: percent) + sendEvent(name: "switch", value: "on") } else { log.warn "$percent is not 0-100" } diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 0da5905..10980af 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -289,7 +289,7 @@ def bulbListHandler(hub, data = "") { def object = new groovy.json.JsonSlurper().parseText(data) object.each { k,v -> if (v instanceof Map) - bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub] + bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub] } } def bridge = null @@ -300,6 +300,40 @@ def bulbListHandler(hub, data = "") { return msg } +private upgradeDeviceType(device, newHueType) { + def deviceType = getDeviceType(newHueType) + + // Automatically change users Hue bulbs to correct device types + if (deviceType && !(device?.typeName?.equalsIgnoreCase(deviceType))) { + log.debug "Update device type: \"$device.label\" ${device?.typeName}->$deviceType" + device.setDeviceType(deviceType) + } +} + +private getDeviceType(hueType) { + // Determine ST device type based on Hue classification of light + if (hueType?.equalsIgnoreCase("Dimmable light")) + return "Hue Lux Bulb" + else if (hueType?.equalsIgnoreCase("Extended Color Light")) + return "Hue Bulb" + else if (hueType?.equalsIgnoreCase("Color Light")) + return "Hue Bloom" + else + return null +} + +private addChildBulb(dni, hueType, name, hub, update=false, device = null) { + def deviceType = getDeviceType(hueType) + + if (deviceType) { + return addChildDevice("smartthings", deviceType, dni, hub, ["label": name]) + } + else { + log.warn "Device type $hueType not supported" + return null + } +} + def addBulbs() { def bulbs = getHueBulbs() selectedBulbs?.each { dni -> @@ -309,11 +343,7 @@ def addBulbs() { if (bulbs instanceof java.util.Map) { newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } if (newHueBulb != null) { - if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) { - d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) - } else { - d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) - } + d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub) log.debug "created ${d.displayName} with id $dni" d.refresh() } else { @@ -322,16 +352,15 @@ def addBulbs() { } else { //backwards compatable newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } - d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name]) + d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub) d.refresh() } } else { log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'" if (bulbs instanceof java.util.Map) { + // Update device type if incorrect def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } - if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") { - d.setDeviceType("Hue Lux Bulb") - } + upgradeDeviceType(d, newHueBulb?.value?.type) } } } @@ -473,7 +502,7 @@ def locationHandler(evt) { def bulbs = getHueBulbs() log.debug "Adding bulbs to state!" body.each { k,v -> - bulbs[k] = [id: k, name: v.name, type: v.type, hub:parsedEvent.hub] + bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:parsedEvent.hub] } } } @@ -836,7 +865,7 @@ def convertBulbListToMap() { if (state.bulbs instanceof java.util.List) { def map = [:] state.bulbs.unique {it.id}.each { bulb -> - map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "hub":bulb.hub]] + map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]] } state.bulbs = map } From 4a096fc884e8df98437f067fcd5bc1e350d7f6f9 Mon Sep 17 00:00:00 2001 From: Mitch Pond Date: Thu, 31 Mar 2016 16:36:42 +0000 Subject: [PATCH 13/14] Iris Smart Fob - cleaned up and commented out fingerprint for submission --- .../iris-smart-fob.src/iris-smart-fob.groovy | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy diff --git a/devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy b/devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy new file mode 100644 index 0000000..037e91f --- /dev/null +++ b/devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy @@ -0,0 +1,194 @@ +/** + * Iris Smart Fob + * + * Copyright 2015 Mitch Pond + * Presence code adapted from SmartThings Arrival Sensor HA device type + * + * 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: "Iris Smart Fob", namespace: "mitchpond", author: "Mitch Pond") { + capability "Battery" + capability "Button" + capability "Configuration" + capability "Presence Sensor" + capability "Sensor" + + //fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0001,0003,0007,0020,0B05", outClusters: "0003,0006,0019", model:"3450-L", manufacturer: "CentraLite" + } + + preferences{ + input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"", + defaultValue: 3, displayDuringSetup: false) + input "checkInterval", "enum", title: "Presence timeout (minutes)", + defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false + input "logging", "bool", title: "Enable debug logging", + defaultValue: false, displayDuringSetup: false + } + + tiles(scale: 2) { + standardTile("presence", "device.presence", width: 4, height: 4, canChangeBackground: true) { + state "present", label: "Present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0" + state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff" + } + standardTile("button", "device.button", decoration: "flat", width: 2, height: 2) { + state "default", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main (["presence"]) + details(["presence","button","battery"]) + } +} + +def parse(String description) { + def descMap = zigbee.parseDescriptionAsMap(description) + logIt descMap + state.lastCheckin = now() + logIt "lastCheckin = ${state.lastCheckin}" + handlePresenceEvent(true) + + def results = [] + if (description?.startsWith('catchall:')) + results = parseCatchAllMessage(descMap) + else if (description?.startsWith('read attr -')) + results = parseReportAttributeMessage(descMap) + else logIt(descMap, "trace") + + return results; +} + +def updated() { + startTimer() + configure() +} + +def configure(){ + logIt "Configuring Smart Fob..." + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 2 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 3 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 4 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 200" + ] + + zigbee.configureReporting(0x0001,0x0020,0x20,20,20,0x01) +} + +def parseCatchAllMessage(descMap) { + if (descMap?.clusterId == "0006" && descMap?.command == "01") //button pressed + handleButtonPress(descMap.sourceEndpoint as int) + else if (descMap?.clusterId == "0006" && descMap?.command == "00") //button released + handleButtonRelease(descMap.sourceEndpoint as int) + else logIt("Parse: Unhandled message: ${descMap}","trace") +} + +def parseReportAttributeMessage(descMap) { + if (descMap?.cluster == "0001" && descMap?.attrId == "0020") createBatteryEvent(getBatteryLevel(descMap.value)) + else logIt descMap +} + +private createBatteryEvent(percent) { + logIt "Battery level at " + percent + return createEvent([name: "battery", value: percent]) +} + +//this method determines if a press should count as a push or a hold and returns the relevant event type +private handleButtonRelease(button) { + logIt "lastPress state variable: ${state.lastPress}" + def sequenceError = {logIt("Uh oh...missed a message? Dropping this event.", "error"); state.lastPress = null; return []} + + if (!state.lastPress) return sequenceError() + else if (state.lastPress.button != button) return sequenceError() + + def currentTime = now() + def startOfPress = state.lastPress?.time + def timeDif = currentTime - startOfPress + def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000 + + state.lastPress = null //we're done with this. clear it to make error conditions easier to catch + + if (timeDif < 0) + //likely a message sequence issue or dropped packet. Drop this press and wait for another. + return sequenceError() + else if (timeDif < holdTimeMillisec) + return createButtonEvent(button,"pushed") + else + return createButtonEvent(button,"held") +} + +private handleButtonPress(button) { + state.lastPress = [button: button, time: now()] +} + +private createButtonEvent(button,action) { + logIt "Button ${button} ${action}" + return createEvent([ + name: "button", + value: action, + data:[buttonNumber: button], + descriptionText: "${device.displayName} button ${button} was ${action}", + isStateChange: true, + displayed: true]) +} + +private getBatteryLevel(rawValue) { + def intValue = Integer.parseInt(rawValue,16) + def min = 2.1 + def max = 3.0 + def vBatt = intValue / 10 + return ((vBatt - min) / (max - min) * 100) as int +} + +private handlePresenceEvent(present) { + def wasPresent = device.currentState("presence")?.value == "present" + if (!wasPresent && present) { + logIt "Sensor is present" + startTimer() + } else if (!present) { + logIt "Sensor is not present" + stopTimer() + } + def linkText = getLinkText(device) + def eventMap = [ + name: "presence", + value: present ? "present" : "not present", + linkText: linkText, + descriptionText: "${linkText} has ${present ? 'arrived' : 'left'}", + ] + logIt "Creating presence event: ${eventMap}" + sendEvent(eventMap) +} + +private startTimer() { + logIt "Scheduling periodic timer" + schedule("0 * * * * ?", checkPresenceCallback) +} + +private stopTimer() { + logIt "Stopping periodic timer" + unschedule() +} + +def checkPresenceCallback() { + def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000 + def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60 + logIt "Sensor checked in ${timeSinceLastCheckin} seconds ago" + if (timeSinceLastCheckin >= theCheckInterval) { + handlePresenceEvent(false) + } +} + +// ****** Utility functions ****** + +private logIt(str, logLevel = 'debug') {if (settings.logging) log."$logLevel"(str) } \ No newline at end of file From c259af43126037a19ae971a66f2a7bfb3d9ef5f4 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Mon, 4 Apr 2016 17:04:15 -0700 Subject: [PATCH 14/14] change name for button to generic name --- .../zigbee-button.src/zigbee-button.groovy} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename devicetypes/{mitchpond/iris-smart-fob.src/iris-smart-fob.groovy => smartthings/zigbee-button.src/zigbee-button.groovy} (98%) diff --git a/devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy similarity index 98% rename from devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy rename to devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy index 037e91f..68e8cee 100644 --- a/devicetypes/mitchpond/iris-smart-fob.src/iris-smart-fob.groovy +++ b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy @@ -15,7 +15,7 @@ * */ metadata { - definition (name: "Iris Smart Fob", namespace: "mitchpond", author: "Mitch Pond") { + definition (name: "ZigBee Button", namespace: "smartthings", author: "Mitch Pond") { capability "Battery" capability "Button" capability "Configuration"