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..56f12ac --- /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 "Tamper Alert" + + 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.contact", 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, , width: 2, height: 2, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + main "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..f65f838 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy @@ -0,0 +1,239 @@ +/** + * 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.contact", 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, , width: 2, height: 2, 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 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..4ab0479 --- /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("dry", icon:"st.alarm.water.dry", backgroundColor:"#79b821") + attributeState("wet", icon:"st.alarm.water.wet", backgroundColor:"#ffa81e") + } + + 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 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..864420f --- /dev/null +++ b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy @@ -0,0 +1,281 @@ +/** + * 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("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821") + attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e") + } + + 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("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, 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 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 c9a6d13..950ac51 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}") } 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..fb7eb81 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 @@ -23,10 +25,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)" @@ -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/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy b/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy new file mode 100644 index 0000000..19a9d91 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy @@ -0,0 +1,225 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "carouselDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Thermostat" + capability "Relative Humidity Measurement" + + command "tempUp" + command "tempDown" + command "heatUp" + command "heatDown" + command "coolUp" + command "coolDown" + command "setTemperature", ["number"] + } + + tiles(scale: 2) { + multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("default", action: "setTemperature") + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}%', unit:"%") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + attributeState("cooling", backgroundColor:"#269bd2") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + } + + main("thermostatMulti") + details([ + "thermostatMulti" + ]) + } +} + +def installed() { + sendEvent(name: "temperature", value: 72, unit: "F") + sendEvent(name: "heatingSetpoint", value: 70, unit: "F") + sendEvent(name: "thermostatSetpoint", value: 70, unit: "F") + sendEvent(name: "coolingSetpoint", value: 76, unit: "F") + sendEvent(name: "thermostatMode", value: "off") + sendEvent(name: "thermostatFanMode", value: "fanAuto") + sendEvent(name: "thermostatOperatingState", value: "idle") + sendEvent(name: "humidity", value: 53, unit: "%") +} + +def parse(String description) { +} + +def evaluate(temp, heatingSetpoint, coolingSetpoint) { + log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint" + def threshold = 1.0 + def current = device.currentValue("thermostatOperatingState") + def mode = device.currentValue("thermostatMode") + + def heating = false + def cooling = false + def idle = false + if (mode in ["heat","emergency heat","auto"]) { + if (heatingSetpoint - temp >= threshold) { + heating = true + sendEvent(name: "thermostatOperatingState", value: "heating") + } + else if (temp - heatingSetpoint >= threshold) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (mode in ["cool","auto"]) { + if (temp - coolingSetpoint >= threshold) { + cooling = true + sendEvent(name: "thermostatOperatingState", value: "cooling") + } + else if (coolingSetpoint - temp >= threshold && !heating) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: coolingSetpoint) + } + else { + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (idle && !heating && !cooling) { + sendEvent(name: "thermostatOperatingState", value: "idle") + } +} + +def setHeatingSetpoint(Double degreesF) { + log.debug "setHeatingSetpoint($degreesF)" + sendEvent(name: "heatingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint")) +} + +def setCoolingSetpoint(Double degreesF) { + log.debug "setCoolingSetpoint($degreesF)" + sendEvent(name: "coolingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF) +} + +def setThermostatMode(String value) { + sendEvent(name: "thermostatMode", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setThermostatFanMode(String value) { + sendEvent(name: "thermostatFanMode", value: value) +} + +def off() { + sendEvent(name: "thermostatMode", value: "off") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heat() { + sendEvent(name: "thermostatMode", value: "heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def auto() { + sendEvent(name: "thermostatMode", value: "auto") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def emergencyHeat() { + sendEvent(name: "thermostatMode", value: "emergency heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def cool() { + sendEvent(name: "thermostatMode", value: "cool") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def fanOn() { + sendEvent(name: "thermostatFanMode", value: "fanOn") +} + +def fanAuto() { + sendEvent(name: "thermostatFanMode", value: "fanAuto") +} + +def fanCirculate() { + sendEvent(name: "thermostatFanMode", value: "fanCirculate") +} + +def tempUp() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue + 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def tempDown() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue - 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setTemperature(value) { + def ts = device.currentState("temperature") + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heatUp() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue + 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + +def heatDown() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue - 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + + +def coolUp() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue + 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} + +def coolDown() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue - 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} diff --git a/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy b/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy new file mode 100644 index 0000000..8086c2c --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy @@ -0,0 +1,52 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "colorWheelDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Color Control" + } + + tiles(scale: 2) { + valueTile("currentColor", "device.color") { + state "default", label: '${currentValue}' + } + + controlTile("rgbSelector", "device.color", "color", height: 6, width: 6, inactiveLabel: false) { + state "color", action: "color control.setColor" + } + + main("currentColor") + details([ + "rgbSelector" + ]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def setSaturation(percent) { + log.debug "Executing 'setSaturation'" + sendEvent(name: "saturation", value: percent) +} + +def setHue(percent) { + log.debug "Executing 'setHue'" + sendEvent(name: "hue", value: percent) +} diff --git a/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy b/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy new file mode 100644 index 0000000..968fc08 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy @@ -0,0 +1,63 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "presenceDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Presence Sensor" + + command "arrived" + command "departed" + } + + tiles(scale: 2) { + // You only get a presence tile view when the size is 3x3 otherwise it's a value tile + standardTile("presence", "device.presence", width: 3, height: 3, canChangeBackground: true) { + state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0") + state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2") + } + + standardTile("notPresentBtn", "device.fake", width: 3, height: 3, decoration: "flat") { + state("not present", label:'not present', backgroundColor:"#ffffff", action:"departed") + } + + standardTile("presentBtn", "device.fake", width: 3, height: 3, decoration: "flat") { + state("present", label:'present', backgroundColor:"#53a7c0", action:"arrived") + } + + main("presence") + details([ + "presence", "presenceControl", "notPresentBtn", "presentBtn" + ]) + } +} + +def installed() { + sendEvent(name: "presence", value: "present") +} + +def parse(String description) { +} + +def arrived() { + log.trace "Executing 'arrived'" + sendEvent(name: "presence", value: "present") +} + +def departed() { + log.trace "Executing 'arrived'" + sendEvent(name: "presence", value: "not present") +} diff --git a/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy b/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy new file mode 100644 index 0000000..12e30be --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy @@ -0,0 +1,75 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "sliderDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Switch Level" + command "setRangedLevel", ["number"] + } + + tiles(scale: 2) { + controlTile("tinySlider", "device.level", "slider", height: 2, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + controlTile("mediumSlider", "device.level", "slider", height: 2, width: 4, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + controlTile("largeSlider", "device.level", "slider", decoration: "flat", height: 2, width: 6, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + controlTile("rangeSlider", "device.rangedLevel", "slider", height: 2, width: 4, range: "(20..80)") { + state "level", action:"setRangedLevel" + } + + valueTile("rangeValue", "device.rangedLevel", height: 2, width: 2) { + state "default", label:'${currentValue}' + } + + controlTile("rangeSliderConstrained", "device.rangedLevel", "slider", height: 2, width: 4, range: "(40..60)") { + state "level", action:"setRangedLevel" + } + + main("rangeValue") + details([ + "tinySlider", "mediumSlider", + "largeSlider", + "rangeSlider", "rangeValue", + "rangeSliderConstrained" + ]) + } +} + +def installed() { + sendEvent(name: "level", value: 63) + sendEvent(name: "rangedLevel", value: 47) +} + +def parse(String description) { +} + +def setLevel(value) { + log.debug "setting level to $value" + sendEvent(name:"level", value:value) +} + +def setRangedLevel(value) { + log.debug "setting ranged level to $value" + sendEvent(name:"rangedLevel", value:value) +} diff --git a/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy b/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy new file mode 100644 index 0000000..3999313 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy @@ -0,0 +1,109 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "standardDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Switch" + } + + tiles(scale: 2) { + // standard tile with actions + standardTile("actionRings", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + + // standard flat tile with actions + standardTile("actionFlat", "device.switch", width: 2, height: 2, canChangeIcon: true, decoration: "flat") { + state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + + // standard flat tile without actions + standardTile("noActionFlat", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${currentValue}',icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + + // standard flat tile with only a label + standardTile("flatLabel", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff" + } + + // standard flat tile with icon and label + standardTile("flatIconLabel", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff" + } + + // standard flat tile with only icon (Refreh text is IN the icon file) + standardTile("flatIcon", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + // standard with defaultState = true + standardTile("flatDefaultState", "null", width: 2, height: 2, decoration: "flat") { + state "off", label: 'Fail!', icon: "st.switches.switch.off" + state "on", label: 'Pass!', icon: "st.switches.switch.on", defaultState: true + } + + // standard with implicit defaultState based on order (0 index is selected) + standardTile("flatImplicitDefaultState1", "null", width: 2, height: 2, decoration: "flat") { + state "on", label: 'Pass!', icon: "st.switches.switch.on" + state "off", label: 'Fail!', icon: "st.switches.switch.off" + } + + // standard with implicit defaultState based on state.name == default + standardTile("flatImplicitDefaultState2", "null", width: 2, height: 2, decoration: "flat") { + state "off", label: 'Fail!', icon: "st.switches.switch.off" + state "default", label: 'Pass!', icon: "st.switches.switch.on" + } + + // utility tiles to fill the spaces + standardTile("empty2x2", "null", width: 2, height: 2, decoration: "flat") { + state "default", label:'' + } + standardTile("empty4x2", "null", width: 4, height: 2, decoration: "flat") { + state "default", label:'' + } + + main("standard1") + details([ + "actionRings", "actionFlat", "noActionFlat", + + "flatLabel", "flatIconLabel", "flatIcon", + + "flatDefaultState", "flatImplicitDefaultState1", "flatImplicitDefaultState2", + ]) + } +} + +def installed() { + sendEvent(name: "switch", value: "off") +} + +def parse(String description) { +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") +} diff --git a/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy b/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy new file mode 100644 index 0000000..cc79f11 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy @@ -0,0 +1,96 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "valueDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Sensor" + } + + tiles(scale: 2) { + valueTile("text", "device.text", width: 2, height: 2) { + state "default", label:'${currentValue}' + } + + valueTile("longText", "device.longText", width: 2, height: 2) { + state "default", label:'${currentValue}' + } + + valueTile("integer", "device.integer", width: 2, height: 2) { + state "default", label:'${currentValue}' + } + + valueTile("integerFloat", "device.integerFloat", width: 2, height: 2) { + state "default", label:'${currentValue}' + } + + valueTile("pi", "device.pi", width: 2, height: 2) { + state "default", label:'${currentValue}' + } + + valueTile("floatAsText", "device.floatAsText", width: 2, height: 2) { + state "default", label:'${currentValue}' + } + + valueTile("bgColor", "device.integer", width: 2, height: 2) { + state "default", label:'${currentValue}', backgroundColor: "#e86d13" + } + + valueTile("bgColorRange", "device.integer", width: 2, height: 2) { + state "default", label:'${currentValue}', backgroundColors: [ + [value: 10, color: "#ff0000"], + [value: 90, color: "#0000ff"] + ] + } + + valueTile("bgColorRangeSingleItem", "device.integer", width: 2, height: 2) { + state "default", label:'${currentValue}', backgroundColors: [ + [value: 10, color: "#333333"] + ] + } + + valueTile("bgColorRangeConflict", "device.integer", width: 2, height: 2) { + state "default", label:'${currentValue}', backgroundColors: [ + [value: 10, color: "#990000"], + [value: 10, color: "#000099"] + ] + } + + valueTile("noValue", "device.nada", width: 2, height: 2) { + state "default", label:'${currentValue}' + } + + main("text") + details([ + "text", "longText", "integer", + "integerFloat", "pi", "floatAsText", + "bgColor", "bgColorRange", "bgColorRangeSingleItem", + "bgColorRangeConflict", "noValue" + ]) + } +} + +def installed() { + sendEvent(name: "text", value: "Test") + sendEvent(name: "longText", value: "The Longer The Text, The Better The Test") + sendEvent(name: "integer", value: 47) + sendEvent(name: "integerFloat", value: 47.0) + sendEvent(name: "pi", value: 3.14159) + sendEvent(name: "floatAsText", value: "3.14159") +} + +def parse(String description) { +} diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy new file mode 100644 index 0000000..4a63c6d --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy @@ -0,0 +1,118 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "genericDeviceTile", namespace: "smartthings/tile-ux", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Switch Level" + + command "levelUp" + command "levelDown" + command "randomizeLevel" + } + + tiles(scale: 2) { + multiAttributeTile(name:"basicTile", type:"generic", width:6, height:4) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + } + multiAttributeTile(name:"sliderTile", type:"generic", width:6, height:4) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute("device.level", key: "SECONDARY_CONTROL") { + attributeState "default", icon: 'st.Weather.weather1', action:"randomizeLevel" + } + tileAttribute("device.level", key: "SLIDER_CONTROL") { + attributeState "default", action:"switch level.setLevel" + } + } + multiAttributeTile(name:"valueTile", type:"generic", width:6, height:4) { + tileAttribute("device.level", key: "PRIMARY_CONTROL") { + attributeState "default", label:'${currentValue}', backgroundColors:[ + [value: 0, color: "#ff0000"], + [value: 20, color: "#ffff00"], + [value: 40, color: "#00ff00"], + [value: 60, color: "#00ffff"], + [value: 80, color: "#0000ff"], + [value: 100, color: "#ff00ff"] + ] + } + tileAttribute("device.switch", key: "SECONDARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'…', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'…', action:"switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute("device.level", key: "VALUE_CONTROL") { + attributeState "VALUE_UP", action: "levelUp" + attributeState "VALUE_DOWN", action: "levelDown" + } + } + + main(["basicTile"]) + details(["basicTile", "sliderTile", "valueTile"]) + } +} + +def installed() { + +} + +def parse() { + // This is a simulated device. No incoming data to parse. +} + +def on() { + log.debug "turningOn" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "turningOff" + sendEvent(name: "switch", value: "off") +} + +def setLevel(percent) { + log.debug "setLevel: ${percent}, this" + sendEvent(name: "level", value: percent) +} + +def randomizeLevel() { + def level = Math.round(Math.random() * 100) + setLevel(level) +} + +def levelUp() { + def level = device.latestValue("level") as Integer ?: 0 + if (level < 100) { + level = level + 1 + } + setLevel(level) +} + +def levelDown() { + def level = device.latestValue("level") as Integer ?: 0 + if (level > 0) { + level = level - 1 + } + setLevel(level) +} diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy new file mode 100644 index 0000000..6ebbb3e --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy @@ -0,0 +1,211 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "lightingDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Power Meter" + capability "Switch" + capability "Refresh" + capability "Sensor" + + command "setAdjustedColor" + command "reset" + command "refresh" + } + + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'Power level: ${currentValue}W', icon: "st.Appliances.appliances17" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } + + multiAttributeTile(name:"switchNoPower", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } + + multiAttributeTile(name:"switchNoSlider", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'The power level is currently: ${currentValue}W', icon: "st.Appliances.appliances17" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } + + multiAttributeTile(name:"switchNoSliderOrColor", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'The light is currently consuming this amount of power: ${currentValue}W', icon: "st.Appliances.appliances17" + } + } + + valueTile("color", "device.color", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "color", label: '${currentValue}' + } + + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["switch", "switchNoPower", "switchNoSlider", "switchNoSliderOrColor", "color", "refresh", "reset"]) + } +} + +// 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 +def on() { + //log.trace parent.on(this) + sendEvent(name: "switch", value: "on") +} + +def off() { + //log.trace parent.off(this) + sendEvent(name: "switch", value: "off") +} + +def 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) +} + +def setLevel(percent) { + log.debug "setLevel: ${percent}, this" + sendEvent(name: "level", value: percent) + def power = Math.round(percent / 1.175) * 0.1 + sendEvent(name: "power", value: power) +} + +def setSaturation(percent) { + log.debug "setSaturation: ${percent}, $this" + sendEvent(name: "saturation", value: percent) +} + +def setHue(percent) { + log.debug "setHue: ${percent}, $this" + sendEvent(name: "hue", value: percent) +} + +def setColor(value) { + log.debug "setColor: ${value}, $this" + if (value.hue) { sendEvent(name: "hue", value: value.hue)} + if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)} + if (value.hex) { sendEvent(name: "color", value: value.hex)} + if (value.level) { sendEvent(name: "level", value: value.level)} + if (value.switch) { sendEvent(name: "switch", value: value.switch)} +} + +def reset() { + log.debug "Executing 'reset'" + setAdjustedColor([level:100, hex:"#90C638", saturation:56, hue:23]) + //parent.poll() +} + +def 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) + } +} + +def 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 +} + diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy new file mode 100644 index 0000000..b22c3f7 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy @@ -0,0 +1,122 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "mediaPlayerDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Sensor" + capability "Music Player" + } + + tiles(scale: 2) { + multiAttributeTile(name: "mediaMulti", type:"mediaPlayer", width:6, height:4) { + tileAttribute("device.status", key: "PRIMARY_CONTROL") { + attributeState("paused", label:"Paused",) + attributeState("playing", label:"Playing") + attributeState("stopped", label:"Stopped") + } + tileAttribute("device.status", key: "MEDIA_STATUS") { + attributeState("paused", label:"Paused", action:"music Player.play", nextState: "playing") + attributeState("playing", label:"Playing", action:"music Player.pause", nextState: "paused") + attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing") + } + tileAttribute("device.status", key: "PREVIOUS_TRACK") { + attributeState("default", action:"music Player.previousTrack") + } + tileAttribute("device.status", key: "NEXT_TRACK") { + attributeState("default", action:"music Player.nextTrack") + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState("level", action:"music Player.setLevel") + } + tileAttribute ("device.mute", key: "MEDIA_MUTED") { + attributeState("unmuted", action:"music Player.mute", nextState: "muted") + attributeState("muted", action:"music Player.unmute", nextState: "unmuted") + } + tileAttribute("device.trackDescription", key: "MARQUEE") { + attributeState("default", label:"${currentValue}") + } + } + + main "mediaMulti" + details(["mediaMulti"]) + } +} + +def installed() { + state.tracks = [ + "Gangnam Style (강남스타일)\nPSY\nPsy 6 (Six Rules), Part 1", + "Careless Whisper\nWham!\nMake It Big", + "Never Gonna Give You Up\nRick Astley\nWhenever You Need Somebody", + "Shake It Off\nTaylor Swift\n1989", + "Ironic\nAlanis Morissette\nJagged Little Pill", + "Hotline Bling\nDrake\nHotline Bling - Single" + ] + state.currentTrack = 0 + + sendEvent(name: "level", value: 72) + sendEvent(name: "mute", value: "unmuted") + sendEvent(name: "status", value: "stopped") +} + +def parse(description) { + // No parsing will happen with this simulated device. +} + +def play() { + sendEvent(name: "status", value: "playing") + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def pause() { + sendEvent(name: "status", value: "paused") + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def stop() { + sendEvent(name: "status", value: "stopped") +} + +def previousTrack() { + state.currentTrack = state.currentTrack - 1 + if (state.currentTrack < 0) + state.currentTrack = state.tracks.size()-1 + + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def nextTrack() { + state.currentTrack = state.currentTrack + 1 + if (state.currentTrack == state.tracks.size()) + state.currentTrack = 0 + + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def mute() { + sendEvent(name: "mute", value: "muted") +} + +def unmute() { + sendEvent(name: "mute", value: "unmuted") +} + +def setLevel(level) { + sendEvent(name: "level", value: level) +} diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy new file mode 100644 index 0000000..93c7c75 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy @@ -0,0 +1,341 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "thermostatDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Thermostat" + capability "Relative Humidity Measurement" + + command "tempUp" + command "tempDown" + command "heatUp" + command "heatDown" + command "coolUp" + command "coolDown" + command "setTemperature", ["number"] + } + + tiles(scale: 2) { + multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}%', unit:"%") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + attributeState("cooling", backgroundColor:"#269bd2") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + } + multiAttributeTile(name:"thermostatNoHumidity", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + attributeState("cooling", backgroundColor:"#269bd2") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + } + multiAttributeTile(name:"thermostatBasic", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label:'${currentValue}', unit:"dF", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ]) + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") + } + } + + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}', unit:"dF", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"tempDown" + } + standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"tempUp" + } + + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff" + } + standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"heatDown" + } + standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"heatUp" + } + + valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"coolDown" + } + standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"coolUp" + } + + standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff" + state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e" + state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2" + state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821" + } + standardTile("fanMode", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff" + state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff" + state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff" + } + standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2) { + state "idle", label:'${name}', backgroundColor:"#ffffff" + state "heating", label:'${name}', backgroundColor:"#ffa81e" + state "cooling", label:'${name}', backgroundColor:"#269bd2" + } + + + main("thermostatFull") + details([ + "thermostatFull", "thermostatNoHumidity", "thermostatBasic" + "temperature","tempDown","tempUp", + "mode", "fanMode", "operatingState", + "heatingSetpoint", "heatDown", "heatUp", + "coolingSetpoint", "coolDown", "coolUp" + ]) + } +} + +def installed() { + sendEvent(name: "temperature", value: 72, unit: "F") + sendEvent(name: "heatingSetpoint", value: 70, unit: "F") + sendEvent(name: "thermostatSetpoint", value: 70, unit: "F") + sendEvent(name: "coolingSetpoint", value: 76, unit: "F") + sendEvent(name: "thermostatMode", value: "off") + sendEvent(name: "thermostatFanMode", value: "fanAuto") + sendEvent(name: "thermostatOperatingState", value: "idle") + sendEvent(name: "humidity", value: 53, unit: "%") +} + +def parse(String description) { +} + +def evaluate(temp, heatingSetpoint, coolingSetpoint) { + log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint" + def threshold = 1.0 + def current = device.currentValue("thermostatOperatingState") + def mode = device.currentValue("thermostatMode") + + def heating = false + def cooling = false + def idle = false + if (mode in ["heat","emergency heat","auto"]) { + if (heatingSetpoint - temp >= threshold) { + heating = true + sendEvent(name: "thermostatOperatingState", value: "heating") + } + else if (temp - heatingSetpoint >= threshold) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (mode in ["cool","auto"]) { + if (temp - coolingSetpoint >= threshold) { + cooling = true + sendEvent(name: "thermostatOperatingState", value: "cooling") + } + else if (coolingSetpoint - temp >= threshold && !heating) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: coolingSetpoint) + } + else { + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + + if (mode == "off") { + idle = true + } + + if (idle && !heating && !cooling) { + sendEvent(name: "thermostatOperatingState", value: "idle") + } +} + +def setHeatingSetpoint(Double degreesF) { + log.debug "setHeatingSetpoint($degreesF)" + sendEvent(name: "heatingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint")) +} + +def setCoolingSetpoint(Double degreesF) { + log.debug "setCoolingSetpoint($degreesF)" + sendEvent(name: "coolingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF) +} + +def setThermostatMode(String value) { + sendEvent(name: "thermostatMode", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setThermostatFanMode(String value) { + sendEvent(name: "thermostatFanMode", value: value) +} + +def off() { + sendEvent(name: "thermostatMode", value: "off") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heat() { + sendEvent(name: "thermostatMode", value: "heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def auto() { + sendEvent(name: "thermostatMode", value: "auto") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def emergencyHeat() { + sendEvent(name: "thermostatMode", value: "emergency heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def cool() { + sendEvent(name: "thermostatMode", value: "cool") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def fanOn() { + sendEvent(name: "thermostatFanMode", value: "fanOn") +} + +def fanAuto() { + sendEvent(name: "thermostatFanMode", value: "fanAuto") +} + +def fanCirculate() { + sendEvent(name: "thermostatFanMode", value: "fanCirculate") +} + +def poll() { + null +} + +def tempUp() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue + 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def tempDown() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue - 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setTemperature(value) { + def ts = device.currentState("temperature") + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heatUp() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue + 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + +def heatDown() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue - 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + + +def coolUp() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue + 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} + +def coolDown() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue - 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy new file mode 100644 index 0000000..792b458 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy @@ -0,0 +1,169 @@ +/** + * Copyright 2016 SmartThings, Inc. + * + * 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: "videoPlayerDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Configuration" + capability "Video Camera" + capability "Video Capture" + capability "Refresh" + capability "Switch" + + // custom commands + command "start" + command "stop" + command "setProfileHD" + command "setProfileSDH" + command "setProfileSDL" + } + + tiles(scale: 2) { + multiAttributeTile(name: "videoPlayer", type: "videoPlayer", width: 6, height: 4) { + tileAttribute("device.switch", key: "CAMERA_STATUS") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#79b821", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#F22000") + } + + tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") { + attributeState("errorMessage", label: "", value: "", defaultState: true) + } + + tileAttribute("device.camera", key: "PRIMARY_CONTROL") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#79b821", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#F22000") + } + + tileAttribute("device.startLive", key: "START_LIVE") { + attributeState("live", action: "start", defaultState: true) + } + + tileAttribute("device.stream", key: "STREAM_URL") { + attributeState("activeURL", defaultState: true) + } + + tileAttribute("device.profile", key: "STREAM_QUALITY") { + attributeState("1", label: "720p", action: "setProfileHD", defaultState: true) + attributeState("2", label: "h360p", action: "setProfileSDH", defaultState: true) + attributeState("3", label: "l360p", action: "setProfileSDL", defaultState: true) + } + + tileAttribute("device.betaLogo", key: "BETA_LOGO") { + attributeState("betaLogo", label: "", value: "", defaultState: true) + } + } + + multiAttributeTile(name: "videoPlayerMin", type: "videoPlayer", width: 6, height: 4) { + tileAttribute("device.switch", key: "CAMERA_STATUS") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#79b821", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#F22000") + } + + tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") { + attributeState("errorMessage", label: "", value: "", defaultState: true) + } + + tileAttribute("device.camera", key: "PRIMARY_CONTROL") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#79b821", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#F22000") + } + + tileAttribute("device.startLive", key: "START_LIVE") { + attributeState("live", action: "start", defaultState: true) + } + + tileAttribute("device.stream", key: "STREAM_URL") { + attributeState("activeURL", defaultState: true) + } + } + + main("videoPlayer") + details([ + "videoPlayer", "videoPlayerMin" + ]) + } +} + +def installed() { +} + +def parse(String description) { +} + +def refresh() { + log.trace "refresh()" + // no-op +} + +def on() { + log.trace "on()" + // no-op +} + +def off() { + log.trace "off()" + // no-op +} + +def setProfile(profile) { + log.trace "setProfile(): ${profile}" + sendEvent(name: "profile", value: profile, displayed: false) +} + +def setProfileHD() { + setProfile(1) +} + +def setProfileSDH() { + setProfile(2) +} + +def setProfileSDL() { + setProfile(3) +} + +def start() { + log.trace "start()" + def dataLiveVideo = [ + OutHomeURL : "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", + InHomeURL : "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", + ThumbnailURL: "http://cdn.device-icons.smartthings.com/camera/dlink-indoor@2x.png", + cookie : [key: "key", value: "value"] + ] + + def event = [ + name : "stream", + value : groovy.json.JsonOutput.toJson(dataLiveVideo).toString(), + data : groovy.json.JsonOutput.toJson(dataLiveVideo), + descriptionText: "Starting the livestream", + eventType : "VIDEO", + displayed : false, + isStateChange : true + ] + sendEvent(event) +} + +def stop() { + log.trace "stop()" +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy new file mode 100644 index 0000000..68e8cee --- /dev/null +++ b/devicetypes/smartthings/zigbee-button.src/zigbee-button.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: "ZigBee Button", namespace: "smartthings", 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 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 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 } diff --git a/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy b/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy new file mode 100644 index 0000000..fbc7195 --- /dev/null +++ b/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy @@ -0,0 +1,107 @@ +/** + * Device Tile Controller + * + * Copyright 2016 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Device Tile Controller", + namespace: "smartthings/tile-ux", + author: "SmartThings", + description: "A controller SmartApp to install virtual devices into your location in order to simulate various native Device Tiles.", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + singleInstance: true) + + +preferences { + // landing page + page(name: "defaultPage") +} + +def defaultPage() { + dynamicPage(name: "defaultPage", install: true, uninstall: true) { + section { + paragraph "Select on Unselect the devices that you want to install" + } + section(title: "Multi Attribute Tile Types") { + input(type: "bool", name: "genericDeviceTile", title: "generic", description: "A device that showcases the various use of generic multi-attribute-tiles.", defaultValue: "false") + input(type: "bool", name: "lightingDeviceTile", title: "lighting", description: "A device that showcases the various use of lighting multi-attribute-tiles.", defaultValue: "false") + input(type: "bool", name: "thermostatDeviceTile", title: "thermostat", description: "A device that showcases the various use of thermostat multi-attribute-tiles.", defaultValue: "true") + input(type: "bool", name: "mediaPlayerDeviceTile", title: "media player", description: "A device that showcases the various use of mediaPlayer multi-attribute-tiles.", defaultValue: "false") + input(type: "bool", name: "videoPlayerDeviceTile", title: "video player", description: "A device that showcases the various use of videoPlayer multi-attribute-tiles.", defaultValue: "false") + } + section(title: "Device Tile Types") { + input(type: "bool", name: "standardDeviceTile", title: "standard device tiles", description: "A device that showcases the various use of standard device tiles.", defaultValue: "false") + input(type: "bool", name: "valueDeviceTile", title: "value device tiles", description: "A device that showcases the various use of value device tiles.", defaultValue: "false") + input(type: "bool", name: "presenceDeviceTile", title: "presence device tiles", description: "A device that showcases the various use of color control device tile.", defaultValue: "false") + } + section(title: "Other Tile Types") { + input(type: "bool", name: "carouselDeviceTile", title: "image carousel", description: "A device that showcases the various use of carousel device tile.", defaultValue: "false") + input(type: "bool", name: "sliderDeviceTile", title: "slider", description: "A device that showcases the various use of slider device tile.", defaultValue: "false") + input(type: "bool", name: "colorWheelDeviceTile", title: "color wheel", description: "A device that showcases the various use of color wheel device tile.", defaultValue: "false") + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" +} + +def uninstalled() { + getChildDevices().each { + deleteChildDevice(it.deviceNetworkId) + } +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initializeDevices() +} + +def initializeDevices() { + settings.each { key, value -> + log.debug "$key : $value" + def existingDevice = getChildDevices().find { it.name == key } + log.debug "$existingDevice" + if (existingDevice && !value) { + deleteChildDevice(existingDevice.deviceNetworkId) + } else if (!existingDevice && value) { + String dni = UUID.randomUUID() + log.debug "$dni" + addChildDevice(app.namespace, key, dni, null, [ + label: labelMap()[key] ?: key, + completedSetup: true + ]) + } + } +} + +// Map the name of the Device to a proper Label +def labelMap() { + [ + genericDeviceTile: "Tile Multiattribute Generic", + lightingDeviceTile: "Tile Multiattribute Lighting", + thermostatDeviceTile: "Tile Multiattribute Thermostat", + mediaPlayerDeviceTile: "Tile Multiattribute Media Player", + videoPlayerDeviceTile: "Tile Multiattribute Video Player", + standardDeviceTile: "Tile Device Standard", + valueDeviceTile: "Tile Device Value", + presenceDeviceTile: "Tile Device Presence", + carouselDeviceTile: "Tile Device Carousel", + sliderDeviceTile: "Tile Device Slider", + colorWheelDeviceTile: "Tile Device Color Wheel" + ] +}