Update laundry-monitor.groovy
[smartapps.git] / third-party / MonitorAndSetEcobeeTemp.groovy
1 /**
2  *  MonitorAndSetEcobeeTemp
3  *
4  *  Copyright 2014 Yves Racine
5  *  LinkedIn profile: ca.linkedin.com/pub/yves-racine-m-sc-a/0/406/4b/
6  *
7  *  Developer retains all right, title, copyright, and interest, including all copyright, patent rights, trade secret 
8  *  in the Background technology. May be subject to consulting fees under the Agreement between the Developer and the Customer. 
9  *  Developer grants a non exclusive perpetual license to use the Background technology in the Software developed for and delivered 
10  *  to Customer under this Agreement. However, the Customer shall make no commercial use of the Background technology without
11  *  Developer's written consent.
12  *
13  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
14  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
15  *
16  * The MonitorAndSetEcobeeTemp monitors the outdoor temp and adjusts the heating and cooling set points
17  * at regular intervals (input parameter in minutes) according to heat/cool thresholds that you set (input parameters).
18  * It also constantly monitors any 'holds' at the thermostat to make sure that these holds are justified according to
19  * the motion sensors at home and the given thresholds.
20  *
21  *  Software Distribution is restricted and shall be done only with Developer's written approval.
22  *  N.B. Requires MyEcobee device available at 
23  *          http://www.ecomatiqhomes.com/#!store/tc3yr 
24  */
25 definition(
26         name: "MonitorAndSetEcobeeTemp",
27         namespace: "yracine",
28         author: "Yves Racine",
29         description: "Monitors And Adjusts Ecobee your programmed temperature according to indoor motion sensors & outdoor temperature and humidity.",
30         category: "My Apps",
31         iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
32         iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
33 )
34
35 preferences {
36
37         page(name: "dashboardPage", title: "D\7fashboard")
38         page(name: "tempSensorSettings", title: "tempSensorSettings")
39         page(name: "motionSensorSettings", title: "motionSensorSettings")
40         page(name: "thresholdSettings", title: "ThresholdSettings")
41         page(name: "otherSettings", title: "OtherSettings")
42 }
43
44 def get_APP_VERSION() { return "3.4.3"}
45
46 def dashboardPage() {
47         dynamicPage(name: "dashboardPage", title: "MonitorAndSetEcobeeTemp-Dashboard", uninstall: true, nextPage: tempSensorSettings,submitOnChange: true) {
48                 section("Press Next in the upper section for Initial setup") {
49                         if (ecobee) {            
50                                 def scale= getTemperatureScale()
51                                 String currentProgName = ecobee?.currentClimateName
52                                 String currentProgType = ecobee?.currentProgramType
53                                 def scheduleProgramName = ecobee?.currentProgramScheduleName
54                                 String mode =ecobee?.currentThermostatMode.toString()
55                                 def operatingState=ecobee?.currentThermostatOperatingState                
56                                 def heatingSetpoint,coolingSetpoint
57                                 switch (mode) { 
58                                         case 'cool':
59                                                 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
60                                         break                        
61                                         case 'auto': 
62                                                 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
63                                         case 'heat':
64                                         case 'emergency heat':
65                                         case 'auto': 
66                                         case 'off': 
67                                                 heatingSetpoint = ecobee?.currentValue('heatingSetpoint')
68                                         break
69                                 }                        
70
71                                 def dParagraph = "TstatMode: $mode\n" +
72                                                 "TstatOperatingState $operatingState\n" + 
73                                                 "EcobeeClimateSet: $currentProgName\n" +
74                                                 "EcobeeProgramType: $currentProgType\n" + 
75                                                 "HoldProgramSet: $state.programHoldSet\n"
76                                 if (coolingSetpoint)  { 
77                                          dParagraph = dParagraph + "CoolingSetpoint: ${coolingSetpoint}$scale\n"
78                                 }     
79                                 if (heatingSetpoint)  { 
80                                         dParagraph = dParagraph + "HeatingSetpoint: ${heatingSetpoint}$scale\n" 
81                                 }
82                                 paragraph dParagraph 
83                                 if (state?.avgTempDiff)  { 
84                                         paragraph "AvgTempDiff: ${state?.avgTempDiff}$scale"                   
85                                 }
86                 
87                         } /* end if ecobee */           
88                 }        
89                 section("Monitor indoor/outdoor temp & adjust the ecobee thermostat's setpoints") {
90                         input "ecobee", "capability.thermostat", title: "For which Ecobee?"
91                 }
92                 section("At which interval in minutes (range=[10..59],default=10 min.)?") {
93                         input "givenInterval", "number", title:"Interval", required: false
94                 }
95                 section("Maximum Temp adjustment in Farenheits/Celsius") {
96                         input "givenTempDiff", "decimal", title: "Max Temp adjustment [default= +/-5°F/2°C]", required: false
97                 }
98                 section("Outdoor/Indoor Temp & Motion Setup") {
99                         href(name: "toTempSensorsPage",  title: "Configure your indoor Temp Sensors", description: "Tap to Configure...", image: getImagePath() + "IndoorTempSensor.png", page: "tempSensorSettings") 
100                         href(name: "toMotionSensorsPage", title: "Configure your indoor Motion Sensors", description: "Tap to Configure...", image: getImagePath() + "MotionDetector.jpg", page: "motionSensorSettings") 
101                         href(name: "toOutdoorSensorsPage", title: "Configure your outdoor Thresholds based on a weatherStation/Sensor", description: "Tap to Configure...", image: getImagePath() + "WeatherStation.jpg", page: "thresholdSettings") 
102                         href(name: "toNotificationsPage", title: "Notification & other Options Setup",  description: "Tap to Configure...", page: "otherSettings")
103                 }            
104                 section("About") {      
105                         paragraph "MonitorAndSetEcobeeTemp,the smartapp that adjusts your programmed ecobee's setpoints based on indoor/outdoor sensors"
106                         paragraph "Version ${get_APP_VERSION()}" 
107                         paragraph "If you like this smartapp, please support the developer via PayPal and click on the Paypal link below " 
108                                 href url: "https://www.paypal.me/ecomatiqhomes",
109                                         title:"Paypal donation..."
110                         paragraph "Copyright©2014 Yves Racine"
111                                 href url:"http://github.com/yracine/device-type.myecobee", style:"embedded", required:false, title:"More information..."  
112                                         description: "http://github.com/yracine/device-type.myecobee/blob/master/README.md"
113                 } /* end section About */
114         } /* end dashboardPage */ 
115
116 def tempSensorSettings() {
117         dynamicPage(name: "tempSensorSettings", title: "Indoor Temp Sensor(s) for setpoint adjustment", install: false, nextPage: motionSensorSettings) {
118                 section("Choose indoor sensor(s) with both Motion & Temp capabilities to be used for dynamic temp adjustment when occupied [optional]") {
119                         input "indoorSensors", "capability.motionSensor", title: "Which Indoor Motion/Temperature Sensor(s)", required: false, multiple:true
120                 }               
121                 section("Choose any other indoor temp sensors for avg temp adjustment [optional]") {
122                         input "tempSensors", "capability.temperatureMeasurement", title: "Any other temp sensors?",  multiple: true, required: false
123                         
124                 }
125                 section {
126                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
127                 }
128         
129         }        
130 }
131  
132  
133 def motionSensorSettings() {
134         dynamicPage(name: "motionSensorSettings", title: "Motion Sensors for setting thermostat to Away/Present", install: false, nextPage: thresholdSettings) {
135                 section("Set your ecobee thermostat to [Away,Present] based on all Room Motion Sensors [default=false] ") {
136                         input (name:"setAwayOrPresentFlag", title: "Set Main thermostat to [Away,Present]?", type:"bool",required:false)
137                 }
138                 section("Choose additional indoor motion sensors for setting ecobee climate to [Away, Home] [optional]") {
139                         input "motions", "capability.motionSensor", title: "Any other motion sensors?",  multiple: true, required: false
140                 }
141                 section("Trigger climate/temp adjustment when motion or no motion has been detected for [default=15 minutes]") {        
142                         input "residentsQuietThreshold", "number", title: "Time in minutes", required: false
143                 }
144                 section {
145                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
146                 }
147         }        
148 }
149 def thresholdSettings() {
150         dynamicPage(name: "thresholdSettings",  title: "Outdoor Thresholds for setpoint adjustment", install: false, uninstall: true, nextPage: otherSettings) {
151                 section("Choose weatherStation or outdoor Temperature & Humidity Sensor to be used for temp adjustment") {
152                         input "outdoorSensor", "capability.temperatureMeasurement", title: "Outdoor Temperature Sensor",required: false
153                 }
154                 section("For more heating in cold season, outdoor temp's threshold [default <= 10°F/-17°C]") {
155                         input "givenMoreHeatThreshold", "decimal", title: "Outdoor temp's threshold for more heating", required: false
156                 }
157                 section("For less heating in cold season, outdoor temp's threshold [default >= 50°F/10°C]") {
158                         input "givenLessHeatThreshold", "decimal", title: "Outdoor temp's threshold for less heating", required: false
159                 }
160                 section("For more cooling in hot season, outdoor temp's threshold [default >= 85°F/30°C]") {
161                         input "givenMoreCoolThreshold", "decimal", title: "Outdoor temp's threshold for more cooling", required: false
162                 }       
163                 section("For less cooling in hot season, outdoor temp's threshold [default <= 75°F/22°C]") {
164                         input "givenLessCoolThreshold", "decimal", title: "Outdoor temp's threshold for less cooling", required: false
165                 }
166                 section("For more cooling/heating, outdoor humidity's threshold [default >= 85%]") {
167                         input "givenHumThreshold", "number", title: "Outdoor Relative humidity's threshold for more cooling/heating",
168                                 required: false
169                 }
170                 section {
171                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
172                 }
173         }
174 }
175 def otherSettings() {
176         dynamicPage(name: "otherSettings", title: "Other Settings", install: true, uninstall: false) {
177     
178                 section("What do I use for the Master on/off switch to enable/disable processing? [optional]") {
179                         input "powerSwitch", "capability.switch", required: false
180                 }
181                 section("Notifications") {
182                         input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required:
183                                 false
184                         input "phoneNumber", "phone", title: "Send a text message?", required: false
185                 }
186                 section("Detailed Notifications") {
187                         input "detailedNotif", "bool", title: "Detailed Notifications?", required:
188                                 false
189                 }
190                 section("Enable Amazon Echo/Ask Alexa Notifications [optional, default=false]") {
191                         input (name:"askAlexaFlag", title: "Ask Alexa verbal Notifications?", type:"bool",
192                                 description:"optional",required:false)
193                 }        
194                 section([mobileOnly:true]) {
195                         label title: "Assign a name for this SmartApp", required: false
196                 }
197                 section {
198                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
199                 }
200         }
201 }
202
203
204
205 def installed() {
206         initialize()
207 }
208
209 def updated() {
210         // we have had an update
211         // remove everything and reinstall
212         unschedule()
213     
214         unsubscribe()
215         initialize()
216 }
217 def initialize() {
218         log.debug "Initialized with settings: ${settings}"
219
220         reset_state_program_values()
221         reset_state_motions()
222         reset_state_tempSensors()
223         state?.exceptionCount=0    
224     
225         Integer delay = givenInterval ?: 10 // By default, do it every 10 minutes
226         if ((delay < 10) || (delay>59)) {
227                 def msg= "Scheduling delay not in range (${delay} min), exiting..."
228                 log.debug msg
229                 send msg        
230                 return
231         }
232         log.debug "Scheduling ecobee temp Monitoring and adjustment every ${delay}  minutes"
233
234         schedule("0 0/${delay} * * * ?", monitorAdjustTemp) // monitor & set indoor temp according to delay specified
235
236
237         subscribe(indoorSensors, "motion",motionEvtHandler, [filterEvents: false])
238         subscribe(motions, "motion", motionEvtHandler, [filterEvents: false])
239     
240         subscribe(ecobee, "programHeatTemp", programHeatEvtHandler)
241         subscribe(ecobee, "programCoolTemp", programCoolEvtHandler)
242         subscribe(ecobee, "setClimate", setClimateEvtHandler)
243         subscribe(ecobee, "thermostatMode", changeModeHandler)
244
245         if (powerSwitch) {
246                 subscribe(powerSwitch, "switch.off", offHandler, [filterEvents: false])
247                 subscribe(powerSwitch, "switch.on", onHandler, [filterEvents: false])
248         }
249         if (detailedNotif) {    
250                 log.debug("initialize state=$state")    
251         }       
252         // Resume program every time a install/update is done to remove any holds at thermostat (reset).
253     
254         ecobee.resumeThisTstat()
255     
256         subscribe(app, appTouch)
257     
258         state?.poll = [ last: 0, rescheduled: now() ]
259
260         //Subscribe to different events (ex. sunrise and sunset events) to trigger rescheduling if needed
261         subscribe(location, "sunrise", rescheduleIfNeeded)
262         subscribe(location, "sunset", rescheduleIfNeeded)
263         subscribe(location, "mode", rescheduleIfNeeded)
264         subscribe(location, "sunriseTime", rescheduleIfNeeded)
265         subscribe(location, "sunsetTime", rescheduleIfNeeded)
266         rescheduleIfNeeded()   
267 }
268
269
270 def rescheduleIfNeeded(evt) {
271         if (evt) log.debug("rescheduleIfNeeded>$evt.name=$evt.value")
272         Integer delay = givenInterval ?: 10 // By default, do it every 10 minutes
273         BigDecimal currentTime = now()    
274         BigDecimal lastPollTime = (currentTime - (state?.poll["last"]?:0))  
275         if (lastPollTime != currentTime) {    
276                 Double lastPollTimeInMinutes = (lastPollTime/60000).toDouble().round(1)      
277                 log.info "rescheduleIfNeeded>last poll was  ${lastPollTimeInMinutes.toString()} minutes ago"
278         }
279         if (((state?.poll["last"]?:0) + (delay * 60000) < currentTime) && canSchedule()) {
280                 log.info "rescheduleIfNeeded>scheduling monitorAdjustTemp in ${delay} minutes.."
281                 schedule("0 0/${delay} * * * ?", monitorAdjustTemp)
282         }
283     
284         monitorAdjustTemp()    
285         // Update rescheduled state
286     
287         if (!evt) state.poll["rescheduled"] = now()
288 }
289     
290
291
292 def changeModeHandler(evt) {
293         log.debug "changeModeHandler>$evt.name: $evt.value"
294         ecobee.resumeThisTstat()    
295         rescheduleIfNeeded(evt)   // Call rescheduleIfNeeded to work around ST scheduling issues
296         monitorAdjustTemp()  
297 }
298
299 def appTouch(evt) {
300         monitorAdjustTemp()
301 }
302
303 private def sendNotifDelayNotInRange() {
304
305         send "scheduling delay (${givenInterval} min.) not in range, please restart..."    
306 }
307
308 def setClimateEvtHandler(evt) {
309         log.debug "SetClimateEvtHandler>$evt.name: $evt.value"
310 }
311
312 def programHeatEvtHandler(evt) {
313         log.debug "programHeatEvtHandler>$evt.name = $evt.value"
314 }
315
316 def programCoolEvtHandler(evt) {
317         log.debug "programCoolEvtHandler>$evt.name = $evt.value"
318 }
319
320 def motionEvtHandler(evt) {
321         if (evt.value == "active") {
322                 log.debug "Motion at home..."
323                 String currentProgName = ecobee.currentClimateName
324                 String currentProgType = ecobee.currentProgramType
325
326                 if (state?.programHoldSet == 'Away') {
327                         check_if_hold_justified()
328                 } else if ((currentProgName.toUpperCase()=='AWAY') && (state?.programHoldSet== "" ) && 
329                                 (currentProgType.toUpperCase()!='VACATION')) {
330                         check_if_hold_needed()
331                 }
332         
333         }
334 }
335
336
337 def offHandler(evt) {
338         log.debug "$evt.name: $evt.value"
339 }
340
341 def onHandler(evt) {
342         log.debug "$evt.name: $evt.value"
343         monitorAdjustTemp()
344 }
345
346
347
348 private addIndoorSensorsWhenOccupied() {
349
350         def threshold = residentsQuietThreshold ?: 15   // By default, the delay is 15 minutes
351         def result = false
352         def t0 = new Date(now() - (threshold * 60 *1000))
353         for (sensor in indoorSensors) {
354                 def recentStates = sensor.statesSince("motion", t0)
355                 if (recentStates.find{it.value == "active"}) {
356                         if (detailedNotif) {        
357                                 log.debug "addTempSensorsWhenOccupied>added occupied sensor ${sensor} as ${sensor.device.id}"
358                         }                
359                         state.tempSensors.add(sensor.device.id)
360                         result= true            
361                 }       
362             
363         }
364         log.debug "addTempSensorsWhenOccupied, result = $result"
365         return result
366 }
367
368 private residentsHaveBeenQuiet() {
369
370         def threshold = residentsQuietThreshold ?: 15   // By default, the delay is 15 minutes
371         def t0 = new Date(now() - (threshold * 60 *1000))
372         for (sensor in motions) {
373 /* Removed the refresh following some ST platform changes which cause "offline" issues to some temp/motion sensors.
374
375                 if (sensor.hasCapability("Refresh"))  { // to get the latest motion values
376                         sensor.refresh()
377                 }                   
378 */        
379                 def recentStates = sensor.statesSince("motion", t0)
380                 if (recentStates.find{it.value == "active"}) {
381                         log.debug "residentsHaveBeenQuiet: false, found motion at $sensor"
382                         return false
383                 }       
384         }
385     
386     
387         for (sensor in indoorSensors) {
388 /* Removed the refresh following some ST platform changes which cause "offline" issues to some temp/motion sensors.
389                 if (sensor.hasCapability("Refresh"))  { // to get the latest motion values
390                         sensor.refresh()
391                 }                   
392 */        
393                 def recentStates = sensor.statesSince("motion", t0)
394                 if (recentStates.find{it.value == "active"}) {
395                         log.debug "residentsHaveBeenQuiet: false, found motion at $sensor"
396                         return false
397                 }       
398             
399         }
400         log.debug "residentsHaveBeenQuiet: true"
401         return true
402 }
403
404
405 private isProgramScheduleSet(climateName, threshold) {
406         def result = false
407         def t0 = new Date(now() - (threshold * 60 *1000))
408         def recentStates = ecobee.statesSince("climateName", t0)
409         if (recentStates.find{it.value == climateName}) {
410                 result = true
411         }
412         log.debug "isProgramScheduleSet: $result"
413         return result
414 }
415
416
417 def monitorAdjustTemp() {
418         
419         Integer delay = givenInterval ?: 10 // By default, do it every 10 minutes
420         def todayDay = new Date().format("dd",location.timeZone)
421         if ((!state?.today) || (todayDay != state?.today)) {
422                 state?.exceptionCount=0   
423                 state?.sendExceptionCount=0        
424                 state?.today=todayDay        
425         }   
426
427
428         state?.poll["last"] = now()
429
430         if (((state?.poll["rescheduled"]?:0) + (delay * 60000)) < now()) {
431                 log.info "scheduling rescheduleIfNeeded() in ${delay} minutes.."
432                 schedule("0 0/${delay} * * * ?", rescheduleIfNeeded)
433                 // Update rescheduled state
434                 state?.poll["rescheduled"] = now()
435         }
436     
437         if (powerSwitch?.currentSwitch == "off") {
438                 if (detailedNotif) {
439                         send("Virtual master switch ${powerSwitch.name} is off, processing on hold...")
440                 }
441                 return
442         }
443
444         if (detailedNotif) {
445                 send("monitoring every ${delay} minute(s)")
446         }
447
448         //  Polling of the latest values at the thermostat and at the outdoor sensor
449         def MAX_EXCEPTION_COUNT=5
450         String exceptionCheck, msg 
451         try {        
452                 ecobee.poll()
453                 exceptionCheck= ecobee.currentVerboseTrace.toString()
454                 if ((exceptionCheck) && ((exceptionCheck.contains("exception") || (exceptionCheck.contains("error")) && 
455                         (!exceptionCheck.contains("Java.util.concurrent.TimeoutException"))))) {  
456                 // check if there is any exception or an error reported in the verboseTrace associated to the device (except the ones linked to rate limiting).
457                         state?.exceptionCount=state.exceptionCount+1    
458                         log.error "found exception/error after polling, exceptionCount= ${state?.exceptionCount}: $exceptionCheck" 
459                 } else {             
460                         // reset exception counter            
461                         state?.exceptionCount=0       
462                 }                
463         } catch (e) {           
464                 log.error "exception $e while trying to poll the device $d, exceptionCount= ${state?.exceptionCount}" 
465         }
466         if ((state?.exceptionCount>=MAX_EXCEPTION_COUNT) || ((exceptionCheck) && (exceptionCheck.contains("Unauthorized")))) {
467                 // need to authenticate again    
468                 msg="too many exceptions/errors or unauthorized exception, $exceptionCheck (${state?.exceptionCount} errors), may need to re-authenticate at ecobee..." 
469                 send "${msg}"
470                 log.error msg
471                 return        
472         }
473     
474 /* Removed the refresh following some ST platform changes which cause "offline" issues to some temp/motion sensors.
475     
476         if ((outdoorSensor) && (outdoorSensor.hasCapability("Refresh"))) {
477     
478                 try {    
479                         outdoorSensor.refresh()
480                 } catch (e) {
481                         log.debug("not able to refresh ${outdoorSensor}'s temp value")
482                 }       
483         }        
484 */    
485         String currentProgType = ecobee.currentProgramType
486         log.trace("program Type= ${currentProgType}")
487         if (currentProgType.toUpperCase().contains("HOLD")) {                                           
488                 log.trace("about to call check_if_hold_justified....")
489                 check_if_hold_justified()
490         }
491         
492         if (!currentProgType.contains("vacation")) {                            // don't make adjustment if on vacation mode
493                 log.trace("about to call check_if_needs_hold....")
494                 check_if_hold_needed()
495         }
496 }
497
498 private def reset_state_program_values() {
499
500         state.programSetTime = null
501         state.programSetTimestamp = ""
502         state.programHoldSet = ""
503 }
504
505 private def reset_state_tempSensors() {
506
507         state.tempSensors=[]    
508         settings.tempSensors.each {
509 //      By default, the 'static' temp Sensors are the ones used for temp avg calculation
510 //      Other 'occupied' sensors may be added dynamically when needed 
511             
512                 state.tempSensors.add(it.device.id) 
513         }
514 }
515
516 private def reset_state_motions() {
517         state.motions=[]
518         if (settings.motions) {
519                 settings.motions.each {
520                         state.motions.add(it.device.id)        
521                 }
522         }
523     
524         if (settings.indoorSensors) {
525                 settings.indoorSensors.each {
526                         state.motions.add(it.device.id)
527                 }
528         }
529 }
530
531 private void addAllTempsForAverage(indoorTemps) {
532
533         for (sensorId in state.tempSensors) {  // Add dynamically any indoor Sensor's when occupied             
534                 def sensor = tempSensors.find{it.device.id == sensorId}
535                 if (detailedNotif) {        
536                         log.debug "addAllTempsForAverage>trying to find sensorId=$sensorId in $tempSensors from $state.tempSensors"
537                 }            
538                 if (sensor != null) {
539                         if (detailedNotif) {        
540                                 log.debug "addAllTempsForAverage>found sensor $sensor in $tempSensors"
541                         } 
542 /* Removed the refresh following some ST platform changes which cause "offline" issues to some temp/motion sensors.
543             
544                         if (sensor.hasCapability("Refresh")) {
545                                 sensor.refresh()            
546                         }                
547 */            
548                         def currentTemp =sensor.currentTemperature
549                         if (currentTemp != null) {            
550                                 indoorTemps.add(currentTemp) // Add indoor temp to calculate the average based on all sensors
551                                 if (detailedNotif) {
552                                         log.trace "addAllTempsForAverage>adding $sensor temp (${currentTemp}) to tempSensors List"
553                                 }                    
554                         }                    
555                 }                    
556                 
557         }
558
559         for (sensorId in state.tempSensors) {  // Add dynamically any indoor Sensor's when occupied             
560                 def sensor = indoorSensors.find{it.device.id == sensorId}
561                 if (detailedNotif) {        
562                         log.debug "addAllTempsForAverage>trying to find sensorId=$sensorId in $indoorSensors from $state.tempSensors"
563                 }            
564                 if (sensor != null) {
565                         log.debug "addAllTempsForAverage>found sensor $sensor in $indoorSensors"
566                         def currentTemp =sensor.currentTemperature
567                         if (currentTemp != null) {            
568                                 indoorTemps.add(currentTemp) // Add indoor temp to calculate the average based on all sensors
569                                 log.trace "addAllTempsForAverage> adding $sensor temp (${currentTemp}) from indoorSensors List"
570                         }                    
571                 }                    
572         }
573 }
574
575 private def check_if_hold_needed() {
576         log.debug "Begin of Fcn check_if_hold_needed, settings= $settings"
577         float max_temp_diff,temp_diff=0
578         Integer humidity_threshold = givenHumThreshold ?: 85 // by default, 85% is the outdoor Humidity's threshold for more cooling
579         float more_heat_threshold, more_cool_threshold
580         float less_heat_threshold, less_cool_threshold
581         def MIN_TEMP_DIFF=0.5    
582
583         def scale = getTemperatureScale()
584         if (scale == 'C') {
585                 max_temp_diff = givenTempDiff ?: 2 // 2°C temp differential is applied by default
586                 more_heat_threshold = (givenMoreHeatThreshold != null) ? givenMoreHeatThreshold : (-17) // by default, -17°C is the outdoor temp's threshold for more heating
587                 more_cool_threshold = (givenMoreCoolThreshold != null) ? givenMoreCoolThreshold : 30 // by default, 30°C is the outdoor temp's threshold for more cooling
588                 less_heat_threshold = (givenLessHeatThreshold != null) ? givenLessHeatThreshold : 10 // by default, 10°C is the outdoor temp's threshold for less heating
589                 less_cool_threshold = (givenLessCoolThreshold != null) ? givenLessCoolThreshold : 22 // by default, 22°C is the outdoor temp's threshold for less cooling
590
591         } else {
592                 max_temp_diff = givenTempDiff ?: 5 // 5°F temp differential is applied by default
593                 more_heat_threshold = (givenMoreHeatThreshold != null) ? givenMoreHeatThreshold : 10 // by default, 10°F is the outdoor temp's threshold for more heating
594                 more_cool_threshold = (givenMoreCoolThreshold != null) ? givenMoreCoolThreshold : 85 // by default, 85°F is the outdoor temp's threshold for more cooling
595                 less_heat_threshold = (givenLessHeatThreshold != null) ? givenLessHeatThreshold : 50 // by default, 50°F is the outdoor temp's threshold for less heating
596                 less_cool_threshold = (givenLessCoolThreshold != null) ? givenLessCoolThreshold : 75 // by default, 75°F is the outdoor temp's threshold for less cooling
597         }
598
599         String currentProgName = ecobee.currentClimateName
600         String currentSetClimate = ecobee.currentSetClimate
601
602         String ecobeeMode = ecobee.currentThermostatMode.toString()
603         float heatTemp = ecobee.currentHeatingSetpoint.toFloat()
604         float coolTemp = ecobee.currentCoolingSetpoint.toFloat()
605         float programHeatTemp = ecobee.currentProgramHeatTemp.toFloat()
606         float programCoolTemp = ecobee.currentProgramCoolTemp.toFloat()
607         Integer ecobeeHumidity = ecobee.currentHumidity
608         float ecobeeTemp = ecobee.currentTemperature.toFloat()
609
610         reset_state_tempSensors()
611         if (addIndoorSensorsWhenOccupied()) {
612         if (detailedNotif) {
613                         log.trace("check_if_hold_needed>some occupied indoor Sensors added for avg calculation")
614                 }
615         }
616         def indoorTemps = [ecobeeTemp]
617         addAllTempsForAverage(indoorTemps)        
618         float avg_indoor_temp = (indoorTemps.sum() / indoorTemps.size()).round(1) // this is the avg indoor temp based on indoor sensors
619         if (detailedNotif) {
620                 log.trace "check_if_hold_needed> location.mode = $location.mode"
621                 log.trace "check_if_hold_needed> ecobee Mode = $ecobeeMode"
622                 log.trace "check_if_hold_needed> currentProgName = $currentProgName"
623                 log.trace "check_if_hold_needed> programHeatTemp = $programHeatTemp°"
624                 log.trace "check_if_hold_needed> programCoolTemp = $programCoolTemp°"
625                 log.trace "check_if_hold_needed> ecobee's indoorTemp = $ecobeeTemp°"
626                 log.trace "check_if_hold_needed> state.tempSensors = $state.tempSensors"
627                 log.trace "check_if_hold_needed> indoorTemps = $indoorTemps"
628                 log.trace "check_if_hold_needed> avgIndoorTemp = $avg_indoor_temp°"
629                 log.trace("check_if_hold_needed>temp sensors count=${indoorTemps.size()}")
630                 log.trace "check_if_hold_needed> max_temp_diff = $max_temp_diff°"
631                 log.trace "check_if_hold_needed> heatTemp = $heatTemp°"
632                 log.trace "check_if_hold_needed> coolTemp = $coolTemp°"
633                 log.trace "check_if_hold_needed> state=${state}"
634         }
635         float targetTstatTemp
636
637         if (detailedNotif) {
638                 send("needs Hold? currentProgName ${currentProgName},indoorTemp ${ecobeeTemp}°,progHeatSetPoint ${programHeatTemp}°,progCoolSetPoint ${programCoolTemp}°")
639                 send("needs Hold? currentProgName ${currentProgName},indoorTemp ${ecobeeTemp}°,heatingSetPoint ${heatTemp}°,coolingSetPoint ${coolTemp}°")
640                 if (state.programHoldSet!= "") {
641                         send("Hold ${state.programHoldSet} has been set")
642                 }
643         }
644         reset_state_motions()
645         def setAwayOrPresent = (setAwayOrPresentFlag)?:false
646         if ((setAwayOrPresent) && (state.motions != [])) {  // the following logic is done only if motion sensors are provided as input parameters
647   
648                 boolean residentAway=residentsHaveBeenQuiet()
649                 if ((!currentProgName.toUpperCase().contains('AWAY')) && (!residentAway)) {
650
651                         ecobee.present()            
652                         send("Program now set to Home, motion detected")
653                         state.programSetTime = now()
654                         state.programSetTimestamp = new Date().format("yyyy-MM-dd HH:mm", location.timeZone)
655                         state.programHoldSet = 'Home'
656                         log.debug "Program now set to Home at ${state.programSetTimestamp}, motion detected"
657                         /* Get latest heat and cool setting points after climate adjustment */
658                         programHeatTemp = ecobee.currentHeatingSetpoint.toFloat() // This is the heat temp associated to the current program
659                         programCoolTemp = ecobee.currentCoolingSetpoint.toFloat() // This is the cool temp associated to the current program
660         
661                 } else if ((!currentProgName.toUpperCase().contains('SLEEP')) && (!currentProgName.toUpperCase().contains('AWAY')) &&  
662                         (residentAway)) {
663                         // Do not adjust the program when ecobee mode = Sleep or Away    
664                 
665                         ecobee.away()          
666                         send("Program now set to Away,no motion detected")
667                         state.programSetTime = now()
668                         state.programSetTimestamp = new Date().format("yyyy-MM-dd HH:mm", location.timeZone)
669                         state.programHoldSet = 'Away'        
670                         /* Get latest heat and cool setting points after climate adjustment */
671                         programHeatTemp = ecobee.currentHeatingSetpoint.toFloat() // This is the heat temp associated to the current program
672                         programCoolTemp = ecobee.currentCoolingSetpoint.toFloat() // This is the cool temp associated to the current program
673
674
675                 }
676         } /* end if state.motions */
677     
678         if ((location.mode.toUpperCase().contains('AWAY')) || (currentProgName.toUpperCase()=='SLEEP')) { 
679     
680         // Do not adjust cooling or heating settings if ST mode == Away or Program Schedule at ecobee == SLEEP
681         
682                 log.debug "ST mode is $location.mode, current program is $currentProgName, no adjustment required, exiting..."
683                 return            
684         }   
685     
686         if (ecobeeMode == 'cool') {
687                 if (outdoorSensor) {            
688                         Integer outdoorHumidity = outdoorSensor.currentHumidity
689                         float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
690                         if (detailedNotif) {
691                                 log.trace "check_if_hold_needed> outdoorTemp = $outdoorTemp°"
692                                 log.trace "check_if_hold_needed> moreCoolThreshold = $more_cool_threshold°"
693                                 log.trace "check_if_hold_needed> lessCoolThreshold = $less_cool_threshold°"
694                                 log.trace(
695                                         "check_if_hold_needed>evaluate: moreCoolThreshold= ${more_cool_threshold}° vs. outdoorTemp ${outdoorTemp}°")
696                                 log.trace(
697                                         "check_if_hold_needed>evaluate: moreCoolThresholdHumidity= ${humidity_threshold}% vs. outdoorHum ${outdoorHumidity}%")
698                                 log.trace(
699                                         "check_if_hold_needed>evaluate: programCoolTemp= ${programCoolTemp}° vs. avg indoor Temp= ${avg_indoor_temp}°")
700                                 send("eval:  moreCoolThreshold ${more_cool_threshold}° vs. outdoorTemp ${outdoorTemp}°")
701                                 send("eval:  moreCoolThresholdHumidty ${humidity_threshold}% vs. outdoorHum ${outdoorHumidity}%")
702                                 send("eval:  programCoolTemp= ${programCoolTemp}° vs. avgIndoorTemp= ${avg_indoor_temp}°")
703                         }
704        
705                         if (outdoorTemp >= more_cool_threshold) {
706                                 targetTstatTemp = (programCoolTemp - max_temp_diff).round(1)
707                                 ecobee.setCoolingSetpoint(targetTstatTemp)
708                                 send("cooling setPoint now =${targetTstatTemp}°,outdoorTemp >=${more_cool_threshold}°")
709                         } else if (outdoorHumidity >= humidity_threshold) {
710                                 def extremes = [less_cool_threshold, more_cool_threshold]
711                                 float median_temp = (extremes.sum() / extremes.size()).round(1) // Increase cooling settings based on median temp
712                                 if (detailedNotif) {
713                                         String medianTempFormat = String.format('%2.1f', median_temp)
714                                         send("eval: cool median temp ${medianTempFormat}° vs.outdoorTemp ${outdoorTemp}°")
715                                 }
716                                 if (outdoorTemp > median_temp) { // Only increase cooling settings when outdoorTemp > median_temp
717                                         targetTstatTemp = (programCoolTemp - max_temp_diff).round(1)
718                                         ecobee.setCoolingSetpoint(targetTstatTemp)
719                                         send("cooling setPoint now=${targetTstatTemp}°, outdoorHum >=${humidity_threshold}%")
720                                 }
721                         }                
722                         if (detailedNotif) {
723                                 send("evaluate: lessCoolThreshold ${less_cool_threshold}° vs. outdoorTemp ${outdoorTemp}°")
724                                 log.trace("check_if_hold_needed>evaluate: lessCoolThreshold= ${less_cool_threshold} vs.outdoorTemp ${outdoorTemp}°")
725                         }
726                         if (outdoorTemp <= less_cool_threshold) {
727                                 targetTstatTemp = (programCoolTemp + max_temp_diff).round(1)
728                                 ecobee.setCoolingSetpoint(targetTstatTemp)
729                                 send(
730                                         "cooling setPoint now=${targetTstatTemp}°, outdoor temp <=${less_cool_threshold}°"
731                                 )
732                         }
733                 } /* end if outdoorSensor */ 
734
735                 if ((state.tempSensors) && (avg_indoor_temp > coolTemp)) {
736                         temp_diff = (ecobee_temp - avg_indoor_temp).round(1) // adjust the coolingSetPoint at the ecobee tstat according to the avg indoor temp measured
737                 
738                         temp_diff = (temp_diff <0-max_temp_diff)?max_temp_diff:(temp_diff >max_temp_diff)?max_temp_diff:temp_diff // determine the temp_diff based on max_temp_diff
739                         targetTstatTemp = (programCoolTemp - temp_diff).round(1)
740                         if (temp_diff.abs() > MIN_TEMP_DIFF) {  // adust the temp only if temp diff is significant
741                                 ecobee.setCoolingSetpoint(targetTstatTemp)
742                                 send("cooling setPoint now =${targetTstatTemp}°,adjusted by temp diff (${temp_diff}°) between sensors")
743                         }   
744                 }                
745         } else if (ecobeeMode in ['heat', 'emergency heat']) {
746                 if (outdoorSensor) {            
747                         Integer outdoorHumidity = outdoorSensor.currentHumidity
748                         float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
749                         if (detailedNotif) {            
750                                 log.trace "check_if_hold_needed> outdoorTemp = $outdoorTemp°"
751                                 log.trace "check_if_hold_needed> moreHeatThreshold = $more_heat_threshold°"
752                                 log.trace "check_if_hold_needed> lessHeatThreshold = $less_heat_threshold°"
753                                 log.trace("check_if_hold_needed>evaluate: moreHeatThreshold ${more_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
754                                 log.trace(
755                                 "check_if_hold_needed>evaluate: moreHeatThresholdHumidity= ${humidity_threshold}% vs.outdoorHumidity ${outdoorHumidity}%")
756                                 log.trace(
757                                         "check_if_hold_needed>evaluate: programHeatTemp= ${programHeatTemp}° vs. avg indoor Temp= ${avg_indoor_temp}°")
758                                 send("eval:  moreHeatThreshold ${more_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
759                                 send("eval:  moreHeatThresholdHumidty=${humidity_threshold}% vs.outdoorHumidity ${outdoorHumidity}%")
760                                 send("eval:  programHeatTemp= ${programHeatTemp}° vs. avgIndoorTemp= ${avg_indoor_temp}°")
761                         }
762                         if (outdoorTemp <= more_heat_threshold) {
763                                 targetTstatTemp = (programHeatTemp + max_temp_diff).round(1)
764                                 ecobee.setHeatingSetpoint(targetTstatTemp)
765                                 send(
766                                         "heating setPoint now= ${targetTstatTemp}°, outdoorTemp <=${more_heat_threshold}°")
767                         } else if (outdoorHumidity >= humidity_threshold) {
768                                 def extremes = [less_heat_threshold, more_heat_threshold]
769                                 float median_temp = (extremes.sum() / extremes.size()).round(1) // Increase heating settings based on median temp
770                                 if (detailedNotif) {
771                                         String medianTempFormat = String.format('%2.1f', median_temp)
772                                         send("eval: heat median temp ${medianTempFormat}° vs.outdoorTemp ${outdoorTemp}°")
773                                 }
774                                 if (outdoorTemp < median_temp) { // Only increase heating settings when outdoorTemp < median_temp
775                                         targetTstatTemp = (programHeatTemp + max_temp_diff).round(1)
776                                         ecobee.setHeatingSetpoint(targetTstatTemp)
777                                         send("heating setPoint now=${targetTstatTemp}°, outdoorHum >=${humidity_threshold}%")
778                                 }
779                         }                
780                         if (detailedNotif) {
781                                 log.trace("eval:lessHeatThreshold=${less_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
782                                 send("eval:  lessHeatThreshold ${less_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
783                         }
784                         if (outdoorTemp >= less_heat_threshold) {
785                                 targetTstatTemp = (programHeatTemp - max_temp_diff).round(1)
786                                 ecobee.setHeatingSetpoint(targetTstatTemp)
787                                 send("heating setPoint now=${targetTstatTemp}°,outdoor temp>= ${less_heat_threshold}°")
788                         }
789                 } /* if outdoorSensor */
790                 if ((state.tempSensors) && (avg_indoor_temp < heatTemp)) {
791                         temp_diff = (ecobeeTemp - avg_indoor_temp).round(1) // adjust the heatingSetPoint at the tstat according to the avg indoor temp measur
792                         temp_diff = (temp_diff <0-max_temp_diff)?max_temp_diff:(temp_diff >max_temp_diff)?max_temp_diff:temp_diff // determine the temp_diff based on max_temp_diff
793                         targetTstatTemp = (programHeatTemp + temp_diff).round(1)
794                         if (temp_diff.abs() > MIN_TEMP_DIFF) {  // adust the temp only if temp diff is significant
795                                 ecobee.setHeatingSetpoint(targetTstatTemp)
796                                 send("heating setPoint now =${targetTstatTemp}°,adjusted by temp diff (${temp_diff}°) between sensors")
797                         }                
798                 }                
799         }  /* end if heat mode */
800         state?.avgTempDiff =temp_diff // to display on the dashboardPage
801         log.debug "End of Fcn check_if_hold_needed"
802 }
803
804 private def check_if_hold_justified() {
805         log.debug "Begin of Fcn check_if_hold_justified, settings=$settings"
806         Integer humidity_threshold = givenHumThreshold ?: 85 // by default, 85% is the outdoor Humidity's threshold for more cooling
807         float more_heat_threshold, more_cool_threshold
808         float less_heat_threshold, less_cool_threshold
809         float max_temp_diff
810         Integer delay = givenInterval ?: 10 // By default, do it every 10 minutes
811
812         def scale = getTemperatureScale()
813         if (scale == 'C') {
814                 max_temp_diff = givenTempDiff ?: 2 // 2°C temp differential is applied by default
815                 more_heat_threshold = (givenMoreHeatThreshold != null) ? givenMoreHeatThreshold : (-17) // by default, -17°C is the outdoor temp's threshold for more heating
816                 more_cool_threshold = (givenMoreCoolThreshold != null) ? givenMoreCoolThreshold : 30 // by default, 30°C is the outdoor temp's threshold for more cooling
817                 less_heat_threshold = (givenLessHeatThreshold != null) ? givenLessHeatThreshold : 10 // by default, 10°C is the outdoor temp's threshold for less heating
818                 less_cool_threshold = (givenLessCoolThreshold != null) ? givenLessCoolThreshold : 22 // by default, 22°C is the outdoor temp's threshold for less cooling
819
820         } else {
821                 max_temp_diff = givenTempDiff ?: 5 // 5°F temp differential is applied by default
822                 more_heat_threshold = (givenMoreHeatThreshold != null) ? givenMoreHeatThreshold : 10 // by default, 10°F is the outdoor temp's threshold for more heating
823                 more_cool_threshold = (givenMoreCoolThreshold != null) ? givenMoreCoolThreshold : 85 // by default, 85°F is the outdoor temp's threshold for more cooling
824                 less_heat_threshold = (givenLessHeatThreshold != null) ? givenLessHeatThreshold : 50 // by default, 50°F is the outdoor temp's threshold for less heating
825                 less_cool_threshold = (givenLessCoolThreshold != null) ? givenLessCoolThreshold : 75 // by default, 75°F is the outdoor temp's threshold for less cooling
826         }
827         String currentProgName = ecobee.currentClimateName
828         String currentSetClimate = ecobee.currentSetClimate
829         float heatTemp = ecobee.currentHeatingSetpoint.toFloat()
830         float coolTemp = ecobee.currentCoolingSetpoint.toFloat()
831         float programHeatTemp = ecobee.currentProgramHeatTemp.toFloat()
832         float programCoolTemp = ecobee.currentProgramCoolTemp.toFloat()
833         Integer ecobeeHumidity = ecobee.currentHumidity
834         float ecobeeTemp = ecobee.currentTemperature.toFloat()
835
836         reset_state_tempSensors()
837         if (addIndoorSensorsWhenOccupied()) {
838                 log.trace("check_if_hold_justified>some occupied indoor Sensors added for avg calculation")
839         }
840     
841         def indoorTemps = [ecobeeTemp]
842         addAllTempsForAverage(indoorTemps)
843         log.trace("check_if_hold_justified> temps count=${indoorTemps.size()}")
844         float avg_indoor_temp = (indoorTemps.sum() / indoorTemps.size()).round(1) // this is the avg indoor temp based on indoor sensors
845
846         String ecobeeMode = ecobee.currentThermostatMode.toString()
847         if (detailedNotif) {    
848                 log.trace "check_if_hold_justified> location.mode = $location.mode"
849                 log.trace "check_if_hold_justified> ecobee Mode = $ecobeeMode"
850                 log.trace "check_if_hold_justified> currentProgName = $currentProgName"
851                 log.trace "check_if_hold_justified> currentSetClimate = $currentSetClimate"
852                 log.trace "check_if_hold_justified> state.tempSensors = $state.tempSensors"
853                 log.trace "check_if_hold_justified> ecobee's indoorTemp = $ecobeeTemp°"
854                 log.trace "check_if_hold_justified> indoorTemps = $indoorTemps"
855                 log.trace "check_if_hold_justified> avgIndoorTemp = $avg_indoor_temp°"
856                 log.trace "check_if_hold_justified> max_temp_diff = $max_temp_diff°"
857                 log.trace "check_if_hold_justified> heatTemp = $heatTemp°"
858                 log.trace "check_if_hold_justified> coolTemp = $coolTemp°"
859                 log.trace "check_if_hold_justified> programHeatTemp = $programHeatTemp°"
860                 log.trace "check_if_hold_justified> programCoolTemp = $programCoolTemp°"
861                 log.trace "check_if_hold_justified>state=${state}"
862                 send("Hold justified? currentProgName ${currentProgName},indoorTemp ${ecobeeTemp}°,progHeatSetPoint ${programHeatTemp}°,progCoolSetPoint ${programCoolTemp}°")
863                 send("Hold justified? currentProgName ${currentProgName},indoorTemp ${ecobeeTemp}°,heatingSetPoint ${heatTemp}°,coolingSetPoint ${coolTemp}°")
864                 if (state?.programHoldSet!= null && state?.programHoldSet!= "") {
865         
866                         send("Hold ${state.programHoldSet} has been set")
867                 }
868         }
869         reset_state_motions()
870         def setAwayOrPresent = (setAwayOrPresentFlag)?:false
871         if ((setAwayOrPresent) && (state.motions != [])) {  // the following logic is done only if motion sensors are provided as input parameters
872                 boolean residentAway=residentsHaveBeenQuiet()
873                 if ((currentSetClimate.toUpperCase()=='AWAY') && (!residentAway)) {
874                         if ((state?.programHoldSet == 'Away') && (!currentProgName.toUpperCase().contains('AWAY'))) {       
875                                 log.trace("check_if_hold_justified>it's not been quiet since ${state.programSetTimestamp},resumed ${currentProgName} program")
876                                 ecobee.resumeThisTstat()
877                                 send("resumed ${currentProgName} program, motion detected")
878                                 reset_state_program_values()
879                                 check_if_hold_needed()   // check if another type of hold is now needed (ex. 'Home' hold or more heat because of outside temp ) 
880                                 return // no more adjustments
881                         }                
882                         else if (state?.programHoldSet != 'Home') {     /* Climate was changed since the last climate set, just reset state program values */
883                                 reset_state_program_values()
884                         }
885                 } else if ((currentSetClimate.toUpperCase()=='AWAY') && (residentAway)) {
886                         if ((state?.programHoldSet == 'Away') && (currentProgName.toUpperCase().contains('AWAY'))) {       
887                                 ecobee.resumeThisTstat()
888                                 reset_state_program_values()
889                                 if (detailedNotif) {
890                                         send("'Away' hold no longer needed, resumed ${currentProgName} program ")
891                                 }
892                         } else if (state?.programHoldSet == 'Away') {
893                                 log.trace("check_if_hold_justified>quiet since ${state.programSetTimestamp}, current program= ${currentProgName},'Away' hold justified")
894                                 send("quiet since ${state.programSetTimestamp}, current program= ${currentProgName}, 'Away' hold justified")
895                                 ecobee.away()
896                                 return // hold justified, no more adjustments
897                         }    
898                 }
899                 if ((currentSetClimate.toUpperCase()=='HOME') && (residentAway)) {
900                         if ((state?.programHoldSet == 'Home') && (currentProgName.toUpperCase().contains('AWAY'))) {       
901                                 log.trace("check_if_hold_justified>it's been quiet since ${state.programSetTimestamp},resume program...")
902                                 ecobee.resumeThisTstat()
903                                 send("it's been quiet since ${state.programSetTimestamp}, resumed ${currentProgName} program")
904                                 reset_state_program_values()
905                                 check_if_hold_needed()   // check if another type of hold is now needed (ex. 'Away' hold or more heat b/c of low outdoor temp ) 
906                                 return // no more adjustments
907                         }  else if (state?.programHoldSet != 'Away') {  /* Climate was changed since the last climate set, just reset state program values */
908                                 reset_state_program_values()
909                         }
910                 } else if ((currentSetClimate.toUpperCase()=='HOME') && (!residentAway)) { 
911                         if ((state?.programHoldSet == 'Home') && (!currentProgName.toUpperCase().contains('AWAY'))) {       
912                                 ecobee.resumeThisTstat()
913                                 reset_state_program_values()
914                                 if (detailedNotif) {
915                                         send("'Away' hold no longer needed,resumed ${currentProgName} program")
916                                 }
917                                 check_if_hold_needed()   // check if another type of hold is now needed (ex. more heat b/c of low outdoor temp ) 
918                                 return
919                         } else if (state?.programHoldSet == 'Home') {
920                                 log.trace("not quiet since ${state.programSetTimestamp}, current program= ${currentProgName}, 'Home' hold justified")
921                                 if (detailedNotif) {
922                                         send("not quiet since ${state.programSetTimestamp}, current program= ${currentProgName}, 'Home' hold justified")
923                                 }
924                                 ecobee.present()
925                 
926                                 return // hold justified, no more adjustments
927                         }
928                 }            
929         }   // end if motions
930     
931         if (ecobeeMode == 'cool') {
932                 if (outdoorSensor) {            
933                         Integer outdoorHumidity = outdoorSensor.currentHumidity
934                         float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
935                             
936                         if (detailedNotif) {
937                                 log.trace "check_if_hold_justified> outdoorTemp = $outdoorTemp°"
938                                 log.trace "check_if_hold_justified> moreCoolThreshold = $more_cool_threshold°"
939                                 log.trace "check_if_hold_justified> lessCoolThreshold = $less_cool_threshold°"
940                                 log.trace("check_if_hold_justified>evaluate: moreCoolThreshold=${more_cool_threshold} vs. outdoorTemp ${outdoorTemp}°")
941                                 log.trace(
942                                         "check_if_hold_justified>evaluate: moreCoolThresholdHumidity= ${humidity_threshold}% vs. outdoorHumidity ${outdoorHumidity}%")
943                                 log.trace("check_if_hold_justified>evaluate: lessCoolThreshold= ${less_cool_threshold} vs.outdoorTemp ${outdoorTemp}°")
944                                 log.trace(
945                                         "check_if_hold_justified>evaluate: programCoolTemp= ${programCoolTemp}° vs.avgIndoorTemp= ${avg_indoor_temp}°")
946                                 send("eval:  moreCoolThreshold ${more_cool_threshold}° vs.outdoorTemp ${outdoorTemp}°")
947                                 send("eval:  lessCoolThreshold ${less_cool_threshold}° vs.outdoorTemp ${outdoorTemp}°")
948                                 send("eval:  moreCoolThresholdHumidity ${humidity_threshold}% vs. outdoorHumidity ${outdoorHumidity}%")
949                                 send("eval:  programCoolTemp= ${programCoolTemp}° vs. avgIndoorTemp= ${avg_indoor_temp}°")
950                         }
951                         if ((outdoorTemp > less_cool_threshold) && (outdoorTemp < more_cool_threshold) &&
952                                 (outdoorHumidity < humidity_threshold)) {
953                                 send("resuming program, ${less_cool_threshold}° < outdoorTemp <${more_cool_threshold}°")
954                                 ecobee.resumeThisTstat()
955                         } else {
956                                 if (detailedNotif) {
957                                         send("Hold justified, cooling setPoint=${coolTemp}°")
958                                 }
959                                 float actual_temp_diff = (programCoolTemp - coolTemp).round(1).abs()
960                                 if (detailedNotif) {
961                                         send("eval: actual_temp_diff ${actual_temp_diff}° vs. Max temp diff ${max_temp_diff}°")
962                                 }
963                                 if ((actual_temp_diff > max_temp_diff) && (!state?.programHoldSet)) {
964                                         if (detailedNotif) {
965                                                 send("Hold differential too big (${actual_temp_diff}), needs adjustment")
966                                         }
967                                         check_if_hold_needed() // call it to adjust cool temp
968                                 }
969                         }                
970                 } /* if outdoorSensor */
971                 if ((state.tempSensors != []) && (avg_indoor_temp > coolTemp)) {
972                         send("Hold justified, avgIndoorTemp ($avg_indoor_temp°) > coolingSetpoint (${coolTemp}°)")
973                         return
974                 }   
975         
976         } else if (ecobeeMode in ['heat', 'emergency heat']) {
977                 if (outdoorSensor) {            
978                         Integer outdoorHumidity = outdoorSensor.currentHumidity
979                         float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
980                         if (detailedNotif) {
981                                 log.trace "check_if_hold_justified> outdoorTemp = $outdoorTemp°"
982                                 log.trace "check_if_hold_justified> moreHeatThreshold = $more_heat_threshold°"
983                                 log.trace "check_if_hold_justified> lessHeatThreshold = $less_heat_threshold°"
984                                 log.trace("eval: moreHeatingThreshold ${more_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
985                                 log.trace(
986                                         "check_if_hold_justified>evaluate: moreHeatingThresholdHum= ${humidity_threshold}% vs. outdoorHum ${outdoorHumidity}%")
987                                 log.trace("eval:lessHeatThreshold=${less_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
988                                 log.trace(
989                                         "check_if_hold_justified>evaluate: programHeatTemp= ${programHeatTemp}° vs.avgIndoorTemp= ${avg_indoor_temp}°")
990                                 send("eval: moreHeatThreshold ${more_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
991                                 send("eval: lessHeatThreshold ${less_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
992                                 send("eval: moreHeatThresholdHum ${humidity_threshold}% vs. outdoorHum ${outdoorHumidity}%")
993                                 send("eval: programHeatTemp= ${programHeatTemp}° vs. avgIndoorTemp= ${avg_indoor_temp}°")
994                         }
995                         if ((outdoorTemp > more_heat_threshold) && (outdoorTemp < less_heat_threshold) && 
996                                 (outdoorHumidity < humidity_threshold)) {
997                                 send("resuming program, ${less_heat_threshold}° < outdoorTemp > ${more_heat_threshold}°")
998                                 ecobee.resumeThisTstat()
999                         } else {
1000                                 if (detailedNotif) {
1001                                         send("Hold justified, heating setPoint=${heatTemp}°")
1002                                 }
1003                                 float actual_temp_diff = (heatTemp - programHeatTemp).round(1).abs()
1004                                 if (detailedNotif) {
1005                                         send("eval: actualTempDiff ${actual_temp_diff}° vs. Max temp Diff ${max_temp_diff}°")
1006                                 }
1007                                 if ((actual_temp_diff > max_temp_diff) && (!state?.programHoldSet)) {
1008                                         if (detailedNotif) {
1009                                                 send("Hold differential too big ${actual_temp_diff}, needs adjustment")
1010                                         }
1011                                         check_if_hold_needed() // call it to adjust heat temp
1012                                 }
1013                         }
1014                 } /* end if outdoorSensor*/
1015         
1016                 if ((state.tempSensors != []) && (avg_indoor_temp < heatTemp)) {
1017                         send("Hold justified, avgIndoorTemp ($avg_indoor_temp°) < heatingSetpoint (${heatTemp}°)")
1018                         return
1019                 }                
1020         } /* end if heat mode */
1021         log.debug "End of Fcn check_if_hold_justified"
1022 }
1023
1024
1025 private send(msg, askAlexa=false) {
1026 int MAX_EXCEPTION_MSG_SEND=5
1027
1028         // will not send exception msg when the maximum number of send notifications has been reached
1029         if ((msg.contains("exception")) || (msg.contains("error"))) {
1030                 state?.sendExceptionCount=state?.sendExceptionCount+1         
1031                 if (detailedNotif) {        
1032                         log.debug "checking sendExceptionCount=${state?.sendExceptionCount} vs. max=${MAX_EXCEPTION_MSG_SEND}"
1033                 }            
1034                 if (state?.sendExceptionCount >= MAX_EXCEPTION_MSG_SEND) {
1035                         log.debug "send>reached $MAX_EXCEPTION_MSG_SEND exceptions, exiting"
1036                         return        
1037                 }        
1038         }    
1039         def message = "${get_APP_NAME()}>${msg}"
1040
1041         if (sendPushMessage != "No") {
1042                 if (location.contactBookEnabled && recipients) {
1043                         log.debug "contact book enabled"
1044                         sendNotificationToContacts(message, recipients)
1045         } else {
1046                         sendPush(message)
1047                 }            
1048         }
1049         if (askAlexa) {
1050                 sendLocationEvent(name: "AskAlexaMsgQueue", value: "${get_APP_NAME()}", isStateChange: true, descriptionText: msg)        
1051         }        
1052         
1053         if (phoneNumber) {
1054                 log.debug("sending text message")
1055                 sendSms(phoneNumber, message)
1056         }
1057 }
1058
1059
1060 // catchall
1061 def event(evt) {
1062         log.debug "value: $evt.value, event: $evt, settings: $settings, handlerName: ${evt.handlerName}"
1063 }
1064
1065 def cToF(temp) {
1066         return (temp * 1.8 + 32)
1067 }
1068
1069 def fToC(temp) {
1070         return (temp - 32) / 1.8
1071 }
1072
1073 def getImagePath() {
1074         return "http://raw.githubusercontent.com/yracine/device-type.myecobee/master/icons/"
1075 }    
1076
1077 def get_APP_NAME() {
1078         return "MonitorAndSetEcobeeTemp"
1079