From 22185c5440f3cc307319ea947a38af26c65f8b18 Mon Sep 17 00:00:00 2001 From: Adam Jensen Date: Tue, 24 May 2016 08:18:51 -0500 Subject: [PATCH 01/30] Added labels to the motion attribute in the main multiattributetile --- .../fibaro-motion-sensor-zw5.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy index 864420f..7504d93 100644 --- a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy @@ -33,8 +33,8 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app tileAttribute("device.motion", key:"PRIMARY_CONTROL") { - attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821") - attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e") + attributeState("inactive", label:"no motion", icon:"st.motion.motion.inactive", backgroundColor:"#79b821") + attributeState("active", label:"motion", icon:"st.motion.motion.active", backgroundColor:"#ffa81e") } tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { @@ -278,4 +278,4 @@ private encap(physicalgraph.zwave.Command cmd) { } else { crc16(cmd) } -} \ No newline at end of file +} From 5e6b4f74e05e1b680064e1710a198f267fff0e15 Mon Sep 17 00:00:00 2001 From: CosmicPuppy Date: Mon, 29 Aug 2016 13:49:49 -0700 Subject: [PATCH 02/30] To Netatmo DTHs, added Capability "Sensor" per http://docs.smartthings.com/en/latest/device-type-developers-guide/overview.html?highlight=sensor%20actuator#actuator-and-sensor. There are some SmartApps out there using the "Actuator" and "Sensor" Capabilities and this Device doesn't show up for them (e.g., SmartTiles V6). --- .../netatmo-additional-module.groovy | 1 + .../dianoga/netatmo-basestation.src/netatmo-basestation.groovy | 1 + .../netatmo-outdoor-module.src/netatmo-outdoor-module.groovy | 1 + devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy | 2 ++ 4 files changed, 5 insertions(+) diff --git a/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy b/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy index 00916de..9717a28 100644 --- a/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy +++ b/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy @@ -15,6 +15,7 @@ */ metadata { definition (name: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" capability "Relative Humidity Measurement" capability "Temperature Measurement" diff --git a/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy b/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy index f0a844c..899a987 100644 --- a/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy +++ b/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy @@ -15,6 +15,7 @@ */ metadata { definition (name: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" capability "Relative Humidity Measurement" capability "Temperature Measurement" diff --git a/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy b/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy index 45ef2b2..9ea2db8 100644 --- a/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy +++ b/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy @@ -15,6 +15,7 @@ */ metadata { definition (name: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" capability "Relative Humidity Measurement" capability "Temperature Measurement" } diff --git a/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy b/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy index a882f23..1ed8474 100644 --- a/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy +++ b/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy @@ -15,6 +15,8 @@ */ metadata { definition (name: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" + attribute "rain", "number" attribute "rainSumHour", "number" attribute "rainSumDay", "number" From fe2fbc3b97f667190ed3b4337ab2a245c4782d3d Mon Sep 17 00:00:00 2001 From: juano2310 Date: Tue, 6 Sep 2016 14:01:20 -0400 Subject: [PATCH 03/30] MKTP-829 - Adding disclaimer --- .../jawbone-up-connect.groovy | 145 +++++++++--------- .../logitech-harmony-connect.groovy | 3 +- .../wemo-connect.src/wemo-connect.groovy | 3 +- 3 files changed, 77 insertions(+), 74 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 26dd7e5..4e120c1 100644 --- a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy +++ b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy @@ -28,7 +28,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" ] } } @@ -44,7 +44,7 @@ def callback() { } else { log.warn "No authQueryString" } - + if (state.JawboneAccessToken) { log.debug "Access token already exists" setup() @@ -73,7 +73,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" @@ -82,12 +82,13 @@ 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 { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } 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 } } @@ -107,7 +108,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 @@ -149,7 +150,7 @@ def connectionStatus(message, redirectUrl = null) { """ } - + def html = """ @@ -229,12 +230,12 @@ def validateCurrentToken() { log.debug "validateCurrentToken" def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken" def requestBody = "secret=${appSettings.clientSecret}" - + try { httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response -> if (response.status == 200) { log.debug "${response.data}" - log.debug "Setting refresh token to ${response.data.data.refresh_token}" + log.debug "Setting refresh token" state.refreshToken = response.data.data.refresh_token } } @@ -258,7 +259,7 @@ def validateCurrentToken() { state.remove("refreshToken") } } else { - log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}" + log.debug "Setting access token" state.JawboneAccessToken = data.access_token state.refreshToken = data.refresh_token } @@ -271,10 +272,10 @@ def validateCurrentToken() { } def initialize() { - log.debug "Callback URL - Webhook" - def localServerUrl = getApiServerUrl() + 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" + def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) } @@ -284,16 +285,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 @@ -312,7 +313,7 @@ def setup() { } def installed() { - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -324,7 +325,7 @@ def installed() { } def updated() { - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -348,29 +349,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) @@ -378,29 +379,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) @@ -408,27 +409,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] @@ -437,39 +438,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" + 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() 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 df4cd5a..726c109 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -51,7 +51,7 @@ definition( } preferences(oauthPage: "deviceAuthorization") { - page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization") + page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization") page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) { section("Allow Logitech Harmony to control these things...") { input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false @@ -119,6 +119,7 @@ def authPage() { discoverDevices() } return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { + section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } 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 } diff --git a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy index 35cbf4f..73b4426 100644 --- a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy +++ b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy @@ -86,6 +86,7 @@ def firstPage() def lightSwitchesDiscovered = lightSwitchesDiscovered() return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { + section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } section("Select a device...") { input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered @@ -681,4 +682,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) { private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } -} \ No newline at end of file +} From c051d719cca7c7d64512fd2149d62332232fa486 Mon Sep 17 00:00:00 2001 From: twack Date: Wed, 7 Sep 2016 09:14:19 -0700 Subject: [PATCH 04/30] 20160907_update_its_to_hot_for_security --- smartapps/smartthings/its-too-hot.src/its-too-hot.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy index 5b8e10d..3ac1ab0 100644 --- a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy +++ b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy @@ -69,10 +69,10 @@ def temperatureHandler(evt) { def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes" + log.debug "SMS already sent to within the last $delta Minutes minutes" // TODO: Send "Temperature back to normal" SMS, turn switch off } else { - log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch" + log.debug "Temperature rose above $tooHot: sending SMS and activating $mySwitch" def tempScale = location.temperatureScale ?: "F" send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}") switch1?.on() From 06acc13575065537df2bcf4040211d3adcdcc7e7 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Wed, 7 Sep 2016 12:14:58 -0400 Subject: [PATCH 05/30] DVCSMP-1959 - Remove sensitive information from logs (#1206) --- .../garage-door-monitor.src/garage-door-monitor.groovy | 2 +- .../the-gun-case-moved.src/the-gun-case-moved.groovy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy b/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy index 4cabc5c..cd07b23 100644 --- a/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy +++ b/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy @@ -90,7 +90,7 @@ def takeAction(){ } def sendTextMessage() { - log.debug "$multisensor was open too long, texting $phone" + log.debug "$multisensor was open too long, texting phone" updateSmsHistory() def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1) diff --git a/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy b/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy index 196d57a..1cce9c2 100644 --- a/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy +++ b/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy @@ -53,13 +53,13 @@ def accelerationActiveHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + log.debug "SMS already sent to phone within the last $deltaSeconds seconds" } else { if (location.contactBookEnabled) { sendNotificationToContacts("Gun case has moved!", recipients) } else { - log.debug "$accelerationSensor has moved, texting $phone1" + log.debug "$accelerationSensor has moved, texting phone" sendSms(phone1, "Gun case has moved!") } } From dedb0f8465cbe97dc1fecb279a1a64ecf2b6fe3f Mon Sep 17 00:00:00 2001 From: twack Date: Wed, 7 Sep 2016 09:20:13 -0700 Subject: [PATCH 06/30] Update its-too-hot.groovy --- smartapps/smartthings/its-too-hot.src/its-too-hot.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy index 3ac1ab0..6ffcebf 100644 --- a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy +++ b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy @@ -69,7 +69,7 @@ def temperatureHandler(evt) { def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1 if (alreadySentSms) { - log.debug "SMS already sent to within the last $delta Minutes minutes" + log.debug "SMS already sent within the last $deltaMinutes minutes" // TODO: Send "Temperature back to normal" SMS, turn switch off } else { log.debug "Temperature rose above $tooHot: sending SMS and activating $mySwitch" From 159d3acf4fcfd575e4b5ca275739dbcc76d1f64d Mon Sep 17 00:00:00 2001 From: David Sainte-Claire Date: Wed, 7 Sep 2016 10:56:25 -0700 Subject: [PATCH 07/30] removed log messages from smartapp that may print sensitive information --- smartapps/smartthings/bon-voyage.src/bon-voyage.groovy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy b/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy index a843a71..cb9593d 100644 --- a/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy +++ b/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy @@ -49,13 +49,15 @@ preferences { def installed() { log.debug "Installed with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // commented out log statement because presence sensor label could contain user's name + //log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" subscribe(people, "presence", presence) } def updated() { log.debug "Updated with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // commented out log statement because presence sensor label could contain user's name + //log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" unsubscribe() subscribe(people, "presence", presence) } From 7568cbf781eb81b9d9f7a9c3b638e0fab43cbb97 Mon Sep 17 00:00:00 2001 From: Andrew Bresee Date: Wed, 7 Sep 2016 11:11:20 -0700 Subject: [PATCH 08/30] DVCSMP-1959: Security review - removing potentially confidential log statements (#1210) * Commented out log statement logging users access token * Commented out log.info logging potentially confidential device information * Remove log logging potentially confidential information on the app --- .../smartthings/lifx-connect.src/lifx-connect.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy index 587c311..7832b68 100644 --- a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy +++ b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy @@ -50,9 +50,9 @@ def authPage() { } def description = "Tap to enter LIFX credentials" def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below -// def redirectUrl = "${apiServerUrl}" - log.debug "app id: ${app.id}" - log.debug "redirect url: ${redirectUrl}" + // def redirectUrl = "${apiServerUrl}" + // log.debug "app id: ${app.id}" + // log.debug "redirect url: ${redirectUrl}"s return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) { section { href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account") @@ -372,7 +372,7 @@ def updateDevices() { def childDevice = getChildDevice(device.id) selectors.add("${device.id}") if (!childDevice) { - log.info("Adding device ${device.id}: ${device.product}") + // log.info("Adding device ${device.id}: ${device.product}") def data = [ label: device.label, level: Math.round((device.brightness ?: 1) * 100), From 614573a15c67a29a1638c9cab81e79d752c135b5 Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Wed, 7 Sep 2016 11:28:08 -0700 Subject: [PATCH 09/30] security fixes for SAs --- smartapps/smartthings/flood-alert.src/flood-alert.groovy | 4 ++-- .../greetings-earthling.src/greetings-earthling.groovy | 9 +++------ .../smartthings/habit-helper.src/habit-helper.groovy | 5 ++--- smartapps/smartthings/it-moved.src/it-moved.groovy | 6 +++--- .../presence-change-push.src/presence-change-push.groovy | 4 ++-- .../presence-change-text.src/presence-change-text.groovy | 4 ++-- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/smartapps/smartthings/flood-alert.src/flood-alert.groovy b/smartapps/smartthings/flood-alert.src/flood-alert.groovy index 73cae0b..f44aa48 100644 --- a/smartapps/smartthings/flood-alert.src/flood-alert.groovy +++ b/smartapps/smartthings/flood-alert.src/flood-alert.groovy @@ -54,10 +54,10 @@ def waterWetHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone within the last $deltaSeconds seconds" + log.debug "SMS already sent within the last $deltaSeconds seconds" } else { def msg = "${alarm.displayName} is wet!" - log.debug "$alarm is wet, texting $phone" + log.debug "$alarm is wet, texting phone number" if (location.contactBookEnabled) { sendNotificationToContacts(msg, recipients) diff --git a/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy b/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy index 2797c09..e97c684 100644 --- a/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy +++ b/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy @@ -47,13 +47,13 @@ preferences { def installed() { log.debug "Installed with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" subscribe(people, "presence", presence) } def updated() { log.debug "Updated with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" unsubscribe() subscribe(people, "presence", presence) } @@ -71,11 +71,10 @@ def presence(evt) def person = getPerson(evt) def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"} if (recentNotPresent) { - log.debug "skipping notification of arrival of ${person.displayName} because last departure was only ${now() - recentNotPresent.date.time} msec ago" + log.debug "skipping notification of arrival of Person because last departure was only ${now() - recentNotPresent.date.time} msec ago" } else { def message = "${person.displayName} arrived at home, changing mode to '${newMode}'" - log.info message send(message) setLocationMode(newMode) } @@ -106,6 +105,4 @@ private send(msg) { sendSms(phone, msg) } } - - log.debug msg } diff --git a/smartapps/smartthings/habit-helper.src/habit-helper.groovy b/smartapps/smartthings/habit-helper.src/habit-helper.groovy index 6e8194c..64d2052 100644 --- a/smartapps/smartthings/habit-helper.src/habit-helper.groovy +++ b/smartapps/smartthings/habit-helper.src/habit-helper.groovy @@ -57,12 +57,11 @@ def scheduleCheck() def message = message1 ?: "SmartThings - Habit Helper Reminder!" if (location.contactBookEnabled) { - log.debug "Texting reminder: ($message) to contacts:${recipients?.size()}" + log.debug "Texting reminder to contacts:${recipients?.size()}" sendNotificationToContacts(message, recipients) } else { - - log.debug "Texting reminder: ($message) to $phone1" + log.debug "Texting reminder" sendSms(phone1, message) } } diff --git a/smartapps/smartthings/it-moved.src/it-moved.groovy b/smartapps/smartthings/it-moved.src/it-moved.groovy index 1023807..a7a6899 100644 --- a/smartapps/smartthings/it-moved.src/it-moved.groovy +++ b/smartapps/smartthings/it-moved.src/it-moved.groovy @@ -53,14 +53,14 @@ def accelerationActiveHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + log.debug "SMS already sent within the last $deltaSeconds seconds" } else { if (location.contactBookEnabled) { - log.debug "$accelerationSensor has moved, texting contacts: ${recipients?.size()}" + log.debug "accelerationSensor has moved, texting contacts: ${recipients?.size()}" sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients) } else { - log.debug "$accelerationSensor has moved, texting $phone1" + log.debug "accelerationSensor has moved, sending text message" sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved") } } diff --git a/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy b/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy index af5fa72..04e8c90 100644 --- a/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy +++ b/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy @@ -41,10 +41,10 @@ def updated() { def presenceHandler(evt) { if (evt.value == "present") { - log.debug "${presence.label ?: presence.name} has arrived at the ${location}" + // log.debug "${presence.label ?: presence.name} has arrived at the ${location}" sendPush("${presence.label ?: presence.name} has arrived at the ${location}") } else if (evt.value == "not present") { - log.debug "${presence.label ?: presence.name} has left the ${location}" + // log.debug "${presence.label ?: presence.name} has left the ${location}" sendPush("${presence.label ?: presence.name} has left the ${location}") } } diff --git a/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy b/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy index d4ad1f3..4c4d5a2 100644 --- a/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy +++ b/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy @@ -47,7 +47,7 @@ def updated() { def presenceHandler(evt) { if (evt.value == "present") { - log.debug "${presence.label ?: presence.name} has arrived at the ${location}" + // log.debug "${presence.label ?: presence.name} has arrived at the ${location}" if (location.contactBookEnabled) { sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients) @@ -56,7 +56,7 @@ def presenceHandler(evt) { sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}") } } else if (evt.value == "not present") { - log.debug "${presence.label ?: presence.name} has left the ${location}" + // log.debug "${presence.label ?: presence.name} has left the ${location}" if (location.contactBookEnabled) { sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients) From 878eb66b8b9d2ebe019a0c1e20f1527d41bcec73 Mon Sep 17 00:00:00 2001 From: Andrew Bresee Date: Wed, 7 Sep 2016 13:30:35 -0700 Subject: [PATCH 10/30] Remove log logging personal information about a device (#1214) --- .../ridiculously-automated-garage-door.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy b/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy index f491c6c..824d8d9 100644 --- a/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy +++ b/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy @@ -67,7 +67,7 @@ def updated() { } def subscribe() { - log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}" + // log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}" subscribe(doorSensor, "contact", garageDoorContact) subscribe(cars, "presence", carPresence) From 324ac13afbd555003827971c0498701e699c0cca Mon Sep 17 00:00:00 2001 From: Andrew Bresee Date: Wed, 7 Sep 2016 15:07:01 -0700 Subject: [PATCH 11/30] DVCSMP-1959: Unsubscribe from a pointless method (#1215) * Unsubscribe from a pointless method * Remove subscription to pointless method --- .../smartthings/smart-security.src/smart-security.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/smart-security.src/smart-security.groovy b/smartapps/smartthings/smart-security.src/smart-security.groovy index 6d15cff..175346e 100644 --- a/smartapps/smartthings/smart-security.src/smart-security.groovy +++ b/smartapps/smartthings/smart-security.src/smart-security.groovy @@ -71,7 +71,7 @@ def updated() { private subscribeToEvents() { subscribe intrusionMotions, "motion", intruderMotion - subscribe residentMotions, "motion", residentMotion + // subscribe residentMotions, "motion", residentMotion subscribe intrusionContacts, "contact", contact subscribe alarms, "alarm", alarm subscribe(app, appTouch) @@ -156,6 +156,7 @@ def residentMotion(evt) // startReArmSequence() // } //} + unsubscribe(‘residentMotion’) } def contact(evt) @@ -214,7 +215,7 @@ def checkForReArm() } else { log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection" - } + } } private startAlarmSequence() From 91eb59a10d60f8d11f87e0d570fee45c57fff44e Mon Sep 17 00:00:00 2001 From: Andrew Bresee Date: Thu, 8 Sep 2016 04:45:30 -0700 Subject: [PATCH 12/30] DVCSMP-1959: Replace log logging personal phone number (#1217) * Replace log logging personal phone number * Removed commented out log --- .../text-me-when-it-opens.src/text-me-when-it-opens.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy b/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy index 08a9887..cbf8698 100644 --- a/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy +++ b/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy @@ -48,7 +48,7 @@ def updated() def contactOpenHandler(evt) { log.trace "$evt.value: $evt, $settings" - log.debug "$contact1 was opened, texting $phone1" + log.debug "$contact1 was opened, sending text" if (location.contactBookEnabled) { sendNotificationToContacts("Your ${contact1.label ?: contact1.name} was opened", recipients) } From 8777ec5f6de15c3209fea61d0e235ede684245f6 Mon Sep 17 00:00:00 2001 From: Luke Bredeson Date: Thu, 8 Sep 2016 10:06:38 -0500 Subject: [PATCH 13/30] DVCSMP-2020: smart-security app contains invalid code --- smartapps/smartthings/smart-security.src/smart-security.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/smart-security.src/smart-security.groovy b/smartapps/smartthings/smart-security.src/smart-security.groovy index 175346e..acc571e 100644 --- a/smartapps/smartthings/smart-security.src/smart-security.groovy +++ b/smartapps/smartthings/smart-security.src/smart-security.groovy @@ -156,7 +156,7 @@ def residentMotion(evt) // startReArmSequence() // } //} - unsubscribe(‘residentMotion’) + unsubscribe(residentMotions) } def contact(evt) From 826993cc4528223486ad73b92faca82c1177f4ff Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 31 Aug 2016 09:55:24 -0500 Subject: [PATCH 14/30] [DVCSMP-1979] Use async http for polling and refresh tokens. --- .../ecobee-sensor.src/ecobee-sensor.groovy | 2 +- .../ecobee-thermostat.groovy | 2 +- .../ecobee-connect.src/ecobee-connect.groovy | 333 ++++++++++++------ 3 files changed, 227 insertions(+), 110 deletions(-) diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index 381f6fc..1f028d9 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -67,6 +67,6 @@ def refresh() { void poll() { log.debug "Executing 'poll' using parent SmartApp" - parent.pollChild() + parent.poll() } diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index a16d28c..134fa5c 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -133,7 +133,7 @@ def refresh() { void poll() { log.debug "Executing 'poll' using parent SmartApp" - parent.pollChild() + parent.poll() } def generateEvent(Map results) { diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index d8c2179..4418d22 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -20,6 +20,8 @@ * JLH - 02-15-2014 - Fuller use of ecobee API * 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines */ +include 'asynchttp_v1' + definition( name: "Ecobee (Connect)", namespace: "smartthings", @@ -244,9 +246,7 @@ def getEcobeeThermostats() { uri: apiEndpoint, path: "/1/thermostat", headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(bodyParams)] + query: [json: toJson(bodyParams)] ] def stats = [:] @@ -265,9 +265,8 @@ def getEcobeeThermostats() { } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception polling children: " + e.response.data.status if (e.response.data.status.code == 14) { - atomicState.action = "getEcobeeThermostats" log.debug "Refreshing your auth_token!" - refreshAuthToken() + refreshAuthToken([async: false, nextAction: "getEcobeeThermostats"]) } } atomicState.thermostats = stats @@ -358,16 +357,22 @@ def initialize() { atomicState.timeSendPush = null atomicState.reAttempt = 0 - pollHandler() //first time polling data data from thermostat + initialPoll() //first time polling data data from thermostat //automatically update devices status every 5 mins runEvery5Minutes("poll") } -def pollHandler() { - log.debug "pollHandler()" - pollChildren(null) // Hit the ecobee API for update on all thermostats +/** + * Polls the child devices (synchronously). + * This is used during app install/update, and is synchronous + * to maintain current behavior that will cause install/update to fail + * if polling fails. + */ +def initialPoll() { + log.debug "initialPoll()" + pollChildrenSync() // Hit the ecobee API for update on all thermostats atomicState.thermostats.each {stat -> def dni = stat.key @@ -380,36 +385,38 @@ def pollHandler() { } } -def pollChildren(child = null) { - def thermostatIdsString = getChildDeviceIdsString() +/** + * Polls Ecobee (asynchronously) for updated device state data. + * Called from within this Connect SmartApp as well as the child + * devices. + */ +def poll() { + log.debug "polling asynchronously" + asynchttp_v1.get('asyncPollResponseHandler', getPollParams()) +} + +/** + * Makes a (synchronous) request to the Ecobee API to get the data for the thermostats. + * This request is made synchronously here because it is called as part of the + * install/updated lifecycle, and changing it to asynchronous during the install/update + * lifecycle may change the behavior if there is an error in polling. + * + * If further analysis shows that polling can be done asynchronously during + * install/update without any adverse consequences, this should then be made + * asynchronous just as the scheduled polling is. + */ +def pollChildrenSync() { log.debug "polling children: $thermostatIdsString" - def requestBody = [ - selection: [ - selectionType: "thermostats", - selectionMatch: thermostatIdsString, - includeExtendedRuntime: true, - includeSettings: true, - includeRuntime: true, - includeSensors: true - ] - ] + def params = getPollParams() + params.query << ["Content-Type": "application/json"] def result = false - - def pollParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(requestBody)] - ] + log.debug "making synchronous poll request" try{ - httpGet(pollParams) { resp -> + httpGet(params) { resp -> if(resp.status == 200) { - log.debug "poll results returned resp.data ${resp.data}" atomicState.remoteSensors = resp.data.thermostatList.remoteSensors updateSensorData() storeThermostatData(resp.data.thermostatList) @@ -420,40 +427,95 @@ def pollChildren(child = null) { } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception polling children: " + e.response.data.status if (e.response.data.status.code == 14) { - atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" - refreshAuthToken() + refreshAuthToken([async: false, nextAction: "pollChildrenSync"]) } } return result } -// Poll Child is invoked from the Child Device itself as part of the Poll Capability -def pollChild() { - def devices = getChildDevices() - - if (pollChildren()) { - devices.each { child -> - if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) { - if(atomicState.thermostats[child.device.deviceNetworkId] != null) { - def tData = atomicState.thermostats[child.device.deviceNetworkId] - log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" - child.generateEvent(tData.data) //parse received message from parent - } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" - return null - } - } - } - } else { - log.info "ERROR: pollChildren()" - return null - } - +/** + * Response handler for asynchronous request to get thermostat data. + * Given a successful response, updates the sensor data, stores the thermostat + * data, and generates child device events. + * + * If the access token has expired, will issue a request to refresh the token + * (and pending successful token refresh, the poll request will be made again). + */ +def asyncPollResponseHandler(response, data) { + log.trace "async poll response handler" + if (!response.hasError()) { + if (response.status == 200) { + def json + try { + json = response.getJson() + } catch (e) { + log.error ("error parsing JSON", e) + } + if (json) { + atomicState.remoteSensors = json.thermostatList.remoteSensors + updateSensorData() + storeThermostatData(json.thermostatList) + generateChildThermostatEvent() + } + } else { + log.warn "Response returned non-200 response. Status: ${response.status}, data: ${response.getData()}" + } + } else { + log.trace "Exception polling children: ${response.getErrorMessage()}" + def errorJson + try { + errorJson = response.getErrorJson() + } catch (e) { + log.error("Unable to parse error json response", e) + } + if (errorJson?.status?.code == 14) { + log.debug "Refreshing your auth_token!" + refreshAuthToken([async: true, nextAction: "poll"]) + } else { + log.warn "Error polling children that is not due to an expired token. Response: ${response.getErrorData()}" + } + } } -void poll() { - pollChild() +private getPollParams() { + def thermostatIdsString = getChildDeviceIdsString() + def requestBody = [ + selection: [ + selectionType: "thermostats", + selectionMatch: thermostatIdsString, + includeExtendedRuntime: true, + includeSettings: true, + includeRuntime: true, + includeSensors: true + ] + ] + return [ + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Authorization": "Bearer ${atomicState.authToken}"], + query: [json: toJson(requestBody)] + ] +} + +/** + * Calls each child thermostat device to generate an event with the thermostat + * data. + */ +def generateChildThermostatEvent() { + log.trace("generateChildThermostatEvent") + getChildDevices().each { child -> + if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ + if(atomicState.thermostats[child.device.deviceNetworkId] != null) { + def tData = atomicState.thermostats[child.device.deviceNetworkId] + log.debug "calling child.generateEvent($tData.data)" + child.generateEvent(tData.data) //parse received message from parent + } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { + log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" + return null + } + } + } } def availableModes(child) { @@ -553,47 +615,104 @@ def toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } -private refreshAuthToken() { - log.debug "refreshing auth token" +/** + * Uses the refresh token to get a new access token, then executes the nextAction. + * @param options - a map of options. valid options are async: true/false, which + * specifies if the refresh token request will be done asynchronously or not (default is false) + * nextAction: "nameOfMethod" specifies what method to execute after + * the token is refreshed (not required). + * (note: using a map as the parameter because we need to call it from a schedueled + * execution and we can only pass a data map to scheduled executions) + */ +private void refreshAuthToken(options) { + if(!atomicState.refreshToken) { + log.warn "Cannot not refresh OAuth token since there is no refreshToken stored" + } else { + def refreshParams = [ + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + ] + if (options.async) { + refreshAuthTokenAsync(refreshParams, options.nextAction) + } else { + refreshAuthTokenSync(refreshParams, options.nextAction) + } + } +} - if(!atomicState.refreshToken) { - log.warn "Can not refresh OAuth token since there is no refreshToken stored" - } else { - def refreshParams = [ - method: 'POST', - uri : apiEndpoint, - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], - ] - - def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." - //changed to httpPost - try { - def jsonMap - httpPost(refreshParams) { resp -> - if(resp.status == 200) { - log.debug "Token refreshed...calling saved RestAction now!" - debugEvent("Token refreshed ... calling saved RestAction now!") - saveTokenAndResumeAction(resp.data) - } +private void refreshAuthTokenSync(params, nextAction = null) { + try { + httpPost(refreshParams) { resp -> + if(resp.status == 200) { + log.debug "Token refreshed...calling saved RestAction now!" + debugEvent("Token refreshed ... calling saved RestAction now!") + saveTokenAndResumeAction(resp.data, nextAction) } - } catch (groovyx.net.http.HttpResponseException e) { - log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" - def reAttemptPeriod = 300 // in sec - if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc. - runIn(reAttemptPeriod, "refreshAuthToken") - } else if (e.statusCode == 401) { // unauthorized - atomicState.reAttempt = atomicState.reAttempt + 1 - log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}" - if (atomicState.reAttempt <= 3) { - runIn(reAttemptPeriod, "refreshAuthToken") - } else { - sendPushAndFeeds(notificationMessage) - atomicState.reAttempt = 0 - } - } - } - } + } + } catch (groovyx.net.http.HttpResponseException e) { + log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" + reauthTokenErrorHandler(e.statusCode) + } +} + +private void refreshAuthTokenAsync(refreshParams, nextAction = null) { + log.debug "making asynchronous refresh request" + asynchttp_v1.post('refreshTokenResponseHandler', refreshParams, [nextAction: nextAction]) +} + +/** + * The response handler for the request to refresh the authorization handler. + * Stores the new authorization token and refresh token, and executes any action + * (method) that failed due to the authorization token expiring. + */ +private void refreshTokenResponseHandler(response, data) { + if (!response.hasError()) { + if (response.status == 200) { + def json + try { + json = response.getJson() + } catch (e) { + log.error "error parsing json from response data: $response.data" + } + if (json) { + log.debug "asnyc refreshTokenHandler: Token refreshed...calling saved RestAction now!" + debugEvent("async Token refreshed ... calling saved RestAction now!") + saveTokenAndResumeAction(json, data.nextAction) + } else { + log.warn "successfully parsed json but result is empty or null" + } + } else { + log.debug "Non 200 response returned. Response code: ${response.code}, data: ${response.getData()}" + } + } else { + log.debug "async refreshTokenHandler: RESPONSE ERROR: ${response.getErrorJson()}" + reauthTokenErrorHandler(response.getErrorJson().code) + } +} + +/** + * Retries refreshing the authorization token. Will attempt to get the refresh + * token later, in case there were errors retrieving it. + * Will retry a fixed number of times before sending a push notification to the + * user instructing them to reauthenticate + */ +private void reauthTokenErrorHandler(responseCode) { + def retryInterval = 300 // in seconds + def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." + // might get non-401 error from exceeding 20 second app limit, connectivity issues, etc. + if (responseCode != 401) { + runIn(retryInterval, "refreshAuthToken", [async: true]) + } else if (responseCode == 401) { // unauthorized + atomicState.reAttempt = atomicState.reAttempt + 1 + log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}" + if (atomicState.reAttempt <= 3) { + runIn(retryInterval, "refreshAuthToken", [async: true]) + } else { + sendPushAndFeeds(notificationMessage) + atomicState.reAttempt = 0 + } + } } /** @@ -603,20 +722,20 @@ private refreshAuthToken() { * * @param json - an object representing the parsed JSON response from Ecobee */ -private void saveTokenAndResumeAction(json) { - log.debug "token response json: $json" +private void saveTokenAndResumeAction(json, String nextAction) { + def debugMessage = "token response, scope: ${json?.scope}, expires_in: ${json?.expires_in}, token_type: ${json?.token_type}" + log.debug "debugMessage" if (json) { - debugEvent("Response = $json") + debugEvent(debugMessage) atomicState.refreshToken = json?.refresh_token atomicState.authToken = json?.access_token - if (atomicState.action) { - log.debug "got refresh token, executing next action: ${atomicState.action}" - "${atomicState.action}"() + if (nextAction) { + log.debug "got refresh token, will execute next action (passed in!): $nextAction" + "$nextAction"() } } else { log.warn "did not get response body from refresh token response" } - atomicState.action = "" } /** @@ -756,7 +875,6 @@ private boolean sendCommandToEcobee(Map bodyParams) { try{ httpPost(cmdParams) { resp -> if(resp.status == 200) { - log.debug "updated ${resp.data}" def returnStatus = resp.data.status.code if (returnStatus == 0) { log.debug "Successful call to ecobee API." @@ -771,11 +889,10 @@ private boolean sendCommandToEcobee(Map bodyParams) { log.trace "Exception Sending Json: " + e.response.data.status debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}") if (e.response.data.status.code == 14) { - // TODO - figure out why we're setting the next action to be pollChildren + // TODO - figure out why we're setting the next action to be poll // after refreshing auth token. Is it to keep UI in sync, or just copy/paste error? - atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" - refreshAuthToken() + refreshAuthToken([async: true, nextAction: "poll"]) } else { debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.") log.error "Authentication error, invalid authentication method, lack of credentials, etc." From 5584020e9613854beb17306305d01696592d87eb Mon Sep 17 00:00:00 2001 From: David Sainte-Claire Date: Thu, 8 Sep 2016 10:31:24 -0700 Subject: [PATCH 15/30] removed logging of sensitive access tokens from smartapp --- smartapps/smartthings/withings.src/withings.groovy | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/smartapps/smartthings/withings.src/withings.groovy b/smartapps/smartthings/withings.src/withings.groovy index 24cfcd3..0e09ffc 100644 --- a/smartapps/smartthings/withings.src/withings.groovy +++ b/smartapps/smartthings/withings.src/withings.groovy @@ -60,7 +60,7 @@ def authPage() { def oauthInitUrl() { def token = getToken() - log.debug "initiateOauth got token: $token" + //log.debug "initiateOauth got token: $token" // store these for validate after the user takes the oauth journey state.oauth_request_token = token.oauth_token @@ -76,7 +76,7 @@ def getToken() { ] def requestTokenBaseUrl = "https://oauth.withings.com/account/request_token" def url = buildSignedUrl(requestTokenBaseUrl, params) - log.debug "getToken - url: $url" + //log.debug "getToken - url: $url" return getJsonFromUrl(url) } @@ -182,7 +182,7 @@ def exchangeToken() { def requestTokenBaseUrl = "https://oauth.withings.com/account/access_token" def url = buildSignedUrl(requestTokenBaseUrl, params, tokenSecret) - log.debug "signed url: $url with secret $tokenSecret" + //log.debug "signed url: $url with secret $tokenSecret" def token = getJsonFromUrl(url) @@ -198,8 +198,8 @@ def exchangeToken() { def load() { def json = get(getMeasurement(new Date() - 30)) - - log.debug "swapped, then received: $json" + // removed logging of actual json payload. Can be put back for debugging + log.debug "swapped, then received json" parse(data:json) def html = """ From 2f889de11a0a210b538050d6e0beb79b5ae3dfce Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Thu, 8 Sep 2016 10:36:28 -0700 Subject: [PATCH 16/30] fixes for dynamic execution --- .../curb/curb-control.src/curb-control.groovy | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/smartapps/curb/curb-control.src/curb-control.groovy b/smartapps/curb/curb-control.src/curb-control.groovy index 3a1c83d..4cea160 100644 --- a/smartapps/curb/curb-control.src/curb-control.groovy +++ b/smartapps/curb/curb-control.src/curb-control.groovy @@ -65,7 +65,16 @@ void updateSwitch() { private void updateAll(devices) { def command = request.JSON?.command if (command) { - devices."$command"() + switch(command) { + case "on": + devices.on() + break; + case "off": + devices.off() + break; + default: + httpError(403, "Access denied. This command is not supported by current capability.") + } } } @@ -77,7 +86,16 @@ private void update(devices) { if (!device) { httpError(404, "Device not found") } else { - device."$command"() + switch(command) { + case "on": + device.on() + break; + case "off": + device.off() + break; + default: + httpError(403, "Access denied. This command is not supported by current capability.") + } } } } From 6aa09bb05223d6634743213d72459e8f9b34f004 Mon Sep 17 00:00:00 2001 From: Andrew Bresee Date: Thu, 8 Sep 2016 10:40:35 -0700 Subject: [PATCH 17/30] Change log statement to not log personal phone number (#1224) --- .../text-me-when-theres-motion-and-im-not-here.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy b/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy index a3fac54..1094255 100644 --- a/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy +++ b/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy @@ -50,7 +50,7 @@ def updated() { def motionActiveHandler(evt) { log.trace "$evt.value: $evt, $settings" - + if (presence1.latestValue("presence") == "not present") { // Don't send a continuous stream of text messages def deltaSeconds = 10 @@ -60,14 +60,14 @@ def motionActiveHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + log.debug "SMS already sent within the last $deltaSeconds seconds" } else { if (location.contactBookEnabled) { log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}" sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients) } else { - log.debug "$motion1 has moved while you were out, texting $phone1" + log.debug "$motion1 has moved while you were out, sending text" sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out") } } From 48e9a4bd6afca9efbb9166faabe5c1ff64f2642d Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Thu, 8 Sep 2016 10:46:40 -0700 Subject: [PATCH 18/30] removed semi colons --- smartapps/curb/curb-control.src/curb-control.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smartapps/curb/curb-control.src/curb-control.groovy b/smartapps/curb/curb-control.src/curb-control.groovy index 4cea160..3bd1ec4 100644 --- a/smartapps/curb/curb-control.src/curb-control.groovy +++ b/smartapps/curb/curb-control.src/curb-control.groovy @@ -68,10 +68,10 @@ private void updateAll(devices) { switch(command) { case "on": devices.on() - break; + break case "off": devices.off() - break; + break default: httpError(403, "Access denied. This command is not supported by current capability.") } @@ -89,10 +89,10 @@ private void update(devices) { switch(command) { case "on": device.on() - break; + break case "off": device.off() - break; + break default: httpError(403, "Access denied. This command is not supported by current capability.") } From 006b5e7bea50e7172c360b0884088ba961f1ec0f Mon Sep 17 00:00:00 2001 From: David Sainte-Claire Date: Thu, 8 Sep 2016 10:54:04 -0700 Subject: [PATCH 19/30] removed logging of device and phone information due to security concerns --- .../smartthings/beacon-control.src/beacon-control.groovy | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/smartapps/smartthings/beacon-control.src/beacon-control.groovy b/smartapps/smartthings/beacon-control.src/beacon-control.groovy index 17fddad..007b455 100644 --- a/smartapps/smartthings/beacon-control.src/beacon-control.groovy +++ b/smartapps/smartthings/beacon-control.src/beacon-control.groovy @@ -114,13 +114,16 @@ def beaconHandler(evt) { if (allOk) { def data = new groovy.json.JsonSlurper().parseText(evt.data) - log.debug " data: $data - phones: " + phones*.deviceNetworkId + // removed logging of device names. can be added back for debugging + //log.debug " data: $data - phones: " + phones*.deviceNetworkId def beaconName = getBeaconName(evt) - log.debug " beaconName: $beaconName" + // removed logging of device names. can be added back for debugging + //log.debug " beaconName: $beaconName" def phoneName = getPhoneName(data) - log.debug " phoneName: $phoneName" + // removed logging of device names. can be added back for debugging + //log.debug " phoneName: $phoneName" if (phoneName != null) { def action = data.presence == "1" ? "arrived" : "left" def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName" From 43b836f4130b2ef6ceaf696bba00ed19276ee4f9 Mon Sep 17 00:00:00 2001 From: Amol Mundayoor Date: Thu, 8 Sep 2016 11:07:31 -0700 Subject: [PATCH 20/30] Removing debug logs for netatmo --- .../netatmo-connect.src/netatmo-connect.groovy | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy index 7a9e946..dd219c7 100644 --- a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -58,7 +58,6 @@ def authPage() { if (canInstallLabs()) { def redirectUrl = getBuildRedirectUrl() - log.debug "Redirect url = ${redirectUrl}" if (state.authToken) { description = "Tap 'Next' to proceed" @@ -113,14 +112,10 @@ def oauthInitUrl() { scope: "read_station" ] - log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}" - redirect (location: getVendorAuthPath() + toQueryString(oauthParams)) } def callback() { - log.debug "callback()>> params: $params, params.code ${params.code}" - def code = params.code def oauthState = params.state @@ -135,8 +130,6 @@ def callback() { scope: "read_station" ] - log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}" - def tokenUrl = getVendorTokenPath() def params = [ uri: tokenUrl, @@ -144,8 +137,6 @@ def callback() { body: tokenParams ] - log.debug "PARAMS: ${params}" - httpPost(params) { resp -> def slurper = new JsonSlurper() @@ -156,7 +147,6 @@ def callback() { state.refreshToken = data.refresh_token state.authToken = data.access_token state.tokenExpires = now() + (data.expires_in * 1000) - log.debug "swapped token: $resp.data" } } @@ -292,7 +282,6 @@ def refreshToken() { response.data.each {key, value -> def data = slurper.parseText(key); - log.debug "Data: $data" state.refreshToken = data.refresh_token state.accessToken = data.access_token From c703543f36843c6199c532fc0c9148add87168b8 Mon Sep 17 00:00:00 2001 From: Amol Mundayoor Date: Thu, 8 Sep 2016 11:20:27 -0700 Subject: [PATCH 21/30] Commenting out log.debug instead of removing them --- .../netatmo-connect.src/netatmo-connect.groovy | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy index dd219c7..65ed151 100644 --- a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -58,6 +58,7 @@ def authPage() { if (canInstallLabs()) { def redirectUrl = getBuildRedirectUrl() + // log.debug "Redirect url = ${redirectUrl}" if (state.authToken) { description = "Tap 'Next' to proceed" @@ -112,10 +113,14 @@ def oauthInitUrl() { scope: "read_station" ] + // log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}" + redirect (location: getVendorAuthPath() + toQueryString(oauthParams)) } def callback() { + // log.debug "callback()>> params: $params, params.code ${params.code}" + def code = params.code def oauthState = params.state @@ -130,6 +135,8 @@ def callback() { scope: "read_station" ] + // log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}" + def tokenUrl = getVendorTokenPath() def params = [ uri: tokenUrl, @@ -137,6 +144,8 @@ def callback() { body: tokenParams ] + // log.debug "PARAMS: ${params}" + httpPost(params) { resp -> def slurper = new JsonSlurper() @@ -147,6 +156,7 @@ def callback() { state.refreshToken = data.refresh_token state.authToken = data.access_token state.tokenExpires = now() + (data.expires_in * 1000) + // log.debug "swapped token: $resp.data" } } @@ -282,6 +292,7 @@ def refreshToken() { response.data.each {key, value -> def data = slurper.parseText(key); + // log.debug "Data: $data" state.refreshToken = data.refresh_token state.accessToken = data.access_token From b78bce55b2cece1fb463879a3d1e8123d7ca9712 Mon Sep 17 00:00:00 2001 From: Jason Terhune Date: Thu, 8 Sep 2016 10:18:25 -0500 Subject: [PATCH 22/30] Configure gradle to compile devicetypes and smartapps. --- build.gradle | 30 ++++++++++++++++++- circle.yml | 6 ++-- .../energy-alerts.src/energy-alerts.groovy | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 2334d49..2667f45 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { username smartThingsArtifactoryUserName password smartThingsArtifactoryPassword } - url "http://artifactory.smartthings.com/libs-release-local" + url "https://artifactory.smartthings.com/libs-release-local" } } } @@ -27,9 +27,37 @@ buildscript { repositories { mavenLocal() jcenter() + maven { + credentials { + username smartThingsArtifactoryUserName + password smartThingsArtifactoryPassword + } + url "https://artifactory.smartthings.com/libs-release-local" + } +} + +sourceSets { + devicetypes { + groovy { + srcDirs = ['devicetypes'] + } + } + smartapps { + groovy { + srcDirs = ['smartapps'] + } + } } dependencies { + devicetypesCompile 'org.codehaus.groovy:groovy-all:2.4.7' + devicetypesCompile 'smartthings:appengine-z-wave:0.1.2' + devicetypesCompile 'smartthings:appengine-zigbee:0.1.11' + smartappsCompile 'org.codehaus.groovy:groovy-all:2.4.7' + smartappsCompile 'smartthings:appengine-common:0.1.8' + smartappsCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1' + smartappsCompile 'org.grails:grails-web:2.3.11' + smartappsCompile 'org.json:json:20140107' } slackSendMessage { diff --git a/circle.yml b/circle.yml index 3785924..0bd6c53 100644 --- a/circle.yml +++ b/circle.yml @@ -3,9 +3,9 @@ machine: version: oraclejdk8 -dependencies: - override: - - echo "Nothing to do." +checkout: + post: + - ./gradlew compileSmartappsGroovy compileDevicetypesGroovy test: override: diff --git a/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy b/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy index 4a4c9ff..bd394a1 100644 --- a/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy +++ b/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy @@ -64,7 +64,7 @@ def meterHandler(evt) { def lastValue = atomicState.lastValue as double atomicState.lastValue = meterValue - def dUnit ? evt.unit : "Watts" + def dUnit = evt.unit ?: "Watts" def aboveThresholdValue = aboveThreshold as int if (meterValue > aboveThresholdValue) { From 81318bafacca8d36ba982bfc6c1944cb5f2c123c Mon Sep 17 00:00:00 2001 From: jackchi Date: Fri, 9 Sep 2016 14:48:19 -0700 Subject: [PATCH 23/30] [CHF-201] Removing DTH workaround now that all events go to Kafka --- .../smartthings/cree-bulb.src/cree-bulb.groovy | 16 +--------------- .../smartpower-outlet.groovy | 16 +--------------- .../smartsense-moisture-sensor.groovy | 17 +---------------- .../smartsense-motion-sensor.groovy | 17 +---------------- .../smartsense-multi-sensor.groovy | 17 +---------------- .../smartsense-open-closed-sensor.groovy | 17 +---------------- .../smartsense-temp-humidity-sensor.groovy | 16 +--------------- .../zigbee-dimmer.src/zigbee-dimmer.groovy | 16 +--------------- .../zigbee-rgbw-bulb.groovy | 16 +--------------- .../zigbee-white-color-temperature-bulb.groovy | 16 +--------------- 10 files changed, 10 insertions(+), 154 deletions(-) diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy index ed5e751..5837ba1 100644 --- a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy +++ b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy @@ -67,12 +67,6 @@ def parse(String description) { def resultMap = zigbee.getEvent(description) if (resultMap) { sendEvent(resultMap) - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() } else { log.debug "DID NOT PARSE MESSAGE for description : $description" @@ -96,15 +90,7 @@ def setLevel(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.levelRefresh() - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.levelRefresh() } def refresh() { diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index b978cc0..fe9d0be 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -101,12 +101,6 @@ def parse(String description) { else { def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off' sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true) - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() } } else { @@ -126,15 +120,7 @@ def on() { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.onOffRefresh() - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.onOffRefresh() } def refresh() { diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy index 2e9745d..f8ea7d9 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -101,13 +101,6 @@ def parse(String description) { map = parseIasMessage(description) } - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() - log.debug "Parse returned $map" def result = map ? createEvent(map) : null @@ -285,15 +278,7 @@ private Map getMoistureResult(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level } def refresh() { diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy index c5128b1..a278644 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -105,13 +105,6 @@ def parse(String description) { map = parseIasMessage(description) } - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() - log.debug "Parse returned $map" def result = map ? createEvent(map) : null @@ -296,15 +289,7 @@ private Map getMotionResult(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level } def refresh() { diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 94575bd..5fa2139 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -127,13 +127,6 @@ def parse(String description) { map = parseIasMessage(description) } - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() - def result = map ? createEvent(map) : null if (description?.startsWith('enroll request')) { @@ -378,15 +371,7 @@ private getAccelerationResult(numValue) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level } def refresh() { diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy index 7fbd099..8bee02a 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -92,13 +92,6 @@ def parse(String description) { map = parseIasMessage(description) } - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() - log.debug "Parse returned $map" def result = map ? createEvent(map) : null @@ -248,15 +241,7 @@ private Map getContactResult(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level } def refresh() { diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index b320853..26955c1 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -83,13 +83,6 @@ def parse(String description) { map = parseCustomMessage(description) } - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() - log.debug "Parse returned $map" return map ? createEvent(map) : null } @@ -253,14 +246,7 @@ private Map getHumidityResult(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level } def refresh() diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 1326496..ed71f9a 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -54,12 +54,6 @@ def parse(String description) { def event = zigbee.getEvent(description) if (event) { - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() if (event.name=="level" && event.value==0) {} else { sendEvent(event) @@ -86,15 +80,7 @@ def setLevel(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.onOffRefresh() - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.onOffRefresh() } def refresh() { diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy index e7137b6..f578311 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -85,12 +85,6 @@ def parse(String description) { def event = zigbee.getEvent(description) if (event) { log.debug event - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() if (event.name=="level" && event.value==0) {} else { if (event.name=="colorTemperature") { @@ -130,15 +124,7 @@ def off() { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.onOffRefresh() - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.onOffRefresh() } def refresh() { diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index de8424d..6d06b35 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 @@ -74,12 +74,6 @@ def parse(String description) { log.debug "description is $description" def event = zigbee.getEvent(description) if (event) { - // Temporary fix for the case when Device is OFFLINE and is connected again - if (state.lastActivity == null){ - state.lastActivity = now() - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } - state.lastActivity = now() if (event.name=="level" && event.value==0) {} else { if (event.name=="colorTemperature") { @@ -110,15 +104,7 @@ def setLevel(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - - if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){ - log.info "ping, alive=no, lastActivity=${state.lastActivity}" - state.lastActivity = null - return zigbee.onOffRefresh() - } else { - log.info "ping, alive=yes, lastActivity=${state.lastActivity}" - sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true) - } + return zigbee.onOffRefresh() } def refresh() { From 78ec280e83a136fc9ae80759f2fead86c122b75a Mon Sep 17 00:00:00 2001 From: Jason Terhune Date: Tue, 13 Sep 2016 08:37:20 -0500 Subject: [PATCH 24/30] Fix circle build. --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 0bd6c53..2def0cb 100644 --- a/circle.yml +++ b/circle.yml @@ -5,7 +5,7 @@ machine: checkout: post: - - ./gradlew compileSmartappsGroovy compileDevicetypesGroovy + - ./gradlew compileSmartappsGroovy compileDevicetypesGroovy -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" test: override: From e89e45e000fd95668e73ab88abe79c5a15b91b08 Mon Sep 17 00:00:00 2001 From: Jason Terhune Date: Tue, 13 Sep 2016 08:47:22 -0500 Subject: [PATCH 25/30] Another circle fix: dependencies task needs artifactory creds too. --- circle.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 2def0cb..a78f804 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,9 @@ machine: version: oraclejdk8 -checkout: +dependencies: + override: + - ./gradlew dependencies -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" post: - ./gradlew compileSmartappsGroovy compileDevicetypesGroovy -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" From 4523498dabca165ed4a30b2af1c91dc09077ec43 Mon Sep 17 00:00:00 2001 From: Andrew Bresee Date: Tue, 13 Sep 2016 10:23:55 -0700 Subject: [PATCH 26/30] Add parens to unsubscribe method (#1231) --- .../smart-home-ventilation.groovy | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy b/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy index 071a251..57fa957 100644 --- a/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy +++ b/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy @@ -14,7 +14,7 @@ * for the specific language governing permissions and limitations under the License. * */ - + definition( name: "Smart Home Ventilation", namespace: "MichaelStruck", @@ -164,7 +164,7 @@ def installed() { def updated() { unschedule() turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule - unsubscribe + unsubscribe() log.debug "Updated with settings: ${settings}" init() } @@ -174,12 +174,12 @@ def init() { schedule (midnightTime, midNight) subscribe(location, "mode", locationHandler) startProcess() -} +} // Common methods def startProcess () { - createDayArray() + createDayArray() state.dayCount=state.data.size() if (state.dayCount){ state.counter = 0 @@ -190,7 +190,7 @@ def startProcess () { def startDay() { def start = convertEpoch(state.data[state.counter].start) def stop = convertEpoch(state.data[state.counter].stop) - + runOnce(start, turnOnSwitch, [overwrite: true]) runOnce(stop, incDay, [overwrite: true]) } @@ -218,7 +218,7 @@ def locationHandler(evt) { } if (!result) { startProcess() - } + } } def midNight(){ @@ -238,7 +238,7 @@ def turnOffSwitch() { } log.debug "Home ventilation switches are off." } - + def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { def title = "" def dayListClean = "On " @@ -252,7 +252,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { dayListClean = "${dayListClean}, " } } - } + } else { dayListClean = "Every day" } @@ -272,7 +272,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { modeListClean = "${modeListClean} ${modePrefix}" } } - } + } else { modeListClean = "${modeListClean}all modes" } @@ -283,16 +283,16 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}" } if (on3 && off3) { - title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}" + title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}" } if (on4 && off4) { - title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}" + title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}" } if (on1 || on2 || on3 || on4) { title += "\n$modeListClean" - title += "\n$dayListClean" + title += "\n$dayListClean" } - + if (!on1 && !on2 && !on3 && !on4) { title="Click to configure scenario" } @@ -374,7 +374,7 @@ def createDayArray() { timeOk(timeOnD1, timeOffD1) timeOk(timeOnD2, timeOffD2) timeOk(timeOnD3, timeOffD3) - timeOk(timeOnD4, timeOffD4) + timeOk(timeOnD4, timeOffD4) } } state.data.sort{it.start} @@ -384,7 +384,7 @@ def createDayArray() { private def textAppName() { def text = "Smart Home Ventilation" -} +} private def textVersion() { def text = "Version 2.1.2 (05/31/2015)" @@ -416,4 +416,4 @@ private def textHelp() { "that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " + "avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " + "at 12:15 am or later." -} \ No newline at end of file +} From ae91f9bff5a552ed1c8d96b0b9afd6019d7f0435 Mon Sep 17 00:00:00 2001 From: jackchi Date: Tue, 13 Sep 2016 16:16:35 -0700 Subject: [PATCH 27/30] [CHF-353] Cree Bulb polling fix; reads status every 5 minutes --- devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy index f956800..979b51c 100644 --- a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy +++ b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy @@ -19,7 +19,6 @@ metadata { capability "Actuator" capability "Configuration" - capability "Polling" capability "Refresh" capability "Switch" capability "Switch Level" @@ -97,11 +96,14 @@ def refresh() { zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() } -def poll() { - zigbee.onOffRefresh() + zigbee.levelRefresh() +def healthPoll() { + def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))} } def configure() { + unschedule() + schedule("0 0/5 * * * ? *", "healthPoll") log.debug "Configuring Reporting and Bindings." // Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"]) From a7acc384a2187cb90d7d613df028aae80808a67f Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Tue, 13 Sep 2016 12:27:49 -0600 Subject: [PATCH 28/30] SSVD-2736 Philips Hue: Color Coordinator does not work -SSVD-2631 Double color events -SSVD-2601 Color picker control does not show the current color -Changed color model for Philips Hue to use hue/sat instead of x/y -Added color events in hex -Added HSV color conversion algorithms --- .../hue-connect.src/hue-connect.groovy | 155 +++++++++++++++--- 1 file changed, 130 insertions(+), 25 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 8df8621..adc64f6 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -801,10 +801,12 @@ def parse(childDevice, description) { } // Philips Hue priority for color is xy > ct > hs +// For SmartThings, try to always send hue, sat and hex private sendColorEvents(device, xy, hue, sat, ct, colormode = null) { if (device == null || (xy == null && hue == null && sat == null && ct == null)) return + def events = [:] // For now, only care about changing color temperature if requested by user if (ct != null && (colormode == "ct" || (xy == null && hue == null && sat == null))) { // for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below @@ -818,13 +820,13 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) { if (hue != null) { // 0-65535 def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int - device.sendEvent([name: "hue", value: value, descriptionText: "Color has changed"]) + events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false] } if (sat != null) { // 0-254 def value = Math.round(sat * 100 / 254) as int - device.sendEvent([name: "saturation", value: value, descriptionText: "Color has changed"]) + events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false] } // Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex @@ -836,17 +838,28 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) { def model = state.bulbs[id]?.modelid def hex = colorFromXY(xy, model) - // TODO Disabled until a solution for the jumping color picker can be figured out - //device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: false]) + // Create Hue and Saturation events if not previously existing + def hsv = hexToHsv(hex) + if (events["hue"] == null) + events["hue"] = [name: "hue", value: hsv[0], descriptionText: "Color has changed", displayed: false] + if (events["saturation"] == null) + events["saturation"] = [name: "saturation", value: hsv[1], descriptionText: "Color has changed", displayed: false] + + events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true] } else if (colormode == "hs" || colormode == null) { // colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above + def hueValue = (hue != null) ? events["hue"].value : Integer.parseInt("$device.currentHue") + def satValue = (sat != null) ? events["saturation"].value : Integer.parseInt("$device.currentSaturation") - // TODO Disabled until the standard behavior of lights is defined (hue and sat events are sent above) - //def hex = colorUtil.hslToHex((int) device.currentHue, (int) device.currentSaturation) - // device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed"]) + + def hex = hsvToHex(hueValue, satValue) + events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true] } - return debug + boolean sendColorChanged = false + events.each { + device.sendEvent(it.value) + } } private sendBasicEvents(device, param, value) { @@ -887,8 +900,6 @@ private handleCommandResponse(body) { def updates = [:] body.each { payload -> - log.debug $payload - if (payload?.success) { def childDeviceNetworkId = app.id + "/" def eventType @@ -1101,26 +1112,22 @@ def setColor(childDevice, huesettings) { def sat = null def xy = null - // For now ignore model to get a consistent color if same color is set across multiple devices - // def model = state.bulbs[getId(childDevice)]?.modelid - if (huesettings.hex != null) { + // Prefer hue/sat over hex to make sure it works with the majority of the smartapps + if (huesettings.hue != null || huesettings.sat != null) { + // If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to + // generate hue/sat events even though bridge will prioritize XY when setting color + if (huesettings.hue != null) + value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) + if (huesettings.saturation != null) + value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254) + } else if (huesettings.hex != null && false) { + // For now ignore model to get a consistent color if same color is set across multiple devices + // def model = state.bulbs[getId(childDevice)]?.modelid // value.xy = calculateXY(huesettings.hex, model) // Once groups, or scenes are introduced it might be a good idea to use unique models again value.xy = calculateXY(huesettings.hex) } - // If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to - // generate hue/sat events even though bridge will prioritize XY when setting color - if (huesettings.hue != null) - value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) - else - value.hue = Math.min(Math.round(childDevice.device?.currentValue("hue") * 65535 / 100), 65535) - - if (huesettings.saturation != null) - value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254) - else - value.sat = Math.min(Math.round(childDevice.device?.currentValue("saturation") * 254 / 100), 254) - /* Disabled for now due to bad behavior via Lightning Wizard if (!value.xy) { // Below will translate values to hex->XY to take into account the color support of the different hue types @@ -1657,3 +1664,101 @@ private boolean checkPointInLampsReach(p, colorPoints) { return false; } } + +/** + * Converts an RGB color in hex to HSV. + * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space. + * + * @param colorStr color value in hex (#ff03d3) + * + * @return HSV representation in an array (0-100) [hue, sat, value] + */ +def hexToHsv(colorStr){ + def r = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) / 255 + def g = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) / 255 + def b = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) / 255; + + def max = Math.max(Math.max(r, g), b) + def min = Math.min(Math.min(r, g), b) + + def h, s, v = max; + + def d = max - min; + s = max == 0 ? 0 : d / max; + + if(max == min){ + h = 0; + }else{ + switch(max){ + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + + return [(h * 100).round(), (s * 100).round(), (v * 100).round()]; +} + +/** + * Converts HSV color to RGB in hex. + * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space. + * + * @param hue hue 0-100 + * @param sat saturation 0-100 + * @param value value 0-100 (defaults to 100) + + * @return the color in hex (#ff03d3) + */ +def hsvToHex(hue, sat, value = 100){ + def r, g, b; + def h = hue / 100 + def s = sat / 100 + def v = value / 100 + + def i = Math.floor(h * 6); + def f = h * 6 - i; + def p = v * (1 - s); + def q = v * (1 - f * s); + def t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: + r = v + g = t + b = p + break + case 1: + r = q + g = v + b = p + break + case 2: + r = p + g = v + b = t + break + case 3: + r = p + g = q + b = v + break + case 4: + r = t + g = p + b = v + break + case 5: + r = v + g = p + b = q + break + } + + // Converting float components to int components. + def r1 = String.format("%02X", (int) (r * 255.0f)); + def g1 = String.format("%02X", (int) (g * 255.0f)); + def b1 = String.format("%02X", (int) (b * 255.0f)); + + return "#$r1$g1$b1" +} From 8b9bff15dc04820938a03d2daddc5d9885b6401b Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Mon, 19 Sep 2016 09:38:33 -0500 Subject: [PATCH 29/30] Revert "[DVCSMP-1979] Use async http for polling and refresh tokens." This reverts commit 826993cc4528223486ad73b92faca82c1177f4ff. --- .../ecobee-sensor.src/ecobee-sensor.groovy | 2 +- .../ecobee-thermostat.groovy | 2 +- .../ecobee-connect.src/ecobee-connect.groovy | 349 ++++++------------ 3 files changed, 118 insertions(+), 235 deletions(-) diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index 1f028d9..381f6fc 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -67,6 +67,6 @@ def refresh() { void poll() { log.debug "Executing 'poll' using parent SmartApp" - parent.poll() + parent.pollChild() } diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 134fa5c..a16d28c 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -133,7 +133,7 @@ def refresh() { void poll() { log.debug "Executing 'poll' using parent SmartApp" - parent.poll() + parent.pollChild() } def generateEvent(Map results) { diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 4418d22..d8c2179 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -20,8 +20,6 @@ * JLH - 02-15-2014 - Fuller use of ecobee API * 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines */ -include 'asynchttp_v1' - definition( name: "Ecobee (Connect)", namespace: "smartthings", @@ -246,7 +244,9 @@ def getEcobeeThermostats() { uri: apiEndpoint, path: "/1/thermostat", headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - query: [json: toJson(bodyParams)] + // TODO - the query string below is not consistent with the Ecobee docs: + // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml + query: [format: 'json', body: toJson(bodyParams)] ] def stats = [:] @@ -265,8 +265,9 @@ def getEcobeeThermostats() { } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception polling children: " + e.response.data.status if (e.response.data.status.code == 14) { + atomicState.action = "getEcobeeThermostats" log.debug "Refreshing your auth_token!" - refreshAuthToken([async: false, nextAction: "getEcobeeThermostats"]) + refreshAuthToken() } } atomicState.thermostats = stats @@ -357,22 +358,16 @@ def initialize() { atomicState.timeSendPush = null atomicState.reAttempt = 0 - initialPoll() //first time polling data data from thermostat + pollHandler() //first time polling data data from thermostat //automatically update devices status every 5 mins runEvery5Minutes("poll") } -/** - * Polls the child devices (synchronously). - * This is used during app install/update, and is synchronous - * to maintain current behavior that will cause install/update to fail - * if polling fails. - */ -def initialPoll() { - log.debug "initialPoll()" - pollChildrenSync() // Hit the ecobee API for update on all thermostats +def pollHandler() { + log.debug "pollHandler()" + pollChildren(null) // Hit the ecobee API for update on all thermostats atomicState.thermostats.each {stat -> def dni = stat.key @@ -385,101 +380,10 @@ def initialPoll() { } } -/** - * Polls Ecobee (asynchronously) for updated device state data. - * Called from within this Connect SmartApp as well as the child - * devices. - */ -def poll() { - log.debug "polling asynchronously" - asynchttp_v1.get('asyncPollResponseHandler', getPollParams()) -} - -/** - * Makes a (synchronous) request to the Ecobee API to get the data for the thermostats. - * This request is made synchronously here because it is called as part of the - * install/updated lifecycle, and changing it to asynchronous during the install/update - * lifecycle may change the behavior if there is an error in polling. - * - * If further analysis shows that polling can be done asynchronously during - * install/update without any adverse consequences, this should then be made - * asynchronous just as the scheduled polling is. - */ -def pollChildrenSync() { +def pollChildren(child = null) { + def thermostatIdsString = getChildDeviceIdsString() log.debug "polling children: $thermostatIdsString" - def params = getPollParams() - params.query << ["Content-Type": "application/json"] - - def result = false - log.debug "making synchronous poll request" - - try{ - httpGet(params) { resp -> - if(resp.status == 200) { - atomicState.remoteSensors = resp.data.thermostatList.remoteSensors - updateSensorData() - storeThermostatData(resp.data.thermostatList) - result = true - log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" - } - } - } catch (groovyx.net.http.HttpResponseException e) { - log.trace "Exception polling children: " + e.response.data.status - if (e.response.data.status.code == 14) { - log.debug "Refreshing your auth_token!" - refreshAuthToken([async: false, nextAction: "pollChildrenSync"]) - } - } - return result -} - -/** - * Response handler for asynchronous request to get thermostat data. - * Given a successful response, updates the sensor data, stores the thermostat - * data, and generates child device events. - * - * If the access token has expired, will issue a request to refresh the token - * (and pending successful token refresh, the poll request will be made again). - */ -def asyncPollResponseHandler(response, data) { - log.trace "async poll response handler" - if (!response.hasError()) { - if (response.status == 200) { - def json - try { - json = response.getJson() - } catch (e) { - log.error ("error parsing JSON", e) - } - if (json) { - atomicState.remoteSensors = json.thermostatList.remoteSensors - updateSensorData() - storeThermostatData(json.thermostatList) - generateChildThermostatEvent() - } - } else { - log.warn "Response returned non-200 response. Status: ${response.status}, data: ${response.getData()}" - } - } else { - log.trace "Exception polling children: ${response.getErrorMessage()}" - def errorJson - try { - errorJson = response.getErrorJson() - } catch (e) { - log.error("Unable to parse error json response", e) - } - if (errorJson?.status?.code == 14) { - log.debug "Refreshing your auth_token!" - refreshAuthToken([async: true, nextAction: "poll"]) - } else { - log.warn "Error polling children that is not due to an expired token. Response: ${response.getErrorData()}" - } - } -} - -private getPollParams() { - def thermostatIdsString = getChildDeviceIdsString() def requestBody = [ selection: [ selectionType: "thermostats", @@ -490,32 +394,66 @@ private getPollParams() { includeSensors: true ] ] - return [ + + def result = false + + def pollParams = [ uri: apiEndpoint, path: "/1/thermostat", - headers: ["Authorization": "Bearer ${atomicState.authToken}"], - query: [json: toJson(requestBody)] + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + // TODO - the query string below is not consistent with the Ecobee docs: + // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml + query: [format: 'json', body: toJson(requestBody)] ] + + try{ + httpGet(pollParams) { resp -> + if(resp.status == 200) { + log.debug "poll results returned resp.data ${resp.data}" + atomicState.remoteSensors = resp.data.thermostatList.remoteSensors + updateSensorData() + storeThermostatData(resp.data.thermostatList) + result = true + log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" + } + } + } catch (groovyx.net.http.HttpResponseException e) { + log.trace "Exception polling children: " + e.response.data.status + if (e.response.data.status.code == 14) { + atomicState.action = "pollChildren" + log.debug "Refreshing your auth_token!" + refreshAuthToken() + } + } + return result } -/** - * Calls each child thermostat device to generate an event with the thermostat - * data. - */ -def generateChildThermostatEvent() { - log.trace("generateChildThermostatEvent") - getChildDevices().each { child -> - if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ - if(atomicState.thermostats[child.device.deviceNetworkId] != null) { - def tData = atomicState.thermostats[child.device.deviceNetworkId] - log.debug "calling child.generateEvent($tData.data)" - child.generateEvent(tData.data) //parse received message from parent - } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" - return null - } - } - } +// Poll Child is invoked from the Child Device itself as part of the Poll Capability +def pollChild() { + def devices = getChildDevices() + + if (pollChildren()) { + devices.each { child -> + if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) { + if(atomicState.thermostats[child.device.deviceNetworkId] != null) { + def tData = atomicState.thermostats[child.device.deviceNetworkId] + log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" + child.generateEvent(tData.data) //parse received message from parent + } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { + log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" + return null + } + } + } + } else { + log.info "ERROR: pollChildren()" + return null + } + +} + +void poll() { + pollChild() } def availableModes(child) { @@ -615,104 +553,47 @@ def toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } -/** - * Uses the refresh token to get a new access token, then executes the nextAction. - * @param options - a map of options. valid options are async: true/false, which - * specifies if the refresh token request will be done asynchronously or not (default is false) - * nextAction: "nameOfMethod" specifies what method to execute after - * the token is refreshed (not required). - * (note: using a map as the parameter because we need to call it from a schedueled - * execution and we can only pass a data map to scheduled executions) - */ -private void refreshAuthToken(options) { - if(!atomicState.refreshToken) { - log.warn "Cannot not refresh OAuth token since there is no refreshToken stored" - } else { - def refreshParams = [ - uri : apiEndpoint, - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], - ] - if (options.async) { - refreshAuthTokenAsync(refreshParams, options.nextAction) - } else { - refreshAuthTokenSync(refreshParams, options.nextAction) - } - } -} +private refreshAuthToken() { + log.debug "refreshing auth token" -private void refreshAuthTokenSync(params, nextAction = null) { - try { - httpPost(refreshParams) { resp -> - if(resp.status == 200) { - log.debug "Token refreshed...calling saved RestAction now!" - debugEvent("Token refreshed ... calling saved RestAction now!") - saveTokenAndResumeAction(resp.data, nextAction) + if(!atomicState.refreshToken) { + log.warn "Can not refresh OAuth token since there is no refreshToken stored" + } else { + def refreshParams = [ + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + ] + + def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." + //changed to httpPost + try { + def jsonMap + httpPost(refreshParams) { resp -> + if(resp.status == 200) { + log.debug "Token refreshed...calling saved RestAction now!" + debugEvent("Token refreshed ... calling saved RestAction now!") + saveTokenAndResumeAction(resp.data) + } } - } - } catch (groovyx.net.http.HttpResponseException e) { - log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" - reauthTokenErrorHandler(e.statusCode) - } -} - -private void refreshAuthTokenAsync(refreshParams, nextAction = null) { - log.debug "making asynchronous refresh request" - asynchttp_v1.post('refreshTokenResponseHandler', refreshParams, [nextAction: nextAction]) -} - -/** - * The response handler for the request to refresh the authorization handler. - * Stores the new authorization token and refresh token, and executes any action - * (method) that failed due to the authorization token expiring. - */ -private void refreshTokenResponseHandler(response, data) { - if (!response.hasError()) { - if (response.status == 200) { - def json - try { - json = response.getJson() - } catch (e) { - log.error "error parsing json from response data: $response.data" - } - if (json) { - log.debug "asnyc refreshTokenHandler: Token refreshed...calling saved RestAction now!" - debugEvent("async Token refreshed ... calling saved RestAction now!") - saveTokenAndResumeAction(json, data.nextAction) - } else { - log.warn "successfully parsed json but result is empty or null" - } - } else { - log.debug "Non 200 response returned. Response code: ${response.code}, data: ${response.getData()}" - } - } else { - log.debug "async refreshTokenHandler: RESPONSE ERROR: ${response.getErrorJson()}" - reauthTokenErrorHandler(response.getErrorJson().code) - } -} - -/** - * Retries refreshing the authorization token. Will attempt to get the refresh - * token later, in case there were errors retrieving it. - * Will retry a fixed number of times before sending a push notification to the - * user instructing them to reauthenticate - */ -private void reauthTokenErrorHandler(responseCode) { - def retryInterval = 300 // in seconds - def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." - // might get non-401 error from exceeding 20 second app limit, connectivity issues, etc. - if (responseCode != 401) { - runIn(retryInterval, "refreshAuthToken", [async: true]) - } else if (responseCode == 401) { // unauthorized - atomicState.reAttempt = atomicState.reAttempt + 1 - log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}" - if (atomicState.reAttempt <= 3) { - runIn(retryInterval, "refreshAuthToken", [async: true]) - } else { - sendPushAndFeeds(notificationMessage) - atomicState.reAttempt = 0 - } - } + } catch (groovyx.net.http.HttpResponseException e) { + log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" + def reAttemptPeriod = 300 // in sec + if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc. + runIn(reAttemptPeriod, "refreshAuthToken") + } else if (e.statusCode == 401) { // unauthorized + atomicState.reAttempt = atomicState.reAttempt + 1 + log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}" + if (atomicState.reAttempt <= 3) { + runIn(reAttemptPeriod, "refreshAuthToken") + } else { + sendPushAndFeeds(notificationMessage) + atomicState.reAttempt = 0 + } + } + } + } } /** @@ -722,20 +603,20 @@ private void reauthTokenErrorHandler(responseCode) { * * @param json - an object representing the parsed JSON response from Ecobee */ -private void saveTokenAndResumeAction(json, String nextAction) { - def debugMessage = "token response, scope: ${json?.scope}, expires_in: ${json?.expires_in}, token_type: ${json?.token_type}" - log.debug "debugMessage" +private void saveTokenAndResumeAction(json) { + log.debug "token response json: $json" if (json) { - debugEvent(debugMessage) + debugEvent("Response = $json") atomicState.refreshToken = json?.refresh_token atomicState.authToken = json?.access_token - if (nextAction) { - log.debug "got refresh token, will execute next action (passed in!): $nextAction" - "$nextAction"() + if (atomicState.action) { + log.debug "got refresh token, executing next action: ${atomicState.action}" + "${atomicState.action}"() } } else { log.warn "did not get response body from refresh token response" } + atomicState.action = "" } /** @@ -875,6 +756,7 @@ private boolean sendCommandToEcobee(Map bodyParams) { try{ httpPost(cmdParams) { resp -> if(resp.status == 200) { + log.debug "updated ${resp.data}" def returnStatus = resp.data.status.code if (returnStatus == 0) { log.debug "Successful call to ecobee API." @@ -889,10 +771,11 @@ private boolean sendCommandToEcobee(Map bodyParams) { log.trace "Exception Sending Json: " + e.response.data.status debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}") if (e.response.data.status.code == 14) { - // TODO - figure out why we're setting the next action to be poll + // TODO - figure out why we're setting the next action to be pollChildren // after refreshing auth token. Is it to keep UI in sync, or just copy/paste error? + atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" - refreshAuthToken([async: true, nextAction: "poll"]) + refreshAuthToken() } else { debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.") log.error "Authentication error, invalid authentication method, lack of credentials, etc." From 260917d515b61f5e33d9ee349fef765018cc733a Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Mon, 19 Sep 2016 14:01:33 -0400 Subject: [PATCH 30/30] MKTP-829 - Moving disclaimer to first page (#1261) --- .../logitech-harmony-connect.groovy | 6 ++---- 1 file changed, 2 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 726c109..b7084c3 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -102,7 +102,8 @@ def authPage() { description = "Click to enter Harmony Credentials" 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 } + section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } + section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } } } else { //device discovery request every 5 //25 seconds @@ -119,7 +120,6 @@ def authPage() { discoverDevices() } return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { - section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } 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 } @@ -315,8 +315,6 @@ def installed() { } def updated() { - unsubscribe() - unschedule() if (!state.accessToken) { log.debug "About to create access token" createAccessToken()