Update Switches.groovy
[smartapps.git] / third-party / MonitorAndSetEcobeeHumidity.groovy
1 /***
2  *  Copyright 2014 Yves Racine
3  *  LinkedIn profile: ca.linkedin.com/pub/yves-racine-m-sc-a/0/406/4b/
4  *
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.
10  *
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. 
13  *
14  *  Software Distribution is restricted and shall be done only with Developer's written approval.
15  *
16  *
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 
23  */
24 // Automatically generated. Make future change here.
25 definition(
26         name: "MonitorAndSetEcobeeHumidity",
27         namespace: "yracine",
28         author: "Yves Racine",
29         description: "Monitor And set Ecobee's humidity via your connected humidifier/dehumidifier/HRV/ERV",
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 def get_APP_VERSION() {return "3.3.5"}
36
37 preferences {
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")
44
45 }
46
47 def dashboardPage() {
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?"
51                 }
52                 section("To this humidity level") {
53                         input "givenHumidityLevel", "number", title: "Humidity level (default=calculated based on outside temp)", required: false
54                 }
55                 section("At which interval in minutes (range=[10..59],default =59 min.)?") {
56                         input "givenInterval", "number", title: "Interval",  required: false
57                 }
58                 section("Humidity differential for adjustments") {
59                         input "givenHumidityDiff", "number", title: "Humidity Differential [default=5%]", required: false
60                 }
61                 section("Press Next in the upper section for Initial setup") {
62                         if (ecobee) {            
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
76                                 switch (mode) { 
77                                         case 'cool':
78                                                 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
79                                         break                        
80                                         case 'auto': 
81                                                 coolingSetpoint = ecobee?.currentValue('coolingSetpoint')
82                                         case 'heat':
83                                         case 'emergency heat':
84                                         case 'auto': 
85                                         case 'off': 
86                                                 heatingSetpoint = ecobee?.currentValue('heatingSetpoint')
87                                         break
88                                 }                        
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"
97                                 }     
98                                 if (heatingSetpoint)  { 
99                                         dParagraph = dParagraph + "HeatingSetpoint: ${heatingSetpoint}$scale\n" 
100                                 }
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"
111                                 paragraph dParagraph 
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." 
119                                         }
120                                         paragraph dParagraph 
121                                 }
122                                 if (hasHumidifier=='true') {
123                                         def frostControlFlag = (frostControl) ? 'true' : 'false'
124                                         dParagraph = "HumidifyFrostControl: $frostControlFlag" 
125                                         paragraph dParagraph 
126                                 }
127                                 if ((hasHrv=='true') || (hasErv=='true')) {
128                                         dParagraph=  "MinVentTime $min_vent_time min."
129                                         def freeCoolingFlag= (freeCooling) ? 'true' : 'false'
130                                         dParagraph = dParagraph + "\nFreeCooling: $freeCoolingFlag"                     
131                                         paragraph dParagraph 
132                                 }
133                                 if (ted) {                
134                                         int max_power = givenPowerLevel ?: 3000 // Do not run above 3000w consumption level by default
135                                         dParagraph = "PowerMeter: $ted" + 
136                                                 "\nDoNotRunOver: ${max_power}W"
137                                         paragraph dParagraph 
138                                 }
139                 
140                         } /* end if ecobee */           
141                 }
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")
148                 }            
149                 section("About") {
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"
158                 }
159        
160         } /* end dashboardPage */ 
161     
162 }
163
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
168                 }
169                 section("Choose Outdoor humidity sensor to be used (weatherStation or sensor)") {
170                         input "outdoorSensor", "capability.relativeHumidityMeasurement", title: "Outdoor Humidity Sensor"
171                 }
172                 section {
173                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
174                 }
175         }
176 }
177
178
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
183                 }
184                 section {
185                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
186                 }
187         }
188 }
189
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
194                 }
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
198                 }
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
201                 }
202                 section {
203                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
204                 }
205         }
206 }
207
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
212                 }
213                 section("Minimum HRV/ERV runtime in minutes") {
214                         input "givenVentMinTime", "number", title: "Minimum HRV/ERV [default=20 min.]", description: 'optional',required: false
215                 }
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
218                 }
219                 section {
220                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
221                 }
222         }
223 }
224
225
226
227 def otherSettings() {
228         def enumModes=location.modes.collect{ it.name }
229
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
233                 }
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
237                 }
238                 section("What do I use for the Master on/off switch to enable/disable processing? (optional)") {
239                         input "powerSwitch", "capability.switch", required: false
240                 }
241                 section("Notifications") {
242                         input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required:
243                                 false
244                         input "phoneNumber", "phone", title: "Send a text message?", required: false
245                 }
246                 section("Detailed Notifications") {
247                         input "detailedNotif", "bool", title: "Detailed Notifications?", required:
248                                 false
249                 }
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)
253                 }        
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")
257                 }
258                 section([mobileOnly: true]) {
259                         label title: "Assign a name for this SmartApp", required: false
260                 }
261                 section {
262                         href(name: "toDashboardPage", title: "Back to Dashboard Page", page: "dashboardPage")
263                 }
264         }
265 }
266
267
268
269 def installed() {
270         initialize()
271 }
272
273 def updated() {
274         // we have had an update
275         // remove everything and reinstall
276         unschedule()
277         unsubscribe()
278         initialize()
279 }
280
281 def initialize() {
282         state.currentRevision = null // for further check with thermostatRevision later
283
284         if (powerSwitch) {
285                 subscribe(powerSwitch, "switch.off", offHandler)
286                 subscribe(powerSwitch, "switch.on", onHandler)
287         }
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")
292                 return
293         }
294         if (detailedNotif) {   
295                 log.debug "initialize>scheduling Humidity Monitoring and adjustment every ${delay} minutes"
296         }
297
298         state?.poll = [ last: 0, rescheduled: now() ]
299
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)
306
307         rescheduleIfNeeded()   
308 }
309
310 def appTouch(evt) {
311         rescheduleIfNeeded()
312
313 }
314
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))  
320  
321         if (lastPollTime != currentTime) {    
322                 Double lastPollTimeInMinutes = (lastPollTime/60000).toDouble().round(1)      
323                 log.info "rescheduleIfNeeded>last poll was  ${lastPollTimeInMinutes.toString()} minutes ago"
324         }
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)
328                 setHumidityLevel()
329         }
330     
331     
332         // Update rescheduled state
333     
334         if (!evt) state.poll["rescheduled"] = now()
335 }
336
337
338
339 private def sendNotifDelayNotInRange() {
340
341         send "scheduling delay (${givenInterval} min.) not in range, please restart..."
342
343 }
344
345
346 def offHandler(evt) {
347         log.debug "$evt.name: $evt.value"
348 }
349
350 def onHandler(evt) {
351         log.debug "$evt.name: $evt.value"
352         setHumidityLevel()
353 }
354
355
356
357 def setHumidityLevel() {
358         Integer scheduleInterval = givenInterval ?: 59 // By default, do it every hour
359
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        
365         }   
366
367         state?.poll["last"] = now()
368         
369     
370     
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()
377         }
378
379         boolean foundMode=selectedMode.find{it == (location.currentMode as String)} 
380         if ((selectedMode != null) && (!foundMode)) {
381                 if (detailedNotif) {    
382                         log.trace("setHumidityLevel does not apply,location.mode= $location.mode, selectedMode=${selectedMode},foundMode=${foundMode}, turning off all equipments")
383                 }
384                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'humidifierMode': 'off', 'dehumidifyWithAC': 'false',
385                         'vent': 'off', 'ventilatorFreeCooling': 'false'
386                         ])
387                 return                  
388         }    
389     
390         if (detailedNotif) {
391                 send("monitoring every ${scheduleInterval} minute(s)")
392                 log.debug "Scheduling Humidity Monitoring & Change every ${scheduleInterval}  minutes"
393         }
394
395
396         if (powerSwitch ?.currentSwitch == "off") {
397                 if (detailedNotif) {
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...")
400                 }
401                 return
402         }
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
409
410         def scale = getTemperatureScale()
411         if (scale == 'C') {
412                 min_temp = (givenMinTemp) ? givenMinTemp : -15 // Min. temp in Celcius for using HRV/ERV,otherwise too cold
413         } else {
414
415                 min_temp = (givenMinTemp) ? givenMinTemp : 10 // Min temp in Farenheits for using HRV/ERV,otherwise too cold
416         }
417         Integer max_power = givenPowerLevel ?: 3000 // Do not run above 3000w consumption level by default
418
419
420         //  Polling of all devices
421
422         def MAX_EXCEPTION_COUNT=10
423         String exceptionCheck, msg 
424         try {        
425                 ecobee.poll()
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" 
432                 } else {             
433                         // reset exception counter            
434                         state?.exceptionCount=0       
435                 }                
436         } catch (e) {
437                 log.error "setHumidityLevel>exception $e while trying to poll the device $d, exceptionCount= ${state?.exceptionCount}" 
438         }
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..." 
442                 send " ${msg}"
443                 log.error msg
444                 return        
445         }    
446
447         if (outdoorSensor.hasCapability("Polling")) {
448                 try {    
449                         outdoorSensor.poll()
450                 } catch (e) {
451                         log.debug("MonitorEcobeeHumdity>not able to poll ${outdoorSensor}'s temp value")
452                 }
453         } else if (outdoorSensor.hasCapability("Refresh")) {
454                 try {    
455                         outdoorSensor.refresh()
456                 } catch (e) {
457                         log.debug("MonitorEcobeeHumdity>not able to refresh ${outdoorSensor}'s temp value")
458                 }
459         }        
460         if (ted) {
461
462                 try {
463                         ted.poll()
464                         Integer powerConsumed = ted.currentPower.toInteger()
465                         if (powerConsumed > max_power) {
466
467                                 // peak of energy consumption, turn off all devices
468
469                                 if (detailedNotif) {
470                                         send "all off,power usage is too high=${ted.currentPower}"
471                                         log.debug "all off,power usage is too high=${ted.currentPower}"
472                                 }
473
474                                 ecobee.setThermostatSettings("", ['vent': 'off', 'dehumidifierMode': 'off', 'humidifierMode': 'off',
475                                         'dehumidifyWithAC': 'false'
476                                 ])
477                                 return
478
479                         }
480                 } catch (e) {
481                         log.error "Exception $e while trying to get power data "
482                 }
483         }
484
485
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
496         def outdoorHumidity
497
498         // use the readings from another sensor if better precision neeeded
499         if (indoorSensor) {
500                 indoorHumidity = indoorSensor.currentHumidity
501                 indoorTemp = indoorSensor.currentTemperature
502         }
503     
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
507
508         def target_humidity = givenHumidityLevel ?: (scale == 'C')? find_ideal_indoor_humidity(outdoorTemp):
509                 find_ideal_indoor_humidity(fToC(outdoorTemp))    
510
511         String ecobeeMode = ecobee.currentThermostatMode.toString()
512         if (detailedNotif) {   
513                 log.debug "MonitorAndSetEcobeeHumidity>location.mode = $location.mode"
514                 log.debug "MonitorAndSetEcobeeHumidity>ecobee Mode = $ecobeeMode"
515         }
516
517         outdoorHumidity = (scale == 'C') ?
518                 calculate_corr_humidity(outdoorTemp, outdoorSensorHumidity, indoorTemp) :
519                 calculate_corr_humidity(fToC(outdoorTemp), outdoorSensorHumidity, fToC(indoorTemp))
520
521
522         //  If indoorSensor specified, use the more precise humidity measure instead of ecobeeHumidity
523
524         if ((indoorSensor) && (indoorHumidity < ecobeeHumidity)) {
525                 ecobeeHumidity = indoorHumidity
526         }
527
528         if (detailedNotif) {
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}")
536         }
537
538         if ((ecobeeMode == 'cool' && (hasHrv == 'true' || hasErv == 'true')) &&
539                 (ecobeeHumidity >= (outdoorHumidity - min_humidity_diff)) &&
540                 (ecobeeHumidity >= (target_humidity + min_humidity_diff))) {
541                 if (detailedNotif) {
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"
545                 }
546         
547                 // Turn on the dehumidifer and HRV/ERV, the outdoor humidity is lower or equivalent than inside
548
549                 ecobee.setThermostatSettings("", ['fanMinOnTime': "${min_fan_time}", 'vent': 'minontime', 'ventilatorMinOnTime': "${min_vent_time}"])
550
551
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)) {
556
557                 if (detailedNotif) {
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"
561                 }
562
563                 //      Turn on the dehumidifer, the outdoor temp is not too cold 
564
565                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}",
566                         'humidifierMode': 'off', 'fanMinOnTime': "${min_fan_time}"
567                         ])
568
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)) {
573
574                 if (detailedNotif) {
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"
578                 }
579
580                 //      Turn on the HRV/ERV, the outdoor temp is not too cold 
581
582                 ecobee.setThermostatSettings("", ['fanMinOnTime': "${min_fan_time}", 'vent': 'minontime', 'ventilatorMinOnTime': "${min_vent_time}"])
583
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)) {
588
589
590                 //      Turn off the dehumidifer and HRV/ERV because it's too cold till the next cycle.
591
592                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'dehumidifierLevel': "${target_humidity}",
593                         'humidifierMode': 'off', 'vent': 'off'
594                         ])
595
596                 if (detailedNotif) {
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}"
600                 }
601         } else if ((((ecobeeMode in ['heat','off', 'auto']) && hasHumidifier == 'true')) &&
602                 (ecobeeHumidity < (target_humidity - min_humidity_diff))) {
603
604                 if (detailedNotif) {
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"
607                 }
608                 //      Need a minimum differential to humidify the house to the target if any humidifier available
609
610                 def humidifierMode = (frostControlFlag) ? 'auto' : 'manual'
611                 ecobee.setThermostatSettings("", ['humidifierMode': "${humidifierMode}", 'humidity': "${target_humidity}", 'dehumidifierMode': 'off'])
612
613         } else if (((ecobeeMode == 'cool') && (hasDehumidifier == 'false') && (hasHrv == 'false' && hasErv == 'false')) &&
614                 (ecobeeHumidity > (target_humidity + min_humidity_diff)) &&
615                 (outdoorHumidity > target_humidity)) {
616
617
618                 if (detailedNotif) {
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"
621
622                 }
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
624
625                 ecobee.setThermostatSettings("", ['dehumidifyWithAC': 'true', 'dehumidifierLevel': "${target_humidity}",
626                         'dehumidiferMode': 'off', 'fanMinOnTime': "${min_fan_time}", 'vent': 'off'
627                         ])
628
629
630         } else if ((ecobeeMode == 'cool') && (hasDehumidifier == 'true') && (!useDehumidifierAsHRVFlag) &&
631                 (ecobeeHumidity > (target_humidity + min_humidity_diff))) {
632
633                 //      If mode is cooling and outdoor humidity is too high, then just use dehumidifier if any available
634
635                 if (detailedNotif) {
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"
638                 }
639
640                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}", 'humidifierMode': 'off',
641                         'dehumidifyWithAC': 'false', 'fanMinOnTime': "${min_fan_time}", 'vent': 'off'
642                         ])
643
644
645         } else if ((ecobeeMode == 'cool') && (hasDehumidifier == 'true') && (useDehumidifierAsHRVFlag) &&
646                 (outdoorHumidity < target_humidity + min_humidity_diff) &&
647                 (ecobeeHumidity > (target_humidity + min_humidity_diff))) {
648
649                 //      If mode is cooling and outdoor humidity is too high, then just use dehumidifier if any available
650
651                 if (detailedNotif) {
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"
654                 }
655                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}", 'humidifierMode': 'off',
656                         'dehumidifyWithAC': 'false', 'fanMinOnTime': "${min_fan_time}", 'vent': 'off'
657                         ])
658
659
660
661
662         } else if (((ecobeeMode == 'cool') && (hasDehumidifier == 'true' && hasErv == 'false' && hasHrv == 'false')) &&
663                 (outdoorTemp < indoorTemp) && (freeCoolingFlag)) {
664
665                 //      If mode is cooling and outdoor temp is lower than inside, then just use dehumidifier for better cooling if any available
666
667                 if (detailedNotif) {
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"
670                 }
671
672                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': "${target_humidity}", 'humidifierMode': 'off',
673                         'dehumidifyWithAC': 'false', 'fanMinOnTime': "${min_fan_time}"
674                         ])
675
676
677         } else if ((ecobeeMode == 'cool' && (hasHrv == 'true')) && (outdoorTemp < indoorTemp) && (freeCoolingFlag)) {
678
679                 if (detailedNotif) {
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"
682
683                 }
684                 //      If mode is cooling and outdoor's temp is lower than inside, then use HRV to get fresh air into the house
685
686                 ecobee.setThermostatSettings("", ['fanMinOnTime': "${min_fan_time}",
687                         'vent': 'minontime', 'ventilatorMinOnTime': "${min_vent_time}", 'ventilatorFreeCooling': 'true'
688                         ])
689
690
691         } else if ((outdoorHumidity > ecobeeHumidity) && (ecobeeHumidity > target_humidity)) {
692
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.
695
696                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'humidifierMode': 'off', 'vent': 'off'])
697                 if (detailedNotif) {
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"
700                 }
701
702         } else {
703
704                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'off', 'humidifierMode': 'off', 'dehumidifyWithAC': 'false',
705                         'vent': 'off', 'ventilatorFreeCooling': 'false'
706                         ])
707                 if (detailedNotif) {
708                         log.trace("All off, humidity level (${ecobeeHumidity}%) within range")
709                         send "all off, humidity level (${ecobeeHumidity}%) within range"
710                 }
711         }
712
713         if (useDehumidifierAsHRVFlag) {
714                 use_dehumidifer_as_HRV()    
715         } // end if useDehumidifierAsHRVFlag '
716
717         log.debug "End of Fcn"
718 }
719
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()
726         if (detailedNotif) {
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)}")
729         }
730
731
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
739         if (detailedNotif) {
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"
742         }
743         if (equipStatus.contains("dehumidifier")) {
744                 if (detailedNotif) {
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."
747                 }
748         }
749
750         if ((diffVentTimeInMin > 0) && (!equipStatus.contains("dehumidifier"))) {
751                 if (detailedNotif) {
752                         send "About to turn the dehumidifier on for ${diffVentTimeInMin.toString()} min. within the next hour..."
753                 }
754
755                 ecobee.setThermostatSettings("", ['dehumidifierMode': 'on', 'dehumidifierLevel': '25',
756                                 'fanMinOnTime': "${min_fan_time}"
757                         ])
758                 // calculate the delay to turn off the dehumidifier according to the scheduled monitoring cycle
759
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) {
767                 if (detailedNotif) {
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")
770                 }
771
772         } else if (equipStatus.contains("dehumidifier")) {
773                 turn_off_dehumidifier()
774         }
775 }
776
777
778 private void turn_off_dehumidifier() {
779
780
781         if (detailedNotif) {
782                 send("about to turn off dehumidifier used as HRV....")
783         }
784         log.trace("About to turn off the dehumidifier used as HRV and the fan after timeout")
785
786
787         ecobee.setThermostatSettings("", ['dehumidifierMode': 'off'])
788
789 }
790
791
792 private def bolton(t) {
793
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
796
797
798         Double es = 6.112 * Math.exp(17.67 * t / (t + 243.5))
799         return es
800
801 }
802
803
804 private def calculate_corr_humidity(t1, rh1, t2) {
805
806
807         log.debug("calculate_corr_humidity t1= $t1, rh1=$rh1, t2=$t2")
808
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)
813
814         log.debug("calculate_corr_humidity rh2= $rh2")
815
816         return rh2
817 }
818
819
820 private send(msg, askAlexa=false) {
821 int MAX_EXCEPTION_MSG_SEND=5
822
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         
826                 if (detailedNotif) {        
827                         log.debug "checking sendExceptionCount=${state?.sendExceptionCount} vs. max=${MAX_EXCEPTION_MSG_SEND}"
828                 }            
829                 if (state?.sendExceptionCount >= MAX_EXCEPTION_MSG_SEND) {
830                         log.debug "send>reached $MAX_EXCEPTION_MSG_SEND exceptions, exiting"
831                         return        
832                 }        
833         }    
834         def message = "${get_APP_NAME()}>${msg}"
835
836         if (sendPushMessage != "No") {
837                 if (location.contactBookEnabled && recipients) {
838                         log.debug "contact book enabled"
839                         sendNotificationToContacts(message, recipients)
840         } else {
841                         sendPush(message)
842                 }            
843         }
844         if (askAlexa) {
845                 sendLocationEvent(name: "AskAlexaMsgQueue", value: "${get_APP_NAME()}", isStateChange: true, descriptionText: msg)        
846         }        
847         
848         if (phoneNumber) {
849                 log.debug("sending text message")
850                 sendSms(phoneNumber, message)
851         }
852 }
853
854
855 private int find_ideal_indoor_humidity(outsideTemp) {
856
857         // -30C => 30%, at 0C => 45%
858
859         int targetHum = 45 + (0.5 * outsideTemp)
860         return (Math.max(Math.min(targetHum, 60), 30))
861 }
862
863
864 // catchall
865 def event(evt) {
866         log.debug "value: $evt.value, event: $evt, settings: $settings, handlerName: ${evt.handlerName}"
867 }
868
869 def cToF(temp) {
870         return (temp * 1.8 + 32)
871 }
872
873 def fToC(temp) {
874                 return (temp - 32) / 1.8
875 }
876
877 def getImagePath() {
878         return "http://raw.githubusercontent.com/yracine/device-type.myecobee/master/icons/"
879
880
881 def get_APP_NAME() {
882         return "MonitorAndSetEcobeeHumidity"
883
884