2 * MonitorAndSetEcobeeTemp
4 * Copyright 2014 Yves Racine
5 * LinkedIn profile: ca.linkedin.com/pub/yves-racine-m-sc-a/0/406/4b/
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.
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.
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.
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
26 name: "MonitorAndSetEcobeeTemp",
28 author: "Yves Racine",
29 description: "Monitors And Adjusts Ecobee your programmed temperature according to indoor motion sensors & outdoor temperature and humidity.",
31 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
32 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
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")
44 def get_APP_VERSION() { return "3.4.3"}
47 dynamicPage(name: "dashboardPage", title: "MonitorAndSetEcobeeTemp-Dashboard", uninstall: true, nextPage: tempSensorSettings,submitOnChange: true) {
48 section("Press Next in the upper section for Initial setup") {
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
59 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
62 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
64 case 'emergency heat':
67 heatingSetpoint = ecobee?.currentValue('heatingSetpoint')
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"
79 if (heatingSetpoint) {
80 dParagraph = dParagraph + "HeatingSetpoint: ${heatingSetpoint}$scale\n"
83 if (state?.avgTempDiff) {
84 paragraph "AvgTempDiff: ${state?.avgTempDiff}$scale"
89 section("Monitor indoor/outdoor temp & adjust the ecobee thermostat's setpoints") {
90 input "ecobee", "capability.thermostat", title: "For which Ecobee?"
92 section("At which interval in minutes (range=[10..59],default=10 min.)?") {
93 input "givenInterval", "number", title:"Interval", required: false
95 section("Maximum Temp adjustment in Farenheits/Celsius") {
96 input "givenTempDiff", "decimal", title: "Max Temp adjustment [default= +/-5°F/2°C]", required: false
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")
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 */
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
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
126 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
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)
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
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
145 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
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
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
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
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
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
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",
171 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
175 def otherSettings() {
176 dynamicPage(name: "otherSettings", title: "Other Settings", install: true, uninstall: false) {
178 section("What do I use for the Master on/off switch to enable/disable processing? [optional]") {
179 input "powerSwitch", "capability.switch", required: false
181 section("Notifications") {
182 input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required:
184 input "phoneNumber", "phone", title: "Send a text message?", required: false
186 section("Detailed Notifications") {
187 input "detailedNotif", "bool", title: "Detailed Notifications?", required:
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)
194 section([mobileOnly:true]) {
195 label title: "Assign a name for this SmartApp", required: false
198 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
210 // we have had an update
211 // remove everything and reinstall
218 log.debug "Initialized with settings: ${settings}"
220 reset_state_program_values()
221 reset_state_motions()
222 reset_state_tempSensors()
223 state?.exceptionCount=0
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..."
232 log.debug "Scheduling ecobee temp Monitoring and adjustment every ${delay} minutes"
234 schedule("0 0/${delay} * * * ?", monitorAdjustTemp) // monitor & set indoor temp according to delay specified
237 subscribe(indoorSensors, "motion",motionEvtHandler, [filterEvents: false])
238 subscribe(motions, "motion", motionEvtHandler, [filterEvents: false])
240 subscribe(ecobee, "programHeatTemp", programHeatEvtHandler)
241 subscribe(ecobee, "programCoolTemp", programCoolEvtHandler)
242 subscribe(ecobee, "setClimate", setClimateEvtHandler)
243 subscribe(ecobee, "thermostatMode", changeModeHandler)
246 subscribe(powerSwitch, "switch.off", offHandler, [filterEvents: false])
247 subscribe(powerSwitch, "switch.on", onHandler, [filterEvents: false])
250 log.debug("initialize state=$state")
252 // Resume program every time a install/update is done to remove any holds at thermostat (reset).
254 ecobee.resumeThisTstat()
256 subscribe(app, appTouch)
258 state?.poll = [ last: 0, rescheduled: now() ]
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)
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"
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)
285 // Update rescheduled state
287 if (!evt) state.poll["rescheduled"] = now()
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
303 private def sendNotifDelayNotInRange() {
305 send "scheduling delay (${givenInterval} min.) not in range, please restart..."
308 def setClimateEvtHandler(evt) {
309 log.debug "SetClimateEvtHandler>$evt.name: $evt.value"
312 def programHeatEvtHandler(evt) {
313 log.debug "programHeatEvtHandler>$evt.name = $evt.value"
316 def programCoolEvtHandler(evt) {
317 log.debug "programCoolEvtHandler>$evt.name = $evt.value"
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
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()
337 def offHandler(evt) {
338 log.debug "$evt.name: $evt.value"
342 log.debug "$evt.name: $evt.value"
348 private addIndoorSensorsWhenOccupied() {
350 def threshold = residentsQuietThreshold ?: 15 // By default, the delay is 15 minutes
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"}) {
357 log.debug "addTempSensorsWhenOccupied>added occupied sensor ${sensor} as ${sensor.device.id}"
359 state.tempSensors.add(sensor.device.id)
364 log.debug "addTempSensorsWhenOccupied, result = $result"
368 private residentsHaveBeenQuiet() {
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.
375 if (sensor.hasCapability("Refresh")) { // to get the latest motion values
379 def recentStates = sensor.statesSince("motion", t0)
380 if (recentStates.find{it.value == "active"}) {
381 log.debug "residentsHaveBeenQuiet: false, found motion at $sensor"
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
393 def recentStates = sensor.statesSince("motion", t0)
394 if (recentStates.find{it.value == "active"}) {
395 log.debug "residentsHaveBeenQuiet: false, found motion at $sensor"
400 log.debug "residentsHaveBeenQuiet: true"
405 private isProgramScheduleSet(climateName, threshold) {
407 def t0 = new Date(now() - (threshold * 60 *1000))
408 def recentStates = ecobee.statesSince("climateName", t0)
409 if (recentStates.find{it.value == climateName}) {
412 log.debug "isProgramScheduleSet: $result"
417 def monitorAdjustTemp() {
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
428 state?.poll["last"] = now()
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()
437 if (powerSwitch?.currentSwitch == "off") {
439 send("Virtual master switch ${powerSwitch.name} is off, processing on hold...")
445 send("monitoring every ${delay} minute(s)")
448 // Polling of the latest values at the thermostat and at the outdoor sensor
449 def MAX_EXCEPTION_COUNT=5
450 String exceptionCheck, msg
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"
460 // reset exception counter
461 state?.exceptionCount=0
464 log.error "exception $e while trying to poll the device $d, exceptionCount= ${state?.exceptionCount}"
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..."
474 /* Removed the refresh following some ST platform changes which cause "offline" issues to some temp/motion sensors.
476 if ((outdoorSensor) && (outdoorSensor.hasCapability("Refresh"))) {
479 outdoorSensor.refresh()
481 log.debug("not able to refresh ${outdoorSensor}'s temp value")
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()
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()
498 private def reset_state_program_values() {
500 state.programSetTime = null
501 state.programSetTimestamp = ""
502 state.programHoldSet = ""
505 private def reset_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
512 state.tempSensors.add(it.device.id)
516 private def reset_state_motions() {
518 if (settings.motions) {
519 settings.motions.each {
520 state.motions.add(it.device.id)
524 if (settings.indoorSensors) {
525 settings.indoorSensors.each {
526 state.motions.add(it.device.id)
531 private void addAllTempsForAverage(indoorTemps) {
533 for (sensorId in state.tempSensors) { // Add dynamically any indoor Sensor's when occupied
534 def sensor = tempSensors.find{it.device.id == sensorId}
536 log.debug "addAllTempsForAverage>trying to find sensorId=$sensorId in $tempSensors from $state.tempSensors"
538 if (sensor != null) {
540 log.debug "addAllTempsForAverage>found sensor $sensor in $tempSensors"
542 /* Removed the refresh following some ST platform changes which cause "offline" issues to some temp/motion sensors.
544 if (sensor.hasCapability("Refresh")) {
548 def currentTemp =sensor.currentTemperature
549 if (currentTemp != null) {
550 indoorTemps.add(currentTemp) // Add indoor temp to calculate the average based on all sensors
552 log.trace "addAllTempsForAverage>adding $sensor temp (${currentTemp}) to tempSensors List"
559 for (sensorId in state.tempSensors) { // Add dynamically any indoor Sensor's when occupied
560 def sensor = indoorSensors.find{it.device.id == sensorId}
562 log.debug "addAllTempsForAverage>trying to find sensorId=$sensorId in $indoorSensors from $state.tempSensors"
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"
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
583 def scale = getTemperatureScale()
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
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
599 String currentProgName = ecobee.currentClimateName
600 String currentSetClimate = ecobee.currentSetClimate
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()
610 reset_state_tempSensors()
611 if (addIndoorSensorsWhenOccupied()) {
613 log.trace("check_if_hold_needed>some occupied indoor Sensors added for avg calculation")
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
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}"
635 float targetTstatTemp
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")
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
648 boolean residentAway=residentsHaveBeenQuiet()
649 if ((!currentProgName.toUpperCase().contains('AWAY')) && (!residentAway)) {
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
661 } else if ((!currentProgName.toUpperCase().contains('SLEEP')) && (!currentProgName.toUpperCase().contains('AWAY')) &&
663 // Do not adjust the program when ecobee mode = Sleep or 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
676 } /* end if state.motions */
678 if ((location.mode.toUpperCase().contains('AWAY')) || (currentProgName.toUpperCase()=='SLEEP')) {
680 // Do not adjust cooling or heating settings if ST mode == Away or Program Schedule at ecobee == SLEEP
682 log.debug "ST mode is $location.mode, current program is $currentProgName, no adjustment required, exiting..."
686 if (ecobeeMode == 'cool') {
688 Integer outdoorHumidity = outdoorSensor.currentHumidity
689 float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
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°"
695 "check_if_hold_needed>evaluate: moreCoolThreshold= ${more_cool_threshold}° vs. outdoorTemp ${outdoorTemp}°")
697 "check_if_hold_needed>evaluate: moreCoolThresholdHumidity= ${humidity_threshold}% vs. outdoorHum ${outdoorHumidity}%")
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}°")
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
713 String medianTempFormat = String.format('%2.1f', median_temp)
714 send("eval: cool median temp ${medianTempFormat}° vs.outdoorTemp ${outdoorTemp}°")
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}%")
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}°")
726 if (outdoorTemp <= less_cool_threshold) {
727 targetTstatTemp = (programCoolTemp + max_temp_diff).round(1)
728 ecobee.setCoolingSetpoint(targetTstatTemp)
730 "cooling setPoint now=${targetTstatTemp}°, outdoor temp <=${less_cool_threshold}°"
733 } /* end if outdoorSensor */
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
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")
745 } else if (ecobeeMode in ['heat', 'emergency heat']) {
747 Integer outdoorHumidity = outdoorSensor.currentHumidity
748 float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
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}°")
755 "check_if_hold_needed>evaluate: moreHeatThresholdHumidity= ${humidity_threshold}% vs.outdoorHumidity ${outdoorHumidity}%")
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}°")
762 if (outdoorTemp <= more_heat_threshold) {
763 targetTstatTemp = (programHeatTemp + max_temp_diff).round(1)
764 ecobee.setHeatingSetpoint(targetTstatTemp)
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
771 String medianTempFormat = String.format('%2.1f', median_temp)
772 send("eval: heat median temp ${medianTempFormat}° vs.outdoorTemp ${outdoorTemp}°")
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}%")
781 log.trace("eval:lessHeatThreshold=${less_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
782 send("eval: lessHeatThreshold ${less_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
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}°")
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")
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"
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
810 Integer delay = givenInterval ?: 10 // By default, do it every 10 minutes
812 def scale = getTemperatureScale()
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
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
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()
836 reset_state_tempSensors()
837 if (addIndoorSensorsWhenOccupied()) {
838 log.trace("check_if_hold_justified>some occupied indoor Sensors added for avg calculation")
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
846 String ecobeeMode = ecobee.currentThermostatMode.toString()
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!= "") {
866 send("Hold ${state.programHoldSet} has been set")
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
882 else if (state?.programHoldSet != 'Home') { /* Climate was changed since the last climate set, just reset state program values */
883 reset_state_program_values()
885 } else if ((currentSetClimate.toUpperCase()=='AWAY') && (residentAway)) {
886 if ((state?.programHoldSet == 'Away') && (currentProgName.toUpperCase().contains('AWAY'))) {
887 ecobee.resumeThisTstat()
888 reset_state_program_values()
890 send("'Away' hold no longer needed, resumed ${currentProgName} program ")
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")
896 return // hold justified, no more adjustments
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()
910 } else if ((currentSetClimate.toUpperCase()=='HOME') && (!residentAway)) {
911 if ((state?.programHoldSet == 'Home') && (!currentProgName.toUpperCase().contains('AWAY'))) {
912 ecobee.resumeThisTstat()
913 reset_state_program_values()
915 send("'Away' hold no longer needed,resumed ${currentProgName} program")
917 check_if_hold_needed() // check if another type of hold is now needed (ex. more heat b/c of low outdoor temp )
919 } else if (state?.programHoldSet == 'Home') {
920 log.trace("not quiet since ${state.programSetTimestamp}, current program= ${currentProgName}, 'Home' hold justified")
922 send("not quiet since ${state.programSetTimestamp}, current program= ${currentProgName}, 'Home' hold justified")
926 return // hold justified, no more adjustments
931 if (ecobeeMode == 'cool') {
933 Integer outdoorHumidity = outdoorSensor.currentHumidity
934 float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
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}°")
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}°")
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}°")
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()
957 send("Hold justified, cooling setPoint=${coolTemp}°")
959 float actual_temp_diff = (programCoolTemp - coolTemp).round(1).abs()
961 send("eval: actual_temp_diff ${actual_temp_diff}° vs. Max temp diff ${max_temp_diff}°")
963 if ((actual_temp_diff > max_temp_diff) && (!state?.programHoldSet)) {
965 send("Hold differential too big (${actual_temp_diff}), needs adjustment")
967 check_if_hold_needed() // call it to adjust cool temp
970 } /* if outdoorSensor */
971 if ((state.tempSensors != []) && (avg_indoor_temp > coolTemp)) {
972 send("Hold justified, avgIndoorTemp ($avg_indoor_temp°) > coolingSetpoint (${coolTemp}°)")
976 } else if (ecobeeMode in ['heat', 'emergency heat']) {
978 Integer outdoorHumidity = outdoorSensor.currentHumidity
979 float outdoorTemp = outdoorSensor.currentTemperature.toFloat()
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}°")
986 "check_if_hold_justified>evaluate: moreHeatingThresholdHum= ${humidity_threshold}% vs. outdoorHum ${outdoorHumidity}%")
987 log.trace("eval:lessHeatThreshold=${less_heat_threshold}° vs.outdoorTemp ${outdoorTemp}°")
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}°")
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()
1000 if (detailedNotif) {
1001 send("Hold justified, heating setPoint=${heatTemp}°")
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}°")
1007 if ((actual_temp_diff > max_temp_diff) && (!state?.programHoldSet)) {
1008 if (detailedNotif) {
1009 send("Hold differential too big ${actual_temp_diff}, needs adjustment")
1011 check_if_hold_needed() // call it to adjust heat temp
1014 } /* end if outdoorSensor*/
1016 if ((state.tempSensors != []) && (avg_indoor_temp < heatTemp)) {
1017 send("Hold justified, avgIndoorTemp ($avg_indoor_temp°) < heatingSetpoint (${heatTemp}°)")
1020 } /* end if heat mode */
1021 log.debug "End of Fcn check_if_hold_justified"
1025 private send(msg, askAlexa=false) {
1026 int MAX_EXCEPTION_MSG_SEND=5
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}"
1034 if (state?.sendExceptionCount >= MAX_EXCEPTION_MSG_SEND) {
1035 log.debug "send>reached $MAX_EXCEPTION_MSG_SEND exceptions, exiting"
1039 def message = "${get_APP_NAME()}>${msg}"
1041 if (sendPushMessage != "No") {
1042 if (location.contactBookEnabled && recipients) {
1043 log.debug "contact book enabled"
1044 sendNotificationToContacts(message, recipients)
1050 sendLocationEvent(name: "AskAlexaMsgQueue", value: "${get_APP_NAME()}", isStateChange: true, descriptionText: msg)
1054 log.debug("sending text message")
1055 sendSms(phoneNumber, message)
1062 log.debug "value: $evt.value, event: $evt, settings: $settings, handlerName: ${evt.handlerName}"
1066 return (temp * 1.8 + 32)
1070 return (temp - 32) / 1.8
1073 def getImagePath() {
1074 return "http://raw.githubusercontent.com/yracine/device-type.myecobee/master/icons/"
1077 def get_APP_NAME() {
1078 return "MonitorAndSetEcobeeTemp"