From 8ba5eaf74dbd79ae8b210fed750317133b6429b9 Mon Sep 17 00:00:00 2001 From: Mike Robinet Date: Thu, 24 Sep 2015 15:51:51 -0500 Subject: [PATCH 01/35] CREX-3129 Update parent and service manager apps to be singleton --- .../obything-music-connect.src/obything-music-connect.groovy | 3 ++- smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy | 3 ++- .../juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy | 3 ++- .../weather-underground-pws-connect.groovy | 3 ++- .../bose-soundtouch-connect.src/bose-soundtouch-connect.groovy | 3 ++- smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy | 3 ++- smartapps/smartthings/foscam-connect.src/foscam-connect.groovy | 3 ++- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 3 ++- .../smartthings/life360-connect.src/life360-connect.groovy | 3 ++- smartapps/smartthings/lifx-connect.src/lifx-connect.groovy | 3 ++- .../logitech-harmony-connect.groovy | 3 ++- .../samsung-tv-connect.src/samsung-tv-connect.groovy | 3 ++- smartapps/smartthings/tesla-connect.src/tesla-connect.groovy | 3 ++- smartapps/smartthings/wemo-connect.src/wemo-connect.groovy | 3 ++- smartapps/smartthings/withings.src/withings.groovy | 3 ++- smartapps/smartthings/yoics-connect.src/yoics-connect.groovy | 3 ++- smartapps/wackford/quirky-connect.src/quirky-connect.groovy | 3 ++- .../wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy | 3 ++- 18 files changed, 36 insertions(+), 18 deletions(-) diff --git a/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy b/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy index bad8f99..f6dd374 100644 --- a/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy +++ b/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy @@ -20,7 +20,8 @@ definition( description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.", category: "SmartThings Labs", iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png", - iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png") + iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png", + singleInstance: true) preferences { diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy index 8260360..905cdde 100644 --- a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -22,7 +22,8 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", - oauth: true + oauth: true, + singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" diff --git a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy index 7d9e193..afa767d 100644 --- a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy +++ b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy @@ -13,7 +13,8 @@ definition( iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png", oauth: true, - usePreferencesForAuthorization: false + usePreferencesForAuthorization: false, + singleInstance: true ) { appSetting "clientId" appSetting "clientSecret" diff --git a/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy b/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy index 620911b..21ecb1d 100644 --- a/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy +++ b/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy @@ -26,7 +26,8 @@ definition( iconUrl: "http://i.imgur.com/HU0ANBp.png", iconX2Url: "http://i.imgur.com/HU0ANBp.png", iconX3Url: "http://i.imgur.com/HU0ANBp.png", - oauth: true) + oauth: true, + singleInstance: true) preferences { diff --git a/smartapps/smartthings/bose-soundtouch-connect.src/bose-soundtouch-connect.groovy b/smartapps/smartthings/bose-soundtouch-connect.src/bose-soundtouch-connect.groovy index bc7cc82..bcb5d8f 100644 --- a/smartapps/smartthings/bose-soundtouch-connect.src/bose-soundtouch-connect.groovy +++ b/smartapps/smartthings/bose-soundtouch-connect.src/bose-soundtouch-connect.groovy @@ -21,7 +21,8 @@ category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", - iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 12b703d..8b7a02e 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -26,7 +26,8 @@ definition( description: "Connect your Ecobee thermostat to SmartThings.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", + singleInstance: true ) { appSetting "clientId" appSetting "serverUrl" diff --git a/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy b/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy index acf3ffb..51ec6fa 100644 --- a/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy +++ b/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy @@ -23,7 +23,8 @@ definition( description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.", category: "SmartThings Internal", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 9833438..64cafd0 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -23,7 +23,8 @@ definition( description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/life360-connect.src/life360-connect.groovy b/smartapps/smartthings/life360-connect.src/life360-connect.groovy index eabf545..ab98fa0 100644 --- a/smartapps/smartthings/life360-connect.src/life360-connect.groovy +++ b/smartapps/smartthings/life360-connect.src/life360-connect.groovy @@ -22,7 +22,8 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png", - oauth: [displayName: "Life360", displayLink: "Life360"] + oauth: [displayName: "Life360", displayLink: "Life360"], + singleInstance: true ) { appSetting "clientId" appSetting "clientSecret" diff --git a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy index 645e5ac..dea5939 100644 --- a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy +++ b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy @@ -13,7 +13,8 @@ definition( iconUrl: "https://cloud.lifx.com/images/lifx.png", iconX2Url: "https://cloud.lifx.com/images/lifx.png", iconX3Url: "https://cloud.lifx.com/images/lifx.png", - oauth: true) { + oauth: true, + singleInstance: true) { appSetting "clientId" appSetting "clientSecret" } diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index aefd723..0816d03 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -43,7 +43,8 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", - oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"] + oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"], + singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" diff --git a/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy b/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy index aa6a228..6a15dc9 100644 --- a/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy +++ b/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy @@ -22,7 +22,8 @@ definition( description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy b/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy index 87b9586..5022c42 100644 --- a/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy +++ b/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy @@ -23,7 +23,8 @@ definition( description: "Integrate your Tesla car with SmartThings.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy index 34f20b1..d79ae63 100644 --- a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy +++ b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy @@ -22,7 +22,8 @@ definition( description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/withings.src/withings.groovy b/smartapps/smartthings/withings.src/withings.groovy index 872e653..24cfcd3 100644 --- a/smartapps/smartthings/withings.src/withings.groovy +++ b/smartapps/smartthings/withings.src/withings.groovy @@ -24,7 +24,8 @@ definition( category: "Connections", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png", - oauth: true + oauth: true, + singleInstance: true ) { appSetting "clientId" appSetting "clientSecret" diff --git a/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy b/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy index aebd0b5..cd18e88 100644 --- a/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy +++ b/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy @@ -24,7 +24,8 @@ definition( category: "SmartThings Internal", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", - oauth: true + oauth: true, + singleInstance: true ) { appSetting "serverUrl" } diff --git a/smartapps/wackford/quirky-connect.src/quirky-connect.groovy b/smartapps/wackford/quirky-connect.src/quirky-connect.groovy index 325f57d..30f5b55 100644 --- a/smartapps/wackford/quirky-connect.src/quirky-connect.groovy +++ b/smartapps/wackford/quirky-connect.src/quirky-connect.groovy @@ -58,7 +58,8 @@ definition( description: "Connect your Quirky to SmartThings.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png", + singleInstance: true ) { appSetting "clientId" appSetting "clientSecret" diff --git a/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy b/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy index 9f0e5a7..69b6e4c 100644 --- a/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy +++ b/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy @@ -25,7 +25,8 @@ definition( description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png", + singleInstance: true ) From cd81871f90da2ce9529add8f7222620d50ede0a0 Mon Sep 17 00:00:00 2001 From: Matt Nohr Date: Wed, 14 Oct 2015 12:00:26 -0500 Subject: [PATCH 02/35] Update the background image for arrival devices --- .../smartthings/arrival-sensor.src/arrival-sensor.groovy | 2 +- .../smartthings/mobile-presence.src/mobile-presence.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy b/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy index 19704c9..a8703fc 100644 --- a/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy +++ b/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy @@ -43,7 +43,7 @@ metadata { tiles { standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0" - state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff" + state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ebeef2" } standardTile("beep", "device.beep", decoration: "flat") { state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff" diff --git a/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy index 99746f2..34bb78a 100644 --- a/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy +++ b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy @@ -25,7 +25,7 @@ metadata { tiles { standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0") - state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff") + state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2") } main "presence" details "presence" From ecb975540b9714c0cfd01390b418d0a3a32c457f Mon Sep 17 00:00:00 2001 From: tslagle13 Date: Wed, 14 Oct 2015 10:05:02 -0700 Subject: [PATCH 03/35] 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 04/35] 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 4d5bf094aa3e5d6a3318832f5706f1b8b02a810e Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Wed, 14 Oct 2015 18:54:57 -0700 Subject: [PATCH 05/35] Updates based on the cloud methods --- .../zigbee-switch-power.groovy | 101 ++++++++++++++++++ .../zigbee-switch.src/zigbee-switch.groovy | 89 +++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy create mode 100644 devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy diff --git a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy new file mode 100644 index 0000000..04f7b6c --- /dev/null +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -0,0 +1,101 @@ +/** + * 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. + * + */ + +metadata { + definition (name: "ZigBee Switch Power", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Power Meter" + capability "Sensor" + capability "Switch" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + 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.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'${currentValue} W' + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def resultMap = zigbee.getKnownDescription(description) + if (resultMap) { + log.info resultMap + if (resultMap.type == "update") { + log.info "$device updates: ${resultMap.value}" + } + else if (resultMap.type == "power") { + def powerValue + if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power + powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(name: "power", value: powerValue) + } + } + else { + sendEvent(name: resultMap.type, value: resultMap.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 refresh() { + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() +} diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy new file mode 100644 index 0000000..24de9cb --- /dev/null +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -0,0 +1,89 @@ +/** + * 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. + * + */ + +metadata { + definition (name: "ZigBee Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Switch" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + 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" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def resultMap = zigbee.getKnownDescription(description) + if (resultMap) { + log.info resultMap + if (resultMap.type == "update") { + log.info "$device updates: ${resultMap.value}" + } + else { + sendEvent(name: resultMap.type, value: resultMap.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 refresh() { + zigbee.onOffRefresh() + zigbee.onOffConfig() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + zigbee.onOffConfig() + zigbee.onOffRefresh() +} From 045a024bca4df7b1bedd9e992d256e3c219cb2dc Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 15 Oct 2015 14:07:18 -0700 Subject: [PATCH 06/35] removing the simulator from the power DTH --- .../zigbee-switch-power.groovy | 11 ----------- 1 file changed, 11 deletions(-) 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 04f7b6c..fddba41 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -25,17 +25,6 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702" } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { From 4ef1d12c6164fd2d6f8ca663d414135ff4fa1e50 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Fri, 2 Oct 2015 16:01:57 -0700 Subject: [PATCH 07/35] updated config with updated requirements --- .../zigbee-dimmer-power.groovy | 98 +++++++++++ .../zigbee-dimmer.src/zigbee-dimmer.groovy | 162 ++++++------------ 2 files changed, 151 insertions(+), 109 deletions(-) create mode 100644 devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy new file mode 100644 index 0000000..0382e0c --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -0,0 +1,98 @@ +/** + * 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. + * + */ + +metadata { + definition (name: "ZigBee Dimmer Power", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Power Meter" + capability "Sensor" + capability "Switch" + capability "Switch Level" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702" + } + + 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.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'${currentValue} W' + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def resultMap = zigbee.getKnownDescription(description) + if (resultMap) { + log.info resultMap + if (resultMap.type == "update") { + log.info "$device updates: ${resultMap.value}" + } + else if (resultMap.type == "power") { + def powerValue + if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power + powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(name: "power", value: powerValue) + } + } + else { + sendEvent(name: resultMap.type, value: resultMap.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.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() +} diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 07dbc37..588a177 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -11,133 +11,77 @@ * for the specific language governing permissions and limitations under the License. * */ + metadata { - definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") { - capability "Switch Level" - capability "Actuator" - capability "Switch" - capability "Configuration" - capability "Sensor" - capability "Refresh" + definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Switch Level" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019" - } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008" + } - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - - 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.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - } - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"switch level.setLevel" - } - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - 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", "level", "levelSliderControl"]) - } + 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" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "refresh"]) + } } // Parse incoming device messages to generate events def parse(String description) { - log.info description - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - log.trace msg - log.trace "data: $msg.data" - } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result - } -} + log.debug "description is $description" -// Commands to device -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" + def resultMap = zigbee.getKnownDescription(description) + if (resultMap) { + log.info resultMap + if (resultMap.type == "update") { + log.info "$device updates: ${resultMap.value}" + } + else { + sendEvent(name: resultMap.type, value: resultMap.value) + } + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } } def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + zigbee.off() } + +def on() { + zigbee.on() +} + def setLevel(value) { - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - - } - - sendEvent(name: "level", value: value) - def level = hexString(Math.round(value * 255/100)) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" - - //log.debug cmds - cmds + zigbee.setLevel(value) } def refresh() { - [ - "st wattr 0x${device.deviceNetworkId} 1 6 0", "delay 200", - "st wattr 0x${device.deviceNetworkId} 1 8 0" - ] + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() } def configure() { - - /*log.debug "binding to switch and level control cluster" - [ - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", - "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}" - ] - */ - - //set transition time to 2 seconds. Not currently working. - "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}" -} - - - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() + log.debug "Configuring Reporting and Bindings." + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() } From f6791d1744f4a1f77116db5cff71e66a968d850c Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Wed, 23 Sep 2015 12:19:56 -0700 Subject: [PATCH 08/35] Fibaro Smoke Sensor: initial device type Fibaro Smoke Sensor: remove CO capability, set default temp report interval, check MSR --- .../fibaro-smoke-sensor.groovy | 450 ++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy diff --git a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy new file mode 100644 index 0000000..882177c --- /dev/null +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy @@ -0,0 +1,450 @@ +/** + * 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. + * + */ +metadata { + definition (name: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Battery" //attributes: battery + capability "Configuration" //commands: configure() + capability "Sensor" + capability "Smoke Detector" //attributes: smoke ("detected","clear","tested") + capability "Temperature Measurement" //attributes: temperature + attribute "tamper", "enum", ["detected", "clear"] + attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"] + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B" + } + simulator { + //battery + for (int i in [0, 5, 10, 15, 50, 99, 100]) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i) + ).incomingMessage() + } + status "battery 100%": "command: 8003, payload: 64" + status "battery 5%": "command: 8003, payload: 05" + //smoke + status "smoke detected": "command: 7105, payload: 01 01" + status "smoke clear": "command: 7105, payload: 01 00" + status "smoke tested": "command: 7105, payload: 01 03" + //temperature + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1) + ).incomingMessage() + } + } + preferences { + input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration", + title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph" + input description: "Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN", + title: "To check smoke detection state", displayDuringSetup: true, type: "paragraph", element: "paragraph" + input description: "Please consult Fibaro Smoke Sensor 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 "smokeSensorSensitivity", "enum", title: "Smoke Sensor Sensitivity", options: ["High","Medium","Low"], defaultValue: "${smokeSensorSensitivity}", displayDuringSetup: true + input "zwaveNotificationStatus", "enum", title: "Notifications Status", options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], + defaultValue: "${zwaveNotificationStatus}", displayDuringSetup: true + input "visualIndicatorNotificationStatus", "enum", title: "Visual Indicator Notifications Status", + options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], + defaultValue: "${visualIndicatorNotificationStatus}", displayDuringSetup: true + input "soundNotificationStatus", "enum", title: "Sound Notifications Status", + options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], + defaultValue: "${soundNotificationStatus}", displayDuringSetup: true + input "temperatureReportInterval", "enum", title: "Temperature Report Interval", + options: ["reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${temperatureReportInterval}", displayDuringSetup: true + input "temperatureReportHysteresis", "number", title: "Temperature Report Hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true + input "temperatureThreshold", "number", title: "Overheat Temperature Threshold", description: "Available settings: 0 or 2-100 C", range: "0..100", displayDuringSetup: true + input "excessTemperatureSignalingInterval", "enum", title: "Excess Temperature Signaling Interval", + options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${excessTemperatureSignalingInterval}", displayDuringSetup: true + input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave Range Indication Interval", + options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${lackOfZwaveRangeIndicationInterval}", displayDuringSetup: true + } + tiles (scale: 2){ + multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){ + tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66") + attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff") + } + tileAttribute ("device.battery", key: "SECONDARY_CONTROL") { + attributeState "battery", label:'Battery: ${currentValue}%', unit:"%" + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"%" + } + valueTile("temperature", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "temperature", label:'${currentValue}°', unit:"C" + } + valueTile("heatAlarm", "device.heatAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "clear", label:'TEMPERATURE OK', backgroundColor:"#ffffff" + state "overheat detected", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff" + state "rapid temperature rise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff" + state "underheat detected", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff" + } + valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "clear", label:'NO TAMPER', backgroundColor:"#ffffff" + state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff" + } + + main "smoke" + details(["smoke","temperature"]) + } +} + +def updated() { + log.debug "Updated with settings: ${settings}" + setConfigured("false") //wait until the next time device wakeup to send configure command +} + +def parse(String description) { + log.debug "parse() >> description: $description" + def result = null + if (description.startsWith("Err 106")) { + 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.") + } else if (description != "updated") { + log.debug "parse() >> zwave.parse(description)" + def cmd = zwave.parse(description, [0x31: 5, 0x71: 3, 0x84: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd" + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: false) +} + + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + setConfigured("true") //when battery is reported back meaning configuration is done + //Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler + state.lastbatt = now() + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + def msg = cmd.status == 0 ? "try again later" : + cmd.status == 1 ? "try again in $cmd.waitTime seconds" : + cmd.status == 2 ? "request queued" : "sorry" + createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + setSecured() + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1]) + if (encapsulatedCommand) { + log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" + setSecured() + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + if (getDataValue("MSR").startsWith("010F-0C02")){ + response(configure()) //configure device using SmartThings default settings + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)" + createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true) + //after device securely joined the network, call configure() to config device + setSecured() + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + if (getDataValue("MSR").startsWith("010F-0C02")){ + response(configure()) //configure device using SmartThings default settings + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd" + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + result << createEvent(name: "tamper", value: "clear", displayed: false) + break + case 3: + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName casing was opened") + break + } + } else if (cmd.notificationType == 1) { //Smoke Alarm (V2) + log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)" + result << smokeAlarmEvent(cmd.event) + } else if (cmd.notificationType == 4) { // Heat Alarm (V2) + log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)" + result << heatAlarmEvent(cmd.event) + } else { + log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def smokeAlarmEvent(value) { + log.debug "smokeAlarmEvent(value): $value" + def map = [name: "smoke"] + if (value == 1 || value == 2) { + map.value = "detected" + map.descriptionText = "$device.displayName detected smoke" + } else if (value == 0) { + map.value = "clear" + map.descriptionText = "$device.displayName is clear (no smoke)" + } else if (value == 3) { + map.value = "tested" + map.descriptionText = "$device.displayName smoke alarm test" + } else if (value == 4) { + map.value = "replacement required" + map.descriptionText = "$device.displayName replacement required" + } else { + map.value = "unknown" + map.descriptionText = "$device.displayName unknown event" + } + createEvent(map) +} + +def heatAlarmEvent(value) { + log.debug "heatAlarmEvent(value): $value" + def map = [name: "heatAlarm"] + if (value == 1 || value == 2) { + map.value = "overheat detected" + map.descriptionText = "$device.displayName overheat detected" + } else if (value == 0) { + map.value = "clear" + map.descriptionText = "$device.displayName heat alarm cleared (no overheat)" + } else if (value == 3 || value == 4) { + map.value = "rapid temperature rise" + map.descriptionText = "$device.displayName rapid temperature rise" + } else if (value == 5 || value == 6) { + map.value = "underheat detected" + map.descriptionText = "$device.displayName underheat detected" + } else { + map.value = "unknown" + map.descriptionText = "$device.displayName unknown event" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd" + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + /* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */ + if (!isConfigured() && getDataValue("MSR").startsWith("010F-0C02")) { + result += response(configure()) // configure a newly joined device or joined device with preference update + } else { + //Only ask for battery if we havn'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()") + result += response(zwave.batteryV1.batteryGet()) + result += response("delay 1200") // leave time for device to respond to batteryGet + } + log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") + result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) //tell device back to sleep + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd" + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + break + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd" + createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true) +} + +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 result = [] + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + def result = [] + if (cmd.nodeId.any { it == zwaveHubNodeId }) { + result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}") + } else if (cmd.groupingIdentifier == 1) { + result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}") + result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId)) + } + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "General zwaveEvent cmd: ${cmd}" + createEvent(descriptionText: cmd.toString(), isStateChange: false) +} + +def configure() { +// This sensor joins as a secure device if you tripple-click the button to include it + log.debug "configure() >> isSecured() : ${isSecured()}" + if (!isSecured()) { + log.debug "Fibaro smoke sensor not sending configure until secure" + return [] + } else { + log.info "${device.displayName} is configuring its settings" + def request = [] + + //1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr) + request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId) + + //2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW + if (smokeSensorSensitivity && smokeSensorSensitivity != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, + scaledConfigurationValue: + smokeSensorSensitivity == "High" ? 1 : + smokeSensorSensitivity == "Medium" ? 2 : + smokeSensorSensitivity == "Low" ? 3 : 2) + } + //3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable + if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){ + request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0) + } + //4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification + if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0) + } + //5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification + if (soundNotificationStatus && soundNotificationStatus != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0) + } + //6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) + if (temperatureReportInterval && temperatureReportInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180) + } else { //send SmartThings default configuration + request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180) + } + //7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C) + if (temperatureReportHysteresis && temperatureReportHysteresis != null) { + request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis) + } + //8. Temperature threshold: 1-100 (C), default 55 (C) + if (temperatureThreshold && temperatureThreshold != null) { + request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold) + } + //9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) + if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180) + } else { //send SmartThings default configuration + request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180) + } + //10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours) + if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160) + } else { + request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160) + } + //11. get battery level when device is paired + request += zwave.batteryV1.batteryGet() + + //12. get temperature reading from device + request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) + + commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + } +} + +private def getTimeOptionValueMap() { [ + "5 minutes" : 30, + "15 minutes" : 90, + "30 minutes" : 180, + "1 hour" : 360, + "6 hours" : 2160, + "12 hours" : 4320, + "18 hours" : 6480, + "24 hours" : 8640, + "reports inactive" : 0, +]} + +private def getNotificationOptionValueMap() { [ + "disabled" : 0, + "casing opened" : 1, + "exceeding temperature threshold" : 2, + "lack of Z-Wave range" : 4, + "all notifications" : 7, +]} + +private command(physicalgraph.zwave.Command cmd) { + if (isSecured()) { + log.info "Sending secured command: ${cmd}" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + log.info "Sending unsecured command: ${cmd}" + cmd.format() + } +} + +private commands(commands, delay=200) { + log.info "inside commands: ${commands}" + delayBetween(commands.collect{ command(it) }, delay) +} + +private setConfigured(configure) { + updateDataValue("configured", configure) +} +private isConfigured() { + getDataValue("configured") == "true" +} +private setSecured() { + updateDataValue("secured", "true") +} +private isSecured() { + getDataValue("secured") == "true" +} From 9ff67e9e1754ef8c834438ce7b306b1753100f84 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Thu, 8 Oct 2015 15:29:27 -0700 Subject: [PATCH 09/35] Fibaro Smoke Sensor: send commands to update tiles during joining process, add save nav operator to MSR --- .../fibaro-smoke-sensor.groovy | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy index 882177c..41d495f 100644 --- a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy @@ -177,7 +177,7 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupported log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" setSecured() log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" - if (getDataValue("MSR").startsWith("010F-0C02")){ + if (getDataValue("MSR")?.startsWith("010F-0C02")){ response(configure()) //configure device using SmartThings default settings } } @@ -188,7 +188,7 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { //after device securely joined the network, call configure() to config device setSecured() log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" - if (getDataValue("MSR").startsWith("010F-0C02")){ + if (getDataValue("MSR")?.startsWith("010F-0C02")){ response(configure()) //configure device using SmartThings default settings } } @@ -267,17 +267,17 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] /* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */ - if (!isConfigured() && getDataValue("MSR").startsWith("010F-0C02")) { - result += response(configure()) // configure a newly joined device or joined device with preference update + if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) { + result << response(configure()) // configure a newly joined device or joined device with preference update } else { //Only ask for battery if we havn'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()") - result += response(zwave.batteryV1.batteryGet()) - result += response("delay 1200") // leave time for device to respond to batteryGet + result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) + result << response("delay 1200") // leave time for device to respond to batteryGet } log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") - result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) //tell device back to sleep + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) //tell device back to sleep } result } @@ -312,8 +312,11 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS def result = [] def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) updateDataValue("MSR", msr) - result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) - result + log.debug "After device is securely joined, send commands to update tiles" + result << zwave.batteryV1.batteryGet() + result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) + result << zwave.wakeUpV1.wakeUpNoMoreInformation() + [[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))] } def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { From 72899ee036751708909bb44c1bbbfbc189831ae0 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Fri, 16 Oct 2015 12:18:43 -0700 Subject: [PATCH 10/35] Fibaro Smoke Sensor: fix request to get batteryreport after device is configured --- .../fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy index 41d495f..4b1ef56 100644 --- a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy @@ -266,18 +266,20 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd" log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + def cmds = [] /* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */ if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) { result << response(configure()) // configure a newly joined device or joined device with preference update } else { - //Only ask for battery if we havn't had a BatteryReport in a while + //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()") - result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) - result << response("delay 1200") // leave time for device to respond to batteryGet + cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format() + cmds << "delay 1200" } log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") - result << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) //tell device back to sleep + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + result << response(cmds) //tell device back to sleep } result } From 66dbc02274bd2c0ec166b992ad13a8f4f916882d Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Fri, 16 Oct 2015 13:45:15 -0500 Subject: [PATCH 11/35] Added fingerprint for Yale YRL220 lock to ZigBee Lock DTH [DVCSMP-101] --- devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index 5205fb9..20384e2 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -31,7 +31,8 @@ manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset 10-Button Deadbolt" fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt" - + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", + manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale YRL220 Lock" } tiles(scale: 2) { From ff39270ba48b80abbff1f1374513b0fd3a094235 Mon Sep 17 00:00:00 2001 From: bflorian Date: Fri, 16 Oct 2015 17:21:59 -0700 Subject: [PATCH 12/35] Removed accidentally added device type --- .../switch-too.src/switch-too.groovy | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 devicetypes/superuser/switch-too.src/switch-too.groovy diff --git a/devicetypes/superuser/switch-too.src/switch-too.groovy b/devicetypes/superuser/switch-too.src/switch-too.groovy deleted file mode 100644 index 03ac829..0000000 --- a/devicetypes/superuser/switch-too.src/switch-too.groovy +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Switch Too - * - * Copyright 2015 Bob Florian - * - * 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: "Switch Too", author: "Bob Florian") { - capability "Switch" - } - - simulator { - // TODO: define status and reply messages here - } - - tiles { - // TODO: define your main and details tiles here - } -} - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" - // TODO: handle 'switch' attribute - -} - -// handle commands -def on() { - log.debug "Executing 'on'" - // TODO: handle 'on' command -} - -def off() { - log.debug "Executing 'off'" - // TODO: handle 'off' command -} - - From e217805d981e5ffac90ec7d4c5fdcb091ea95f5e Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Tue, 22 Sep 2015 23:56:17 -0700 Subject: [PATCH 13/35] 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 14/35] 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 15/35] 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 cbd5c91d5224ff388f90443dfda7e21850d80e76 Mon Sep 17 00:00:00 2001 From: Mike Cousins Date: Thu, 22 Oct 2015 13:22:53 -0400 Subject: [PATCH 16/35] update keen home smart vent device handler --- .../keen-home-smart-vent.groovy | 195 +++++++++--------- 1 file changed, 101 insertions(+), 94 deletions(-) diff --git a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy index c2e0556..232378a 100644 --- a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy +++ b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy @@ -1,12 +1,9 @@ -/** - * Keen Home Smart Vent - * - * Author: Keen Home - * Date: 2015-06-23 - */ +// keen home smart vent +// http://www.keenhome.io +// SmartThings Device Handler v1.0.0 metadata { - definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") { + definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") { capability "Switch Level" capability "Switch" capability "Configuration" @@ -21,6 +18,7 @@ metadata { command "getBattery" command "getTemperature" command "setZigBeeIdTile" + command "clearObstruction" fingerprint endpoint: "1", profileId: "0104", @@ -42,9 +40,10 @@ metadata { // UI tile definitions tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0" - state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff" - state "obstructed", action: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000" + state "on", action: "switch.off", icon: "st.vents.vent-open-text", backgroundColor: "#53a7c0" + state "off", action: "switch.on", icon: "st.vents.vent-closed", backgroundColor: "#ffffff" + state "obstructed", action: "clearObstruction", icon: "st.vents.vent-closed", backgroundColor: "#ff0000" + state "clearing", action: "", icon: "st.vents.vent-closed", backgroundColor: "#ffff33" } controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { state "level", action:"switch level.setLevel" @@ -206,12 +205,12 @@ private Map makeOnOffResult(rawValue) { private Map makeLevelResult(rawValue) { def linkText = getLinkText(device) - // log.debug "rawValue: ${rawValue}" def value = Integer.parseInt(rawValue, 16) def rangeMax = 254 + // catch obstruction level if (value == 255) { - log.debug "obstructed" + log.debug "${linkText} is obstructed" // Just return here. Once the vent is power cycled // it will go back to the previous level before obstruction. // Therefore, no need to update level on the display. @@ -220,24 +219,9 @@ private Map makeLevelResult(rawValue) { value: "obstructed", descriptionText: "${linkText} is obstructed. Please power cycle." ] - } else if ( device.currentValue("switch") == "obstructed" && - value == 254) { - // When the device is reset after an obstruction, the switch - // state will be obstructed and the value coming from the device - // will be 254. Since we're not using heating/cooling mode from - // the device type handler, we need to bump it down to the lower - // (cooling) range - sendEvent(makeOnOffResult(1)) // clear the obstructed switch state - value = rangeMax } - // else if (device.currentValue("switch") == "off") { - // sendEvent(makeOnOffResult(1)) // turn back on if in off state - // } - - // log.debug "pre-value: ${value}" value = Math.floor(value / rangeMax * 100) - // log.debug "post-value: ${value}" return [ name: "level", @@ -327,35 +311,79 @@ private def makeSerialResult(serial) { value: serial, descriptionText: "${linkText} has serial ${serial}" ] } -/**** COMMAND METHODS ****/ -// def mfgCode() { -// ["zcl mfg-code 0x115B", "delay 200"] -// } +// takes a level from 0 to 100 and translates it to a ZigBee move to level with on/off command +private def makeLevelCommand(level) { + def rangeMax = 254 + def scaledLevel = Math.round(level * rangeMax / 100) + log.debug "scaled level for ${level}%: ${scaledLevel}" + + // convert to hex string and pad to two digits + def hexLevel = new BigInteger(scaledLevel.toString()).toString(16).padLeft(2, '0') + + "st cmd 0x${device.deviceNetworkId} 1 8 4 {${hexLevel} 0000}" +} + +/**** COMMAND METHODS ****/ def on() { - log.debug "on()" + def linkText = getLinkText(device) + log.debug "open ${linkText}" + + // only change the state if the vent is not obstructed + if (device.currentValue("switch") == "obstructed") { + log.error("cannot open because ${linkText} is obstructed") + return + } + sendEvent(makeOnOffResult(1)) "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" } def off() { - log.debug "off()" + def linkText = getLinkText(device) + log.debug "close ${linkText}" + + // only change the state if the vent is not obstructed + if (device.currentValue("switch") == "obstructed") { + log.error("cannot close because ${linkText} is obstructed") + return + } + sendEvent(makeOnOffResult(0)) "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" } -// does this work? -def toggle() { - log.debug "toggle()" +def clearObstruction() { + def linkText = getLinkText(device) + log.debug "attempting to clear ${linkText} obstruction" - "st cmd 0x${device.deviceNetworkId} 1 6 2 {}" + sendEvent([ + name: "switch", + value: "clearing", + descriptionText: "${linkText} is clearing obstruction" + ]) + + // send a move command to ensure level attribute gets reset for old, buggy firmware + // then send a reset to factory defaults + // finally re-configure to ensure reports and binding is still properly set after the rtfd + [ + makeLevelCommand(device.currentValue("level")), "delay 500", + "st cmd 0x${device.deviceNetworkId} 1 0 0 {}", "delay 5000" + ] + configure() } def setLevel(value) { log.debug "setting level: ${value}" - def linkText = getLinkText(device) + // only change the level if the vent is not obstructed + def currentState = device.currentValue("switch") + + if (currentState == "obstructed") { + log.error("cannot set level because ${linkText} is obstructed") + return + } + sendEvent(name: "level", value: value) if (value > 0) { sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level") @@ -363,29 +391,26 @@ def setLevel(value) { else { sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0") } - def rangeMax = 254 - def computedLevel = Math.round(value * rangeMax / 100) - log.debug "computedLevel: ${computedLevel}" - def level = new BigInteger(computedLevel.toString()).toString(16) - log.debug "level: ${level}" - - if (level.size() < 2){ - level = '0' + level - } - - "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}" + makeLevelCommand(value) } - def getOnOff() { log.debug "getOnOff()" + // disallow on/off updates while vent is obstructed + if (device.currentValue("switch") == "obstructed") { + log.error("cannot update open/close status because ${getLinkText(device)} is obstructed") + return [] + } + ["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"] } def getPressure() { log.debug "getPressure()" + + // using a Keen Home specific attribute in the pressure measurement cluster [ "zcl mfg-code 0x115B", "delay 200", "zcl global read 0x0403 0x20", "delay 200", @@ -395,12 +420,13 @@ def getPressure() { def getLevel() { log.debug "getLevel()" - // rattr = read attribute - // 0x${} = device net id - // 1 = endpoint - // 8 = cluster id (level control, in this case) - // 0 = attribute within cluster - // sendEvent(name: "level", value: value) + + // disallow level updates while vent is obstructed + if (device.currentValue("switch") == "obstructed") { + log.error("cannot update level status because ${getLinkText(device)} is obstructed") + return [] + } + ["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"] } @@ -425,78 +451,59 @@ def setZigBeeIdTile() { name: "zigbeeId", value: device.zigbeeId, descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]) - return [ + return [ name: "zigbeeId", value: device.zigbeeId, descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ] } def refresh() { - getOnOff() + + getOnOff() + getLevel() + getTemperature() + getPressure() + getBattery() } -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - def configure() { log.debug "CONFIGURE" - log.debug "zigbeeId: ${device.hub.zigbeeId}" + // get ZigBee ID by hidden tile because that's the only way we can do it setZigBeeIdTile() def configCmds = [ - // binding commands + // bind reporting clusters to hub "zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500", - - // configure report commands - // [cluster] [attr] [type] [min-interval] [max-interval] [min-change] + "zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500" - // mike 2015/06/22: preconfigured; see tech spec + // configure report commands + // zcl global send-me-a-report [cluster] [attr] [type] [min-interval] [max-interval] [min-change] + + // report with these parameters is preconfigured in firmware, can be overridden here // vent on/off state - type: boolean, change: 1 // "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200", // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - // mike 2015/06/22: preconfigured; see tech spec + // report with these parameters is preconfigured in firmware, can be overridden here // vent level - type: int8u, change: 1 // "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200", // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - // mike 2015/06/22: temp and pressure reports are preconfigured, but - // we'd like to override their settings for our own purposes + // report with these parameters is preconfigured in firmware, can be overridden here // temperature - type: int16s, change: 0xA = 10 = 0.1C - "zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + // "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200", + // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - // mike 2015/06/22: use new custom pressure attribute - // pressure - type: int32u, change: 1 = 0.1Pa - "zcl mfg-code 0x115B", "delay 200", - "zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500" + // report with these parameters is preconfigured in firmware, can be overridden here + // keen home custom pressure (tenths of Pascals) - type: int32u, change: 1 = 0.1Pa + // "zcl mfg-code 0x115B", "delay 200", + // "zcl global send-me-a-report 0x0403 0x20 0x22 60 60 {010000}", "delay 200", + // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - // mike 2015/06/22: preconfigured; see tech spec + // report with these parameters is preconfigured in firmware, can be overridden here // battery - type: int8u, change: 1 // "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200", // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", From 47210ca8b492d09a17cf36c7f0dc468dc02c89e1 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Wed, 28 Oct 2015 14:12:35 -0400 Subject: [PATCH 17/35] 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 18/35] 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 } } From 2d22b5a384a7e747e45dd6b9b48076100930927f Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Mon, 2 Nov 2015 09:51:03 -0600 Subject: [PATCH 19/35] Fix 'Low Battery Handler' exception caused by non-integer battery events ZigBee locks report battery percentage remaining in .5% increments. However the Low Battery Handler Smart App in Hello Home expects it to be an integer. --- devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index 20384e2..0e46ebf 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -139,8 +139,7 @@ private Map parseReportAttributeMessage(String description) { Map resultMap = [:] if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { resultMap.name = "battery" - // BatteryPercentageRemaining is specified in .5% increments - resultMap.value = Integer.parseInt(descMap.value, 16) / 2 + resultMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}" } else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) { From f337e8a085ef86ce73d720bf91e12046ddd8237c Mon Sep 17 00:00:00 2001 From: bflorian Date: Tue, 3 Nov 2015 17:55:11 -0800 Subject: [PATCH 20/35] Corrected filename of Z-Wave Device Multichannel --- .../zwave-device-multichannel.groovy} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename devicetypes/smartthings/{zwave-device-mc.src/zwave-device-mc.groovy => zwave-device-multichannel.src/zwave-device-multichannel.groovy} (100%) diff --git a/devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy b/devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy similarity index 100% rename from devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy rename to devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy From d830c1fae084b6ac8ccecb4c9a068c08b6f75b5c Mon Sep 17 00:00:00 2001 From: bflorian Date: Tue, 3 Nov 2015 18:08:51 -0800 Subject: [PATCH 21/35] Filename corrections --- .../aeon-led-bulb.groovy} | 0 .../simulated-color-control.src/simulated-color-control.groovy | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename devicetypes/smartthings/{aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy => aeon-led-bulb.src/aeon-led-bulb.groovy} (100%) diff --git a/devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy b/devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy similarity index 100% rename from devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy rename to devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy diff --git a/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy b/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy index 5f33a03..f32fa5c 100644 --- a/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy +++ b/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy @@ -1,5 +1,5 @@ metadata { - definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") { + definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") { capability "Color Control" } From bbc680746e0925471cff9e41be861ff4b64e26a4 Mon Sep 17 00:00:00 2001 From: juano2310 Date: Wed, 4 Nov 2015 13:05:49 -0500 Subject: [PATCH 22/35] Jawbone Global Oauth --- .../jawbone-up-connect.groovy | 171 +++++++++--------- 1 file changed, 82 insertions(+), 89 deletions(-) diff --git a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy index 7d9e193..1b16045 100644 --- a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy +++ b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy @@ -17,7 +17,6 @@ definition( ) { appSetting "clientId" appSetting "clientSecret" - appSetting "serverUrl" } preferences { @@ -28,16 +27,16 @@ mappings { path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} path("/oauth/callback") { action: [ GET: "callback" ] } } -def getSmartThingsClientId() { - return appSettings.clientId -} - -def getSmartThingsClientSecret() { - return appSettings.clientSecret -} +def getSmartThingsClientId() { return appSettings.clientId } +def getSmartThingsClientSecret() { return appSettings.clientSecret } +def getServerUrl() { return "https://graph.api.smartthings.com" } +def getShardUrl() { return getApiServerUrl() } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" } +def buildRedirectUrl(page) { return buildActionUrl(page) } def callback() { def redirectUrl = null @@ -47,7 +46,7 @@ def callback() { } else { log.warn "No authQueryString" } - + if (state.JawboneAccessToken) { log.debug "Access token already exists" setup() @@ -77,20 +76,21 @@ def callback() { def authPage() { log.debug "authPage" - def description = null + def description = null if (state.JawboneAccessToken == null) { if (!state.accessToken) { log.debug "About to create access token" createAccessToken() } description = "Click to enter Jawbone Credentials" - def redirectUrl = oauthInitUrl() - // log.debug "RedirectURL = ${redirectUrl}" - return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) { - section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description } + def redirectUrl = buildRedirectUrl + log.debug "RedirectURL = ${redirectUrl}" + def donebutton= state.JawboneAccessToken != null + return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) { + section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description } } } else { - description = "Jawbone Credentials Already Entered." + description = "Jawbone Credentials Already Entered." return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) { section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description } } @@ -101,8 +101,8 @@ def oauthInitUrl() { log.debug "oauthInitUrl" def stcid = getSmartThingsClientId() state.oauthInitState = UUID.randomUUID().toString() - def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ] - return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}" + def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ] + redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") } def receiveToken(redirectUrl = null) { @@ -113,7 +113,7 @@ def receiveToken(redirectUrl = null) { def params = [ uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}", ] - httpGet(params) { response -> + httpGet(params) { response -> log.debug "${response.data}" log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}" state.JawboneAccessToken = response.data.access_token @@ -155,7 +155,7 @@ def connectionStatus(message, redirectUrl = null) { """ } - + def html = """ @@ -231,19 +231,11 @@ String toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } -def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" } - -def buildRedirectUrl(page) { - // log.debug "buildRedirectUrl" - // /api/token/:st_token/smartapps/installations/:id/something - return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" -} - def validateCurrentToken() { log.debug "validateCurrentToken" def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken" def requestBody = "secret=${getSmartThingsClientSecret()}" - + try { httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response -> if (response.status == 200) { @@ -287,9 +279,10 @@ def validateCurrentToken() { } def initialize() { - def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" + log.debug "Callback URL - Webhook" + def localServerUrl = getApiServerUrl() + def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" - log.debug "Callback URL: $webhook" httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) } @@ -299,16 +292,16 @@ def setup() { if (state.JawboneAccessToken) { def urlmember = "https://jawbone.com/nudge/api/users/@me/" - def member = null - httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def member = null + httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> member = response.data.data } - + if (member) { state.member = member def externalId = "${app.id}.${member.xid}" - // find the appropriate child device based on my app id and the device network id + // find the appropriate child device based on my app id and the device network id def deviceWrapper = getChildDevice("${externalId}") // invoke the generatePresenceEvent method on the child device @@ -328,7 +321,7 @@ def setup() { def installed() { enableCallback() - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -341,7 +334,7 @@ def installed() { def updated() { enableCallback() - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -365,29 +358,29 @@ def uninstalled() { } def pollChild(childDevice) { - def member = state.member - generatePollingEvents (member, childDevice) + def member = state.member + generatePollingEvents (member, childDevice) } def generatePollingEvents (member, childDevice) { // lets figure out if the member is currently "home" (At the place) - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" - def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" def goals = null def moves = null - def sleeps = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def sleeps = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> moves = response.data.data.items[0] - } - + } + try { // we are going to just ignore any errors log.debug "Member = ${member.first}" log.debug "Moves Goal = ${goals.move_steps} Steps" - log.debug "Moves = ${moves.details.steps} Steps" + log.debug "Moves = ${moves.details.steps} Steps" childDevice?.sendEvent(name:"steps", value: moves.details.steps) childDevice?.sendEvent(name:"goal", value: goals.move_steps) @@ -395,29 +388,29 @@ def generatePollingEvents (member, childDevice) { } catch (e) { // eat it - } + } } def generateInitialEvent (member, childDevice) { // lets figure out if the member is currently "home" (At the place) - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" - def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" def goals = null def moves = null - def sleeps = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def sleeps = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> moves = response.data.data.items[0] - } - + } + try { // we are going to just ignore any errors log.debug "Member = ${member.first}" log.debug "Moves Goal = ${goals.move_steps} Steps" log.debug "Moves = ${moves.details.steps} Steps" - log.debug "Sleeping state = false" + log.debug "Sleeping state = false" childDevice?.generateSleepingEvent(false) childDevice?.sendEvent(name:"steps", value: moves.details.steps) childDevice?.sendEvent(name:"goal", value: goals.move_steps) @@ -425,27 +418,27 @@ def generateInitialEvent (member, childDevice) { } catch (e) { // eat it - } + } } def setColor (steps,goal,childDevice) { def result = steps * 100 / goal - if (result < 25) + if (result < 25) childDevice?.sendEvent(name:"steps", value: "steps", label: steps) - else if ((result >= 25) && (result < 50)) + else if ((result >= 25) && (result < 50)) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) - else if ((result >= 50) && (result < 75)) + else if ((result >= 50) && (result < 75)) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) - else if (result >= 75) - childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) + else if (result >= 75) + childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) } def hookEventHandler() { // log.debug "In hookEventHandler method." log.debug "request = ${request}" - - def json = request.JSON - + + def json = request.JSON + // get some stuff we need def userId = json.events.user_xid[0] def json_type = json.events.type[0] @@ -454,39 +447,39 @@ def hookEventHandler() { //log.debug json log.debug "Userid = ${userId}" log.debug "Notification Type: " + json_type - log.debug "Notification Action: " + json_action - + log.debug "Notification Action: " + json_action + // find the appropriate child device based on my app id and the device network id def externalId = "${app.id}.${userId}" def childDevice = getChildDevice("${externalId}") - + if (childDevice) { - switch (json_action) { - case "enter_sleep_mode": - childDevice?.generateSleepingEvent(true) - break - case "exit_sleep_mode": - childDevice?.generateSleepingEvent(false) - break - case "creation": + switch (json_action) { + case "enter_sleep_mode": + childDevice?.generateSleepingEvent(true) + break + case "exit_sleep_mode": + childDevice?.generateSleepingEvent(false) + break + case "creation": childDevice?.sendEvent(name:"steps", value: 0) break case "updation": - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" def goals = null - def moves = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def moves = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> moves = response.data.data.items[0] - } + } log.debug "Goal = ${goals.move_steps} Steps" log.debug "Steps = ${moves.details.steps} Steps" childDevice?.sendEvent(name:"steps", value: moves.details.steps) - childDevice?.sendEvent(name:"goal", value: goals.move_steps) - //setColor(moves.details.steps,goals.move_steps,childDevice) + childDevice?.sendEvent(name:"goal", value: goals.move_steps) + //setColor(moves.details.steps,goals.move_steps,childDevice) break case "deletion": app.delete() @@ -499,4 +492,4 @@ def hookEventHandler() { def html = """{"code":200,"message":"OK"}""" render contentType: 'application/json', data: html -} \ No newline at end of file +} From ac5f15efd86fefa726c3edb379bbc33b46241099 Mon Sep 17 00:00:00 2001 From: juano2310 Date: Wed, 4 Nov 2015 14:18:46 -0500 Subject: [PATCH 23/35] buildActionUrl("hookCallback") --- .../jawbone-up-connect.src/jawbone-up-connect.groovy | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy index 1b16045..62c48a2 100644 --- a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy +++ b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy @@ -27,7 +27,7 @@ mappings { path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } - path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} path("/oauth/callback") { action: [ GET: "callback" ] } } @@ -280,10 +280,9 @@ def validateCurrentToken() { def initialize() { log.debug "Callback URL - Webhook" - def localServerUrl = getApiServerUrl() - def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" + def hookUrl = buildActionUrl("hookCallback") def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" - httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) + httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) } def setup() { From 8c55b6314a47019d6b3b3b31d8bc584da9d7fae1 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Wed, 4 Nov 2015 15:07:26 -0500 Subject: [PATCH 24/35] Force Level = 1% to 1 This ensures that the bulb will be able to dim to it minimum --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index a6246d6..14dfd95 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -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 { @@ -623,7 +623,8 @@ def off(childDevice) { def setLevel(childDevice, percent) { log.debug "Executing 'setLevel'" - def level = Math.min(Math.round(percent * 255 / 100), 255) + def level + if (percent == 1) level = 1 else level = Math.min(Math.round(percent * 255 / 100), 255) put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0]) } @@ -648,7 +649,7 @@ def setColor(childDevice, huesettings) { def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition] if (huesettings.level != null) { - value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) + if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) value.on = value.bri > 0 } From 54e5334ccadba54bee2924bf8abfdd86e70898cb Mon Sep 17 00:00:00 2001 From: Mike Robinet Date: Wed, 4 Nov 2015 14:38:28 -0600 Subject: [PATCH 25/35] CREX-1094 Delete stale device subscriptions on IFTTT app update --- smartapps/smartthings/ifttt.src/ifttt.groovy | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/smartapps/smartthings/ifttt.src/ifttt.groovy b/smartapps/smartthings/ifttt.src/ifttt.groovy index 0bc3b20..cad83d2 100644 --- a/smartapps/smartthings/ifttt.src/ifttt.groovy +++ b/smartapps/smartthings/ifttt.src/ifttt.groovy @@ -98,6 +98,15 @@ def installed() { } def updated() { + def currentDeviceIds = settings.collect { k, devices -> devices }.flatten().collect { it.id }.unique() + def subscriptionDevicesToRemove = app.subscriptions*.device.findAll { device -> + !currentDeviceIds.contains(device.id) + } + subscriptionDevicesToRemove.each { device -> + log.debug "Removing $device.displayName subscription" + state.remove(device.id) + unsubscribe(device) + } log.debug settings } From c416560f199c3e64b18e047ed383cd1927fd5e4a Mon Sep 17 00:00:00 2001 From: bflorian Date: Wed, 4 Nov 2015 13:57:17 -0800 Subject: [PATCH 26/35] Misc filename and namespace changes. --- .../smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy | 0 .../send-ham-bridge-command-when.groovy | 2 +- .../sprayer-controller-2.src/sprayer-controller-2.groovy | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename smartapps/{smart-auto-lock-/-unlock => smart-auto-lock-unlock}/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy (100%) diff --git a/smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy b/smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy similarity index 100% rename from smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy rename to smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy diff --git a/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy index 9258122..9d15957 100644 --- a/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy +++ b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy @@ -16,7 +16,7 @@ * */ definition( - name: "Send HAM Bridge Command When…", + name: "Send HAM Bridge Command When", namespace: "soletc.com", author: "Scottin Pollock", description: "Sends a command to your HAM Bridge server when SmartThings are activated.", diff --git a/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy b/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy index 54f38ab..e23da21 100644 --- a/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy +++ b/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy @@ -15,7 +15,7 @@ */ definition( name: "Sprayer Controller 2", - namespace: "", + namespace: "sprayercontroller", author: "Cooper Lee", description: "Control Sprayers for a period of time a number of times per hour", category: "My Apps", From f260e36d54586dfff086dbd5f4f5e89c64e02b35 Mon Sep 17 00:00:00 2001 From: bflorian Date: Wed, 4 Nov 2015 14:09:51 -0800 Subject: [PATCH 27/35] More name/namespace changes --- .../smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy | 4 ++-- .../send-ham-bridge-command-when.groovy | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy b/smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy index 7a0fbb5..b22c764 100644 --- a/smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy +++ b/smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy @@ -14,8 +14,8 @@ * */ definition( - name: "Smart Lock / Unlock", - namespace: "", + name: "Smart Auto Lock / Unlock", + namespace: "smart-auto-lock-unlock", author: "Arnaud", description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.", category: "Safety & Security", diff --git a/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy index 9d15957..4e7b3d0 100644 --- a/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy +++ b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy @@ -17,7 +17,7 @@ */ definition( name: "Send HAM Bridge Command When", - namespace: "soletc.com", + namespace: "smartthings", author: "Scottin Pollock", description: "Sends a command to your HAM Bridge server when SmartThings are activated.", category: "Convenience", @@ -25,7 +25,6 @@ definition( iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png" ) - preferences { section("Choose one or more, when..."){ input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true From 7aabd9bc5f0147f16feb1c1ed387163fb9f3fee5 Mon Sep 17 00:00:00 2001 From: bflorian Date: Wed, 4 Nov 2015 14:44:06 -0800 Subject: [PATCH 28/35] Deleted lights on when door opens after sundown --- ...ts-on-when-door-opens-after-sundown.groovy | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy diff --git a/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy b/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy deleted file mode 100644 index 8be8a70..0000000 --- a/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy +++ /dev/null @@ -1,53 +0,0 @@ -/** - * - * Lights On When Door Open After Sundown - * - * Based on "Turn It On When It Opens" by SmartThings - * - * Author: Aaron Crocco - */ -preferences { - section("When the door opens..."){ - input "contact1", "capability.contactSensor", title: "Where?" - } - section("Turn on these lights..."){ - input "switches", "capability.switch", multiple: true - } - section("and change mode to...") { - input "HomeAfterDarkMode", "mode", title: "Mode?" - } -} - - -def installed() -{ - subscribe(contact1, "contact.open", contactOpenHandler) -} - -def updated() -{ - unsubscribe() - subscribe(contact1, "contact.open", contactOpenHandler) -} - -def contactOpenHandler(evt) { - log.debug "$evt.value: $evt, $settings" - - //Check current time to see if it's after sundown. - def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset) - def now = new Date() - def setTime = s.sunset - log.debug "Sunset is at $setTime. Current time is $now" - - - if (setTime.before(now)) { //Executes only if it's after sundown. - - log.trace "Turning on switches: $switches" - switches.on() - log.trace "Changing house mode to $HomeAfterDarkMode" - setLocationMode(HomeAfterDarkMode) - sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.") - - } -} - From 5f1ff8a5c6a692cafb554531bb0c22b9d0b226c7 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 5 Nov 2015 10:07:19 -0500 Subject: [PATCH 29/35] Code Cleanup --- .../jawbone-up-connect.groovy | 164 ++++++++---------- 1 file changed, 77 insertions(+), 87 deletions(-) diff --git a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy index 62c48a2..3a2736a 100644 --- a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy +++ b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy @@ -27,15 +27,12 @@ mappings { path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } - path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} path("/oauth/callback") { action: [ GET: "callback" ] } } -def getSmartThingsClientId() { return appSettings.clientId } -def getSmartThingsClientSecret() { return appSettings.clientSecret } def getServerUrl() { return "https://graph.api.smartthings.com" } -def getShardUrl() { return getApiServerUrl() } -def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" } def buildRedirectUrl(page) { return buildActionUrl(page) } def callback() { @@ -46,7 +43,7 @@ def callback() { } else { log.warn "No authQueryString" } - + if (state.JawboneAccessToken) { log.debug "Access token already exists" setup() @@ -62,9 +59,8 @@ def callback() { // SmartThings code, which we ignore, as we don't need to exchange for an access token. // Instead, go initiate the Jawbone OAuth flow. log.debug "Executing callback redirect to auth page" - def stcid = getSmartThingsClientId() state.oauthInitState = UUID.randomUUID().toString() - def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"] + def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"] redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") } } else { @@ -76,7 +72,7 @@ def callback() { def authPage() { log.debug "authPage" - def description = null + def description = null if (state.JawboneAccessToken == null) { if (!state.accessToken) { log.debug "About to create access token" @@ -85,12 +81,12 @@ def authPage() { description = "Click to enter Jawbone Credentials" def redirectUrl = buildRedirectUrl log.debug "RedirectURL = ${redirectUrl}" - def donebutton= state.JawboneAccessToken != null + def donebutton= state.JawboneAccessToken != null return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) { section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description } } } else { - description = "Jawbone Credentials Already Entered." + description = "Jawbone Credentials Already Entered." return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) { section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description } } @@ -99,21 +95,18 @@ def authPage() { def oauthInitUrl() { log.debug "oauthInitUrl" - def stcid = getSmartThingsClientId() state.oauthInitState = UUID.randomUUID().toString() - def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ] + def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ] redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") } def receiveToken(redirectUrl = null) { log.debug "receiveToken" - def stcid = getSmartThingsClientId() - def oauthClientSecret = getSmartThingsClientSecret() - def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ] + def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ] def params = [ uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}", ] - httpGet(params) { response -> + httpGet(params) { response -> log.debug "${response.data}" log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}" state.JawboneAccessToken = response.data.access_token @@ -155,7 +148,7 @@ def connectionStatus(message, redirectUrl = null) { """ } - + def html = """ @@ -234,8 +227,8 @@ String toQueryString(Map m) { def validateCurrentToken() { log.debug "validateCurrentToken" def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken" - def requestBody = "secret=${getSmartThingsClientSecret()}" - + def requestBody = "secret=${appSettings.clientSecret}" + try { httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response -> if (response.status == 200) { @@ -248,9 +241,7 @@ def validateCurrentToken() { if (e.statusCode == 401) { // token is expired log.debug "Access token is expired" if (state.refreshToken) { // if we have this we are okay - def stcid = getSmartThingsClientId() - def oauthClientSecret = getSmartThingsClientSecret() - def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken] + def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken] def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}" def params = [ uri: tokenUrl @@ -279,10 +270,11 @@ def validateCurrentToken() { } def initialize() { - log.debug "Callback URL - Webhook" - def hookUrl = buildActionUrl("hookCallback") - def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" - httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) + log.debug "Callback URL - Webhook" + def localServerUrl = getApiServerUrl() + def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" + def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" + httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) } def setup() { @@ -291,16 +283,16 @@ def setup() { if (state.JawboneAccessToken) { def urlmember = "https://jawbone.com/nudge/api/users/@me/" - def member = null - httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def member = null + httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> member = response.data.data } - + if (member) { state.member = member def externalId = "${app.id}.${member.xid}" - // find the appropriate child device based on my app id and the device network id + // find the appropriate child device based on my app id and the device network id def deviceWrapper = getChildDevice("${externalId}") // invoke the generatePresenceEvent method on the child device @@ -319,8 +311,7 @@ def setup() { } def installed() { - enableCallback() - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -332,8 +323,7 @@ def installed() { } def updated() { - enableCallback() - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -357,29 +347,29 @@ def uninstalled() { } def pollChild(childDevice) { - def member = state.member - generatePollingEvents (member, childDevice) + def member = state.member + generatePollingEvents (member, childDevice) } def generatePollingEvents (member, childDevice) { // lets figure out if the member is currently "home" (At the place) - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" - def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" def goals = null def moves = null - def sleeps = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def sleeps = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> moves = response.data.data.items[0] - } - + } + try { // we are going to just ignore any errors log.debug "Member = ${member.first}" log.debug "Moves Goal = ${goals.move_steps} Steps" - log.debug "Moves = ${moves.details.steps} Steps" + log.debug "Moves = ${moves.details.steps} Steps" childDevice?.sendEvent(name:"steps", value: moves.details.steps) childDevice?.sendEvent(name:"goal", value: goals.move_steps) @@ -387,29 +377,29 @@ def generatePollingEvents (member, childDevice) { } catch (e) { // eat it - } + } } def generateInitialEvent (member, childDevice) { // lets figure out if the member is currently "home" (At the place) - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" - def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" def goals = null def moves = null - def sleeps = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def sleeps = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> moves = response.data.data.items[0] - } - + } + try { // we are going to just ignore any errors log.debug "Member = ${member.first}" log.debug "Moves Goal = ${goals.move_steps} Steps" log.debug "Moves = ${moves.details.steps} Steps" - log.debug "Sleeping state = false" + log.debug "Sleeping state = false" childDevice?.generateSleepingEvent(false) childDevice?.sendEvent(name:"steps", value: moves.details.steps) childDevice?.sendEvent(name:"goal", value: goals.move_steps) @@ -417,27 +407,27 @@ def generateInitialEvent (member, childDevice) { } catch (e) { // eat it - } + } } def setColor (steps,goal,childDevice) { def result = steps * 100 / goal - if (result < 25) + if (result < 25) childDevice?.sendEvent(name:"steps", value: "steps", label: steps) - else if ((result >= 25) && (result < 50)) + else if ((result >= 25) && (result < 50)) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) - else if ((result >= 50) && (result < 75)) + else if ((result >= 50) && (result < 75)) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) - else if (result >= 75) - childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) + else if (result >= 75) + childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) } def hookEventHandler() { // log.debug "In hookEventHandler method." log.debug "request = ${request}" - - def json = request.JSON - + + def json = request.JSON + // get some stuff we need def userId = json.events.user_xid[0] def json_type = json.events.type[0] @@ -446,39 +436,39 @@ def hookEventHandler() { //log.debug json log.debug "Userid = ${userId}" log.debug "Notification Type: " + json_type - log.debug "Notification Action: " + json_action - + log.debug "Notification Action: " + json_action + // find the appropriate child device based on my app id and the device network id def externalId = "${app.id}.${userId}" def childDevice = getChildDevice("${externalId}") - + if (childDevice) { - switch (json_action) { - case "enter_sleep_mode": - childDevice?.generateSleepingEvent(true) - break - case "exit_sleep_mode": - childDevice?.generateSleepingEvent(false) - break - case "creation": + switch (json_action) { + case "enter_sleep_mode": + childDevice?.generateSleepingEvent(true) + break + case "exit_sleep_mode": + childDevice?.generateSleepingEvent(false) + break + case "creation": childDevice?.sendEvent(name:"steps", value: 0) break case "updation": - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" def goals = null - def moves = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def moves = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> moves = response.data.data.items[0] - } + } log.debug "Goal = ${goals.move_steps} Steps" log.debug "Steps = ${moves.details.steps} Steps" childDevice?.sendEvent(name:"steps", value: moves.details.steps) - childDevice?.sendEvent(name:"goal", value: goals.move_steps) - //setColor(moves.details.steps,goals.move_steps,childDevice) + childDevice?.sendEvent(name:"goal", value: goals.move_steps) + //setColor(moves.details.steps,goals.move_steps,childDevice) break case "deletion": app.delete() From dec9ff20b037099eae963c50b8a77100894ff0f8 Mon Sep 17 00:00:00 2001 From: juano2310 Date: Thu, 5 Nov 2015 14:11:38 -0500 Subject: [PATCH 30/35] Harmony Global Oauth --- .../harmony-activity.groovy | 81 +++++++++ .../logitech-harmony-connect.groovy | 164 +++++++++--------- 2 files changed, 165 insertions(+), 80 deletions(-) create mode 100644 devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy diff --git a/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy b/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy new file mode 100644 index 0000000..3f89262 --- /dev/null +++ b/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy @@ -0,0 +1,81 @@ +/** + * Logitech Harmony Activity + * + * Copyright 2015 Juan Risso + * + * 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: "Logitech Harmony Activity", namespace: "smartthings", author: "Juan Risso") { + capability "Switch" + capability "Actuator" + capability "Refresh" + + command "huboff" + command "alloff" + command "refresh" + } + + // simulator metadata + simulator { + } + + // UI tile definitions + tiles { + standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: 'Off', action: "switch.on", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#ffffff", nextState: "on" + state "on", label: 'On', action: "switch.off", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#79b821", nextState: "off" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("forceoff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'Force End', action:"switch.off", icon:"st.secondary.off" + } + standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'End Hub Action', action:"huboff", icon:"st.harmony.harmony-hub-icon" + } + standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'All Actions', action:"alloff", icon:"st.secondary.off" + } + main "button" + details(["button", "refresh", "forceoff", "huboff", "alloff"]) + } +} + +def parse(String description) { +} + +def on() { + sendEvent(name: "switch", value: "on") + log.trace parent.activity(device.deviceNetworkId,"start") +} + +def off() { + sendEvent(name: "switch", value: "off") + log.trace parent.activity(device.deviceNetworkId,"end") +} + +def huboff() { + sendEvent(name: "switch", value: "off") + log.trace parent.activity(device.deviceNetworkId,"hub") +} + +def alloff() { + sendEvent(name: "switch", value: "off") + log.trace parent.activity("all","end") +} + + +def refresh() { + log.debug "Executing 'refresh'" + log.trace parent.poll() +} diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 0816d03..a978787 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -34,7 +34,7 @@ * locks | lock | lock, unlock | locked, unlocked * ---------------------+-------------------+-----------------------------+------------------------------------ */ - + definition( name: "Logitech Harmony (Connect)", namespace: "smartthings", @@ -43,12 +43,10 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", - oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"], - singleInstance: true + oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"] ){ appSetting "clientId" appSetting "clientSecret" - appSetting "callbackUrl" } preferences(oauthPage: "deviceAuthorization") { @@ -89,17 +87,20 @@ mappings { path("/oauth/initialize") { action: [ GET: "init"] } } +def getShardUrl() { return getApiServerUrl() } def getServerUrl() { return "https://graph.api.smartthings.com" } +def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } def authPage() { - def description = null + def description = null if (!state.HarmonyAccessToken) { if (!state.accessToken) { log.debug "About to create access token" createAccessToken() } description = "Click to enter Harmony Credentials" - def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" + def redirectUrl = buildRedirectUrl return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } } @@ -111,7 +112,7 @@ def authPage() { def huboptions = state.HarmonyHubs ?: [] def actoptions = state.HarmonyActivities ?: [] - + def numFoundHub = huboptions.size() ?: 0 def numFoundAct = actoptions.size() ?: 0 if((deviceRefreshCount % 5) == 0) { @@ -121,13 +122,14 @@ def authPage() { section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions } - if (numFoundHub > 0 && numFoundAct > 0 && false) + // Virtual activity flag + if (numFoundHub > 0 && numFoundAct > 0 && true) section("You can also add activities as virtual switches for other convenient integrations") { input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions - } + } if (state.resethub) - section("Connection to the hub timed out. Please restart the hub and try again.") {} - } + section("Connection to the hub timed out. Please restart the hub and try again.") {} + } } } @@ -139,7 +141,7 @@ def callback() { } else { log.warn "No authQueryString" } - + if (state.HarmonyAccessToken) { log.debug "Access token already exists" discovery() @@ -164,7 +166,7 @@ def callback() { def init() { log.debug "Requesting Code" - def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ] + def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ] redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") } @@ -175,7 +177,7 @@ def receiveToken(redirectUrl = null) { uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}", ] try { - httpPost(params) { response -> + httpPost(params) { response -> state.HarmonyAccessToken = response.data.access_token } } catch (java.util.concurrent.TimeoutException e) { @@ -222,7 +224,7 @@ def connectionStatus(message, redirectUrl = null) { """ } - + def html = """ @@ -303,30 +305,28 @@ def buildRedirectUrl(page) { } def installed() { - enableCallback() if (!state.accessToken) { log.debug "About to create access token" createAccessToken() - } else { + } else { initialize() } } def updated() { unsubscribe() - unschedule() - enableCallback() + unschedule() if (!state.accessToken) { log.debug "About to create access token" createAccessToken() - } else { + } else { initialize() - } + } } def uninstalled() { if (state.HarmonyAccessToken) { - try { + try { state.HarmonyAccessToken = "" log.debug "Success disconnecting Harmony from SmartThings" } catch (groovyx.net.http.HttpResponseException e) { @@ -340,7 +340,7 @@ def initialize() { if (selectedhubs || selectedactivities) { addDevice() runEvery5Minutes("discovery") - } + } } def getHarmonydevices() { @@ -360,20 +360,20 @@ Map discoverDevices() { def hubname = getHubName(it.key) def hubvalue = "${hubname}" hubs["harmony-${hubkey}"] = hubvalue - it.value.response.data.activities.each { + it.value.response.data.activities.each { def value = "${it.value.name}" def key = "harmony-${hubkey}-${it.key}" activities["${key}"] = value - } + } } state.HarmonyHubs = hubs - state.HarmonyActivities = activities - } + state.HarmonyActivities = activities + } } //CHILD DEVICE METHODS def discovery() { - def Params = [auth: state.HarmonyAccessToken] + def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" try { httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> @@ -385,11 +385,11 @@ def discovery() { poll() } else { log.debug "Error: $response.status" - } + } } } catch (groovyx.net.http.HttpResponseException e) { if (e.statusCode == 401) { // token is expired - state.remove("HarmonyAccessToken") + state.remove("HarmonyAccessToken") log.warn "Harmony Access token has expired" } } catch (java.net.SocketTimeoutException e) { @@ -397,12 +397,12 @@ def discovery() { state.resethub = true } catch (e) { log.warn "Hostname in certificate didn't match. Please try again later." - } + } return null } def addDevice() { - log.trace "Adding Hubs" + log.trace "Adding Hubs" selectedhubs.each { dni -> def d = getChildDevice(dni) if(!d) { @@ -413,8 +413,8 @@ def addDevice() { } else { log.trace "found ${d.displayName} with id $dni already exists" } - } - log.trace "Adding Activities" + } + log.trace "Adding Activities" selectedactivities.each { dni -> def d = getChildDevice(dni) if(!d) { @@ -425,7 +425,7 @@ def addDevice() { } else { log.trace "found ${d.displayName} with id $dni already exists" } - } + } } def activity(dni,mode) { @@ -433,26 +433,26 @@ def activity(dni,mode) { def msg = "Command failed" def url = '' if (dni == "all") { - url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}" - } else { + url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}" + } else { def aux = dni.split('-') def hubId = aux[1] - if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ - url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}" + if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ + url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}" } else { def activityId = aux[2] url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}" } - } + } try { - httpPostJson(uri: url) { response -> + httpPostJson(uri: url) { response -> if (response.data.code == 200 || dni == "all") { msg = "Command sent succesfully" - state.aux = 0 + state.aux = 0 } else { msg = "Command failed. Error: $response.data.code" } - } + } } catch (groovyx.net.http.HttpResponseException ex) { log.error ex if (state.aux == 0) { @@ -460,7 +460,7 @@ def activity(dni,mode) { activity(dni,mode) } else { msg = ex - state.aux = 0 + state.aux = 0 } } catch(Exception ex) { msg = ex @@ -473,10 +473,10 @@ def poll() { // GET THE LIST OF ACTIVITIES if (state.HarmonyAccessToken) { getActivityList() - def Params = [auth: state.HarmonyAccessToken] + def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}" try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> def map = [:] response.data.hubs.each { if (it.value.message == "OK") { @@ -489,20 +489,20 @@ def poll() { def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key) hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false) } - } + } } else { log.trace it.value.message - } + } } - def activities = getChildDevices() + def activities = getChildDevices() def activitynotrunning = true activities.each { activity -> - def act = activity.deviceNetworkId.split('-') + def act = activity.deviceNetworkId.split('-') if (act.size() > 2) { def aux = map.find { it.key == act[1] } if (aux) { def aux2 = aux.value.split(',') - def childDevice = getChildDevice(activity.deviceNetworkId) + def childDevice = getChildDevice(activity.deviceNetworkId) if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) { childDevice?.sendEvent(name: "switch", value: "on") if (aux2[1] == "1") @@ -512,30 +512,30 @@ def poll() { if (aux2[1] == "3") runIn(5, "poll", [overwrite: true]) } - } - } + } + } } return "Poll completed $map - $state.hubs" } } catch (groovyx.net.http.HttpResponseException e) { if (e.statusCode == 401) { // token is expired - state.remove("HarmonyAccessToken") + state.remove("HarmonyAccessToken") return "Harmony Access token has expired" - } + } } catch(Exception e) { log.trace e } - } + } } def getActivityList() { // GET ACTIVITY'S NAME if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] + def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> response.data.hubs.each { def hub = getChildDevice("harmony-${it.key}") if (hub) { @@ -548,10 +548,10 @@ def getActivityList() { } activities += [id: "off", name: "Activity OFF", type: "0"] log.trace activities - } - hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false) + } + hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false) } - } + } } } catch (groovyx.net.http.HttpResponseException e) { log.trace e @@ -560,7 +560,7 @@ def getActivityList() { } catch(Exception e) { log.trace e } - } + } return activity } @@ -568,16 +568,16 @@ def getActivityName(activity,hubId) { // GET ACTIVITY'S NAME def actname = activity if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] + def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> actname = response.data.data.activities[activity].name } } catch(Exception e) { log.trace e } - } + } return actname } @@ -585,19 +585,19 @@ def getActivityId(activity,hubId) { // GET ACTIVITY'S NAME def actid = activity if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] + def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> response.data.data.activities.each { if (it.value.name == activity) actid = it.key - } + } } } catch(Exception e) { log.trace e } - } + } return actid } @@ -605,16 +605,16 @@ def getHubName(hubId) { // GET HUB'S NAME def hubname = hubId if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] + def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}" try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> hubname = response.data.data.name } } catch(Exception e) { log.trace e - } - } + } + } return hubname } @@ -625,8 +625,8 @@ def sendNotification(msg) { def hookEventHandler() { // log.debug "In hookEventHandler method." log.debug "request = ${request}" - - def json = request.JSON + + def json = request.JSON def html = """{"code":200,"message":"OK"}""" render contentType: 'application/json', data: html @@ -660,12 +660,16 @@ def updateDevice() { } else { def device = allDevices.find { it.id == params.id } if (device) { - if (arguments) { - device."$command"(*arguments) - } else { - device."$command"() + if (device.hasCommand("$command")) { + if (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + render status: 204, data: "{}" + } else { + render status: 404, data: '{"msg": "Command not supported by this Device"}' } - render status: 204, data: "{}" } else { render status: 404, data: '{"msg": "Device not found"}' } From 9654c27ca86faf9a15c67f000c84ef6be45ab9e3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 5 Nov 2015 14:15:50 -0500 Subject: [PATCH 31/35] added singleInstance: true --- .../logitech-harmony-connect.src/logitech-harmony-connect.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index a978787..2f25fa8 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -44,6 +44,7 @@ definition( iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"] + singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" From d19ec0b525e67bb29443aba36e7fb1394b71c690 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 5 Nov 2015 14:19:20 -0500 Subject: [PATCH 32/35] removed function ShardUrl() --- .../logitech-harmony-connect.groovy | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 2f25fa8..3d246b8 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -88,10 +88,9 @@ mappings { path("/oauth/initialize") { action: [ GET: "init"] } } -def getShardUrl() { return getApiServerUrl() } def getServerUrl() { return "https://graph.api.smartthings.com" } def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } -def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${apiServerUrl}" } def authPage() { def description = null @@ -167,8 +166,8 @@ def callback() { def init() { log.debug "Requesting Code" - def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ] - redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") + def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ] + redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") } def receiveToken(redirectUrl = null) { From ea65ed58dc3b4fa040bcaefef9f6a3152c279711 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 5 Nov 2015 14:24:15 -0500 Subject: [PATCH 33/35] Added a "," --- .../logitech-harmony-connect.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 3d246b8..e197337 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -43,7 +43,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", - oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"] + oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"], singleInstance: true ){ appSetting "clientId" From 4e88a3ac607be0ecfb67863442b7928cd5a273da Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Mon, 9 Nov 2015 12:56:38 -0500 Subject: [PATCH 34/35] Replaced atomicState with State --- .../logitech-harmony-connect.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index e197337..03c5ccc 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -90,7 +90,7 @@ mappings { def getServerUrl() { return "https://graph.api.smartthings.com" } def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } -def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${apiServerUrl}" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" } def authPage() { def description = null From 0445b415f7136bf9b97c58f5ea0941bce740c6ee Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Mon, 9 Nov 2015 15:55:32 -0500 Subject: [PATCH 35/35] Reverted to previously working getBridgeIP() --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 14dfd95..7d81c41 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -724,8 +724,11 @@ private getBridgeIP() { host = d.latestState('networkAddress').stringValue } if (host == null || host == "") { - def macAddress = selectedHue - def bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(macAddress) }?.value + 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}"