· 6 years ago · Jan 04, 2020, 04:30 PM
1/*
2 * Copyright 2015 SmartThings
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at:
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 * for the specific language governing permissions and limitations under the License.
12 *
13 * (Based on) Ecobee Thermostat
14 *
15 * Author: SmartThings
16 * Date: 2013-06-13
17 */
18metadata {
19 definition (name: "Carrier Thermostat", namespace: "SmartThingsMod", author: "SmartThingsMod") {
20 capability "Actuator"
21 capability "Thermostat"
22 capability "Temperature Measurement"
23 capability "Sensor"
24 capability "Refresh"
25 capability "Relative Humidity Measurement"
26 capability "Health Check"
27
28 command "generateEvent"
29 command "resumeProgram"
30 command "switchMode"
31 command "switchFanMode"
32 command "lowerHeatingSetpoint"
33 command "raiseHeatingSetpoint"
34 command "lowerCoolSetpoint"
35 command "raiseCoolSetpoint"
36 // To satisfy some SA/rules that incorrectly using poll instead of Refresh
37 command "poll"
38
39 attribute "thermostat", "string"
40 attribute "maxHeatingSetpoint", "number"
41 attribute "minHeatingSetpoint", "number"
42 attribute "maxCoolingSetpoint", "number"
43 attribute "minCoolingSetpoint", "number"
44 attribute "deviceTemperatureUnit", "string"
45 attribute "deviceAlive", "enum", ["true", "false"]
46 attribute "thermostatSchedule", "string"
47 attribute "outsideTemp", "number"
48 }
49
50 tiles {
51 multiAttributeTile(name:"temperature", type:"generic", width:3, height:2, canChangeIcon: true) {
52 tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
53 attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal",
54 backgroundColors:[
55 // Celsius
56 [value: 0, color: "#153591"],
57 [value: 7, color: "#1e9cbb"],
58 [value: 15, color: "#90d2a7"],
59 [value: 23, color: "#44b621"],
60 [value: 28, color: "#f1d801"],
61 [value: 35, color: "#d04e00"],
62 [value: 37, color: "#bc2323"],
63 // Fahrenheit
64 [value: 40, color: "#153591"],
65 [value: 44, color: "#1e9cbb"],
66 [value: 59, color: "#90d2a7"],
67 [value: 74, color: "#44b621"],
68 [value: 84, color: "#f1d801"],
69 [value: 95, color: "#d04e00"],
70 [value: 96, color: "#bc2323"]
71 ]
72 )
73 }
74 tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
75 attributeState "humidity", label:'${currentValue}%', icon:"st.Weather.weather12"
76 }
77 }
78 standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
79 state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left"
80 }
81 valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
82 state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff"
83 }
84 standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
85 state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right"
86 }
87 standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
88 state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left"
89 }
90 valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
91 state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff"
92 }
93 standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
94 state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right"
95 }
96 standardTile("mode", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
97 state "off", action:"switchMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off"
98 state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
99 state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
100 state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
101 state "emergency heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.emergency-heat"
102 state "updating", label:"Updating...", icon: "st.secondary.secondary"
103 }
104 standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
105 state "auto", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-auto"
106 state "on", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-on"
107 state "updating", label:"Updating...", icon: "st.secondary.secondary"
108 }
109 valueTile("thermostat", "device.thermostat", width:2, height:1, decoration: "flat") {
110 state "thermostat", label:'${currentValue}', backgroundColor:"#ffffff"
111 }
112 standardTile("refresh", "device.thermostatMode", width:2, height:1, inactiveLabel: false, decoration: "flat") {
113 state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
114 }
115 standardTile("resumeProgram", "device.resumeProgram", width:2, height:1, inactiveLabel: false, decoration: "flat") {
116 state "resume", action:"resumeProgram", nextState: "updating", label:'Resume', icon:"st.samsung.da.oven_ic_send"
117 state "updating", label:"Working", icon: "st.secondary.secondary"
118 }
119 valueTile("thermostatSchedule", "device.thermostatSchedule", width:2, height:1, decoration: "flat") {
120 state "thermostatSchedule", label:'${currentValue}', backgroundColor:"#ffffff"
121 }
122 valueTile("outsideTemp", "device.outsideTemp", width:2, height:1, inactiveLabel: false, decoration: "flat") {
123 state "outsideTemp", label:'${currentValue}°\noutside', backgroundColor:"#ffffff"
124 }
125 main "temperature"
126 details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint",
127 "lowerCoolSetpoint", "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode",
128 "thermostat", "thermostatSchedule", "refresh", "resumeProgram", "outsideTemp"])
129 }
130
131 preferences {
132 input "holdType", "enum", title: "Hold Type",
133 description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)",
134 required: false, options:["Temporary", "Permanent"]
135 input "deadbandSetting", "number", title: "Minimum temperature difference between the desired Heat and Cool " +
136 "temperatures in Auto mode:\nNote! This must be the same as configured on the thermostat",
137 description: "temperature difference °F", defaultValue: 5,
138 required: false
139 }
140
141}
142
143void installed() {
144 // The device refreshes every 5 minutes by default so if we miss 2 refreshes we can consider it offline
145 // Using 12 minutes because in testing, device health team found that there could be "jitter"
146 sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "cloud"], displayed: false)
147}
148
149// Device Watch will ping the device to proactively determine if the device has gone offline
150// If the device was online the last time we refreshed, trigger another refresh as part of the ping.
151def ping() {
152 def isAlive = device.currentValue("deviceAlive") == "true" ? true : false
153 if (isAlive) {
154 refresh()
155 }
156}
157
158// parse events into attributes
159def parse(String description) {
160 log.debug "Parsing '${description}'"
161}
162
163def refresh() {
164 log.debug "refresh"
165 sendEvent([name: "thermostat", value: "updating"])
166 poll2()
167}
168void poll() {
169}
170void poll2() {
171 log.debug "Executing poll using parent SmartApp"
172 //log.debug "Id: " + device.id + ", Name: " + device.name + ", Label: " + device.label + ", NetworkId: " + device.deviceNetworkId
173 //parent.refreshChild(device.deviceNetworkId)
174 parent.syncThermostats()
175}
176
177def zUpdate(temp, systemStatus, hum, hsp, csp, fan, currSched, oat){
178 log.debug "zupdate: " + temp + ", " + systemStatus + ", " + hum + ", " + hsp + ", " + csp + ", " + fan + ", " + currSched + ", " + oat
179 sendEvent([name: "temperature", value: temp, unit: "F"])
180 sendEvent([name: "thermostat", value: systemStatus])
181 sendEvent([name: "humidity", value: hum])
182 sendEvent([name: "heatingSetpoint", value: hsp])
183 sendEvent([name: "coolingSetpoint", value: csp])
184 sendEvent([name: "thermostatFanMode", value: fan])
185 sendEvent([name: "outsideTemp", value: oat])
186 sendEvent([name: "thermostatSchedule", value: currSched])
187}
188def generateEvent(Map results) {
189 if(results) {
190 def linkText = getLinkText(device)
191 def supportedThermostatModes = ["off"]
192 def thermostatMode = null
193 def locationScale = getTemperatureScale()
194
195 results.each { name, value ->
196 def event = [name: name, linkText: linkText, handlerName: name]
197 def sendValue = value
198
199 if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
200 sendValue = getTempInLocalScale(value, "F") // API return temperature values in F
201 event << [value: sendValue, unit: locationScale]
202 } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
203 // Old attributes, keeping for backward compatibility
204 sendValue = getTempInLocalScale(value, "F") // API return temperature values in F
205 event << [value: sendValue, unit: locationScale, displayed: false]
206 // Store min/max setpoint in device unit to avoid conversion rounding error when updating setpoints
207 device.updateDataValue(name+"Fahrenheit", "${value}")
208 } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
209 if (value == true) {
210 supportedThermostatModes << ((name == "auxHeatMode") ? "emergency heat" : name - "Mode")
211 }
212 return // as we don't want to send this event here, proceed to next name/value pair
213 } else if (name=="thermostatFanMode"){
214 sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false)
215 event << [value: value, data:[supportedThermostatFanModes: fanModes()]]
216 } else if (name=="humidity") {
217 event << [value: value, displayed: false, unit: "%"]
218 } else if (name == "deviceAlive") {
219 event['displayed'] = false
220 } else if (name == "thermostatMode") {
221 thermostatMode = (value == "auxHeatOnly") ? "emergency heat" : value.toLowerCase()
222 return // as we don't want to send this event here, proceed to next name/value pair
223 } else {
224 event << [value: value.toString()]
225 }
226 event << [descriptionText: getThermostatDescriptionText(name, sendValue, linkText)]
227 sendEvent(event)
228 }
229 if (state.supportedThermostatModes != supportedThermostatModes) {
230 state.supportedThermostatModes = supportedThermostatModes
231 sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
232 }
233 if (thermostatMode) {
234 sendEvent(name: "thermostatMode", value: thermostatMode, data:[supportedThermostatModes:state.supportedThermostatModes], linkText: linkText,
235 descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode")
236 }
237 generateSetpointEvent ()
238 generateStatusEvent ()
239 }
240}
241
242//return descriptionText to be shown on mobile activity feed
243private getThermostatDescriptionText(name, value, linkText) {
244 if(name == "temperature") {
245 return "temperature is ${value}°${location.temperatureScale}"
246
247 } else if(name == "heatingSetpoint") {
248 return "heating setpoint is ${value}°${location.temperatureScale}"
249
250 } else if(name == "coolingSetpoint"){
251 return "cooling setpoint is ${value}°${location.temperatureScale}"
252
253 } else if (name == "thermostatMode") {
254 return "thermostat mode is ${value}"
255
256 } else if (name == "thermostatFanMode") {
257 return "thermostat fan mode is ${value}"
258
259 } else if (name == "humidity") {
260 return "humidity is ${value} %"
261 } else {
262 return "${name} = ${value}"
263 }
264}
265
266void setHeatingSetpoint(setpoint) {
267log.debug "***setHeatingSetpoint($setpoint)"
268 if (setpoint) {
269 state.heatingSetpoint = setpoint.toDouble()
270 runIn(2, "updateSetpoints", [overwrite: true])
271 }
272}
273
274def setCoolingSetpoint(setpoint) {
275log.debug "***setCoolingSetpoint($setpoint)"
276 if (setpoint) {
277 state.coolingSetpoint = setpoint.toDouble()
278 runIn(2, "updateSetpoints", [overwrite: true])
279 }
280}
281
282def updateSetpoints() {
283 def deviceScale = "F" //API return/expects temperature values in F
284 def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null]
285 def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
286 def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
287 if (state.heatingSetpoint) {
288 data = enforceSetpointLimits("heatingSetpoint", [targetValue: state.heatingSetpoint,
289 heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
290 }
291 if (state.coolingSetpoint) {
292 heatingSetpoint = data.targetHeatingSetpoint ? getTempInLocalScale(data.targetHeatingSetpoint, deviceScale) : heatingSetpoint
293 coolingSetpoint = data.targetCoolingSetpoint ? getTempInLocalScale(data.targetCoolingSetpoint, deviceScale) : coolingSetpoint
294 data = enforceSetpointLimits("coolingSetpoint", [targetValue: state.coolingSetpoint,
295 heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
296 }
297 state.heatingSetpoint = null
298 state.coolingSetpoint = null
299 updateSetpoint(data)
300}
301
302void resumeProgram() {
303 log.debug "resumeProgram() is called"
304
305 sendEvent("name":"thermostat", "value":"resuming schedule", "description":statusText, displayed: false)
306 def deviceId = device.deviceNetworkId.split(/\./).last()
307 if (parent.resumeProgram(deviceId)) {
308 sendEvent("name":"thermostat", "value":"setpoint is updating", "description":statusText, displayed: false)
309 sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
310 } else {
311 sendEvent("name":"thermostat", "value":"failed resume click refresh", "description":statusText, displayed: false)
312 log.error "Error resumeProgram() check parent.resumeProgram(deviceId)"
313 }
314 //xyzrunIn(5, "refresh", [overwrite: true])
315}
316
317def modes() {
318 return state.supportedThermostatModes
319}
320
321def fanModes() {
322 // Ecobee does not report its supported fanModes; use hard coded values
323 ["on", "auto"]
324}
325
326def switchSchedule() {
327 //TODO
328}
329def switchMode() {
330 def currentMode = device.currentValue("thermostatMode")
331 def modeOrder = modes()
332 if (modeOrder) {
333 def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
334 def nextMode = next(currentMode)
335 switchToMode(nextMode)
336 } else {
337 log.warn "supportedThermostatModes not defined"
338 }
339}
340
341def switchToMode(mode) {
342 log.debug "switchToMode: ${mode}"
343 def deviceId = device.deviceNetworkId.split(/\./).last()
344 // Thermostat's mode for "emergency heat" is "auxHeatOnly"
345 if (!(parent.setMode(((mode == "emergency heat") ? "auxHeatOnly" : mode), deviceId))) {
346 log.warn "Error setting mode:$mode"
347 // Ensure the DTH tile is reset
348 generateModeEvent(device.currentValue("thermostatMode"))
349 }
350 //XYZ runIn(5, "refresh", [overwrite: true])
351}
352
353def switchFanMode() {
354 def currentFanMode = device.currentValue("thermostatFanMode")
355 def fanModeOrder = fanModes()
356 def next = { fanModeOrder[fanModeOrder.indexOf(it) + 1] ?: fanModeOrder[0] }
357 switchToFanMode(next(currentFanMode))
358}
359
360def switchToFanMode(fanMode) {
361 log.debug "switchToFanMode: $fanMode"
362 def heatingSetpoint = getTempInDeviceScale("heatingSetpoint")
363 def coolingSetpoint = getTempInDeviceScale("coolingSetpoint")
364 def deviceId = device.deviceNetworkId.split(/\./).last()
365 def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite"
366
367 if (!(parent.setFanMode(heatingSetpoint, coolingSetpoint, deviceId, sendHoldType, fanMode))) {
368 log.warn "Error setting fanMode:fanMode"
369 // Ensure the DTH tile is reset
370 generateFanModeEvent(device.currentValue("thermostatFanMode"))
371 }
372 //XYZ runIn(5, "refresh", [overwrite: true])
373}
374
375def getDataByName(String name) {
376 state[name] ?: device.getDataValue(name)
377}
378
379def setThermostatMode(String mode) {
380 log.debug "setThermostatMode($mode)"
381 def supportedModes = modes()
382 if (supportedModes) {
383 mode = mode.toLowerCase()
384 def modeIdx = supportedModes.indexOf(mode)
385 if (modeIdx < 0) {
386 log.warn("Thermostat mode $mode not valid for this thermostat")
387 return
388 }
389 mode = supportedModes[modeIdx]
390 switchToMode(mode)
391 } else {
392 log.warn "supportedThermostatModes not defined"
393 }
394}
395
396def setThermostatFanMode(String mode) {
397 log.debug "setThermostatFanMode($mode)"
398 mode = mode.toLowerCase()
399 def supportedFanModes = fanModes()
400 def modeIdx = supportedFanModes.indexOf(mode)
401 if (modeIdx < 0) {
402 log.warn("Thermostat fan mode $mode not valid for this thermostat")
403 return
404 }
405 mode = supportedFanModes[modeIdx]
406 switchToFanMode(mode)
407}
408
409def generateModeEvent(mode) {
410 sendEvent(name: "thermostatMode", value: mode, data:[supportedThermostatModes: device.currentValue("supportedThermostatModes")],
411 isStateChange: true, descriptionText: "$device.displayName is in ${mode} mode")
412}
413
414def generateFanModeEvent(fanMode) {
415 sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: device.currentValue("supportedThermostatFanModes")],
416 isStateChange: true, descriptionText: "$device.displayName fan is in ${fanMode} mode")
417}
418
419def generateOperatingStateEvent(operatingState) {
420 sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true)
421}
422
423def off() { setThermostatMode("off") }
424def heat() { setThermostatMode("heat") }
425def emergencyHeat() { setThermostatMode("emergency heat") }
426def cool() { setThermostatMode("cool") }
427def auto() { setThermostatMode("auto") }
428
429def fanOn() { setThermostatFanMode("on") }
430def fanAuto() { setThermostatFanMode("auto") }
431def fanCirculate() { setThermostatFanMode("circulate") }
432
433// =============== Setpoints ===============
434def generateSetpointEvent() {
435 def mode = device.currentValue("thermostatMode")
436 def setpoint = getTempInLocalScale("heatingSetpoint") // (mode == "heat") || (mode == "emergency heat")
437 def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
438
439 if (mode == "cool") {
440 setpoint = coolingSetpoint
441 } else if ((mode == "auto") || (mode == "off")) {
442 setpoint = roundC((setpoint + coolingSetpoint) / 2)
443 } // else (mode == "heat") || (mode == "emergency heat")
444 sendEvent("name":"thermostatSetpoint", "value":setpoint, "unit":location.temperatureScale)
445}
446
447def raiseHeatingSetpoint() {
448 alterSetpoint(true, "heatingSetpoint")
449}
450
451def lowerHeatingSetpoint() {
452 alterSetpoint(false, "heatingSetpoint")
453}
454
455def raiseCoolSetpoint() {
456 alterSetpoint(true, "coolingSetpoint")
457}
458
459def lowerCoolSetpoint() {
460 alterSetpoint(false, "coolingSetpoint")
461}
462
463// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false
464def alterSetpoint(raise, setpoint) {
465 // don't allow setpoint change if thermostat is off
466 if (device.currentValue("thermostatMode") == "off") {
467 return
468 }
469 def locationScale = getTemperatureScale()
470 def deviceScale = "F"
471 def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
472 def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
473 def targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint
474 def delta = (locationScale == "F") ? 1 : 0.5
475 targetValue += raise ? delta : - delta
476
477 def data = enforceSetpointLimits(setpoint,
478 [targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint], raise)
479 // update UI without waiting for the device to respond, this to give user a smoother UI experience
480 // also, as runIn's have to overwrite and user can change heating/cooling setpoint separately separate runIn's have to be used
481 if (data.targetHeatingSetpoint) {
482 sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, deviceScale),
483 unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
484 }
485 if (data.targetCoolingSetpoint) {
486 sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale),
487 unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
488 }
489 runIn(5, "updateSetpoint", [data: data, overwrite: true])
490}
491
492def enforceSetpointLimits(setpoint, data, raise = null) {
493 def locationScale = getTemperatureScale()
494 def minSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("minHeatingSetpointFahrenheit") : device.getDataValue("minCoolingSetpointFahrenheit")
495 def maxSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("maxHeatingSetpointFahrenheit") : device.getDataValue("maxCoolingSetpointFahrenheit")
496 minSetpoint = minSetpoint ? Double.parseDouble(minSetpoint) : ((setpoint == "heatingSetpoint") ? 45 : 65) // default 45 heat, 65 cool
497 maxSetpoint = maxSetpoint ? Double.parseDouble(maxSetpoint) : ((setpoint == "heatingSetpoint") ? 79 : 92) // default 79 heat, 92 cool
498 def deadband = deadbandSetting ? deadbandSetting : 5 // °F
499 def delta = (locationScale == "F") ? 1 : 0.5
500 def targetValue = getTempInDeviceScale(data.targetValue, locationScale)
501 def heatingSetpoint = getTempInDeviceScale(data.heatingSetpoint, locationScale)
502 def coolingSetpoint = getTempInDeviceScale(data.coolingSetpoint, locationScale)
503 // Enforce min/mix for setpoints
504 if (targetValue > maxSetpoint) {
505 targetValue = maxSetpoint
506 } else if (targetValue < minSetpoint) {
507 targetValue = minSetpoint
508 } else if ((raise != null) && ((setpoint == "heatingSetpoint" && targetValue == heatingSetpoint) ||
509 (setpoint == "coolingSetpoint" && targetValue == coolingSetpoint))) {
510 // Ensure targetValue differes from old. When location scale differs from device,
511 // converting between C -> F -> C may otherwise result in no change.
512 targetValue += raise ? delta : - delta
513 }
514 // Enforce deadband between setpoints
515 if (setpoint == "heatingSetpoint") {
516 heatingSetpoint = targetValue
517 coolingSetpoint = (heatingSetpoint + deadband > coolingSetpoint) ? heatingSetpoint + deadband : coolingSetpoint
518 }
519 if (setpoint == "coolingSetpoint") {
520 coolingSetpoint = targetValue
521 heatingSetpoint = (coolingSetpoint - deadband < heatingSetpoint) ? coolingSetpoint - deadband : heatingSetpoint
522 }
523 return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint]
524}
525
526def updateSetpoint(data) {
527 def deviceId = device.deviceNetworkId.split(/\./).last()
528 def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite"
529
530 if (parent.setHold(data.targetHeatingSetpoint, data.targetCoolingSetpoint, deviceId, sendHoldType)) {
531 log.debug "alterSetpoint succeed to change setpoints:${data}"
532 } else {
533 log.error "Error alterSetpoint"
534 }
535 //XYZ runIn(5, "refresh", [overwrite: true])
536}
537
538def generateStatusEvent() {
539 def mode = device.currentValue("thermostatMode")
540 def heatingSetpoint = device.currentValue("heatingSetpoint")
541 def coolingSetpoint = device.currentValue("coolingSetpoint")
542 def temperature = device.currentValue("temperature")
543 def statusText = "Right Now: Idle"
544 def operatingState = "idle"
545
546 if (mode == "heat" || mode == "emergency heat") {
547 if (temperature < heatingSetpoint) {
548 statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
549 operatingState = "heating"
550 }
551 } else if (mode == "cool") {
552 if (temperature > coolingSetpoint) {
553 statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
554 operatingState = "cooling"
555 }
556 } else if (mode == "auto") {
557 if (temperature < heatingSetpoint) {
558 statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
559 operatingState = "heating"
560 } else if (temperature > coolingSetpoint) {
561 statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
562 operatingState = "cooling"
563 }
564 } else if (mode == "off") {
565 statusText = "Right Now: Off"
566 } else {
567 statusText = "?"
568 }
569
570 sendEvent("name":"thermostat", "value":statusText, "description":statusText, displayed: true)
571 sendEvent("name":"thermostatOperatingState", "value":operatingState, "description":operatingState, displayed: false)
572}
573
574def generateActivityFeedsEvent(notificationMessage) {
575 sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
576}
577
578// Get stored temperature from currentState in current local scale
579def getTempInLocalScale(state) {
580 def temp = device.currentState(state)
581 def scaledTemp = convertTemperatureIfNeeded(temp.value.toBigDecimal(), temp.unit).toDouble()
582 return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
583}
584
585// Get/Convert temperature to current local scale
586def getTempInLocalScale(temp, scale) {
587 def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble()
588 return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
589}
590
591// Get stored temperature from currentState in device scale
592def getTempInDeviceScale(state) {
593 def temp = device.currentState(state)
594 if (temp && temp.value && temp.unit) {
595 return getTempInDeviceScale(temp.value.toBigDecimal(), temp.unit)
596 }
597 return 0
598}
599
600def getTempInDeviceScale(temp, scale) {
601 if (temp && scale) {
602 //API return/expects temperature values in F
603 return ("F" == scale) ? temp : celsiusToFahrenheit(temp).toDouble().round(0).toInteger()
604 }
605 return 0
606}
607
608def roundC (tempC) {
609 return (Math.round(tempC.toDouble() * 2))/2
610}