From 8ba5eaf74dbd79ae8b210fed750317133b6429b9 Mon Sep 17 00:00:00 2001 From: Mike Robinet Date: Thu, 24 Sep 2015 15:51:51 -0500 Subject: [PATCH 01/29] 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 ef21fd42572f34ef6ae0ba9d200611434454e416 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Tue, 20 Oct 2015 01:01:17 -0700 Subject: [PATCH 02/29] 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 03/29] 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 04/29] 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 05/29] 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 06/29] 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 07/29] 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 08/29] 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 09/29] 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 10/29] 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 11/29] 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 12/29] 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 13/29] 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 14/29] 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 15/29] 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 16/29] 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 17/29] 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 18/29] 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 19/29] 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 20/29] 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 21/29] 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 22/29] 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 23/29] 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}" From 0af48a5caf23ec863cc47ef749fd811713757864 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 12 Nov 2015 17:06:07 -0800 Subject: [PATCH 24/29] new fingerprints for Yale. battery issue resolution for Yale --- .../smartthings/zigbee-lock.src/zigbee-lock.groovy | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index 0e46ebf..eb073e5 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -32,7 +32,13 @@ 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" + manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale Touch Screen Lever Lock" + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", + manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock" + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", + manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock" + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", + manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock" } tiles(scale: 2) { @@ -140,6 +146,9 @@ private Map parseReportAttributeMessage(String description) { if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { resultMap.name = "battery" resultMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) + if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting + resultMap.value = Integer.parseInt(descMap.value, 16) + } log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}" } else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) { From 5d1630b367739ec56aae0d82acb22cab442af6a8 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 12 Nov 2015 17:31:45 -0800 Subject: [PATCH 25/29] Adding fingerprint for new bulbs and transition old color temperature osram bulbs to new one --- .../osram-lightify-led-tunable-white-60w.groovy | 7 +++---- .../zigbee-white-color-temperature-bulb.groovy | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy index bd97941..8253582 100644 --- a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy +++ b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy @@ -5,6 +5,8 @@ that issue by using state variables */ +//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH + metadata { definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") { @@ -20,10 +22,7 @@ metadata { // indicates that device keeps track of heartbeat (in state.heartbeat) attribute "heartbeat", "string" - - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White" + } diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index dfe7475..953a2a6 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -31,6 +31,10 @@ metadata { command "setGenericName" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W" } // UI tile definitions From 1f76b9e960289fc00a035579c62f950900adef9e Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 12 Nov 2015 17:51:57 -0800 Subject: [PATCH 26/29] adding fingerprints for osram and sengled dimmable bulbs --- .../zigbee-dimmer-power.src/zigbee-dimmer-power.groovy | 1 + devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy | 1 + 2 files changed, 2 insertions(+) diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 0382e0c..27fcec0 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -24,6 +24,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch" } tiles(scale: 2) { diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 588a177..58ad78a 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -23,6 +23,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light" } tiles(scale: 2) { From ac7592d13206554162f52c230052ae1e92f5a97e Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 12 Nov 2015 19:18:32 -0800 Subject: [PATCH 27/29] Moving re-certified device to generic zigbee DTH. --- devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy | 2 +- .../smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy | 2 -- .../smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy | 2 -- .../smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy | 3 +++ devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy | 3 ++- .../zigbee-dimmer-power.src/zigbee-dimmer-power.groovy | 2 ++ devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy | 2 ++ .../zigbee-switch-power.src/zigbee-switch-power.groovy | 2 ++ 8 files changed, 12 insertions(+), 6 deletions(-) diff --git a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy index e48822f..ea47b6d 100644 --- a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy +++ b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy @@ -50,7 +50,7 @@ metadata { capability "Switch Level" capability "Polling" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Link Bulb" } // UI tile definitions diff --git a/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy b/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy index 2d2e2fb..d71b5a8 100644 --- a/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy +++ b/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy @@ -26,8 +26,6 @@ metadata { capability "Actuator" capability "Sensor" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857" } // simulator metadata diff --git a/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy b/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy index a731354..d04393f 100644 --- a/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy +++ b/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy @@ -26,8 +26,6 @@ metadata { capability "Actuator" capability "Sensor" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856" } // simulator metadata diff --git a/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy b/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy index 1c111a0..4058ff6 100644 --- a/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy +++ b/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy @@ -11,6 +11,9 @@ * for the specific language governing permissions and limitations under the License. * */ + +//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH + metadata { definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") { capability "Switch Level" diff --git a/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy index 397ca7b..243dafb 100644 --- a/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy +++ b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy @@ -15,6 +15,8 @@ * Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart * */ +//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH + metadata { definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") { @@ -25,7 +27,6 @@ metadata { capability "Switch" capability "Switch Level" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019" } // simulator metadata diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 27fcec0..c47bc50 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -25,6 +25,8 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer" } tiles(scale: 2) { diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 58ad78a..c3114dc 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -24,6 +24,8 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ" } tiles(scale: 2) { 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 fddba41..17cbf09 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -23,6 +23,8 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch" } tiles(scale: 2) { From ef4a32ac2e9b24ddec39497801992e20863d870d Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 12 Nov 2015 19:28:57 -0800 Subject: [PATCH 28/29] Cree DTH refresh --- .../cree-bulb.src/cree-bulb.groovy | 189 ++++-------------- 1 file changed, 42 insertions(+), 147 deletions(-) diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy index fc944c2..23fd384 100644 --- a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy +++ b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy @@ -38,168 +38,63 @@ metadata { } // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - - main(["switch"]) - details(["switch", "level", "levelSliderControl", "refresh"]) - } + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.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.trace description - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - log.trace msg - log.trace "data: $msg.data" - - if(description?.endsWith("0100") ||description?.endsWith("1001")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result + 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}" } - - if(description?.endsWith("0000") || description?.endsWith("1000")) - { - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result + else { + sendEvent(name: resultMap.type, value: resultMap.value) } - } - - - if (description?.startsWith("read attr")) { - - log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - - sendEvent( name: "level", value: i ) } - - + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } +} + +def off() { + zigbee.off() } def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - } - -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - -} - -def refresh() { - // Schedule poll every 1 min - //schedule("0 */1 * * * ?", poll) - //poll() - - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0" - ] + zigbee.on() } def setLevel(value) { - log.trace "setLevel($value)" - def cmds = [] + zigbee.setLevel(value) +} - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: value) - def level = hex(value * 255/100) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" - - //log.debug cmds - cmds +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() } def configure() { - - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", - ] - return configCmds + refresh() // send refresh cmds as part of config -} - -def uninstalled() { - - log.debug "uninstalled()" - - response("zcl rftd") - -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - - - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -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 + log.debug "Configuring Reporting and Bindings." + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() } From cf69d6127d5c25aacf15d43c8f84dc105ce0c607 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 12 Nov 2015 20:08:03 -0800 Subject: [PATCH 29/29] temporary changes to the lock DTH to account for the upcoming zigbee library changes --- .../zigbee-lock.src/zigbee-lock.groovy | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index eb073e5..6c8557c 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -91,11 +91,24 @@ def uninstalled() { } def configure() { + /* def cmds = zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}", "${TYPE_ENUM8}", 0, 3600, "{01}") + zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}", - "${TYPE_U8}", 3600, 3600, "{01}") + "${TYPE_U8}", 600, 21600, "{01}") + */ + def zigbeeId = device.zigbeeId + def cmds = + [ + "zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200", + "zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200", + + "zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200", + "zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200", + ] log.info "configure() --- cmds: $cmds" return cmds + refresh() // send refresh cmds as part of config } @@ -125,13 +138,15 @@ def parse(String description) { def lock() { def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}") log.info "lock() -- cmds: $cmds" - return cmds + //return cmds + "st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}" } def unlock() { def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}") log.info "unlock() -- cmds: $cmds" - return cmds + //return cmds + "st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}" } // Private methods