From bbdf9ff02a21d2fff559d63bd055b40caedb1101 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Tue, 11 Oct 2016 17:12:40 -0700 Subject: [PATCH 01/11] DVCSMP-1447 Support for RGB ZigBee DTH --- .../osram-lightify-gardenspot-mini-rgb.groovy | 3 - .../zigbee-rgb-bulb.groovy | 154 ++++++++++++++++++ .../zigbee-rgbw-bulb.groovy | 4 +- .../zll-rgb-bulb.src/zll-rgb-bulb.groovy | 134 +++++++++++++++ .../zll-rgbw-bulb.src/zll-rgbw-bulb.groovy | 6 +- 5 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy create mode 100644 devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy diff --git a/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy b/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy index 2dc1bf6..a5c98ac 100644 --- a/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy +++ b/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy @@ -21,9 +21,6 @@ metadata { attribute "colorName", "string" command "setAdjustedColor" - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB" } // simulator metadata diff --git a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy new file mode 100644 index 0000000..068d4ec --- /dev/null +++ b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy @@ -0,0 +1,154 @@ +/** + * 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. + * + * Author: SmartThings + * Date: 2016-01-19 + * + * This DTH should serve as the generic DTH to handle RGB ZigBee HA devices (For color bulbs with no color temperature) + */ + +metadata { + definition (name: "ZigBee RGB Bulb", namespace: "smartthings", author: "SmartThings") { + + capability "Actuator" + capability "Color Control" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB" + } + + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.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:"color control.setColor" + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["switch", "refresh"]) + } +} + +//Globals +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +private getHUE_COMMAND() { 0x00 } +private getSATURATION_COMMAND() { 0x03 } +private getCOLOR_CONTROL_CLUSTER() { 0x0300 } + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def event = zigbee.getEvent(description) + if (event) { + log.debug event + if (event.name=="level" && event.value==0) {} + else { + sendEvent(event) + } + } + else { + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + def cluster = zigbee.parse(description) + + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { + if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed") + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false) + } + } + else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { + if (cluster.data[0] == 0x00){ + log.debug "ON/OFF REPORTING CONFIG RESPONSE: $cluster" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else { + log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" + } + } + else { + log.info "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbeeMap + } + } +} + +def on() { + zigbee.on() +} + +def off() { + zigbee.off() +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} + +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + // Device-Watch allows 3 check-in misses from device (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +} + +def setLevel(value) { + zigbee.setLevel(value) +} + +def setColor(value){ + log.trace "setColor($value)" + zigbee.on() + setHue(value.hue) + "delay 500" + setSaturation(value.saturation) +} + +def setHue(value) { + def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) +} + +def setSaturation(value) { + def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy index ab1f76a..5c629bb 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -138,7 +138,7 @@ def ping() { } def refresh() { - zigbee.onOffRefresh() + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) } def configure() { @@ -148,7 +148,7 @@ def configure() { sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } def setColorTemperature(value) { diff --git a/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy new file mode 100644 index 0000000..5988334 --- /dev/null +++ b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy @@ -0,0 +1,134 @@ +/** + * 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. + * + */ + +metadata { + definition (name: "ZLL RGB Bulb", namespace: "smartthings", author: "SmartThings") { + + capability "Actuator" + capability "Color Control" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Switch" + capability "Switch Level" + } + + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.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:"color control.setColor" + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["switch", "refresh"]) + } +} + +//Globals +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +private getHUE_COMMAND() { 0x00 } +private getSATURATION_COMMAND() { 0x03 } +private getCOLOR_CONTROL_CLUSTER() { 0x0300 } + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def finalResult = zigbee.getEvent(description) + if (finalResult) { + log.debug finalResult + sendEvent(finalResult) + } + else { + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + log.trace "zigbeeMap : $zigbeeMap" + + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { + if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360) + sendEvent(name: "hue", value: hueValue, displayed:false) + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "saturation", value: saturationValue, displayed:false) + } + } + else { + log.info "DID NOT PARSE MESSAGE for description : $description" + } + } +} + +def on() { + zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() +} + +def off() { + zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() +} + +def refresh() { + refreshAttributes() + configureAttributes() +} + +def poll() { + refreshAttributes() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + configureAttributes() + refreshAttributes() +} + +def configureAttributes() { + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) +} + +def refreshAttributes() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +} + +def setLevel(value) { + zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report +} + +def setColor(value){ + log.trace "setColor($value)" + zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes() +} + +def setHue(value) { + def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) +} + +def setSaturation(value) { + def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time +} diff --git a/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy index 9d9be84..e02d48d 100644 --- a/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy @@ -123,7 +123,7 @@ def configureAttributes() { } def refreshAttributes() { - zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } def setColorTemperature(value) { @@ -141,10 +141,10 @@ def setColor(value){ def setHue(value) { def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) - zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) } def setSaturation(value) { def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) - zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time } From 409658e899e543752b2932d0504ba02c0f66f9cd Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Wed, 12 Oct 2016 09:23:19 -0500 Subject: [PATCH 02/11] Call refresh before configure With the changes in https://smartthings.atlassian.net/browse/DPROT-168 it can take a little longer for the configuration process for a device to complete. As a result sensor values may not be available immediately by calling the refresh commands before the configure commands we can populate those values sooner. This resolves: https://smartthings.atlassian.net/browse/DPROT-177 --- .../smartpower-dimming-outlet.groovy | 2 +- .../smartpower-outlet.src/smartpower-outlet.groovy | 2 +- .../smartsense-moisture-sensor.groovy | 9 +-------- .../smartsense-motion-sensor.groovy | 9 +-------- .../smartsense-motion-temp-sensor.groovy | 6 +----- .../smartsense-multi-sensor.groovy | 5 ++--- .../smartsense-open-closed-accelerometer-sensor.groovy | 6 +----- .../smartsense-open-closed-sensor.groovy | 7 +------ .../smartsense-temp-humidity-sensor.groovy | 2 +- .../zigbee-dimmer-power.src/zigbee-dimmer-power.groovy | 2 +- .../smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy | 2 +- .../smartthings/zigbee-lock.src/zigbee-lock.groovy | 2 +- .../zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy | 2 +- .../zigbee-switch-power.src/zigbee-switch-power.groovy | 2 +- .../smartthings/zigbee-switch.src/zigbee-switch.groovy | 2 +- .../smartthings/zigbee-valve.src/zigbee-valve.groovy | 7 +------ .../zigbee-white-color-temperature-bulb.groovy | 2 +- 17 files changed, 18 insertions(+), 51 deletions(-) diff --git a/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy index 51bde7b..27e9734 100644 --- a/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy +++ b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy @@ -133,7 +133,7 @@ def refresh() { } def configure() { - onOffConfig() + levelConfig() + powerConfig() + refresh() + refresh() + onOffConfig() + levelConfig() + powerConfig() } diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index ec7cdad..685e041 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -146,7 +146,7 @@ def configure() { sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - zigbee.onOffConfig(0, 300) + powerConfig() + refresh() + refresh() + zigbee.onOffConfig(0, 300) + powerConfig() } //power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy index 22a8a61..f214cbd 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -308,16 +308,9 @@ def configure() { // enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - log.debug "Configuring Reporting, IAS CIE, and Bindings." - def enrollCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - ] - // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config + return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config } def enrollResponse() { diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy index d885613..d02d41d 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -323,16 +323,9 @@ def configure() { // enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - log.debug "Configuring Reporting, IAS CIE, and Bindings." - - def enrollCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - ] // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config + return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config } def enrollResponse() { diff --git a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy index dd0d801..6146673 100644 --- a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy @@ -255,13 +255,9 @@ def refresh() { } def configure() { - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) log.debug "Configuring Reporting, IAS CIE, and Bindings." def configCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", @@ -270,7 +266,7 @@ def configure() { "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" ] - return configCmds + refresh() // send refresh cmds as part of config + return refresh() + configCmds // send refresh cmds as part of config } def enrollResponse() { diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 9240ec5..94104b9 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -420,15 +420,14 @@ def configure() { // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - def configCmds = enrollResponse() + - zigbee.batteryConfig() + + def configCmds = zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) + zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) - return configCmds + refresh() + return refresh() + configCmds } private getEndpointId() { diff --git a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy index a5b0b6f..9af4f2d 100644 --- a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy @@ -277,12 +277,8 @@ def getTemperature(value) { def configure() { sendEvent(name: "checkInterval", value: 7200, displayed: false) - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) log.debug "Configuring Reporting, IAS CIE, and Bindings." def configCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", @@ -295,7 +291,7 @@ def configure() { "zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" ] - return configCmds + refresh() // send refresh cmds as part of config + return refresh() + configCmds // send refresh cmds as part of config } def enrollResponse() { diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy index bda54e6..7d411f5 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -270,16 +270,11 @@ def configure() { // enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) log.debug "Configuring Reporting, IAS CIE, and Bindings." - def enrollCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - ] // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config + return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config } def enrollResponse() { diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index ea94edb..3429788 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -292,7 +292,7 @@ def configure() { // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config + return refresh() + humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config } private hex(value) { diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index af8c227..6ffdf22 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -93,5 +93,5 @@ def refresh() { def configure() { log.debug "Configuring Reporting and Bindings." - zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + refresh() } diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index b0239e6..79c460f 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -107,5 +107,5 @@ def configure() { sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() } diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index b4edfd1..41de4e8 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -90,7 +90,7 @@ def configure() { zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, TYPE_U8, 600, 21600, 0x01) log.info "configure() --- cmds: $cmds" - return cmds + refresh() // send refresh cmds as part of config + return refresh() + cmds // send refresh cmds as part of config } def refresh() { diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy index 5c629bb..1e8102b 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -148,7 +148,7 @@ def configure() { sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + refresh() } def setColorTemperature(value) { diff --git a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy index a5eb0ac..bb5f754 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -82,5 +82,5 @@ def refresh() { def configure() { log.debug "Configuring Reporting and Bindings." - zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + refresh() } diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy index 7e243f4..fb1685b 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -78,5 +78,5 @@ def refresh() { def configure() { log.debug "Configuring Reporting and Bindings." - zigbee.onOffConfig() + zigbee.onOffRefresh() + zigbee.onOffRefresh() + zigbee.onOffConfig() } diff --git a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy index 3d0e1e4..4b66fc6 100644 --- a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy +++ b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy @@ -134,10 +134,5 @@ def refresh() { def configure() { log.debug "Configuring Reporting and Bindings." - zigbee.onOffConfig() + - zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, TYPE_U8, 600, 21600, 1) + - zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, TYPE_ENUM8, 5, 21600, 1) + - zigbee.onOffRefresh() + - zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) + - zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + refresh() } diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index 8a210f2..9283628 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -131,7 +131,7 @@ def configure() { sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + refresh() } def setColorTemperature(value) { From 50696902cfbbb8b1d259ce934a6419c1f123a10f Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Thu, 13 Oct 2016 10:59:18 -0500 Subject: [PATCH 03/11] Handle both mfgIDs for temp-humidity sensors With upcoming OTA support for ZigBee end devices centralite devices change manufacturer IDs between versions. As a result when reading a manufacturer specific value, we have to handle the possibility of either mfgID being correct. The simplest solution (and the one employed here) was to simply read the attribute using both mfgIDs and ignoring null responses. This resolves: https://smartthings.atlassian.net/browse/DPROT-183 --- .../smartsense-temp-humidity-sensor.groovy | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index ea94edb..cadac3d 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -250,11 +250,7 @@ private Map getTemperatureResult(value) { private Map getHumidityResult(value) { log.debug 'Humidity' - return [ - name: 'humidity', - value: value, - unit: '%' - ] + return value ? [name: 'humidity', value: value, unit: '%'] : [:] } /** @@ -267,15 +263,10 @@ def ping() { def refresh() { log.debug "refresh temperature, humidity, and battery" - [ - - "zcl mfg-code 0xC2DF", "delay 1000", - "zcl global read 0xFC45 0", "delay 1000", - "send 0x${device.deviceNetworkId} 1 1", "delay 1000", - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20" - - ] + return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware + zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0x104E]) + // New firmware + zigbee.readAttribute(0x0402, 0x0000) + + zigbee.readAttribute(0x0001, 0x0020) } def configure() { From b12df3f360d103483ba227dc62d9310f8b15054d Mon Sep 17 00:00:00 2001 From: James Chen Date: Tue, 11 Oct 2016 15:20:53 -0700 Subject: [PATCH 04/11] Convert setpoints in case of a temperature scale change. Removed useage of convertTemperatureIfNeeded which is an undocumented API fixed spacing On temperature change it will update all setpoints in alterSetpoint function fixed spacing issues spacing changes removed comments --- .../ecobee-thermostat.groovy | 132 ++++++++++-------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 37cbb85..adf6433 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -148,14 +148,12 @@ def generateEvent(Map results) { handlerName: name] if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) { - def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F - sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger() isChange = isTemperatureStateChange(device, name, value.toString()) isDisplayed = isChange event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed] } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") { - def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F - sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger() event << [value: sendValue, unit: temperatureScale, displayed: false] } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ isChange = isStateChange(device, name, value.toString()) @@ -253,7 +251,6 @@ void setCoolingSetpoint(setpoint) { def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") - if (coolingSetpoint > maxCoolingSetpoint) { coolingSetpoint = maxCoolingSetpoint } else if (coolingSetpoint < minCoolingSetpoint) { @@ -283,7 +280,6 @@ void setCoolingSetpoint(setpoint) { } void resumeProgram() { - log.debug "resumeProgram() is called" sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false) def deviceId = device.deviceNetworkId.split(/\./).last() @@ -354,7 +350,6 @@ def switchFanMode() { } def switchToFanMode(nextMode) { - log.debug "switching to fan mode: $nextMode" def returnCommand @@ -520,63 +515,56 @@ def fanAuto() { } def generateSetpointEvent() { - log.debug "Generate SetPoint Event" def mode = device.currentValue("thermostatMode") - log.debug "Current Mode = ${mode}" def heatingSetpoint = device.currentValue("heatingSetpoint") - log.debug "Heating Setpoint = ${heatingSetpoint}" - def coolingSetpoint = device.currentValue("coolingSetpoint") - log.debug "Cooling Setpoint = ${coolingSetpoint}" - def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") - if(location.temperatureScale == "C") - { - maxHeatingSetpoint = roundC(maxHeatingSetpoint) - maxCoolingSetpoint = roundC(maxCoolingSetpoint) - minHeatingSetpoint = roundC(minHeatingSetpoint) - minCoolingSetpoint = roundC(minCoolingSetpoint) - heatingSetpoint = roundC(heatingSetpoint) - coolingSetpoint = roundC(coolingSetpoint) + if(location.temperatureScale == "C") { + maxHeatingSetpoint = maxHeatingSetpoint > 40 ? roundC(convertFtoC(maxHeatingSetpoint)) : roundC(maxHeatingSetpoint) + maxCoolingSetpoint = maxCoolingSetpoint > 40 ? roundC(convertFtoC(maxCoolingSetpoint)) : roundC(maxCoolingSetpoint) + minHeatingSetpoint = minHeatingSetpoint > 40 ? roundC(convertFtoC(minHeatingSetpoint)) : roundC(minHeatingSetpoint) + minCoolingSetpoint = minCoolingSetpoint > 40 ? roundC(convertFtoC(minCoolingSetpoint)) : roundC(minCoolingSetpoint) + heatingSetpoint = heatingSetpoint > 40 ? roundC(convertFtoC(heatingSetpoint)) : roundC(heatingSetpoint) + coolingSetpoint = coolingSetpoint > 40 ? roundC(convertFtoC(coolingSetpoint)) : roundC(coolingSetpoint) + } else { + maxHeatingSetpoint = maxHeatingSetpoint < 40 ? roundC(convertCtoF(maxHeatingSetpoint)) : maxHeatingSetpoint + maxCoolingSetpoint = maxCoolingSetpoint < 40 ? roundC(convertCtoF(maxCoolingSetpoint)) : maxCoolingSetpoint + minHeatingSetpoint = minHeatingSetpoint < 40 ? roundC(convertCtoF(minHeatingSetpoint)) : minHeatingSetpoint + minCoolingSetpoint = minCoolingSetpoint < 40 ? roundC(convertCtoF(minCoolingSetpoint)) : minCoolingSetpoint + heatingSetpoint = heatingSetpoint < 40 ? roundC(convertCtoF(heatingSetpoint)) : heatingSetpoint + coolingSetpoint = coolingSetpoint < 40 ? roundC(convertCtoF(coolingSetpoint)) : coolingSetpoint } + log.debug "Current Mode = ${mode}" + log.debug "Heating Setpoint = ${heatingSetpoint}" + log.debug "Cooling Setpoint = ${coolingSetpoint}" sendEvent("name":"maxHeatingSetpoint", "value":maxHeatingSetpoint, "unit":location.temperatureScale) sendEvent("name":"maxCoolingSetpoint", "value":maxCoolingSetpoint, "unit":location.temperatureScale) sendEvent("name":"minHeatingSetpoint", "value":minHeatingSetpoint, "unit":location.temperatureScale) sendEvent("name":"minCoolingSetpoint", "value":minCoolingSetpoint, "unit":location.temperatureScale) - + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) if (mode == "heat") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) - } else if (mode == "cool") { - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) - } else if (mode == "auto") { - sendEvent("name":"thermostatSetpoint", "value":"Auto") - } else if (mode == "off") { - sendEvent("name":"thermostatSetpoint", "value":"Off") - } else if (mode == "auxHeatOnly") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) - } - } void raiseSetpoint() { @@ -585,21 +573,31 @@ void raiseSetpoint() { def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") - if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow raiseSetpoint" } else { + def heatingSetpoint = device.currentValue("heatingSetpoint") def coolingSetpoint = device.currentValue("coolingSetpoint") def thermostatSetpoint = device.currentValue("thermostatSetpoint") + + if (location.temperatureScale == "C") { + maxHeatingSetpoint = maxHeatingSetpoint > 40 ? convertFtoC(maxHeatingSetpoint) : maxHeatingSetpoint + maxCoolingSetpoint = maxCoolingSetpoint > 40 ? convertFtoC(maxCoolingSetpoint) : maxCoolingSetpoint + heatingSetpoint = heatingSetpoint > 40 ? convertFtoC(heatingSetpoint) : heatingSetpoint + coolingSetpoint = coolingSetpoint > 40 ? convertFtoC(coolingSetpoint) : coolingSetpoint + thermostatSetpoint = thermostatSetpoint > 40 ? convertFtoC(thermostatSetpoint) : thermostatSetpoint + } else { + maxHeatingSetpoint = maxHeatingSetpoint < 40 ? convertCtoF(maxHeatingSetpoint) : maxHeatingSetpoint + maxCoolingSetpoint = maxCoolingSetpoint < 40 ? convertCtoF(maxCoolingSetpoint) : maxCoolingSetpoint + heatingSetpoint = heatingSetpoint < 40 ? convertCtoF(heatingSetpoint) : heatingSetpoint + coolingSetpoint = coolingSetpoint < 40 ? convertCtoF(coolingSetpoint) : coolingSetpoint + thermostatSetpoint = thermostatSetpoint < 40 ? convertCtoF(thermostatSetpoint) : thermostatSetpoint + } + log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" - if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value - targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() - } else { - targetvalue = 0 - } + targetvalue = thermostatSetpoint ? thermostatSetpoint : 0 targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5 if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue > maxHeatingSetpoint) { @@ -622,20 +620,29 @@ void lowerSetpoint() { def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") - if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow lowerSetpoint" } else { def heatingSetpoint = device.currentValue("heatingSetpoint") def coolingSetpoint = device.currentValue("coolingSetpoint") def thermostatSetpoint = device.currentValue("thermostatSetpoint") - log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" - if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value - targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() + + if (location.temperatureScale == "C") { + minHeatingSetpoint = minHeatingSetpoint > 40 ? convertFtoC(minHeatingSetpoint) : minHeatingSetpoint + minCoolingSetpoint = minCoolingSetpoint > 40 ? convertFtoC(minCoolingSetpoint) : minCoolingSetpoint + heatingSetpoint = heatingSetpoint > 40 ? convertFtoC(heatingSetpoint) : heatingSetpoint + coolingSetpoint = coolingSetpoint > 40 ? convertFtoC(coolingSetpoint) : coolingSetpoint + thermostatSetpoint = thermostatSetpoint > 40 ? convertFtoC(thermostatSetpoint) : thermostatSetpoint } else { - targetvalue = 0 + minHeatingSetpoint = minHeatingSetpoint < 40 ? convertCtoF(minHeatingSetpoint) : minHeatingSetpoint + minCoolingSetpoint = minCoolingSetpoint < 40 ? convertCtoF(minCoolingSetpoint) : minCoolingSetpoint + heatingSetpoint = heatingSetpoint < 40 ? convertCtoF(heatingSetpoint) : heatingSetpoint + coolingSetpoint = coolingSetpoint < 40 ? convertCtoF(coolingSetpoint) : coolingSetpoint + thermostatSetpoint = thermostatSetpoint < 40 ? convertCtoF(thermostatSetpoint) : thermostatSetpoint } + log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" + + targetvalue = thermostatSetpoint ? thermostatSetpoint : 0 targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5 if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue < minHeatingSetpoint) { @@ -653,7 +660,6 @@ void lowerSetpoint() { //called by raiseSetpoint() and lowerSetpoint() void alterSetpoint(temp) { - def mode = device.currentValue("thermostatMode") if (mode == "off" || mode == "auto") { @@ -666,6 +672,18 @@ void alterSetpoint(temp) { def targetHeatingSetpoint def targetCoolingSetpoint + def temperatureScaleHasChanged = false + + if (location.temperatureScale == "C") { + if ( heatingSetpoint > 40.0 || coolingSetpoint > 40.0 ) { + temperatureScaleHasChanged = true + } + } else { + if ( heatingSetpoint < 40.0 || coolingSetpoint < 40.0 ) { + temperatureScaleHasChanged = true + } + } + //step1: check thermostatMode, enforce limits before sending request to cloud if (mode == "heat" || mode == "auxHeatOnly"){ if (temp.value > coolingSetpoint){ @@ -707,17 +725,18 @@ void alterSetpoint(temp) { sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false) } } + + if ( temperatureScaleHasChanged ) + generateSetpointEvent() generateStatusEvent() } } def generateStatusEvent() { - def mode = device.currentValue("thermostatMode") def heatingSetpoint = device.currentValue("heatingSetpoint") def coolingSetpoint = device.currentValue("coolingSetpoint") def temperature = device.currentValue("temperature") - def statusText log.debug "Generate Status Event for Mode = ${mode}" @@ -727,36 +746,25 @@ def generateStatusEvent() { log.debug "HVAC Mode = ${mode}" if (mode == "heat") { - if (temperature >= heatingSetpoint) statusText = "Right Now: Idle" else statusText = "Heating to ${heatingSetpoint} ${location.temperatureScale}" - } else if (mode == "cool") { - if (temperature <= coolingSetpoint) statusText = "Right Now: Idle" else statusText = "Cooling to ${coolingSetpoint} ${location.temperatureScale}" - } else if (mode == "auto") { - statusText = "Right Now: Auto" - } else if (mode == "off") { - statusText = "Right Now: Off" - } else if (mode == "auxHeatOnly") { - statusText = "Emergency Heat" - } else { - statusText = "?" - } + log.debug "Generate Status Event = ${statusText}" sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true) } @@ -770,7 +778,7 @@ def roundC (tempC) { } def convertFtoC (tempF) { - return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) + return ((Math.round(((tempF - 32)*(5/9)) * 2))/2).toDouble() } def convertCtoF (tempC) { From a5d95fb025f70ad551c278761f280b7a77127e0f Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Thu, 13 Oct 2016 10:19:20 -0500 Subject: [PATCH 05/11] Correctly parse zone status in zigbee-button Previously the DTH didn't handle the extended status from zone status reporting. This moves to use the library for parsing zone status which will handle the extended status properly. This resolves: https://smartthings.atlassian.net/browse/DPROT-180 --- .../smartthings/zigbee-button.src/zigbee-button.groovy | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy index 12e06ea..6bd1858 100644 --- a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy +++ b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy @@ -89,14 +89,8 @@ def parse(String description) { } private Map parseIasButtonMessage(String description) { - int zoneInt = Integer.parseInt((description - "zone status 0x"), 16) - if (zoneInt & 0x02) { - resultMap = getButtonResult('press') - } else { - resultMap = getButtonResult('release') - } - - return resultMap + def zs = zigbee.parseZoneStatus(description) + return zs.isAlarm2Set() ? getButtonResult("press") : getButtonResult("release") } private Map getBatteryResult(rawValue) { From 94f57dd249268bcb6e3ba014a482ac28f2db5f11 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Fri, 14 Oct 2016 16:48:14 -0600 Subject: [PATCH 06/11] DVCSMP-2088 Philips Hue: Add explicit online events --- .../hue-bloom.src/hue-bloom.groovy | 5 +- .../hue-bridge.src/hue-bridge.groovy | 5 +- .../smartthings/hue-bulb.src/hue-bulb.groovy | 5 +- .../hue-lux-bulb.src/hue-lux-bulb.groovy | 5 +- .../hue-white-ambiance-bulb.groovy | 5 +- .../hue-connect.src/hue-connect.groovy | 408 +++++++++--------- 6 files changed, 206 insertions(+), 227 deletions(-) diff --git a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy index e6a02ab..ae48f0d 100644 --- a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy +++ b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy @@ -57,7 +57,7 @@ metadata { } void installed() { - sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}") } // parse events into attributes @@ -172,6 +172,3 @@ def verifyPercent(percent) { } } -def ping() { - log.debug "${parent.ping(this)}" -} diff --git a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy index fee818f..e7fbce1 100644 --- a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy +++ b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy @@ -45,7 +45,7 @@ metadata { } void installed() { - sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan"], displayed: false) + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}") } // parse events into attributes @@ -87,6 +87,3 @@ def parse(description) { results } -def ping() { - log.debug "${parent.ping(this)}" -} diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 4dbae74..143b0b3 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -66,7 +66,7 @@ metadata { } void installed() { - sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}") } // parse events into attributes @@ -188,6 +188,3 @@ def verifyPercent(percent) { } } -def ping() { - log.trace "${parent.ping(this)}" -} 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 158e11a..d855d38 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -50,7 +50,7 @@ metadata { } void installed() { - sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}") } // parse events into attributes @@ -93,6 +93,3 @@ void refresh() { parent.manualRefresh() } -def ping() { - log.debug "${parent.ping(this)}" -} diff --git a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy index 10315a5..718ed57 100644 --- a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy +++ b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy @@ -55,7 +55,7 @@ metadata { } void installed() { - sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}") } // parse events into attributes @@ -107,6 +107,3 @@ void refresh() { parent.manualRefresh() } -def ping() { - log.debug "${parent.ping(this)}" -} diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index d39d451..7ee6e60 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -8,7 +8,7 @@ * 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 + * 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 @@ -24,7 +24,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png", - singleInstance: true + singleInstance: true ) preferences { @@ -110,7 +110,7 @@ def bridgeLinking() { if (state.refreshUsernameNeeded) { paragraphText = "The current Hue username is invalid.\n\nPlease press the button on your Hue Bridge to re-link. " } else { - paragraphText = "Press the button on your Hue Bridge to setup a link. " + paragraphText = "Press the button on your Hue Bridge to setup a link. " } } else { paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next." @@ -198,7 +198,7 @@ void ssdpSubscribe() { private sendDeveloperReq() { def token = app.id - def host = getBridgeIP() + def host = getBridgeIP() sendHubCommand(new physicalgraph.device.HubAction([ method: "POST", path: "/api", @@ -209,7 +209,7 @@ private sendDeveloperReq() { } private discoverHueBulbs() { - def host = getBridgeIP() + def host = getBridgeIP() sendHubCommand(new physicalgraph.device.HubAction([ method: "GET", path: "/api/${state.username}/lights", @@ -231,8 +231,8 @@ private verifyHueBridge(String deviceNetworkId, String host) { private verifyHueBridges() { def devices = getHueBridges().findAll { it?.value?.verified != true } devices.each { - def ip = convertHexToIP(it.value.networkAddress) - def port = convertHexToInt(it.value.deviceAddress) + def ip = convertHexToIP(it.value.networkAddress) + def port = convertHexToInt(it.value.deviceAddress) verifyHueBridge("${it.value.mac}", (ip + ":" + port)) } } @@ -261,7 +261,7 @@ Map bulbsDiscovered() { bulbs.each { def value = "${it.name}" def key = app.id +"/"+ it.id - logg += "$value - $key, " + logg += "$value - $key, " bulbmap["${key}"] = value } } @@ -288,22 +288,22 @@ def installed() { def updated() { log.trace "Updated with settings: ${settings}" unsubscribe() - unschedule() + unschedule() initialize() } def initialize() { log.debug "Initializing" - unsubscribe(bridge) - state.inBulbDiscovery = false - state.bridgeRefreshCount = 0 - state.bulbRefreshCount = 0 + unsubscribe(bridge) + state.inBulbDiscovery = false + state.bridgeRefreshCount = 0 + state.bulbRefreshCount = 0 state.updating = false if (selectedHue) { - addBridge() - addBulbs() - doDeviceSync() - runEvery5Minutes("doDeviceSync") + addBridge() + addBulbs() + doDeviceSync() + runEvery5Minutes("doDeviceSync") } } @@ -369,7 +369,6 @@ def addBulbs() { if (d) { log.debug "created ${d.displayName} with id $dni" d.completedSetup = true - d.refresh() } } else { log.debug "$dni in not longer paired to the Hue Bridge or ID changed" @@ -399,23 +398,23 @@ def addBridge() { if(vbridge) { def d = getChildDevice(selectedHue) if(!d) { - // compatibility with old devices - def newbridge = true - childDevices.each { - if (it.getDeviceDataByName("mac")) { - def newDNI = "${it.getDeviceDataByName("mac")}" - if (newDNI != it.deviceNetworkId) { - def oldDNI = it.deviceNetworkId - log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" - it.setDeviceNetworkId("${newDNI}") + // compatibility with old devices + def newbridge = true + childDevices.each { + if (it.getDeviceDataByName("mac")) { + def newDNI = "${it.getDeviceDataByName("mac")}" + if (newDNI != it.deviceNetworkId) { + def oldDNI = it.deviceNetworkId + log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" + it.setDeviceNetworkId("${newDNI}") if (oldDNI == selectedHue) { app.updateSetting("selectedHue", newDNI) } - newbridge = false - } - } - } - if (newbridge) { + newbridge = false + } + } + } + if (newbridge) { // Hue uses last 6 digits of MAC address as ID number, this number is shown on the bottom of the bridge def idNumber = getBridgeIdNumber(selectedHue) d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub, ["label": "Hue Bridge ($idNumber)"]) @@ -426,9 +425,11 @@ def addBridge() { d.completedSetup = true log.debug "created ${d.displayName} with id ${d.deviceNetworkId}" def childDevice = getChildDevice(d.deviceNetworkId) + childDevice?.sendEvent(name: "status", value: "Online") + childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true) updateBridgeStatus(childDevice) - childDevice.sendEvent(name: "idNumber", value: idNumber) + childDevice?.sendEvent(name: "idNumber", value: idNumber) if (vbridge.value.ip && vbridge.value.port) { if (vbridge.value.ip.contains(".")) { childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port) @@ -580,47 +581,47 @@ void usernameHandler(physicalgraph.device.HubResponse hubResponse) { @Deprecated def locationHandler(evt) { def description = evt.description - log.trace "Location: $description" + log.trace "Location: $description" def hub = evt?.hubId def parsedEvent = parseLanMessage(description) parsedEvent << ["hub":hub] if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1")) { - //SSDP DISCOVERY EVENTS + //SSDP DISCOVERY EVENTS log.trace "SSDP DISCOVERY EVENTS" def bridges = getHueBridges() log.trace bridges.toString() if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) { - //bridge does not exist + //bridge does not exist log.trace "Adding bridge ${parsedEvent.ssdpUSN}" bridges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] } else { // update the values - def ip = convertHexToIP(parsedEvent.networkAddress) - def port = convertHexToInt(parsedEvent.deviceAddress) - def host = ip + ":" + port + def ip = convertHexToIP(parsedEvent.networkAddress) + def port = convertHexToInt(parsedEvent.deviceAddress) + def host = ip + ":" + port log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host." - def dstate = bridges."${parsedEvent.ssdpUSN.toString()}" + def dstate = bridges."${parsedEvent.ssdpUSN.toString()}" def dni = "${parsedEvent.mac}" - def d = getChildDevice(dni) - def networkAddress = null - if (!d) { - childDevices.each { - if (it.getDeviceDataByName("mac")) { - def newDNI = "${it.getDeviceDataByName("mac")}" + def d = getChildDevice(dni) + def networkAddress = null + if (!d) { + childDevices.each { + if (it.getDeviceDataByName("mac")) { + def newDNI = "${it.getDeviceDataByName("mac")}" d = it - if (newDNI != it.deviceNetworkId) { - def oldDNI = it.deviceNetworkId - log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" - it.setDeviceNetworkId("${newDNI}") + if (newDNI != it.deviceNetworkId) { + def oldDNI = it.deviceNetworkId + log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" + it.setDeviceNetworkId("${newDNI}") if (oldDNI == selectedHue) { app.updateSetting("selectedHue", newDNI) } - doDeviceSync() - } - } - } + doDeviceSync() + } + } + } } else { updateBridgeStatus(d) if (d.getDeviceDataByName("networkAddress")) { @@ -628,22 +629,22 @@ def locationHandler(evt) { } else { networkAddress = d.latestState('networkAddress').stringValue } - log.trace "Host: $host - $networkAddress" - if(host != networkAddress) { - log.debug "Device's port or ip changed for device $d..." - dstate.ip = ip - dstate.port = port - dstate.name = "Philips hue ($ip)" - d.sendEvent(name:"networkAddress", value: host) - d.updateDataValue("networkAddress", host) - } - } + log.trace "Host: $host - $networkAddress" + if(host != networkAddress) { + log.debug "Device's port or ip changed for device $d..." + dstate.ip = ip + dstate.port = port + dstate.name = "Philips hue ($ip)" + d.sendEvent(name:"networkAddress", value: host) + d.updateDataValue("networkAddress", host) + } + } } } else if (parsedEvent.headers && parsedEvent.body) { log.trace "HUE BRIDGE RESPONSES" def headerString = parsedEvent.headers.toString() if (headerString?.contains("xml")) { - log.trace "description.xml response (application/xml)" + log.trace "description.xml response (application/xml)" def body = new XmlSlurper().parseText(parsedEvent.body) if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) { def bridges = getHueBridges() @@ -655,7 +656,7 @@ def locationHandler(evt) { } } } else if(headerString?.contains("json") && isValidSource(parsedEvent.mac)) { - log.trace "description.xml response (application/json)" + log.trace "description.xml response (application/json)" def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body) if (body.success != null) { if (body.success[0] != null) { @@ -692,7 +693,7 @@ def doDeviceSync(){ poll() ssdpSubscribe() discoverBridges() - checkBridgeStatus() + checkBridgeStatus() } /** @@ -705,10 +706,14 @@ def doDeviceSync(){ private void updateBridgeStatus(childDevice) { // Update activity timestamp if child device is a valid bridge def vbridges = getVerifiedHueBridges() - def vbridge = vbridges.find {"${it.value.mac}".toUpperCase() == childDevice?.device?.deviceNetworkId?.toUpperCase()} + def vbridge = vbridges.find { + "${it.value.mac}".toUpperCase() == childDevice?.device?.deviceNetworkId?.toUpperCase() + } vbridge?.value?.lastActivity = now() - if(vbridge) { + if (vbridge && childDevice?.device?.currentValue("status") == "Offline") { + log.debug "$childDevice is back Online" childDevice?.sendEvent(name: "status", value: "Online") + childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true) } } @@ -717,29 +722,37 @@ private void updateBridgeStatus(childDevice) { * for the bridge and all connected lights. Also, set ID number on bridge if not done previously. */ private void checkBridgeStatus() { - def bridges = getHueBridges() - // Check if each bridge has been heard from within the last 11 minutes (2 poll intervals times 5 minutes plus buffer) - def time = now() - (1000 * 60 * 11) - bridges.each { - def d = getChildDevice(it.value.mac) - if(d) { - // Set id number on bridge if not done - if (it.value.idNumber == null) { - it.value.idNumber = getBridgeIdNumber(it.value.serialNumber) + def bridges = getHueBridges() + // Check if each bridge has been heard from within the last 11 minutes (2 poll intervals times 5 minutes plus buffer) + def time = now() - (1000 * 60 * 11) + bridges.each { + def d = getChildDevice(it.value.mac) + if (d) { + // Set id number on bridge if not done + if (it.value.idNumber == null) { + it.value.idNumber = getBridgeIdNumber(it.value.serialNumber) d.sendEvent(name: "idNumber", value: it.value.idNumber) - } + } if (it.value.lastActivity < time) { // it.value.lastActivity != null && - log.warn "Bridge $it.value.idNumber is Offline" - d.sendEvent(name: "status", value: "Offline") + if (d.currentStatus == "Online") { + log.warn "$d is Offline" + d.sendEvent(name: "status", value: "Offline") + d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true) - state.bulbs?.each { - it.value.online = false + Calendar currentTime = Calendar.getInstance() + getChildDevices().each { + def id = getId(it) + if (state.bulbs[id]?.online == true) { + state.bulbs[id]?.online = false + state.bulbs[id]?.unreachableSince = currentTime.getTimeInMillis() + it.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true) + } + } } - getChildDevices().each { - it.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", isStateChange: true, displayed: false) - } - } else { + } else if (d.currentStatus == "Offline") { + log.debug "$d is back Online" + d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true) d.sendEvent(name: "status", value: "Online")//setOnline(false) } } @@ -791,24 +804,24 @@ def parse(childDevice, description) { def parsedEvent = parseLanMessage(description) if (parsedEvent.headers && parsedEvent.body) { def headerString = parsedEvent.headers.toString() - def bodyString = parsedEvent.body.toString() + def bodyString = parsedEvent.body.toString() if (headerString?.contains("json")) { - def body - try { - body = new groovy.json.JsonSlurper().parseText(bodyString) - } catch (all) { - log.warn "Parsing Body failed - trying again..." - poll() - } - if (body instanceof java.util.Map) { - // get (poll) reponse - return handlePoll(body) + def body + try { + body = new groovy.json.JsonSlurper().parseText(bodyString) + } catch (all) { + log.warn "Parsing Body failed - trying again..." + poll() + } + if (body instanceof java.util.Map) { + // get (poll) reponse + return handlePoll(body) } else { //put response return handleCommandResponse(body) - } - } - } else { + } + } + } else { log.debug "parse - got something other than headers,body..." return [] } @@ -829,19 +842,19 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) { device.sendEvent([name: "colorTemperature", value: temp, descriptionText: "Color temperature has changed"]) // Return because color temperature change is not counted as a color change in SmartThings so no hex update necessary return - } + } if (hue != null) { // 0-65535 def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false] - } + } if (sat != null) { // 0-254 def value = Math.round(sat * 100 / 254) as int - events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false] - } + events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false] + } // Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex if (xy != null && colormode != "hs") { @@ -943,12 +956,9 @@ private handleCommandResponse(body) { updates.each { childDeviceNetworkId, params -> def device = getChildDevice(childDeviceNetworkId) def id = getId(device) - // If device is offline, then don't send events which will update device watch - if (isOnline(id)) { - sendBasicEvents(device, "on", params.on) - sendBasicEvents(device, "bri", params.bri) - sendColorEvents(device, params.xy, params.hue, params.sat, params.ct) - } + sendBasicEvents(device, "on", params.on) + sendBasicEvents(device, "bri", params.bri) + sendColorEvents(device, params.xy, params.hue, params.sat, params.ct) } return [] } @@ -981,39 +991,38 @@ private handlePoll(body) { def bulbs = getChildDevices() for (bulb in body) { - def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"} + def device = bulbs.find { it.deviceNetworkId == "${app.id}/${bulb.key}" } if (device) { if (bulb.value.state?.reachable) { - if (state.bulbs[bulb.key]?.online == false) { + if (state.bulbs[bulb.key]?.online == false || state.bulbs[bulb.key]?.online == null) { // light just came back online, notify device watch - def lastActivity = now() - device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true) + device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true) log.debug "$device is Online" } // Mark light as "online" state.bulbs[bulb.key]?.unreachableSince = null state.bulbs[bulb.key]?.online = true - - // If user just executed commands, then do not send events to avoid confusing the turning on/off state - if (!state.updating) { - sendBasicEvents(device, "on", bulb.value?.state?.on) - sendBasicEvents(device, "bri", bulb.value?.state?.bri) - sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) - } } else { if (state.bulbs[bulb.key]?.unreachableSince == null) { // Store the first time where device was reported as "unreachable" state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis() - } else if (state.bulbs[bulb.key]?.online) { + } + if (state.bulbs[bulb.key]?.online || state.bulbs[bulb.key]?.online == null) { // Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary - if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis()) { + if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis() || state.bulbs[bulb.key]?.online == null) { log.warn "$device went Offline" state.bulbs[bulb.key]?.online = false - device.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", displayed: false, isStateChange: true) + device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true) } } log.warn "$device may not reachable by Hue bridge" } + // If user just executed commands, then do not send events to avoid confusing the turning on/off state + if (!state.updating) { + sendBasicEvents(device, "on", bulb.value?.state?.on) + sendBasicEvents(device, "bri", bulb.value?.state?.bri) + sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) + } } } return [] @@ -1032,16 +1041,16 @@ def updateHandler() { def hubVerification(bodytext) { log.trace "Bridge sent back description.xml for verification" - def body = new XmlSlurper().parseText(bodytext) - if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) { - def bridges = getHueBridges() - def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())} - if (bridge) { - bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true] - } else { - log.error "/description.xml returned a bridge that didn't exist" - } - } + def body = new XmlSlurper().parseText(bodytext) + if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) { + def bridges = getHueBridges() + def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())} + if (bridge) { + bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true] + } else { + log.error "/description.xml returned a bridge that didn't exist" + } + } } def on(childDevice) { @@ -1050,7 +1059,7 @@ def on(childDevice) { updateInProgress() createSwitchEvent(childDevice, "on") put("lights/$id/state", [on: true]) - return "Bulb is turning On" + return "Bulb is turning On" } def off(childDevice) { @@ -1059,15 +1068,15 @@ def off(childDevice) { updateInProgress() createSwitchEvent(childDevice, "off") put("lights/$id/state", [on: false]) - return "Bulb is turning Off" + return "Bulb is turning Off" } def setLevel(childDevice, percent) { log.debug "Executing 'setLevel'" def id = getId(childDevice) - updateInProgress() + updateInProgress() // 1 - 254 - def level + def level if (percent == 1) level = 1 else @@ -1101,7 +1110,7 @@ def setSaturation(childDevice, percent) { def setHue(childDevice, percent) { log.debug "Executing 'setHue($percent)'" def id = getId(childDevice) - updateInProgress() + updateInProgress() // 0 - 65535 def level = Math.min(Math.round(percent * 65535 / 100), 65535) // TODO should this be done by app only or should we default to on? @@ -1113,7 +1122,7 @@ def setHue(childDevice, percent) { def setColorTemperature(childDevice, huesettings) { log.debug "Executing 'setColorTemperature($huesettings)'" def id = getId(childDevice) - updateInProgress() + updateInProgress() // 153 (6500K) to 500 (2000K) def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings) createSwitchEvent(childDevice, "on") @@ -1122,14 +1131,14 @@ def setColorTemperature(childDevice, huesettings) { } def setColor(childDevice, huesettings) { - log.debug "Executing 'setColor($huesettings)'" + log.debug "Executing 'setColor($huesettings)'" def id = getId(childDevice) - updateInProgress() + updateInProgress() - def value = [:] - def hue = null - def sat = null - def xy = null + def value = [:] + def hue = null + def sat = null + def xy = null // Prefer hue/sat over hex to make sure it works with the majority of the smartapps if (huesettings.hue != null || huesettings.sat != null) { @@ -1154,48 +1163,32 @@ def setColor(childDevice, huesettings) { // value.xy = calculateXY(hex, model) // Once groups, or scenes are introduced it might be a good idea to use unique models again value.xy = calculateXY(hex) - } + } */ - // Default behavior is to turn light on - value.on = true + // Default behavior is to turn light on + value.on = true - if (huesettings.level != null) { - if (huesettings.level <= 0) - value.on = false - else if (huesettings.level == 1) - value.bri = 1 - else + if (huesettings.level != null) { + if (huesettings.level <= 0) + value.on = false + else if (huesettings.level == 1) + value.bri = 1 + else value.bri = Math.min(Math.round(huesettings.level * 254 / 100), 254) - } - value.alert = huesettings.alert ? huesettings.alert : "none" - value.transitiontime = huesettings.transitiontime ? huesettings.transitiontime : 4 + } + value.alert = huesettings.alert ? huesettings.alert : "none" + value.transitiontime = huesettings.transitiontime ? huesettings.transitiontime : 4 - // Make sure to turn off light if requested - if (huesettings.switch == "off") - value.on = false + // Make sure to turn off light if requested + if (huesettings.switch == "off") + value.on = false createSwitchEvent(childDevice, value.on ? "on" : "off") - put("lights/$id/state", value) + put("lights/$id/state", value) return "Setting color to $value" } -def ping(childDevice) { - if (childDevice.device?.deviceNetworkId?.equalsIgnoreCase(selectedHue)) { - if (childDevice.device?.currentValue("status")?.equalsIgnoreCase("Online")) { - childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Bridge is reachable", displayed: false, isStateChange: true) - return "Bridge is Online" - } else { - return "Bridge is Offline" - } - } else if (isOnline(getId(childDevice))) { - childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Light is reachable", displayed: false, isStateChange: true) - return "Device is Online" - } else { - return "Device is Offline" - } -} - private getId(childDevice) { if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) { return childDevice.device?.deviceNetworkId[3..-1] @@ -1246,30 +1239,30 @@ private getBridgeIdNumber(serialNumber) { private getBridgeIP() { def host = null if (selectedHue) { - def d = getChildDevice(selectedHue) - if (d) { - if (d.getDeviceDataByName("networkAddress")) - host = d.getDeviceDataByName("networkAddress") - else - host = d.latestState('networkAddress').stringValue - } - if (host == null || host == "") { - def serialNumber = selectedHue - def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value - if (!bridge) { - bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value - } - if (bridge?.ip && bridge?.port) { - if (bridge?.ip.contains(".")) - host = "${bridge?.ip}:${bridge?.port}" - else - host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}" - } else if (bridge?.networkAddress && bridge?.deviceAddress) - host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}" - } - log.trace "Bridge: $selectedHue - Host: $host" - } - return host + def d = getChildDevice(selectedHue) + if (d) { + if (d.getDeviceDataByName("networkAddress")) + host = d.getDeviceDataByName("networkAddress") + else + host = d.latestState('networkAddress').stringValue + } + if (host == null || host == "") { + def serialNumber = selectedHue + def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value + if (!bridge) { + bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value + } + if (bridge?.ip && bridge?.port) { + if (bridge?.ip.contains(".")) + host = "${bridge?.ip}:${bridge?.port}" + else + host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}" + } else if (bridge?.networkAddress && bridge?.deviceAddress) + host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}" + } + log.trace "Bridge: $selectedHue - Host: $host" + } + return host } private Integer convertHexToInt(hex) { @@ -1309,7 +1302,7 @@ private List getRealHubFirmwareVersions() { * @param childDevice device to send event for * @param setSwitch The new switch state, "on" or "off" * @param setLevel Optional, switchLevel between 0-100, used if you set level to 0 for example since - * that should generate "off" instead of level change + * that should generate "off" instead of level change */ private void createSwitchEvent(childDevice, setSwitch, setLevel = null) { @@ -1452,8 +1445,8 @@ private float[] calculateXY(colorStr, model = null) { xy[0] = closestPoint.x; xy[1] = closestPoint.y; } - // xy[0] = PHHueHelper.precision(4, xy[0]); - // xy[1] = PHHueHelper.precision(4, xy[1]); + // xy[0] = PHHueHelper.precision(4, xy[0]); + // xy[1] = PHHueHelper.precision(4, xy[1]); // TODO needed, assume it just sets number of decimals? @@ -1471,7 +1464,7 @@ private float[] calculateXY(colorStr, model = null) { * * @param points the float array contain x and the y value. [x,y] * @param model the model of the lamp, example: "LCT001" for hue bulb. Used to calculate the color gamut. - * If this value is empty the default gamut values are used. + * If this value is empty the default gamut values are used. * @return the color value in hex (#ff03d3). If xy is null OR xy is not an array of size 2, Color. BLACK will be returned */ private String colorFromXY(points, model ) { @@ -1785,3 +1778,4 @@ def hsvToHex(hue, sat, value = 100){ return "#$r1$g1$b1" } + From db5237ca336175b0b0990d61bc1cf6830663e69d Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Tue, 18 Oct 2016 18:45:27 -0700 Subject: [PATCH 07/11] adding missing deviceJoinName in the generic dth --- .../smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy index 068d4ec..9bb7f20 100644 --- a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy @@ -28,8 +28,8 @@ metadata { capability "Switch Level" capability "Health Check" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB", deviceJoinName: "OSRAM LIGHTIFY Gardenspot mini RGB" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB", deviceJoinName: "OSRAM LIGHTIFY Gardenspot mini RGB" } // UI tile definitions From b582c3d832335cd1f17fa361e80651175a426148 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Tue, 18 Oct 2016 15:47:49 -0600 Subject: [PATCH 08/11] DVCSMP-2108 LIFX: Add devicewatch support --- .../lifx-color-bulb.groovy | 38 ++++++++-------- .../lifx-white-bulb.groovy | 41 ++++++++--------- .../lifx-connect.src/lifx-connect.groovy | 44 ++++++++++++++----- 3 files changed, 71 insertions(+), 52 deletions(-) diff --git a/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy b/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy index b93235e..5ecfae0 100644 --- a/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy +++ b/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy @@ -11,9 +11,9 @@ metadata { capability "Color Temperature" capability "Switch" capability "Switch Level" // brightness - capability "Polling" capability "Refresh" capability "Sensor" + capability "Health Check" } simulator { @@ -23,7 +23,6 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666" attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" @@ -64,12 +63,8 @@ metadata { } } -// parse events into attributes -def parse(String description) { - if (description == 'updated') { - return // don't poll when config settings is being updated as it may time out - } - poll() +void installed() { + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}") } // handle commands @@ -141,7 +136,6 @@ def setLevel(percentage) { percentage = 1 // clamp to 1% } if (percentage == 0) { - sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update return off() // if the brightness is set to 0, just turn it off } parent.logErrors(logObject:log) { @@ -193,14 +187,17 @@ def off() { return [] } -def poll() { - log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" +def refresh() { + log.debug "Executing 'refresh'" + def resp = parent.apiGET("/lights/${selector()}") if (resp.status == 404) { - sendEvent(name: "switch", value: "unreachable") + state.online = false + sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false) + log.warn "$device is Offline" return [] } else if (resp.status != 200) { - log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") + log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}") return [] } def data = resp.data[0] @@ -209,19 +206,20 @@ def poll() { sendEvent(name: "label", value: data.label) sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100)) sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") + sendEvent(name: "switch", value: data.power) sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int)) sendEvent(name: "hue", value: data.color.hue / 3.6) sendEvent(name: "saturation", value: data.color.saturation * 100) sendEvent(name: "colorTemperature", value: data.color.kelvin) - sendEvent(name: "model", value: "${data.product.company} ${data.product.name}") + sendEvent(name: "model", value: data.product.name) - return [] + if (data.connected) { + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) + log.debug "$device is Online" + } else { + sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) + log.warn "$device is Offline" } - -def refresh() { - log.debug "Executing 'refresh'" - poll() } def selector() { diff --git a/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy b/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy index 54a78df..16e7c57 100644 --- a/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy +++ b/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy @@ -10,9 +10,9 @@ metadata { capability "Color Temperature" capability "Switch" capability "Switch Level" // brightness - capability "Polling" capability "Refresh" capability "Sensor" + capability "Health Check" } simulator { @@ -22,13 +22,12 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666" attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" - } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel" } @@ -53,15 +52,10 @@ metadata { main "switch" details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) } - } -// parse events into attributes -def parse(String description) { - if (description == 'updated') { - return // don't poll when config settings is being updated as it may time out - } - poll() +void installed() { + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}") } // handle commands @@ -71,7 +65,6 @@ def setLevel(percentage) { percentage = 1 // clamp to 1% } if (percentage == 0) { - sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update return off() // if the brightness is set to 0, just turn it off } parent.logErrors(logObject:log) { @@ -123,14 +116,17 @@ def off() { return [] } -def poll() { - log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" +def refresh() { + log.debug "Executing 'refresh'" + def resp = parent.apiGET("/lights/${selector()}") if (resp.status == 404) { - sendEvent(name: "switch", value: "unreachable") + state.online = false + sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false) + log.warn "$device is Offline" return [] } else if (resp.status != 200) { - log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") + log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}") return [] } def data = resp.data[0] @@ -138,16 +134,17 @@ def poll() { sendEvent(name: "label", value: data.label) sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100)) sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") + sendEvent(name: "switch", value: data.power) sendEvent(name: "colorTemperature", value: data.color.kelvin) sendEvent(name: "model", value: data.product.name) - return [] -} - -def refresh() { - log.debug "Executing 'refresh'" - poll() + if (data.connected) { + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) + log.debug "$device is Online" + } else { + sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) + log.warn "$device is Offline" + } } def selector() { diff --git a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy index 7832b68..7fc13c9 100644 --- a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy +++ b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy @@ -242,8 +242,6 @@ def installed() { } else { initialize() } - // Check for new devices and remove old ones every 3 hours - runEvery3Hours('updateDevices') } // called after settings are changed @@ -271,6 +269,8 @@ private removeChildDevices(devices) { def initialize() { log.debug "initialize" updateDevices() + // Check for new devices and remove old ones every 3 hours + runEvery5Minutes('updateDevices') } // Misc @@ -376,7 +376,7 @@ def updateDevices() { def data = [ label: device.label, level: Math.round((device.brightness ?: 1) * 100), - switch: device.connected ? device.power : "unreachable", + switch: device.power, colorTemperature: device.color.kelvin ] if (device.product.capabilities.has_color) { @@ -387,18 +387,42 @@ def updateDevices() { } else { childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data) } + childDevice?.completedSetup = true + } else { + if (device.product.capabilities.has_color) { + sendEvent(name: "color", value: colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)) + sendEvent(name: "hue", value: device.color.hue / 3.6) + sendEvent(name: "saturation", value: device.color.saturation * 100) + } + childDevice.sendEvent(name: "label", value: device.label) + childDevice.sendEvent(name: "level", value: Math.round((device.brightness ?: 1) * 100)) + childDevice.sendEvent(name: "switch.setLevel", value: Math.round((device.brightness ?: 1) * 100)) + childDevice.sendEvent(name: "switch", value: device.power) + childDevice.sendEvent(name: "colorTemperature", value: device.color.kelvin) + childDevice.sendEvent(name: "model", value: device.product.name) } + + if (state.devices[device.id] == null) { + // State missing, add it and set it to opposite status as current status to provoke event below + state.devices[device.id] = [online : !device.connected] + } + + if (!state.devices[device.id]?.online && device.connected) { + // Device came online after being offline + childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) + log.debug "$device is back Online" + } else if (state.devices[device.id]?.online && !device.connected) { + // Device went offline after being online + childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) + log.debug "$device went Offline" + } + state.devices[device.id] = [online: device.connected] } getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each { log.info("Deleting ${it.deviceNetworkId}") + state.devices[it.deviceNetworkId] = null deleteChildDevice(it.deviceNetworkId) } - runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block } -def refreshDevices() { - log.info("Refreshing all devices...") - getChildDevices().each { device -> - device.refresh() - } -} + From 5e48e710d4da3c15e0fc03d11b17c3484a09647d Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Mon, 24 Oct 2016 14:44:08 -0600 Subject: [PATCH 09/11] DVCSMP-2168 Philips Hue/LIFX: Send device watch registration -Send device watch register events in update() --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 9 +++++++++ .../smartthings/lifx-connect.src/lifx-connect.groovy | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 7ee6e60..621d0d0 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -299,6 +299,7 @@ def initialize() { state.bridgeRefreshCount = 0 state.bulbRefreshCount = 0 state.updating = false + setupDeviceWatch() if (selectedHue) { addBridge() addBulbs() @@ -321,6 +322,14 @@ def uninstalled(){ state.username = null } +private setupDeviceWatch() { + def hub = location.hubs[0] + // Make sure that all child devices are enrolled in device watch + getChildDevices().each { + it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}") + } +} + private upgradeDeviceType(device, newHueType) { def deviceType = getDeviceType(newHueType) diff --git a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy index 7fc13c9..41f20ef 100644 --- a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy +++ b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy @@ -271,9 +271,17 @@ def initialize() { updateDevices() // Check for new devices and remove old ones every 3 hours runEvery5Minutes('updateDevices') + setupDeviceWatch() } // Misc +private setupDeviceWatch() { + def hub = location.hubs[0] + // Make sure that all child devices are enrolled in device watch + getChildDevices().each { + it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}") + } +} Map apiRequestHeaders() { return ["Authorization": "Bearer ${state.lifxAccessToken}", From 583d42df1359c6492bbb77c6a2964973681ffbd2 Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Tue, 25 Oct 2016 09:10:39 -0500 Subject: [PATCH 10/11] Remove read attribute for new mfg code Because of the interaction between the DTH running in the cloud and the one running in appengine, we can't make this change yet as for unupdated devices the humidity gets replaced with null. So we back out the call with the new mfgID as there aren't devices out there yet using it. This relates to: https://smartthings.atlassian.net/browse/DPROT-183 --- .../smartsense-temp-humidity-sensor.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index 638b9d4..98af1a4 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -264,7 +264,6 @@ def refresh() { log.debug "refresh temperature, humidity, and battery" return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware - zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0x104E]) + // New firmware zigbee.readAttribute(0x0402, 0x0000) + zigbee.readAttribute(0x0001, 0x0020) } From b8111e8760647ebb5bad22073c967ab61962db6c Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Tue, 25 Oct 2016 15:17:34 -0600 Subject: [PATCH 11/11] Revert LIFX device watch DVCSMP-2108 LIFX: Add devicewatch support DVCSMP-2168 LIFX: Send device watch registration events in update() Left Hue change for second ticket above --- .../lifx-color-bulb.groovy | 37 ++++++------- .../lifx-white-bulb.groovy | 38 +++++++------- .../lifx-connect.src/lifx-connect.groovy | 52 ++++--------------- 3 files changed, 49 insertions(+), 78 deletions(-) diff --git a/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy b/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy index 5ecfae0..d98e8d7 100644 --- a/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy +++ b/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy @@ -11,9 +11,9 @@ metadata { capability "Color Temperature" capability "Switch" capability "Switch Level" // brightness + capability "Polling" capability "Refresh" capability "Sensor" - capability "Health Check" } simulator { @@ -23,6 +23,7 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666" attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" @@ -63,8 +64,12 @@ metadata { } } -void installed() { - sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}") +// parse events into attributes +def parse(String description) { + if (description == 'updated') { + return // don't poll when config settings is being updated as it may time out + } + poll() } // handle commands @@ -187,17 +192,14 @@ def off() { return [] } -def refresh() { - log.debug "Executing 'refresh'" - +def poll() { + log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" def resp = parent.apiGET("/lights/${selector()}") if (resp.status == 404) { - state.online = false - sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false) - log.warn "$device is Offline" + sendEvent(name: "switch", value: "unreachable") return [] } else if (resp.status != 200) { - log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}") + log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") return [] } def data = resp.data[0] @@ -206,20 +208,19 @@ def refresh() { sendEvent(name: "label", value: data.label) sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100)) sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.power) + sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int)) sendEvent(name: "hue", value: data.color.hue / 3.6) sendEvent(name: "saturation", value: data.color.saturation * 100) sendEvent(name: "colorTemperature", value: data.color.kelvin) - sendEvent(name: "model", value: data.product.name) + sendEvent(name: "model", value: "${data.product.company} ${data.product.name}") - if (data.connected) { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - log.debug "$device is Online" - } else { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - log.warn "$device is Offline" + return [] } + +def refresh() { + log.debug "Executing 'refresh'" + poll() } def selector() { diff --git a/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy b/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy index 16e7c57..d089f77 100644 --- a/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy +++ b/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy @@ -10,9 +10,9 @@ metadata { capability "Color Temperature" capability "Switch" capability "Switch Level" // brightness + capability "Polling" capability "Refresh" capability "Sensor" - capability "Health Check" } simulator { @@ -22,12 +22,13 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666" attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" - } + } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel" } @@ -52,10 +53,15 @@ metadata { main "switch" details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) } + } -void installed() { - sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}") +// parse events into attributes +def parse(String description) { + if (description == 'updated') { + return // don't poll when config settings is being updated as it may time out + } + poll() } // handle commands @@ -116,17 +122,14 @@ def off() { return [] } -def refresh() { - log.debug "Executing 'refresh'" - +def poll() { + log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" def resp = parent.apiGET("/lights/${selector()}") if (resp.status == 404) { - state.online = false - sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false) - log.warn "$device is Offline" + sendEvent(name: "switch", value: "unreachable") return [] } else if (resp.status != 200) { - log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}") + log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") return [] } def data = resp.data[0] @@ -134,17 +137,16 @@ def refresh() { sendEvent(name: "label", value: data.label) sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100)) sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.power) + sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") sendEvent(name: "colorTemperature", value: data.color.kelvin) sendEvent(name: "model", value: data.product.name) - if (data.connected) { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - log.debug "$device is Online" - } else { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - log.warn "$device is Offline" + return [] } + +def refresh() { + log.debug "Executing 'refresh'" + poll() } def selector() { diff --git a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy index 41f20ef..8988011 100644 --- a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy +++ b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy @@ -242,6 +242,8 @@ def installed() { } else { initialize() } + // Check for new devices and remove old ones every 3 hours + runEvery3Hours('updateDevices') } // called after settings are changed @@ -269,19 +271,9 @@ private removeChildDevices(devices) { def initialize() { log.debug "initialize" updateDevices() - // Check for new devices and remove old ones every 3 hours - runEvery5Minutes('updateDevices') - setupDeviceWatch() } // Misc -private setupDeviceWatch() { - def hub = location.hubs[0] - // Make sure that all child devices are enrolled in device watch - getChildDevices().each { - it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}") - } -} Map apiRequestHeaders() { return ["Authorization": "Bearer ${state.lifxAccessToken}", @@ -384,7 +376,7 @@ def updateDevices() { def data = [ label: device.label, level: Math.round((device.brightness ?: 1) * 100), - switch: device.power, + switch: device.connected ? device.power : "unreachable", colorTemperature: device.color.kelvin ] if (device.product.capabilities.has_color) { @@ -395,42 +387,18 @@ def updateDevices() { } else { childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data) } - childDevice?.completedSetup = true - } else { - if (device.product.capabilities.has_color) { - sendEvent(name: "color", value: colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)) - sendEvent(name: "hue", value: device.color.hue / 3.6) - sendEvent(name: "saturation", value: device.color.saturation * 100) } - childDevice.sendEvent(name: "label", value: device.label) - childDevice.sendEvent(name: "level", value: Math.round((device.brightness ?: 1) * 100)) - childDevice.sendEvent(name: "switch.setLevel", value: Math.round((device.brightness ?: 1) * 100)) - childDevice.sendEvent(name: "switch", value: device.power) - childDevice.sendEvent(name: "colorTemperature", value: device.color.kelvin) - childDevice.sendEvent(name: "model", value: device.product.name) - } - - if (state.devices[device.id] == null) { - // State missing, add it and set it to opposite status as current status to provoke event below - state.devices[device.id] = [online : !device.connected] - } - - if (!state.devices[device.id]?.online && device.connected) { - // Device came online after being offline - childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - log.debug "$device is back Online" - } else if (state.devices[device.id]?.online && !device.connected) { - // Device went offline after being online - childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - log.debug "$device went Offline" - } - state.devices[device.id] = [online: device.connected] } getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each { log.info("Deleting ${it.deviceNetworkId}") - state.devices[it.deviceNetworkId] = null deleteChildDevice(it.deviceNetworkId) } + runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block } - +def refreshDevices() { + log.info("Refreshing all devices...") + getChildDevices().each { device -> + device.refresh() + } +}