2 * Copyright 2014 Yves Racine
3 * LinkedIn profile: ca.linkedin.com/pub/yves-racine-m-sc-a/0/406/4b/
5 * Developer retains all right, title, copyright, and interest, including all copyright, patent rights, trade secret
6 * in the Background technology. May be subject to consulting fees under the Agreement between the Developer and the Customer.
7 * Developer grants a non exclusive perpetual license to use the Background technology in the Software developed for and delivered
8 * to Customer under this Agreement. However, the Customer shall make no commercial use of the Background technology without
9 * Developer's written consent.
11 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * Software Distribution is restricted and shall be done only with Developer's written approval.
17 * Monitor and set Humidity with Ecobee Thermostat(s):
18 * Monitor humidity level indoor vs. outdoor at a regular interval (in minutes) and
19 * set the humidifier/dehumidifier to a target humidity level.
20 * Use also HRV/ERV/dehumidifier to get fresh air (free cooling) when appropriate based on outdoor temperature.
21 * N.B. Requires MyEcobee device available at
22 * http://www.ecomatiqhomes.com/#!store/tc3yr
24 // Automatically generated. Make future change here.
26 name: "MonitorAndSetEcobeeHumidity",
28 author: "Yves Racine",
29 description: "Monitor And set Ecobee's humidity via your connected humidifier/dehumidifier/HRV/ERV",
31 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
32 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
35 def get_APP_VERSION() {return "3.3.5"}
38 page(name: "dashboardPage", title: "DashboardPage")
39 page(name: "humidifySettings", title: "HumidifySettings")
40 page(name: "dehumidifySettings", title: "DehumidifySettings")
41 page(name: "ventilatorSettings", title: "ventilatorSettings")
42 page(name: "sensorSettings", title: "SensorSettings")
43 page(name: "otherSettings", title: "OtherSettings")
48 dynamicPage(name: "dashboardPage", title: "MonitorAndSetEcobeeHumidity-Dashboard", uninstall: true, nextPage:sensorSettings, submitOnChange: true) {
49 section("Monitor & set the ecobee thermostat's dehumidifer/humidifier/HRV/ERV settings") {
50 input "ecobee", "capability.thermostat", title: "Which Ecobee?"
52 section("To this humidity level") {
53 input "givenHumidityLevel", "number", title: "Humidity level (default=calculated based on outside temp)", required: false
55 section("At which interval in minutes (range=[10..59],default =59 min.)?") {
56 input "givenInterval", "number", title: "Interval", required: false
58 section("Humidity differential for adjustments") {
59 input "givenHumidityDiff", "number", title: "Humidity Differential [default=5%]", required: false
61 section("Press Next in the upper section for Initial setup") {
63 def scale= getTemperatureScale()
64 String currentProgName = ecobee?.currentClimateName
65 String currentProgType = ecobee?.currentProgramType
66 def scheduleProgramName = ecobee?.currentProgramScheduleName
67 String mode =ecobee?.currentThermostatMode.toString()
68 def operatingState=ecobee?.currentThermostatOperatingState
69 def ecobeeHumidity = ecobee.currentHumidity
70 def indoorTemp = ecobee.currentTemperature
71 def hasDehumidifier = (ecobee.currentHasDehumidifier) ? ecobee.currentHasDehumidifier : 'false'
72 def hasHumidifier = (ecobee.currentHasHumidifier) ? ecobee.currentHasHumidifier : 'false'
73 def hasHrv = (ecobee.currentHasHrv) ? ecobee.currentHasHrv : 'false'
74 def hasErv = (ecobee.currentHasErv) ? ecobee.currentHasErv : 'false'
75 def heatingSetpoint,coolingSetpoint
78 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
81 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
83 case 'emergency heat':
86 heatingSetpoint = ecobee?.currentValue('heatingSetpoint')
89 def detailedNotifFlag = (detailedNotif)? 'true':'false'
90 int min_vent_time = (givenVentMinTime!=null) ? givenVentMinTime : 20 // 20 min. ventilator time per hour by default
91 int min_fan_time = (givenFanMinTime!=null) ? givenFanMinTime : 20 // 20 min. fan time per hour by default
92 def dParagraph = "TstatMode: $mode\n" +
93 "TstatOperatingState $operatingState\n" +
94 "TstatTemperature: $indoorTemp${scale}\n"
95 if (coolingSetpoint) {
96 dParagraph = dParagraph + "CoolingSetpoint: ${coolingSetpoint}$scale\n"
98 if (heatingSetpoint) {
99 dParagraph = dParagraph + "HeatingSetpoint: ${heatingSetpoint}$scale\n"
101 dParagraph = dParagraph +
102 "EcobeeClimateSet: $currentProgName\n" +
103 "EcobeeProgramType: $currentProgType\n" +
104 "EcobeeHasHumidifier: $hasHumidifier\n" +
105 "EcobeeHasDeHumidifier: $hasDehumidifier\n" +
106 "EcobeeHasHRV: $hasHrv\n" +
107 "EcobeeHasERV: $hasErv\n" +
108 "EcobeeHumidity: $ecobeeHumidity%\n" +
109 "MinFanTime: ${min_fan_time} min.\n" +
110 "DetailedNotification: ${detailedNotif}\n"
112 if (hasDehumidifier=='true') {
113 def min_temp = (givenMinTemp) ? givenMinTemp : ((scale=='C') ? -15 : 10)
114 def useDehumidifierAsHRVFlag = (useDehumidifierAsHRV) ? 'true' : 'false'
115 dParagraph= "UseDehumidifierAsHRV: $useDehumidifierAsHRVFlag" +
116 "\nMinDehumidifyTemp: $min_temp${scale}"
117 if (useDehumidifierAsHRVFlag=='true') {
118 dParagraph= dParagraph + "\nMinVentTime $min_vent_time min."
122 if (hasHumidifier=='true') {
123 def frostControlFlag = (frostControl) ? 'true' : 'false'
124 dParagraph = "HumidifyFrostControl: $frostControlFlag"
127 if ((hasHrv=='true') || (hasErv=='true')) {
128 dParagraph= "MinVentTime $min_vent_time min."
129 def freeCoolingFlag= (freeCooling) ? 'true' : 'false'
130 dParagraph = dParagraph + "\nFreeCooling: $freeCoolingFlag"
134 int max_power = givenPowerLevel ?: 3000 // Do not run above 3000w consumption level by default
135 dParagraph = "PowerMeter: $ted" +
136 "\nDoNotRunOver: ${max_power}W"
140 } /* end if ecobee */
142 section("Humidifier/Dehumidifier/HRV/ERV Setup") {
143 href(name: "toSensorsPage", title: "Configure your sensors", description: "Tap to Configure...", image: getImagePath() + "HumiditySensor.png", page: "sensorSettings")
144 href(name: "toHumidifyPage", title: "Configure your humidifier settings", description: "Tap to Configure...", image: getImagePath() + "Humidifier.jpg", page: "humidifySettings")
145 href(name: "toDehumidifyPage", title: "Configure your dehumidifier settings", description: "Tap to Configure...", image: getImagePath() + "dehumidifier.png", page: "dehumidifySettings")
146 href(name: "toVentilatorPage", title: "Configure your HRV/ERV settings", description: "Tap to Configure...", image: getImagePath() + "HRV.jpg", page: "ventilatorSettings")
147 href(name: "toNotificationsPage", title: "Other Options & Notification Setup", description: "Tap to Configure...", image: getImagePath() + "Fan.png", page: "otherSettings")
150 paragraph "MonitorAndSetEcobeeHumdity, the smartapp that can control your house's humidity via your connected humidifier/dehumidifier/HRV/ERV"
151 paragraph "Version ${get_APP_VERSION()}"
152 paragraph "If you like this smartapp, please support the developer via PayPal and click on the Paypal link below "
153 href url: "https://www.paypal.me/ecomatiqhomes",
154 title:"Paypal donation..."
155 paragraph "Copyright©2014 Yves Racine"
156 href url:"http://github.com/yracine/device-type.myecobee", style:"embedded", required:false, title:"More information..."
157 description: "http://github.com/yracine/device-type.myecobee/blob/master/README.md"
160 } /* end dashboardPage */
164 def sensorSettings() {
165 dynamicPage(name: "sensorSettings", title: "Sensors to be used", install: false, nextPage: humidifySettings) {
166 section("Choose Indoor humidity sensor to be used for better adjustment (optional, default=ecobee sensor)") {
167 input "indoorSensor", "capability.relativeHumidityMeasurement", title: "Indoor Humidity Sensor", required: false
169 section("Choose Outdoor humidity sensor to be used (weatherStation or sensor)") {
170 input "outdoorSensor", "capability.relativeHumidityMeasurement", title: "Outdoor Humidity Sensor"
173 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
179 def humidifySettings() {
180 dynamicPage(name: "humidifySettings", install: false, uninstall: false, nextPage: dehumidifySettings) {
181 section("Frost control for humidifier [optional]") {
182 input "frostControl", "bool", title: "Frost control [default=false]?", description: 'optional', required: false
185 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
190 def dehumidifySettings() {
191 dynamicPage(name: "dehumidifySettings", install: false, uninstall: false, nextPage: ventilatorSettings) {
192 section("Free cooling using HRV/Dehumidifier [By default=false]") {
193 input "freeCooling", "bool", title: "Free Cooling?", required: false
195 section("Your dehumidifier as HRV input parameters section [optional]") {
196 input "useDehumidifierAsHRV", "bool", title: "Use Dehumidifier as HRV (By default=false)?", description: 'optional', required: false
197 input "givenVentMinTime", "number", title: "Minimum HRV/ERV runtime [default=20]", description: 'optional',required: false
199 section("Minimum outdoor threshold for stopping dehumidification (in Farenheits/Celsius) [optional]") {
200 input "givenMinTemp", "decimal", title: "Min Outdoor Temp [default=10°F/-15°C]", description: 'optional', required: false
203 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
208 def ventilatorSettings() {
209 dynamicPage(name: "ventilatorSettings", install: false, uninstall: false, nextPage: otherSettings) {
210 section("Free cooling using HRV [By default=false]") {
211 input "freeCooling", "bool", title: "Free Cooling?", required: false
213 section("Minimum HRV/ERV runtime in minutes") {
214 input "givenVentMinTime", "number", title: "Minimum HRV/ERV [default=20 min.]", description: 'optional',required: false
216 section("Minimum outdoor threshold for stopping ventilation (in Farenheits/Celsius) [optional]") {
217 input "givenMinTemp", "decimal", title: "Min Outdoor Temp [default=10°F/-15°C]", description: 'optional', required: false
220 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
227 def otherSettings() {
228 def enumModes=location.modes.collect{ it.name }
230 dynamicPage(name: "otherSettings", title: "Other Settings", install: true, uninstall: false) {
231 section("Minimum fan runtime per hour in minutes") {
232 input "givenFanMinTime", "number", title: "Minimum fan runtime [default=20]", required: false
234 section("Check energy consumption at [optional, to avoid using HRV/ERV/Humidifier/Dehumidifier at peak]") {
235 input "ted", "capability.powerMeter", title: "Power meter?", description: 'optional', required: false
236 input "givenPowerLevel", "number", title: "power?", description: 'optional',required: false
238 section("What do I use for the Master on/off switch to enable/disable processing? (optional)") {
239 input "powerSwitch", "capability.switch", required: false
241 section("Notifications") {
242 input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required:
244 input "phoneNumber", "phone", title: "Send a text message?", required: false
246 section("Detailed Notifications") {
247 input "detailedNotif", "bool", title: "Detailed Notifications?", required:
250 section("Enable Amazon Echo/Ask Alexa Notifications [optional, default=false]") {
251 input (name:"askAlexaFlag", title: "Ask Alexa verbal Notifications?", type:"bool",
252 description:"optional",required:false)
254 section("Set Humidity Level only for specific mode(s) [default=all]") {
255 input (name:"selectedMode", type:"enum", title: "Choose Mode", options: enumModes,
256 required: false, multiple:true, description: "Optional")
258 section([mobileOnly: true]) {
259 label title: "Assign a name for this SmartApp", required: false
262 href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
274 // we have had an update
275 // remove everything and reinstall
282 state.currentRevision = null // for further check with thermostatRevision later
285 subscribe(powerSwitch, "switch.off", offHandler)
286 subscribe(powerSwitch, "switch.on", onHandler)
288 Integer delay = givenInterval ?: 59 // By default, do it every hour
289 if ((delay < 10) || (delay > 59)) {
290 log.error "Scheduling delay not in range (${delay} min.), exiting"
291 runIn(30, "sendNotifDelayNotInRange")
295 log.debug "initialize>scheduling Humidity Monitoring and adjustment every ${delay} minutes"
298 state?.poll = [ last: 0, rescheduled: now() ]
300 //Subscribe to different events (ex. sunrise and sunset events) to trigger rescheduling if needed
301 subscribe(location, "sunrise", rescheduleIfNeeded)
302 subscribe(location, "sunset", rescheduleIfNeeded)
303 subscribe(location, "mode", rescheduleIfNeeded)
304 subscribe(location, "sunriseTime", rescheduleIfNeeded)
305 subscribe(location, "sunsetTime", rescheduleIfNeeded)
315 def rescheduleIfNeeded(evt) {
316 if (evt) log.debug("rescheduleIfNeeded>$evt.name=$evt.value")
317 Integer delay = givenInterval ?: 59 // By default, do it every hour
318 BigDecimal currentTime = now()
319 BigDecimal lastPollTime = (currentTime - (state?.poll["last"]?:0))
321 if (lastPollTime != currentTime) {
322 Double lastPollTimeInMinutes = (lastPollTime/60000).toDouble().round(1)
323 log.info "rescheduleIfNeeded>last poll was ${lastPollTimeInMinutes.toString()} minutes ago"
325 if (((state?.poll["last"]?:0) + (delay * 60000) < currentTime) && canSchedule()) {
326 log.info "rescheduleIfNeeded>scheduling setHumidityLevel in ${delay} minutes.."
327 schedule("0 0/${delay} * * * ?", setHumidityLevel)
332 // Update rescheduled state
334 if (!evt) state.poll["rescheduled"] = now()
339 private def sendNotifDelayNotInRange() {
341 send "scheduling delay (${givenInterval} min.) not in range, please restart..."
346 def offHandler(evt) {
347 log.debug "$evt.name: $evt.value"
351 log.debug "$evt.name: $evt.value"
357 def setHumidityLevel() {
358 Integer scheduleInterval = givenInterval ?: 59 // By default, do it every hour
360 def todayDay = new Date().format("dd",location.timeZone)
361 if ((!state?.today) || (todayDay != state?.today)) {
362 state?.exceptionCount=0
363 state?.sendExceptionCount=0
364 state?.today=todayDay
367 state?.poll["last"] = now()
371 //schedule the rescheduleIfNeeded() function
372 if (((state?.poll["rescheduled"]?:0) + (scheduleInterval * 60000)) < now()) {
373 log.info "takeAction>scheduling rescheduleIfNeeded() in ${scheduleInterval} minutes.."
374 schedule("0 0/${scheduleInterval} * * * ?", rescheduleIfNeeded)
375 // Update rescheduled state
376 state?.poll["rescheduled"] = now()
379 boolean foundMode=selectedMode.find{it == (location.currentMode as String)}
380 if ((selectedMode != null) && (!foundMode)) {
382 log.trace("setHumidityLevel does not apply,location.mode= $location.mode, selectedMode=${selectedMode},foundMode=${foundMode}, turning off all equipments")
384 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'humidifierMode': 'off', 'dehumidifyWithAC': 'false',
385 'vent': 'off', 'ventilatorFreeCooling': 'false'
391 send("monitoring every ${scheduleInterval} minute(s)")
392 log.debug "Scheduling Humidity Monitoring & Change every ${scheduleInterval} minutes"
396 if (powerSwitch ?.currentSwitch == "off") {
398 send("Virtual master switch ${powerSwitch.name} is off, processing on hold...")
399 log.debug("Virtual master switch ${powerSwitch.name} is off, processing on hold...")
403 def min_humidity_diff = givenHumidityDiff ?: 5 // 5% humidity differential by default
404 Integer min_fan_time = (givenFanMinTime!=null) ? givenFanMinTime : 20 // 20 min. fan time per hour by default
405 Integer min_vent_time = (givenVentMinTime!=null) ? givenVentMinTime : 20 // 20 min. ventilator time per hour by default
406 def freeCoolingFlag = (freeCooling != null) ? freeCooling : false // Free cooling using the Hrv/Erv/dehumidifier
407 def frostControlFlag = (frostControl != null) ? frostControl : false // Frost Control for humdifier, by default=false
408 def min_temp // Min temp in Farenheits for using HRV/ERV,otherwise too cold
410 def scale = getTemperatureScale()
412 min_temp = (givenMinTemp) ? givenMinTemp : -15 // Min. temp in Celcius for using HRV/ERV,otherwise too cold
415 min_temp = (givenMinTemp) ? givenMinTemp : 10 // Min temp in Farenheits for using HRV/ERV,otherwise too cold
417 Integer max_power = givenPowerLevel ?: 3000 // Do not run above 3000w consumption level by default
420 // Polling of all devices
422 def MAX_EXCEPTION_COUNT=10
423 String exceptionCheck, msg
426 exceptionCheck= ecobee.currentVerboseTrace.toString()
427 if ((exceptionCheck) && ((exceptionCheck.contains("exception") || (exceptionCheck.contains("error")) &&
428 (!exceptionCheck.contains("Java.util.concurrent.TimeoutException"))))) {
429 // check if there is any exception or an error reported in the verboseTrace associated to the device (except the ones linked to rate limiting).
430 state?.exceptionCount=state.exceptionCount+1
431 log.error "setHumidityLevel>found exception/error after polling, exceptionCount= ${state?.exceptionCount}: $exceptionCheck"
433 // reset exception counter
434 state?.exceptionCount=0
437 log.error "setHumidityLevel>exception $e while trying to poll the device $d, exceptionCount= ${state?.exceptionCount}"
439 if ((state?.exceptionCount>=MAX_EXCEPTION_COUNT) || ((exceptionCheck) && (exceptionCheck.contains("Unauthorized")))) {
440 // need to authenticate again
441 msg="too many exceptions/errors or unauthorized exception, $exceptionCheck (${state?.exceptionCount} errors), may need to re-authenticate at ecobee..."
447 if (outdoorSensor.hasCapability("Polling")) {
451 log.debug("MonitorEcobeeHumdity>not able to poll ${outdoorSensor}'s temp value")
453 } else if (outdoorSensor.hasCapability("Refresh")) {
455 outdoorSensor.refresh()
457 log.debug("MonitorEcobeeHumdity>not able to refresh ${outdoorSensor}'s temp value")
464 Integer powerConsumed = ted.currentPower.toInteger()
465 if (powerConsumed > max_power) {
467 // peak of energy consumption, turn off all devices
470 send "all off,power usage is too high=${ted.currentPower}"
471 log.debug "all off,power usage is too high=${ted.currentPower}"
474 ecobee.setThermostatSettings("", ['vent': 'off', 'dehumidifierMode': 'off', 'humidifierMode': 'off',
475 'dehumidifyWithAC': 'false'
481 log.error "Exception $e while trying to get power data "
486 def heatTemp = ecobee.currentHeatingSetpoint
487 def coolTemp = ecobee.currentCoolingSetpoint
488 def ecobeeHumidity = ecobee.currentHumidity
489 def indoorHumidity = 0
490 def indoorTemp = ecobee.currentTemperature
491 def hasDehumidifier = (ecobee.currentHasDehumidifier) ? ecobee.currentHasDehumidifier : 'false'
492 def hasHumidifier = (ecobee.currentHasHumidifier) ? ecobee.currentHasHumidifier : 'false'
493 def hasHrv = (ecobee.currentHasHrv) ? ecobee.currentHasHrv : 'false'
494 def hasErv = (ecobee.currentHasErv) ? ecobee.currentHasErv : 'false'
495 def useDehumidifierAsHRVFlag = (useDehumidifierAsHRV) ? useDehumidifierAsHRV : false
498 // use the readings from another sensor if better precision neeeded
500 indoorHumidity = indoorSensor.currentHumidity
501 indoorTemp = indoorSensor.currentTemperature
504 def outdoorSensorHumidity = outdoorSensor.currentHumidity
505 def outdoorTemp = outdoorSensor.currentTemperature
506 // by default, the humidity level is calculated based on a sliding scale target based on outdoorTemp
508 def target_humidity = givenHumidityLevel ?: (scale == 'C')? find_ideal_indoor_humidity(outdoorTemp):
509 find_ideal_indoor_humidity(fToC(outdoorTemp))
511 String ecobeeMode = ecobee.currentThermostatMode.toString()
513 log.debug "MonitorAndSetEcobeeHumidity>location.mode = $location.mode"
514 log.debug "MonitorAndSetEcobeeHumidity>ecobee Mode = $ecobeeMode"
517 outdoorHumidity = (scale == 'C') ?
518 calculate_corr_humidity(outdoorTemp, outdoorSensorHumidity, indoorTemp) :
519 calculate_corr_humidity(fToC(outdoorTemp), outdoorSensorHumidity, fToC(indoorTemp))
522 // If indoorSensor specified, use the more precise humidity measure instead of ecobeeHumidity
524 if ((indoorSensor) && (indoorHumidity < ecobeeHumidity)) {
525 ecobeeHumidity = indoorHumidity
529 log.trace("Ecobee's humidity: ${ecobeeHumidity} vs. indoor humidity ${indoorHumidity}")
530 log.debug "outdoorSensorHumidity = $outdoorSensorHumidity%, normalized outdoorHumidity based on ambient temperature = $outdoorHumidity%"
531 send "normalized outdoor humidity is ${outdoorHumidity}%,sensor outdoor humidity ${outdoorSensorHumidity}%,vs. indoor Humidity ${ecobeeHumidity}%"
532 log.trace("Evaluate: Ecobee humidity: ${ecobeeHumidity} vs. outdoor humidity ${outdoorHumidity}," +
533 "coolingSetpoint: ${coolTemp} , heatingSetpoint: ${heatTemp}, target humidity=${target_humidity}, fanMinOnTime=${min_fan_time}")
534 log.trace("hasErv=${hasErv}, hasHrv=${hasHrv},hasHumidifier=${hasHumidifier},hasDehumidifier=${hasDehumidifier}, freeCoolingFlag=${freeCoolingFlag}," +
535 "useDehumidifierAsHRV=${useDehumidifierAsHRVFlag}")
538 if ((ecobeeMode == 'cool' && (hasHrv == 'true' || hasErv == 'true')) &&
539 (ecobeeHumidity >= (outdoorHumidity - min_humidity_diff)) &&
540 (ecobeeHumidity >= (target_humidity + min_humidity_diff))) {
542 log.trace "Ecobee is in ${ecobeeMode} mode and its humidity > target humidity level=${target_humidity}, " +
543 "need to dehumidify the house and normalized outdoor humidity is lower (${outdoorHumidity})"
544 send "dehumidify to ${target_humidity}% in ${ecobeeMode} mode, using ERV/HRV"
547 // Turn on the dehumidifer and HRV/ERV, the outdoor humidity is lower or equivalent than inside
549 ecobee.setThermostatSettings("", ['fanMinOnTime': "${min_fan_time}", 'vent': 'minontime', 'ventilatorMinOnTime': "${min_vent_time}"])
552 } else if (((ecobeeMode in ['heat','off', 'auto']) && (hasHrv == 'false' && hasErv == 'false' && hasDehumidifier == 'true')) &&
553 (ecobeeHumidity >= (target_humidity + min_humidity_diff)) &&
554 (ecobeeHumidity >= outdoorHumidity - min_humidity_diff) &&
555 (outdoorTemp > min_temp)) {
558 log.trace "Ecobee is in ${ecobeeMode} mode and its humidity > target humidity level=${target_humidity}, need to dehumidify the house " +
559 "normalized outdoor humidity is within range (${outdoorHumidity}) & outdoor temp is ${outdoorTemp},not too cold"
560 send "dehumidify to ${target_humidity}% in ${ecobeeMode} mode"
563 // Turn on the dehumidifer, the outdoor temp is not too cold
565 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}",
566 'humidifierMode': 'off', 'fanMinOnTime': "${min_fan_time}"
569 } else if (((ecobeeMode in ['heat','off', 'auto']) && ((hasHrv == 'true' || hasErv == 'true') && hasDehumidifier == 'false')) &&
570 (ecobeeHumidity >= (target_humidity + min_humidity_diff)) &&
571 (ecobeeHumidity >= outdoorHumidity - min_humidity_diff) &&
572 (outdoorTemp > min_temp)) {
575 log.trace "Ecobee is in ${ecobeeMode} mode and its humidity > target humidity level=${target_humidity}, need to dehumidify the house " +
576 "normalized outdoor humidity is within range (${outdoorHumidity}) & outdoor temp is ${outdoorTemp},not too cold"
577 send "use HRV/ERV to dehumidify ${target_humidity}% in ${ecobeeMode} mode"
580 // Turn on the HRV/ERV, the outdoor temp is not too cold
582 ecobee.setThermostatSettings("", ['fanMinOnTime': "${min_fan_time}", 'vent': 'minontime', 'ventilatorMinOnTime': "${min_vent_time}"])
584 } else if (((ecobeeMode in ['heat','off', 'auto']) && (hasHrv == 'true' || hasErv == 'true' || hasDehumidifier == 'true')) &&
585 (ecobeeHumidity >= (target_humidity + min_humidity_diff)) &&
586 (ecobeeHumidity >= outdoorHumidity - min_humidity_diff) &&
587 (outdoorTemp <= min_temp)) {
590 // Turn off the dehumidifer and HRV/ERV because it's too cold till the next cycle.
592 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'dehumidifierLevel': "${target_humidity}",
593 'humidifierMode': 'off', 'vent': 'off'
597 log.trace "Ecobee is in ${ecobeeMode} mode and its humidity > target humidity level=${target_humidity}, need to dehumidify the house " +
598 "normalized outdoor humidity is lower (${outdoorHumidity}), but outdoor temp is ${outdoorTemp}: too cold to dehumidify"
599 send "Too cold (${outdoorTemp}°) to dehumidify to ${target_humidity}"
601 } else if ((((ecobeeMode in ['heat','off', 'auto']) && hasHumidifier == 'true')) &&
602 (ecobeeHumidity < (target_humidity - min_humidity_diff))) {
605 log.trace("In ${ecobeeMode} mode, Ecobee's humidity provided is way lower than target humidity level=${target_humidity}, need to humidify the house")
606 send " humidify to ${target_humidity} in ${ecobeeMode} mode"
608 // Need a minimum differential to humidify the house to the target if any humidifier available
610 def humidifierMode = (frostControlFlag) ? 'auto' : 'manual'
611 ecobee.setThermostatSettings("", ['humidifierMode': "${humidifierMode}", 'humidity': "${target_humidity}", 'dehumidifierMode': 'off'])
613 } else if (((ecobeeMode == 'cool') && (hasDehumidifier == 'false') && (hasHrv == 'false' && hasErv == 'false')) &&
614 (ecobeeHumidity > (target_humidity + min_humidity_diff)) &&
615 (outdoorHumidity > target_humidity)) {
619 log.trace("Ecobee humidity provided is way higher than target humidity level=${target_humidity}, need to dehumidify with AC, because normalized outdoor humidity is too high=${outdoorHumidity}")
620 send "dehumidifyWithAC in cooling mode, indoor humidity is ${ecobeeHumidity}% and normalized outdoor humidity (${outdoorHumidity}%) is too high to dehumidify"
623 // If mode is cooling and outdoor humidity is too high then use the A/C to lower humidity in the house if there is no dehumidifier
625 ecobee.setThermostatSettings("", ['dehumidifyWithAC': 'true', 'dehumidifierLevel': "${target_humidity}",
626 'dehumidiferMode': 'off', 'fanMinOnTime': "${min_fan_time}", 'vent': 'off'
630 } else if ((ecobeeMode == 'cool') && (hasDehumidifier == 'true') && (!useDehumidifierAsHRVFlag) &&
631 (ecobeeHumidity > (target_humidity + min_humidity_diff))) {
633 // If mode is cooling and outdoor humidity is too high, then just use dehumidifier if any available
636 log.trace "Dehumidify to ${target_humidity} in ${ecobeeMode} mode using the dehumidifier"
637 send "dehumidify to ${target_humidity}% in ${ecobeeMode} mode using the dehumidifier only"
640 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}", 'humidifierMode': 'off',
641 'dehumidifyWithAC': 'false', 'fanMinOnTime': "${min_fan_time}", 'vent': 'off'
645 } else if ((ecobeeMode == 'cool') && (hasDehumidifier == 'true') && (useDehumidifierAsHRVFlag) &&
646 (outdoorHumidity < target_humidity + min_humidity_diff) &&
647 (ecobeeHumidity > (target_humidity + min_humidity_diff))) {
649 // If mode is cooling and outdoor humidity is too high, then just use dehumidifier if any available
652 log.trace "Dehumidify to ${target_humidity} in ${ecobeeMode} mode using the dehumidifier"
653 send "dehumidify to ${target_humidity}% in ${ecobeeMode} mode using the dehumidifier only"
655 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}", 'humidifierMode': 'off',
656 'dehumidifyWithAC': 'false', 'fanMinOnTime': "${min_fan_time}", 'vent': 'off'
662 } else if (((ecobeeMode == 'cool') && (hasDehumidifier == 'true' && hasErv == 'false' && hasHrv == 'false')) &&
663 (outdoorTemp < indoorTemp) && (freeCoolingFlag)) {
665 // If mode is cooling and outdoor temp is lower than inside, then just use dehumidifier for better cooling if any available
668 log.trace "In cooling mode, outdoor temp is lower than inside, using dehumidifier for free cooling"
669 send "Outdoor temp is lower than inside, using dehumidifier for more efficient cooling"
672 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}", 'humidifierMode': 'off',
673 'dehumidifyWithAC': 'false', 'fanMinOnTime': "${min_fan_time}"
677 } else if ((ecobeeMode == 'cool' && (hasHrv == 'true')) && (outdoorTemp < indoorTemp) && (freeCoolingFlag)) {
680 log.trace("In cooling mode, outdoor temp is lower than inside, using the HRV to get fresh air")
681 send "Outdoor temp is lower than inside, using the HRV for more efficient cooling"
684 // If mode is cooling and outdoor's temp is lower than inside, then use HRV to get fresh air into the house
686 ecobee.setThermostatSettings("", ['fanMinOnTime': "${min_fan_time}",
687 'vent': 'minontime', 'ventilatorMinOnTime': "${min_vent_time}", 'ventilatorFreeCooling': 'true'
691 } else if ((outdoorHumidity > ecobeeHumidity) && (ecobeeHumidity > target_humidity)) {
693 // If indoor humidity is greater than target, but outdoor humidity is way higher than indoor humidity,
694 // just wait for the next cycle & do nothing for now.
696 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'humidifierMode': 'off', 'vent': 'off'])
698 log.trace("Indoor humidity is ${ecobeeHumidity}%, but outdoor humidity (${outdoorHumidity}%) is too high to dehumidify")
699 send "indoor humidity is ${ecobeeHumidity}%, but outdoor humidity ${outdoorHumidity}% is too high to dehumidify"
704 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'humidifierMode': 'off', 'dehumidifyWithAC': 'false',
705 'vent': 'off', 'ventilatorFreeCooling': 'false'
708 log.trace("All off, humidity level (${ecobeeHumidity}%) within range")
709 send "all off, humidity level (${ecobeeHumidity}%) within range"
713 if (useDehumidifierAsHRVFlag) {
714 use_dehumidifer_as_HRV()
715 } // end if useDehumidifierAsHRVFlag '
717 log.debug "End of Fcn"
720 private void use_dehumidifer_as_HRV() {
721 Date now = new Date()
722 String nowInLocalTime = new Date().format("yyyy-MM-dd HH:mm", location.timeZone)
723 Calendar oneHourAgoCal = new GregorianCalendar()
724 oneHourAgoCal.add(Calendar.HOUR, -1)
725 Date oneHourAgo = oneHourAgoCal.getTime()
727 log.debug("local date/time= ${nowInLocalTime}, date/time now in UTC = ${String.format('%tF %<tT',now)}," +
728 "oneHourAgo's date/time in UTC= ${String.format('%tF %<tT',oneHourAgo)}")
732 // Get the dehumidifier's runtime
733 ecobee.getReportData("", oneHourAgo, now, null, null, "dehumidifier", 'false', 'true')
734 ecobee.generateReportRuntimeEvents("dehumidifier", oneHourAgo, now, 0, null, 'lastHour')
735 def dehumidifierRunInMinString=ecobee.currentDehumidifierRuntimeInPeriod
736 float dehumidifierRunInMin = (dehumidifierRunInMinString)? dehumidifierRunInMinString.toFloat().round():0
737 float diffVentTimeInMin = min_vent_time - dehumidifierRunInMin as Float
738 def equipStatus = ecobee.currentEquipmentStatus
740 send "dehumidifier runtime in the last hour is ${dehumidifierRunInMin.toString()} min. vs. desired ventilatorMinOnTime =${min_vent_time.toString()} minutes"
741 log.debug "equipStatus = $equipStatus"
743 if (equipStatus.contains("dehumidifier")) {
745 log.trace("dehumidifier should be running (${equipStatus}), time left to run = ${diffVentTimeInMin.toString()} min. within the current cycle")
746 send "dehumidifier (used as HRV) already running,time left to run = ${diffVentTimeInMin.toString()} min."
750 if ((diffVentTimeInMin > 0) && (!equipStatus.contains("dehumidifier"))) {
752 send "About to turn the dehumidifier on for ${diffVentTimeInMin.toString()} min. within the next hour..."
755 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': '25',
756 'fanMinOnTime': "${min_fan_time}"
758 // calculate the delay to turn off the dehumidifier according to the scheduled monitoring cycle
760 float delay = ((min_vent_time.toFloat() / 60) * scheduleInterval.toFloat()).round()
761 int delayInt = delay.toInteger()
762 delayInt = (delayInt > 1) ? delayInt : 1 // Min. delay should be at least 1 minute, otherwise, the dehumidifier won't stop.
763 send "turning off the dehumidifier (used as HRV) in ${delayInt} minute(s)..."
764 // save the current setpoints before scheduling the dehumidifier to be turned off
765 runIn((delayInt * 60), "turn_off_dehumidifier") // turn off the dehumidifier after delay
766 } else if (diffVentTimeInMin <= 0) {
768 send "dehumidifier has run for at least ${min_vent_time} min. within the last hour, waiting for the next cycle"
769 log.trace("dehumidifier has run for at least ${min_vent_time} min. within the last hour, waiting for the next cycle")
772 } else if (equipStatus.contains("dehumidifier")) {
773 turn_off_dehumidifier()
778 private void turn_off_dehumidifier() {
782 send("about to turn off dehumidifier used as HRV....")
784 log.trace("About to turn off the dehumidifier used as HRV and the fan after timeout")
787 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off'])
792 private def bolton(t) {
794 // Estimates the saturation vapour pressure in hPa at a given temperature, T, in Celcius
795 // return saturation vapour pressure at a given temperature in Celcius
798 Double es = 6.112 * Math.exp(17.67 * t / (t + 243.5))
804 private def calculate_corr_humidity(t1, rh1, t2) {
807 log.debug("calculate_corr_humidity t1= $t1, rh1=$rh1, t2=$t2")
809 Double es = bolton(t1)
810 Double es2 = bolton(t2)
811 Double vapor = rh1 / 100.0 * es
812 Double rh2 = ((vapor / es2) * 100.0).round(2)
814 log.debug("calculate_corr_humidity rh2= $rh2")
820 private send(msg, askAlexa=false) {
821 int MAX_EXCEPTION_MSG_SEND=5
823 // will not send exception msg when the maximum number of send notifications has been reached
824 if ((msg.contains("exception")) || (msg.contains("error"))) {
825 state?.sendExceptionCount=state?.sendExceptionCount+1
827 log.debug "checking sendExceptionCount=${state?.sendExceptionCount} vs. max=${MAX_EXCEPTION_MSG_SEND}"
829 if (state?.sendExceptionCount >= MAX_EXCEPTION_MSG_SEND) {
830 log.debug "send>reached $MAX_EXCEPTION_MSG_SEND exceptions, exiting"
834 def message = "${get_APP_NAME()}>${msg}"
836 if (sendPushMessage != "No") {
837 if (location.contactBookEnabled && recipients) {
838 log.debug "contact book enabled"
839 sendNotificationToContacts(message, recipients)
845 sendLocationEvent(name: "AskAlexaMsgQueue", value: "${get_APP_NAME()}", isStateChange: true, descriptionText: msg)
849 log.debug("sending text message")
850 sendSms(phoneNumber, message)
855 private int find_ideal_indoor_humidity(outsideTemp) {
857 // -30C => 30%, at 0C => 45%
859 int targetHum = 45 + (0.5 * outsideTemp)
860 return (Math.max(Math.min(targetHum, 60), 30))
866 log.debug "value: $evt.value, event: $evt, settings: $settings, handlerName: ${evt.handlerName}"
870 return (temp * 1.8 + 32)
874 return (temp - 32) / 1.8
878 return "http://raw.githubusercontent.com/yracine/device-type.myecobee/master/icons/"
882 return "MonitorAndSetEcobeeHumidity"