From b7484ff0b83d8bb0a1450d851a9fc8bcfcb68792 Mon Sep 17 00:00:00 2001 From: Daniel Kurin Date: Sun, 7 Feb 2016 11:36:24 -0500 Subject: [PATCH 1/9] MSA-866: Currently, the SmartSense Moisture handles both the FortrezZ WWA01 and the '02, however the '02 sends a temperature measurement. This PR expands the existing DH to add a valueTile with that temperature data (per https://github.com/SmartThingsCommunity/SmartThingsPublic/pull/447) --- .../smartsense-moisture.groovy | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy index 5483cb1..43577f1 100644 --- a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy +++ b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy @@ -16,6 +16,7 @@ metadata { capability "Water Sensor" capability "Sensor" capability "Battery" + capability "Temperature Measurement" fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86" fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86" @@ -39,17 +40,29 @@ metadata { attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" } } - standardTile("temperature", "device.temperature", width: 2, height: 2) { + standardTile("temperatureState", "device.temperature", width: 2, height: 2) { state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0" state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - - main (["water", "temperature"]) - details(["water", "temperature", "battery"]) + main (["water", "temperatureState"]) + details(["water", "temperatureState", "temperature", "battery"]) } } @@ -115,7 +128,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) map.descriptionText = "${device.displayName} is ${map.value}" } if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) { - map.name = "temperature" + map.name = "temperatureState" if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"} if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"} if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"} @@ -129,17 +142,30 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) map } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { def map = [:] - map.name = "water" - map.value = cmd.value ? "wet" : "dry" - map.descriptionText = "${device.displayName} is ${map.value}" + if(cmd.sensorType == 1) { + map.name = "temperature" + if(cmd.scale == 0) { + map.value = getTemperature(cmd.scaledSensorValue) + } else { + map.value = cmd.scaledSensorValue + } + map.unit = location.temperatureScale + } map } +def getTemperature(value) { + if(location.temperatureScale == "C"){ + return value + } else { + return Math.round(celsiusToFahrenheit(value)) + } +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { log.debug "COMMAND CLASS: $cmd" -} - +} \ No newline at end of file From 81fb356a216b13d5b3d32e017fb97a878edc87e9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Fri, 12 Feb 2016 14:31:04 -0500 Subject: [PATCH 2/9] PROB-537 - Fix error in line 335 --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 053c266..b8dd89b 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -326,6 +326,7 @@ def addBulbs() { d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) } log.debug "created ${d.displayName} with id $dni" + d.refresh() } else { log.debug "$dni in not longer paired to the Hue Bridge or ID changed" } @@ -333,8 +334,8 @@ def addBulbs() { //backwards compatable newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name]) + d.refresh() } - d.refresh() } else { log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'" if (bulbs instanceof java.util.Map) { @@ -774,4 +775,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) { private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } -} \ No newline at end of file +} From d9aa1e378d1188c47214d59db9bc95a18f6901c7 Mon Sep 17 00:00:00 2001 From: vlaminck Date: Mon, 15 Feb 2016 21:39:06 -0600 Subject: [PATCH 3/9] remove segmented style input to prevent iOS crash --- .../smartthings/gentle-wake-up.src/gentle-wake-up.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy index 9b5f11c..11f295c 100644 --- a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy +++ b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy @@ -201,8 +201,8 @@ def completionPage() { section("Switches") { input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true) - if (completionSwitches || androidClient()) { - input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on") + if (completionSwitches) { + input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on") input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)") } } From 664af57708210f96efda286a6282de7418bfada8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Tue, 16 Feb 2016 09:35:21 -0500 Subject: [PATCH 4/9] Removed canInstallLabs() --- .../hue-connect.src/hue-connect.groovy | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index b8dd89b..b8d3f54 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -35,23 +35,11 @@ preferences { } def mainPage() { - if(canInstallLabs()) { - def bridges = bridgesDiscovered() - if (state.username && bridges) { - return bulbDiscovery() - } else { - return bridgeDiscovery() - } + def bridges = bridgesDiscovered() + if (state.username && bridges) { + return bulbDiscovery() } else { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { - section("Upgrade") { - paragraph "$upgradeNeeded" - } - } + return bridgeDiscovery() } } @@ -765,10 +753,6 @@ private String convertHexToIP(hex) { [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } -private Boolean canInstallLabs() { - return hasAllHubsOver("000.011.00603") -} - private Boolean hasAllHubsOver(String desiredFirmware) { return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } } From 86e097ba0a0297d277938cddcc8e050e81374b66 Mon Sep 17 00:00:00 2001 From: dylanbijnagte Date: Fri, 18 Dec 2015 12:27:57 -0600 Subject: [PATCH 5/9] add event translation --- .../i18n/messages.properties | 29 +++++++ .../notify-me-when.src/notify-me-when.groovy | 84 ++++++++++--------- 2 files changed, 74 insertions(+), 39 deletions(-) create mode 100644 smartapps/smartthings/notify-me-when.src/i18n/messages.properties diff --git a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties new file mode 100644 index 0000000..a50bd54 --- /dev/null +++ b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties @@ -0,0 +1,29 @@ +'''Acceleration Detected'''.ko=가속화 감지됨 +'''Arrival Of'''.ko=도착 +'''Both Push and SMS?'''.ko=푸시 메시지와 SMS를 모두 사용하시겠습니까? +'''Button Pushed'''.ko=버튼이 눌렸습니다 +'''Contact Closes'''.ko=접점 닫힘 +'''Contact Opens'''.ko=접점 열림 +'''Departure Of'''.ko=출발 +'''Message Text'''.ko=문자 메시지 +'''Minutes'''.ko=분 +'''Motion Here'''.ko=동작 +'''Phone Number (for SMS, optional)'''.ko=휴대전화 번호(문자 메시지 - 옵션) +'''Receive notifications when anything happens in your home.'''.ko=집 안에 무슨 일이 일어나면 알림이 전송됩니다. +'''Smoke Detected'''.ko=연기가 감지되었습니다 +'''Switch Turned Off'''.ko=스위치 꺼짐 +'''Switch Turned On'''.ko=스위치 꺼짐 +'''Choose one or more, when...'''.ko=다음의 경우 하나 이상 선택 +'''Yes'''.ko=예 +'''No'''.ko=아니요 +'''Send this message (optional, sends standard status message if not specified)'''.ko=이 메시지 전송(선택적, 지정되지 않은 경우 표준 상태 메시지를 보냅니다) +'''Via a push notification and/or an SMS message'''.ko=푸시 알림 및/또는 문자 메시지를 통해 +'''Set for specific mode(s)'''.ko=특정 모드 설정 +'''Tap to set'''.ko=눌러서 설정 +'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지작 간 최소 시간(선택 사항, 모든 메시지의 기본 설정) +'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 거주자는 적절한 국가 코드를 입력했는지 확인하십시오 +'''Water Sensor Wet'''.ko=Water Sensor에서 물이 감지되었습니다 +'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 +'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 +'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 +'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 diff --git a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy index 12fa2a7..f124260 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -20,19 +20,19 @@ * 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell. */ definition( - name: "Notify Me When", - namespace: "smartthings", - author: "SmartThings", - description: "Receive notifications when anything happens in your home.", - category: "Convenience", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" + name: "Notify Me When", + namespace: "smartthings", + author: "SmartThings", + description: "Receive notifications when anything happens in your home.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" ) preferences { section("Choose one or more, when..."){ input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw - input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true @@ -47,11 +47,11 @@ preferences { input "messageText", "text", title: "Message Text", required: false } section("Via a push notification and/or an SMS message"){ - input("recipients", "contact", title: "Send notifications to") { - input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false - paragraph "If outside the US please make sure to enter the proper country code" - input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] - } + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false + paragraph "If outside the US please make sure to enter the proper country code" + input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] + } } section("Minimum time between messages (optional, defaults to every message)") { input "frequency", "decimal", title: "Minutes", required: false @@ -71,7 +71,7 @@ def updated() { def subscribeToEvents() { subscribe(button, "button.pushed", eventHandler) //tw - subscribe(contact, "contact.open", eventHandler) + subscribe(contact, "contact.open", eventHandler) subscribe(contactClosed, "contact.closed", eventHandler) subscribe(acceleration, "acceleration.active", eventHandler) subscribe(motion, "motion.active", eventHandler) @@ -99,49 +99,55 @@ def eventHandler(evt) { } private sendMessage(evt) { - def msg = messageText ?: defaultText(evt) + String msg = messageText + Map options = [:] + + if (!messageText) { + msg = defaultText(evt) + options = [translatable: true, triggerEvent: evt] + } log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" - if (location.contactBookEnabled) { - sendNotificationToContacts(msg, recipients) - } - else { + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients, options) + } else { + if (!phone || pushAndPhone != 'No') { + log.debug 'sending push' + options.method = 'push' + //sendPush(msg) + } + if (phone) { + options.phone = phone + log.debug 'sending SMS' + //sendSms(phone, msg) + } + sendNotification(msg, options) + } - if (!phone || pushAndPhone != "No") { - log.debug "sending push" - sendPush(msg) - } - if (phone) { - log.debug "sending SMS" - sendSms(phone, msg) - } - } if (frequency) { state[evt.deviceId] = now() } } private defaultText(evt) { - if (evt.name == "presence") { - if (evt.value == "present") { + if (evt.name == 'presence') { + if (evt.value == 'present') { if (includeArticle) { - "$evt.linkText has arrived at the $location.name" + '{{ triggerEvent.linkText }} has arrived at the {{ location.name }}' } else { - "$evt.linkText has arrived at $location.name" + '{{ triggerEvent.linkText }} has arrived at {{ location.name }}' } - } - else { + } else { if (includeArticle) { - "$evt.linkText has left the $location.name" + '{{ triggerEvent.linkText }} has left the {{ location.name }}' } else { - "$evt.linkText has left $location.name" + '{{ triggerEvent.linkText }} has left {{ location.name }}' } } - } - else { - evt.descriptionText + } else { + '{{ triggerEvent.descriptionText }}' } } From 512bd3adc41d349d81f3041160fcc89f2e7cc1b8 Mon Sep 17 00:00:00 2001 From: dylanbijnagte Date: Tue, 16 Feb 2016 11:30:20 -0600 Subject: [PATCH 6/9] add missing translations --- .../smartthings/notify-me-when.src/i18n/messages.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties index a50bd54..a190968 100644 --- a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties +++ b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties @@ -27,3 +27,5 @@ '''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 '''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 '''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 +'''Assign a name'''.ko=이름 배정 +'''Choose Modes'''.ko=모드 선택 From 7d07b93694f4f1fd6f4fa7ad19931f111277a796 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Tue, 16 Feb 2016 14:38:37 -0500 Subject: [PATCH 7/9] PROB-870 - Harmony fails to save credentials It seams like the user removed some activities on Harmony side without removing them from SmartThings. This is causing an issue when adding new activities. This fix checks if the activity still exists before creating a new device. --- .../logitech-harmony-connect.groovy | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 03c5ccc..9fc1a5d 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -419,9 +419,11 @@ def addDevice() { def d = getChildDevice(dni) if(!d) { def newAction = state.HarmonyActivities.find { it.key == dni } - d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) - log.trace "created ${d.displayName} with id $dni" - poll() + if (newAction) { + d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) + log.trace "created ${d.displayName} with id $dni" + poll() + } } else { log.trace "found ${d.displayName} with id $dni already exists" } From 13d9137c9afeca7c5de0753403de9ae4434bfbf1 Mon Sep 17 00:00:00 2001 From: Yaima Valdivia Date: Tue, 16 Feb 2016 13:48:47 -0800 Subject: [PATCH 8/9] Ecobee 3 - https://smartthings.atlassian.net/browse/DEVC-285 https://smartthings.atlassian.net/browse/DEVC-285 https://smartthings.atlassian.net/browse/DVCSMP-1431 --- .../ecobee-sensor.src/ecobee-sensor.groovy | 12 +- .../ecobee-thermostat.groovy | 260 +++++++++------ .../ecobee-connect.src/ecobee-connect.groovy | 310 +++++++++--------- 3 files changed, 313 insertions(+), 269 deletions(-) diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index e984bb6..2753e9b 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -22,10 +22,6 @@ metadata { capability "Polling" } - simulator { - // TODO: define status and reply messages here - } - tiles { valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', unit:"F", @@ -56,16 +52,12 @@ metadata { } def refresh() { - log.debug "refresh..." + log.debug "refresh called" poll() } void poll() { log.debug "Executing 'poll' using parent SmartApp" - parent.pollChildren(this) -} + parent.pollChild(this) -//generate custom mobile activity feeds event -def generateActivityFeedsEvent(notificationMessage) { - sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) } diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index b439a63..7c718b3 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -19,34 +19,39 @@ metadata { definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") { capability "Actuator" capability "Thermostat" + capability "Temperature Measurement" capability "Polling" capability "Sensor" - capability "Refresh" + capability "Refresh" + capability "Relative Humidity Measurement" - command "generateEvent" - command "raiseSetpoint" - command "lowerSetpoint" - command "resumeProgram" - command "switchMode" + command "generateEvent" + command "raiseSetpoint" + command "lowerSetpoint" + command "resumeProgram" + command "switchMode" - attribute "thermostatSetpoint","number" - attribute "thermostatStatus","string" + attribute "thermostatSetpoint","number" + attribute "thermostatStatus","string" + attribute "maxHeatingSetpoint", "number" + attribute "minHeatingSetpoint", "number" + attribute "maxCoolingSetpoint", "number" + attribute "minCoolingSetpoint", "number" + attribute "deviceTemperatureUnit", "number" } - simulator { } - - tiles { + tiles { valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', unit:"F", - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] ) } standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { @@ -94,8 +99,11 @@ metadata { state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send" state "updating", label:"Working", icon: "st.secondary.secondary" } + valueTile("humidity", "device.humidity", decoration: "flat") { + state "humidity", label:'${currentValue}% humidity' + } main "temperature" - details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) + details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh", "humidity"]) } preferences { @@ -107,8 +115,6 @@ metadata { // parse events into attributes def parse(String description) { log.debug "Parsing '${description}'" - // TODO: handle '' attribute - } def refresh() { @@ -133,16 +139,24 @@ def generateEvent(Map results) { def isChange = false def isDisplayed = true def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name] + handlerName: name] - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { - def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F + if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) { + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue isChange = isTemperatureStateChange(device, name, value.toString()) isDisplayed = isChange event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed] - } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ + } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") { + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + event << [value: sendValue, displayed: false] + } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ isChange = isStateChange(device, name, value.toString()) event << [value: value.toString(), isStateChange: isChange, displayed: false] + } else if (name=="humidity") { + isChange = isStateChange(device, name, value.toString()) + event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"] } else { isChange = isStateChange(device, name, value.toString()) isDisplayed = isChange @@ -158,13 +172,19 @@ def generateEvent(Map results) { //return descriptionText to be shown on mobile activity feed private getThermostatDescriptionText(name, value, linkText) { if(name == "temperature") { - return "$linkText temperature is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "$linkText temperature is $sendValue ${location.temperatureScale}" } else if(name == "heatingSetpoint") { - return "heating setpoint is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "heating setpoint is $sendValue ${location.temperatureScale}" } else if(name == "coolingSetpoint"){ - return "cooling setpoint is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "cooling setpoint is $sendValue ${location.temperatureScale}" } else if (name == "thermostatMode") { return "thermostat mode is ${value}" @@ -172,26 +192,26 @@ private getThermostatDescriptionText(name, value, linkText) { } else if (name == "thermostatFanMode") { return "thermostat fan mode is ${value}" + } else if (name == "humidity") { + return "humidity is ${value} %" } else { return "${name} = ${value}" } } void setHeatingSetpoint(setpoint) { - setHeatingSetpoint(setpoint.toDouble()) -} - -void setHeatingSetpoint(Double setpoint) { -// def mode = device.currentValue("thermostatMode") - def heatingSetpoint = setpoint + log.debug "***heating setpoint $setpoint" + def heatingSetpoint = setpoint.toDouble() def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble() def deviceId = device.deviceNetworkId.split(/\./).last() + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint").toDouble() + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint").toDouble() //enforce limits of heatingSetpoint - if (heatingSetpoint > 79) { - heatingSetpoint = 79 - } else if (heatingSetpoint < 45) { - heatingSetpoint = 45 + if (heatingSetpoint > maxHeatingSetpoint) { + heatingSetpoint = maxHeatingSetpoint + } else if (heatingSetpoint < minHeatingSetpoint) { + heatingSetpoint = minHeatingSetpoint } //enforce limits of heatingSetpoint vs coolingSetpoint @@ -201,32 +221,34 @@ void setHeatingSetpoint(Double setpoint) { log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() } else { - log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app + log.error "Error setHeatingSetpoint(setpoint)" } } void setCoolingSetpoint(setpoint) { - setCoolingSetpoint(setpoint.toDouble()) -} - -void setCoolingSetpoint(Double setpoint) { -// def mode = device.currentValue("thermostatMode") + log.debug "***cooling setpoint $setpoint" def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble() - def coolingSetpoint = setpoint + def coolingSetpoint = setpoint.toDouble() def deviceId = device.deviceNetworkId.split(/\./).last() + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint").toDouble() + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint").toDouble() - if (coolingSetpoint > 92) { - coolingSetpoint = 92 - } else if (coolingSetpoint < 65) { - coolingSetpoint = 65 + + if (coolingSetpoint > maxCoolingSetpoint) { + coolingSetpoint = maxCoolingSetpoint + } else if (coolingSetpoint < minCoolingSetpoint) { + coolingSetpoint = minCoolingSetpoint } //enforce limits of heatingSetpoint vs coolingSetpoint @@ -236,15 +258,18 @@ void setCoolingSetpoint(Double setpoint) { log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() } else { - log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app + log.error "Error setCoolingSetpoint(setpoint)" } } @@ -448,25 +473,21 @@ def auto() { def fanOn() { log.debug "fanOn" // parent.setFanMode (this,"on") - } def fanAuto() { log.debug "fanAuto" // parent.setFanMode (this,"auto") - } def fanCirculate() { log.debug "fanCirculate" // parent.setFanMode (this,"circulate") - } def fanOff() { log.debug "fanOff" // parent.setFanMode (this,"off") - } def generateSetpointEvent() { @@ -476,20 +497,41 @@ def generateSetpointEvent() { def mode = device.currentValue("thermostatMode") log.debug "Current Mode = ${mode}" - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") log.debug "Heating Setpoint = ${heatingSetpoint}" - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint") log.debug "Cooling Setpoint = ${coolingSetpoint}" + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") + + if(location.temperatureScale == "C") + { + maxHeatingSetpoint = roundC(maxHeatingSetpoint) + maxCoolingSetpoint = roundC(maxCoolingSetpoint) + minHeatingSetpoint = roundC(minHeatingSetpoint) + minCoolingSetpoint = roundC(minCoolingSetpoint) + heatingSetpoint = roundC(heatingSetpoint) + coolingSetpoint = roundC(coolingSetpoint) + } + + sendEvent("name":"maxHeatingSetpoint", "value":maxHeatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"maxCoolingSetpoint", "value":maxCoolingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"minHeatingSetpoint", "value":minHeatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"minCoolingSetpoint", "value":minCoolingSetpoint, "unit":location.temperatureScale) + + if (mode == "heat") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint ) } else if (mode == "cool") { - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint) } else if (mode == "auto") { @@ -499,9 +541,9 @@ def generateSetpointEvent() { sendEvent("name":"thermostatSetpoint", "value":"Off") - } else if (mode == "emergencyHeat") { + } else if (mode == "auxHeatOnly") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint) } @@ -510,26 +552,30 @@ def generateSetpointEvent() { void raiseSetpoint() { def mode = device.currentValue("thermostatMode") def targetvalue + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") + if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow raiseSetpoint" } else { - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def thermostatSetpoint = device.currentValue("thermostatSetpoint") log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value as Integer + targetvalue = device.latestState('thermostatSetpoint').value + targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() } else { targetvalue = 0 } - targetvalue = targetvalue + 1 + targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5 - if (mode == "heat" && targetvalue > 79) { - targetvalue = 79 - } else if (mode == "cool" && targetvalue > 92) { - targetvalue = 92 + if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue > maxHeatingSetpoint) { + targetvalue = maxHeatingSetpoint + } else if (mode == "cool" && targetvalue > maxCoolingSetpoint) { + targetvalue = maxCoolingSetpoint } sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) @@ -543,25 +589,29 @@ void raiseSetpoint() { void lowerSetpoint() { def mode = device.currentValue("thermostatMode") def targetvalue + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") + if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow lowerSetpoint" } else { - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def thermostatSetpoint = device.currentValue("thermostatSetpoint") log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value as Integer + targetvalue = device.latestState('thermostatSetpoint').value + targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() } else { targetvalue = 0 } - targetvalue = targetvalue - 1 + targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5 - if (mode == "heat" && targetvalue.toInteger() < 45) { - targetvalue = 45 - } else if (mode == "cool" && targetvalue.toInteger() < 65) { - targetvalue = 65 + if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue < minHeatingSetpoint) { + targetvalue = minHeatingSetpoint + } else if (mode == "cool" && targetvalue < minCoolingSetpoint) { + targetvalue = minCoolingSetpoint } sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) @@ -575,15 +625,15 @@ void lowerSetpoint() { void alterSetpoint(temp) { def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") def deviceId = device.deviceNetworkId.split(/\./).last() def targetHeatingSetpoint def targetCoolingSetpoint //step1: check thermostatMode, enforce limits before sending request to cloud - if (mode == "heat"){ + if (mode == "heat" || mode == "auxHeatOnly"){ if (temp.value > coolingSetpoint){ targetHeatingSetpoint = temp.value targetCoolingSetpoint = temp.value @@ -602,19 +652,22 @@ void alterSetpoint(temp) { } } - log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " + - "coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}" + log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " + + "coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}" def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - //step2: call parent.setHold to send http request to 3rd party cloud - if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) { - sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false) + + def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint + + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false) sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint) sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint) log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}" } else { log.error "Error alterSetpoint()" - if (mode == "heat"){ + if (mode == "heat" || mode == "auxHeatOnly"){ sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false) } else if (mode == "cool") { sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false) @@ -626,9 +679,9 @@ void alterSetpoint(temp) { def generateStatusEvent() { def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def temperature = device.currentValue("temperature").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def temperature = device.currentValue("temperature") def statusText @@ -643,14 +696,14 @@ def generateStatusEvent() { if (temperature >= heatingSetpoint) statusText = "Right Now: Idle" else - statusText = "Heating to ${heatingSetpoint}° F" + statusText = "Heating to ${heatingSetpoint} ${location.temperatureScale}" } else if (mode == "cool") { if (temperature <= coolingSetpoint) statusText = "Right Now: Idle" else - statusText = "Cooling to ${coolingSetpoint}° F" + statusText = "Cooling to ${coolingSetpoint} ${location.temperatureScale}" } else if (mode == "auto") { @@ -660,7 +713,7 @@ def generateStatusEvent() { statusText = "Right Now: Off" - } else if (mode == "emergencyHeat") { + } else if (mode == "auxHeatOnly") { statusText = "Emergency Heat" @@ -673,7 +726,18 @@ def generateStatusEvent() { sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true) } -//generate custom mobile activity feeds event def generateActivityFeedsEvent(notificationMessage) { sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) } + +def roundC (tempC) { + return (Math.round(tempC.toDouble() * 2))/2 +} + +def convertFtoC (tempF) { + return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) +} + +def convertCtoF (tempC) { + return (Math.round(tempC * (9/5)) + 32).toInteger() +} diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 474a4f2..11d6f55 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -28,7 +28,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", - singleInstance: true + singleInstance: true ) { appSetting "clientId" } @@ -61,7 +61,7 @@ def authPage() { description = "Click to enter Ecobee Credentials" } - def redirectUrl = buildRedirectUrl //"${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}" + def redirectUrl = buildRedirectUrl log.debug "RedirectUrl = ${redirectUrl}" // get rid of next button until the user is actually auth'd if (!oauthTokenProvided) { @@ -103,7 +103,7 @@ def oauthInitUrl() { scope: "smartRead,smartWrite", client_id: smartThingsClientId, state: atomicState.oauthInitState, - redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" + redirect_uri: callbackUrl ] redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}") @@ -115,14 +115,13 @@ def callback() { def code = params.code def oauthState = params.state - //verify oauthState == atomicState.oauthInitState, so the callback corresponds to the authentication request if (oauthState == atomicState.oauthInitState){ def tokenParams = [ grant_type: "authorization_code", code : code, client_id : smartThingsClientId, - redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" + redirect_uri: callbackUrl ] def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" @@ -247,32 +246,32 @@ def getEcobeeThermostats() { ] def stats = [:] - try { - httpGet(deviceListParams) { resp -> + try { + httpGet(deviceListParams) { resp -> - if (resp.status == 200) { - resp.data.thermostatList.each { stat -> - atomicState.remoteSensors = stat.remoteSensors - def dni = [app.id, stat.identifier].join('.') - stats[dni] = getThermostatDisplayName(stat) - } - } else { - log.debug "http status: ${resp.status}" - //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) { - log.debug "Storing the failed action to try later" - atomicState.action = "getEcobeeThermostats" - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } else { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - } - } catch(Exception e) { - log.debug "___exception getEcobeeThermostats(): " + e - refreshAuthToken() - } + if (resp.status == 200) { + resp.data.thermostatList.each { stat -> + atomicState.remoteSensors = stat.remoteSensors + def dni = [app.id, stat.identifier].join('.') + stats[dni] = getThermostatDisplayName(stat) + } + } else { + log.debug "http status: ${resp.status}" + //refresh the auth token + if (resp.data.status.code == 14) { + log.debug "Storing the failed action to try later" + atomicState.action = "getEcobeeThermostats" + log.debug "Refreshing your auth_token!" + refreshAuthToken() + } else { + log.error "Authentication error, invalid authentication method, lack of credentials, etc." + } + } + } + } catch(Exception e) { + log.debug "___exception getEcobeeThermostats(): " + e + refreshAuthToken() + } atomicState.thermostats = stats return stats } @@ -317,7 +316,7 @@ def initialize() { def devices = thermostats.collect { dni -> def d = getChildDevice(dni) if(!d) { - d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"Ecobee Thermostat:${atomicState.thermostats[dni]}"]) + d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"${atomicState.thermostats[dni]}" ?: "Ecobee Thermostat"]) log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -328,7 +327,7 @@ def initialize() { def sensors = ecobeesensors.collect { dni -> def d = getChildDevice(dni) if(!d) { - d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"Ecobee Sensor:${atomicState.sensors[dni]}"]) + d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"${atomicState.sensors[dni]}" ?:"Ecobee Sensor"]) log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -354,21 +353,17 @@ def initialize() { atomicState.thermostatData = [:] //reset Map to store thermostat data - //send activity feeds to tell that device is connected - def notificationMessage = "is connected to SmartThings" - sendActivityFeeds(notificationMessage) - state.timeSendPush = null + //send activity feeds to tell that device is connected + def notificationMessage = "is connected to SmartThings" + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = null + atomicState.reAttempt = 0 pollHandler() //first time polling data data from thermostat //automatically update devices status every 5 mins runEvery5Minutes("poll") - //since access_token expires every 2 hours - runEvery1Hour("refreshAuthToken") - - atomicState.reAttempt = 0 - } def pollHandler() { @@ -389,18 +384,10 @@ def pollHandler() { def pollChildren(child = null) { def thermostatIdsString = getChildDeviceIdsString() log.debug "polling children: $thermostatIdsString" + def data = "" def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}' def result = false - // // TODO: test this: - // - // def jsonRequestBody = toJson([ - // selection:[ - // selectionType: "thermostats", - // selectionMatch: getChildDeviceIdsString(), - // includeRuntime: true - // ] - // ]) def pollParams = [ uri: apiEndpoint, @@ -411,11 +398,6 @@ def pollChildren(child = null) { try{ httpGet(pollParams) { resp -> - -// if (resp.data) { -// debugEventFromParent(child, "pollChildren(child) >> resp.status = ${resp.status}, resp.data = ${resp.data}") -// } - if(resp.status == 200) { log.debug "poll results returned resp.data ${resp.data}" atomicState.remoteSensors = resp.data.thermostatList.remoteSensors @@ -426,20 +408,41 @@ def pollChildren(child = null) { log.debug "updating dni $dni" - def data = [ + data = [ coolMode: (stat.settings.coolStages > 0), heatMode: (stat.settings.heatStages > 0), + deviceTemperatureUnit: stat.settings.useCelsius, + minHeatingSetpoint: (stat.settings.heatRangeLow / 10), + maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), + minCoolingSetpoint: (stat.settings.coolRangeLow / 10), + maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), autoMode: stat.settings.autoHeatCoolFeatureEnabled, auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), - temperature: stat.runtime.actualTemperature / 10, + temperature: (stat.runtime.actualTemperature / 10), heatingSetpoint: stat.runtime.desiredHeat / 10, coolingSetpoint: stat.runtime.desiredCool / 10, - thermostatMode: stat.settings.hvacMode + thermostatMode: stat.settings.hvacMode, + humidity: stat.runtime.actualHumidity ] - data["temperature"] = data["temperature"] ? data["temperature"].toDouble().toInteger() : data["temperature"] - data["heatingSetpoint"] = data["heatingSetpoint"] ? data["heatingSetpoint"].toDouble().toInteger() : data["heatingSetpoint"] - data["coolingSetpoint"] = data["coolingSetpoint"] ? data["coolingSetpoint"].toDouble().toInteger() : data["coolingSetpoint"] -// debugEventFromParent(child, "Event Data = ${data}") + + if (location.temperatureScale == "F") + { + data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] + data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] + data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] + data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] + data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] + data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] + data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] + + } + + if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { + data["deviceTemperatureUnit"] = "F" + + } else { + data["deviceTemperatureUnit"] = "C" + } collector[dni] = [data:data] return collector @@ -450,9 +453,8 @@ def pollChildren(child = null) { log.error "polling children & got http status ${resp.status}" //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) { - log.debug "Storing the failed action to try later" - atomicState.action = "pollChildren"; + if (resp.data.status.code == 14) { + atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" refreshAuthToken() } @@ -463,7 +465,6 @@ def pollChildren(child = null) { } } catch(Exception e) { log.debug "___exception polling children: " + e -// debugEventFromParent(child, "___exception polling children: " + e) refreshAuthToken() } return result @@ -476,18 +477,14 @@ def pollChild(child){ if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ if(atomicState.thermostats[child.device.deviceNetworkId] != null) { def tData = atomicState.thermostats[child.device.deviceNetworkId] -// debugEventFromParent(child, "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}") //TODO comment log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" child.generateEvent(tData.data) //parse received message from parent -// return tData.data } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { -// debugEventFromParent(child, "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling") //TODO comment log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" return null } } } else { -// debugEventFromParent(child, "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling") //TODO comment log.info "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling" return null } @@ -513,9 +510,6 @@ def availableModes(child) { { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - // TODO: flag device as in error state - // child.errorState = true - return null } @@ -542,8 +536,6 @@ def currentMode(child) { if(!tData) { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - // TODO: flag device as in error state - // child.errorState = true return null } @@ -561,8 +553,12 @@ def updateSensorData() { def occupancy = "" it.capability.each { if (it.type == "temperature") { - temperature = it.value as Double - temperature = (temperature / 10).toInteger() + if (location.temperatureScale == "F") { + temperature = Math.round(it.value.toDouble() / 10) + } else { + temperature = convertFtoC(it.value.toDouble() / 10) + } + } else if (it.type == "occupancy") { if(it.value == "true") occupancy = "active" @@ -575,7 +571,6 @@ def updateSensorData() { if(d) { d.sendEvent(name:"temperature", value: temperature) d.sendEvent(name:"motion", value: occupancy) -// debugEventFromParent(d, "temperature : ${temperature}, motion:${occupancy}") } } } @@ -595,64 +590,63 @@ def toQueryString(Map m) { } private refreshAuthToken() { - log.debug "refreshing auth token" + log.debug "refreshing auth token" - if(!atomicState.refreshToken) { - log.warn "Can not refresh OAuth token since there is no refreshToken stored" - } else { + 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 refreshParams = [ + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + ] - log.debug refreshParams + log.debug refreshParams - 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 -> + 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!" + if(resp.status == 200) { + log.debug "Token refreshed...calling saved RestAction now!" - debugEvent("Token refreshed ... calling saved RestAction now!") + debugEvent("Token refreshed ... calling saved RestAction now!") - log.debug resp + log.debug resp - jsonMap = resp.data + jsonMap = resp.data - if(resp.data) { + if(resp.data) { - log.debug resp.data - debugEvent("Response = ${resp.data}") + log.debug resp.data + debugEvent("Response = ${resp.data}") - atomicState.refreshToken = resp?.data?.refresh_token - atomicState.authToken = resp?.data?.access_token + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token - debugEvent("Refresh Token = ${atomicState.refreshToken}") - debugEvent("OAUTH Token = ${atomicState.authToken}") + debugEvent("Refresh Token = ${atomicState.refreshToken}") + debugEvent("OAUTH Token = ${atomicState.authToken}") - if(atomicState.action && atomicState.action != "") { - log.debug "Executing next action: ${atomicState.action}" + if(atomicState.action && atomicState.action != "") { + log.debug "Executing next action: ${atomicState.action}" - "${atomicState.action}"() + "${atomicState.action}"() - //remove saved action - atomicState.action = "" - } + atomicState.action = "" + } - } - atomicState.action = "" - } else { - log.debug "refresh failed ${resp.status} : ${resp.status.code}" - } - } - } catch(Exception e) { - log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" + } + atomicState.action = "" + } else { + log.debug "refresh failed ${resp.status} : ${resp.status.code}" + } + } + } 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") @@ -665,20 +659,16 @@ private refreshAuthToken() { sendPushAndFeeds(notificationMessage) atomicState.reAttempt = 0 } - } - } - } + } + } + } } def resumeProgram(child, deviceId) { -// def thermostatIdsString = getChildDeviceIdsString() -// log.debug "resumeProgram children: $thermostatIdsString" def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' - //, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } } def result = sendJson(jsonRequestBody) -// debugEventFromParent(child, "resumeProgram(child) with result ${result}") return result } @@ -687,27 +677,16 @@ def setHold(child, heating, cooling, deviceId, sendHoldType) { int h = heating * 10 int c = cooling * 10 -// log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c" -// def thermostatIdsString = getChildDeviceIdsString() def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}' -// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' def result = sendJson(child, jsonRequestBody) -// debugEventFromParent(child, "setHold: heating: ${h}, cooling: ${c} with result ${result}") return result } def setMode(child, mode, deviceId) { -// def thermostatIdsString = getChildDeviceIdsString() -// log.debug "setCoolingSetpoint children: $thermostatIdsString" - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' -// log.debug "Mode Request Body = ${jsonRequestBody}" -// debugEvent ("Mode Request Body = ${jsonRequestBody}") - def result = sendJson(jsonRequestBody) -// debugEventFromParent(child, "setMode to ${mode} with result ${result}") return result } @@ -724,8 +703,6 @@ def sendJson(child = null, String jsonBody) { try{ httpPost(cmdParams) { resp -> -// debugEventFromParent(child, "sendJson >> resp.status ${resp.status}, resp.data: ${resp.data}") - if(resp.status == 200) { log.debug "updated ${resp.data}" @@ -741,8 +718,7 @@ def sendJson(child = null, String jsonBody) { debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}") //refresh the auth token - if (resp.status == 500 && resp.status.code == 14) { - //log.debug "Storing the failed action to try later" + if (resp.status.code == 14) { log.debug "Refreshing your auth_token!" debugEvent ("Refreshing OAUTH Token") refreshAuthToken() @@ -757,7 +733,7 @@ def sendJson(child = null, String jsonBody) { } catch(Exception e) { log.debug "Exception Sending Json: " + e debugEvent ("Exception Sending JSON: " + e) - refreshAuthToken() + refreshAuthToken() return false } @@ -794,25 +770,37 @@ def debugEventFromParent(child, message) { //send both push notification and mobile activity feeds def sendPushAndFeeds(notificationMessage){ - log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" - log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" - if (atomicState.timeSendPush){ - if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day - sendPush("Your Ecobee thermostat " + notificationMessage) - sendActivityFeeds(notificationMessage) - atomicState.timeSendPush = now() - } - } else { - sendPush("Your Ecobee thermostat " + notificationMessage) - sendActivityFeeds(notificationMessage) - atomicState.timeSendPush = now() - } - atomicState.authToken = null + log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" + log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" + if (atomicState.timeSendPush){ + if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + } else { + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + atomicState.authToken = null } def sendActivityFeeds(notificationMessage) { - def devices = getChildDevices() - devices.each { child -> - child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent - } + def devices = getChildDevices() + devices.each { child -> + child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent + } +} + +def roundC (tempC) { + return String.format("%.1f", (Math.round(tempC * 2))/2) +} + +def convertFtoC (tempF) { + return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) +} + +def convertCtoF (tempC) { + return (Math.round(tempC * (9/5)) + 32).toInteger() } From 69a6fc4f9e3420b002d980420959fd0b6d9bc571 Mon Sep 17 00:00:00 2001 From: Oso Technologies Date: Wed, 17 Feb 2016 13:34:04 -0600 Subject: [PATCH 9/9] MSA-884: This is a quick update to a previously published device handler to accept the "update" packet that occurs when the zigbee pairs with the hub to stop an exception due to it not being in the map format that other packets are in. --- devicetypes/osotech/plantlink.src/plantlink.groovy | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/devicetypes/osotech/plantlink.src/plantlink.groovy b/devicetypes/osotech/plantlink.src/plantlink.groovy index b1eb898..1633fb5 100644 --- a/devicetypes/osotech/plantlink.src/plantlink.groovy +++ b/devicetypes/osotech/plantlink.src/plantlink.groovy @@ -120,7 +120,7 @@ def setInstallSmartApp(value){ } def parse(String description) { - + log.debug description def description_map = parseDescriptionAsMap(description) def event_name = "" def measurement_map = [ @@ -129,10 +129,7 @@ def parse(String description) { zigbeedeviceid: device.zigbeeId, created: new Date().time /1000 as int ] - if (description_map.cluster == "0000"){ - /* version number, not used */ - - } else if (description_map.cluster == "0001"){ + if (description_map.cluster == "0001"){ /* battery voltage in mV (device needs minimium 2.1v to run) */ log.debug "PlantLink - id ${device.zigbeeId} battery ${description_map.value}" event_name = "battery_status" @@ -158,6 +155,10 @@ def parse(String description) { def parseDescriptionAsMap(description) { (description - "read attr - ").split(",").inject([:]) { map, param -> def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + if(nameAndValue.length == 2){ + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + }else{ + map += [] + } } } \ No newline at end of file