From ecb975540b9714c0cfd01390b418d0a3a32c457f Mon Sep 17 00:00:00 2001 From: tslagle13 Date: Wed, 14 Oct 2015 10:05:02 -0700 Subject: [PATCH 1/7] Fix requirement for SMS Removed requirement to provide a SMS number is the user does not have contacts. Add logic to verify a number was provided before sending SMS. --- .../routine-director.src/routine-director.groovy | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/smartapps/tslagle13/routine-director.src/routine-director.groovy b/smartapps/tslagle13/routine-director.src/routine-director.groovy index 86263b9..21d0c48 100644 --- a/smartapps/tslagle13/routine-director.src/routine-director.groovy +++ b/smartapps/tslagle13/routine-director.src/routine-director.groovy @@ -50,7 +50,7 @@ preferences { } section("Send Notifications?") { input("recipients", "contact", title: "Send notifications to") { - input "phone", "phone", title: "Send an SMS to this number?" + input "phone", "phone", title: "Send an SMS to this number?", required:false } } @@ -266,7 +266,9 @@ def sendAway(msg) { } else { sendPush(msg) - sendSms(phone, msg) + if(phone){ + sendSms(phone, msg) + } } } @@ -280,7 +282,9 @@ def sendHome(msg) { } else { sendPush(msg) - sendSms(phone, msg) + if(phone){ + sendSms(phone, msg) + } } } @@ -339,4 +343,4 @@ private getTimeIntervalLabel() { private hideOptionsSection() { (starting || ending || days || modes) ? false: true -} \ No newline at end of file +} From 9d016839c8e6626347be56f8b4fc23e775f64116 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Wed, 14 Oct 2015 16:07:38 -0700 Subject: [PATCH 2/7] Refactor and update dimmer-swith DTH --- .../dimmer-switch.src/dimmer-switch.groovy | 199 +++++++++--------- 1 file changed, 100 insertions(+), 99 deletions(-) diff --git a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy index 4218b3d..6b7eca1 100644 --- a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy +++ b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy @@ -55,141 +55,136 @@ metadata { } } - standardTile("indicator", "device.indicatorStatus", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off" state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on" state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit" } - standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff" } main(["switch"]) - details(["switch", "refresh", "indicator"]) + details(["switch", "level", "indicator", "refresh"]) + } } def parse(String description) { - def item1 = [ - canBeCurrentState: false, - linkText: getLinkText(device), - isStateChange: false, - displayed: false, - descriptionText: description, - value: description - ] - def result - def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1]) - if (cmd) { - result = createEvent(cmd, item1) + def result = null + if (description != "updated") { + log.debug "parse() >> zwave.parse($description)" + def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } } - else { - item1.displayed = displayed(description, item1.isStateChange) - result = [item1] + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + log.debug "Was hailed: requesting state update" + } else { + log.debug "Parse returned ${result?.descriptionText}" } - log.debug "Parse returned ${result?.descriptionText}" - result + return result } -def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - for (int i = 0; i < result.size(); i++) { - result[i].type = "physical" +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(physicalgraph.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value)] + if (cmd.value && cmd.value <= 100) { + result << createEvent(name: "level", value: cmd.value, unit: "%") } - result + return result } -def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - for (int i = 0; i < result.size(); i++) { - result[i].type = "physical" - } - result -} - -def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) { - [] -} - -def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) { - [response(zwave.basicV1.basicGet())] -} - -def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - for (int i = 0; i < result.size(); i++) { - result[i].type = "physical" - } - result -} - -def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - result[0].descriptionText = "${item1.linkText} is ${item1.value}" - result[0].handlerName = cmd.value ? "statusOn" : "statusOff" - for (int i = 0; i < result.size(); i++) { - result[i].type = "digital" - } - result -} - -def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { - def result = [item1] - - item1.name = "switch" - item1.value = cmd.value ? "on" : "off" - item1.handlerName = item1.value - item1.descriptionText = "${item1.linkText} was turned ${item1.value}" - item1.canBeCurrentState = true - item1.isStateChange = isStateChange(device, item1.name, item1.value) - item1.displayed = item1.isStateChange - - if (cmd.value >= 5) { - def item2 = new LinkedHashMap(item1) - item2.name = "level" - item2.value = cmd.value as String - item2.unit = "%" - item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" - item2.canBeCurrentState = true - item2.isStateChange = isStateChange(device, item2.name, item2.value) - item2.displayed = false - result << item2 - } - result -} def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "ConfigurationReport $cmd" def value = "when off" if (cmd.configurationValue[0] == 1) {value = "when on"} if (cmd.configurationValue[0] == 2) {value = "never"} - [name: "indicatorStatus", value: value, display: false] + createEvent([name: "indicatorStatus", value: value]) } -def createEvent(physicalgraph.zwave.Command cmd, Map map) { - // Handles any Z-Wave commands we aren't interested in - log.debug "UNHANDLED COMMAND $cmd" +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false]) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) { + [createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] } def on() { - log.info "on" - delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) } def off() { - delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) } def setLevel(value) { + log.debug "setLevel >> value: $value" def valueaux = value as Integer - def level = Math.min(valueaux, 99) + def level = Math.max(Math.min(valueaux, 99), 0) + if (level > 0) { + sendEvent(name: "switch", value: "on") + } else { + sendEvent(name: "switch", value: "off") + } + sendEvent(name: "level", value: level, unit: "%") delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) } def setLevel(value, duration) { + log.debug "setLevel >> value: $value, duration: $duration" def valueaux = value as Integer - def level = Math.min(valueaux, 99) + def level = Math.max(Math.min(valueaux, 99), 0) def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) - zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format() + def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000 + delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay) } def poll() { @@ -197,21 +192,27 @@ def poll() { } def refresh() { - zwave.switchMultilevelV1.switchMultilevelGet().format() + log.debug "refresh() is called" + def commands = [] + commands << zwave.switchMultilevelV1.switchMultilevelGet().format() + if (getDataValue("MSR") == null) { + commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + delayBetween(commands,100) } def indicatorWhenOn() { - sendEvent(name: "indicatorStatus", value: "when on", display: false) + sendEvent(name: "indicatorStatus", value: "when on") zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() } def indicatorWhenOff() { - sendEvent(name: "indicatorStatus", value: "when off", display: false) + sendEvent(name: "indicatorStatus", value: "when off") zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() } def indicatorNever() { - sendEvent(name: "indicatorStatus", value: "never", display: false) + sendEvent(name: "indicatorStatus", value: "never") zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format() } @@ -222,4 +223,4 @@ def invertSwitch(invert=true) { else { zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format() } -} +} \ No newline at end of file From e217805d981e5ffac90ec7d4c5fdcb091ea95f5e Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Tue, 22 Sep 2015 23:56:17 -0700 Subject: [PATCH 3/7] generic device type for zigbee color temperature bulb --- ...zigbee-white-color-temperature-bulb.groovy | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy 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 new file mode 100644 index 0000000..dfe7475 --- /dev/null +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -0,0 +1,130 @@ +/** + * Copyright 2015 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. + * + * ZigBee White Color Temperature Bulb + * + * Author: SmartThings + * Date: 2015-09-22 + */ + +metadata { + definition (name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") { + + capability "Actuator" + capability "Color Temperature" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Switch Level" + + attribute "colorName", "string" + command "setGenericName" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019" + } + + // 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.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("colorName", key: "SECONDARY_CONTROL") { + attributeState "colorName", label:'${currentValue}' + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorTemperature", label: '${currentValue} K' + } + + main(["switch"]) + details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def finalResult = zigbee.getKnownDescription(description) + if (finalResult) { + log.info finalResult + if (finalResult.type == "update") { + log.info "$device updates: ${finalResult.value}" + } + else { + sendEvent(name: finalResult.type, value: finalResult.value) + } + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def setLevel(value) { + zigbee.setLevel(value) +} + +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() +} + +def setColorTemperature(value) { + setGenericName(value) + zigbee.setColorTemperature(value) +} + +//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature +def setGenericName(value){ + if (value != null) { + def genericName = "White" + if (value < 3300) { + genericName = "Soft White" + } else if (value < 4150) { + genericName = "Moonlight" + } else if (value <= 5000) { + genericName = "Cool White" + } else if (value >= 5000) { + genericName = "Daylight" + } + sendEvent(name: "colorName", value: genericName) + } +} From ef21fd42572f34ef6ae0ba9d200611434454e416 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Tue, 20 Oct 2015 01:01:17 -0700 Subject: [PATCH 4/7] DVCSMP-668 The following changes has been made to the original Aeon Multisensor device type handler to improve and modernize it: 1. Add "powerSupply" attribute to be able to tell power source (USB Cable/Battery) 2. Add preference page for user to customize "motion delay time", "motion sensitivity", and "sensor report interval" 3. Add color backgroud of "illuminance" value tile 4. Add tile for "ultravioletIndex" 5. Add tile for "powerSupply" 6. Modify updated() to be able to send configuration commands to sensor whether it is powered by USB cable or battery 7. When battery operated, send command to get update battery level if it hasn't been reported for a while 8. Report MSR of the sensor 9. Add handle for ConfigurationReport command class to update the "powerSupply" tile and change opetion mode of the sensor accordingly 10. Update configure() to configure parameters changed by user in preference page 11. Take out the "Configure" tile and instead send the configuration commands on every wakeup (sensor is battey powered) --- .../aeon-multisensor-6.groovy | 206 ++++++++++++++---- 1 file changed, 164 insertions(+), 42 deletions(-) diff --git a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy index 744879d..a453abf 100644 --- a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy +++ b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy @@ -24,6 +24,7 @@ metadata { capability "Battery" attribute "tamper", "enum", ["detected", "clear"] + attribute "powerSupply", "enum", ["USB Cable", "Battery"] fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" } @@ -63,6 +64,19 @@ metadata { status "wake up" : "command: 8407, payload: " } + preferences { + input description: "Please consult AEOTEC MULTISENSOR 6 operating manual for advanced setting options. You can skip this configuration to use default settings", + title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph" + + input "motionDelayTime", "enum", title: "Motion Sensor Delay Time", + options: ["20 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "${motionDelayTime}", displayDuringSetup: true + + input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["normal","maximum","minimum"], defaultValue: "${motionSensitivity}", displayDuringSetup: true + + input "reportInterval", "enum", title: "Sensors Report Interval", + options: ["8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${reportInterval}", displayDuringSetup: true + } + tiles(scale: 2) { multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { @@ -85,53 +99,89 @@ metadata { valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { state "humidity", label:'${currentValue}% humidity', unit:"" } + valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { - state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + state "illuminance", label:'${currentValue} ${unit}', unit:"lux", + backgroundColors:[ + [value: 0, color: "#000000"], + [value: 1, color: "#060053"], + [value: 3, color: "#3E3900"], + [value: 12, color: "#8E8400"], + [value: 24, color: "#C5C08B"], + [value: 36, color: "#DAD7B6"], + [value: 128, color: "#F3F2E9"], + [value: 1000, color: "#FFFFFF"] + ] } + + valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) { + state "ultravioletIndex", label:'${currentValue} UV index', unit:"" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - main(["motion", "temperature", "humidity", "illuminance"]) - details(["motion", "temperature", "humidity", "illuminance", "battery"]) + valueTile("powerSupply", "device.powerSupply", height: 2, width: 2, decoration: "flat") { + state "powerSupply", label:'${currentValue} powered', backgroundColor:"#ffffff" + } + + main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"]) + details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "battery", "powerSupply"]) } } -def updated() -{ - if (state.sec && !isConfigured()) { - // in case we miss the SCSR +def updated() { + log.debug "Updated with settings: ${settings}" + + log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}" + + if (device.latestValue("powerSupply") == "USB Cable") { //case1: USB powered response(configure()) + } else if (device.latestValue("powerSupply") == "Battery") { //case2: battery powered + // setConfigured("false") is used by WakeUpNotification + setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference + } else { //case3: power source is not identified, ask user to properly pair the sensor again + sendEvent( name: "powerSupply", value: "failed", isStateChange: true, displayed: true, + descriptionText: "This sensor failed to update it's power supply source. Unplug and plug-in the USB cable again or reinsert batteries to the sensor") } } -def parse(String description) -{ +def parse(String description) { + log.debug "parse() >> description: $description" def result = null if (description.startsWith("Err 106")) { - state.sec = 0 + log.debug "parse() >> Err 106" result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, - descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") } else if (description != "updated") { + log.debug "parse() >> zwave.parse(description)" def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) if (cmd) { result = zwaveEvent(cmd) } } - log.debug "Parsed '${description}' to ${result.inspect()}" + log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" return result } -def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) -{ +//this notification will be sent only when device is battery powered +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] - + def cmds = [] if (!isConfigured()) { - // we're still in the process of configuring a newly joined device log.debug("late configure") - result += response(configure()) + result << response(configure()) } else { - result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + //Only ask for battery if we haven't had a BatteryReport in a while + if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) { + log.debug("Device has been configured sending >> batteryGet()") + cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format() + cmds << "delay 1200" + } + log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + result << response(cmds) } result } @@ -149,7 +199,25 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { - response(configure()) + log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" + state.sec = 1 +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + state.sec = 1 + log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)" + def result = [createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)] + result +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd" + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { @@ -165,8 +233,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { createEvent(map) } -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ def map = [:] switch (cmd.sensorType) { case 1: @@ -208,7 +275,6 @@ def motionEvent(value) { } def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { - setConfigured() motionEvent(cmd.sensorValue) } @@ -225,47 +291,102 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm result << createEvent(name: "tamper", value: "clear", displayed: false) break case 3: - result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved") + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered") break case 7: result << motionEvent(1) break } } else { + log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" result << createEvent(descriptionText: cmd.toString(), isStateChange: false) } result } +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def result = [] + def value + if (cmd.configurationValue[0] == 0) { + value = "USB Cable" + if (!isConfigured()) { + log.debug("ConfigurationReport: configuring device") + result << response(configure()) + } + } + if (cmd.configurationValue[0] == 1) {value = "Battery"} + result << createEvent(name: "powerSupply", value: value, displayed: false) + result +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "General zwaveEvent cmd: ${cmd}" createEvent(descriptionText: cmd.toString(), isStateChange: false) } def configure() { // This sensor joins as a secure device if you double-click the button to include it - if (device.device.rawDescription =~ /98/ && !state.sec) { - log.debug "Multi 6 not sending configure until secure" + if (!state.sec) { + log.debug "${device.displayName} not sending configure until secure" return [] + } else { + log.debug "${device.displayName} is configuring its settings" + def request = [] + + //1. automatic report flags + // param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports + request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 227) + + //2. no-motion report x seconds after motion stops (default 20 secs) + request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20) + + //3. motionSensitivity 3 levels: 64-normal (default), 127-maximum, 0-minimum + request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1, + scaledConfigurationValue: + motionSensitivity == "normal" ? 64 : + motionSensitivity == "maximum" ? 127 : + motionSensitivity == "minimum" ? 0 : 64) + + //4. report every x minutes (threshold reports don't work on battery power, default 8 mins) + request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: 8*60) + + //5. report automatically on threshold change + request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1) + + //6. query sensor data + request << zwave.batteryV1.batteryGet() + request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex + + setConfigured("true") + + commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] } - log.debug "Multi 6 configure()" - def request = [ - // send no-motion report 20 seconds after motion stops - zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20), - - // report every 8 minutes (threshold reports don't work on battery power) - zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60), - - // report automatically on threshold change - zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1), - - zwave.batteryV1.batteryGet(), - zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C), - ] - commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] } -private setConfigured() { - updateDataValue("configured", "true") +private def getTimeOptionValueMap() { [ + "20 seconds" : 20, + "40 seconds" : 40, + "1 minute" : 60, + "2 minutes" : 2*60, + "3 minutes" : 3*60, + "4 minutes" : 4*60, + "5 minutes" : 5*60, + "8 minutes" : 8*60, + "15 minutes" : 15*60, + "30 minutes" : 30*60, + "1 hours" : 1*60*60, + "6 hours" : 6*60*60, + "12 hours" : 12*60*60, + "18 hours" : 6*60*60, + "24 hours" : 24*60*60, +]} + +private setConfigured(configure) { + updateDataValue("configured", configure) } private isConfigured() { @@ -281,5 +402,6 @@ private command(physicalgraph.zwave.Command cmd) { } private commands(commands, delay=200) { + log.info "sending commands: ${commands}" delayBetween(commands.collect{ command(it) }, delay) } From 794ff6b68aba4d2c48f753e8a9f7900e72cf2b74 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Wed, 21 Oct 2015 15:33:51 -0700 Subject: [PATCH 5/7] DVCSMP-668 - Show batteryStatus tile insteady of battery tile to be able to display both when sensor is USB powered or battery powered - Remove background for illuminance. This can be added when we have best practice of showing colors for lux. - Instead of using powerSupply:failed, configurationGet cmd is sent and then the configure() is triggered by configurationReport to determine powerSupply (USB Cable/Battery) - Instead of querying battery level on wake up, battery report is put in association group 2 that is configured to report every 6 hours by default - Update configure() to send both unsecure and secure configuration commands when sensor is joined normally or securely --- .../aeon-multisensor-6.groovy | 117 +++++++++--------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy index a453abf..108a72a 100644 --- a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy +++ b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy @@ -24,6 +24,7 @@ metadata { capability "Battery" attribute "tamper", "enum", ["detected", "clear"] + attribute "batteryStatus", "string" attribute "powerSupply", "enum", ["USB Cable", "Battery"] fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" @@ -101,17 +102,7 @@ metadata { } valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { - state "illuminance", label:'${currentValue} ${unit}', unit:"lux", - backgroundColors:[ - [value: 0, color: "#000000"], - [value: 1, color: "#060053"], - [value: 3, color: "#3E3900"], - [value: 12, color: "#8E8400"], - [value: 24, color: "#C5C08B"], - [value: 36, color: "#DAD7B6"], - [value: 128, color: "#F3F2E9"], - [value: 1000, color: "#FFFFFF"] - ] + state "illuminance", label:'${currentValue} ${unit}', unit:"lux" } valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) { @@ -122,18 +113,21 @@ metadata { state "battery", label:'${currentValue}% battery', unit:"" } + valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "batteryStatus", label:'${currentValue}', unit:"" + } + valueTile("powerSupply", "device.powerSupply", height: 2, width: 2, decoration: "flat") { state "powerSupply", label:'${currentValue} powered', backgroundColor:"#ffffff" } main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"]) - details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "battery", "powerSupply"]) + details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "batteryStatus"]) } } def updated() { log.debug "Updated with settings: ${settings}" - log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}" if (device.latestValue("powerSupply") == "USB Cable") { //case1: USB powered @@ -142,8 +136,10 @@ def updated() { // setConfigured("false") is used by WakeUpNotification setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference } else { //case3: power source is not identified, ask user to properly pair the sensor again - sendEvent( name: "powerSupply", value: "failed", isStateChange: true, displayed: true, - descriptionText: "This sensor failed to update it's power supply source. Unplug and plug-in the USB cable again or reinsert batteries to the sensor") + log.warn "power source is not identified, check it sensor is powered by USB, if so > configure()" + def request = [] + request << zwave.configurationV1.configurationGet(parameterNumber: 101) + response(commands(request)) } } @@ -173,12 +169,6 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { log.debug("late configure") result << response(configure()) } else { - //Only ask for battery if we haven't had a BatteryReport in a while - if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) { - log.debug("Device has been configured sending >> batteryGet()") - cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format() - cmds << "delay 1200" - } log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() result << response(cmds) @@ -221,6 +211,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def result = [] def map = [ name: "battery", unit: "%" ] if (cmd.batteryLevel == 0xFF) { map.value = 1 @@ -230,7 +221,11 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.value = cmd.batteryLevel } state.lastbatt = now() - createEvent(map) + result << createEvent(map) + if (device.latestValue("powerSupply") != "USB Cable"){ + result << createEvent(name: "batteryStatus", value: "${map.value} % battery", displayed: false) + } + result } def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ @@ -305,17 +300,23 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm } def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "ConfigurationReport: $cmd" def result = [] def value - if (cmd.configurationValue[0] == 0) { + if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) { value = "USB Cable" if (!isConfigured()) { log.debug("ConfigurationReport: configuring device") result << response(configure()) } + result << createEvent(name: "batteryStatus", value: value, displayed: false) + result << createEvent(name: "powerSupply", value: value, displayed: false) + }else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) { + value = "Battery" + result << createEvent(name: "powerSupply", value: value, displayed: false) + } else if (cmd.parameterNumber == 101){ + result << response(configure()) } - if (cmd.configurationValue[0] == 1) {value = "Battery"} - result << createEvent(name: "powerSupply", value: value, displayed: false) result } @@ -326,45 +327,49 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { def configure() { // This sensor joins as a secure device if you double-click the button to include it - if (!state.sec) { - log.debug "${device.displayName} not sending configure until secure" - return [] - } else { - log.debug "${device.displayName} is configuring its settings" - def request = [] + log.debug "${device.displayName} is configuring its settings" + def request = [] - //1. automatic report flags - // param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports - request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 227) + //1. set association groups for hub + request << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) - //2. no-motion report x seconds after motion stops (default 20 secs) - request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20) + request << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId) - //3. motionSensitivity 3 levels: 64-normal (default), 127-maximum, 0-minimum - request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1, - scaledConfigurationValue: - motionSensitivity == "normal" ? 64 : - motionSensitivity == "maximum" ? 127 : - motionSensitivity == "minimum" ? 0 : 64) + //2. automatic report flags + // param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports + request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 226) //association group 1 - //4. report every x minutes (threshold reports don't work on battery power, default 8 mins) - request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: 8*60) + request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) //association group 2 - //5. report automatically on threshold change - request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1) + //3. no-motion report x seconds after motion stops (default 20 secs) + request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20) - //6. query sensor data - request << zwave.batteryV1.batteryGet() - request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion - request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature - request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance - request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity - request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex + //4. motionSensitivity 3 levels: 64-normal (default), 127-maximum, 0-minimum + request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1, + scaledConfigurationValue: + motionSensitivity == "normal" ? 64 : + motionSensitivity == "maximum" ? 127 : + motionSensitivity == "minimum" ? 0 : 64) - setConfigured("true") + //5. report every x minutes (threshold reports don't work on battery power, default 8 mins) + request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: 8*60) //association group 1 - commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] - } + request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6*60*60) //association group 2 + + //6. report automatically on threshold change + request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1) + + //7. query sensor data + request << zwave.batteryV1.batteryGet() + request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex + + setConfigured("true") + + commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] } private def getTimeOptionValueMap() { [ From 47210ca8b492d09a17cf36c7f0dc468dc02c89e1 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Wed, 28 Oct 2015 14:12:35 -0400 Subject: [PATCH 6/7] Fix to Hue reverts dimmer settings (DVCSMP-1227) if you use the hue native app to adjust the dimmer setting, smartthings will reset the dimmer to previous value when toggling from ST app (and automations) --- .../hue-connect.src/hue-connect.groovy | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index f318ab7..5d6bf79 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -143,7 +143,7 @@ def bulbDiscovery() { if (numFound == 0) app.updateSetting("selectedBulbs", "") - if((bulbRefreshCount % 3) == 0) { + if((bulbRefreshCount % 5) == 0) { discoverHueBulbs() } @@ -318,11 +318,15 @@ def addBulbs() { def newHueBulb if (bulbs instanceof java.util.Map) { newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } - if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) { - d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) - } else { - d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) - } + if (newHueBulb != null) { + if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) { + d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) + } else { + d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) + } + } else { + log.debug "$dni in not longer paired to the Hue Bridge or ID changed" + } } else { //backwards compatable newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } @@ -604,18 +608,16 @@ def parse(childDevice, description) { } } -def on(childDevice, transition_deprecated = 0) { +def on(childDevice) { log.debug "Executing 'on'" - def percent = childDevice.device?.currentValue("level") as Integer - def level = Math.min(Math.round(percent * 255 / 100), 255) - put("lights/${getId(childDevice)}/state", [bri: level, on: true]) - return "level: $percent" + put("lights/${getId(childDevice)}/state", [on: true]) + return "Bulb is On" } -def off(childDevice, transition_deprecated = 0) { +def off(childDevice) { log.debug "Executing 'off'" put("lights/${getId(childDevice)}/state", [on: false]) - return "level: 0" + return "Bulb is Off" } def setLevel(childDevice, percent) { @@ -636,7 +638,7 @@ def setHue(childDevice, percent) { put("lights/${getId(childDevice)}/state", [hue: level]) } -def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) { +def setColor(childDevice, huesettings) { log.debug "Executing 'setColor($huesettings)'" def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) @@ -720,13 +722,8 @@ private getBridgeIP() { host = d.latestState('networkAddress').stringValue } if (host == null || host == "") { - def serialNumber = selectedHue - def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value - if (!bridge) { - //failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number - //in this case we will look up the bridge by comparing the incorrect mac addresses - bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value - } + def macAddress = selectedHue + def bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(macAddress) }?.value if (bridge?.ip && bridge?.port) { if (bridge?.ip.contains(".")) host = "${bridge?.ip}:${bridge?.port}" From 20f1a7688909923f3f33084708476416ba6438db Mon Sep 17 00:00:00 2001 From: juano2310 Date: Tue, 27 Oct 2015 15:37:38 -0400 Subject: [PATCH 7/7] Wemo refactor final (DVCSMP-1189) https://smartthings.atlassian.net/browse/DVCSMP-1189 Detect and mark device offline within 5 minutes. Show Device offline in device tile. Show Device offline in Recent Activity. Log the current IP address to Recent Activity. Log the changed IP address to Recent Activity. Support 'Turning on' and 'Turning off' (blindly changing the state of device to ON or OFF without confirming bulb responded correctly) Turn on / off through Wemo-App reflected timely in SmartThings App/Ecosystem. Manual turn on / off of device is reflected timely in SmartThings App/Ecosystem. Lower case createEvent Bug Fixes Bug fixes setOffline Minor cosmetic fixes --- .../wemo-light-switch.groovy | 75 +++-- .../wemo-motion.src/wemo-motion.groovy | 54 +++- .../wemo-switch.src/wemo-switch.groovy | 268 +++++++++--------- .../wemo-connect.src/wemo-connect.groovy | 186 ++++-------- 4 files changed, 275 insertions(+), 308 deletions(-) diff --git a/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy index b5f9f5c..1bd78f3 100644 --- a/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy +++ b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy @@ -25,6 +25,8 @@ metadata { capability "Refresh" capability "Sensor" + attribute "currentIP", "string" + command "subscribe" command "resubscribe" command "unsubscribe" @@ -34,21 +36,36 @@ metadata { // simulator metadata simulator {} - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" - state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" - } + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } - main "switch" - details (["switch", "refresh"]) - } + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" + state "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000" + } + + standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["rich-control", "refresh"]) + } } // parse events into attributes @@ -68,6 +85,7 @@ def parse(String description) { def result = [] def bodyString = msg.body if (bodyString) { + unschedule("setOffline") def body = new XmlSlurper().parseText(bodyString) if (body?.property?.TimeSyncRequest?.text()) { @@ -78,13 +96,14 @@ def parse(String description) { } else if (body?.property?.BinaryState?.text()) { def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" log.trace "Notify: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}") } else if (body?.property?.TimeZoneNotification?.text()) { log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" log.trace "GetBinaryResponse: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) + def dispaux = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux) } } @@ -101,14 +120,6 @@ private getCallBackAddress() { device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - private getHostAddress() { def ip = getDataValue("ip") def port = getDataValue("port") @@ -195,6 +206,8 @@ def subscribe(ip, port) { if (ip && ip != existingIp) { log.debug "Updating ip from $existingIp to $ip" updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") } if (port && port != existingPort) { log.debug "Updating port from $existingPort to $port" @@ -259,6 +272,8 @@ User-Agent: CyberGarage-HTTP/1.0 def poll() { log.debug "Executing 'poll'" +if (device.currentValue("currentIP") != "Offline") + runIn(10, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 @@ -274,3 +289,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } + +def setOffline() { + sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} diff --git a/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy index eb3ea10..9649db4 100644 --- a/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy +++ b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy @@ -21,6 +21,8 @@ capability "Refresh" capability "Sensor" + attribute "currentIP", "string" + command "subscribe" command "resubscribe" command "unsubscribe" @@ -31,17 +33,30 @@ } // UI tile definitions - tiles { + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "motion", canChangeIcon: true){ + tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" + attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + attributeState "offline", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#ff0000" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } + standardTile("motion", "device.motion", width: 2, height: 2) { state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") - } - standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + state("offline", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#ff0000") } + standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "motion" - details (["motion", "refresh"]) + details (["rich-control", "refresh"]) } } @@ -62,6 +77,7 @@ def parse(String description) { def result = [] def bodyString = msg.body if (bodyString) { + unschedule("setOffline") def body = new XmlSlurper().parseText(bodyString) if (body?.property?.TimeSyncRequest?.text()) { @@ -72,7 +88,7 @@ def parse(String description) { } else if (body?.property?.BinaryState?.text()) { def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive" log.debug "Notify - BinaryState = ${value}" - result << createEvent(name: "motion", value: value) + result << createEvent(name: "motion", value: value, descriptionText: "Motion is ${value}") } else if (body?.property?.TimeZoneNotification?.text()) { log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" } @@ -91,14 +107,6 @@ private getCallBackAddress() { device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - private getHostAddress() { def ip = getDataValue("ip") def port = getDataValue("port") @@ -125,6 +133,8 @@ def refresh() { //////////////////////////// def getStatus() { log.debug "Executing WeMo Motion 'getStatus'" +if (device.currentValue("currentIP") != "Offline") + runIn(10, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 @@ -165,7 +175,9 @@ def subscribe(ip, port) { def existingPort = getDataValue("port") if (ip && ip != existingIp) { log.debug "Updating ip from $existingIp to $ip" - updateDataValue("ip", ip) + updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") } if (port && port != existingPort) { log.debug "Updating port from $existingPort to $port" @@ -226,3 +238,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } + +def setOffline() { + sendEvent(name: "motion", value: "offline", descriptionText: "The device is offline") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} diff --git a/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy index b385ceb..cd9e0ec 100644 --- a/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy +++ b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy @@ -10,120 +10,142 @@ * 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. * - * Wemo Switch + * Wemo Switch * - * Author: superuser - * Date: 2013-10-11 + * Author: Juan Risso (SmartThings) + * Date: 2015-10-11 */ metadata { - definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" - capability "Switch" - capability "Polling" - capability "Refresh" - capability "Sensor" + definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" - command "subscribe" - command "resubscribe" - command "unsubscribe" - } + attribute "currentIP", "string" - // simulator metadata - simulator {} + command "subscribe" + command "resubscribe" + command "unsubscribe" + } - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" - } + // simulator metadata + simulator {} - main "switch" - details (["switch", "refresh"]) - } + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } + + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + state "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000" + } + + standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["rich-control", "refresh"]) + } } // parse events into attributes def parse(String description) { - log.debug "Parsing '${description}'" + log.debug "Parsing '${description}'" - def msg = parseLanMessage(description) - def headerString = msg.header + def msg = parseLanMessage(description) + def headerString = msg.header - if (headerString?.contains("SID: uuid:")) { - def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" - sid -= "SID: uuid:".trim() + if (headerString?.contains("SID: uuid:")) { + def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" + sid -= "SID: uuid:".trim() - updateDataValue("subscriptionId", sid) - } + updateDataValue("subscriptionId", sid) + } - def result = [] - def bodyString = msg.body - if (bodyString) { - def body = new XmlSlurper().parseText(bodyString) - - if (body?.property?.TimeSyncRequest?.text()) { - log.trace "Got TimeSyncRequest" - result << timeSyncResponse() - } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { - log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" - } else if (body?.property?.BinaryState?.text()) { - def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" - log.trace "Notify: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) - } else if (body?.property?.TimeZoneNotification?.text()) { - log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" - } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { - def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" - log.trace "GetBinaryResponse: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) - } - } - - result + def result = [] + def bodyString = msg.body + if (bodyString) { + unschedule("setOffline") + def body = new XmlSlurper().parseText(bodyString) + if (body?.property?.TimeSyncRequest?.text()) { + log.trace "Got TimeSyncRequest" + result << timeSyncResponse() + } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { + log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" + } else if (body?.property?.BinaryState?.text()) { + def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" + log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}" + def dispaux = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux) + } else if (body?.property?.TimeZoneNotification?.text()) { + log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" + } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { + def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" + log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}" + log.info "Connection: ${device.currentValue("connection")}" + if (device.currentValue("currentIP") == "Offline") { + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "IP", value: ipvalue, descriptionText: "IP is ${ipvalue}") + } + def dispaux2 = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux2) + } + } + result } private getTime() { - // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. - ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() + // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. + ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() } private getCallBackAddress() { - device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") + device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) + Integer.parseInt(hex,16) } private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } private getHostAddress() { - def ip = getDataValue("ip") - def port = getDataValue("port") - - if (!ip || !port) { - def parts = device.deviceNetworkId.split(":") - if (parts.length == 2) { - ip = parts[0] - port = parts[1] - } else { - log.warn "Can't figure out ip and port for device: ${device.id}" - } - } - log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" - return convertHexToIP(ip) + ":" + convertHexToInt(port) + def ip = getDataValue("ip") + def port = getDataValue("port") + if (!ip || !port) { + def parts = device.deviceNetworkId.split(":") + if (parts.length == 2) { + ip = parts[0] + port = parts[1] + } else { + log.warn "Can't figure out ip and port for device: ${device.id}" + } + } + log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" + return convertHexToIP(ip) + ":" + convertHexToInt(port) } - def on() { - log.debug "Executing 'on'" - sendEvent(name: "switch", value: "on") +log.debug "Executing 'on'" def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" Host: ${getHostAddress()} @@ -133,17 +155,16 @@ Content-Length: 333 - + 1 - + """, physicalgraph.device.Protocol.LAN) } def off() { - log.debug "Executing 'off'" - sendEvent(name: "switch", value: "off") - def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +log.debug "Executing 'off'" +def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" Host: ${getHostAddress()} Content-Type: text/xml @@ -152,36 +173,13 @@ Content-Length: 333 - + 0 - + """, physicalgraph.device.Protocol.LAN) } -/*def refresh() { - log.debug "Executing 'refresh'" -new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 -SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" -Content-Length: 277 -Content-Type: text/xml; charset="utf-8" -HOST: ${getHostAddress()} -User-Agent: CyberGarage-HTTP/1.0 - - - - - - - -""", physicalgraph.device.Protocol.LAN) -}*/ - -def refresh() { - log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'" - [subscribe(), timeSyncResponse(), poll()] -} - def subscribe(hostAddress) { log.debug "Executing 'subscribe()'" def address = getCallBackAddress() @@ -200,27 +198,30 @@ def subscribe() { subscribe(getHostAddress()) } -def subscribe(ip, port) { - def existingIp = getDataValue("ip") - def existingPort = getDataValue("port") - if (ip && ip != existingIp) { - log.debug "Updating ip from $existingIp to $ip" - updateDataValue("ip", ip) - } - if (port && port != existingPort) { - log.debug "Updating port from $existingPort to $port" - updateDataValue("port", port) - } +def refresh() { + log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'" + [subscribe(), timeSyncResponse(), poll()] +} +def subscribe(ip, port) { + def existingIp = getDataValue("ip") + def existingPort = getDataValue("port") + if (ip && ip != existingIp) { + log.debug "Updating ip from $existingIp to $ip" + updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") + } + if (port && port != existingPort) { + log.debug "Updating port from $existingPort to $port" + updateDataValue("port", port) + } subscribe("${ip}:${port}") } -//////////////////////////// def resubscribe() { -log.debug "Executing 'resubscribe()'" - -def sid = getDeviceDataByName("subscriptionId") - + log.debug "Executing 'resubscribe()'" + def sid = getDeviceDataByName("subscriptionId") new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 HOST: ${getHostAddress()} SID: uuid:${sid} @@ -228,12 +229,11 @@ TIMEOUT: Second-5400 """, physicalgraph.device.Protocol.LAN) - } -//////////////////////////// + def unsubscribe() { -def sid = getDeviceDataByName("subscriptionId") + def sid = getDeviceDataByName("subscriptionId") new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1 HOST: ${getHostAddress()} SID: uuid:${sid} @@ -242,7 +242,7 @@ SID: uuid:${sid} """, physicalgraph.device.Protocol.LAN) } -//////////////////////////// + //TODO: Use UTC Timezone def timeSyncResponse() { log.debug "Executing 'timeSyncResponse()'" @@ -267,9 +267,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } +def setOffline() { + //sendEvent(name: "currentIP", value: "Offline", displayed: false) + sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline") +} def poll() { log.debug "Executing 'poll'" +if (device.currentValue("currentIP") != "Offline") + runIn(10, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 diff --git a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy index 34f20b1..e82e5c7 100644 --- a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy +++ b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy @@ -61,10 +61,7 @@ def firstPage() log.debug "REFRESH COUNT :: ${refreshCount}" - if(!state.subscribe) { - subscribe(location, null, locationHandler, [filterEvents:false]) - state.subscribe = true - } + subscribe(location, null, locationHandler, [filterEvents:false]) //ssdp request every 25 seconds if((refreshCount % 5) == 0) { @@ -168,21 +165,30 @@ def getWemoLightSwitches() def installed() { log.debug "Installed with settings: ${settings}" initialize() - - runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds - runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds - runIn(900, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 15 minutes - - // SUBSCRIBE responses come back with TIMEOUT-1801 (30 minutes), so we refresh things a bit before they expire (29 minutes) - runIn(1740, "refresh", [overwrite: false]) } def updated() { log.debug "Updated with settings: ${settings}" initialize() +} - runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds - runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds +def initialize() { + unsubscribe() + unschedule() + subscribe(location, null, locationHandler, [filterEvents:false]) + + if (selectedSwitches) + addSwitches() + + if (selectedMotions) + addMotions() + + if (selectedLightSwitches) + addLightSwitches() + + runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds + runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds + runEvery5Minutes("refresh") } def resubscribe() { @@ -192,8 +198,7 @@ def resubscribe() { def refresh() { log.debug "refresh() called" - //reschedule the refreshes - runIn(1740, "refresh", [overwrite: false]) + doDeviceSync() refreshDevices() } @@ -236,7 +241,8 @@ def addSwitches() { "port": selectedSwitch.value.port ] ]) - + def ipvalue = convertHexToIP(selectedSwitch.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -266,8 +272,9 @@ def addMotions() { "port": selectedMotion.value.port ] ]) - - log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" + def ipvalue = convertHexToIP(selectedMotion.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") + log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" } else { log.debug "found ${d.displayName} with id $dni already exists" } @@ -296,7 +303,8 @@ def addLightSwitches() { "port": selectedLightSwitch.value.port ] ]) - + def ipvalue = convertHexToIP(selectedLightSwitch.value.ip) + d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}") log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -304,27 +312,6 @@ def addLightSwitches() { } } -def initialize() { - // remove location subscription afterwards - unsubscribe() - state.subscribe = false - - if (selectedSwitches) - { - addSwitches() - } - - if (selectedMotions) - { - addMotions() - } - - if (selectedLightSwitches) - { - addLightSwitches() - } -} - def locationHandler(evt) { def description = evt.description def hub = evt?.hubId @@ -333,53 +320,32 @@ def locationHandler(evt) { log.debug parsedEvent if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) { - def switches = getWemoSwitches() - - if (!(switches."${parsedEvent.ssdpUSN.toString()}")) - { //if it doesn't already exist + if (!(switches."${parsedEvent.ssdpUSN.toString()}")) { + //if it doesn't already exist switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - + } else { log.debug "Device was already found in state..." - def d = switches."${parsedEvent.ssdpUSN.toString()}" boolean deviceChangedValues = false - + log.debug "$d.ip <==> $parsedEvent.ip" if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { d.ip = parsedEvent.ip d.port = parsedEvent.port deviceChangedValues = true log.debug "Device's port or ip changed..." + def child = getChildDevice(parsedEvent.mac) + child.subscribe(parsedEvent.ip, parsedEvent.port) + child.poll() } - - if (deviceChangedValues) { - def children = getChildDevices() - log.debug "Found children ${children}" - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" - it.subscribe(parsedEvent.ip, parsedEvent.port) - } - } - } - } - } else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) { - def motions = getWemoMotions() - - if (!(motions."${parsedEvent.ssdpUSN.toString()}")) - { //if it doesn't already exist + if (!(motions."${parsedEvent.ssdpUSN.toString()}")) { + //if it doesn't already exist motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - + } else { // just update the values log.debug "Device was already found in state..." def d = motions."${parsedEvent.ssdpUSN.toString()}" @@ -412,10 +378,7 @@ def locationHandler(evt) { if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) { //if it doesn't already exist lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - + } else { log.debug "Device was already found in state..." def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}" @@ -426,21 +389,11 @@ def locationHandler(evt) { d.port = parsedEvent.port deviceChangedValues = true log.debug "Device's port or ip changed..." + def child = getChildDevice(parsedEvent.mac) + log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}" + child.subscribe(parsedEvent.ip, parsedEvent.port) } - - if (deviceChangedValues) { - def children = getChildDevices() - log.debug "Found children ${children}" - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" - it.subscribe(parsedEvent.ip, parsedEvent.port) - } - } - } - } - } else if (parsedEvent.headers && parsedEvent.body) { String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase() @@ -580,73 +533,30 @@ private def parseDiscoveryMessage(String description) { } } } - device } def doDeviceSync(){ log.debug "Doing Device Sync!" - runIn(900, "doDeviceSync" , [overwrite: false]) //schedule to run again in 15 minutes - - if(!state.subscribe) { - subscribe(location, null, locationHandler, [filterEvents:false]) - state.subscribe = true - } - discoverAllWemoTypes() } -def pollChildren() { - def devices = getAllChildDevices() - devices.each { d -> - //only poll switches? - d.poll() - } +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } -def delayPoll() { - log.debug "Executing 'delayPoll'" - - runIn(5, "pollChildren") +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) } -/*def poll() { - log.debug "Executing 'poll'" - runIn(600, "poll", [overwrite: false]) //schedule to run again in 10 minutes - - def lastPoll = getLastPollTime() - def currentTime = now() - def lastPollDiff = currentTime - lastPoll - log.debug "lastPoll: $lastPoll, currentTime: $currentTime, lastPollDiff: $lastPollDiff" - setLastPollTime(currentTime) - - doDeviceSync() -} - - -def setLastPollTime(currentTime) { - state.lastpoll = currentTime -} - -def getLastPollTime() { - state.lastpoll ?: now() -} - -def now() { - new Date().getTime() -}*/ - -private Boolean canInstallLabs() -{ +private Boolean canInstallLabs() { return hasAllHubsOver("000.011.00603") } -private Boolean hasAllHubsOver(String desiredFirmware) -{ +private Boolean hasAllHubsOver(String desiredFirmware) { return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } } -private List getRealHubFirmwareVersions() -{ +private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } }