/******************************************************************************************** | Application Name: NST Automations | | Copyright (C) 2017, 2018, 2019 Anthony S. | | Authors: Anthony S. (@tonesto7), Eric S. (@E_sch) | | Contributors: Ben W. (@desertblade) | | A few code methods are modeled from those in CoRE by Adrian Caramaliu | | | | December 10, 2022 | | License Info: https://github.com/tonesto7/nest-manager/blob/master/app_license.txt | |********************************************************************************************/ import groovy.json.* import java.text.SimpleDateFormat import groovy.transform.Field @Field static final String sNULL = (String)null @Field static final String sBLANK = '' @Field static final String sSPACE = ' ' @Field static final String sLINEBR = '
' @Field static final String sBOOL = 'bool' @Field static final String sENUM = 'enum' @Field static final String sTIME = 'time' @Field static final String sMODE = 'mode' @Field static final String sCOMPLT = 'complete' @Field static final String sFALSE = 'false' @Field static final String sTRUE = 'true' @Field static final String sON = 'on' @Field static final String sOFF = 'off' @Field static final String sHEAT = 'heat' @Field static final String sCOOL = 'cool' @Field static final String sAUTO = 'auto' @Field static final String sECO = 'eco' @Field static final String sSWIT = 'switch' @Field static final String sTEMP = 'temperature' @Field static final String sTHERM = 'thermostat' @Field static final String sPRESENCE = 'presence' @Field static final String sPRESENT = 'present' @Field static final String sTRACE = 'trace' @Field static final String sINFO = 'info' @Field static final String sDEBUG = 'debug' @Field static final String sWARN = 'warn' @Field static final String sERR = 'error' @Field static final String sCLRRED = 'red' @Field static final String sCLRGRY = 'gray' @Field static final String sCLRORG = 'orange' definition( name: "NST Automations", namespace: "tonesto7", author: "Anthony S.", parent: "tonesto7:NST Manager", description: "This App is used to enable built-in automations for NST Manager", category: "Convenience", iconUrl: sBLANK, iconX2Url: sBLANK, iconX3Url: sBLANK, importUrl: "https://raw.githubusercontent.com/tonesto7/nst-manager-he/master/apps/nstAutomations.groovy") static String appVersion(){ "2.0.8" } preferences{ page(name: "startPage") //Automation Pages page(name: "notAllowedPage") page(name: "selectAutoPage") page(name: "mainAutoPage") page(name: "mainAutoPage1") page(name: "mainAutoPage2") page(name: "remSenShowTempsPage") page(name: "nestModePresPage") page(name: "schMotModePage") page(name: "watchDogPage") //shared pages page(name: "schMotSchedulePage") page(name: "schMotSchedulePage1") page(name: "schMotSchedulePage2") page(name: "schMotSchedulePage3") page(name: "schMotSchedulePage4") page(name: "schMotSchedulePage5") page(name: "schMotSchedulePage6") page(name: "schMotSchedulePage7") page(name: "schMotSchedulePage8") page(name: "scheduleConfigPage") page(name: "tstatConfigAutoPage") page(name: "tstatConfigAutoPage1") page(name: "tstatConfigAutoPage2") page(name: "tstatConfigAutoPage3") page(name: "tstatConfigAutoPage4") page(name: "tstatConfigAutoPage5") page(name: "tstatConfigAutoPage6") page(name: "tstatConfigAutoPage7") page(name: "setNotificationPage") page(name: "setNotificationPage1") page(name: "setNotificationPage2") page(name: "setNotificationPage3") page(name: "setNotificationPage4") page(name: "setNotificationPage5") page(name: "setDayModeTimePage") page(name: "setDayModeTimePage1") page(name: "setDayModeTimePage2") page(name: "setDayModeTimePage3") page(name: "setDayModeTimePage4") page(name: "setDayModeTimePage5") //page(name: "setNotificationTimePage") } /****************************************************************************** | Application Pages | *******************************************************************************/ def startPage(){ //log.info "startPage" if(parent){ Boolean t0=parent.getStateVal("ok2InstallAutoFlag") if( /* !state.isInstalled && */ !t0){ //Logger("Not installed ${t0}") notAllowedPage() }else{ state.isParent=false selectAutoPage() } }else{ notAllowedPage() } } def notAllowedPage (){ dynamicPage(name: "notAllowedPage", title: "This install Method is Not Allowed", install: false, uninstall: true){ section(){ paragraph imgTitle(getAppImg("disable_icon2.png"), paraTitleStr("WE HAVE A PROBLEM!\n\nNST Automations can't be directly installed.\n\nPlease use the Nest Integrations App to configure them.")), required: true, state: null } } } private Boolean isHubitat(){ return hubUID != null } void installed(){ log.debug "${app.getLabel()} Installed with settings: ${settings}" // MUST BE log.debug if(isHubitat() && !app.id) return initialize() } void updated(){ log.debug "${app.getLabel()} Updated...with settings: ${settings}" state.isInstalled=true String appLbl=getCurAppLbl() if(appLbl?.contains("Watchdog")){ if(!(String)state.autoTyp){ state.autoTyp="watchDog" } } initialize() state.lastUpdatedDt=getDtNow() } void uninstalled(){ log.debug "uninstalled" uninstAutomationApp() } void initialize(){ log.debug "${app.label} Initialize..." // Must be log.debug if(!state.isInstalled){ state.isInstalled=true } //Boolean settingsReset=parent.getSettingVal("resetAllData") //if(state.resetAllData || settingsReset){ // if(fixState()){ return } // runIn of fixState will call initAutoApp() //} runIn(6, "initAutoApp", [overwrite: true]) } def subscriber(){ } private Double adj_temp(Double tempF){ Double res = tempF if(getTemperatureScale() == "C"){ return (res - 32.0D) * ( 5.0D/9.0D ) }else{ return res } } void setMyLockId(val){ if(state.myID == null && parent && val){ state.myID=val.toString() } } String getMyLockId(){ if(parent){ return state.myID }else{ return null } } /* def fixState(){ Boolean result=false LogTrace("fixState") def before=getStateSizePerc() if(!state.resetAllData && parent.getSettingVal("resetAllData")){ // automation cleanup called from update() -> initAutoApp() def data=getState()?.findAll { !(it?.key in [ "autoTyp", "autoDisabled", "scheduleList", "resetAllData", "autoDisabledDt", "leakWatRestoreMode", "leakWatTstatOffRequested", "conWatRestoreMode", "conWatlastMode", "conWatTstatOffRequested", "oldremSenTstat", "haveRunFan", "fanCtrlRunDt", "fanCtrlFanOffDt", "extTmpRestoreMode", "extTmpTstatOffRequested", "extTmpSavedTemp", "extTmplastMode", "extTmpSavedCTemp", "extTmpSavedHTemp", "extTmpChgWhileOnDt", "extTmpChgWhileOffDt", // "remDiagLogDataStore", // "restoreId", "restoredFromBackup", "restoreCompleted", "autoTypFlag", "installData", "usageMetricsStore" ]) } // "watchDogAlarmActive", "extTmpAlarmActive", "conWatAlarmActive", "leakWatAlarmActive", data.each { item -> state.remove(item?.key.toString()) } setAutomationStatus() unschedule() unsubscribe() result=true }else if(state.resetAllData && !parent.getSettingVal("resetAllData")){ LogAction("fixState: resetting ALL toggle", sINFO, true) state.resetAllData=false } if(result){ state.resetAllData=true LogAction("fixState: State Data: before: $before after: ${getStateSizePerc()}", sINFO, true) runIn(20, "finishFixState", [overwrite: true]) } return result } void finishFixState(migrate=false){ LogTrace("finishFixState") if(state.resetAllData || migrate){ def tstat=settings.schMotTstat if(tstat){ LogAction("finishFixState found tstat", sINFO, true) getTstatCapabilities(tstat, schMotPrefix()) if(!getMyLockId()){ setMyLockId(app.id) } if((Boolean)settings.schMotRemoteSensor){ LogAction("finishFixState found remote sensor", sINFO, true) if( parent?.remSenLock(tstat?.deviceNetworkId, getMyLockId()) ){ // lock new ID state.remSenTstat=tstat?.deviceNetworkId } if(isRemSenConfigured() && (List)settings.remSensorDay){ LogAction("finishFixState found remote sensor configured", sINFO, true) if(settings.vthermostat != null){ parent?.addRemoveVthermostat(tstat.deviceNetworkId, vthermostat, getMyLockId()) } } } } if(!migrate){ initAutoApp() } //updated() } } */ def selectAutoPage(){ //LogTrace("selectAutoPage()") if(!(String)state.autoTyp){ return dynamicPage(name: "selectAutoPage", title: "Choose an Automation Type", uninstall: false, install: true, nextPage: null){ Boolean thereIsChoice=!parent.automationNestModeEnabled(null) if(thereIsChoice){ section("Set Nest Presence Based on location Modes, Presence Sensor, or Switches:"){ href "mainAutoPage1", title: imgTitle(getAppImg("mode_automation_icon.png"), inputTitleStr("Nest Mode Automations")), description: sBLANK//, params: ["aTyp": "nMode"] } } section("Thermostat Automations: Setpoints, Remote Sensor, External Temp, Humidifier, Contact Sensor, Leak Sensor, Fan Control"){ href "mainAutoPage2", title: imgTitle(getAppImg("thermostat_automation_icon.png"), inputTitleStr("Thermostat Automations")), description: sBLANK //, params: ["aTyp": "schMot"] } } } else { return mainAutoPage([aTyp: (String)state.autoTyp] as Map) } } static String sectionTitleStr(String title) { return title ? "

" + title + "

" : '' } private static String inputTitleStr(String title) { return ''+title+'' } private static String pageTitleStr(String title) { return '

'+title+'

' } private static String paraTitleStr(String title) { return ''+title+'' } static String imgTitle(String imgSrc, String titleStr, String color=sNULL, Integer imgWidth=30, Integer imgHeight=0){ String imgStyle=sBLANK imgStyle += imgWidth>0 ? 'width: '+imgWidth.toString()+'px !important;':'' imgStyle += imgHeight>0 ? imgWidth!=0 ? ' ':''+'height: '+imgHeight.toString()+'px !important;':'' if(color!=sNULL){ return """
${titleStr}
""" } else { return """ ${titleStr}""" } } // string table for titles static String titles(String name, Object... args){ Map page_titles=[ // "page_main": "${lname} setup and management", // "page_add_new_cid_confirm": "Add new CID switch : %s", // "input_selected_devices": "Select device(s) (%s found)", "t_dtse": "Delay to set ECO (in Minutes)", "t_dr": "Delay Restore (in Minutes)", "t_ca": "Configured Alerts", "t_cr": "Configured Restrictions", "t_nt": "Notifications:", "t_nlw": "Nest Location Watchdog" ] if(args) return String.format(page_titles[name], args) else return page_titles[name] } // string table for descriptions static String descriptions(String name, Object... args){ Map element_descriptions=[ "d_ttc": "Tap to configure", "d_ttm": "\n\nTap to modify" ] if(args) return String.format((String)element_descriptions[name],args) else return (String)element_descriptions[name] } static String icons(String name, String napp="App"){ Map icon_names=[ "i_dt": "delay_time", "i_not": "notification", "i_calf": "cal_filter", "i_set": "settings", "i_sw": "switch_on", "i_mod": sMODE, "i_hmod": "hvac_mode", "i_inst": "instruct", "i_err": sERR, "i_cfg": "configure", "i_t": sTEMP //ERS ] //return icon_names[name] String t0=icon_names?."${name}" //LogAction("t0 ${t0}", sWARN, true) if(t0) return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${t0}_icon.png".toString() else return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${name}".toString() } static String getAppImg(String imgName, Boolean on=true){ //return (!disAppIcons || on) ? "https://raw.githubusercontent.com/${gitPath()}/Images/App/$imgName" : sBLANK return on ? icons(imgName) : sBLANK } static String getDevImg(String imgName, Boolean on=true){ //return (!disAppIcons || on) ? "https://raw.githubusercontent.com/${gitPath()}/Images/Devices/$imgName" : sBLANK return on ? icons(imgName, "Devices") : sBLANK } @SuppressWarnings('unused') def mainAutoPage1(params){ //LogTrace("mainAutoPage1()") Map t0=[:] t0.aTyp="nMode" return mainAutoPage( t0 ) //[autoType: "nMode"]) } @SuppressWarnings('unused') def mainAutoPage2(params){ //LogTrace("mainAutoPage2()") Map t0=[:] t0.aTyp="schMot" return mainAutoPage( t0 ) //[autoType: "schMot"]) } def mainAutoPage(Map params){ //LogTrace("mainAutoPage()") String t0=getTemperatureScale() state.tempUnit=(t0 != sNULL) ? t0 : state.tempUnit if(!(Boolean)state.autoDisabled){ state.autoDisabled=false } String autoType=sNULL //If params.aTyp is not null then save to state. if(!(String)state.autoTyp){ if(!params?.aTyp){ Logger("nothing is set mainAutoPage") } else { //Logger("setting autoTyp") state.autoTyp=params?.aTyp autoType=params?.aTyp } }else{ //Logger("setting autoTyp") autoType=(String)state.autoTyp } //Logger("mainPage: ${state.autoTyp} ${autoType}") // If the selected automation has not been configured take directly to the config page. Else show main page //Logger("in mainAutoPage ${autoType} ${state.autoTyp}") if(autoType == "nMode" && !isNestModesConfigured()) { return nestModePresPage() } else if(autoType == "watchDog" && !isWatchdogConfigured()) { return watchDogPage() } else if(autoType == "schMot" && !isSchMotConfigured()) { return schMotModePage() } else { //Logger("in main page") // Main Page Entries //return dynamicPage(name: "mainAutoPage", title: "Automation Configuration", uninstall: false, install: false, nextPage: "nameAutoPage" ){ return dynamicPage(name: "mainAutoPage", title: pageTitleStr("Automation Configuration"), uninstall: true, install: true, nextPage:null ){ section(){ if((Boolean)settings.autoDisabledreq){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("This Automation is currently disabled!\nTurn it back on to to make changes or resume operation")), required: true, state: null }else{ if(getIsAutomationDisabled()){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("This Automation is still disabled!\nPress Next and Done to Activate this Automation Again")), state: sCOMPLT } } if(!getIsAutomationDisabled()){ if(autoType == "nMode"){ //paragraph paraTitleStr("Set Nest Presence Based on location Modes, Presence Sensor, or Switches:") String nDesc=sBLANK nDesc += isNestModesConfigured() ? "Nest Mode:\n • Status: (${strCapitalize(getNestLocPres())})" : sBLANK if(((!(List)settings.nModePresSensor && !settings.nModeSwitch) && ((List)settings.nModeAwayModes && (List)settings.nModeHomeModes))){ nDesc += (List)settings.nModeHomeModes ? "\n • Home Modes: (${((List)settings.nModeHomeModes).size()})" : sBLANK nDesc += (List)settings.nModeAwayModes ? "\n • Away Modes: (${((List)settings.nModeAwayModes).size()})" : sBLANK } nDesc += ((List)settings.nModePresSensor && !settings.nModeSwitch) ? "\n\n${nModePresenceDesc()}" : sBLANK nDesc += (settings.nModeSwitch && !(List)settings.nModePresSensor) ? "\n • Using Switch: (State: ${isSwitchOn(settings.nModeSwitch) ? "ON" : "OFF"})" : sBLANK nDesc += ((Boolean)settings.nModeDelay && settings.nModeDelayVal) ? "\n • Change Delay: (${getEnumValue(longTimeSecEnum(), settings.nModeDelayVal)})" : sBLANK nDesc += (isNestModesConfigured() ) ? "\n • Restrictions Active: (${autoScheduleOk(getAutoType()) ? "NO" : "YES"})" : sBLANK if(isNestModesConfigured()){ nDesc += "\n • Set Thermostats to ECO: (${(Boolean)settings.nModeSetEco ? "On" : "Off"})" if(parent.getSettingVal("cameras")){ nDesc += "\n • Cams On when Away: (${(Boolean)settings.nModeCamOnAway ? "On" : "Off"})" nDesc += "\n • Cams Off when Home: (${(Boolean)settings.nModeCamOffHome ? "On" : "Off"})" if(settings.nModeCamsSel){ nDesc += "\n • Nest Cams Selected: (${nModeCamsSel.size()})" } } } String t1=getNotifConfigDesc("nMode") nDesc += t1 ? "\n\n${t1}" : sBLANK nDesc += t1 || ((List)settings.nModePresSensor || settings.nModeSwitch) || (!(List)settings.nModePresSensor && !settings.nModeSwitch && ((List)settings.nModeAwayModes && (List)settings.nModeHomeModes)) ? descriptions("d_ttm") : sBLANK String nModeDesc=isNestModesConfigured() ? nDesc : sNULL //Logger("nModeDesc ${nModeDesc}") href "nestModePresPage", title: imgTitle(getAppImg("mode_automation_icon.png"), inputTitleStr("Nest Mode Automation Config")), description: nModeDesc ?: descriptions("d_ttc"), state: (nModeDesc ? sCOMPLT : null) } if(autoType == "schMot"){ //Logger("calling schMot config and page") //Logger("in mainAutoPage7") String sModeDesc=(String)getSchMotConfigDesc() href "schMotModePage", title: imgTitle(getAppImg("thermostat_automation_icon.png"), inputTitleStr("Thermostat Automation Config")), description: sModeDesc ?: descriptions("d_ttc"), state: (sModeDesc ? sCOMPLT : null) } if(autoType == "watchDog"){ //paragraph paraTitleStr("Watch your Nest Location for Events:") String watDesc=sBLANK String t1=getNotifConfigDesc("watchDog") if(t1){ List tstats=parent.getSettingVal("thermostats") List prots=parent.getSettingVal("protects") List cams=parent.getSettingVal("cameras") if(tstats || prots || cams){ if(settings.onlineStatMon != false){ t1 += "\n\nWatchDog Monitors:" t1 += "\n • Notify if device is offline" if(tstats){ t1 += "\n • Notify on low temperature extremes" if(settings.thermMissedEco != false){ t1 += "\n • Notify When Away and Thermostat not in Eco Mode" } } if(cams && (settings.onlineStatMon != false)){ Boolean camStreamNotif=parent.getSettingVal("camStreamNotifMsg") if(camStreamNotif != false){ t1 += "\n • Notify on Camera Streaming status changes" } } } } Boolean locPres=parent.getSettingVal("locPresChangeMsg") if(locPres != false){ t1 += "\n • Notify Nest Home/Away Status changes" } } watDesc += t1 ? t1 + descriptions("d_ttm") : sBLANK String watDogDesc=isWatchdogConfigured() ? watDesc : sNULL href "watchDogPage", title: imgTitle(getAppImg("watchdog_icon.png"), inputTitleStr(titles("t_nlw"))), description: watDogDesc ?: descriptions("d_ttc"), state: (watDogDesc ? sCOMPLT : null) } } } section(sectionTitleStr("Automation Options:")){ if(/* state.isInstalled && */ (isNestModesConfigured() || isWatchdogConfigured() || isSchMotConfigured())){ //paragraph paraTitleStr("Enable/Disable this Automation") input "autoDisabledreq", sBOOL, title: imgTitle(getAppImg("disable_icon2.png"), inputTitleStr("Disable this Automation?")), required: false, defaultValue: false /* state.autoDisabled */, submitOnChange: true setAutomationStatus() } input ("showDebug", sBOOL, title: imgTitle(getAppImg("debug_icon.png"), inputTitleStr("Debug Option")), description: "Show ${app?.name} Logs in the IDE?", required: false, defaultValue: false, submitOnChange: true) if(showDebug){ input (name: "advAppDebug", type: sBOOL, title: imgTitle(getAppImg("list_icon.png"), inputTitleStr("Show Verbose Logs?")), required: false, defaultValue: false, submitOnChange: true) }else{ settingUpdate("advAppDebug", sFALSE, sBOOL) } } section(paraTitleStr("Automation Name:")){ String newName=getAutoTypeLabel() if(!app?.label){ app?.updateLabel(newName) } label title: imgTitle(getAppImg("name_tag_icon.png"), inputTitleStr("Label this Automation: Suggested Name: ${newName}")), defaultValue: "${newName}", required: true //, wordWrap: true if(!state.isInstalled){ paragraph "Make sure to name it something that you can easily recognize." } } } } } def getSchMotConfigDesc(Boolean retAsList=false){ if(!isSchMotConfigured()) return null List list=[] if((Boolean)settings.schMotWaterOff){ list.push("Turn Off if Leak Detected") } if((Boolean)settings.schMotContactOff){ list.push("Set ECO if Contact Open") } if((Boolean)settings.schMotExternalTempOff){ list.push("Set ECO based on External Temp") } if((Boolean)settings.schMotRemoteSensor){ list.push("Use Remote Temp Sensors") } if(isTstatSchedConfigured()){ list.push("Setpoint Schedules Created") } if((Boolean)settings.schMotOperateFan){ list.push("Control Fans with HVAC") } if((Boolean)settings.schMotHumidityControl){ list.push("Control Humidifier") } if(retAsList){ return list }else{ String sDesc=sBLANK sDesc += settings.schMotTstat ? "${settings.schMotTstat?.label}" : sBLANK list.each { String ls -> sDesc += "\n • "+ls } String t1=getNotifConfigDesc("schMot") sDesc += t1 ? "\n\n"+t1 : sBLANK sDesc += settings.schMotTstat ? descriptions("d_ttm") : sBLANK return sDesc } } void setAutomationStatus(Boolean upd=false){ Boolean myDis=((Boolean)settings.autoDisabledreq == true) Boolean settingsReset=(parent.getSettingVal("disableAllAutomations") == true) Boolean storAutoType=getAutoType() == "storage" if(settingsReset && !storAutoType){ if(!myDis && settingsReset){ LogAction("setAutomationStatus: Nest Integrations forcing disable", sINFO, true) } myDis=true }else if(storAutoType){ myDis=false } if(!getIsAutomationDisabled() && myDis){ LogAction('Automation Disabled at ('+getDtNow()+')', sINFO, true) state.autoDisabledDt=getDtNow() }else if(getIsAutomationDisabled() && !myDis){ LogAction('Automation Enabled at ('+getDtNow()+')', sINFO, true) state.autoDisabledDt=sNULL } state.autoDisabled=myDis if(upd){ app.update() } } void settingUpdate(String name, value, String type=null){ //LogTrace("settingUpdate($name, $value, $type)...") if(name){ if(value == sBLANK || value == null || value == []){ settingRemove(name) return } } if(name && type){ app?.updateSetting(name, [type: type, value: value]) } else if(name && type == null){ app?.updateSetting(name, value) } } void settingRemove(String name){ //LogTrace("settingRemove($name)...") if(name){ app?.clearSetting(name.toString()) } } def stateUpdate(String key, value){ if(key){ state."${key}"=value; return true } //else { LogAction("stateUpdate: null key $key $value", sERR, true); return false } } void stateRemove(String key){ state.remove(key.toString()) } void initAutoApp(){ //log.debug "${app.label} initAutoApp..." // Must be log.debug if(settings["watchDogFlag"]){ state.autoTyp="watchDog" } String autoType=getAutoType() if(autoType == "nMode"){ parent.automationNestModeEnabled(true) } unschedule() unsubscribe() //def autoDisabled=getIsAutomationDisabled() setAutomationStatus() automationsInst() if(autoType == "schMot" && isSchMotConfigured()){ updateScheduleStateMap() List schedList=getScheduleList() Boolean timersActive; timersActive=false String sLbl Integer cnt, numact cnt=1 numact=0 schedList?.each { Integer scd -> sLbl="schMot_${scd}_" stateRemove("sched${cnt}restrictions") stateRemove("schedule${cnt}SwEnabled") stateRemove("schedule${cnt}PresEnabled") stateRemove("schedule${cnt}MotionEnabled") stateRemove("schedule${cnt}SensorEnabled") stateRemove("schedule${cnt}TimeActive") stateRemove("${sLbl}MotionActiveDt") stateRemove("${sLbl}MotionInActiveDt") stateRemove("${sLbl}oldMotionActive") stateRemove("motion${cnt}UseMotionSettings") stateRemove("motion${cnt}LastisBtwn") Map newscd Boolean act=(Boolean)settings["${sLbl}SchedActive"] if(act){ newscd=cleanUpMap([ m: settings["${sLbl}rstrctMode"], tf: settings["${sLbl}rstrctTimeFrom"], tfc: settings["${sLbl}rstrctTimeFromCustom"], tfo: settings["${sLbl}rstrctTimeFromOffset"], tt: settings["${sLbl}rstrctTimeTo"], ttc: settings["${sLbl}rstrctTimeToCustom"], tto: settings["${sLbl}rstrctTimeToOffset"], w: settings["${sLbl}restrictionDOW"], p1: buildDeviceNameList((List)settings["${sLbl}rstrctPHome"], "and"), p0: buildDeviceNameList((List)settings["${sLbl}rstrctPAway"], "and"), s1: buildDeviceNameList((List)settings["${sLbl}rstrctSWOn"], "and"), s0: buildDeviceNameList((List)settings["${sLbl}rstrctSWOff"], "and"), ctemp: roundTemp(settings["${sLbl}CoolTemp"].toDouble()), htemp: roundTemp(settings["${sLbl}HeatTemp"].toDouble()), hvacm: settings["${sLbl}HvacMode"], sen0: (Boolean)settings["schMotRemoteSensor"] ? buildDeviceNameList((List)settings["${sLbl}remSensor"], "and") : sNULL, thres: (Boolean)settings["schMotRemoteSensor"] ? settings["${sLbl}remSenThreshold"] : null, m0: buildDeviceNameList((List)settings["${sLbl}Motion"], "and"), mctemp: (List)settings["${sLbl}Motion"] ? roundTemp(settings["${sLbl}MCoolTemp"].toDouble()) : null, mhtemp: (List)settings["${sLbl}Motion"] ? roundTemp(settings["${sLbl}MHeatTemp"].toDouble()) : null, mhvacm: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MHvacMode"] : sNULL, // mpresHome: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MPresHome"] : null, // mpresAway: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MPresAway"] : null, mdelayOn: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MDelayValOn"] : null, mdelayOff: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MDelayValOff"] : null ]) numact += 1 //LogTrace("initAutoApp: [Schedule: $scd | sLbl: $sLbl | act: $act | newscd: $newscd]") state."sched${cnt}restrictions"=newscd state."schedule${cnt}SwEnabled"=(newscd?.s1 || newscd?.s0) state."schedule${cnt}PresEnabled"=(newscd?.p1 || newscd?.p0) state."schedule${cnt}MotionEnabled"=!!(newscd?.m0) state."schedule${cnt}SensorEnabled"=!!(newscd?.sen0) //state."schedule${cnt}FanCtrlEnabled"=!!(newscd?.fan0) state."schedule${cnt}TimeActive"=(newscd?.tf || newscd?.tfc || newscd?.tfo || newscd?.tt || newscd?.ttc || newscd?.tto || newscd?.w) if((Boolean)state."schedule${cnt}MotionEnabled"){ Boolean newact=isMotionActive((List)settings["${sLbl}Motion"]) if(newact){ state."${sLbl}MotionActiveDt"=getDtNow() } else { state."${sLbl}MotionInActiveDt"=getDtNow() } state."${sLbl}oldMotionActive"=newact state."motion${cnt}UseMotionSettings"=null // clear automation state of schedule in use motion state state."motion${cnt}LastisBtwn"=false } } timersActive=(timersActive || state."schedule${cnt}TimeActive") cnt += 1 } state.scheduleTimersActive=timersActive state.schedLast=null // clear automation state of schedule in use state.scheduleActiveCnt=numact } subscribeToEvents() scheduler() app.updateLabel(getAutoTypeLabel()) LogAction("Automation Label: ${getAutoTypeLabel()}", sINFO, false) stateRemove("motionnullLastisBtwn") //state.remove("motion1InBtwn") //state.remove("motion2InBtwn") //state.remove("motion3InBtwn") //state.remove("motion4InBtwn") //state.remove("TstatTurnedOff") //state.remove("schedule{1}TimeActive") //state.remove("schedule{2}TimeActive") //state.remove("schedule{3}TimeActive") //state.remove("schedule{4}TimeActive") //state.remove("schedule{5}TimeActive") //state.remove("schedule{6}TimeActive") //state.remove("schedule{7}TimeActive") //state.remove("schedule{8}TimeActive") //state.remove("lastaway") stateRemove("evalSched") stateRemove("dbgAppndName") // cause Automations to re-check with parent for value stateRemove("wDevInst") // cause Automations to re-check with parent for value after updated is called stateRemove("enRemDiagLogging") // cause recheck Boolean dbgState=settings.showDebug || settings.advAppDebug if(!dbgState){ settingUpdate("showDebug", sTRUE, sBOOL); dbgState=true } //settingUpdate("advAppDebug", sFALSE, sBOOL) stateRemove("detailEventHistory") stateRemove("detailExecutionHistory") scheduleAutomationEval(30) if(dbgState){ runIn(1800, logsOff) } } void logsOff(){ log.warn "debug logging disabled..." settingUpdate("showDebug", sFALSE, sBOOL) settingUpdate("advAppDebug", sFALSE, sBOOL) } def uninstAutomationApp(){ //LogTrace("uninstAutomationApp") String autoType=getAutoType() if(autoType == "schMot"){ removeVstat("uninstAutomationApp") } if(autoType == "nMode"){ parent.automationNestModeEnabled(false) } } String getCurAppLbl(){ return app?.label?.toString() } String getAutoTypeLabel(){ //LogTrace("getAutoTypeLabel()") String type=(String)state.autoTyp String appLbl=getCurAppLbl() String newName=appName() == appLabel() ? "NST Automations" : appName() String typeLabel=sBLANK String newLbl String dis=(getIsAutomationDisabled()) ? "\n(Disabled)" : sBLANK if(type == "nMode") { typeLabel="${newName} (NestMode)" } else if(type == "watchDog") { typeLabel="Nest Location ${location.name} Watchdog"} else if(type == "schMot") { typeLabel="${newName} (${settings.schMotTstat?.label})" } //log.info "getAutoTypeLabel: ${type} ${appLbl} ${appName()} ${appLabel()} ${typeLabel}" if(appLbl && appLbl != "Nest Manager" && appLbl != appLabel()){ if(appLbl.contains("\n(Disabled)")){ newLbl=appLbl.replaceAll('\\\n\\(Disabled\\)', '') }else{ newLbl=appLbl } }else{ newLbl=typeLabel } return newLbl+dis } /* def getAppStateData(){ return getState() } */ def getSettingsData(){ List sets=[] settings.sort().each { st -> sets << st } return sets } def getSettingVal(String vara){ if(vara == sNULL){ return settings } return settings[vara] ?: null } def getStateVal(String vara){ return state[vara] ?: null } public void automationsInst(){ state.isNestModesConfigured = isNestModesConfigured() state.isWatchdogConfigured = isWatchdogConfigured() state.isSchMotConfigured = isSchMotConfigured() state.isLeakWatConfigured = isLeakWatConfigured() state.isConWatConfigured = isConWatConfigured() state.isHumCtrlConfigured = isHumCtrlConfigured() state.isExtTmpConfigured = isExtTmpConfigured() state.isRemSenConfigured = isRemSenConfigured() state.isTstatSchedConfigured = isTstatSchedConfigured() state.isFanCtrlConfigured = isFanCtrlSwConfigured() state.isFanCircConfigured = isFanCircConfigured() state.isInstalled=true } List getAutomationsInstalled(){ List list=[] String aType=(String)state.autoTyp switch(aType){ case "nMode": list.push(aType) break case "schMot": Map tmp=[:] tmp[aType]=[] if(isLeakWatConfigured()) { tmp[aType].push("leakWat") } if(isConWatConfigured()) { tmp[aType].push("conWat") } if(isHumCtrlConfigured()) { tmp[aType].push("humCtrl") } if(isExtTmpConfigured()) { tmp[aType].push("extTmp") } if(isRemSenConfigured()) { tmp[aType].push("remSen") } if(isTstatSchedConfigured()) { tmp[aType].push("tSched") } if(isFanCtrlSwConfigured()) { tmp[aType].push("fanCtrl") } if(isFanCircConfigured()) { tmp[aType].push("fanCirc") } if(tmp?.size()){ list.push(tmp) } break case "watchDog": list.push(aType) break } //LogTrace("getAutomationsInstalled List: $list") return list } String getAutomationType(){ return (String)state.autoTyp ?: sNULL } String getAutoType(){ return !parent ? sBLANK : (String)state.autoTyp } Boolean getIsAutomationDisabled(){ Boolean dis=(Boolean)state.autoDisabled return !!dis } void subscribeToEvents(){ //Remote Sensor Subscriptions String autoType=getAutoType() List swlist=[] //Nest Mode Subscriptions if(autoType == "nMode"){ if(isNestModesConfigured()){ if(!(List)settings.nModePresSensor && !settings.nModeSwitch && ((List)settings.nModeHomeModes || (List)settings.nModeAwayModes)){ subscribe(location, sMODE, nModeGenericEvt) } if((List)settings.nModePresSensor && !settings.nModeSwitch){ subscribe((List)settings.nModePresSensor, sPRESENCE, nModeGenericEvt) } if((List)settings.nModeSwitch && !(List)settings.nModePresSensor){ subscribe(settings.nModeSwitch, sSWIT, nModeGenericEvt) } /* List tstats=parent.getSettingVal("thermostats") def foundTstats if(tstats){ foundTstats=tstats?.collect { dni -> def d1=parent.getDevice(dni) if(d1){ //LogAction("Found: ${d1?.displayName} with (Id: ${dni?.key})", sDEBUG, false) //subscribe(d1, "ThermostatMode", automationGenericEvt) // this is not needed for nMode //subscribe(d1, sPRESENCE, automationGenericEvt) // this is not needed, tracking only } return d1 } } */ List t0=[] if((List)settings["nModerstrctSWOn"]){ t0=t0 + (List)settings["nModerstrctSWOn"] } if((List)settings["nModerstrctSWOff"]){ t0=t0 + (List)settings["nModerstrctSWOff"] } for(sw in t0){ if(swlist?.contains(sw)){ //log.trace "found $sw" }else{ swlist.push(sw) subscribe(sw, sSWIT, automationGenericEvt) } } } } //ST Thermostat Motion if(autoType == "schMot"){ if(isSchMotConfigured()){ if((Boolean)settings.schMotWaterOff){ if(isLeakWatConfigured()){ //setInitialVoiceMsgs(leakWatPrefix()) //setCustomVoice(leakWatPrefix()) subscribe(leakWatSensors, "water", leakWatSensorEvt) } } if((Boolean)settings.schMotContactOff){ if(isConWatConfigured()){ //setInitialVoiceMsgs(conWatPrefix()) //setCustomVoice(conWatPrefix()) subscribe(conWatContacts, "contact", conWatContactEvt) List t0=[] if((List)settings["conWatrstrctSWOn"]){ t0=t0 + (List)settings["conWatrstrctSWOn"] } if((List)settings["conWatrstrctSWOff"]){ t0=t0 + (List)settings["conWatrstrctSWOff"] } for(sw in t0){ if(swlist?.contains(sw)){ //log.trace "found $sw" }else{ swlist.push(sw) subscribe(sw, sSWIT, automationGenericEvt) } } } } if((Boolean)settings.schMotHumidityControl){ if(isHumCtrlConfigured()){ subscribe(humCtrlSwitches, sSWIT, automationGenericEvt) subscribe(humCtrlHumidity, "humidity", automationGenericEvt) if(!(Boolean)settings.humCtrlUseWeather && settings.humCtrlTempSensor){ subscribe(humCtrlTempSensor, sTEMP, automationGenericEvt) } if((Boolean)settings.humCtrlUseWeather){ //state.needWeathUpd=true def weather=parent.getSettingVal("weatherDevice") if(weather){ subscribe(weather, sTEMP, extTmpGenericEvt) }else{ LogAction("No weather device found", sERR, true) } } List t0=[] if((List)settings["humCtrlrstrctSWOn"]){ t0=t0 + (List)settings["humCtrlrstrctSWOn"] } if((List)settings["humCtrlrstrctSWOff"]){ t0=t0 + (List)settings["humCtrlrstrctSWOff"] } for(sw in t0){ if(swlist?.contains(sw)){ //log.trace "found $sw" }else{ swlist.push(sw) subscribe(sw, sSWIT, automationGenericEvt) } } } } if((Boolean)settings.schMotExternalTempOff){ if(isExtTmpConfigured()){ //setInitialVoiceMsgs(extTmpPrefix()) //setCustomVoice(extTmpPrefix()) if((Boolean)settings.extTmpUseWeather){ //state.needWeathUpd=true def weather=parent.getSettingVal("weatherDevice") if(weather){ subscribe(weather, sTEMP, extTmpGenericEvt) subscribe(weather, "humidity", extTmpGenericEvt) }else{ LogAction("No weather device found", sERR, true) } } List t0=[] if((List)settings["extTmprstrctSWOn"]){ t0=t0 + (List)settings["extTmprstrctSWOn"] } if((List)settings["extTmprstrctSWOff"]){ t0=t0 + (List)settings["extTmprstrctSWOff"] } for(sw in t0){ if(swlist?.contains(sw)){ //log.trace "found $sw" }else{ swlist.push(sw) subscribe(sw, sSWIT, automationGenericEvt) } } if(!(Boolean)settings.extTmpUseWeather && settings.extTmpTempSensor){ subscribe(extTmpTempSensor, sTEMP, extTmpGenericEvt) } state.extTmpChgWhileOnDt=getDtNow() state.extTmpChgWhileOffDt=getDtNow() } } List senlist=[] if((Boolean)settings.schMotRemoteSensor){ if(isRemSenConfigured()){ // if((List)settings.remSensorDay){ for(sen in (List)settings.remSensorDay){ if(senlist?.contains(sen)){ //log.trace "found $sen" }else{ senlist.push(sen) subscribe(sen, sTEMP, automationGenericEvt) subscribe(sen, "humidity", automationGenericEvt) if((Boolean)settings.schMotExternalTempOff && isExtTmpConfigured()){ subscribe(sen, sTEMP, extTmpGenericEvt) subscribe(sen, "humidity", extTmpGenericEvt) } } } // } } } // if(isTstatSchedConfigured()){ } if((Boolean)settings.schMotOperateFan){ if(isFanCtrlSwConfigured() && fanCtrlFanSwitches){ subscribe(fanCtrlFanSwitches, sSWIT, automationGenericEvt) subscribe(fanCtrlFanSwitches, "level", automationGenericEvt) } List t0=[] if((List)settings["fanCtrlrstrctSWOn"]){ t0=t0 + (List)settings["fanCtrlrstrctSWOn"] } if((List)settings["fanCtrlrstrctSWOff"]){ t0=t0 + (List)settings["fanCtrlrstrctSWOff"] } for(sw in t0){ if(swlist?.contains(sw)){ //log.trace "found $sw" }else{ swlist.push(sw) subscribe(sw, sSWIT, automationGenericEvt) } } } Boolean hasFan=!!(Boolean)state.schMotTstatHasFan if(hasFan && ((Boolean)settings.schMotOperateFan || (Boolean)settings.schMotRemoteSensor || (Boolean)settings.schMotHumidityControl)){ subscribe(settings.schMotTstat, "thermostatFanMode", automationGenericEvt) } List schedList=getScheduleList() Integer cnt; cnt=1 List prlist=[] List mtlist=[] schedList?.each { Integer scd -> String sLbl="schMot_${scd}_" Map restrict=(Map)state."sched${cnt}restrictions" Boolean act=(Boolean)settings["${sLbl}SchedActive"] if(act){ if(state."schedule${cnt}SwEnabled"){ if(restrict?.s1){ for(sw in (List)settings["${sLbl}rstrctSWOn"]){ if(swlist?.contains(sw)){ //log.trace "found $sw" }else{ swlist.push(sw) subscribe(sw, sSWIT, automationGenericEvt) } } } if(restrict?.s0){ for(sw in (List)settings["${sLbl}rstrctSWOff"]){ if(swlist?.contains(sw)){ //log.trace "found $sw" }else{ swlist.push(sw) subscribe(sw, sSWIT, automationGenericEvt) } } } } if(state."schedule${cnt}PresEnabled"){ if(restrict?.p1){ for(pr in (List)settings["${sLbl}rstrctPHome"]){ if(prlist?.contains(pr)){ //log.trace "found $pr" }else{ prlist.push(pr) subscribe(pr, sPRESENCE, automationGenericEvt) } } } if(restrict?.p0){ for(pr in settings["${sLbl}rstrctPAway"]){ if(prlist?.contains(pr)){ //log.trace "found $pr" }else{ prlist.push(pr) subscribe(pr, sPRESENCE, automationGenericEvt) } } } } if(state."schedule${cnt}MotionEnabled"){ if(restrict?.m0){ for(mt in (List)settings["${sLbl}Motion"]){ if(mtlist?.contains(mt)){ //log.trace "found $mt" }else{ mtlist.push(mt) subscribe(mt, "motion", automationMotionEvt) } } } } if(state."schedule${cnt}SensorEnabled"){ if(restrict?.sen0){ for(sen in (List)settings["${sLbl}remSensor"]){ if(senlist?.contains(sen)){ //log.trace "found $sen" }else{ senlist.push(sen) subscribe(sen, sTEMP, automationGenericEvt) } } } } } cnt += 1 } subscribe(settings.schMotTstat, "thermostatMode", automationGenericEvt) subscribe(settings.schMotTstat, "thermostatOperatingState", automationGenericEvt) subscribe(settings.schMotTstat, sTEMP, automationGenericEvt) subscribe(settings.schMotTstat, sPRESENCE, automationGenericEvt) Boolean canCool=state.schMotTstatCanCool if(canCool){ subscribe(settings.schMotTstat, "coolingSetpoint", automationGenericEvt) } Boolean canHeat=state.schMotTstatCanHeat if(canHeat){ subscribe(settings.schMotTstat, "heatingSetpoint", automationGenericEvt) } subscribe(location, "sunset", automationGenericEvt) subscribe(location, "sunrise", automationGenericEvt) subscribe(location, sMODE, automationGenericEvt) } } //watchDog Subscriptions if(autoType == "watchDog"){ // if(isWatchdogConfigured()) List tstats=(List)parent.getSettingVal("thermostats") def foundTstats if(tstats){ foundTstats=tstats?.collect { dni -> def d1=parent.getDevice(dni) if(d1){ //LogAction("Found: ${d1?.displayName} with (Id: ${dni?.key})", sDEBUG, false) subscribe(d1, sTEMP, automationGenericEvt) subscribe(d1, "thermostatMode", automationGenericEvt) subscribe(d1, sPRESENCE, automationGenericEvt) subscribe(d1, "onlineStatus", automationGenericEvt) subscribe(location, sMODE, automationGenericEvt) } return d1 } } List prots=(List)parent.getSettingVal("protects") def foundProts if(prots){ foundProts=prots?.collect { dni -> def d1=parent.getDevice(dni) if(d1){ //LogAction("Found: ${d1?.displayName} with (Id: ${dni?.key})", sDEBUG, false) subscribe(d1, "onlineStatus", automationGenericEvt) } return d1 } } List cams=(List)parent.getSettingVal("cameras") def foundCams if(cams){ foundCams=cams?.collect { dni -> def d1=parent.getDevice(dni) if(d1){ //LogAction("Found: ${d1?.displayName} with (Id: ${dni?.key})", sDEBUG, false) subscribe(d1, "onlineStatus", automationGenericEvt) subscribe(d1, "isStreaming", automationGenericEvt) } return d1 } } } //Alarm status monitoring if any automation has alarm notification enabled if(settings["${autoType}AlarmDevices"] && settings."${pName}AllowAlarmNotif"){ if(settings["${autoType}_Alert_1_Use_Alarm"] || settings["${autoType}_Alert_2_Use_Alarm"]){ subscribe(settings["${autoType}AlarmDevices"], "alarm", alarmAlertEvt) } } } void scheduler(){ def random=new Random() Integer random_int=random.nextInt(60) Integer random_dint=random.nextInt(9) String autoType=getAutoType() if(autoType == "schMot" && (Integer)state.scheduleActiveCnt && (Boolean)state.scheduleTimersActive){ LogTrace("${autoType} scheduled (${random_int} ${random_dint}/5 * * * ?)") schedule("${random_int} ${random_dint}/5 * * * ?", heartbeatAutomation) }else if(autoType != "remDiag" && autoType != "storage"){ LogTrace("${autoType} scheduled (${random_int} ${random_dint}/30 * * * ?)") schedule("${random_int} ${random_dint}/30 * * * ?", heartbeatAutomation) } } @SuppressWarnings('unused') void heartbeatAutomation(){ String autoType=getAutoType() String str="heartbeatAutomation() ${autoType}" Integer val=900 if(autoType == "schMot"){ val=220 } if(getAutoRunInSec() > val){ LogTrace(str+' RUN') runAutomationEval() }else{ LogTrace(str+' NOT NEEDED') } } static Integer defaultAutomationTime(){ return 20 } void scheduleAutomationEval(Integer schedtime=defaultAutomationTime()){ Integer theTime=schedtime if(theTime < defaultAutomationTime()){ theTime=defaultAutomationTime() } String autoType=getAutoType() def random=new Random() Integer random_int=random.nextInt(6) // this randomizes a bunch of automations firing at same time off same event Boolean waitOverride=false switch(autoType){ case "nMode": if(theTime == defaultAutomationTime()){ theTime=14 + random_int // this has nMode fire first as it may change the Nest Mode } break case "schMot": if(theTime == defaultAutomationTime()){ theTime += random_int } Integer schWaitVal=settings.schMotWaitVal?.toInteger() ?: 60 if(schWaitVal > 120){ schWaitVal=120 } Integer t0=getAutoRunSec() if((schWaitVal - t0) >= theTime ){ theTime=(schWaitVal - t0) waitOverride=true } //theTime=Math.min( Math.max(theTime,defaultAutomationTime()), 120) break case "watchDog": if(theTime == defaultAutomationTime()){ theTime=35 + random_int // this has watchdog fire last so other automations can finish changes } break } if(!state.evalSched){ runIn(theTime, "runAutomationEval", [overwrite: true]) state.autoRunInSchedDt=getDtNow() state.evalSched=true state.evalSchedLastTime=theTime }else{ String str="scheduleAutomationEval: " Integer t0=state.evalSchedLastTime if(t0 == null){ t0=0 } Integer timeLeftPrev=t0 - getAutoRunInSec() if(timeLeftPrev < 0){ timeLeftPrev=100 } String str1=" Schedule change: from (${timeLeftPrev}sec) to (${theTime}sec)" if(timeLeftPrev > (theTime + 5) || waitOverride){ if(Math.abs(timeLeftPrev - theTime) > 3){ runIn(theTime, "runAutomationEval", [overwrite: true]) LogTrace(str+'Performing'+str1) state.autoRunInSchedDt=getDtNow() state.evalSched=true state.evalSchedLastTime=theTime } }else{ LogTrace(str+'Skipping'+str1) } } } //def getAutoRunInSec(){ return !state.autoRunInSchedDt ? 100000 : GetTimeDiffSeconds(state.autoRunInSchedDt, null, "getAutoRunInSec").toInteger() } Integer getAutoRunInSec(){ return getTimeSeconds("autoRunInSchedDt", 100000, "getAutoRunInSec") } void runAutomationEval(){ LogTrace("runAutomationEval") String autoType=getAutoType() state.evalSched=false switch(autoType){ case "nMode": if(isNestModesConfigured()){ checkNestMode() } break case "schMot": /* not needed if streaming if(state.needChildUpdate){ state.needChildUpdate=false parent.setNeedChildUpdate() } */ if(isSchMotConfigured()){ schMotCheck() } break case "watchDog": if(isWatchdogConfigured()){ watchDogCheck() } break default: LogAction("runAutomationEval: Invalid Option Received ${autoType}", sWARN, true) break } } /* def getAutomationStats(){ return [ "lastUpdatedDt":state.lastUpdatedDt, "lastEvalDt":state.autoRunDt, "lastEvent":state.lastEventData, "lastActionData":getAutoActionData(), "lastSchedDt":state.autoRunInSchedDt, "lastExecVal":state.autoExecMS, "execAvgVal":(state.evalExecutionHistory != [] ? getAverageValue(state.evalExecutionHistory) : null) ] } */ void storeLastAction(String actionDesc, String actionDt, String autoType){ if(actionDesc && actionDt){ Map newVal=["actionDesc":actionDesc, "dt":actionDt, "autoType":autoType] state.lastAutoActionData=newVal List list list=state.detailActionHistory ?: [] Integer listSize=30 if(list.size() < listSize){ list.push(newVal) } else if(list.size() > listSize){ Integer nSz=(list.size()-listSize) + 1 List nList=list.drop(nSz) nList.push(newVal) list=nList } else if(list.size() == listSize){ List nList=list.drop(1) nList.push(newVal) list=nList } if(list){ state.detailActionHistory=list } } } /* def getAutoActionData(){ if(state.lastAutoActionData){ return state.lastAutoActionData } } */ def automationGenericEvt(evt){ Long startTime=now() Long eventDelay=startTime - (Long)evt.date.getTime() LogAction("${evt?.name?.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", sINFO, false) /* if streaming, this is not needed if(isRemSenConfigured() && settings.vthermostat){ state.needChildUpdate=true } if((Boolean)settings.humCtrlUseWeather && isHumCtrlConfigured()){ state.needWeathUpd=true } */ doTheEvent(evt) } def doTheEvent(evt){ if(!getIsAutomationDisabled()){ scheduleAutomationEval() storeLastEventData(evt) } } /****************************************************************************** | WATCHDOG AUTOMATION LOGIC CODE | *******************************************************************************/ static String watchDogPrefix(){ return "watchDog" } def watchDogPage(){ String pName=watchDogPrefix() dynamicPage(name: "watchDogPage", title: pageTitleStr(titles("t_nlw")), uninstall: false, install: true){ section(sectionTitleStr(titles("t_nt"))){ String t0=getNotifConfigDesc(pName) String pageDesc=t0 ? "${t0}" + descriptions("d_ttm") : sBLANK href "setNotificationPage1", title: imgTitle(getAppImg("i_not"), inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] //ERS if(settings."${pName}NotifOn"){ List tstats=parent.getSettingVal("thermostats") List prots=parent.getSettingVal("protects") List cams=parent.getSettingVal("cameras") if(tstats || prots || cams){ input "onlineStatMon", sBOOL, title: paraTitleStr("Notify When Devices are offline?"), required: false, defaultValue: true, submitOnChange: true } if(tstats && ((Boolean)settings.onlineStatMon != false)){ paragraph imgTitle(getAppImg("i_sw"), paraTitleStr("Temperature warnings on")) input "thermMissedEco", sBOOL, title: paraTitleStr("Notify When Away and Thermostat Not in Eco Mode?"), required: false, defaultValue: true, submitOnChange: true } if(cams && ((Boolean)settings.onlineStatMon != false)){ Boolean camStreamNotif=parent.getSettingVal("camStreamNotifMsg") Boolean mys=camStreamNotif != false String iiact=mys ? "i_sw" : "switch_off_icon.png" //settingUpdate("camStNot", mys.toString(), sBOOL) paragraph imgTitle(getAppImg(iiact), paraTitleStr("Stream Notification (setting from mgr) ${mys}")) //input "camStNot", sBOOL, title: imgTitle(getAppImg("i_sw"), inputTitleStr("Stream Notification (setting from mgr)")), required: false, defaultValue: mys, submitOnChange: true } Boolean locPres=parent.getSettingVal("locPresChangeMsg") Boolean myp=locPres != false String iact=myp ? "i_sw" : "switch_off_icon.png" paragraph imgTitle(getAppImg(iact), paraTitleStr("Nest Location Home/Away changes (setting from mgr) ${myp}")) }else{ settingRemove("thermMissedEco") settingRemove("onlineStatMon") } } } } /* def automationSafetyTempEvt(evt){ Long startTime=now() Long eventDelay=startTime - (Long)evt.date.getTime() LogTrace("Event | Thermostat Safety Temp Exceeded: '${evt.displayName}' (${evt.value}) with a delay of ${eventDelay}ms") if(getIsAutomationDisabled()){ return } else { if(evt?.value == sTRUE){ scheduleAutomationEval() } } storeLastEventData(evt) } */ // Alarms will repeat every watDogRepeatMsgDelay (1 hr default) ALL thermostats void watchDogCheck(){ if(!getIsAutomationDisabled()){ Long execTime=now() state.autoRunDt=getDtNow() List tstats=(List)parent.getSettingVal("thermostats") def foundTstats if(tstats){ foundTstats=tstats?.collect { dni -> if(checkOnline(dni)){ def d1=parent.getDevice(dni) if(d1){ if(!getSafetyTempsOk(d1)){ watchDogAlarmActions(d1.displayName, dni, "temp") //LogAction("watchDogCheck: | Thermostat: ${d1?.displayName} Safety Temp Exceeded: ${exceeded}", sWARN, true) } // This is allowing for warning if Nest has problem of system coming out of ECO while away Boolean nestModeAway=(d1?.currentPresence?.toString() == "not present") //def nestModeAway=(getNestLocPres() == "home") ? false : true if(nestModeAway){ String curMode=d1?.currentThermostatMode?.toString() if(!(curMode in [sECO, sOFF ])){ watchDogAlarmActions(d1.displayName, dni.toString(), sECO) //def pres=d1?.currentPresence?.toString() //LogAction("watchDogCheck: | Thermostat: ${d1?.displayName} is Away and Mode Is Not in ECO | CurMode: (${curMode}) | CurrentPresence: (${pres})", sWARN, true) } } } return d1 } return null } } List prots=(List)parent.getSettingVal("protects") def foundProts if(prots){ foundProts=prots?.collect { dni -> Boolean a=checkOnline(dni) return dni } } List cams=(List)parent.getSettingVal("cameras") def foundCams if(cams){ foundCams=cams?.collect { dni -> if(checkOnline(dni)){ def d1=parent.getDevice(dni) if(d1){ String lastStr=state."lastStr${dni}" String curStream=d1?.currentIsStreaming?.toString() lastStr=lastStr ?: curStream if(curStream){ if(curStream != lastStr){ watchDogAlarmActions(d1.displayName, dni.toString(), "stream", curStream, lastStr) //LogAction("watchDogCheck: | ${d1?.displayName} streaming changed | CurStream: (${curStream}) | prev: (${lastStr})", sWARN, true) } state."lastStr${dni}"=curStream } return dni } } return null } } //Boolean locPres=parent.getSettingVal("locPresChangeMsg") //Boolean myp=locPres != false String curPres=parent.getLocationPresence() ?: sBLANK String lastPres=state.lastPresence ?: sBLANK if(lastpres && (lastPres != curPres)){ state.lastPresence=curPres watchDogAlarmActions(location.name, "Location", "locPres", curPres, lastPres) //LogAction("watchDogCheck: | Nest Location changed | Cur: (${curPres}) | prev: (${lastPres})", sINFO, true) } storeExecutionHistory((now()-execTime), "watchDogCheck") } } Boolean checkOnline(String dni){ def d1=parent.getDevice(dni) if(d1){ String curOnline=d1?.currentOnlineStatus?.toString() if(curOnline != "online"){ watchDogAlarmActions(d1.displayName, dni, "online") //LogAction("watchDogCheck: | ${d1?.displayName} is not online | CurOnline: (${curOnline})", sWARN, true) return false } return true } return false } private void watchDogAlarmActions(dev, String dni, String actType, String p1=sNULL, String p2=sNULL){ String pName=watchDogPrefix() String evtNotifMsg=sBLANK String eventType="Warning" Integer lvl switch(actType){ case "temp": evtNotifMsg="Safety Temp exceeded on ${dev}." break case sECO: if(settings["thermMissedEco"] != false){ evtNotifMsg="Nest Location Home/Away Mode is 'Away' and thermostat [${dev}] is not in ECO." }else{return} break case "online": if(settings["onlineStatMon"] != false){ evtNotifMsg="Device offline ${dev}." }else{return} break case "stream": evtNotifMsg="Camera streaming changed for ${dev} New: ${p1} Old:${p2}." lvl=3 break case "locPres": evtNotifMsg="${dev} Nest Location has changed New: ${p1} Old: ${p2}." lvl=2 eventType="Info" break } Boolean allowNotif=!!settings["${pName}NotifOn"] Boolean canNotif=(allowNotif && (getWatDogSafetyAlertDtSec(dni) > getWatDogRepeatMsgDelayVal())) if(canNotif){ sendNofificationMsg(evtNotifMsg, eventType, pName, lvl) // this uses parent Boolean allowAlarm=allowNotif && settings."${pName}AllowAlarmNotif" if(allowAlarm){ scheduleAlarmOn(pName) } state."watDogSafetyAlDt${dni}"=getDtNow() } String t0=eventType == "Info" ? sINFO : sWARN LogAction("watchDogAlarmActions() | SENT: ${canNotif} | ${evtNotifMsg}", t0, true) } //def getWatDogSafetyAlertDtSec(dni){ return !state."watDogSafetyAlDt${dni}" ? 10000 : GetTimeDiffSeconds(state."watDogSafetyAlDt${dni}", null, "getWatDogSafetyAlertDtSec").toInteger() } Integer getWatDogSafetyAlertDtSec(String dni){ return getTimeSeconds("watDogSafetyAlDt${dni}", 10000, "getWatDogSafetyAlertDtSec") } Integer getWatDogRepeatMsgDelayVal(){ return !watDogRepeatMsgDelay ? 3600 : watDogRepeatMsgDelay.toInteger() } Boolean isWatchdogConfigured(){ return state.autoTyp=="watchDog" } /////////////////////THERMOSTAT AUTOMATION CODE LOGIC /////////////////////// /**************************************************************************** | REMOTE SENSOR AUTOMATION CODE | *****************************************************************************/ static String remSenPrefix(){ return "remSen" } void removeVstat(String callerStr){ String autoType=getAutoType() if(autoType == "schMot"){ String mycallerStr="${callerStr} removeVstat: Could " String t0=mycallerStr String myID=getMyLockId() if(!myID){ setMyLockId(app.id) myID=getMyLockId() } def toRemove=state.remSenTstat if(settings.schMotTstat && myID && parent && toRemove){ if(!parent?.addRemoveVthermostat(toRemove, false, myID)){ t0 += "NOT " } t0 += "cleanup virtual thermostat\n" state.oldremSenTstat=state.remSenTstat state.remSenTstat=null t0 += mycallerStr if( !parent?.remSenUnlock(toRemove, myID) ){ // attempt unlock old ID t0 += "NOT " } LogAction(t0+'Release remote sensor lock', sINFO, false) } } } //Requirements Section Boolean remSenCoolTempsReq() { return ((String)settings.remSenRuleType in ["Cool", "Heat_Cool", "Cool_Circ", "Heat_Cool_Circ"]) } Boolean remSenHeatTempsReq() { return ((String)settings.remSenRuleType in ["Heat", "Heat_Cool", "Heat_Circ", "Heat_Cool_Circ"]) } Boolean remSenDayHeatTempOk() { return (!remSenHeatTempsReq() || (remSenHeatTempsReq() && settings.remSenDayHeatTemp)) } Boolean remSenDayCoolTempOk() { return (!remSenCoolTempsReq() || (remSenCoolTempsReq() && settings.remSenDayCoolTemp)) } Boolean isRemSenConfigured(){ Boolean devOk= !!((List)settings.remSensorDay) return (Boolean)settings.schMotRemoteSensor && devOk && (String)settings.remSenRuleType && remSenDayHeatTempOk() && remSenDayCoolTempOk() } @SuppressWarnings('unused') void automationMotionEvt(evt){ Long startTime=now() Long eventDelay=startTime - evt.date.getTime() LogAction("${evt.name.toUpperCase()} Event | Device: '${evt?.displayName}' | Motion: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", sINFO, false) if(!getIsAutomationDisabled()){ storeLastEventData(evt) Boolean dorunIn=false Integer delay=120 String sLbl Integer mySched=getCurrentSchedule() List schedList=getScheduleList() for (Integer cnt in schedList){ sLbl="schMot_${cnt}_" Boolean act=settings["${sLbl}SchedActive"] if(act && (List)settings["${sLbl}Motion"]){ String str=((List)settings["${sLbl}Motion"]).toString() if(str.contains((String)evt.displayName)){ Boolean oldActive=state."${sLbl}oldMotionActive" Boolean newActive=isMotionActive((List)settings["${sLbl}Motion"]) state."${sLbl}oldMotionActive"=newActive if(oldActive != newActive){ if(newActive){ if(cnt == mySched){ delay=settings."${sLbl}MDelayValOn"?.toInteger() ?: 60 } state."${sLbl}MotionActiveDt"=getDtNow() }else{ if(cnt == mySched){ delay=settings."${sLbl}MDelayValOff"?.toInteger() ?: 30*60 } state."${sLbl}MotionInActiveDt"=getDtNow() } } LogAction("Updating Schedule Motion Sensor State | Schedule: (${cnt} - ${getSchedLbl(cnt)}) | Previous Active: (${oldActive}) | Current Status: ($newActive)", sINFO, false) if(cnt == mySched){ dorunIn=true } } } } /* if(settings["${sLbl}MPresHome"] || settings["${sLbl}MPresAway"]){ if(settings["${sLbl}MPresHome"]){ if(!isSomebodyHome(settings["${sLbl}MPresHome"])) { dorunIn=false } } if(settings["${sLbl}MPresAway"]){ if(isSomebodyHome(settings["${sLbl}MPresAway"])) { dorunIn=false } } } */ if(dorunIn){ Integer val=Math.min( Math.max(delay,defaultAutomationTime()), 60) LogTrace("Automation Schedule Motion | Scheduling Delay Check: ($delay sec) | adjusted (${val}) | Schedule: ($mySched - ${getSchedLbl(mySched)})") scheduleAutomationEval(val) }else{ String str="Motion Event | Skipping Motion Check: " if(mySched){ str += "Motion Sensor is Not Used in Active Schedule (#${mySched} - ${getSchedLbl(getCurrentSchedule())})" }else{ str += "No Active Schedule" } LogTrace(str) } } } Boolean isMotionActive(List sensors){ return anyDevAttValsEqual(sensors, "motion", "active") } static Double getDeviceVarAvg(items, String vara){ Double tempVal=0.0D if(!items){ return tempVal } else { List tmpAvg=items*."${vara}" if(tmpAvg && tmpAvg.size() > 0){ tempVal=(tmpAvg.sum().toDouble() / tmpAvg.size().toDouble()).round(1) } } return tempVal } static Double getDeviceTempAvg(items){ return getDeviceVarAvg(items, "currentTemperature") } static Double getDeviceTemp(dev){ return getDeviceVarAvg(dev, "currentTemperature") } @SuppressWarnings('unused') def remSenShowTempsPage(){ dynamicPage(name: "remSenShowTempsPage", uninstall: false){ if((List)settings.remSensorDay){ String t0=tUnitStr() section("Default Sensor Temps: (Schedules can override)"){ Integer cnt=0 Integer rCnt=((List)settings.remSensorDay).size() String str=sBLANK str += "Sensor Temp (average): (${getDeviceTempAvg((List)settings.remSensorDay)}${t0})\n│" ((List)settings.remSensorDay)?.each { t -> cnt=cnt+1 str += "${(cnt >= 1) ? "${(cnt == rCnt) ? "\n└" : "\n├"}" : "\n└"} ${t?.label}: ${(t?.label?.toString()?.length() > 10) ? "\n${(rCnt == 1 || cnt == rCnt) ? " " : "│"}└ " : sBLANK}(${getDeviceTemp(t)}${t0})" } paragraph imgTitle(getAppImg("i_t"), sectionTitleStr(str)), state: sCOMPLT } } } } Boolean remSendoSetCool(Double ichgval, Double onTemp, Double offTemp){ Double chgval; chgval=ichgval def remSenTstat=settings.schMotTstat def remSenTstatMir=settings.schMotTstatMir try { String hvacMode=remSenTstat ? remSenTstat?.currentThermostatMode?.toString() : sNULL Double curCoolSetpoint=getTstatSetpoint(remSenTstat, sCOOL) Double curHeatSetpoint=getTstatSetpoint(remSenTstat, sHEAT) Double tempChangeVal=!settings.remSenTstatTempChgVal ? 5.0D : Math.min(Math.max(settings.remSenTstatTempChgVal.toDouble(), 2.0D), 5.0D) Double maxTempChangeVal=tempChangeVal * 3.0D chgval=(chgval > (onTemp + maxTempChangeVal)) ? onTemp + maxTempChangeVal : chgval chgval=(chgval < (offTemp - maxTempChangeVal)) ? offTemp - maxTempChangeVal : chgval String t0=tUnitStr() if(chgval != curCoolSetpoint){ scheduleAutomationEval(70) Double cHeat; cHeat=null if(hvacMode in [sAUTO]){ if(curHeatSetpoint >= (offTemp-tempChangeVal)){ cHeat=offTemp - tempChangeVal LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to (${cHeat}${t0}) to allow COOL setting", sINFO, false) if(remSenTstatMir){ remSenTstatMir*.setHeatingSetpoint(cHeat) } } } if(setTstatAutoTemps(remSenTstat, chgval, cHeat, "remSen")){ //LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to (${chgval}${t0}) ", sINFO, true) //storeLastAction("Adjusted Cool Setpoint to (${chgval}${t0}) Heat Setpoint to (${cHeat}${t0})", getDtNow(), "remSen") if(remSenTstatMir){ remSenTstatMir*.setCoolingSetpoint(chgval) } } return true // let all this take effect }else{ LogAction("Remote Sensor: COOL - CoolSetpoint is already (${chgval}${t0}) ", sINFO, false) } } catch (ex){ log.error "remSendoSetCool Exception: ${ex?.message}" } return false } Boolean remSendoSetHeat(Double ichgval, Double onTemp, Double offTemp){ Double chgval; chgval=ichgval def remSenTstat=settings.schMotTstat def remSenTstatMir=settings.schMotTstatMir try { String hvacMode=remSenTstat ? remSenTstat?.currentThermostatMode?.toString() : sNULL Double curCoolSetpoint=getTstatSetpoint(remSenTstat, sCOOL) Double curHeatSetpoint=getTstatSetpoint(remSenTstat, sHEAT) Double tempChangeVal=!settings.remSenTstatTempChgVal ? 5.0D : Math.min(Math.max(settings.remSenTstatTempChgVal.toDouble(), 2.0D), 5.0D) Double maxTempChangeVal=tempChangeVal * 3.0D chgval=(chgval < (onTemp - maxTempChangeVal)) ? onTemp - maxTempChangeVal : chgval chgval=(chgval > (offTemp + maxTempChangeVal)) ? offTemp + maxTempChangeVal : chgval String t0=tUnitStr() if(chgval != curHeatSetpoint){ scheduleAutomationEval(70) Double cCool; cCool=null if(hvacMode in [sAUTO]){ if(curCoolSetpoint <= (offTemp+tempChangeVal)){ cCool=offTemp + tempChangeVal LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to (${cCool}${t0}) to allow HEAT setting", sINFO, false) if(remSenTstatMir){ remSenTstatMir*.setCoolingSetpoint(cCool) } } } if(setTstatAutoTemps(remSenTstat, cCool, chgval, "remSen")){ //LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to (${chgval}${t0})", sINFO, false) //storeLastAction("Adjusted Heat Setpoint to (${chgval}${t0}) Cool Setpoint to (${cCool}${t0})", getDtNow(), "remSen") if(remSenTstatMir){ remSenTstatMir*.setHeatingSetpoint(chgval) } } return true // let all this take effect }else{ LogAction("Remote Sensor: HEAT - HeatSetpoint is already (${chgval}${t0})", sINFO, false) } } catch (ex){ log.error "remSendoSetHeat Exception: ${ex?.message}" } return false } /* Boolean getRemSenModeOk(){ Boolean result=false if((List)settings.remSensorDay ){ result=true } //log.debug "getRemSenModeOk: $result" return result } */ void remSenCheck(){ LogTrace("remSenCheck") if(getIsAutomationDisabled()){ return } try { def remSenTstat=settings.schMotTstat Long execTime=now() String noGoDesc; noGoDesc=sBLANK if( !(List)settings.remSensorDay || !remSenTstat){ noGoDesc += !(List)settings.remSensorDay ? "Missing Required Sensor Selections" : sBLANK noGoDesc += !remSenTstat ? "Missing Required Thermostat device" : sBLANK LogTrace("Remote Sensor NOT Evaluating Status: ${noGoDesc}") }else{ //log.info "remSenCheck: Evaluating Event" // String tempScaleStr=tUnitStr() String hvacMode=remSenTstat ? remSenTstat.currentThermostatMode?.toString() : sNULL if(hvacMode in [ sOFF, sECO] ){ LogAction("Remote Sensor: Skipping Evaluation; The Current Thermostat Mode is '${strCapitalize(hvacMode)}'", sINFO, false) disableOverrideTemps() storeExecutionHistory((now() - execTime), "remSenCheck") return } Double reqSenHeatSetPoint=getRemSenHeatSetTemp(hvacMode) Double reqSenCoolSetPoint=getRemSenCoolSetTemp(hvacMode) Double threshold=getRemoteSenThreshold() if(hvacMode in [sAUTO]){ // check that requested setpoints make sense & notify Double coolheatDiff=Math.abs(reqSenCoolSetPoint - reqSenHeatSetPoint) if( !((reqSenCoolSetPoint > reqSenHeatSetPoint) && (coolheatDiff >= 2.0)) ){ LogAction("remSenCheck: Invalid Setpoints with auto mode: (${reqSenCoolSetPoint})/(${reqSenHeatSetPoint}, ${threshold})", sWARN, true) storeExecutionHistory((now() - execTime), "remSenCheck") return } } Double tempChangeVal=!settings.remSenTstatTempChgVal ? 5.0D : Math.min(Math.max(settings.remSenTstatTempChgVal.toDouble(), 2.0D), 5.0D) Double maxTempChangeVal=tempChangeVal * 3.0D Double curTstatTemp=getDeviceTemp(remSenTstat) Double curSenTemp=getRemoteSenTemp() String curTstatOperState=remSenTstat.currentThermostatOperatingState //String curTstatFanMode=remSenTstat.currentThermostatFanMode //Boolean fanOn=(curTstatFanMode == sON || curTstatFanMode == "circulate") Double curCoolSetpoint=getTstatSetpoint(remSenTstat, sCOOL) Double curHeatSetpoint=getTstatSetpoint(remSenTstat, sHEAT) Boolean acRunning=(curTstatOperState == "cooling") Boolean heatRunning=(curTstatOperState == "heating") /* LogAction("remSenCheck: Rule Type: ${getEnumValue(remSenRuleEnum("heatcool"), settings.remSenRuleType)}", sINFO, false) LogAction("remSenCheck: Sensor Temp: ${curSenTemp}", sINFO, false) LogAction("remSenCheck: Thermostat Info - ( Temperature: (${curTstatTemp}) | HeatSetpoint: (${curHeatSetpoint}) | CoolSetpoint: (${curCoolSetpoint}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) | FanMode: (${curTstatFanMode}) )", sINFO, false) LogAction("remSenCheck: Desired Temps - Heat: ${reqSenHeatSetPoint} | Cool: ${reqSenCoolSetPoint}", sINFO, false) LogAction("remSenCheck: Threshold Temp: ${threshold} | Change Temp Increments: ${tempChangeVal}", sINFO, false) */ Boolean chg; chg=false Double chgval if(hvacMode in [sCOOL,sAUTO]){ //Changes Cool Setpoints if((String)settings.remSenRuleType in ["Cool", "Heat_Cool", "Heat_Cool_Circ"]){ Double onTemp=reqSenCoolSetPoint + threshold Double offTemp=reqSenCoolSetPoint Boolean turnOn, turnOff turnOn=false turnOff=false LogTrace("Remote Sensor: COOL - (Sensor Temp: ${curSenTemp} - CoolSetpoint: ${reqSenCoolSetPoint})") if(curSenTemp <= offTemp){ turnOff=true }else if(curSenTemp >= onTemp){ turnOn=true } if(turnOff && acRunning){ chgval=curTstatTemp + tempChangeVal chg=true LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to Turn Off Thermostat", sINFO, false) //acRunning=false state.remSenCoolOn=false }else if(turnOn && !acRunning){ chgval=curTstatTemp - tempChangeVal chg=true //acRunning=true state.remSenCoolOn=true LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to Turn On Thermostat", sINFO, false) }else{ // logic to decide if we need to nudge thermostat to keep it on or off if(acRunning){ chgval=curTstatTemp - tempChangeVal state.remSenCoolOn=true }else{ chgval=curTstatTemp + tempChangeVal state.remSenCoolOn=false } Double coolDiff1=Math.abs(curTstatTemp - curCoolSetpoint) //LogAction("Remote Sensor: COOL - coolDiff1: ${coolDiff1} tempChangeVal: ${tempChangeVal}", sINFO, false) if(coolDiff1 < (tempChangeVal / 2.0)){ chg=true LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to maintain state", sINFO, false) } } if(chg){ if(remSendoSetCool(chgval, onTemp, offTemp)){ storeExecutionHistory((now() - execTime), "remSenCheck") return // let all this take effect } } //else { LogAction("Remote Sensor: NO CHANGE TO COOL - CoolSetpoint is (${curCoolSetpoint}${tempScaleStr}) ", sINFO, false) } } } chg=false //chgval=0.0D //LogAction("remSenCheck: Thermostat Info - ( Temperature: (${curTstatTemp}) | HeatSetpoint: (${curHeatSetpoint}) | CoolSetpoint: (${curCoolSetpoint}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) | FanMode: (${curTstatFanMode}) )", sINFO, false) //Heat Functions. if(hvacMode in [sHEAT, "emergency heat", sAUTO]){ if((String)settings.remSenRuleType in ["Heat", "Heat_Cool", "Heat_Cool_Circ"]){ Double onTemp=reqSenHeatSetPoint - threshold Double offTemp=reqSenHeatSetPoint Boolean turnOn, turnOff turnOn=false turnOff=false //LogAction("Remote Sensor: HEAT - (Sensor Temp: ${curSenTemp} - HeatSetpoint: ${reqSenHeatSetPoint})", sINFO, false) if(curSenTemp <= onTemp){ turnOn=true }else if(curSenTemp >= offTemp){ turnOff=true } if(turnOff && heatRunning){ chgval=curTstatTemp - tempChangeVal chg=true LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to Turn Off Thermostat", sINFO, false) //heatRunning=false state.remSenHeatOn=false }else if(turnOn && !heatRunning){ chgval=curTstatTemp + tempChangeVal chg=true LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to Turn On Thermostat", sINFO, false) state.remSenHeatOn=true //heatRunning=true }else{ // logic to decide if we need to nudge thermostat to keep it on or off if(heatRunning){ chgval=curTstatTemp + tempChangeVal state.remSenHeatOn=true }else{ chgval=curTstatTemp - tempChangeVal state.remSenHeatOn=false } Double heatDiff1=Math.abs(curTstatTemp - curHeatSetpoint) //LogAction("Remote Sensor: HEAT - heatDiff1: ${heatDiff1} tempChangeVal: ${tempChangeVal}", sINFO, false) if(heatDiff1 < (tempChangeVal / 2)){ chg=true LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to maintain state", sINFO, false) } } if(chg){ if(remSendoSetHeat(chgval, onTemp, offTemp)){ storeExecutionHistory((now() - execTime), "remSenCheck") return // let all this take effect } } //else { LogAction("Remote Sensor: NO CHANGE TO HEAT - HeatSetpoint is already (${curHeatSetpoint}${tempScaleStr})", sINFO, false) } } } } /* // // if all thermostats (primary and mirrors) are Nest, then AC/HEAT & fan may be off (or set back) with away mode. (depends on user's home/away assist settings in Nest) // if thermostats were not all Nest, then non Nest units could still be on for AC/HEAT or FAN // current presumption in this implementation is: // they are all nests or integrated with Nest (Works with Nest) as we don't have away/home temps for each mirror thermostats. (They could be mirrored from primary) // all thermostats in an automation are in the same Nest structure, so that all share home/away settings // */ storeExecutionHistory((now() - execTime), "remSenCheck") } catch (ex){ log.error "remSenCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "remSenCheck", true, getAutoType()) } } @SuppressWarnings('unused') List getRemSenTempsToList(){ Integer mySched=getCurrentSchedule() List sensors; sensors=[] if(mySched){ String sLbl="schMot_${mySched}_" if((List)settings["${sLbl}remSensor"]){ sensors=(List)settings["${sLbl}remSensor"] } } if(!sensors){ sensors=(List)settings.remSensorDay } if(sensors?.size() >= 1){ String t0=tUnitStr() List info=[] sensors.sort().each { info.push("${it.displayName}": " ${it.currentTemperature.toString()}${t0}") } return info } return null } Double getTstatSetpoint(tstat, String type){ if(tstat){ if(type == sCOOL){ def coolSp=tstat?.currentCoolingSetpoint //log.debug "getTstatSetpoint(cool): $coolSp" return coolSp!=null ? coolSp.toDouble() : 0.0D }else{ def heatSp=tstat?.currentHeatingSetpoint //log.debug "getTstatSetpoint(heat): $heatSp" return heatSp!=null ? heatSp.toDouble() : 0.0D } } else { return 0.0D } } Double getRemoteSenThreshold(){ Double threshold threshold=settings.remSenTempDiffDegrees?.toDouble() Integer mySched=getCurrentSchedule() if(mySched){ String sLbl="schMot_${mySched}_" if(settings["${sLbl}remSenThreshold"]){ threshold=settings["${sLbl}remSenThreshold"].toDouble() } } Double theMin=getTemperatureScale() == "C" ? 0.3D : 0.6D threshold=!threshold ? 2.0D : Math.min(Math.max(threshold,theMin), 4.0D) return threshold } Double getRemoteSenTemp(){ Integer mySched=getCurrentSchedule() state.remoteTempSourceStr=sNULL state.currentSchedNum=null List sens if(mySched){ String sLbl="schMot_${mySched}_" if((List)settings["${sLbl}remSensor"]){ state.remoteTempSourceStr="Schedule" state.currentSchedNum=mySched sens=(List)settings["${sLbl}remSensor"] return getDeviceTempAvg(sens) } } if(isRemSenConfigured()){ state.remoteTempSourceStr="Remote Sensor" state.currentSchedNum=null return getDeviceTempAvg((List)settings.remSensorDay) }else{ state.remoteTempSourceStr="Thermostat" state.currentSchedNum=null return getDeviceTemp(settings.schMotTstat) /* else { LogAction("getRemoteSenTemp: No Temperature Found!", sWARN, true) return 0.0D */ } } Double fixTempSetting(temp){ Double newtemp; newtemp=temp?.toDouble() if(temp != null){ if(getTemperatureScale() == "C"){ if(newtemp > 35.0D){ // setting was done in F newtemp=roundTemp( ((newtemp - 32.0D) * (5.0D / 9.0D)) as Double) } }else if(getTemperatureScale() == "F"){ if(temp < 40){ // setting was done in C newtemp=roundTemp( (((newtemp * (9.0D / 5.0D)) as Double) + 32.0D) )//.toInteger() } } } return newtemp } def setRemoteSenTstat(val){ LogAction("setRemoteSenTstat $val", sINFO, false) state.remSenTstat=val } Double getRemSenCoolSetTemp(String curMode=sNULL, Boolean isEco=false, Boolean useCurrent=true){ Double coolTemp; coolTemp=null String theMode; theMode= curMode if(!theMode){ def tstat=settings.schMotTstat theMode=tstat ? tstat.currentThermostatMode.toString() : sNULL } state.remoteCoolSetSourceStr=sBLANK if(theMode != sECO){ if(getOverrideCoolSec() < (3600 * 4)){ if(state.remSenCoverride != null){ coolTemp=fixTempSetting(state.remSenCoverride.toDouble()) state.remoteCoolSetSourceStr="Remote Sensor Override" } }else{ state.remSenCoverride=null } if(coolTemp == null){ Integer mySched=getCurrentSchedule() if(mySched){ Boolean useMotion=(Boolean)state."motion${mySched}UseMotionSettings" Map hvacSettings=(Map)state."sched${mySched}restrictions" coolTemp=!useMotion ? (Double)hvacSettings?.ctemp : (Double)hvacSettings?.mctemp ?: (Double)hvacSettings?.ctemp state.remoteCoolSetSourceStr="Schedule" } // ERS if Remsensor is enabled if(isRemSenConfigured()){ if(theMode == sCOOL && coolTemp == null /* && isEco */){ if(state.extTmpSavedTemp){ coolTemp=state.extTmpSavedTemp.toDouble() state.remoteCoolSetSourceStr="Last Desired Temp" } } if(theMode == sAUTO && coolTemp == null /* && isEco */){ if(state.extTmpSavedCTemp){ coolTemp=state.extTmpSavedCTemp.toDouble() state.remoteCoolSetSourceStr="Last Desired CTemp" } } if(coolTemp == null && settings.remSenDayCoolTemp){ coolTemp=settings.remSenDayCoolTemp.toDouble() state.remoteCoolSetSourceStr="RemSen Day Cool Temp" } if(coolTemp == null){ Double desiredCoolTemp=getGlobalDesiredCoolTemp() if(desiredCoolTemp){ coolTemp=desiredCoolTemp.toDouble() state.remoteCoolSetSourceStr="Global Desired Cool Temp" } } if(coolTemp!=null){ coolTemp=fixTempSetting(coolTemp) } } } } if(coolTemp == null && useCurrent){ coolTemp=settings.schMotTstat ? getTstatSetpoint(settings.schMotTstat, sCOOL) : coolTemp state.remoteCoolSetSourceStr="Thermostat" } return coolTemp } Double getRemSenHeatSetTemp(String curMode=sNULL, Boolean isEco=false, Boolean useCurrent=true){ Double heatTemp; heatTemp=null String theMode theMode=curMode != sNULL ? curMode : sNULL if(theMode == sNULL){ def tstat=settings.schMotTstat theMode=tstat ? tstat.currentThermostatMode.toString() : sNULL } state.remoteHeatSetSourceStr=sBLANK if(theMode != sECO){ if(getOverrideHeatSec() < (3600 * 4)){ if(state.remSenHoverride != null){ heatTemp=fixTempSetting(state.remSenHoverride.toDouble()) state.remoteHeatSetSourceStr="Remote Sensor Override" } }else{ state.remSenHoverride=null } if(heatTemp == null){ Integer mySched=getCurrentSchedule() if(mySched){ Boolean useMotion=(Boolean)state."motion${mySched}UseMotionSettings" Map hvacSettings=(Map)state."sched${mySched}restrictions" heatTemp=!useMotion ? (Double)hvacSettings.htemp : (Double)hvacSettings.mhtemp ?: (Double)hvacSettings.htemp state.remoteHeatSetSourceStr="Schedule" } // ERS if Remsensor is enabled if(isRemSenConfigured()){ if(theMode == sHEAT && heatTemp == null /* && isEco */){ if(state.extTmpSavedTemp){ heatTemp=state.extTmpSavedTemp.toDouble() state.remoteHeatSetSourceStr="Last Desired Temp" } } if(theMode == sAUTO && heatTemp == null /* && isEco */){ if(state.extTmpSavedHTemp){ heatTemp=state.extTmpSavedHTemp.toDouble() state.remoteHeatSetSourceStr="Last Desired HTemp" } } if(heatTemp == null && settings.remSenDayHeatTemp){ heatTemp=settings.remSenDayHeatTemp.toDouble() state.remoteHeatSetSourceStr="RemSen Day Heat Temp" } if(heatTemp == null){ Double desiredHeatTemp=getGlobalDesiredHeatTemp() if(desiredHeatTemp){ heatTemp=desiredHeatTemp.toDouble() state.remoteHeatSetSourceStr="Global Desired Heat Temp" } } if(heatTemp){ heatTemp=fixTempSetting(heatTemp) } } } } if(heatTemp == null && useCurrent){ heatTemp=settings.schMotTstat ? getTstatSetpoint(settings.schMotTstat, sHEAT) : heatTemp state.remoteHeatSetSourceStr="Thermostat" } return heatTemp } // When a temp change is sent to virtual device, it lasts for 4 hours, next turn off, or next schedule change, then we return to automation settings // Other choices could be to change the schedule setpoint permanently if one is active, or allow folks to set timer Integer getOverrideCoolSec(){ return !(String)state.remSenCoverrideDt ? 100000 : GetTimeDiffSeconds((String)state.remSenCoverrideDt, sNULL, "getOverrideCoolSec").toInteger() } Integer getOverrideHeatSec(){ return !(String)state.remSenHoverrideDt ? 100000 : GetTimeDiffSeconds((String)state.remSenHoverrideDt, sNULL, "getOverrideHeatSec").toInteger() } void disableOverrideTemps(){ if(state.remSenHoverride || state.remSenCoverride){ stateRemove("remSenCoverride") stateRemove("remSenHoverride") stateRemove("remSenCoverrideDt") stateRemove("remSenHoverrideDt") LogAction("disableOverrideTemps: Disabling Override temps", sINFO, false) } } Boolean remSenTempUpdate(temp, String mode){ //LogAction("remSenTempUpdate(${temp}, ${mode})", sINFO, false) Boolean res; res=false if(getIsAutomationDisabled()){ return res } switch(mode){ case sHEAT: if(remSenHeatTempsReq()){ //LogAction("remSenTempUpdate Set Heat Override to: ${temp} for 4 hours", sINFO, false) state.remSenHoverride=temp.toDouble() state.remSenHoverrideDt=getDtNow() res=true } break case sCOOL: if(remSenCoolTempsReq()){ //LogAction("remSenTempUpdate Set Cool Override to: ${temp} for 4 hours", sINFO, false) state.remSenCoverride=temp.toDouble() state.remSenCoverrideDt=getDtNow() res=true } break default: LogAction("remSenTempUpdate Invalid Request: ${mode}, ${temp}", sWARN, true) break } if(res){ scheduleAutomationEval() LogAction("remSenTempUpdate Set ${mode} Override to: ${temp} for 4 hours", sINFO, false) } return res } Map remSenRuleEnum(String type=sNULL){ // Determines that available rules to display based on the selected thermostats capabilites. Boolean canCool=(Boolean)state.schMotTstatCanCool Boolean canHeat=(Boolean)state.schMotTstatCanHeat Boolean hasFan=(Boolean)state.schMotTstatHasFan //log.debug "remSenRuleEnum -- hasFan: $hasFan (${state.schMotTstatHasFan} | canCool: $canCool (${state.schMotTstatCanCool} | canHeat: $canHeat (${state.schMotTstatCanHeat}" Map vals; vals=[:] if(type){ if(type == "fan"){ vals=["Circ":"Eco/Circulate(Fan)"] if(canCool){ vals << ["Cool_Circ":"Cool/Circulate(Fan)"] } if(canHeat){ vals << ["Heat_Circ":"Heat/Circulate(Fan)"] } if(canHeat && canCool){ vals << [ "Heat_Cool_Circ":"Auto/Circulate(Fan)"] } } else if(type == "heatcool"){ if(!canCool && canHeat){ vals=["Heat":"Heat"] } else if(canCool && !canHeat){ vals=["Cool":"Cool"] } else { vals=["Heat_Cool":"Auto", "Heat":"Heat", "Cool":"Cool"] } } else { LogAction("remSenRuleEnum: Invalid Type ($type)", sERR, true) } } else { if(canCool && !canHeat && hasFan){ vals=["Cool":"Cool", "Circ":"Eco/Circulate(Fan)", "Cool_Circ":"Cool/Circulate(Fan)"] } else if(canCool && !canHeat && !hasFan){ vals=["Cool":"Cool"] } else if(!canCool && canHeat && hasFan){ vals=["Circ":"Eco/Circulate(Fan)", "Heat":"Heat", "Heat_Circ":"Heat/Circulate(Fan)"] } else if(!canCool && canHeat && !hasFan){ vals=["Heat":"Heat"] } else if(!canCool && !canHeat && hasFan){ vals=["Circ":"Eco/Circulate(Fan)"] } else if(canCool && canHeat && !hasFan){ vals=["Heat_Cool":"Auto", "Heat":"Heat", "Cool":"Cool"] } else { vals=[ "Heat_Cool":"Auto", "Heat":"Heat", "Cool":"Cool", "Circ":"Eco/Circulate(Fan)", "Heat_Cool_Circ":"Auto/Circulate(Fan)", "Heat_Circ":"Heat/Circulate(Fan)", "Cool_Circ":"Cool/Circulate(Fan)" ] } } //log.debug "remSenRuleEnum vals: $vals" return vals } /************************************************************************ | FAN CONTROL AUTOMATION CODE | *************************************************************************/ static String fanCtrlPrefix(){ return "fanCtrl" } Boolean isFanCtrlConfigured(){ return (Boolean)settings.schMotOperateFan && (isFanCtrlSwConfigured() || isFanCircConfigured()) } Boolean isFanCtrlSwConfigured(){ return (Boolean)settings.schMotOperateFan && settings.fanCtrlFanSwitches && settings.fanCtrlFanSwitchTriggerType && (List)settings.fanCtrlFanSwitchHvacModeFilter } Boolean isFanCircConfigured(){ return (Boolean)settings.schMotOperateFan && (settings.schMotCirculateTstatFan || settings.schMotCirculateExtFan) && settings.schMotFanRuleType } String getFanSwitchDesc(Boolean showOpt=true){ String swDesc; swDesc=sBLANK Integer swCnt; swCnt=0 String pName=fanCtrlPrefix() if(showOpt){ swDesc += (settings."${pName}FanSwitches" && (settings."${pName}FanSwitchSpeedCtrl" || settings."${pName}FanSwitchTriggerType" || (List)settings."${pName}FanSwitchHvacModeFilter")) ? "Fan Switch Config:" : sBLANK } swDesc += settings."${pName}FanSwitches" ? "${showOpt ? "\n" : sBLANK}• Fan Switches:" : sBLANK Integer rmSwCnt=settings."${pName}FanSwitches"?.size() ?: 0 settings."${pName}FanSwitches"?.sort { it?.displayName }?.each { sw -> swCnt=swCnt+1 swDesc += "${swCnt >= 1 ? "${swCnt == rmSwCnt ? "\n └" : "\n ├"}" : "\n └"} ${sw?.label}: (${strCapitalize(sw?.currentSwitch)})" swDesc += checkFanSpeedSupport(sw) ? "\n └ Current Spd: (${sw?.currentSpeed?.toString()})" : sBLANK } if(showOpt){ if(settings."${pName}FanSwitches"){ swDesc += (settings."${pName}FanSwitchSpeedCtrl" || settings."${pName}FanSwitchTriggerType" || (List)settings."${pName}FanSwitchHvacModeFilter") ? "\n\nFan Triggers:" : sBLANK swDesc += (settings."${pName}FanSwitchSpeedCtrl") ? "\n • Fan Speed Support: (Active)" : sBLANK swDesc += (settings."${pName}FanSwitchTriggerType") ? "\n • Fan Trigger:\n └(${getEnumValue(switchRunEnum(), settings."${pName}FanSwitchTriggerType")})" : sBLANK swDesc += ((List)settings."${pName}FanSwitchHvacModeFilter") ? "\n • Hvac Mode Filter:\n └(${getEnumValue(fanModeTrigEnum(), (List)settings."${pName}FanSwitchHvacModeFilter")})" : sBLANK } } Boolean t0=isFanCircConfigured() swDesc += (t0) ? "\n\nFan Circulation Enabled:" : sBLANK swDesc += (t0) ? "\n • Fan Circulation Rule:\n └(${getEnumValue(remSenRuleEnum("fan"), settings.schMotFanRuleType)})" : sBLANK swDesc += (t0 && settings.fanCtrlTempDiffDegrees) ? ("\n • Threshold: (${settings.fanCtrlTempDiffDegrees}${tUnitStr()})") : sBLANK swDesc += (t0 && settings.fanCtrlOnTime) ? ("\n • Circulate Time: (${getEnumValue(fanTimeSecEnum(), settings.fanCtrlOnTime)})") : sBLANK swDesc += (t0 && settings.fanCtrlTimeBetweenRuns) ? ("\n • Time Between Cycles:\n └ (${getEnumValue(longTimeSecEnum(), settings.fanCtrlTimeBetweenRuns)})") : sBLANK swDesc += (settings."${pName}FanSwitches" || t0) ? "\n\nRestrictions Active: (${autoScheduleOk(fanCtrlPrefix()) ? "No" : "Yes"})" : sBLANK return (swDesc == sBLANK) ? sNULL : swDesc } Boolean getFanSwitchesSpdChk(){ Integer devCnt; devCnt=0 String pName=fanCtrlPrefix() if(settings."${pName}FanSwitches"){ settings."${pName}FanSwitches"?.each { sw -> if(checkFanSpeedSupport(sw)){ devCnt=devCnt+1 } } } return devCnt>0 } Boolean fanCtrlScheduleOk(){ return autoScheduleOk(fanCtrlPrefix()) } void fanCtrlCheck(){ //LogAction("FanControl Event | Fan Switch Check", sINFO, false) try { def fanCtrlTstat=settings.schMotTstat if(getIsAutomationDisabled()){ return } if( !isFanCtrlConfigured()){ return } Long execTime=now() //state.autoRunDt=getDtNow() String curMode=settings.schMotTstat ? settings.schMotTstat.currentThermostatMode.toString() : sNULL Boolean modeEco= (curMode in [sECO]) Double reqHeatSetPoint; reqHeatSetPoint=null Double reqCoolSetPoint; reqCoolSetPoint=null if(!modeEco){ reqHeatSetPoint=getRemSenHeatSetTemp(curMode) reqCoolSetPoint=getRemSenCoolSetTemp(curMode) } String lastMode lastMode=settings.schMotTstat ? settings.schMotTstat?.currentpreviousthermostatMode?.toString() : sNULL if(!lastMode && modeEco && isRemSenConfigured()){ if( /* !lastMode && */ state.extTmpTstatOffRequested && state.extTmplastMode){ lastMode=state.extTmplastMode } } if(lastMode){ if(!reqHeatSetPoint){ reqHeatSetPoint=getRemSenHeatSetTemp(lastMode, modeEco, false) } if(!reqCoolSetPoint){ reqCoolSetPoint=getRemSenCoolSetTemp(lastMode, modeEco, false) } if(isRemSenConfigured()){ if(reqHeatSetPoint == null){ reqHeatSetPoint=state.extTmpSavedHTemp } if(reqCoolSetPoint == null){ reqCoolSetPoint=state.extTmpSavedCTemp } } LogAction("fanCtrlCheck: Using lastMode: ${lastMode} | extTmpTstatOffRequested: ${state.extTmpTstatOffRequested} | curMode: ${curMode}", sINFO, false) } reqHeatSetPoint=reqHeatSetPoint ?: 0.0D reqCoolSetPoint=reqCoolSetPoint ?: 0.0D Double curTstatTemp=getRemoteSenTemp() Map sTemp=getReqSetpointTemp(curTstatTemp, reqHeatSetPoint, reqCoolSetPoint) Double t0=(Double)sTemp.req Double curSetPoint=t0 ? t0 : 0.0D Double tempDiff=Math.abs(curSetPoint - curTstatTemp) LogAction("fanCtrlCheck: Desired Temps - Heat: ${reqHeatSetPoint} | Cool: ${reqCoolSetPoint}", sINFO, false) LogAction("fanCtrlCheck: Current Thermostat Sensor Temp: ${curTstatTemp} Temp Difference: (${tempDiff})", sINFO, false) Boolean circWantsOn; circWantsOn=null if(isFanCircConfigured()){ Double adjust=(getTemperatureScale() == "C") ? 0.5D : 1.0D Double threshold=!settings.fanCtrlTempDiffDegrees ? adjust : settings.fanCtrlTempDiffDegrees.toDouble() String hvacMode=curMode /* String curTstatFanMode=settings.schMotTstat?.currentThermostatFanMode.toString() Boolean fanOn= curTstatFanMode == sON || curTstatFanMode == "circulate" if((Boolean)state.haveRunFan){ if(schMotFanRuleType in ["Circ", "Cool_Circ", "Heat_Circ", "Heat_Cool_Circ"]){ if(fanOn){ LogAction("fantCtrlCheck: Turning OFF '${settings.schMotTstat?.displayName}' Fan; Modes do not match evaluation", sINFO, false) storeLastAction("Turned ${settings.schMotTstat} Fan to (Auto)", getDtNow(), "fanCtrl", settings.schMotTstat) settings.schMotTstat?.fanAuto() if(settings.schMotTstatMir){ settings.schMotTstatMir*.fanAuto() } } } state.haveRunFan=false } */ // Map sTemp=getReqSetpointTemp(curTstatTemp, reqHeatSetPoint, reqCoolSetPoint) String resultMode=(String)sTemp.type Boolean can_Circ; can_Circ=false if( !(hvacMode in [sOFF]) && ( ( hvacMode in [sCOOL] && schMotFanRuleType in ["Cool_Circ"]) || ( resultMode in [sCOOL] && schMotFanRuleType in ["Cool_Circ", "Heat_Cool_Circ"]) || ( hvacMode in [sHEAT] && schMotFanRuleType in ["Heat_Circ"]) || ( resultMode in [sHEAT] && schMotFanRuleType in ["Heat_Circ", "Heat_Cool_Circ"]) || ( hvacMode in [sAUTO] && schMotFanRuleType in ["Heat_Cool_Circ"]) || ( hvacMode in [sECO] && schMotFanRuleType in ["Circ"]) ) ){ can_Circ=true } circWantsOn=circulateFanControl(resultMode, curTstatTemp, (Double)sTemp.req, threshold, can_Circ) } if(isFanCtrlSwConfigured()){ doFanOperation(tempDiff, curTstatTemp, reqHeatSetPoint, reqCoolSetPoint, circWantsOn) } storeExecutionHistory((now()-execTime), "fanCtrlCheck") } catch (ex){ log.error "fanCtrlCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "fanCtrlCheck", true, getAutoType()) } } Map getReqSetpointTemp(curTemp, reqHeatSetPoint, reqCoolSetPoint){ //LogAction("getReqSetpointTemp: Current Temp: ${curTemp} Req Heat: ${reqHeatSetPoint} Req Cool: ${reqCoolSetPoint}", sINFO, false) def tstat=settings.schMotTstat //def modeEco=(curMode == sECO) //def modeAuto=(curMode == sAUTO) Boolean canHeat=state.schMotTstatCanHeat Boolean canCool=state.schMotTstatCanCool String hvacMode=tstat ? tstat.currentThermostatMode.toString() : sNULL String operState=tstat ? tstat.currentThermostatOperatingState.toString() : sNULL String opType; opType=hvacMode if(hvacMode == sOFF){ return ["req":null, "type":sOFF] } if((hvacMode == sCOOL) || (operState == "cooling") || (hvacMode == sECO && !canHeat && canCool) ){ opType=sCOOL }else if((hvacMode == sHEAT) || (operState == "heating")|| (hvacMode == sECO && !canCool && canHeat) ){ opType=sHEAT }else if(hvacMode == sAUTO || hvacMode == sECO){ Double coolDiff=Math.abs(curTemp - reqCoolSetPoint) Double heatDiff=Math.abs(curTemp - reqHeatSetPoint) opType=coolDiff < heatDiff ? sCOOL : sHEAT } Double temp=(opType == sCOOL) ? reqCoolSetPoint?.toDouble() : reqHeatSetPoint?.toDouble() return ["req":temp, "type":opType] } def doFanOperation(Double tempDiff, Double curTstatTemp, Double curHeatSetpoint, Double curCoolSetpoint, Boolean icircWantsOn){ Boolean circWantsOn circWantsOn=icircWantsOn String pName=fanCtrlPrefix() try { def tstat=settings.schMotTstat if(!tstat) return //LogAction("doFanOperation: Temp Difference: (${tempDiff})", sINFO, false) /* Double curTstatTemp=tstat ? getRemoteSenTemp() : null Double curCoolSetpoint=getRemSenCoolSetTemp() Double curHeatSetpoint=getRemSenHeatSetTemp() */ String hvacMode= tstat.currentThermostatMode.toString() String curTstatOperState=tstat.currentThermostatOperatingState.toString() String curTstatFanMode=tstat.currentThermostatFanMode.toString() //LogAction("doFanOperation: Thermostat Info - ( Temperature: (${curTstatTemp}) | HeatSetpoint: (${curHeatSetpoint}) | CoolSetpoint: (${curCoolSetpoint}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) | FanMode: (${curTstatFanMode}) )", sINFO, false) if(state.haveRunFan == null){ state.haveRunFan=false } Boolean savedHaveRun=(Boolean)state.haveRunFan //def wantFanOn=circWantsOn != null ? circWantsOn ? false Boolean wantFanOn; wantFanOn=false // 1:"Heating/Cooling", 2:"With Fan Only", 3:"Heating", 4:"Cooling" List validOperModes switch ( settings."${pName}FanSwitchTriggerType".toInteger() ){ case 1: validOperModes=["heating", "cooling"] wantFanOn=(curTstatOperState in validOperModes) break case 2: wantFanOn=(curTstatFanMode in [sON, "circulate"]) break case 3: validOperModes=["heating"] wantFanOn=(curTstatOperState in validOperModes) break case 4: validOperModes=["cooling"] wantFanOn=(curTstatOperState in validOperModes) break default: break } if( !( ("any" in (List)settings."${pName}FanSwitchHvacModeFilter") || (hvacMode in (List)settings."${pName}FanSwitchHvacModeFilter") ) ){ if(savedHaveRun){ LogAction("doFanOperation: Evaluating turn fans off; Thermostat Mode does not Match the required Mode", sINFO, false) } wantFanOn=false // force off of fans } Boolean schedOk=fanCtrlScheduleOk() if(!schedOk){ if(savedHaveRun){ LogAction("doFanOperation: Evaluating turn fans off; Schedule is restricted", sINFO, false) } wantFanOn=false // force off of fans circWantsOn=false // force off of fans } Boolean allOff; allOff=true settings."${pName}FanSwitches"?.each { sw -> Boolean swOn swOn=(sw?.currentSwitch?.toString() == sON) if(wantFanOn || circWantsOn){ if(!swOn && !savedHaveRun){ LogAction("doFanOperation: Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (ON)", sINFO, false) sw.on() swOn=true state.haveRunFan=true storeLastAction("Turned On $sw)", getDtNow(), pName) }else{ if(!swOn && savedHaveRun){ LogAction("doFanOperation: savedHaveRun state shows switch ${sw} turned OFF outside of automation requests", sINFO, false) } } if(swOn && (Boolean)state.haveRunFan && checkFanSpeedSupport(sw)){ def t0=sw?.currentSpeed String speed=t0 ? t0.toString() : sNULL if(settings."${pName}FanSwitchSpeedCtrl" && settings."${pName}FanSwitchHighSpeed" && settings."${pName}FanSwitchMedSpeed" && settings."${pName}FanSwitchLowSpeed"){ if(tempDiff < settings."${pName}FanSwitchMedSpeed".toDouble()){ if(speed != "low"){ sw.setSpeed("low") LogAction("doFanOperation: Temp Difference (${tempDiff}${tUnitStr()}) is BELOW the Medium Speed Threshold of (${settings."${pName}FanSwitchMedSpeed"}) | Turning '${sw}' Fan Switch on (LOW SPEED)", sINFO, false) storeLastAction("Set Fan $sw to Low Speed", getDtNow(), pName) } } else if(tempDiff >= settings."${pName}FanSwitchMedSpeed".toDouble() && tempDiff < settings."${pName}FanSwitchHighSpeed".toDouble()){ if(speed != "medium"){ sw.setSpeed("medium") LogAction("doFanOperation: Temp Difference (${tempDiff}${tUnitStr()}) is ABOVE the Medium Speed Threshold of (${settings."${pName}FanSwitchMedSpeed"}) | Turning '${sw}' Fan Switch on (MEDIUM SPEED)", sINFO, false) storeLastAction("Set Fan $sw to Medium Speed", getDtNow(), pName) } } else if(tempDiff >= settings."${pName}FanSwitchHighSpeed".toDouble()){ if(speed != "high"){ sw.setSpeed("high") LogAction("doFanOperation: Temp Difference (${tempDiff}${tUnitStr()}) is ABOVE the High Speed Threshold of (${settings."${pName}FanSwitchHighSpeed"}) | Turning '${sw}' Fan Switch on (HIGH SPEED)", sINFO, false) storeLastAction("Set Fan $sw to High Speed", getDtNow(), pName) } } }else{ if(speed != "high"){ sw.setSpeed("high") LogAction("doFanOperation: Fan supports multiple speeds, with speed control disabled | Turning '${sw}' Fan Switch on (HIGH SPEED)", sINFO, false) storeLastAction("Set Fan $sw to High Speed", getDtNow(), pName) } } } }else{ if(swOn && savedHaveRun && !wantfanOn){ LogAction("doFanOperation: Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (OFF)", sINFO, false) storeLastAction("Turned Off (${sw})", getDtNow(), pName) swOn=false sw.off() state.haveRunFan=false }else{ if(swOn && !savedHaveRun){ LogAction("doFanOperation: Saved have run state shows switch ${sw} turned ON outside of automation requests", sINFO, false) } } } if(swOn){ allOff=false } } if(allOff){ state.haveRunFan=false } } catch (ex){ log.error "doFanOperation Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "doFanOperation", true, getAutoType()) } } Integer getFanCtrlFanRunDtSec(){ return !(String)state.fanCtrlRunDt ? 100000 : GetTimeDiffSeconds((String)state.fanCtrlRunDt, sNULL, "getFanCtrlFanRunDtSec").toInteger() } Integer getFanCtrlFanOffDtSec(){ return !(String)state.fanCtrlFanOffDt ? 100000 : GetTimeDiffSeconds((String)state.fanCtrlFanOffDt, sNULL, "getFanCtrlFanOffDtSec").toInteger() } // CONTROLS THE THERMOSTAT FAN def circulateFanControl(operType, Double curSenTemp, Double reqSetpointTemp, Double threshold, can_Circ){ String pName=fanCtrlPrefix() Boolean theFanIsOn; theFanIsOn=null def tstat=settings.schMotTstat def tstatsMir=settings.schMotTstatMir // input (name: "schMotCirculateTstatFan", type: sBOOL, title: imgTitle(getAppImg("fan_circulation_icon.png"), inputTitleStr("Run HVAC Fan for Circulation?")), description: desc, required: reqinp, defaultValue: false, submitOnChange: true) // input (name: "schMotCirculateExtFan", type: sBOOL, title: imgTitle(getAppImg("fan_circulation_icon.png"), inputTitleStr("Run External Fan for Circulation?")), description: desc, required: reqinp, defaultValue: false, submitOnChange: true) //ERS TODO Operate external fan String hvacMode=tstat ? tstat?.currentThermostatMode?.toString() : sNULL String curTstatFanMode=tstat?.currentThermostatFanMode?.toString() Boolean fanOn=(curTstatFanMode == sON || curTstatFanMode == "circulate") Boolean returnToAuto returnToAuto=!can_Circ if(hvacMode in [sOFF]){ returnToAuto=true } Long nn = now() Long fanRunStart, fanOff fanRunStart=(String)state.fanCtrlRunDt ? Date.parse("E MMM dd HH:mm:ss z yyyy", (String)state.fanCtrlRunDt).getTime() : nn fanOff=(String)state.fanCtrlFanOffDt ? Date.parse("E MMM dd HH:mm:ss z yyyy", (String)state.fanCtrlFanOffDt).getTime() : nn // Track approximate fan on / off times if( !fanOn && fanRunStart > fanOff ){ state.fanCtrlFanOffDt=getDtNow() returnToAuto=true } if( fanOn && fanRunStart < fanOff ){ state.fanCtrlFanRunDt=getDtNow() } Boolean schedOk=fanCtrlScheduleOk() if(!schedOk){ returnToAuto=true } String curOperState=tstat?.currentnestThermostatOperatingState?.toString() Boolean tstatOperStateOk=(curOperState == "idle") // if ac or heat is on, we should put fan back to auto if(!tstatOperStateOk){ fanRunStart=Date.parse("E MMM dd HH:mm:ss z yyyy", (String)state.fanCtrlRunDt).getTime() fanOff=Date.parse("E MMM dd HH:mm:ss z yyyy", (String)state.fanCtrlFanOffDt).getTime() if(fanOff > fanRunStart){ return false } LogAction("Circulate Fan Run: The Thermostat OperatingState is Currently (${strCapitalize(curOperState)}) Skipping", sINFO, false) state.fanCtrlFanOffDt=getDtNow() returnToAuto=true } Boolean fanTempOk=getCirculateFanTempOk(curSenTemp, reqSetpointTemp, threshold, fanOn, operType) if(hvacMode in [sHEAT, sAUTO, sCOOL, sECO] && fanTempOk && !returnToAuto){ if(!fanOn){ Integer waitTimeVal=settings.fanCtrlTimeBetweenRuns?.toInteger() ?: 1200 Boolean timeSinceLastOffOk=(getFanCtrlFanOffDtSec() > waitTimeVal) if(!timeSinceLastOffOk){ Integer remaining=waitTimeVal - getFanCtrlFanOffDtSec() LogAction("Circulate Fan: Want to RUN Fan | Delaying for wait period ${waitTimeVal}, remaining ${remaining} seconds", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) return false // leave off } LogAction("Circulate Fan: Activating '${tstat?.displayName}'' Fan for ${strCapitalize(operType)}ING Circulation", sINFO, false) tstat?.fanOn() storeLastAction("Turned ${tstat} Fan 'On'", getDtNow(), pName) state.fanCtrlRunDt=getDtNow() if(tstatsMir){ tstatsMir?.each { mt -> LogAction("Circulate Fan: Mirroring Primary Thermostat: Activating '${mt?.displayName}' Fan", sINFO, false) mt?.fanOn() storeLastAction("Turned ${mt.displayName} Fan 'On'", getDtNow(), pName) } } } theFanIsOn=true }else{ if(returnToAuto || !fanTempOk){ if(fanOn && !returnToAuto){ Integer fanOnTimeVal=settings.fanCtrlOnTime?.toInteger() ?: 240 Boolean timeSinceLastRunOk=(getFanCtrlFanRunDtSec() > fanOnTimeVal) // fan left on for minimum if(!timeSinceLastRunOk){ Integer remaining=fanOnTimeVal - getFanCtrlFanRunDtSec() LogAction("Circulate Fan Run: Want to STOP Fan | Delaying for run period ${fanOnTimeVal}, remaining ${remaining} seconds", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) return true // leave on } } if(fanOn){ LogAction("Circulate Fan: Turning OFF '${tstat?.displayName}' Fan that was used for ${strCapitalize(operType)}ING Circulation", sINFO, false) tstat?.fanAuto() storeLastAction("Turned ${tstat} Fan to 'Auto'", getDtNow(), pName) state.fanCtrlFanOffDt=getDtNow() if(tstatsMir){ tstatsMir?.each { mt -> LogAction("Circulate Fan: Mirroring Primary Thermostat: Turning OFF '${mt?.displayName}' Fan", sINFO, false) mt?.fanAuto() storeLastAction("Turned ${mt.displayName} Fan 'Off'", getDtNow(), pName) } } } } theFanIsOn=false } if(theFanIsOn){ scheduleAutomationEval(120) } return theFanIsOn } Boolean getCirculateFanTempOk(Double senTemp, Double reqsetTemp, Double threshold, Boolean fanOn, String operType){ Boolean turnOn; turnOn=false // String tempScaleStr=tUnitStr() /* Double adjust=(getTemperatureScale() == "C") ? 0.5 : 1.0 if(threshold > (adjust * 2.0)){ adjust=adjust * 2.0 } if(adjust >= threshold){ LogAction("getCirculateFanTempOk: Bad threshold setting ${threshold} <= ${adjust}", sWARN, true) return false } LogAction(" ├ adjust: ${adjust}}${tUnitStr()}", sINFO, false) */ //LogAction(" ├ operType: (${strCapitalize(operType)}) | Temp Threshold: ${threshold}${tempScaleStr} | FanAlreadyOn: (${strCapitalize(fanOn)})", sINFO, false) //LogAction(" ├ Sensor Temp: ${senTemp}${tempScaleStr} | Requested Setpoint Temp: ${reqsetTemp}${tempScaleStr}", sINFO, false) if(!reqsetTemp){ //LogAction("getCirculateFanTempOk: Bad reqsetTemp ${reqsetTemp}", sWARN, true) //LogAction("getCirculateFanTempOk:", sINFO, false) return false } // Double ontemp Double offtemp if(operType == sCOOL){ // ontemp=reqsetTemp + threshold offtemp=reqsetTemp if(senTemp >= (offtemp + threshold)){ turnOn=true } // if((senTemp > offtemp) && (senTemp <= (ontemp - adjust))){ turnOn=true } } if(operType == sHEAT){ // ontemp=reqsetTemp - threshold offtemp=reqsetTemp if(senTemp <= (offtemp - threshold)){ turnOn=true } // if((senTemp < offtemp) && (senTemp >= (ontemp + adjust))){ turnOn=true } } // LogAction(" ├ onTemp: ${ontemp} | offTemp: ${offtemp}}${tempScaleStr}", sINFO, false) //LogAction(" ├ offTemp: ${offtemp}${tempScaleStr} | Temp Threshold: ${threshold}${tempScaleStr}", sINFO, false) //LogAction(" ┌ Final Result: (${strCapitalize(turnOn)})", sINFO, false) // LogAction("getCirculateFanTempOk: ", sINFO, false) String resultStr resultStr="getCirculateFanTempOk: The Temperature Difference is " if(turnOn){ resultStr += " within " }else{ resultStr += " Outside " } Boolean disp; disp=false resultStr += "of Threshold Limits | " if(!turnOn && fanOn){ resultStr += "Turning Thermostat Fan OFF" disp=true }else if(turnOn && !fanOn){ resultStr += "Turning Thermostat Fan ON" disp=true }else if(turnOn && fanOn){ resultStr += "Fan is ON" }else if(!turnOn && !fanOn){ resultStr += "Fan is OFF" } LogAction(resultStr, sINFO, disp) return turnOn } /******************************************************************************** | HUMIDITY CONTROL AUTOMATION CODE | *********************************************************************************/ static String humCtrlPrefix(){ return "humCtrl" } Boolean isHumCtrlConfigured(){ return (Boolean)settings.schMotHumidityControl && ((Boolean)settings.humCtrlUseWeather || settings.humCtrlTempSensor) && (List)settings.humCtrlHumidity && (List)settings.humCtrlSwitches } String humCtrlSwitchDesc(Boolean showOpt=true){ if((List)settings.humCtrlSwitches){ Integer cCnt=((List)settings.humCtrlSwitches).size() ?: 0 String str; str=sBLANK Integer cnt; cnt=0 str += "Switch Status:" ((List)settings.humCtrlSwitches).sort { it?.displayName }?.each { dev -> cnt=cnt+1 String val=strCapitalize(dev?.currentSwitch) ?: "Not Set" str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: (${val})" } if(showOpt){ str += (settings.humCtrlSwitchTriggerType || (List)settings.humCtrlSwitchHvacModeFilter) ? "\n\nSwitch Triggers:" : sBLANK str += (settings.humCtrlSwitchTriggerType) ? "\n • Switch Trigger: (${getEnumValue(switchRunEnum(true), settings.humCtrlSwitchTriggerType)})" : sBLANK str += ((List)settings.humCtrlSwitchHvacModeFilter) ? "\n • Hvac Mode Filter: (${getEnumValue(fanModeTrigEnum(), (List)settings.humCtrlSwitchHvacModeFilter).toString().replaceAll("\\[|\\]", sBLANK)})" : sBLANK } return str } return sNULL } String humCtrlHumidityDesc(){ if((List)settings.humCtrlHumidity){ Integer cCnt=((List)settings.humCtrlHumidity).size() ?: 0 String str; str=sBLANK Integer cnt; cnt=0 str += "Sensor Humidity (average): (${getDeviceVarAvg((List)settings.humCtrlHumidity, "currentHumidity")}%)" ((List)settings.humCtrlHumidity).sort { it?.displayName }?.each { dev -> cnt=cnt+1 String t0=strCapitalize(dev?.currentHumidity) String val=t0 ?: "Not Set" str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: ${(dev?.label?.toString()?.length() > 10) ? "\n${(cCnt == 1 || cnt == cCnt) ? " " : "│"}└ " : sBLANK}(${val}%)" } return str } return sNULL } Double getHumCtrlTemperature(){ Double extTemp; extTemp=0.0D if(!(Boolean)settings.humCtrlUseWeather && settings.humCtrlTempSensor){ extTemp=getDeviceTemp(settings.humCtrlTempSensor) }else{ if((Boolean)settings.humCtrlUseWeather && (state.curWeaTemp_f || state.curWeaTemp_c)){ if(getTemperatureScale() == "C"){ extTemp=state.curWeaTemp_c.toDouble() } else { extTemp=state.curWeaTemp_f.toDouble() } } } return extTemp } Integer getMaxHumidity(Double curExtT){ Double maxhum; maxhum=15.0D Double curExtTemp = curExtT if(curExtTemp != null){ if(curExtTemp >= adj_temp(40.0D)){ maxhum=45.0D }else if(curExtTemp >= adj_temp(32.0D)){ maxhum=45.0D - ( (adj_temp(40.0D) - curExtTemp)/(adj_temp(40.0D)-adj_temp(32.0D)) ) * 5.0D //maxhum=40 }else if(curExtTemp >= adj_temp(20.0D)){ maxhum=40.0D - ( (adj_temp(32.0D) - curExtTemp)/(adj_temp(32.0D)-adj_temp(20.0D)) ) * 5.0D //maxhum=35 }else if(curExtTemp >= adj_temp(10)){ maxhum=35.0D - ( (adj_temp(20.0D) - curExtTemp)/(adj_temp(20)-adj_temp(10.0D)) ) * 5.0D //maxhum=30 }else if(curExtTemp >= adj_temp(0.0D)){ maxhum=30.0D - ( (adj_temp(10.0D) - curExtTemp)/(adj_temp(10)-adj_temp(0.0D)) ) * 5.0D //maxhum=25 }else if(curExtTemp >= adj_temp(-10.0D)){ maxhum=25.0D- Math.abs( (adj_temp(0.0D) - curExtTemp) / (adj_temp(0.0D)-adj_temp(-10.0D)) ) * 5.0D //maxhum=20 }else if(curExtTemp >= adj_temp(-20.0D)){ maxhum=15.0D } } return maxhum.toInteger() } Boolean humCtrlScheduleOk(){ return autoScheduleOk(humCtrlPrefix()) } void humCtrlCheck(){ //LogAction("humCtrlCheck", sINFO, false) String pName=humCtrlPrefix() String meth="humCtrlCheck: | " if(getIsAutomationDisabled()){ return } try { Long execTime=now() def tstat=settings.schMotTstat String hvacMode=tstat ? tstat.currentThermostatMode.toString() : sNULL String curTstatOperState=tstat.currentThermostatOperatingState.toString() String curTstatFanMode=tstat.currentThermostatFanMode.toString() //def curHum=humCtrlHumidity?.currentHumidity Double curHum=getDeviceVarAvg((List)settings.humCtrlHumidity, "currentHumidity") Double curExtTemp=getHumCtrlTemperature() Integer maxHum=getMaxHumidity(curExtTemp) Boolean schedOk=humCtrlScheduleOk() LogAction(meth+"( Humidity: (${curHum}) | External Temp: (${curExtTemp}) | Max Humidity: (${maxHum}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) )", sINFO, false) if(state.haveRunHumidifier == null){ state.haveRunHumidifier=false } Boolean savedHaveRun=(Boolean)state.haveRunHumidifier Boolean humOn; humOn=false if(curHum < maxHum){ humOn=true } // 1:"Heating/Cooling", 2:"With Fan Only", 3:"Heating", 4:"Cooling" 5:"All Operating Modes" List validOperModes Boolean validOperating; validOperating=true switch ( settings.humCtrlSwitchTriggerType?.toInteger() ){ case 1: validOperModes=["heating", "cooling"] validOperating=(curTstatOperState in validOperModes) break case 2: validOperating=(curTstatFanMode in [sON, "circulate"]) break case 3: validOperModes=["heating"] validOperating=(curTstatOperState in validOperModes) break case 4: validOperModes=["cooling"] validOperating=(curTstatOperState in validOperModes) break case 5: break default: break } Boolean validHvac; validHvac=true if( !( ("any" in (List)settings.humCtrlSwitchHvacModeFilter) || (hvacMode in (List)settings.humCtrlSwitchHvacModeFilter) ) ){ //LogAction("humCtrlCheck: Evaluating turn humidifier off; Thermostat Mode does not Match the required Mode", sINFO, false) validHvac=false // force off } Boolean turnOn=(humOn && validOperating && validHvac && schedOk) ?: false //LogAction("humCtrlCheck: turnOn: ${turnOn} | humOn: ${humOn} | validOperating: ${validOperating} | validHvac: ${validHvac} | schedOk: ${schedOk} | savedHaveRun: ${savedHaveRun}", sINFO, false) ((List)settings.humCtrlSwitches)?.each { sw -> Boolean swOn=(sw?.currentSwitch?.toString() == sON) if(turnOn){ //if(!swOn && !savedHaveRun){ if(!swOn){ LogAction(meth+"Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (ON)", sINFO, false) sw.on() //swOn=true state.haveRunHumidifier=true storeLastAction("Turned On $sw)", getDtNow(), pName) }else{ if(!swOn && savedHaveRun){ LogAction(meth+"savedHaveRun state shows switch ${sw} turned OFF outside of automation requests", sINFO, false) } } }else{ //if(swOn && savedHaveRun){ if(swOn){ LogAction(meth+"Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (OFF)", sINFO, false) storeLastAction("Turned Off (${sw})", getDtNow(), pName) sw.off() state.haveRunHumidifier=false }else{ if(swOn && !savedHaveRun){ LogAction(meth+"Saved have run state shows switch ${sw} turned ON outside of automation requests", sINFO, false) } state.haveRunHumidifier=false } } } storeExecutionHistory((now()-execTime), "humCtrlCheck") } catch (ex){ log.error "humCtrlCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "humCtrlCheck", true, getAutoType()) } } /******************************************************************************** | EXTERNAL TEMP AUTOMATION CODE | *********************************************************************************/ static String extTmpPrefix(){ return "extTmp" } Boolean isExtTmpConfigured(){ return (Boolean)settings.schMotExternalTempOff && ((Boolean)settings.extTmpUseWeather || settings.extTmpTempSensor) && settings.extTmpDiffVal } Integer getWeathUpdSec(){ return !(String)state.weatherUpdDt ? 100000 : GetTimeDiffSeconds((String)state.weatherUpdDt, sNULL, "getWeathUpdSec").toInteger() } void getExtConditions( doEvent=false ){ LogTrace("getExtConditions") Long execTime=now() def t0 if(state.wDevInst == null){ state.wDevInst=false t0=parent.getSettingVal("weatherDevice") state.wDevInst=t0 ? true : false } if((Boolean)state.wDevInst){ def weather=parent.getSettingVal("weatherDevice") if(weather){ Double temp0 Double hum0 if((Boolean)state.needWeathUpd || getWeathUpdSec() > 3600){ stateRemove("needWeathUpd") state.weatherUpdDt=getDtNow() try { weather.refresh() } catch (ex){ log.error "getExtConditions Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "getExtConditions", true, getAutoType()) } } temp0=getDeviceTempAvg(weather) hum0=getDeviceVarAvg(weather, "currentHumidity") if(temp0 || hum0){ state.curWeather=true } else { state.curWeather=null; return } //Logger("temp0: ${temp0} hum0: ${hum0} loc: ${state.curWeatherLoc}") state.curWeatherLoc="${weather?.currentCity} ${weather?.currentCountry}" state.curWeatherHum=hum0 Double c_temp Long f_temp if(getTemperatureScale() == "C"){ c_temp=temp0 f_temp=Math.round((c_temp * (9.0D / 5.0D)) + 32.0D) }else{ f_temp=temp0.toLong() c_temp=((f_temp - 32.0D) * (5.0D / 9.0D)) } state.curWeaTemp_f=Math.round(f_temp) as Integer state.curWeaTemp_c=Math.round(c_temp.round(1) * 2.0D) / 2.0D c_temp=estimateDewPoint(hum0, c_temp) if(state.curWeaTemp_c < c_temp){ c_temp=state.curWeaTemp_c } f_temp=Math.round(c_temp * 9.0D/5.0D + 32.0D) state.curWeatherDewpointTemp_c=Math.round(c_temp.round(1) * 2.0D) / 2.0D state.curWeatherDewpointTemp_f=Math.round(f_temp) as Integer } } storeExecutionHistory((now()-execTime), "getExtConditions") } private static Double estimateDewPoint(Double rh,Double t){ Double L=Math.log(rh/100) Double M=17.27D * t Double N=237.3D + t Double B=(L + (M/N)) / 17.27D Double dp=(237.3 * B) / (1 - B) Double dp1=243.04D * ( Math.log(rh / 100) + ( (17.625D * t) / (243.04 + t) ) ) / (17.625D - Math.log(rh / 100) - ( (17.625D * t) / (243.04D + t) ) ) Double ave=(dp + dp1)/2.0D //LogAction("dp: ${dp.round(1)} dp1: ${dp1.round(1)} ave: ${ave.round(1)}") ave=dp1 return ave.round(1) } Double getExtTmpTemperature(){ Double extTemp; extTemp=0.0D if(!(Boolean)settings.extTmpUseWeather && settings.extTmpTempSensor){ extTemp=getDeviceTemp(settings.extTmpTempSensor) }else{ if((Boolean)settings.extTmpUseWeather && (state.curWeaTemp_f || state.curWeaTemp_c)){ if(getTemperatureScale() == "C"){ extTemp=state.curWeaTemp_c.toDouble() } else { extTemp=state.curWeaTemp_f.toDouble() } } } return extTemp } Double getExtTmpDewPoint(){ Double extDp; extDp=0.0D if((Boolean)settings.extTmpUseWeather && (state.curWeatherDewpointTemp_f || state.curWeatherDewpointTemp_c)){ if((String)getTemperatureScale() == "C"){ extDp=roundTemp(state.curWeatherDewpointTemp_c.toDouble()) } else { extDp=roundTemp(state.curWeatherDewpointTemp_f.toDouble()) } } //TODO if an external sensor, if it has temp and humidity, we can calculate DP return extDp } Double getDesiredTemp(){ def extTmpTstat=settings.schMotTstat String curMode=extTmpTstat ? extTmpTstat.currentThermostatMode?.toString() : sNULL Boolean modeOff, modeEco, modeCool, modeHeat, modeAuto modeOff=(curMode in [sOFF]) modeEco=(curMode in [sECO]) modeCool=(curMode == sCOOL) modeHeat=(curMode == sHEAT) modeAuto=(curMode == sAUTO) Double desiredHeatTemp; desiredHeatTemp=getRemSenHeatSetTemp(curMode) Double desiredCoolTemp; desiredCoolTemp=getRemSenCoolSetTemp(curMode) String lastMode; lastMode=extTmpTstat?.currentpreviousthermostatMode?.toString() if(modeEco){ if( !lastMode && state.extTmpTstatOffRequested && state.extTmplastMode){ lastMode=state.extTmplastMode //state.extTmpSavedTemp } if(lastMode){ desiredHeatTemp=getRemSenHeatSetTemp(lastMode, modeEco, false) desiredCoolTemp=getRemSenCoolSetTemp(lastMode, modeEco, false) if(!desiredHeatTemp){ desiredHeatTemp=state.extTmpSavedHTemp } if(!desiredCoolTemp){ desiredCoolTemp=state.extTmpSavedCTemp } //LogAction("getDesiredTemp: Using lastMode: ${lastMode} | extTmpTstatOffRequested: ${state.extTmpTstatOffRequested} | curMode: ${curMode}", sINFO, false) modeOff=(lastMode in [sOFF]) modeCool=(lastMode == sCOOL) modeHeat=(lastMode == sHEAT) modeAuto=(lastMode == sAUTO) } } Double desiredTemp; desiredTemp=0.0D if(!modeOff){ if(desiredHeatTemp && modeHeat) { desiredTemp=desiredHeatTemp } else if(desiredCoolTemp && modeCool) { desiredTemp=desiredCoolTemp } else if(desiredHeatTemp && desiredCoolTemp && (desiredHeatTemp < desiredCoolTemp) && modeAuto ){ desiredTemp=(desiredCoolTemp + desiredHeatTemp) / 2.0D } //else if(desiredHeatTemp && modeEco) { desiredTemp=desiredHeatTemp } //else if(desiredCoolTemp && modeEco) { desiredTemp=desiredCoolTemp } else if(!desiredTemp && state.extTmpSavedTemp){ desiredTemp=state.extTmpSavedTemp } //LogAction("getDesiredTemp: curMode: ${curMode} | lastMode: ${lastMode} | Desired Temp: ${desiredTemp} | Desired Heat Temp: ${desiredHeatTemp} | Desired Cool Temp: ${desiredCoolTemp} extTmpSavedTemp: ${state.extTmpSavedTemp}", sINFO, false) } return desiredTemp } Boolean extTmpTempOk(Boolean disp=false, Boolean last=false){ String meth = "extTmpTempOk: | " LogTrace(meth+"(disp: $disp, last: $last)") String pName=extTmpPrefix() try { Long execTime=now() def extTmpTstat=settings.schMotTstat def extTmpTstatMir=settings.schMotTstatMir Double intTemp=extTmpTstat ? getRemoteSenTemp() : null Double extTemp=getExtTmpTemperature() Double dpLimit=getComfortDewpoint(extTmpTstat) Double curDp=getExtTmpDewPoint() Double diffThresh=Math.abs(getExtTmpTempDiffVal()) String curMode=extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : sNULL Boolean modeOff=(curMode == sOFF) Boolean modeCool, modeHeat, modeEco, modeAuto modeCool=(curMode == sCOOL) modeHeat=(curMode == sHEAT) modeEco=(curMode == sECO) modeAuto=(curMode == sAUTO) Boolean canHeat=state.schMotTstatCanHeat Boolean canCool=state.schMotTstatCanCool //LogAction(meth+"Inside Temp: ${intTemp} | curMode: ${curMode} | modeOff: ${modeOff} | modeEco: ${modeEco} | modeAuto: ${modeAuto} || extTmpTstatOffRequested: ${state.extTmpTstatOffRequested}", sINFO, false) Boolean retval, externalTempOk, internalTempOk retval=true externalTempOk=true internalTempOk=true Boolean dpOk= curDp= (desiredHeatTemp+diffThresh) && extTemp <= (desiredCoolTemp-diffThresh)) ){ retval=false externalTempOk=false str="within range (${desiredHeatTemp} ${desiredCoolTemp})" } //ERS state.extTmpSavedHTemp=desiredHeatTemp state.extTmpSavedCTemp=desiredCoolTemp } Double tempDiff Double desiredTemp; desiredTemp=null Double insideThresh if(!modeAuto && retval){ desiredTemp=getDesiredTemp() //ERS if(desiredTemp){ state.extTmpSavedTemp=desiredTemp } if(!desiredTemp){ desiredTemp=intTemp if(!modeOff){ LogAction(meth+"No Desired Temp found, using interior Temp", sWARN, true) } retval=false }else{ tempDiff=Math.abs(extTemp - desiredTemp) str="enough different (${tempDiff})" insideThresh=getExtTmpInsideTempDiffVal() LogAction(meth+"Outside Temp: ${extTemp} | Inside Temp: ${intTemp} | Desired Temp: ${desiredTemp} | Inside Temp Threshold: ${insideThresh} | Outside Temp Threshold: ${diffThresh} | Actual Difference: ${tempDiff} | Outside Dew point: ${curDp} | Dew point Limit: ${dpLimit}", sTRACE, false) if(diffThresh && tempDiff < diffThresh){ retval=false externalTempOk=false } Boolean extTempHigh= extTemp>=desiredTemp Boolean extTempLow= extTemp<=desiredTemp String oldMode=state.extTmpRestoreMode if(modeCool || oldMode == sCOOL || (!canHeat && canCool)){ str="greater than" if(extTempHigh){ retval=false; externalTempOk=false } if(intTemp > desiredTemp+insideThresh){ retval=false; internalTempOk=false } // too hot inside } if(modeHeat || oldMode == sHEAT || (!canCool && canHeat)){ str="less than" if(extTempLow){ retval=false; externalTempOk=false } if(intTemp < desiredTemp-insideThresh){ retval=false; internalTempOk=false } // too cold inside } //LogAction(meth+"extTempHigh: ${extTempHigh} | extTempLow: ${extTempLow}", sINFO, false) } } Boolean showRes=disp ? (retval!=last) : false if(!dpOk){ LogAction(meth+"${retval} Dewpoint: (${curDp}${tUnitStr()}) is ${dpOk ? "ok" : "TOO HIGH"}", sINFO, showRes) }else{ if(!modeAuto){ LogAction(meth+"${retval} Desired Inside Temp: (${desiredTemp}${tUnitStr()}) is ${externalTempOk ? sBLANK : "Not"} ${str} $diffThresh\u00b0 of Outside Temp: (${extTemp}${tUnitStr()}) ${retval ? "AND" : "OR"} Inside Temp: (${intTemp}) is ${internalTempOk ? sBLANK : "Not"} within Inside Threshold: ${insideThresh} of desired (${desiredTemp})", sINFO, showRes) }else{ LogAction(meth+"${retval} Exterior Temperature (${extTemp}${tUnitStr()}) is ${externalTempOk ? sBLANK : "Not"} ${str} using $diffThresh\u00b0 offset | Inside Temp: (${intTemp}${tUnitStr()})", sINFO, showRes) } } storeExecutionHistory((now() - execTime), "extTmpTempOk") return retval } catch (ex){ log.error "extTmpTempOk Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "extTmpTempOk", true, getAutoType()) } return false } Boolean extTmpScheduleOk(){ return autoScheduleOk(extTmpPrefix()) } Double getExtTmpTempDiffVal(){ return !settings.extTmpDiffVal ? 1.0D : settings.extTmpDiffVal.toDouble() } Double getExtTmpInsideTempDiffVal(){ return !settings.extTmpInsideDiffVal ? (getTemperatureScale() == "C" ? 2.0D : 4.0D) : settings.extTmpInsideDiffVal.toDouble() } Integer getExtTmpWhileOnDtSec(){ return !(String)state.extTmpChgWhileOnDt ? 100000 : GetTimeDiffSeconds((String)state.extTmpChgWhileOnDt, sNULL, "getExtTmpWhileOnDtSec").toInteger() } Integer getExtTmpWhileOffDtSec(){ return !(String)state.extTmpChgWhileOffDt ? 100000 : GetTimeDiffSeconds((String)state.extTmpChgWhileOffDt, sNULL, "getExtTmpWhileOffDtSec").toInteger() } // allow override from schedule? Integer getExtTmpOffDelayVal(){ return !settings.extTmpOffDelay ? 300 : settings.extTmpOffDelay.toInteger() } Integer getExtTmpOnDelayVal(){ return !settings.extTmpOnDelay ? 300 : settings.extTmpOnDelay.toInteger() } void extTmpTempCheck(Boolean cTimeOut=false){ //LogAction("extTmpTempCheck", sINFO, false) String pName=extTmpPrefix() String meth="extTmpTempCheck: | " try { if(!getIsAutomationDisabled()){ def extTmpTstat=settings.schMotTstat def extTmpTstatMir=settings.schMotTstatMir Long execTime=now() //state.autoRunDt=getDtNow() if(state."${pName}TimeoutOn" == null){ state."${pName}TimeoutOn"=false } if(cTimeOut){ state."${pName}TimeoutOn"=true } Boolean timeOut timeOut=state."${pName}TimeoutOn" ?: false String curMode=extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : sNULL Boolean modeOff=(curMode in [sOFF]) Boolean modeInActive=(curMode in [sOFF, sECO]) Boolean modeEco=(curMode in [sECO]) Boolean modeAuto=(curMode == sAUTO) Boolean allowNotif=settings."${pName}NotifOn" // Boolean allowSpeech=allowNotif && settings."${pName}AllowSpeechNotif" Boolean allowAlarm=allowNotif && settings."${pName}AllowAlarmNotif" // Boolean speakOnRestore=allowSpeech && settings."${pName}SpeechOnRestore" if(!modeInActive){ state."${pName}TimeoutOn"=false; timeOut=false } // if we requested off; and someone switched us on or nMode took over... if( state.extTmpTstatOffRequested && (!modeEco || (modeEco && parent.setNModeActive(null))) ){ // reset timer and states LogAction(meth+"${!modeEco ? "HVAC turned on when automation had OFF" : "Automation overridden by nMODE"}, resetting state to match", sWARN, true) state.extTmpChgWhileOnDt=getDtNow() state.extTmpTstatOffRequested=false state.extTmpChgWhileOffDt=getDtNow() state.extTmpRestoreMode=sNULL state."${pName}TimeoutOn"=false unschedTimeoutRestore(pName) } if(modeOff){ storeExecutionHistory((now() - execTime), "extTmpTempCheck") return } String mylastMode=state.extTmplastMode // when we state change that could change desired Temp ensure delays happen before off can happen again Double lastDesired=state.extTmpSavedTemp // this catches scheduled temp or hvac mode changes Double desiredTemp=getDesiredTemp() if( (mylastMode != curMode) || (desiredTemp && desiredTemp != lastDesired)){ if(!modeInActive){ state.extTmplastMode=curMode //ERS if(desiredTemp){ state.extTmpSavedTemp=desiredTemp } Double desiredHeatTemp Double desiredCoolTemp if(modeAuto){ desiredHeatTemp=getRemSenHeatSetTemp(curMode) desiredCoolTemp=getRemSenCoolSetTemp(curMode) if(desiredHeatTemp && desiredCoolTemp){ state.extTmpSavedHTemp=desiredHeatTemp state.extTmpSavedCTemp=desiredCoolTemp } } state.extTmpChgWhileOnDt=getDtNow() }else{ //state.extTmpChgWhileOffDt=getDtNow() } } Boolean safetyOk=getSafetyTempsOk(extTmpTstat) Boolean schedOk=extTmpScheduleOk() Boolean okToRestore= modeEco && state.extTmpTstatOffRequested && state.extTmpRestoreMode Boolean tempWithinThreshold=extTmpTempOk( ((modeEco && okToRestore) || (!modeEco && !okToRestore)), okToRestore) if(!tempWithinThreshold || timeOut || !safetyOk || !schedOk){ if(allowAlarm){ alarmEvtSchedCleanup(extTmpPrefix()) } String rmsg if(okToRestore){ if(getExtTmpWhileOffDtSec() >= (getExtTmpOnDelayVal() - 5) || timeOut || !safetyOk){ String lastMode; lastMode=sNULL if(state.extTmpRestoreMode){ lastMode=extTmpTstat?.currentpreviousthermostatMode?.toString() if(!lastMode){ lastMode=state.extTmpRestoreMode } } if(lastMode && (lastMode != curMode || timeOut || !safetyOk || !schedOk)){ scheduleAutomationEval(70) if(setTstatMode(extTmpTstat, lastMode, pName)){ storeLastAction("Restored Mode ($lastMode)", getDtNow(), pName) state.extTmpRestoreMode=sNULL state.extTmpTstatOffRequested=false state.extTmpRestoredDt=getDtNow() state.extTmpChgWhileOnDt=getDtNow() state."${pName}TimeoutOn"=false unschedTimeoutRestore(pName) if(extTmpTstatMir){ if(setMultipleTstatMode(extTmpTstatMir, lastMode, pName)){ LogAction("Mirroring (${lastMode}) Restore to ${extTmpTstatMir}", sINFO, false) } } rmsg=meth+"Restoring '${extTmpTstat?.label}' to '${strCapitalize(lastMode)}' mode: " Boolean needAlarm; needAlarm=false if(!safetyOk){ rmsg += "External Temp Safety Temps reached" needAlarm=true }else if(!schedOk){ rmsg += "the schedule does not allow automation control" }else if(timeOut){ rmsg += "the (${getEnumValue(longTimeSecEnum(), extTmpOffTimeout)}) Timeout reached" }else{ rmsg += "External Temp above the Threshold for (${getEnumValue(longTimeSecEnum(), extTmpOnDelay)})" } LogAction(rmsg, (needAlarm ? sWARN : sINFO), true) if(allowNotif){ if(!timeOut && safetyOk){ sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times others do NOT // if(speakOnRestore){ sendEventVoiceNotifications(voiceNotifString(state."${pName}OnVoiceMsg", pName), pName, "nmExtTmpOn_${app?.id}", true, "nmExtTmpOff_${app?.id}") } }else if(needAlarm){ sendEventPushNotifications(rmsg, "Warning", pName) if(allowAlarm){ scheduleAlarmOn(pName) } } } storeExecutionHistory((now() - execTime), "extTmpTempCheck") return }else{ LogAction(meth+"There was problem restoring the last mode to ${lastMode}", sERR, true) } }else{ if(!lastMode){ LogAction(meth+"Unable to restore settings: previous mode not found. Likely other automation operation", sWARN, true) state.extTmpTstatOffRequested=false }else if(!timeOut && safetyOk){ LogAction("extTmpTstatCheck: | Skipping Restore: Mode to Restore is same as Current Mode ${curMode}", sINFO, false) } if(!safetyOk){ LogAction(meth+"Unable to restore mode and safety temperatures are exceeded", sWARN, true) } // TODO check if timeout quickly cycles back } }else{ if(safetyOk){ Integer remaining=getExtTmpOnDelayVal() - getExtTmpWhileOffDtSec() LogAction(meth+"Delaying restore for wait period ${getExtTmpOnDelayVal()}, remaining ${remaining}", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) } } }else{ if(modeInActive){ if(timeOut || !safetyOk){ LogAction(meth+"Timeout or Safety temps exceeded and Unable to restore settings okToRestore is false", sWARN, true) state."${pName}TimeoutOn"=false } else if( (!state.extTmpRestoreMode && state.extTmpTstatOffRequested) || (state.extTmpRestoreMode && !state.extTmpTstatOffRequested) ){ LogAction(meth+"Unable to restore settings: previous mode not found.", sWARN, true) state.extTmpRestoreMode=sNULL state.extTmpTstatOffRequested=false } } } } if(tempWithinThreshold && !timeOut && safetyOk && schedOk && !modeEco){ String rmsg if(!modeInActive){ if(getExtTmpWhileOnDtSec() >= (getExtTmpOffDelayVal() - 2)){ state."${pName}TimeoutOn"=false state.extTmpRestoreMode=curMode LogAction(meth+"Saving ${extTmpTstat?.label} (${strCapitalize(state.extTmpRestoreMode)}) mode", sINFO, false) scheduleAutomationEval(70) if(setTstatMode(extTmpTstat, sECO, pName)){ storeLastAction("Set Thermostat ${extTmpTstat?.displayName} to ECO", getDtNow(), pName) state.extTmpTstatOffRequested=true state.extTmpChgWhileOffDt=getDtNow() scheduleTimeoutRestore(pName) //modeInActive=true //modeEco=true rmsg="${extTmpTstat.label} turned 'ECO': External Temp is at the temp threshold for (${getEnumValue(longTimeSecEnum(), extTmpOffDelay)})" if(extTmpTstatMir){ if(setMultipleTstatMode(extTmpTstatMir, sECO, pName)){ LogAction("Mirroring (ECO) Mode to ${extTmpTstatMir}", sINFO, false) } } LogAction(rmsg, sINFO, false) if(allowNotif){ sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT // if(allowSpeech){ sendEventVoiceNotifications(voiceNotifString(state."${pName}OffVoiceMsg",pName), pName, "nmExtTmpOff_${app?.id}", true, "nmExtTmpOn_${app?.id}") } if(allowAlarm){ scheduleAlarmOn(pName) } } }else{ LogAction(meth+"Error turning themostat to Eco", sWARN, true) } }else{ Integer remaining=getExtTmpOffDelayVal() - getExtTmpWhileOnDtSec() LogAction(meth+"Delaying ECO for wait period ${getExtTmpOffDelayVal()} seconds | Wait time remaining: ${remaining} seconds", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) } }else{ LogAction(meth+"Skipping: Exterior temperatures in range and '${extTmpTstat?.label}' mode is 'OFF or ECO'", sINFO, false) } }else{ if(timeOut){ LogAction(meth+"Skipping: active timeout", sINFO, false) } else if(!safetyOk){ LogAction(meth+"Skipping: Safety Temps Exceeded", sINFO, false) } else if(!schedOk){ LogAction(meth+"Skipping: Schedule Restrictions", sINFO, false) } //else if(!tempWithinThreshold){ LogAction("extTmpTempCheck: Exterior temperatures not in range", sINFO, false) } //else if(modeEco){ LogAction("extTmpTempCheck: Skipping: in ECO mode extTmpTstatOffRequested: (${state.extTmpTstatOffRequested})", sINFO, false) } } storeExecutionHistory((now() - execTime), "extTmpTempCheck") } } catch (ex){ log.error "extTmpTempCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "extTmpTempCheck", true, getAutoType()) } } @SuppressWarnings('unused') void extTmpGenericEvt(evt){ Long startTime=now() Long eventDelay=startTime - evt.date.getTime() String evntN=(String)evt.name LogAction(evntN.toUpperCase()+" Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", sDEBUG, false) storeLastEventData(evt) extTmpDpOrTempEvt(evntN) } void extTmpDpOrTempEvt(String type){ if(getIsAutomationDisabled()){ return } else { //state.needWeathUpd=false if((Boolean)settings.humCtrlUseWeather || (Boolean)settings.extTmpUseWeather){ state.needWeathUpd=false state.weatherUpdDt=getDtNow() getExtConditions() } } if(isExtTmpConfigured()){ def extTmpTstat=settings.schMotTstat String curMode=extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : sNULL Boolean modeOff=(curMode in [sOFF]) if(modeOff){ //LogAction("${type} | Thermostat is off HVAC mode: ${curMode}", sINFO, false) return } Boolean lastTempWithinThreshold=state.extTmpWithinThreshold Boolean tempWithinThreshold=extTmpTempOk(false,false) state.extTmpWithinThreshold=tempWithinThreshold if(lastTempWithinThreshold == null || tempWithinThreshold != lastTempWithinThreshold){ //def extTmpTstat=settings.schMotTstat //def curMode=extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : sNULL Boolean modeActive=!(curMode in [sOFF, sECO]) Integer offVal=getExtTmpOffDelayVal() Integer onVal=getExtTmpOnDelayVal() Map timeVal if(modeActive){ state.extTmpChgWhileOnDt=getDtNow() timeVal=["valNum":offVal, "valLabel":getEnumValue(longTimeSecEnum(), offVal)] }else{ state.extTmpChgWhileOffDt=getDtNow() timeVal=["valNum":onVal, "valLabel":getEnumValue(longTimeSecEnum(), onVal)] } Integer val=Math.min( Math.max(timeVal?.valNum,defaultAutomationTime()), 60) LogAction(type+" | External Temp Check scheduled for (${timeVal.valLabel}) HVAC mode: ${curMode}", sINFO, false) scheduleAutomationEval(val) } //else { LogAction("${type}: Skipping no state change | tempWithinThreshold: ${tempWithinThreshold}", sINFO, false) } }else{ scheduleAutomationEval() } } /****************************************************************************** | WATCH CONTACTS AUTOMATION CODE | *******************************************************************************/ static String conWatPrefix(){ return "conWat" } String autoStateDesc(String autotype){ String str; str=sBLANK String t0=state."${autotype}RestoreMode" Boolean t1=state."${autotype}TstatOffRequested" str += "ECO State:" str += "\n • Mode Adjusted: (${t0 != null ? "TRUE" : "FALSE"})" str += "\n • Last Mode: (${t0 ? strCapitalize(t0) : "Not Set"})" str += t1 ? "\n • Last Eco Requested: (${t1})" : sBLANK return str != sBLANK ? str :sNULL } String conWatContactDesc(){ if(settings.conWatContacts){ Integer cCnt=settings.conWatContacts?.size() ?: 0 String str; str=sBLANK Integer cnt; cnt=0 str += "Contact Status:" settings.conWatContacts.sort { it.displayName }?.each { dev -> cnt=cnt+1 String t0=strCapitalize(dev?.currentContact) String val=t0 ?: "Not Set" str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: (${val})" } return str } return null } Boolean isConWatConfigured(){ return (Boolean)settings.schMotContactOff && settings.conWatContacts && settings.conWatOffDelay } Boolean getConWatContactsOk(){ return settings.conWatContacts?.currentContact?.contains("open") ? false : true } //def conWatContactOk(){ return (!settings.conWatContacts) ? false : true } Boolean conWatScheduleOk(){ return autoScheduleOk(conWatPrefix()) } Integer getConWatOpenDtSec(){ return !(String)state.conWatOpenDt ? 100000 : GetTimeDiffSeconds((String)state.conWatOpenDt, sNULL, "getConWatOpenDtSec").toInteger() } Integer getConWatCloseDtSec(){ return !(String)state.conWatCloseDt ? 100000 : GetTimeDiffSeconds((String)state.conWatCloseDt, sNULL, "getConWatCloseDtSec").toInteger() } Integer getConWatRestoreDelayBetweenDtSec(){ return !(String)state.conWatRestoredDt ? 100000 : GetTimeDiffSeconds((String)state.conWatRestoredDt, sNULL, "getConWatRestoreDelayBetweenDtSec").toInteger() } // allow override from schedule? Integer getConWatOffDelayVal(){ return !settings.conWatOffDelay ? 300 : (settings.conWatOffDelay.toInteger()) } Integer getConWatOnDelayVal(){ return !settings.conWatOnDelay ? 300 : (settings.conWatOnDelay.toInteger()) } Integer getConWatRestoreDelayBetweenVal(){ return !settings.conWatRestoreDelayBetween ? 600 : settings.conWatRestoreDelayBetween.toInteger() } void conWatCheck(Boolean cTimeOut=false){ LogTrace("conWatCheck $cTimeOut") // // There should be monitoring of actual temps for min and max warnings given on/off automations // // Should have some check for stuck contacts // String pName=conWatPrefix() String meth="conWatCheck: | " def conWatTstat=settings.schMotTstat def conWatTstatMir=settings.schMotTstatMir try { if(!getIsAutomationDisabled()){ Long execTime=now() //state.autoRunDt=getDtNow() if(state."${pName}TimeoutOn" == null){ state."${pName}TimeoutOn"=false } if(cTimeOut){ state."${pName}TimeoutOn"=true } Boolean timeOut timeOut=state."${pName}TimeoutOn" ?: false String curMode=conWatTstat ? conWatTstat.currentThermostatMode.toString() : sNULL Boolean modeEco=(curMode in [sECO]) //def curNestPres=getTstatPresence(conWatTstat) Boolean modeOff=(curMode in [sOFF, sECO]) Boolean allowNotif=settings."${pName}NotifOn" ? true : false // Boolean allowSpeech=allowNotif && settings."${pName}AllowSpeechNotif" Boolean allowAlarm=allowNotif && settings."${pName}AllowAlarmNotif" // Boolean speakOnRestore=allowSpeech && settings."${pName}SpeechOnRestore" //log.debug "curMode: $curMode | modeOff: $modeOff | conWatRestoreOnClose: $conWatRestoreOnClose | lastMode: $lastMode" //log.debug "conWatTstatOffRequested: ${state.conWatTstatOffRequested} | getConWatCloseDtSec(): ${getConWatCloseDtSec()}" if(!modeEco){ state."${pName}TimeoutOn"=false; timeOut=false } // if we requested off; and someone switched us on or nMode took over... if( state.conWatTstatOffRequested && (!modeEco || (modeEco && parent.setNModeActive(null))) ){ // so reset timer and states LogAction(meth+"${!modeEco ? "HVAC turned on when automation had OFF" : "Automation overridden by nMODE"}, resetting state to match", sWARN, true) state.conWatRestoreMode=sNULL state.conWatTstatOffRequested=false state.conWatOpenDt=getDtNow() state."${pName}TimeoutOn"=false unschedTimeoutRestore(pName) } String mylastMode=(String)state.conWatlastMode // when we state change modes, ensure delays happen before off can happen again state.conWatlastMode=curMode if(!modeOff && (mylastMode != curMode)){ state.conWatOpenDt=getDtNow() } Boolean safetyOk=getSafetyTempsOk(conWatTstat) Boolean schedOk=conWatScheduleOk() Boolean okToRestore= modeEco && state.conWatTstatOffRequested Boolean contactsOk=getConWatContactsOk() if(contactsOk || timeOut || !safetyOk || !schedOk){ if(allowAlarm){ alarmEvtSchedCleanup(conWatPrefix()) } String rmsg if(okToRestore){ if(getConWatCloseDtSec() >= (getConWatOnDelayVal() - 5) || timeOut || !safetyOk){ String lastMode; lastMode=sNULL if(state.conWatRestoreMode){ lastMode=conWatTstat?.currentpreviousthermostatMode?.toString() if(!lastMode){ lastMode=state.conWatRestoreMode } } if(lastMode && (lastMode != curMode || timeOut || !safetyOk || !schedOk)){ scheduleAutomationEval(70) if(setTstatMode(conWatTstat, lastMode, pName)){ storeLastAction("Restored Mode ($lastMode) to $conWatTstat", getDtNow(), pName) state.conWatRestoreMode=sNULL state.conWatTstatOffRequested=false state.conWatRestoredDt=getDtNow() state.conWatOpenDt=getDtNow() state."${pName}TimeoutOn"=false unschedTimeoutRestore(pName) //modeEco=false //modeOff=false if(conWatTstatMir){ if(setMultipleTstatMode(conWatTstatMir, lastMode, pName)){ LogAction("Mirroring (${lastMode}) Restore to ${conWatTstatMir}", sINFO, false) } } rmsg="Restoring '${conWatTstat?.label}' to '${strCapitalize(lastMode)}' mode: " Boolean needAlarm; needAlarm=false if(!safetyOk){ rmsg += "Global Safety Values reached" needAlarm=true }else if(timeOut){ rmsg += "(${getEnumValue(longTimeSecEnum(), conWatOffTimeout)}) Timeout reached" }else if(!schedOk){ rmsg += "of Schedule restrictions" }else{ rmsg += "ALL contacts 'Closed' for (${getEnumValue(longTimeSecEnum(), conWatOnDelay)})" } LogAction(rmsg, (needAlarm ? sWARN : sINFO), true) //ERS if(allowNotif){ if(!timeOut && safetyOk){ sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT // if(speakOnRestore){ sendEventVoiceNotifications(voiceNotifString(state."${pName}OnVoiceMsg",pName), pName, "nmConWatOn_${app?.id}", true, "nmConWatOff_${app?.id}") } }else if(needAlarm){ sendEventPushNotifications(rmsg, "Warning", pName) if(allowAlarm){ scheduleAlarmOn(pName) } } } storeExecutionHistory((now() - execTime), "conWatCheck") return }else{ LogAction(meth+"There was problem restoring the last mode to ($lastMode)", sERR, true) } }else{ if(!lastMode){ LogAction(meth+"Unable to restore settings: previous mode not found. Likely other automation operation", sWARN, true) state.conWatTstatOffRequested=false }else if(!timeOut && safetyOk){ LogAction(meth+"Skipping Restore: Mode to Restore is same as Current Mode ${curMode}", sINFO, false) } if(!safetyOk){ LogAction(meth+"Unable to restore mode and safety temperatures are exceeded", sWARN, true) } } }else{ if(safetyOk){ Integer remaining=getConWatOnDelayVal() - getConWatCloseDtSec() LogAction(meth+"Delaying restore for wait period ${getConWatOnDelayVal()}, remaining ${remaining}", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) } } }else{ if(modeOff){ if(timeOut || !safetyOk){ LogAction(meth+"Timeout or Safety temps exceeded and Unable to restore settings okToRestore is false", sWARN, true) state."${pName}TimeoutOn"=false } else if(!state.conWatRestoreMode && state.conWatTstatOffRequested){ LogAction(meth+"Unable to restore settings: previous mode not found. Likely other automation operation", sWARN, true) state.conWatTstatOffRequested=false } } } } if(!contactsOk && safetyOk && !timeOut && schedOk && !modeEco){ String rmsg if(!modeOff){ if((getConWatOpenDtSec() >= (getConWatOffDelayVal() - 2)) && (getConWatRestoreDelayBetweenDtSec() >= (getConWatRestoreDelayBetweenVal() - 2))){ state."${pName}TimeoutOn"=false state.conWatRestoreMode=curMode List t0=getOpenContacts(conWatContacts) String openCtDesc=t0 ? " '${t0?.join(", ")}' " : " a selected contact " LogAction(meth+"Saving ${conWatTstat?.label} mode (${strCapitalize(state.conWatRestoreMode)})", sINFO, false) LogAction(meth+"${openCtDesc}${t0?.size() > 1 ? "are" : "is"} still Open: Turning 'OFF' '${conWatTstat?.label}'", sDEBUG, false) scheduleAutomationEval(70) if(setTstatMode(conWatTstat, sECO, pName)){ storeLastAction("Set $conWatTstat to 'ECO'", getDtNow(), pName) state.conWatTstatOffRequested=true state.conWatCloseDt=getDtNow() scheduleTimeoutRestore(pName) if(conWatTstatMir){ if(setMultipleTstatMode(conWatTstatMir, sECO, pName)){ LogAction("Mirroring (ECO) Mode to ${conWatTstatMir}", sINFO, false) } } rmsg="${conWatTstat.label} turned to 'ECO': ${openCtDesc}Opened for (${getEnumValue(longTimeSecEnum(), conWatOffDelay)})" LogAction(rmsg, sINFO, false) if(allowNotif){ sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT // if(allowSpeech){ sendEventVoiceNotifications(voiceNotifString(state."${pName}OffVoiceMsg",pName), pName, "nmConWatOff_${app?.id}", true, "nmConWatOn_${app?.id}") } if(allowAlarm){ scheduleAlarmOn(pName) } } }else{ LogAction(meth+"Error turning themostat to ECO", sWARN, true) } }else{ if(getConWatRestoreDelayBetweenDtSec() < (getConWatRestoreDelayBetweenVal() - 2)){ Integer remaining=getConWatRestoreDelayBetweenVal() - getConWatRestoreDelayBetweenDtSec() //LogAction("conWatCheck: | Skipping ECO change: delay since last restore not met (${getEnumValue(longTimeSecEnum(), conWatRestoreDelayBetween)})", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) }else{ Integer remaining=getConWatOffDelayVal() - getConWatOpenDtSec() LogAction(meth+"Delaying ECO for wait period ${getConWatOffDelayVal()} seconds | Wait time remaining: ${remaining} seconds", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) } } }else{ LogAction(meth+"Skipping ECO change: '${conWatTstat?.label}' mode is '${curMode}'", sINFO, false) } }else{ if(timeOut){ LogAction(meth+"Skipping: active timeout", sINFO, false) } else if(!schedOk){ LogAction(meth+"Skipping: Schedule Restrictions", sINFO, false) } else if(!safetyOk){ LogAction(meth+"Skipping: Safety Temps Exceeded", sWARN, true) } else if(contactsOk){ LogAction(meth+"Contacts are closed", sINFO, false) } //else if(modeEco){ LogAction("conWatTempCheck: Skipping: in ECO mode conWatTstatOffRequested: (${state.conWatTstatOffRequested})", sINFO, false) } } storeExecutionHistory((now() - execTime), "conWatCheck") } } catch (ex){ log.error "conWatCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "conWatCheck", true, getAutoType()) } } @SuppressWarnings('unused') void conWatContactEvt(evt){ Long startTime=now() Long eventDelay=startTime - (Long)evt.date.getTime() LogAction("${evt?.name?.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", sDEBUG, false) if(!getIsAutomationDisabled()){ def conWatTstat=settings.schMotTstat String curMode=conWatTstat ? conWatTstat?.currentThermostatMode?.toString() : sNULL Boolean isModeOff=(curMode in [sECO]) Boolean conOpen=((String)evt?.value == "open") Boolean canSched; canSched=false Map timeVal if(conOpen){ state.conWatOpenDt=getDtNow() timeVal=["valNum":getConWatOffDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getConWatOffDelayVal())] canSched=true } else if(!conOpen && getConWatContactsOk()){ state.conWatCloseDt=getDtNow() if(isModeOff){ timeVal=["valNum":getConWatOnDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getConWatOnDelayVal())] canSched=true } } storeLastEventData(evt) if(canSched){ //LogAction("conWatContactEvt: Contact Check scheduled for (${timeVal?.valLabel})", sINFO, false) Integer val=Math.min( Math.max(timeVal?.valNum,defaultAutomationTime()), 60) scheduleAutomationEval(val) }else{ LogAction("conWatContactEvt: Skipping Event", sINFO, false) } } } /****************************************************************************** | WATCH FOR LEAKS AUTOMATION LOGIC CODE | ******************************************************************************/ static String leakWatPrefix(){ return "leakWat" } String leakWatSensorsDesc(){ if((List)settings.leakWatSensors){ Integer cCnt=settings.leakWatSensors?.size() ?: 0 String str; str=sBLANK Integer cnt; cnt=0 str += "Leak Sensors:" ((List)settings.leakWatSensors)?.sort { it?.displayName }?.each { dev -> cnt=cnt+1 String t0=strCapitalize(dev?.currentWater) String val=t0 ?: "Not Set" str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: (${val})" } return str } return sNULL } Boolean isLeakWatConfigured(){ return (Boolean)settings.schMotWaterOff && (List)settings.leakWatSensors } Boolean getLeakWatSensorsOk(){ return ((List)settings.leakWatSensors)?.currentWater?.contains("wet") ? false : true } //def leakWatSensorsOk(){ return (!settings.leakWatSensors) ? false : true } //def leakWatScheduleOk(){ return autoScheduleOk(leakWatPrefix()) } // allow override from schedule? Integer getLeakWatOnDelayVal(){ return !settings.leakWatOnDelay ? 300 : settings.leakWatOnDelay.toInteger() } Integer getLeakWatDryDtSec(){ return !(String)state.leakWatDryDt ? 100000 : GetTimeDiffSeconds((String)state.leakWatDryDt, sNULL, "getLeakWatDryDtSec").toInteger() } void leakWatCheck(){ //LogTrace("leakWatCheck") // // if we cannot save/restore settings, don't bother turning things off // String pName=leakWatPrefix() String meth="leakWatCheck: | " try { if(!getIsAutomationDisabled()){ def leakWatTstat=settings.schMotTstat def leakWatTstatMir=settings.schMotTstatMir Long execTime=now() //state.autoRunDt=getDtNow() String curMode=leakWatTstat.currentThermostatMode.toString() //def curNestPres=getTstatPresence(leakWatTstat) Boolean modeOff=(curMode == sOFF) Boolean allowNotif=!!(settings."${pName}NotifOn") // Boolean allowSpeech=allowNotif && settings."${pName}AllowSpeechNotif" Boolean allowAlarm=allowNotif && settings."${pName}AllowAlarmNotif" // Boolean speakOnRestore=allowSpeech && settings."${pName}SpeechOnRestore" if(!modeOff && (Boolean)state.leakWatTstatOffRequested){ // someone switched us on when we had turned things off, so reset timer and states LogAction(meth+"System turned on when automation had OFF, resetting state to match", sWARN, true) state.leakWatRestoreMode=sNULL state.leakWatTstatOffRequested=false } Boolean safetyOk=getSafetyTempsOk(leakWatTstat) //def schedOk=leakWatScheduleOk() Boolean okToRestore=(modeOff && (Boolean)state.leakWatTstatOffRequested) Boolean sensorsOk=getLeakWatSensorsOk() if(sensorsOk || !safetyOk){ if(allowAlarm){ alarmEvtSchedCleanup(leakWatPrefix()) } String rmsg if(okToRestore){ if(getLeakWatDryDtSec() >= (getLeakWatOnDelayVal() - 5) || !safetyOk){ String lastMode; lastMode=sNULL if(state.leakWatRestoreMode){ lastMode=(String)state.leakWatRestoreMode } if(lastMode && (lastMode != curMode || !safetyOk)){ scheduleAutomationEval(70) if(setTstatMode(leakWatTstat, lastMode, pName)){ storeLastAction("Restored Mode ($lastMode) to $leakWatTstat", getDtNow(), pName) state.leakWatTstatOffRequested=false state.leakWatRestoreMode=sNULL state.leakWatRestoredDt=getDtNow() if(leakWatTstatMir){ if(setMultipleTstatMode(leakWatTstatMir, lastMode, pName)){ LogAction(meth+"Mirroring Restoring Mode (${lastMode}) to ${leakWatTstatMir}", sINFO, false) } } rmsg="Restoring '${leakWatTstat?.label}' to '${strCapitalize(lastMode)}' mode: " Boolean needAlarm; needAlarm=false if(!safetyOk){ rmsg += "External Temp Safety Temps reached" needAlarm=true }else{ rmsg += "ALL leak sensors 'Dry' for (${getEnumValue(longTimeSecEnum(), leakWatOnDelay)})" } LogAction(rmsg, needAlarm ? sWARN : sINFO, true) if(allowNotif){ if(safetyOk){ sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT // if(speakOnRestore){ sendEventVoiceNotifications(voiceNotifString(state."${pName}OnVoiceMsg", pName), pName, "nmLeakWatOn_${app?.id}", true, "nmLeakWatOff_${app?.id}") } }else if(needAlarm){ sendEventPushNotifications(rmsg, "Warning", pName) if(allowAlarm){ scheduleAlarmOn(pName) } } } storeExecutionHistory((now() - execTime), "leakWatCheck") return }else{ LogAction(meth+"There was problem restoring the last mode to ${lastMode}", sERR, true) } }else{ if(!safetyOk){ LogAction(meth+"Unable to restore mode and safety temperatures are exceeded", sWARN, true) }else{ LogAction(meth+"Skipping Restore: Mode to Restore (${lastMode}) is same as Current Mode ${curMode}", sINFO, false) } } }else{ if(safetyOk){ Integer remaining=getLeakWatOnDelayVal() - getLeakWatDryDtSec() LogAction(meth+"Delaying restore for wait period ${getLeakWatOnDelayVal()}, remaining ${remaining}", sINFO, false) Integer val=Math.min( Math.max(remaining,defaultAutomationTime()), 60) scheduleAutomationEval(val) } } }else{ if(modeOff){ if(!safetyOk){ LogAction(meth+"Safety temps exceeded and Unable to restore settings okToRestore is false", sWARN, true) } else if(!state.leakWatRestoreMode && state.leakWatTstatOffRequested){ LogAction(meth+"Unable to restore settings: previous mode not found. Likely other automation operation", sWARN, true) state.leakWatTstatOffRequested=false } } } } // tough decision here: there is a leak, do we care about schedule ? // if(!getLeakWatSensorsOk() && safetyOk && schedOk){ if(!sensorsOk && safetyOk){ String rmsg if(!modeOff){ state.leakWatRestoreMode=curMode List t0=getWetWaterSensors((List)settings.leakWatSensors) String wetCtDesc=t0 ? " '${t0?.join(", ")}' " : " a selected leak sensor " LogAction(meth+"Saving ${leakWatTstat?.label} mode (${strCapitalize((String)state.leakWatRestoreMode)})", sINFO, false) LogAction(meth+"${wetCtDesc}${t0?.size() > 1 ? "are" : "is"} Wet: Turning 'OFF' '${leakWatTstat?.label}'", sDEBUG, false) scheduleAutomationEval(70) if(setTstatMode(leakWatTstat, sOFF, pName)){ storeLastAction("Turned Off $leakWatTstat", getDtNow(), pName) state.leakWatTstatOffRequested=true state.leakWatDryDt=sNULL // getDtNow() if(leakWatTstatMir){ if(setMultipleTstatMode(leakWatTstatMir, sOFF, pName)){ LogAction(meth+"Mirroring (Off) Mode to ${leakWatTstatMir}", sINFO, false) } } rmsg="${leakWatTstat.label} turned 'OFF': ${wetCtDesc}has reported it's WET" LogAction(rmsg, sWARN, true) if(allowNotif){ sendEventPushNotifications(rmsg, "Warning", pName) // this uses parent and honors quiet times, others do NOT // if(allowSpeech){ sendEventVoiceNotifications(voiceNotifString(state."${pName}OffVoiceMsg",pName), pName, "nmLeakWatOff_${app?.id}", true, "nmLeakWatOn_${app?.id}") } if(allowAlarm){ scheduleAlarmOn(pName) } } }else{ LogAction(meth+"Error turning themostat Off", sWARN, true) } }else{ LogAction(meth+"Skipping change: '${leakWatTstat?.label}' mode is already 'OFF'", sINFO, false) } }else{ //if(!schedOk){ LogAction("leakWatCheck: Skipping: Schedule Restrictions", sWARN, true) } if(!safetyOk){ LogAction(meth+"Skipping: Safety Temps Exceeded", sWARN, true) } if(sensorsOk){ LogAction(meth+"Sensors are ok", sINFO, false) } } storeExecutionHistory((now() - execTime), "leakWatCheck") } } catch (ex){ log.error "leakWatCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "leakWatCheck", true, getAutoType()) } } @SuppressWarnings('unused') void leakWatSensorEvt(evt){ Long startTime=now() Long eventDelay=startTime - evt.date.getTime() LogAction("${evt?.name?.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", sDEBUG, false) if(!getIsAutomationDisabled()){ def leakWatTstat=settings.schMotTstat String curMode=leakWatTstat?.currentThermostatMode?.toString() Boolean isModeOff=(curMode == sOFF) Boolean leakWet=(evt?.value == "wet") Boolean canSched; canSched=false Map timeVal if(leakWet){ canSched=true timeVal=["valNum":0, "valLabel":"leak is wet now"] } else if(!leakWet && getLeakWatSensorsOk()){ if(isModeOff){ state.leakWatDryDt=getDtNow() timeVal=["valNum":getLeakWatOnDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getLeakWatOnDelayVal())] canSched=true } } storeLastEventData(evt) if(canSched){ LogAction("leakWatSensorEvt: Leak Check scheduled (${timeVal?.valLabel})", sINFO, false) Integer val=Math.min( Math.max(timeVal?.valNum,defaultAutomationTime()), 60.0D) scheduleAutomationEval(val) }else{ LogAction("leakWatSensorEvt: Skipping Event", sINFO, false) } } } /******************************************************************************** | MODE AUTOMATION CODE | *********************************************************************************/ static String nModePrefix(){ return "nMode" } def nestModePresPage(){ //Logger("in nestModePresPage") String pName=nModePrefix() dynamicPage(name: "nestModePresPage", title: "Nest Mode - Nest Home/Away Automation", uninstall: false, install: true){ if(!(List)settings.nModePresSensor && !settings.nModeSwitch){ Boolean modeReq=((List)settings.nModeHomeModes && (List)settings.nModeAwayModes) section(sectionTitleStr("Set Nest Presence with location Modes:")){ input "nModeHomeModes", sMODE, title: imgTitle(getAppImg("mode_home_icon.png"), inputTitleStr("Modes to Set Nest Location 'Home'")), multiple: true, submitOnChange: true, required: modeReq if(checkModeDuplication((List)settings.nModeHomeModes, (List)settings.nModeAwayModes)){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("ERROR:\nDuplicate Mode(s) were found under both the Home and Away Modes.\nPlease Correct to Proceed")), required: true, state: null } input "nModeAwayModes", sMODE, title: imgTitle(getAppImg("mode_away_icon.png"), inputTitleStr("Modes to Set Nest Location 'Away'")), multiple: true, submitOnChange: true, required: modeReq if((List)settings.nModeHomeModes || (List)settings.nModeAwayModes){ //Logger("in part 11") String str; str=sBLANK String locPres=getNestLocPres() String locMode=location.mode.toString() str += locMode || locPres ? "Location Mode Status:" : sBLANK str += locMode ? "\n${locPres ? "├" : "└"} Hub: (${locMode})" : sBLANK str += locPres ? "\n└ Nest Location: (${locPres == "away" ? "Away" : "Home"})" : sBLANK paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != sBLANK ? sCOMPLT : null) } } } if(!(List)settings.nModeHomeModes && !(List)settings.nModeAwayModes && !settings.nModeSwitch){ section(sectionTitleStr("(Optional) Set Nest Presence using Presence Sensor:")){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Choose a Presence Sensor(s) to use to set your Nest to Home/Away")) String t0=nModePresenceDesc() String presDesc=t0 ? "\n\n${t0}" + descriptions("d_ttm") : descriptions("d_ttc") //Logger("in part 12") input "nModePresSensor", "capability.presenceSensor", title: imgTitle(getAppImg("presence_icon.png"), inputTitleStr("Select Presence Sensor(s)")), description: presDesc, multiple: true, submitOnChange: true, required: false if((List)settings.nModePresSensor){ if(((List)settings.nModePresSensor).size() > 1){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("How this Works!")) paragraph sectionTitleStr("Nest Location will be set to 'Away' when all Presence sensors leave and will return to 'Home' when someone arrives") } paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${t0}")), state: sCOMPLT } } } if(!(List)settings.nModePresSensor && !(List)settings.nModeHomeModes && !(List)settings.nModeAwayModes){ //Logger("in part 13") section(sectionTitleStr("(Optional) Set Nest Presence based on the state of a Switch:")){ input "nModeSwitch", "capability.switch", title: imgTitle(getAppImg("i_sw"), inputTitleStr("Select a Switch")), required: false, multiple: false, submitOnChange: true if(settings.nModeSwitch){ input "nModeSwitchOpt", sENUM, title: imgTitle(getAppImg("i_set"), inputTitleStr("Switch State to Trigger 'Away'?")), required: true, defaultValue: "On", options: ["On", "Off"], submitOnChange: true } } } if(parent.getSettingVal("cameras")){ section(sectionTitleStr("Nest Cam Options:")){ input (name: "nModeCamOnAway", type: sBOOL, title: imgTitle(getAppImg("camera_green_icon.png"), inputTitleStr("Turn On Nest Cams when Away?")), required: false, defaultValue: false, submitOnChange: true) input (name: "nModeCamOffHome", type: sBOOL, title: imgTitle(getAppImg("camera_gray_icon.png"), inputTitleStr("Turn Off Nest Cams when Home?")), required: false, defaultValue: false, submitOnChange: true) if((Boolean)settings.nModeCamOffHome || (Boolean)settings.nModeCamOnAway){ paragraph paraTitleStr("Optional") paragraph sectionTitleStr("You can choose which cameras are changed when Home/Away. If you don't select any devices all will be changed.") input (name: "nModeCamsSel", type: "capability.soundSensor", title: imgTitle(getAppImg("camera_blue_icon.png"), inputTitleStr("Select your Nest Cams?")), required: false, multiple: true, submitOnChange: true) } } } if(((List)settings.nModeHomeModes && (List)settings.nModeAwayModes) || (List)settings.nModePresSensor || settings.nModeSwitch){ section(sectionTitleStr("Additional Settings:")){ //Logger("in part 14") input (name: "nModeSetEco", type: sBOOL, title: imgTitle(getDevImg("eco_icon.png"), inputTitleStr("Set ECO mode when away?")), required: false, defaultValue: false, submitOnChange: true) input (name: "nModeDelay", type: sBOOL, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Changes?")), required: false, defaultValue: false, submitOnChange: true) if((Boolean)settings.nModeDelay){ input "nModeDelayVal", sENUM, title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Delay before change?")), required: false, defaultValue: 60, options:longTimeSecEnum(), submitOnChange: true } } } if((((List)settings.nModeHomeModes && (List)settings.nModeAwayModes) && !(List)settings.nModePresSensor) || (List)settings.nModePresSensor){ //Logger("in part 15") section(getDmtSectionDesc(pName)){ String pageDesc=getDayModeTimeDesc(pName) href "setDayModeTimePage1", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName": "${pName}"] } section(sectionTitleStr(titles("t_nt"))){ String t0=getNotifConfigDesc(pName) String pageDesc=t0 ? t0 + descriptions("d_ttm") : sBLANK href "setNotificationPage2", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName":"${pName}", "allowSpeech":false, "allowAlarm":false, "showSchedule":true] } } /* if(state.showHelp){ section("Help:"){ href url:"${getAutoHelpPageUrl()}", style:"embedded", required:false, title:"Help and Instructions", description:sBLANK, image: getAppImg("info.png") } } */ } } String nModePresenceDesc(){ if((List)settings.nModePresSensor){ Integer cCnt=((List)settings.nModePresSensor).size() ?: 0 String str; str=sBLANK Integer cnt; cnt=0 str += "Presence Status:" ((List)settings.nModePresSensor).sort { it?.displayName }?.each { dev -> cnt=cnt+1 String t0=strCapitalize(dev?.currentPresence) String presState=t0 ?: "No State" str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: ${(dev?.label?.toString()?.length() > 10) ? "\n${(cCnt == 1 || cnt == cCnt) ? " " : " │"} └ " : sBLANK}(${presState})" } return str } return sNULL } Boolean isNestModesConfigured(){ return ((!(List)settings.nModePresSensor && !settings.nModeSwitch && ((List)settings.nModeHomeModes && (List)settings.nModeAwayModes)) || ((List)settings.nModePresSensor && !settings.nModeSwitch) || (!(List)settings.nModePresSensor && settings.nModeSwitch)) } @SuppressWarnings('unused') void nModeGenericEvt(evt){ Long startTime=now() Long eventDelay=startTime - evt.date.getTime() LogAction("${evt.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", sDEBUG, false) if(!getIsAutomationDisabled()){ storeLastEventData(evt) if((Boolean)settings.nModeDelay){ Integer delay=settings.nModeDelayVal.toInteger() ?: 60 if(delay > defaultAutomationTime()){ LogAction("Event | A Check is scheduled (${getEnumValue(longTimeSecEnum(), settings.nModeDelayVal)})", sINFO, false) scheduleAutomationEval(delay) }else{ scheduleAutomationEval() } }else{ scheduleAutomationEval() } } } void adjustCameras(Boolean on, String sendAutoType=sNULL){ def cams=parent.getSettingVal("cameras") if(cams){ List foundCams = (List)settings.nModeCamsSel ?: cams.collect { parent.getDevice(it) } //parent.getCameraDevice(it) } foundCams.each { dev -> if(dev){ String didstr; didstr="On" try { if(on){ dev?.on() }else{ dev?.off() didstr="Off" } LogAction("adjustCameras: Turning Streaming ${didstr} for (${dev?.displayName})", sINFO, false) storeLastAction("Turned ${didstr} Streaming ${dev?.displayName}", getDtNow(), sendAutoType) } catch (ex){ log.error "adjustCameras() Exception: ${dev?.label} does not support commands on / off ${ex?.message}" sendEventPushNotifications("Camera commands not found, check IDE logs and installation instructions", "Warning", nModePrefix()) //parent?.sendExceptionData(ex, "adjustCameras", true, getAutoType()) } return dev } } } } void adjustEco(Boolean on, String senderAutoType){ def tstats=parent.getSettingVal("thermostats") def foundTstats; foundTstats=null if(tstats){ foundTstats=tstats.collect { dni -> //foundTstats=tstats?.each { d1 -> def d1=parent.getDevice(dni) //def d1=parent.getThermostatDevice(dni) if(d1){ String didstr, tstatAction didstr=sNULL tstatAction=sNULL String curMode=d1.currentThermostatMode String prevMode=d1.currentpreviousthermostatMode //LogAction("adjustEco: CURMODE: ${curMode} ON: ${on} PREVMODE: ${prevMode}", sINFO, false) if(on && !(curMode in [sECO, sOFF])){ didstr="ECO" tstatAction=sECO } if(!on && curMode in [sECO]){ if(prevMode && prevMode != curMode){ didstr=prevMode tstatAction=prevMode } } if(didstr){ Boolean a=setTstatMode(d1, tstatAction, senderAutoType) LogAction("adjustEco($on): | Thermostat: ${d1?.displayName} setting to HVAC mode $didstr was $curMode", sDEBUG, false) storeLastAction("Set ${d1?.displayName} to $didstr", getDtNow(), senderAutoType) }else{ LogAction("adjustEco: | Thermostat: ${d1?.displayName} NOCHANGES CURMODE: ${curMode} ON: ${on} PREVMODE: ${prevMode}", sDEBUG, false) } return d1 }else{ LogAction("adjustEco NO D1", sWARN, true); return null} } } } void setAway(Boolean away){ def tstats=parent.getSettingVal("thermostats") String didstr=away ? "AWAY" : "HOME" def foundTstats if(tstats){ foundTstats=tstats?.collect { dni -> //foundTstats=tstats?.each { d1 -> def d1=parent.getDevice(dni) //def d1=parent.getThermostatDevice(dni) if(d1){ if(away){ d1?.away() }else{ d1?.present() } LogAction("setAway($away): | Thermostat: ${d1?.displayName} setting to $didstr", sDEBUG, false) storeLastAction("Set ${d1?.displayName} to $didstr", getDtNow(), "nMode") return d1 }else{ LogAction("setaway NO D1", sWARN, true); return null } } }else{ if(away){ parent.setStructureAway(null, true) }else{ parent.setStructureAway(null, false) } LogAction("setAway($away): | Setting structure to $didstr", sDEBUG, false) storeLastAction("Set structure to $didstr", getDtNow(), "nMode") } } Boolean nModeScheduleOk(){ return autoScheduleOk(nModePrefix()) } Integer getnModeActionSec(){ return !(String)state.nModeActionDt ? 100000 : GetTimeDiffSeconds((String)state.nModeActionDt, sNULL, "getnModeActionSec").toInteger() } void checkNestMode(){ LogAction("checkNestMode", sDEBUG, false) // // This automation only works with Nest as it toggles non-ST standard home/away // String pName=nModePrefix() String meth="checkNestMode: | " try { if(getIsAutomationDisabled()){ return } if(!nModeScheduleOk()){ LogAction(meth+"Skipping: Schedule Restrictions", sINFO, false) }else{ Long execTime=now() state.autoRunDt=getDtNow() String curStMode=location.mode.toString() Boolean allowNotif=!!((Boolean)settings."${nModePrefix()}NotifOn") Boolean nestModeAway= !(getNestLocPres() == "home") String awayPresDesc=((List)settings.nModePresSensor && !settings.nModeSwitch) ? "All Presence device(s) have left setting " : sBLANK String homePresDesc=((List)settings.nModePresSensor && !settings.nModeSwitch) ? "A Presence Device is Now Present setting " : sBLANK String awaySwitDesc=(settings.nModeSwitch && !(List)settings.nModePresSensor) ? "${settings.nModeSwitch} State is 'Away' setting " : sBLANK String homeSwitDesc=(settings.nModeSwitch && !(List)settings.nModePresSensor) ? "${settings.nModeSwitch} State is 'Home' setting " : sBLANK String modeDesc=((!settings.nModeSwitch && !(List)settings.nModePresSensor) && (List)settings.nModeHomeModes && (List)settings.nModeAwayModes) ? "The ST Mode (${curStMode}) has triggered" : sBLANK String awayDesc=awayPresDesc+awaySwitDesc+modeDesc String homeDesc=homePresDesc+homeSwitDesc+modeDesc Boolean away, home away=false home=false // ERS figure out what state we are in if((List)settings.nModePresSensor && !settings.nModeSwitch){ if(!isPresenceHome((List)settings.nModePresSensor)){ away=true }else{ home=true } }else if(settings.nModeSwitch && !(List)settings.nModePresSensor){ Boolean swOptAwayOn=((String)settings.nModeSwitchOpt == "On") if(swOptAwayOn){ !isSwitchOn(settings.nModeSwitch) ? (home=true) : (away=true) }else{ !isSwitchOn(settings.nModeSwitch) ? (away=true) : (home=true) } }else if((List)settings.nModeHomeModes && (List)settings.nModeAwayModes){ if(isInMode((List)settings.nModeHomeModes)){ home=true }else{ if(isInMode((List)settings.nModeAwayModes)){ away=true } } }else{ LogAction(meth+"Nothing Matched", sINFO, true) } // Track changes that happen outside of nMode // this won't attempt to reset Nest device eco or camera state - you chose to do it outside the automation Boolean NMisEnabled=parent.automationNestModeEnabled(true) Boolean NMecoisEnabled=parent.setNModeActive(null) Boolean t0=(!(Boolean)settings.nModeSetEco) Boolean t1=(home && (!nestModeAway) ) if( (t0 || t1) && NMecoisEnabled){ LogAction(meth+"adjusting manager state NM is not setting eco", sWARN, true) parent.setNModeActive(false) // clear nMode has it in manager } if(t1){ state.nModeTstatLocAway=false } Boolean t2=(away && nestModeAway) if((Boolean)settings.nModeSetEco && t2 && (!NMecoisEnabled)){ LogAction(meth+"adjusting manager state NM will clear eco", sWARN, true) parent.setNModeActive(true) // set nMode has it in manager } if(t2){ state.nModeTstatLocAway=true } Boolean homeChgd, nestModeChgd homeChgd=false nestModeChgd=false if((Boolean)state.nModeLastHome != home){ homeChgd=true LogAction("NestMode Home Changed: ${homeChgd} Home: ${home}", sINFO, false) state.nModeLastHome=home } String t5=getNestLocPres() if((String)state.nModeLastNestMode != t5){ nestModeChgd=true String t6; t6=sINFO if(!homeChgd){ t6=sWARN } LogAction("Nest location mode Changed: ${t5}", t6, true) state.nModeLastNestMode=t5 } Boolean didsomething; didsomething=false // Manage state changes if(away && !nestModeAway){ LogAction(meth+"${awayDesc} Nest 'Away' ${away} ${nestModeAway}", sINFO, false) if(getnModeActionSec() < 4*60){ LogAction(meth+"did change recently - SKIPPING", sWARN, true) scheduleAutomationEval(90) storeExecutionHistory((now() - execTime), "checkNestMode") return } didsomething=true setAway(true) state.nModeLastNestMode="away" state.nModeTstatLocAway=true if((Boolean)settings.nModeSetEco){ parent.setNModeActive(true) // set nMode has it in manager adjustEco(true, pName) } if(allowNotif){ sendEventPushNotifications("${awayDesc} Nest 'Away'", "Info", pName) } if((Boolean)settings.nModeCamOnAway){ adjustCameras(true, pName) } }else if(home && nestModeAway){ LogAction(meth+"${homeDesc} Nest 'Home' ${home} ${nestModeAway}", sINFO, false) if(getnModeActionSec() < 4*60){ LogAction(meth+"did change recently - SKIPPING", sWARN, true) scheduleAutomationEval(90) storeExecutionHistory((now() - execTime), "checkNestMode") return } didsomething=true setAway(false) parent.setNModeActive(false) // clear nMode has it in manager state.nModeLastNestMode="home" state.nModeTstatLocAway=false if((Boolean)settings.nModeSetEco){ adjustEco(false, pName) } if(allowNotif){ sendEventPushNotifications("${homeDesc} Nest 'Home'", "Info", pName) } if((Boolean)settings.nModeCamOffHome){ adjustCameras(false, pName) } } else { LogAction(meth+"No Changes | ${(List)settings.nModePresSensor ? "isPresenceHome: ${isPresenceHome((List)settings.nModePresSensor)} | " : sBLANK}ST-Mode: ($curStMode) | NestModeAway: ($nestModeAway) | Away: ($away) | Home: ($home)", sINFO, false) } if(didsomething){ state.nModeActionDt=getDtNow() scheduleAutomationEval(90) } storeExecutionHistory((now() - execTime), "checkNestMode") } } catch (ex){ log.error "checkNestMode Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "checkNestMode", true, getAutoType()) } } String getNestLocPres(){ if(getIsAutomationDisabled()){ return sNULL } String plocationPresence=parent?.getLocationPresence() if(!plocationPresence){ return sNULL } else { return plocationPresence } } /******************************************************************************** | SCHEDULE, MODE, or MOTION CHANGES ADJUST THERMOSTAT SETPOINTS | | (AND THERMOSTAT MODE) AUTOMATION CODE | *********************************************************************************/ String getTstatAutoDevId(){ if(settings.schMotTstat){ return settings.schMotTstat.deviceNetworkId.toString() } return sNULL } private String tempRangeValues(){ return (getTemperatureScale() == "C") ? "10..32" : "50..90" } private static List timeComparisonOptionValues(){ return ["custom time", "midnight", "sunrise", "noon", "sunset"] } private static List timeDayOfWeekOptions(){ return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] } private String getDayOfWeekName(Date idate=null){ Date date date=idate if(!date){ date=adjustTime() } Integer theDay=date.day List alist=timeDayOfWeekOptions() //LogAction("theDay: $theDay date.date: ${date.day}") return alist[theDay] } /* private getDayOfWeekNumber(date=null){ if(!date){ date=adjustTime(now()) } if(date instanceof Date){ return date.day } switch (date){ case "Sunday": return 0 case "Monday": return 1 case "Tuesday": return 2 case "Wednesday": return 3 case "Thursday": return 4 case "Friday": return 5 case "Saturday": return 6 } return null } */ //adjusts the time to local timezone // TODO HE this may not be right private Date adjustTime(itime=null){ def time; time=itime if(time instanceof String){ //get UTC time time=timeToday(time, location.timeZone).getTime() } if(time instanceof Date){ //get unix time time=time.getTime() } if(!time){ time=now() } if(time){ return new Date(time) // return new Date(time + location.timeZone.getOffset(time)) } return null } private String formatLocalTime(itime, format="EEE, MMM d yyyy @ h:mm a z"){ def time; time=itime if(time instanceof Long){ time=new Date(time) } if(time instanceof String){ //get UTC time time=timeToday(time, location.timeZone) } if(!(time instanceof Date)){ return null } SimpleDateFormat formatter=new SimpleDateFormat(format) formatter.setTimeZone((TimeZone)location.timeZone) return formatter.format(time) } private static convertDateToUnixTime(idate){ def date; date=idate if(!date){ return null } if(!(date instanceof Date)){ date=new Date(date) } //return date.time - location.timeZone.getOffset(date.time) return date.time } /* private convertTimeToUnixTime(time){ if(!time){ return null } return time - location.timeZone.getOffset(time) } */ private String formatTime(time, zone=null){ //we accept both a Date or a settings' Time return formatLocalTime(time, "h:mm a${zone ? " z" : sBLANK}") } private static String formatHour(h){ return (h == 0 ? "midnight" : (h < 12 ? "${h} AM" : (h == 12 ? "noon" : "${h-12} PM"))).toString() } private static Map cleanUpMap(Map map){ List washer; washer=[] //find dirty laundry for (item in map){ if(item.value == null) washer.push((String)item.key) } //clean it for (String item in washer){ map.remove(item) } washer=null return map } private String buildDeviceNameList(List devices, String suffix){ Integer cnt; cnt=1 String result; result=sBLANK for (device in devices){ String label=getDeviceLabel(device) result += label + (cnt < devices.size() ? (cnt == devices.size() - 1 ? " $suffix " : ", ") : sBLANK) cnt++ } if(result == sBLANK){ result=sNULL } return result } private String getDeviceLabel(device){ return device instanceof String ? device : (device ? ( device.label ? device.label : (device.name ? device.name : "$device")) : "Unknown device") } Integer getCurrentSchedule(){ Boolean noSched; noSched=false Integer mySched; mySched=null List schedList=getScheduleList() String res1; res1=sBLANK Integer ccnt; ccnt=1 for (Integer cnt in schedList){ res1=checkRestriction(cnt) if(res1 == null){ break } ccnt += 1 } if(ccnt > schedList?.size()){ noSched=true } else { mySched=ccnt } if(mySched != null){ LogTrace("getCurrentSchedule: mySched: $mySched noSched: $noSched ccnt: $ccnt res1: $res1") } return mySched } private String checkRestriction(Integer cnt){ //LogTrace("checkRestriction:( $cnt )") String sLbl="schMot_${cnt}_" String restriction; restriction=sBLANK Boolean act=settings["${sLbl}SchedActive"] if(act){ Map apprestrict=(Map)state."sched${cnt}restrictions" if(apprestrict && apprestrict.m && apprestrict.m.size() && !(location.mode.toString() in apprestrict.m)){ restriction="a HE MODE mismatch" }else if(apprestrict && apprestrict.w && apprestrict.w.size() && !(getDayOfWeekName() in apprestrict.w)){ restriction="a day of week mismatch" }else if(apprestrict && apprestrict.tf && apprestrict.tt && !(checkTimeCondition(apprestrict?.tf, apprestrict?.tfc, apprestrict?.tfo, apprestrict?.tt, apprestrict?.ttc, apprestrict?.tto))){ restriction="a time of day mismatch" }else{ if((List)settings["${sLbl}rstrctSWOn"]){ for(sw in (List)settings["${sLbl}rstrctSWOn"]){ String aa = sw.currentValue(sSWIT) if(aa != sON){ restriction="switch ${sw} being ${aa}" break } } } if(!restriction && (List)settings["${sLbl}rstrctSWOff"]){ for(sw in (List)settings["${sLbl}rstrctSWOff"]){ String aa = sw.currentValue(sSWIT) if(aa != sOFF){ restriction="switch ${sw} being ${aa}" break } } } if(!restriction && (List)settings["${sLbl}rstrctPHome"] && !isSomebodyHome((List)settings["${sLbl}rstrctPHome"])){ for(pr in (List)settings["${sLbl}rstrctPHome"]){ String aa = pr.currentValue(sPRESENCE) if(aa != sPRESENT){ restriction="presence ${pr} being ${aa}" break } } } if(!restriction && (List)settings["${sLbl}rstrctPAway"] && isSomebodyHome((List)settings["${sLbl}rstrctPAway"])){ for(pr in (List)settings["${sLbl}rstrctPAway"]){ String aa = pr.currentValue(sPRESENCE) if(aa == sPRESENT){ restriction="presence ${pr} being ${aa}" break } } } } LogTrace("checkRestriction:( $cnt ) restriction: $restriction") }else{ restriction="an inactive schedule" } return restriction } public Map getActiveScheduleState(){ return (Map)state.activeSchedData ?: null } Boolean getSchRestrictDoWOk(Integer cnt){ Map apprestrict=(Map)state.activeSchedData Boolean result; result=true apprestrict?.each { sch -> if(sch.key.toInteger() == cnt){ if(!(getDayOfWeekName() in (List)sch.value.w)){ result=false } } } return result } Boolean checkTimeCondition(String timeFrom, String timeFromCustom, Integer itimeFromOffset, String timeTo, String timeToCustom, Integer itimeToOffset){ Integer timeFromOffset; timeFromOffset=itimeFromOffset Integer timeToOffset; timeToOffset=itimeToOffset Date time=adjustTime() //convert to minutes since midnight Integer tc=time.hours * 60 + time.minutes Integer tf,tt,i tf = 0 tt = 0 i=0 while (i < 2){ Date t; t=null Integer h; h=null Integer m switch(i == 0 ? timeFrom : timeTo){ case "custom time": t=adjustTime(i == 0 ? timeFromCustom : timeToCustom) if(i == 0){ timeFromOffset=0 }else{ timeToOffset=0 } break case "sunrise": t=getSunrise() break case "sunset": t=getSunset() break case "noon": h=12 break case "midnight": h=(i == 0 ? 0 : 24) break } if(h != null){ m=0 }else{ h=t.hours m=t.minutes } switch (i){ case 0: tf=h * 60 + m + (Integer)cast(timeFromOffset, "number") break case 1: tt=h * 60 + m + (Integer)cast(timeToOffset, "number") break } i += 1 } //due to offsets, let's make sure all times are within 0-1440 minutes while(tf < 0) tf += 1440 while(tf > 1440) tf -= 1440 while(tt < 0) tt += 1440 while(tt > 1440) tt -= 1440 if(tf < tt){ return (tc >= tf) && (tc < tt) }else{ return (tc < tt) || (tc >= tf) } } private cast(value, String dataType){ List trueStrings=["1", sTRUE, sON, "open", "locked", "active", "wet", "detected", sPRESENT, "occupied", "muted", "sleeping"] List falseStrings=["0", sFALSE, sOFF, "closed", "unlocked", "inactive", "dry", "clear", "not detected", "not present", "not occupied", "unmuted", "not sleeping"] switch (dataType){ case "string": case "text": if(value instanceof Boolean){ return value ? sTRUE : sFALSE } return value ? "$value" : sBLANK case "number": if(value == null) return (Integer) 0 if(value instanceof String){ if(value.isInteger()) return value.toInteger() if(value.isFloat()) return (Integer) Math.floor(value.toFloat()) if(value.toLowerCase() in trueStrings) return (Integer) 1 } Integer result; result=0 try { result=(Integer) value } catch(ignored){ } return result ? result : (Integer) 0 case "long": if(value == null) return 0L if(value instanceof String){ if(value.isInteger()) return (Long)value.toLong() if(value.isFloat()) return (Long)Math.round(value.toFloat()) if(value.toLowerCase() in trueStrings) return 1L } Long result; result=0L try { result=(Long)value } catch(ignored){ } return result ? result : 0L case "decimal": if(value == null) return (float)0.0 if(value instanceof String){ if(value.isFloat()) if(value.isInteger()) return (float)value.toFloat() if(value.toLowerCase() in trueStrings) return (float)1.0 } def result; result=(float) 0 try { result=(float) value } catch(ignored){ } return result ? result : (float) 0 case "boolean": if(value instanceof String){ if(!value || (value.toLowerCase() in falseStrings)) return false return true } return !!value case sTIME: return value instanceof String ? (Long)adjustTime(value).time : (Long)cast(value, "long") case "vector3": return value instanceof String ? (Long)adjustTime(value).time : (Long)cast(value, "long") } return value } @Field static Map svSunTFLD private void initSunriseAndSunset(){ Map t0; t0=svSunTFLD Long t; t=now() if(t0!=null){ if(t<(Long)t0.nextM){ //rtD.sunTimes=[:]+t0 }else{ t0=null; svSunTFLD=null } } if(t0==null){ Map sunTimes=app.getSunriseAndSunset() if(sunTimes.sunrise==null){ log.warn 'Actual sunrise and sunset times are unavailable; please reset the location for your hub' Long t1=timeToday('00:00', location.timeZone).getTime() sunTimes.sunrise=new Date(Math.round(t1+7.0D*3600000.0D)) sunTimes.sunset=new Date(Math.round(t1+19.0D*3600000.0D)) t=0L } t0=[ s: sunTimes, updated: t, nextM: timeTodayAfter('23:59', '00:00', location.timeZone).getTime() ] if(t!=0L){ svSunTFLD=t0 if(eric())log.debug 'updating global sunrise' } } } //TODO is this expensive? private Date getSunrise(){ initSunriseAndSunset() Map sunTimes=(Map)svSunTFLD.s return adjustTime(sunTimes.sunrise) } private Date getSunset(){ initSunriseAndSunset() Map sunTimes=(Map)svSunTFLD.s return adjustTime(sunTimes.sunset) } Boolean isTstatSchedConfigured(){ //return (settings.schMotSetTstatTemp && state.activeSchedData.size()) return !!((Integer)state.scheduleActiveCnt) } /* //NOT IN USE ANYMORE (Maybe we should keep for future use) Boolean isTimeBetween(start, end, now, tz){ Long startDt=Date.parse("E MMM dd HH:mm:ss z yyyy", start).getTime() Long endDt=Date.parse("E MMM dd HH:mm:ss z yyyy", end).getTime() Long nowDt=Date.parse("E MMM dd HH:mm:ss z yyyy", now).getTime() Boolean result=false if(nowDt > startDt && nowDt < endDt){ result=true } //def result=timeOfDayIsBetween(startDt, endDt, nowDt, tz) ? true : false return result } */ Integer getMotionActiveSec(String sLbl){ // String sLbl="schMot_${mySched}_" return !(String)state."${sLbl}MotionActiveDt" ? 0 : GetTimeDiffSeconds((String)state."${sLbl}MotionActiveDt", sNULL, "getMotionActiveSec").toInteger() } Integer getMotionInActiveSec(String sLbl){ // String sLbl="schMot_${mySched}_" return !(String)state."${sLbl}MotionInActiveDt" ? 0 : GetTimeDiffSeconds((String)state."${sLbl}MotionInActiveDt", sNULL, "getMotionInActiveSec").toInteger() } Boolean checkOnMotion(String sLbl, Integer mySched, Boolean motionOn){ LogTrace("checkOnMotion($sLbl, $mySched, $motionOn)") // String sLbl="schMot_${mySched}_" if((List)settings["${sLbl}Motion"] && state."${sLbl}MotionActiveDt"){ // Boolean motionOn=isMotionActive((List)settings["${sLbl}Motion"]) Long lastActiveMotionDt=Date.parse("E MMM dd HH:mm:ss z yyyy", (String)state."${sLbl}MotionActiveDt").getTime() Integer lastActiveMotionSec=getMotionActiveSec(sLbl) Long lastInactiveMotionDt=1L Integer lastInactiveMotionSec if(state."${sLbl}MotionInActiveDt"){ lastInactiveMotionDt=Date.parse("E MMM dd HH:mm:ss z yyyy", (String)state."${sLbl}MotionInActiveDt").getTime() lastInactiveMotionSec=getMotionInActiveSec(sLbl) } LogAction("checkOnMotion: [ActiveDt: ${lastActiveMotionDt} (${lastActiveMotionSec} sec) | InActiveDt: ${lastInactiveMotionDt} (${lastInactiveMotionSec} sec) | MotionOn: ($motionOn)", sINFO, false) Integer ontimedelay=(settings."${sLbl}MDelayValOn"?.toInteger() ?: 60) * 1000 // default to 60s Integer offtimedelay=(settings."${sLbl}MDelayValOff"?.toInteger() ?: 30*60) * 1000 // default to 30 min Long ontimeNum, offtimeNum ontimeNum=lastActiveMotionDt + ontimedelay offtimeNum=lastInactiveMotionDt + offtimedelay Long nowDt=now() // Date.parse("E MMM dd HH:mm:ss z yyyy", getDtNow()).getTime() if(ontimeNum > offtimeNum){ // means motion is on now, so ensure offtime is in future offtimeNum=nowDt + offtimedelay } Long lastOnTime // if we are on now, backup ontime to not oscillate if((Boolean)state."motion${mySched}UseMotionSettings" && (String)state."motion${mySched}TurnedOnDt"){ lastOnTime=Date.parse("E MMM dd HH:mm:ss z yyyy", (String)state."motion${mySched}TurnedOnDt").getTime() if(ontimeNum > lastOnTime){ ontimeNum=lastOnTime - ontimedelay } } String ontime=formatDt( new Date(ontimeNum) ) String offtime=formatDt( new Date(offtimeNum) ) LogAction("checkOnMotion: [ActiveDt: (${state."${sLbl}MotionActiveDt"}) | OnTime: ($ontime) | InActiveDt: (${state."${sLbl}MotionInActiveDt"}) | OffTime: ($offtime)]", sINFO, false) Boolean result; result=false if(nowDt >= ontimeNum && nowDt <= offtimeNum){ result=true } if(nowDt < ontimeNum || (result && !motionOn)){ LogAction("checkOnMotion: (Schedule $mySched - ${getSchedLbl(mySched)}) Scheduling Motion Check (60 sec)", sINFO, false) scheduleAutomationEval(60) } return result } return false } void setTstatTempCheck(){ LogAction("setTstatTempCheck", sDEBUG, false) /* NOTE: // This automation only works with Nest as it checks non-ST presence & thermostat capabilities // Presumes: That all thermostats in an automation are in the same Nest structure, so that all share home/away settings and tStat modes */ try { if(getIsAutomationDisabled()){ return } Long execTime=now() def tstat=settings.schMotTstat def tstatMir=settings.schMotTstatMir String pName=schMotPrefix() String meth="setTstatTempCheck: | " String curMode curMode=tstat ? tstat?.currentThermostatMode?.toString() : sNULL String lastMode=(String)state.schMotlastMode Boolean samemode=lastMode == curMode Integer mySched=getCurrentSchedule() LogAction(meth+"Current Schedule: (${mySched ? ("${mySched} - ${getSchedLbl(mySched)}") : "None Active"})", sDEBUG, false) Boolean noSched= mySched == null Integer previousSched=state.schedLast Boolean samesched=previousSched == mySched if((!samesched || !samemode) && previousSched){ // schedule change - set old schedule to not use motion if((Boolean)state."schedule${previousSched}MotionEnabled") { if((Boolean)state."motion${previousSched}UseMotionSettings"){ LogAction(meth+"Disabled Motion Settings Used for Previous Schedule (${previousSched} - ${getSchedLbl(previousSched)}", sINFO, false) } state."motion${previousSched}UseMotionSettings"=false state."motion${previousSched}LastisBtwn"=false } } if(!samesched || !samemode ){ // schedule change, clear out overrides disableOverrideTemps() } LogAction(meth+"[Current Schedule: (${getSchedLbl(mySched)}) | Previous Schedule: (${previousSched} - ${getSchedLbl(previousSched)}) | None: ($noSched)]", sINFO, false) if(noSched){ LogAction(meth+"Skipping check [No matching Schedule]", sINFO, false) }else{ Boolean samemotion; samemotion = true if((Boolean)state."schedule${mySched}MotionEnabled") { String sLbl = "schMot_${mySched}_" Boolean motionOn = isMotionActive((List)settings["${sLbl}Motion"]) Boolean isBtwn = checkOnMotion(sLbl, mySched, motionOn) Boolean previousBtwn = state."motion${mySched}LastisBtwn" state."motion${mySched}LastisBtwn" = isBtwn if (!isBtwn) { if ((Boolean) state."motion${mySched}UseMotionSettings") { LogAction(meth + "Disabled Use of Motion Settings for Schedule (${mySched} - ${getSchedLbl(mySched)})", sINFO, false) } state."motion${mySched}UseMotionSettings" = false } if (!(Boolean)state."motion${mySched}UseMotionSettings" && isBtwn && !previousBtwn) { // transitioned to use Motion if (motionOn) { // if motion is on use motion now state."motion${mySched}UseMotionSettings" = true state."motion${mySched}TurnedOnDt" = getDtNow() disableOverrideTemps() LogAction(meth + "Enabled Use of Motion Settings for schedule ${mySched}", sINFO, false) } else { state."${sLbl}MotionActiveDt" = null // this will clear isBtwn state."motion${mySched}LastisBtwn" = false LogAction(meth + "Motion Sensors were NOT Active at Transition Time to Motion ON for Schedule (${mySched} - ${getSchedLbl(mySched)})", sINFO, false) } } samemotion = previousBtwn == isBtwn } Boolean schedMatch= (samesched && samemotion) String strv; strv="Using " if(schedMatch){ strv=sBLANK } if((Boolean)state."schedule${mySched}MotionEnabled") { LogAction(meth + "${strv}Schedule ${mySched} (${previousSched}) use Motion settings: ${(Boolean) state."motion${mySched}UseMotionSettings"} | isBtwn: $isBtwn | previousBtwn: $previousBtwn | motionOn $motionOn", sDEBUG, false) } else { LogAction(meth + "${strv}Schedule ${mySched} (${previousSched}) no Motion settings", sDEBUG, false) } if(tstat && !schedMatch){ Map hvacSettings=(Map)state."sched${mySched}restrictions" Boolean useMotion=(Boolean)state."motion${mySched}UseMotionSettings" String newHvacMode newHvacMode=(!useMotion ? hvacSettings?.hvacm : (hvacSettings?.mhvacm ?: hvacSettings?.hvacm)) if(newHvacMode && (newHvacMode != curMode)){ if(newHvacMode == "rtnFromEco"){ if(curMode == sECO){ String t0=tstat?.currentpreviousthermostatMode?.toString() if(t0){ newHvacMode=t0 } }else{ newHvacMode=curMode } LogAction(meth+"New Mode is rtnFromEco; Setting Thermostat Mode to (${strCapitalize(newHvacMode)})", sINFO, false) } if(newHvacMode && (newHvacMode.toString() != curMode)){ if(setTstatMode(settings.schMotTstat, newHvacMode, pName)){ storeLastAction("Set ${tstat} Mode to ${strCapitalize(newHvacMode)}", getDtNow(), pName) LogAction(meth+"Setting ${tstat} Thermostat Mode to (${strCapitalize(newHvacMode)})", sINFO, false) }else{ LogAction(meth+"Error Setting ${tstat} Thermostat Mode to (${strCapitalize(newHvacMode)})", sWARN, true) } if(tstatMir){ if(setMultipleTstatMode(tstatMir, newHvacMode, pName)){ LogAction(meth+"Mirroring (${newHvacMode}) to ${tstatMir}", sINFO, false) } } } } curMode=tstat?.currentThermostatMode?.toString() // if remote sensor is on, let it handle temp changes (above took care of a mode change) if((Boolean)settings.schMotRemoteSensor && isRemSenConfigured()){ state.schedLast=mySched state.schMotlastMode=curMode storeExecutionHistory((now() - execTime), "setTstatTempCheck") return } Boolean isModeOff= (curMode in [sOFF, sECO]) String tstatHvacMode=curMode Double heatTemp, coolTemp heatTemp=null coolTemp=null Boolean needChg; needChg=false if(!isModeOff && state.schMotTstatCanHeat){ Double oldHeat=getTstatSetpoint(tstat, sHEAT) heatTemp=getRemSenHeatSetTemp(curMode) if(heatTemp && oldHeat != heatTemp){ needChg=true LogAction(meth+"Schedule Heat Setpoint '${heatTemp}${tUnitStr()}' on (${tstat}) | Old Setpoint: '${oldHeat}${tUnitStr()}'", sINFO, false) //storeLastAction("Set ${settings.schMotTstat} Heat Setpoint to ${heatTemp}", getDtNow(), pName, tstat) }else{ heatTemp=null } } if(!isModeOff && state.schMotTstatCanCool){ Double oldCool=getTstatSetpoint(tstat, sCOOL) coolTemp=getRemSenCoolSetTemp(curMode) if(coolTemp && oldCool != coolTemp){ needChg=true LogAction(meth+"Schedule Cool Setpoint '${coolTemp}${tUnitStr()}' on (${tstat}) | Old Setpoint: '${oldCool}${tUnitStr()}'", sINFO, false) //storeLastAction("Set ${settings.schMotTstat} Cool Setpoint to ${coolTemp}", getDtNow(), pName, tstat) }else{ coolTemp=null } } if(needChg){ if(setTstatAutoTemps(settings.schMotTstat, coolTemp, heatTemp, pName, tstatMir)){ //LogAction(meth+"[Temp Change | newHvacMode: $newHvacMode | tstatHvacMode: $tstatHvacMode | heatTemp: $heatTemp | coolTemp: $coolTemp ]", sINFO, false) //storeLastAction("Set ${tstat} Cool Setpoint ${coolTemp} Heat Setpoint ${heatTemp}", getDtNow(), pName, tstat) }else{ LogAction(meth+"Thermostat Set ERROR [ newHvacMode: $newHvacMode | tstatHvacMode: $tstatHvacMode | heatTemp: ${heatTemp}${tUnitStr()} | coolTemp: ${coolTemp}${tUnitStr()} ]", sWARN, true) } } } } state.schedLast=mySched state.schMotlastMode=curMode storeExecutionHistory((now() - execTime), "setTstatTempCheck") } catch (ex){ log.error "setTstatTempCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "setTstatTempCheck", true, getAutoType()) } } /******************************************************************************** | MASTER AUTOMATION FOR THERMOSTATS | *********************************************************************************/ static String schMotPrefix(){ return "schMot" } def schMotModePage(){ //Logger("in schmotModePage") //def pName=schMotPrefix() dynamicPage(name: "schMotModePage", title: "Thermostat Automation", uninstall: false, install: true){ def dupTstat def dupTstat1 def dupTstat2 def dupTstat3 Boolean tStatPhys //Logger("in schmotModePage0") String tempScale=getTemperatureScale() String tempScaleStr=tUnitStr() section("Configure Thermostat"){ input name: "schMotTstat", type: "capability.thermostat", title: imgTitle(getAppImg("thermostat_icon.png"), inputTitleStr("Select Thermostat?")), multiple: false, submitOnChange: true, required: true //log.debug "schMotTstat: ${schMotTstat}" def tstat=settings.schMotTstat def tstatMir=settings.schMotTstatMir if(tstat){ //Logger("in schmotModePage1") getTstatCapabilities(tstat, schMotPrefix()) //Logger("in schmotModePage2") Boolean canHeat=state.schMotTstatCanHeat Boolean canCool=state.schMotTstatCanCool tStatPhys=tstat.currentNestType != "virtual" //Logger("in schmotModePage3") String str; str=sBLANK Double reqSenHeatSetPoint=getRemSenHeatSetTemp() Double reqSenCoolSetPoint=getRemSenCoolSetTemp() Double curZoneTemp=getRemoteSenTemp() //Logger("in schmotModePage4") Integer t1=getCurrentSchedule() String tt0= getSchedLbl(t1) String tt1= tt0 ?: "Not Found" String tempSrcStr=(t1 && state.remoteTempSourceStr == "Schedule") ? "Schedule ${t1} (${tt1})" : "(${state.remoteTempSourceStr})" //Logger("in schmotModePage5") str += tempSrcStr ? "Zone Status:\n• Temp Source:${tempSrcStr.length() > 15 ? "\n └" : sBLANK} ${tempSrcStr}" : sBLANK str += curZoneTemp ? "\n• Temperature: (${curZoneTemp}${tempScaleStr})" : sBLANK String hstr; hstr=canHeat ? "H: ${reqSenHeatSetPoint}${tempScaleStr}" : sBLANK String cstr; cstr=canHeat && canCool ? "/" : sBLANK cstr += canCool ? "C: ${reqSenCoolSetPoint}${tempScaleStr}" : sBLANK str += "\n• Setpoints: (${hstr}${cstr})\n" str += "\nThermostat Status:\n• Temperature: (${getDeviceTemp(tstat)}${tempScaleStr})" hstr=canHeat ? "H: ${getTstatSetpoint(tstat, sHEAT)}${tempScaleStr}" : sBLANK cstr=canHeat && canCool ? "/" : sBLANK cstr += canCool ? "C: ${getTstatSetpoint(tstat, sCOOL)}${tempScaleStr}" : sBLANK str += "\n• Setpoints: (${hstr}${cstr})" str += "\n• Mode: (${strCapitalize(tstat.currentThermostatOperatingState)}/${strCapitalize(tstat.currentThermostatMode)})" str += ((Boolean)state.schMotTstatHasFan) ? "\n• FanMode: (${strCapitalize(tstat.currentThermostatFanMode)})" : "\n• No Fan on HVAC system" str += "\n• Presence: (${strCapitalize(getTstatPresence(tstat))})" Map safetyTemps=getSafetyTemps(tstat) str += safetyTemps ? "\n• Safety Temps:\n └ Min: ${safetyTemps.min}${tempScaleStr}/Max: ${safetyTemps.max}${tempScaleStr}" : sBLANK str += "\n• Virtual: (${tstat.currentNestType?.toString() == "virtual" ? "True" : "False"})" paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("${tstat.displayName} Zone Status")), state: (str != sBLANK ? sCOMPLT : null) paragraph sectionTitleStr(str), state: (str != sBLANK ? sCOMPLT : null) //Logger("in schmotModePage6") String t0Str="ERROR:\nThe " String t1Str="Primary Thermostat was found in Mirror Thermostat List.\nPlease Correct to Proceed" if(!tStatPhys){ // if virtual thermostat, check if physical thermostat is in mirror list def mylist=[ deviceNetworkId:"${tstat.deviceNetworkId.toString().replaceFirst("v", sBLANK)}" ] dupTstat1=checkThermostatDupe(mylist, tstatMir) if(dupTstat1){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("${t0Str}${t1Str}")), required: true, state: null } }else{ // if physcial thermostat, see if virtual version is in mirror list def mylist=[ deviceNetworkId:"v${tstat.deviceNetworkId.toString()}" ] dupTstat2=checkThermostatDupe(mylist, tstatMir) if(dupTstat2){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("${t0Str}Virtual version of the ${t1Str}")), required: true, state: null } } dupTstat3=checkThermostatDupe(tstat, tstatMir) // make sure thermostat is not in mirror list if(dupTstat3){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("${t0Str}${t1Str}")), required: true, state: null } dupTstat=dupTstat1 || dupTstat2 || dupTstat3 if(!tStatPhys){ } input "schMotTstatMir", "capability.thermostat", title: imgTitle(getAppImg("thermostat_icon.png"), inputTitleStr("Mirror Changes to these Thermostats")), multiple: true, submitOnChange: true, required: false if(tstatMir && !dupTstat){ tstatMir?.each { t -> paragraph "Thermostat Temp: ${getDeviceTemp(t)}${tempScaleStr}" } } } } if(settings.schMotTstat && !dupTstat){ updateScheduleStateMap() section(){ paragraph paraTitleStr("Choose Automations:"), required: false paragraph sectionTitleStr("The options below allow you to configure a thermostat with automations that will help save energy and maintain comfort"), required: false } section("Schedule Automation:"){ Integer actSch=((Map)state.activeSchedData)?.size() String tDesc=(isTstatSchedConfigured() || ((Map)state.activeSchedData)?.size()) ? "Tap to modify Schedules" : sNULL href "tstatConfigAutoPage1", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Use Schedules to adjust Temp Setpoints and HVAC mode?")), description: (tDesc != null ? tDesc : sBLANK), state: (tDesc != null ? sCOMPLT : sBLANK)//, params: ["configType":"tstatSch"] if(actSch>0){ Map schInfo=getScheduleDesc() if(schInfo?.size()){ Integer curSch=getCurrentSchedule() schInfo?.each { schItem -> Integer schNum=(Integer)schItem?.key String schDesc=schItem?.value?.toString() Boolean schInUse= (curSch == schNum) if(schNum && schDesc){ href "schMotSchedulePage${schNum}", title: sBLANK, description: "${schDesc}\n\nTap to modify this Schedule", state: (schInUse ? sCOMPLT : sBLANK)//, params: ["sNum":schNum] } } } } } String t3Str=" is not available on a VIRTUAL Thermostat" String t4Str="ERROR:\nThe Primary Thermostat is VIRTUAL and UNSUPPORTED for automation.\nPlease Correct to Proceed" section("Fan Control:"){ if(tStatPhys || (Boolean)settings.schMotOperateFan){ String desc=sBLANK String titStr; titStr=sBLANK if((Boolean)state.schMotTstatHasFan){ titStr += "Use HVAC Fan for Circulation\nor\n" } titStr += "Run External Fan while HVAC is Operating" input (name: "schMotOperateFan", type: sBOOL, title: imgTitle(getAppImg("fan_control_icon.png"), inputTitleStr("${titStr}?")), description: desc, required: false, defaultValue: false, submitOnChange: true) String fanCtrlDescStr; fanCtrlDescStr=sBLANK String t0=getFanSwitchDesc() if((Boolean)settings.schMotOperateFan){ fanCtrlDescStr += t0 ? "${t0}" : sBLANK String fanCtrlDesc=isFanCtrlConfigured() ? "${fanCtrlDescStr}" + descriptions("d_ttm") : sNULL href "tstatConfigAutoPage2", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Fan Control Config")), description: fanCtrlDesc ?: "Not Configured", state: (fanCtrlDesc ? sCOMPLT : null), required: true//, params: ["configType":"fanCtrl"] } }else if(!tStatPhys){ paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Fan Control${t3Str}")), state: sCOMPLT } if(!tStatPhys && (Boolean)settings.schMotOperateFan){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("FAN ${t4Str}")), required: true, state: null } } section("Remote Sensor:"){ if(tStatPhys || (Boolean)settings.schMotRemoteSensor){ String desc=sBLANK input (name: "schMotRemoteSensor", type: sBOOL, title: imgTitle(getAppImg("remote_sensor_icon.png"), inputTitleStr("Use Alternate Temp Sensors to Control Zone temperature?")), description: desc, required: false, defaultValue: false, submitOnChange: true) if((Boolean)settings.schMotRemoteSensor){ String remSenDescStr; remSenDescStr=sBLANK remSenDescStr += (String)settings.remSenRuleType ? "Rule-Type: ${getEnumValue(remSenRuleEnum("heatcool"), (String)settings.remSenRuleType)}" : sBLANK remSenDescStr += settings.remSenTempDiffDegrees ? ("\n • Threshold: (${settings.remSenTempDiffDegrees}${tempScaleStr})") : sBLANK remSenDescStr += settings.remSenTstatTempChgVal ? ("\n • Adjust Temp: (${settings.remSenTstatTempChgVal}${tempScaleStr})") : sBLANK String hstr=remSenHeatTempsReq() ? "H: ${fixTempSetting(settings.remSenDayHeatTemp) ?: 0}${tempScaleStr}" : sBLANK String cstr; cstr=remSenHeatTempsReq() && remSenCoolTempsReq() ? "/" : sBLANK cstr += remSenCoolTempsReq() ? "C: ${fixTempSetting(settings.remSenDayCoolTemp) ?: 0}${tempScaleStr}" : sBLANK remSenDescStr += ((List)settings.remSensorDay && (settings.remSenDayHeatTemp || settings.remSenDayCoolTemp)) ? "\n • Default Temps:\n └ (${hstr}${cstr})" : sBLANK remSenDescStr += (settings.vthermostat) ? "\n\nVirtual Thermostat:" : sBLANK remSenDescStr += (settings.vthermostat) ? "\n• Enabled" : sBLANK //remote sensor/Day String dayModeDesc; dayModeDesc=sBLANK dayModeDesc += (List)settings.remSensorDay ? "\n\nDefault Sensor${((List)settings.remSensorDay)?.size() > 1 ? "s" : sBLANK}:" : sBLANK // Integer rCnt=((List)settings.remSensorDay)?.size() ((List)settings.remSensorDay)?.each { t -> dayModeDesc += "\n ├ ${t?.label}: ${(t?.label?.toString()?.length() > 10) ? "\n │ └ " : sBLANK}(${getDeviceTemp(t)}${tempScaleStr})" } dayModeDesc += (List)settings.remSensorDay ? "\n └ Temp${(settings.remSensorDay?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg((List)settings.remSensorDay)}${tempScaleStr})" : sBLANK remSenDescStr += (List)settings.remSensorDay ? "${dayModeDesc}" : sBLANK String remSenDesc=isRemSenConfigured() ? remSenDescStr + descriptions("d_ttm") : sNULL href "tstatConfigAutoPage3", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Remote Sensor Config")), description: remSenDesc ?: "Not Configured", required: true, state: (remSenDesc ? sCOMPLT : null)//, params: ["configType":"remSen"] }else{ if(settings.vthermostat != null){ //ERS settingRemove("vthermostat") removeVstat("automation Selection") } } }else if(!tStatPhys){ paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Remote Sensor${t3Str}")), state: sCOMPLT } if(!tStatPhys && (Boolean)settings.schMotRemoteSensor){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Remote Sensor ${t4Str}")), required: true, state: null } } section("Leak Detection:"){ if(tStatPhys || (Boolean)settings.schMotWaterOff){ String desc=sBLANK input (name: "schMotWaterOff", type: sBOOL, title: imgTitle(getAppImg("leak_icon.png"), inputTitleStr("Turn Off if Water Leak is detected?")), description: desc, required: false, defaultValue: false, submitOnChange: true) if((Boolean)settings.schMotWaterOff){ String leakDesc; leakDesc=sBLANK String t0=leakWatSensorsDesc() leakDesc += (settings.leakWatSensors && t0) ? t0 : sBLANK leakDesc += settings.leakWatSensors ? '\n\n'+autoStateDesc("leakWat") : sBLANK leakDesc += (settings.leakWatSensors) ? "\n\nSettings:" : sBLANK leakDesc += settings.leakWatOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings.leakWatOnDelay)})" : sBLANK //leakDesc += (settings.leakWatModes || settings.leakWatDays || (settings.leakWatStartTime && settings.leakWatStopTime)) ? //"\n • Restrictions Active: (${autoScheduleOk(leakWatPrefix()) ? "NO" : "YES"})" : sBLANK String t1=getNotifConfigDesc(leakWatPrefix()) leakDesc += t1 ? '\n\n'+t1 : sBLANK leakDesc += (settings.leakWatSensors) ? descriptions("d_ttm") : sBLANK String leakWatDesc=isLeakWatConfigured() ? leakDesc : sNULL href "tstatConfigAutoPage4", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Leak Sensor Automation")), description: leakWatDesc ?: descriptions("d_ttc"), required: true, state: (leakWatDesc ? sCOMPLT : null)//, params: ["configType":"leakWat"] } }else if(!tStatPhys){ paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Leak Detection${t3Str}")), state: sCOMPLT } if(!tStatPhys && (Boolean)settings.schMotWaterOff){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Leak ${t4Str}")), required: true, state: null } } section("Contact Automation:"){ if(tStatPhys || (Boolean)settings.schMotContactOff){ String desc=sBLANK input (name: "schMotContactOff", type: sBOOL, title: imgTitle(getAppImg("open_window.png"), inputTitleStr("Set ECO if Door/Window Contact Open?")), description: desc, required: false, defaultValue: false, submitOnChange: true) if((Boolean)settings.schMotContactOff){ String conDesc; conDesc=sBLANK String t0=conWatContactDesc() conDesc += (settings.conWatContacts && t0) ? t0 : sBLANK conDesc += settings.conWatContacts ? '\n\n'+autoStateDesc("conWat") : sBLANK conDesc += settings.conWatContacts ? "\n\nSettings:" : sBLANK conDesc += settings.conWatOffDelay ? "\n • Eco Delay: (${getEnumValue(longTimeSecEnum(), settings.conWatOffDelay)})" : sBLANK conDesc += settings.conWatOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings.conWatOnDelay)})" : sBLANK conDesc += settings.conWatRestoreDelayBetween ? "\n • Delay Between Restores:\n └ (${getEnumValue(longTimeSecEnum(), settings.conWatRestoreDelayBetween)})" : sBLANK conDesc += (settings.conWatContacts) ? "\n • Restrictions Active: (${autoScheduleOk(conWatPrefix()) ? "NO" : "YES"})" : sBLANK String t1=getNotifConfigDesc(conWatPrefix()) conDesc += t1 ? '\n\n'+t1 : sBLANK conDesc += (settings.conWatContacts) ? descriptions("d_ttm") : sBLANK String conWatDesc=isConWatConfigured() ? "${conDesc}" : sNULL href "tstatConfigAutoPage5", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Contact Sensors Config")), description: conWatDesc ?: descriptions("d_ttc"), required: true, state: (conWatDesc ? sCOMPLT : null)//, params: ["configType":"conWat"] } }else if(!tStatPhys){ paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Contact automation${t3Str}")), state: sCOMPLT } if(!tStatPhys && (Boolean)settings.schMotContactOff){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Contact ${t4Str}")), required: true, state: null } } section("Humidity Control:"){ String desc=sBLANK input (name: "schMotHumidityControl", type: sBOOL, title: imgTitle(getAppImg("humidity_automation_icon.png"), inputTitleStr("Turn Humidifier On / Off?")), description: desc, required: false, defaultValue: false, submitOnChange: true) if((Boolean)settings.schMotHumidityControl){ String humDesc; humDesc=sBLANK humDesc += ((List)settings.humCtrlSwitches) ? humCtrlSwitchDesc() : sBLANK humDesc += ((List)settings.humCtrlHumidity) ? "${(List)settings.humCtrlSwitches ? "\n\n" : sBLANK}${humCtrlHumidityDesc()}" : sBLANK humDesc += ((Boolean)settings.humCtrlUseWeather || settings.humCtrlTempSensor) ? "\n\nSettings:" : sBLANK humDesc += (!(Boolean)settings.humCtrlUseWeather && settings.humCtrlTempSensor) ? "\n • Temp Sensor: (${getHumCtrlTemperature()}${tempScaleStr})" : sBLANK humDesc += ((Boolean)settings.humCtrlUseWeather && !settings.humCtrlTempSensor) ? "\n • Weather: (${getHumCtrlTemperature()}${tempScaleStr})" : sBLANK humDesc += ((List)settings.humCtrlSwitches) ? "\n • Restrictions Active: (${autoScheduleOk(humCtrlPrefix()) ? "NO" : "YES"})" : sBLANK //TODO need this in schedule humDesc += ((settings.humCtrlTempSensor || (Boolean)settings.humCtrlUseWeather) ) ? descriptions("d_ttm") : sBLANK String humCtrlDesc=isHumCtrlConfigured() ? "${humDesc}" : sNULL href "tstatConfigAutoPage6", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Humidifer Config")), description: humCtrlDesc ?: descriptions("d_ttc"), required: true, state: (humCtrlDesc ? sCOMPLT : null)//, params: ["configType":"humCtrl"] } } section("External Temp:"){ if(tStatPhys || (Boolean)settings.schMotExternalTempOff){ String desc=sBLANK input (name: "schMotExternalTempOff", type: sBOOL, title: imgTitle(getAppImg("external_temp_icon.png"), inputTitleStr("Set ECO if External Temp is near comfort settings")), description: desc, required: false, defaultValue: false, submitOnChange: true) if((Boolean)settings.schMotExternalTempOff){ String extDesc; extDesc=sBLANK extDesc += ((Boolean)settings.extTmpUseWeather || settings.extTmpTempSensor) ? autoStateDesc("extTmp")+'\n\n' : sBLANK extDesc += ((Boolean)settings.extTmpUseWeather || settings.extTmpTempSensor) ? "Settings:" : sBLANK extDesc += (!(Boolean)settings.extTmpUseWeather && settings.extTmpTempSensor) ? "\n • Sensor: (${getExtTmpTemperature()}${tempScaleStr})" : sBLANK extDesc += ((Boolean)settings.extTmpUseWeather && !settings.extTmpTempSensor) ? "\n • Weather: (${getExtTmpTemperature()}${tempScaleStr})" : sBLANK //TODO need this in schedule extDesc += settings.extTmpDiffVal ? "\n • Outside Threshold: (${settings.extTmpDiffVal}${tempScaleStr})" : sBLANK extDesc += settings.extTmpInsideDiffVal ? "\n • Inside Threshold: (${settings.extTmpInsideDiffVal}${tempScaleStr})" : sBLANK extDesc += settings.extTmpOffDelay ? "\n • ECO Delay: (${getEnumValue(longTimeSecEnum(), settings.extTmpOffDelay)})" : sBLANK extDesc += settings.extTmpOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings.extTmpOnDelay)})" : sBLANK extDesc += (settings.extTmpTempSensor || (Boolean)settings.extTmpUseWeather) ? "\n • Restrictions Active: (${autoScheduleOk(extTmpPrefix()) ? "NO" : "YES"})" : sBLANK String t0=getNotifConfigDesc(extTmpPrefix()) extDesc += t0 ? "\n\n${t0}" : sBLANK extDesc += ((settings.extTmpTempSensor || (Boolean)settings.extTmpUseWeather) ) ? descriptions("d_ttm") : sBLANK String extTmpDesc=isExtTmpConfigured() ? "${extDesc}" : sNULL href "tstatConfigAutoPage7", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("External Temps Config")), description: extTmpDesc ?: descriptions("d_ttc"), required: true, state: (extTmpDesc ? sCOMPLT : null)//, params: ["configType":"extTmp"] } }else if(!tStatPhys){ paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("External Temp Automation${t3Str}")), state: sCOMPLT } if(!tStatPhys && (Boolean)settings.schMotExternalTempOff){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("External Temp ${t4Str}")), required: true, state: null } } section("Settings:"){ input "schMotWaitVal", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Minimum Wait Time between Evaluations?")), required: false, defaultValue: 60, options: [30:"30 Seconds", 60:"60 Seconds",90:"90 Seconds",120:"120 Seconds"] } } /* if(state.showHelp){ section("Help:"){ href url:"${getAutoHelpPageUrl()}", style:"embedded", required:false, title:"Help and Instructions", description:sBLANK, image: getAppImg("info.png") } } */ } } String getSchedLbl(Integer num){ String result; result=sBLANK if(num){ Map schData=(Map)state.activeSchedData schData?.each { sch -> if(num == sch.key.toInteger()){ //log.debug "Label:(${sch?.value?.lbl})" result=sch.value?.lbl } } } return result } /* def getSchedData(num){ if(!num){ return null } def resData=[:] Map schData=(Map)state.activeSchedData schData?.each { sch -> //log.debug "sch: $sch" if(sch?.key != null && num?.toInteger() == sch?.key.toInteger()){ // log.debug "Data:(${sch?.value})" resData=sch?.value } } return resData != [:] ? resData : null }*/ /* NOTE Schedule Rules: You ALWAYS HAVE TEMPS in A SCHEDULE • You ALWAYS OFFER OPTION OF MOTION TEMPS in A SCHEDULE • If Motion is ENABLED, it MUST HAVE MOTION TEMPS • You ALWAYS OFFER RESTRICTION OPTIONS in A SCHEDULE • If REMSEN is ON, you offer remote sensors options */ @SuppressWarnings('unused') def tstatConfigAutoPage1(params){ def t0=[:]; t0.configType="tstatSch"; return tstatConfigAutoPage( t0 ) } @SuppressWarnings('unused') def tstatConfigAutoPage2(params){ def t0=[:]; t0.configType="fanCtrl"; return tstatConfigAutoPage( t0 ) } @SuppressWarnings('unused') def tstatConfigAutoPage3(params){ def t0=[:]; t0.configType="remSen"; return tstatConfigAutoPage( t0 ) } @SuppressWarnings('unused') def tstatConfigAutoPage4(params){ def t0=[:]; t0.configType="leakWat"; return tstatConfigAutoPage( t0 ) } @SuppressWarnings('unused') def tstatConfigAutoPage5(params){ def t0=[:]; t0.configType="conWat"; return tstatConfigAutoPage( t0 ) } @SuppressWarnings('unused') def tstatConfigAutoPage6(params){ def t0=[:]; t0.configType="humCtrl"; return tstatConfigAutoPage( t0 ) } @SuppressWarnings('unused') def tstatConfigAutoPage7(params){ def t0=[:]; t0.configType="extTmp"; return tstatConfigAutoPage( t0 ) } def tstatConfigAutoPage(params){ String configType configType=params?.configType if(params && params.configType){ //Logger("tstatConfigAutoPage got params") state.tempTstatConfigPageData=params; configType=params.configType }else{ configType=state.tempTstatConfigPageData?.configType } String pName, pTitle, pDesc pName=sBLANK pTitle=sBLANK pDesc=sNULL switch(configType){ case "tstatSch": pName=schMotPrefix() pTitle="Thermostat Schedule Automation" pDesc="Configure Schedules and Setpoints" break case "fanCtrl": pName=fanCtrlPrefix() pTitle="Fan Automation" break case "remSen": pName=remSenPrefix() pTitle="Remote Sensor Automation" break case "leakWat": pName=leakWatPrefix() pTitle="Thermostat/Leak Automation" break case "conWat": pName=conWatPrefix() pTitle="Thermostat/Contact Automation" break case "humCtrl": pName=humCtrlPrefix() pTitle="Humidifier Automation" break case "extTmp": pName=extTmpPrefix() pTitle="Thermostat/External Temps Automation" break } dynamicPage(name: "tstatConfigAutoPage", title: pTitle, description: pDesc, uninstall: false){ def tstat=settings.schMotTstat if(tstat){ String tempScale=getTemperatureScale() String tempScaleStr=tUnitStr() String tStatName=(String)tstat.displayName Double tStatHeatSp=getTstatSetpoint(tstat, sHEAT) Double tStatCoolSp=getTstatSetpoint(tstat, sCOOL) //String tStatMode=tstat?.currentThermostatMode String tStatTemp="${getDeviceTemp(tstat)}${tempScaleStr}" Boolean canHeat=state.schMotTstatCanHeat Boolean canCool=state.schMotTstatCanCool //String locMode=location.mode.toString() List hidestr hidestr=["fanCtrl"] // fan schedule is turned off if(!(Boolean)settings.schMotRemoteSensor){ // no remote sensors requested or used hidestr=["fanCtrl", "remSen"] } def params1= ["sData":["hideStr":hidestr]] state.t_tempSData=params1 // if(!(Boolean)settings.schMotOperateFan){ } //if(!settings.schMotSetTstatTemp){ //motSen means no motion sensors offered restrict means no restrictions offered tstatTemp says no tstat temps offered //"tstatTemp", "motSen", "restrict" //} // if(!settings.schMotExternalTempOff){ } if(configType == "tstatSch"){ section(){ String str; str=sBLANK str += "• Temperature: (${tStatTemp})" str += "\n• Setpoints: (H: ${canHeat ? "${tStatHeatSp}${tempScaleStr}" : "NA"} / C: ${canCool ? "${tStatCoolSp}${tempScaleStr}" : "NA"})" paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("${tStatName}\nSchedules and Setpoints:")), state: sCOMPLT paragraph sectionTitleStr("${str}"), state: sCOMPLT } showUpdateSchedule(null, hidestr) } if(configType == "fanCtrl"){ Boolean reqinp=!(settings["schMotCirculateTstatFan"] || settings["${pName}FanSwitches"]) section("Control Fans/Switches based on Thermostat\n(3-Speed Fans Supported)"){ input "${pName}FanSwitches", "capability.switch", title: imgTitle(getAppImg("fan_ventilation_icon.png"), inputTitleStr("Select Fan Switches?")), required: reqinp, submitOnChange: true, multiple: true if(settings."${pName}FanSwitches"){ String t0=getFanSwitchDesc(false) paragraph paraTitleStr(t0), state: t0 ? sCOMPLT : null } } if(settings["${pName}FanSwitches"]){ section("Fan Event Triggers"){ paragraph "Triggers are evaluated when Thermostat sends an operating event. Poll time may take 1 minute or more for fan to switch on.", title: "What are these triggers?", image: getAppImg("i_inst") input "${pName}FanSwitchTriggerType", sENUM, title: imgTitle(getAppImg("${settings."${pName}FanSwitchTriggerType" == 1 ? sTHERM : "home_fan"}_icon.png"), inputTitleStr("Control Switches When?")), defaultValue: 1, options: switchRunEnum(), submitOnChange: true input "${pName}FanSwitchHvacModeFilter", sENUM, title: imgTitle(getAppImg("i_mod"), inputTitleStr("Thermostat Mode Triggers?")), defaultValue: "any", options: fanModeTrigEnum(), submitOnChange: true, multiple: true } if(getFanSwitchesSpdChk()){ section("Fan Speed Options"){ input "${pName}FanSwitchSpeedCtrl", sBOOL, title: imgTitle(getAppImg("speed_knob_icon.png"), inputTitleStr("Enable Speed Control?")), defaultValue: true, submitOnChange: true if(settings["${pName}FanSwitchSpeedCtrl"]){ paragraph paraTitleStr("What do these values mean?") paragraph sectionTitleStr("These threshold settings allow you to configure the speed of the fan based on it's closeness to the desired temp") input "${pName}FanSwitchLowSpeed", "decimal", title: imgTitle(getAppImg("fan_low_speed.png"), inputTitleStr( "Low Speed Threshold (${tempScaleStr})")), required: true, defaultValue: 1.0, submitOnChange: true input "${pName}FanSwitchMedSpeed", "decimal", title: imgTitle(getAppImg("fan_med_speed.png"), inputTitleStr("Medium Speed Threshold (${tempScaleStr})")), required: true, defaultValue: 2.0, submitOnChange: true input "${pName}FanSwitchHighSpeed", "decimal", title: imgTitle(getAppImg("fan_high_speed.png"), inputTitleStr("High Speed Threshold (${tempScaleStr})")), required: true, defaultValue: 4.0, submitOnChange: true } } } } if((Boolean)state.schMotTstatHasFan || settings["${pName}FanSwitches"]){ // ERS allow for external fans also? section("Fan Circulation:"){ String desc=sBLANK if((Boolean)state.schMotTstatHasFan){ input (name: "schMotCirculateTstatFan", type: sBOOL, title: imgTitle(getAppImg("fan_circulation_icon.png"), inputTitleStr("Run HVAC Fan for Circulation?")), description: desc, required: reqinp, defaultValue: false, submitOnChange: true) } if((List)settings["${pName}FanSwitches"]){ input (name: "schMotCirculateExtFan", type: sBOOL, title: imgTitle(getAppImg("fan_circulation_icon.png"), inputTitleStr("Run External Fan for Circulation?")), description: desc, required: reqinp, defaultValue: false, submitOnChange: true) }else{ settingRemove("schMotCirculateExtFan") } if((Boolean)settings.schMotCirculateTstatFan || (Boolean)settings.schMotCirculateExtFan){ input("schMotFanRuleType", sENUM, title: imgTitle(getAppImg("rule_icon.png"), inputTitleStr("(Rule) Action Type")), options: remSenRuleEnum("fan"), required: true) paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What is the Action Threshold Temp?")) paragraph sectionTitleStr("Temp difference to trigger Action Type.") def adjust=(getTemperatureScale() == "C") ? 0.5 : 1.0 input "fanCtrlTempDiffDegrees", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Action Threshold Temp (${tempScaleStr})")), required: true, defaultValue: adjust input name: "fanCtrlOnTime", type: sENUM, title: imgTitle(getAppImg("timer_icon.png"), inputTitleStr("Minimum circulate Time\n(Optional)")), defaultValue: 240, options: fanTimeSecEnum(), required: true, submitOnChange: true input "fanCtrlTimeBetweenRuns", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Between On/Off Cycles\n(Optional)")), required: true, defaultValue: 1200, options: longTimeSecEnum(), submitOnChange: true } } } section(getDmtSectionDesc(fanCtrlPrefix())){ String pageDesc=getDayModeTimeDesc(pName) href "setDayModeTimePage2", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName": "${pName}"] } if(settings."${pName}FanSwitches"){ String schTitle if(!((Map)state.activeSchedData)?.size()){ schTitle="Optionally create schedules to set temperatures based on schedule" }else{ schTitle="Temperature settings based on schedule" } section(schTitle){ // FANS USE TEMPS IN LOGIC href "scheduleConfigPage", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Enable/Modify Schedules")), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["sData":["hideStr":"${hidestr}"]] } } } Boolean cannotLock; cannotLock=null Double defHeat Double defCool Double curTemp if(!getMyLockId()){ setMyLockId(app.id) } //ERS // this deals with changing the tstat on the automation if(state.remSenTstat){ if(tstat.deviceNetworkId != state.remSenTstat){ removeVstat("settings pages") } } if((Boolean)settings.schMotRemoteSensor){ if(parent.remSenLock(tstat?.deviceNetworkId, getMyLockId()) ){ // lock new ID state.remSenTstat=tstat?.deviceNetworkId cannotLock=false }else{ cannotLock=true } } if(configType == "remSen"){ if(cannotLock){ section(sBLANK){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Cannot Lock thermostat for remote sensor - thermostat may already be in use. Please Correct")), required: true, state: null } settingRemove("vthermostat") } if(!cannotLock){ section("Select the Allowed (Rule) Action Type:"){ if(!(String)settings.remSenRuleType){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What are Rule Actions?")) paragraph sectionTitleStr("They determine the actions taken when the temperature threshold is reached, to balance temperatures") } input(name: "remSenRuleType", type: sENUM, title: imgTitle(getAppImg("rule_icon.png"), inputTitleStr("(Rule) Action Type")), options: remSenRuleEnum("heatcool"), required: true, submitOnChange: true) } if((String)settings.remSenRuleType){ String senLblStr="Default" section("Choose Temperature Sensor(s) to use:"){ Boolean daySenReq= !(List)settings.remSensorDay input "remSensorDay", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("${senLblStr} Temp Sensor(s)")), submitOnChange: true, required: daySenReq, multiple: true if((List)settings.remSensorDay){ curTemp=getDeviceTempAvg((List)settings.remSensorDay) String tt0=((List)settings.remSensorDay)?.size() > 1 ? " (avg):" : ":" String tmpVal="Temp${tt0} (${curTemp} ${tempScaleStr})" //String tmpVal="Temp${(settings.remSensorDay?.size() > 1) ? " (avg):" : ":"} (${curTemp}${tempScaleStr})" if(((List)settings.remSensorDay).size() > 1){ href "remSenShowTempsPage", title: inputTitleStr("View ${senLblStr} Sensor Temps"), description: tmpVal, state: sCOMPLT paragraph imgTitle(getAppImg("i_icon.png"), paraTitleStr("Multiple temp sensors will return the average of those sensors.")) }else{ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Remote Sensor Temp")), state: sCOMPLT paragraph sectionTitleStr(tmpVal), state: sCOMPLT } } } if((List)settings.remSensorDay){ section("Desired Setpoints"){ paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("What are these temps for?")) paragraph sectionTitleStr("These temps are used when remote sensors are enabled and no schedules are created or active") if(isTstatSchedConfigured()){ // if(settings.schMotSetTstatTemp){ paragraph "If schedules are enabled and that schedule is in use it's setpoints will take precendence over the setpoints below", required: true, state: null } String tempStr="Default " if(remSenHeatTempsReq()){ defHeat=fixTempSetting(getGlobalDesiredHeatTemp()) defHeat=defHeat ?: (tStatHeatSp ?: curTemp-1.0D) input "remSenDayHeatTemp", "decimal", title: imgTitle(getAppImg("heat_icon.png"), inputTitleStr("Desired ${tempStr}Heat Temp (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", range: tempRangeValues(), required: true, defaultValue: defHeat } if(remSenCoolTempsReq()){ defCool=fixTempSetting(getGlobalDesiredCoolTemp()) defCool=defCool ?: (tStatCoolSp ?: curTemp+1.0D) input "remSenDayCoolTemp", "decimal", title: imgTitle(getAppImg("cool_icon.png"), inputTitleStr("Desired ${tempStr}Cool Temp (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", range: tempRangeValues(), required: true, defaultValue: defCool } } section("Remote Sensor Settings"){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What is the Action Threshold Temp?")) paragraph sectionTitleStr("Temp difference to trigger Actions.") input "remSenTempDiffDegrees", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Action Threshold Temp (${tempScaleStr})")), required: true, defaultValue: 2.0 if((String)settings.remSenRuleType != "Circ"){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What are Temp Increments?")) paragraph sectionTitleStr("Is the amount the thermostat temp is adjusted +/- to turn on/off the HVAC system.") input "remSenTstatTempChgVal", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Change Temp Increments (${tempScaleStr})")), required: true, defaultValue: 5.0 } } section("(Optional) Create a Virtual Nest Thermostat:"){ input(name: "vthermostat", type: sBOOL, title: imgTitle(getAppImg("thermostat_icon.png"), inputTitleStr("Create Virtual Nest Thermostat")), required: false, submitOnChange: true) if(settings.vthermostat!=null && !parent.addRemoveVthermostat((String)tstat.deviceNetworkId, settings.vthermostat, getMyLockId())){ paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Unable to ${(settings.vthermostat ? "enable" : "disable")} Virtual Thermostat!. Please Correct")) } } String schTitle if(!((Map)state.activeSchedData)?.size()){ schTitle="Optionally create schedules to set temperatures, alternate sensors based on schedule" }else{ schTitle="Temperature settings and optionally alternate sensors based on schedule" } section(schTitle){ href "scheduleConfigPage", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Enable/Modify Schedules")), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["sData":["hideStr":"${hidestr}"]] } } } } } if(configType == "leakWat"){ section("When Leak is Detected, Turn Off this Thermostat"){ Boolean req=(settings.leakWatSensors || settings.schMotTstat) input name: "leakWatSensors", type: "capability.waterSensor", title: imgTitle(getAppImg("water_icon.png"), inputTitleStr("Which Leak Sensor(s)?")), multiple: true, submitOnChange: true, required: req if(settings.leakWatSensors){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr(leakWatSensorsDesc())), state: sCOMPLT } } if(settings.leakWatSensors){ section("Restore On when Dry:"){ input "${pName}OnDelay", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dr"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true // Delay Restore } section(sectionTitleStr(titles("t_nt"))){ String t0=getNotifConfigDesc(pName) String pageDesc=t0 ? t0 + descriptions("d_ttm") : sBLANK href "setNotificationPage3", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] } } } if(configType == "conWat"){ section("When these Contacts are open, Set this Thermostat to ECO"){ Boolean req=!settings.conWatContacts input name: "conWatContacts", type: "capability.contactSensor", title: imgTitle(getAppImg("contact_icon.png"), inputTitleStr("Which Contact(s)?")), multiple: true, submitOnChange: true, required: req if(settings.conWatContacts){ String str; str=sBLANK str += settings.conWatContacts ? conWatContactDesc()+'\n' : sBLANK paragraph imgTitle(getAppImg("i_inst"), paraTitleStr(str)), state: (str != sBLANK ? sCOMPLT : null) } } if(settings.conWatContacts){ section("Delay Values:"){ input "${pName}OffDelay", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dtse"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true // Delay to set ECO input "${pName}OnDelay", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dr"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true input "conWatRestoreDelayBetween", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Between On/Eco Cycles\n(Optional)")), required: false, defaultValue: 600, options: longTimeSecEnum(), submitOnChange: true } section("Restoration Preferences:"){ input "${pName}OffTimeout", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Auto Restore after (Optional)")), defaultValue: 0, options: longTimeSecEnum(), required: false, submitOnChange: true if(!settings."${pName}OffTimeout"){ state."${pName}TimeoutScheduled"=false } } section(getDmtSectionDesc(conWatPrefix())){ String pageDesc=getDayModeTimeDesc(pName) href "setDayModeTimePage3", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName": "${pName}"] } section(sectionTitleStr(titles("t_nt"))){ String t0=getNotifConfigDesc(pName) String pageDesc=t0 ? t0 + descriptions("d_ttm") : sBLANK href "setNotificationPage4", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] } } } if(configType == "humCtrl"){ section("Switch for Humidifier"){ def reqinp=!((List)settings.humCtrlSwitches) // TODO needs new icon input "humCtrlSwitches", "capability.switch", title: imgTitle(getAppImg("fan_ventilation_icon.png"), inputTitleStr("Select Switches?")), required: reqinp, submitOnChange: true, multiple: true /* TODO this does not work... */ List t00=(List)settings.humCtrlSwitches List t01=t00?.collect { it?.id } List t1=(List)state.oldhumCtrlSwitches List t2=t1?.collect { it?.id } if(t2?.sort(false) != t01?.sort(false)){ state.haveRunHumidifier=false if(t00){ humCtrlSwitches*.off() } if(t1){ t1*.off() } state.oldhumCtrlSwitches=t00 LogAction("humCtrl: found different settings of humCtrlSwitches; turned all off", sWARN) } if(t00){ String t0=humCtrlSwitchDesc(false) paragraph paraTitleStr("${t0}"), state: t0 ? sCOMPLT : null } } if((List)settings.humCtrlSwitches){ section("Humidifier Triggers"){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr( "What are these triggers?")) paragraph sectionTitleStr("Triggers are evaluated when Thermostat sends an operating event. Poll time may take 1 minute or more for fan to switch on.") // TODO needs to fix icon input "humCtrlSwitchTriggerType", sENUM, title: imgTitle(getAppImg("${settings.humCtrlSwitchTriggerType == 1 ? sTHERM : "home_fan"}_icon.png"), inputTitleStr("Control Switches When?")), defaultValue: "5", options: switchRunEnum(true), submitOnChange: true input "humCtrlSwitchHvacModeFilter", sENUM, title: imgTitle(getAppImg("i_mod"), inputTitleStr("Thermostat Mode Triggers?")), defaultValue: "any", options: fanModeTrigEnum(), submitOnChange: true, multiple: true } section("Indoor Humidity Measurement"){ Boolean req= !(List)settings.humCtrlHumidity input name: "humCtrlHumidity", type: "capability.relativeHumidityMeasurement", title: imgTitle(getAppImg("humidity_icon.png"), inputTitleStr("Which Humidity Sensor(s)?")), multiple: true, submitOnChange: true, required: req if((List)settings.humCtrlHumidity){ String str; str=sBLANK str += "${humCtrlHumidityDesc()}\n" paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != sBLANK ? sCOMPLT : null) } } section("Select the External Temp Sensor to Use:"){ if(!parent.getSettingVal("weatherDevice")){ paragraph "Please Enable the Weather Device under the Manager App before trying to use External Weather as the External Temperature Sensor!", required: true, state: null }else{ if(!settings.humCtrlTempSensor){ input "humCtrlUseWeather", sBOOL, title: imgTitle(getAppImg("weather_icon.png"), inputTitleStr("Use Local Weather as External Sensor?")), required: false, defaultValue: false, submitOnChange: true //state.needWeathUpd=true if((Boolean)settings.humCtrlUseWeather){ if(state.curWeather == null){ getExtConditions() } def tmpVal=(tempScale == "C") ? state.curWeaTemp_c : state.curWeaTemp_f paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Local Weather:\n• ${state.curWeatherLoc}\n• Temp: (${tmpVal}${tempScaleStr})")), state: sCOMPLT } } } if(!(Boolean)settings.humCtrlUseWeather){ state.curWeather=null // force refresh of weather if toggled Boolean senReq= (!(Boolean)settings.humCtrlUseWeather && !settings.humCtrlTempSensor) input "humCtrlTempSensor", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("Select a Temp Sensor?")), submitOnChange: true, multiple: false, required: senReq if(settings.humCtrlTempSensor){ String str; str=sBLANK str += "Sensor Status:" str += "\n└ Temp: (${settings.humCtrlTempSensor?.currentTemperature}${tempScaleStr})" paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != sBLANK ? sCOMPLT : null) } } } section(getDmtSectionDesc(humCtrlPrefix())){ String pageDesc=getDayModeTimeDesc(pName) href "setDayModeTimePage4", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName": "${pName}"] } } } if(configType == "extTmp"){ section("Select the External Temps to Use:"){ if(!parent.getSettingVal("weatherDevice")){ paragraph "Please Enable the Weather Device under the Manager App before trying to use External Weather as an External Sensor!", required: true, state: null }else{ if(!settings.extTmpTempSensor){ input "extTmpUseWeather", sBOOL, title: imgTitle(getAppImg("weather_icon.png"), inputTitleStr("Use Local Weather as External Sensor?")), required: false, defaultValue: false, submitOnChange: true //state.needWeathUpd=true if((Boolean)settings.extTmpUseWeather){ if(state.curWeather == null){ getExtConditions() } def tmpVal=(tempScale == "C") ? state.curWeaTemp_c : state.curWeaTemp_f Double curDp=getExtTmpDewPoint() paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Local Weather:")), state: sCOMPLT paragraph sectionTitleStr("• ${state.curWeatherLoc}\n• Temp: (${tmpVal}${tempScaleStr})\n• Dewpoint: (${curDp}${tempScaleStr})"), state: sCOMPLT } } } if(!(Boolean)settings.extTmpUseWeather){ state.curWeather=null // force refresh of weather if toggled Boolean senReq=(!(Boolean)settings.extTmpUseWeather && !settings.extTmpTempSensor) input "extTmpTempSensor", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("Select a Temp Sensor?")), submitOnChange: true, multiple: false, required: senReq if(settings.extTmpTempSensor){ String str; str=sBLANK str += "Sensor Status:" str += "\n└ Temp: (${settings.extTmpTempSensor?.currentTemperature}${tempScaleStr})" paragraph imgTitle(getAppImg("i_inst"), paraTitleStr(str)), state: (str != sBLANK ? sCOMPLT : null) } } } if((Boolean)settings.extTmpUseWeather || settings.extTmpTempSensor){ section("When the threshold Temps are Reached\nSet the Thermostat to ECO"){ input name: "extTmpDiffVal", type: "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("When desired and external temp difference is at least this many degrees (${tempScaleStr})?")), defaultValue: 1.0, submitOnChange: true, required: true input name: "extTmpInsideDiffVal", type: "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("AND When desired and internal temp difference is within this many degrees (${tempScaleStr})?")), defaultValue: getTemperatureScale() == "C" ? 2.0 : 4.0, submitOnChange: true, required: true } section("Delay Values:"){ input "${pName}OffDelay", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dtse"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true // Delay to set eco input "${pName}OnDelay", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dr"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true } section("Restoration Preferences:"){ input "${pName}OffTimeout", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Auto Restore after (Optional)")), defaultValue: 0, options: longTimeSecEnum(), required: false, submitOnChange: true if(!settings."${pName}OffTimeout"){ state."${pName}TimeoutScheduled"=false } } section(getDmtSectionDesc(extTmpPrefix())){ String pageDesc=getDayModeTimeDesc(pName) href "setDayModeTimePage5", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//,params: ["pName": "${pName}"] } section(sectionTitleStr(titles("t_nt"))){ String t0=getNotifConfigDesc(pName) String pageDesc=t0 ? t0 + descriptions("d_ttm") : sBLANK href "setNotificationPage5", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] } String schTitle if(!((Map)state.activeSchedData)?.size()){ schTitle="Optionally create schedules to set temperatures based on schedule" }else{ schTitle="Temperature settings based on schedule" } section(schTitle){ // EXTERNAL TEMPERATURE has TEMP Setting href "scheduleConfigPage", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Enable/Modify Schedules")), description: pageDesc, state: (pageDesc ? sCOMPLT : null)//, params: ["sData":["hideStr":"${hidestr}"]] } } } } } } @SuppressWarnings('unused') def scheduleConfigPage(params){ //LogTrace("scheduleConfigPage ($params)") def sData; sData=params?.sData if(params && params.sData){ state.t_tempSData=params sData=params.sData }else{ sData=state.t_tempSData?.sData } dynamicPage(name: "scheduleConfigPage", title: "Thermostat Schedule Page", description: "Configure/View Schedules", uninstall: false){ if(settings.schMotTstat){ def tstat=settings.schMotTstat Boolean canHeat=(Boolean)state.schMotTstatCanHeat Boolean canCool=(Boolean)state.schMotTstatCanCool String str; str=sBLANK Double reqSenHeatSetPoint=getRemSenHeatSetTemp() Double reqSenCoolSetPoint=getRemSenCoolSetTemp() Double curZoneTemp=getRemoteSenTemp() String tempSrcStr=(String)state.remoteTempSourceStr String tempScaleStr=tUnitStr() section(){ str += "Zone Status:\n• Temp Source: (${tempSrcStr})\n• Temperature: (${curZoneTemp}${tempScaleStr})" String hstr, cstr hstr=canHeat ? "H: ${reqSenHeatSetPoint}${tempScaleStr}" : sBLANK cstr=canHeat && canCool ? "/" : sBLANK cstr += canCool ? "C: ${reqSenCoolSetPoint}${tempScaleStr}" : sBLANK str += "\n• Setpoints: (${hstr}${cstr})\n" str += "\nThermostat Status:\n• Temperature: (${getDeviceTemp(tstat)}${tempScaleStr})" String tt0=tstat ? "${strCapitalize(tstat?.currentThermostatOperatingState)}/${strCapitalize(tstat?.currentThermostatMode)}" : "unknown" str += "\n• Mode: (${tt0})" hstr=canHeat ? "H: ${getTstatSetpoint(tstat, sHEAT)}${tempScaleStr}" : sBLANK cstr=canHeat && canCool ? "/" : sBLANK cstr += canCool ? "C: ${getTstatSetpoint(tstat, sCOOL)}${tempScaleStr}" : sBLANK str += "\n• Setpoints: (${hstr}${cstr})" str += ((Boolean)state.schMotTstatHasFan) ? "\n• FanMode: (${strCapitalize(tstat?.currentThermostatFanMode)})" : "\n• No Fan on HVAC system" str += "\n• Presence: (${strCapitalize(getTstatPresence(tstat))})" paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("${tstat?.displayName}\nSchedules and Setpoints:")), state: sCOMPLT paragraph sectionTitleStr("${str}"), state: sCOMPLT } showUpdateSchedule(null,(List)sData?.hideStr) } } } //ERS @SuppressWarnings('unused') def schMotSchedulePage1(params){ def t0=[:]; t0.sNum=1; return schMotSchedulePage( t0 ) } @SuppressWarnings('unused') def schMotSchedulePage2(params){ def t0=[:]; t0.sNum=2; return schMotSchedulePage( t0 ) } @SuppressWarnings('unused') def schMotSchedulePage3(params){ def t0=[:]; t0.sNum=3; return schMotSchedulePage( t0 ) } @SuppressWarnings('unused') def schMotSchedulePage4(params){ def t0=[:]; t0.sNum=4; return schMotSchedulePage( t0 ) } @SuppressWarnings('unused') def schMotSchedulePage5(params){ def t0=[:]; t0.sNum=5; return schMotSchedulePage( t0 ) } @SuppressWarnings('unused') def schMotSchedulePage6(params){ def t0=[:]; t0.sNum=6; return schMotSchedulePage( t0 ) } @SuppressWarnings('unused') def schMotSchedulePage7(params){ def t0=[:]; t0.sNum=7; return schMotSchedulePage( t0 ) } @SuppressWarnings('unused') def schMotSchedulePage8(params){ def t0=[:]; t0.sNum=8; return schMotSchedulePage( t0 ) } def schMotSchedulePage(params){ //LogTrace("schMotSchedulePage($params)") Integer sNum; sNum=params?.sNum if(params?.sNum){ state.t_schedData=params sNum=params?.sNum }else{ sNum=state.t_schedData?.sNum } dynamicPage(name: "schMotSchedulePage", title: "Edit Schedule Page", description: "Modify Schedules", uninstall: false){ if(sNum){ showUpdateSchedule(sNum) } } } List getScheduleList(){ def cnt=null // parent ? parent?.state?.appData?.settings.schedules?.count : null Integer maxCnt maxCnt=cnt ? cnt.toInteger() : 8 maxCnt=Math.min( Math.max(maxCnt,4), 8) if(maxCnt < state.scheduleList?.size()){ maxCnt=state.scheduleList?.size() LogAction("A schedule size issue has occurred. The configured schedule size is smaller than the previous configuration restoring previous schedule size.", sWARN, true) } List list=1..maxCnt state.scheduleList=list return list } def showUpdateSchedule(Integer sNum=null, List hideStr=null){ updateScheduleStateMap() List schedList=getScheduleList() // setting in initAutoApp adjust # of schedule slots Boolean lact Boolean act; act=true String sLbl schedList?.each { Integer scd -> sLbl="schMot_${scd}_" if(sNum != null){ if(sNum == scd){ lact=act act=settings["${sLbl}SchedActive"] String schName=settings["${sLbl}name"] editSchedule("secData":["scd":scd, "schName":schName, "hideable":(sNum ? false : true), "hidden": (act || (!act && scd == 1)), "hideStr":hideStr]) } }else{ lact=act act=settings["${sLbl}SchedActive"] if(lact || act){ String schName=settings["${sLbl}name"] editSchedule("secData":["scd":scd, "schName":schName, "hideable":true, "hidden": (act || (!act && scd == 1)), "hideStr":hideStr]) } } } } def editSchedule(Map schedData){ Integer cnt=schedData?.secData?.scd LogTrace("editSchedule (${schedData?.secData})") String sLbl="schMot_${cnt}_" Boolean canHeat=state.schMotTstatCanHeat Boolean canCool=state.schMotTstatCanCool String tempScaleStr=tUnitStr() Boolean act=settings["${sLbl}SchedActive"] String actIcon=act ? "active" : "inactive" List hideStr=schedData?.secData?.hideStr String sectStr=schedData?.secData?.schName ? (act ? "Enabled" : "Disabled") : "Tap to Enable" String titleStr="Schedule ${schedData?.secData?.scd} (${sectStr})" section(title: "${titleStr} "){//, hideable:schedData?.secData?.hideable, hidden: schedData?.secData?.hidden) { input "${sLbl}SchedActive", sBOOL, title: imgTitle(getAppImg("${actIcon}_icon.png"), inputTitleStr("Schedule Enabled")), description: ( (cnt == 1 && !act) ? "Enable to Edit Schedule" : sNULL), required: true, defaultValue: false, submitOnChange: true if(act){ input "${sLbl}name", "text", title: imgTitle(getAppImg("name_tag_icon.png"), inputTitleStr("Schedule Name")), required: true, defaultValue: "Schedule ${cnt}", multiple: false, submitOnChange: true } } if(act){ section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Setpoint Configuration: ", hideable: true, hidden: (settings["${sLbl}HeatTemp"] != null && settings["${sLbl}CoolTemp"] != null) ){ paragraph paraTitleStr("Setpoints and Mode") paragraph sectionTitleStr("Configure Setpoints and HVAC modes that will be set when this Schedule is in use") if(canHeat){ input "${sLbl}HeatTemp", "decimal", title: imgTitle(getAppImg("heat_icon.png"), inputTitleStr("Heat Set Point (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(), submitOnChange: true } if(canCool){ input "${sLbl}CoolTemp", "decimal", title: imgTitle(getAppImg("cool_icon.png"), inputTitleStr("Cool Set Point (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(), submitOnChange: true } input "${sLbl}HvacMode", sENUM, title: imgTitle(getAppImg("i_hmod"), inputTitleStr("Set Hvac Mode (Optional):")), required: false, description: "No change set", options: tModeHvacEnum(canHeat,canCool, true), multiple: false } if((Boolean)settings.schMotRemoteSensor && !("remSen" in hideStr)){ section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Remote Sensor Options: ", hideable: true, hidden: (!(List)settings["${sLbl}remSensor"] && settings["${sLbl}remSenThreshold"] == null)){ paragraph paraTitleStr("Alternate Remote Sensors\n(Optional)") paragraph sectionTitleStr("Configure alternate Remote Temp sensors that are active with this schedule") input "${sLbl}remSensor", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("Alternate Temp Sensors")), description: "For Remote Sensor Automation", submitOnChange: true, required: false, multiple: true if((List)settings."${sLbl}remSensor"){ String tt0=((List)settings["${sLbl}remSensor"])?.size() > 1 ? " (avg):" : ":" def t1=getDeviceTempAvg((List)settings["${sLbl}remSensor"]) String tmpVal="Temp${tt0} (${t1} ${tempScaleStr})" paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${tmpVal}")), state: sCOMPLT } paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Alternate Action Threshold Temp\n(Optional)?")) paragraph sectionTitleStr("Temp difference to trigger HVAC operations used with this schedule") input "${sLbl}remSenThreshold", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Action Threshold Temp (${tempScaleStr})")), required: false, defaultValue: 2.0 } } section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Motion Sensor Setpoints: ", hideable: true, hidden:((List)settings["${sLbl}Motion"] == null) ){ paragraph paraTitleStr("Optional") paragraph sectionTitleStr("Activate alternate HVAC settings with Motion") List mmot=(List)settings["${sLbl}Motion"] input "${sLbl}Motion", "capability.motionSensor", title: imgTitle(getAppImg("motion_icon.png"), inputTitleStr("Motion Sensors")), description: "Select Sensors to Use", required: false, multiple: true, submitOnChange: true if((List)settings["${sLbl}Motion"]){ paragraph imgTitle(getAppImg("i_inst"), paraTitleStr(" • Motion State: (${isMotionActive(mmot) ? "Active" : "Not Active"})")), state: sCOMPLT if(canHeat){ input "${sLbl}MHeatTemp", "decimal", title: imgTitle(getAppImg("heat_icon.png"), inputTitleStr("Heat Setpoint with Motion(${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues() } if(canCool){ input "${sLbl}MCoolTemp", "decimal", title: imgTitle(getAppImg("cool_icon.png"), inputTitleStr("Cool Setpoint with Motion (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues() } input "${sLbl}MHvacMode", sENUM, title: imgTitle(getAppImg("i_hmod"), inputTitleStr("Set Hvac Mode with Motion:")), required: false, description: "No change set", options: tModeHvacEnum(canHeat,canCool,true), multiple: false // input "${sLbl}MRestrictionMode", sMODE, title: "Ignore in these modes", description: "Any location mode", required: false, multiple: true, image: getAppImg("i_mod") // input "${sLbl}MPresHome", "capability.presenceSensor", title: "Only act when these people are home", description: "Always", required: false, multiple: true, image: getAppImg("nest_dev_pres_icon.png") // input "${sLbl}MPresAway", "capability.presenceSensor", title: "Only act when these people are away", description: "Always", required: false, multiple: true, image: getAppImg("nest_dev_away_icon.png") input "${sLbl}MDelayValOn", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Motion Setting Changes")), required: false, defaultValue: 60, options: longTimeSecEnum(), multiple: false input "${sLbl}MDelayValOff", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay disabling Motion Settings")), required: false, defaultValue: 1800, options: longTimeSecEnum(), multiple: false } } String timeFrom=settings["${sLbl}rstrctTimeFrom"] String timeTo=settings["${sLbl}rstrctTimeTo"] Boolean showTime= (timeFrom || timeTo || settings."${sLbl}rstrctTimeFromCustom" || settings."${sLbl}rstrctTimeToCustom") Boolean myShow=!(settings["${sLbl}rstrctMode"] || settings["${sLbl}restrictionDOW"] || showTime || (List)settings["${sLbl}rstrctSWOn"] || (List)settings["${sLbl}rstrctSWOff"] || (List)settings["${sLbl}rstrctPHome"] || (List)settings["${sLbl}rstrctPAway"] ) section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Schedule Restrictions: ", hideable: true, hidden: myShow){ paragraph paraTitleStr("Optional") paragraph sectionTitleStr("Restrict when this Schedule is in use") input "${sLbl}rstrctMode", sMODE, title: imgTitle(getAppImg("i_mod"), inputTitleStr("Only execute in these modes")), description: "Any location mode", required: false, multiple: true input "${sLbl}restrictionDOW", sENUM, options: timeDayOfWeekOptions(), title: imgTitle(getAppImg("day_calendar_icon2.png"), inputTitleStr("Only execute on these days")), description: "Any week day", required: false, multiple: true input "${sLbl}rstrctTimeFrom", sENUM, title: imgTitle(getAppImg("start_time_icon.png"), inputTitleStr((timeFrom ? "Only execute if time is between" : "Only execute during this time"))), options: timeComparisonOptionValues(), required: showTime, multiple: false, submitOnChange: true if(showTime){ if((timeFrom && timeFrom.contains("custom")) || settings."${sLbl}rstrctTimeFromCustom" != null){ input "${sLbl}rstrctTimeFromCustom", sTIME, title: inputTitleStr("Custom time"), required: true, multiple: false }else{ input "${sLbl}rstrctTimeFromOffset", "number", title: imgTitle(getAppImg("offset_icon.png"), inputTitleStr("Offset (+/- minutes)")), range: "*..*", required: true, multiple: false, defaultValue: 0 } input "${sLbl}rstrctTimeTo", sENUM, title: imgTitle(getAppImg("stop_time_icon.png"), inputTitleStr("And")), options: timeComparisonOptionValues(), required: true, multiple: false, submitOnChange: true if((timeTo && timeTo.contains("custom")) || settings."${sLbl}rstrctTimeToCustom" != null){ input "${sLbl}rstrctTimeToCustom", sTIME, title: inputTitleStr("Custom time"), required: true, multiple: false }else{ input "${sLbl}rstrctTimeToOffset", "number", title: imgTitle(getAppImg("offset_icon.png"), inputTitleStr("Offset (+/- minutes)")), range: "*..*", required: true, multiple: false, defaultValue: 0 } } input "${sLbl}rstrctPHome", "capability.presenceSensor", title: imgTitle(getAppImg("nest_dev_pres_icon.png"), inputTitleStr("Only execute when one or more of these People are home")), description: "Always", required: false, multiple: true input "${sLbl}rstrctPAway", "capability.presenceSensor", title: imgTitle(getAppImg("nest_dev_away_icon.png"), inputTitleStr("Only execute when all these People are away")), description: "Always", required: false, multiple: true input "${sLbl}rstrctSWOn", "capability.switch", title: imgTitle(getAppImg("i_sw"), inputTitleStr("Only execute when these switches are all on")), description: "Always", required: false, multiple: true input "${sLbl}rstrctSWOff", "capability.switch", title: imgTitle(getAppImg("switch_off_icon.png"), inputTitleStr("Only execute when these switches are all off")), description: "Always", required: false, multiple: true } } } Map getScheduleDesc(Integer num=null){ Map result=[:] Map schedData=(Map)state.activeSchedData Integer actSchedNum=getCurrentSchedule() String tempScaleStr=tUnitStr() Integer schNum Map schData def sData; sData=schedData if(num){ sData=schedData?.find { it?.key?.toInteger() == num } } if(sData?.size()){ sData.sort().each { scd -> String str; str=sBLANK schNum=scd.key schData=scd.value String sLbl="schMot_${schNum}_" Boolean isRestrict=(schData.m || schData.tf || schData.tfc || schData.tfo || schData.tt || schData.ttc || schData.tto || schData.w || schData.s1 || schData.s0 || schData.p1 || schData.p0) Boolean isTimeRes=(schData.tf || schData.tfc || schData.tfo || schData.tt || schData.ttc || schData.tto) Boolean isDayRes=schData.w Boolean isTemp=(schData.ctemp || schData.htemp || schData.hvacm) Boolean isSw=(schData.s1 || schData.s0) Boolean isPres=(schData.p1 || schData.p0) Boolean isMot=schData.m0 Boolean isRemSen=(schData.sen0 || schData.thres) Boolean isFanEn=schData?.fan0 String resPreBar=isSw || isPres || isTemp ? "│" : sSPACE String tempPreBar=isMot || isRemSen ? "│" : " " Boolean motPreBar=isRemSen str += schData?.lbl ? " • ${schData?.lbl}${(actSchedNum?.toInteger() == schNum?.toInteger()) ? " (In Use)" : " (Not In Use)"}" : sBLANK //restriction section str += isRestrict ? "\n ${isSw || isPres || isTemp ? "├" : "└"} Restrictions:" : sBLANK // Integer mLen=schData?.m ? schData?.m?.toString().length() : 0 String mStr; mStr=sBLANK Integer mdSize; mdSize=1 schData?.m?.each { md -> mStr += md ? "\n ${isSw || isPres || isTemp ? "│ ${(isDayRes || isTimeRes || isPres || isSw) ? "│" : " "}" : " "} ${mdSize < schData.m?.size() ? "├" : "└"} ${md.toString()}" : sBLANK mdSize=mdSize+1 } str += schData?.m ? "\n ${resPreBar} ${(isTimeRes || schData.w) ? "├" : "└"} Mode${((List)schData.m)?.size() > 1 ? "s" : sBLANK}:${isInMode((List)schData?.m) ? " (${okSym()})" : " (${notOkSym()})"}" : sBLANK str += schData?.m ? "$mStr" : sBLANK String dayStr=getAbrevDay(schData.w) String timeDesc=getScheduleTimeDesc((String)schData.tf, (String)schData.tfc, (Integer)schData.tfo, (String)schData.tt, (String)schData.ttc, (Integer)schData.tto, (isSw || isPres || isDayRes)) str += isTimeRes ? "\n │ ${isDayRes || isPres || isSw ? "├" : "└"} ${timeDesc}" : sBLANK str += isDayRes ? "\n │ ${isSw || isPres ? "├" : "└"} Days:${getSchRestrictDoWOk(schNum) ? " (${okSym()})" : " (${notOkSym()})"}" : sBLANK str += isDayRes ? "\n │ ${isSw || isPres ? "│" :" "} └ ${dayStr}" : sBLANK // def p1Len=schData?.p1 ? schData?.p1?.toString().length() : 0 // def p1Str=sBLANK // def p1dSize=1 // settings["${sLbl}rstrctPAway"]?.each { ps1 -> // p1Str += ps1 ? "\n ${isSw || isPres || isTemp ? "│ " : " "} ${p1dSize < settings["${sLbl}rstrctPAway"].size() ? "├" : "└"} ${ps1.toString()}${!isPresenceHome(ps1) ? " (${okSym()})" : " (${notOkSym()})"}" : sBLANK // p1dSize=p1dSize+1 // } // def p0Len=schData?.p0 ? schData?.p0?.toString().length() : 0 // def p0Str=sBLANK // def p0dSize=1 // settings["${sLbl}rstrctPHome"]?.each { ps0 -> // p0Str += ps0 ? "\n ${isSw || isPres || isTemp ? "│ " : " "} ${p0dSize < settings["${sLbl}rstrctPHome"].size() ? "├" : "└"} ${ps0.toString()}" : sBLANK // p0dSize=p0dSize+1 // } str += schData.p1 ? "\n │ ${(schData?.p0 || isSw) ? "├" : "└"} Presence Home:${isSomebodyHome((List)settings["${sLbl}rstrctPHome"]) ? " (${okSym()})" : " (${notOkSym()})"}" : sBLANK //str += schData?.p1 ? "$p1Str" : sBLANK str += schData.p1 ? "\n │ ${(schData?.p0 || isSw) ? "│" : " "} └ (${schData.p1?.size()} Selected)" : sBLANK str += schData.p0 ? "\n │ ${isSw ? "├" : "└"} Presence Away:${!isSomebodyHome((List)settings["${sLbl}rstrctPAway"]) ? " (${okSym()})" : " (${notOkSym()})"}" : sBLANK //str += schData.p0 ? "$p0Str" : sBLANK str += schData.p0 ? "\n │ ${isSw ? "│" : " "} └ (${schData.p0?.size()} Selected)" : sBLANK str += schData.s1 ? "\n │ ${schData?.s0 ? "├" : "└"} Switches On:${allDevAttValsEqual((List)settings["${sLbl}rstrctSWOn"], sSWIT, sON) ? " (${okSym()})" : " (${notOkSym()})"}" : sBLANK str += schData.s1 ? "\n │ ${schData?.s0 ? "│" : " "} └ (${schData.s1?.size()} Selected)" : sBLANK str += schData.s0 ? "\n │ └ Switches Off:${allDevAttValsEqual((List)settings["${sLbl}rstrctSWOff"], sSWIT, sOFF) ? " (${okSym()})" : " (${notOkSym()})"}" : sBLANK str += schData.s0 ? "\n │ └ (${schData.s0?.size()} Selected)" : sBLANK //Temp Setpoints str += isTemp ? "${isRestrict ? "\n │\n" : "\n"} ${(isMot || isRemSen) ? "├" : "└"} Temp Setpoints:" : sBLANK str += schData.ctemp ? "\n ${tempPreBar} ${schData.htemp ? "├" : "└"} Cool Setpoint: (${fixTempSetting(schData.ctemp)}${tempScaleStr})" : sBLANK str += schData.htemp ? "\n ${tempPreBar} ${schData.hvacm ? "├" : "└"} Heat Setpoint: (${fixTempSetting(schData.htemp)}${tempScaleStr})" : sBLANK str += schData.hvacm ? "\n ${tempPreBar} └ HVAC Mode: (${strCapitalize(schData.hvacm)})" : sBLANK //Motion Info // def m0Len=schData?.p0 ? schData?.p0?.toString().length() : 0 // def m0Str=sBLANK // def m0dSize=1 // schData?.m0?.each { ms0 -> // m0Str += ms0 ? "\n ${isTemp || isFanEn || isRemSen || isRestrict ? "│" : " "} ${m0dSize < schData?.m0.size() ? "├" : "└"} ${ms0.toString()}" : sBLANK // m0dSize=m0dSize+1 // } str += isMot ? "${isTemp || isFanEn || isRemSen || isRestrict ? "\n │\n" : "\n"} ${isRemSen ? "├" : "└"} Motion Settings:" : sBLANK str += isMot ? "\n ${motPreBar ? "│" : " "} ${(schData?.mctemp || schData?.mhtemp) ? "├" : "└"} Motion Sensors: (${schData.m0?.size()})" : sBLANK //str += schData?.m0 ? "$m0Str" : sBLANK //str += isMot ? "\n ${motPreBar ? "│" : " "} ${schData?.mctemp || schData?.mhtemp ? "│" : sBLANK} └ (${isMotionActive((List)settings["${sLbl}Motion"]) ? "Active" : "None Active"})" : sBLANK str += isMot && schData.mctemp ? "\n ${motPreBar ? "│" : " "} ${(schData.mctemp || schData.mhtemp) ? "├" : "└"} Mot. Cool Setpoint: (${fixTempSetting(schData.mctemp)}${tempScaleStr})" : sBLANK str += isMot && schData.mhtemp ? "\n ${motPreBar ? "│" : " "} ${schData.mdelayOn || schData.mdelayOff ? "├" : "└"} Mot. Heat Setpoint: (${fixTempSetting(schData.mhtemp)}${tempScaleStr})" : sBLANK str += isMot && schData.mhvacm ? "\n ${motPreBar ? "│" : " "} ${(schData.mdelayOn || schData.mdelayOff) ? "├" : "└"} Mot. HVAC Mode: (${strCapitalize(schData.mhvacm)})" : sBLANK str += isMot && schData.mdelayOn ? "\n ${motPreBar ? "│" : " "} ${schData.mdelayOff ? "├" : "└"} Mot. On Delay: (${getEnumValue(longTimeSecEnum(), schData.mdelayOn)})" : sBLANK str += isMot && schData.mdelayOff ? "\n ${motPreBar ? "│" : " "} └ Mot. Off Delay: (${getEnumValue(longTimeSecEnum(), schData?.mdelayOff)})" : sBLANK //Remote Sensor Info str += isRemSen && schData.sen0 ? "${isRemSen || isRestrict ? "\n │\n" : "\n"} └ Alternate Remote Sensor:" : sBLANK //str += isRemSen && schData?.sen0 ? "\n ├ Temp Sensors: (${schData?.sen0.size()})" : sBLANK ((List)settings["${sLbl}remSensor"])?.each { t -> str += "\n ├ ${t?.label}: ${(t?.label?.toString()?.length() > 10) ? "\n │ └ " : sBLANK}(${getDeviceTemp(t)}${tempScaleStr})" } str += isRemSen && schData.sen0 ? "\n └ Temp${(((List)settings["${sLbl}remSensor"])?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg((List)settings["${sLbl}remSensor"])}${tempScaleStr})" : sBLANK str += isRemSen && schData.thres ? "\n └ Threshold: (${settings["${sLbl}remSenThreshold"]}${tempScaleStr})" : sBLANK //log.debug "str: \n$str" if(str != sBLANK){ result[schNum]=str.toString() } } } return (result.size() >= 1) ? result : null } String getScheduleTimeDesc(String timeFrom, String timeFromCustom, Integer timeFromOffset, String timeTo, String timeToCustom, Integer timeToOffset, Boolean showPreLine=false){ SimpleDateFormat tf=new SimpleDateFormat("h:mm a") tf.setTimeZone(location?.timeZone) String spl=showPreLine ? "│" : sBLANK String timeToVal, timeFromVal timeToVal=sNULL timeFromVal=sNULL Integer i; i=0 if(timeFrom && timeTo){ while (i < 2){ switch(i == 0 ? timeFrom : timeTo){ case "custom time": if(i == 0){ timeFromVal=(String)tf.format(Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", timeFromCustom)) } else { timeToVal=(String)tf.format(Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", timeToCustom)) } break case "sunrise": def sunTime=((timeFromOffset > 0 || timeToOffset > 0) ? getSunriseAndSunset(zipCode: location.zipCode, sunriseOffset: "00:${i == 0 ? timeFromOffset : timeToOffset}") : getSunriseAndSunset(zipCode: location.zipCode)) if(i == 0){ timeFromVal="Sunrise: (" + (String)tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunrise?.toString())) + ")" } else { timeToVal="Sunrise: (" + (String)tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunrise?.toString())) + ")" } break case "sunset": def sunTime=((timeFromOffset > 0 || timeToOffset > 0) ? getSunriseAndSunset(zipCode: location.zipCode, sunriseOffset: "00:${i == 0 ? timeFromOffset : timeToOffset}") : getSunriseAndSunset(zipCode: location.zipCode)) if(i == 0){ timeFromVal="Sunset: (" + (String)tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunset?.toString())) + ")" } else { timeToVal="Sunset: (" + (String)tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunset?.toString())) + ")" } break case "noon": Long rightNow=adjustTime().time def offSet=(timeFromOffset != null || timeToOffset != null) ? (i == 0 ? (timeFromOffset * 60 * 1000) : (timeToOffset * 60 * 1000)) : 0 String res="Noon: " + formatTime(convertDateToUnixTime((rightNow - rightNow.mod(86400000) + 43200000) + offSet)) if(i == 0){ timeFromVal=res } else { timeToVal=res } break case "midnight": Long rightNow=adjustTime().time def offSet=(timeFromOffset != null || timeToOffset != null) ? (i == 0 ? (timeFromOffset * 60 * 1000) : (timeToOffset * 60 * 1000)) : 0 String res="Midnight: " + formatTime(convertDateToUnixTime((rightNow - rightNow.mod(86400000)) + offSet)) if(i == 0){ timeFromVal=res } else { timeToVal=res } break } i += 1 } } Boolean timeOk= ((timeFrom && (timeFromCustom || timeFromOffset) && timeTo && (timeToCustom || timeToOffset)) && checkTimeCondition(timeFrom, timeFromCustom, timeFromOffset, timeTo, timeToCustom, timeToOffset)) String out; out=sBLANK out += (timeFromVal && timeToVal) ? "Time:${timeOk ? " (${okSym()})" : " (${notOkSym()})"}\n │ ${spl} ├ $timeFromVal\n │ ${spl} ├ to\n │ ${spl} └ $timeToVal" : sBLANK return out } /* void updSchedActiveState(Integer schNum, String active){ LogTrace("updSchedActiveState(schNum: $schNum, active: $active)") if(schNum && active){ String sLbl="schMot_${schNum}_SchedActive" Boolean curAct=settings["${sLbl}"] if(curAct.toString() == active.toString()){ return } LogAction("updSchedActiveState | Setting Schedule (${schNum} - ${getSchedLbl(schNum)}) Active to ($active)", sINFO, false) settingUpdate("${sLbl}", "${active}") }else{ return } } */ static String okSym(){ return "✓"// ☑" } static String notOkSym(){ return "✘" } @SuppressWarnings('unused') String getRemSenTempSrc(){ return (String)state.remoteTempSourceStr ?: sNULL } static List getAbrevDay(vals){ List alist=[] if(vals){ //log.debug "days: $vals | (${vals?.size()})" Integer len=(vals?.toString()?.length() < 7) ? 3 : 2 vals?.each { d -> alist.push(d?.toString()?.substring(0, len)) } } return alist } Double roundTemp(Double temp){ if(temp == null){ return null } Double newtemp if( getTemperatureScale() == "C"){ newtemp=Math.round(temp.round(1) * 2) / 2.0f }else{ newtemp=temp.round(0) /* if(temp instanceof Integer){ //log.debug "roundTemp: ($temp) is Integer" newTemp=temp.toInteger() } else if(temp instanceof Double){ //log.debug "roundTemp: ($temp) is Double" newtemp=temp.round(0).toInteger() } else if(temp instanceof BigDecimal){ //log.debug "roundTemp: ($temp) is BigDecimal" newtemp=temp.toInteger() } */ } return newtemp } void updateScheduleStateMap(){ if(autoType == "schMot" && isSchMotConfigured()){ Map actSchedules=[:] Integer numAct; numAct=0 getScheduleList()?.each { Integer scdNum -> String sLbl="schMot_${scdNum}_" Map newScd Boolean schActive=settings["${sLbl}SchedActive"] if(schActive){ actSchedules?."${scdNum}"=[:] newScd=cleanUpMap([ lbl: settings["${sLbl}name"], m: settings["${sLbl}rstrctMode"], tf: settings["${sLbl}rstrctTimeFrom"], tfc: settings["${sLbl}rstrctTimeFromCustom"], tfo: settings["${sLbl}rstrctTimeFromOffset"], tt: settings["${sLbl}rstrctTimeTo"], ttc: settings["${sLbl}rstrctTimeToCustom"], tto: settings["${sLbl}rstrctTimeToOffset"], w: settings["${sLbl}restrictionDOW"], p1: deviceInputToList((List)settings["${sLbl}rstrctPHome"]), p0: deviceInputToList((List)settings["${sLbl}rstrctPAway"]), s1: deviceInputToList((List)settings["${sLbl}rstrctSWOn"]), s0: deviceInputToList((List)settings["${sLbl}rstrctSWOff"]), ctemp: roundTemp(settings["${sLbl}CoolTemp"].toDouble()), htemp: roundTemp(settings["${sLbl}HeatTemp"].toDouble()), hvacm: settings["${sLbl}HvacMode"], sen0: (Boolean)settings["schMotRemoteSensor"] ? deviceInputToList((List)settings["${sLbl}remSensor"]) : null, thres: (Boolean)settings["schMotRemoteSensor"] ? settings["${sLbl}remSenThreshold"] : null, m0: deviceInputToList((List)settings["${sLbl}Motion"]), mctemp: (List)settings["${sLbl}Motion"] ? roundTemp(settings["${sLbl}MCoolTemp"].toDouble()) : null, mhtemp: (List)settings["${sLbl}Motion"] ? roundTemp(settings["${sLbl}MHeatTemp"].toDouble()) : null, mhvacm: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MHvacMode"] : sNULL, // mpresHome: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MPresHome"] : null, // mpresAway: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MPresAway"] : null, mdelayOn: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MDelayValOn"] : null, mdelayOff: (List)settings["${sLbl}Motion"] ? settings["${sLbl}MDelayValOff"] : null ]) numAct += 1 actSchedules?."${scdNum}"=newScd //LogAction("updateScheduleMap [ ScheduleNum: $scdNum | PrefixLbl: $sLbl | SchedActive: $schActive | NewSchedData: $newScd ]", sINFO, false) } } state.activeSchedData=actSchedules } } List deviceInputToList(List items){ List list=[] if(items){ items.sort().each { d -> list.push(d.displayName.toString()) } return list } return null } /* def inputItemsToList(items){ def list=[] if(items){ items?.each { d -> list.push(d) } return list } return null } */ Boolean isSchMotConfigured(){ return (settings.schMotTstat && ( (Boolean)settings.schMotOperateFan || (Boolean)settings.schMotRemoteSensor || (Boolean)settings.schMotWaterOff || (Boolean)settings.schMotContactOff || (Boolean)settings.schMotHumidityControl || (Boolean)settings.schMotExternalTempOff)) } Integer getAutoRunSec(){ return !(String)state.autoRunDt ? 100000 : GetTimeDiffSeconds((String)state.autoRunDt, sNULL, "getAutoRunSec").toInteger() } void schMotCheck(){ LogTrace("schMotCheck") try { if(getIsAutomationDisabled()){ return } Integer schWaitVal schWaitVal=settings.schMotWaitVal?.toInteger() ?: 60 if(schWaitVal > 120){ schWaitVal=120 } Integer t0=getAutoRunSec() if(t0 < schWaitVal){ Integer schChkVal=((schWaitVal - t0) < 30) ? 30 : (schWaitVal - t0) scheduleAutomationEval(schChkVal) LogAction("Too Soon to Evaluate Actions; Re-Evaluation in (${schChkVal} seconds)", sINFO, false) return } Long execTime=now() state.autoRunDt=getDtNow() // This order is important // turn system on/off, then update schedule mode/temps, then remote sensors, then update fans Boolean updatedWeather=false if((Boolean)settings.schMotWaterOff){ if(isLeakWatConfigured()){ leakWatCheck() } } if((Boolean)settings.schMotContactOff){ if(isConWatConfigured()){ conWatCheck() } } if((Boolean)settings.schMotExternalTempOff){ if(isExtTmpConfigured()){ if((Boolean)settings.extTmpUseWeather && !updatedWeather){ updatedWeather=true; getExtConditions() } extTmpTempCheck() } } // if(settings.schMotSetTstatTemp){ if(isTstatSchedConfigured()){ setTstatTempCheck() } // } if((Boolean)settings.schMotRemoteSensor){ if(isRemSenConfigured()){ remSenCheck() } } if((Boolean)settings.schMotHumidityControl){ if(isHumCtrlConfigured()){ if((Boolean)settings.humCtrlUseWeather && !updatedWeather){ getExtConditions() } humCtrlCheck() } } if((Boolean)settings.schMotOperateFan){ if(isFanCtrlConfigured()){ fanCtrlCheck() } } storeExecutionHistory((now() - execTime), "schMotCheck") } catch (ex){ log.error "schMotCheck Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "schMotCheck", true, getAutoType()) } } void storeLastEventData(evt){ if(evt){ Map newVal=["name":evt.name, "displayName":evt.displayName, "value":evt.value, "date":formatDt((Date)evt.date), "unit":evt.unit] state.lastEventData=newVal //log.debug "LastEvent: ${state.lastEventData}" List list list=(List)state.detailEventHistory ?: [] Integer listSize=15 if(list.size() < listSize){ list.push(newVal) } else if(list.size() > listSize){ Integer nSz=(list.size()-listSize) + 1 List nList=list?.drop(nSz) nList.push(newVal) list=nList } else if(list.size() == listSize){ List nList=list?.drop(1) nList?.push(newVal) list=nList } if(list){ state.detailEventHistory=list } } } void storeExecutionHistory(val, String method=sNULL){ //log.debug "storeExecutionHistory($val, $method)" // try { if(method){ LogTrace("${method} Execution Time: (${val} milliseconds)") } if(method in ["watchDogCheck", "checkNestMode", "schMotCheck"]){ state.autoExecMS=val ?: null List list list=(List)state.evalExecutionHistory ?: [] Integer listSize=20 list=addToList(val, list, listSize) if(list){ state.evalExecutionHistory=list } } if(!(method in ["watchDogCheck", "checkNestMode"])){ List list list=(List)state.detailExecutionHistory ?: [] Integer listSize=30 list=addToList([val, method, getDtNow()], list, listSize) if(list){ state.detailExecutionHistory=list } } // } catch (ex){ // log.error "storeExecutionHistory Exception:", ex //parent?.sendExceptionData(ex, "storeExecutionHistory", true, getAutoType()) // } } static List addToList(val, List ilist, Integer listSize){ List list; list=ilist if(list?.size() < listSize){ list.push(val) }else if(list?.size() > listSize){ Integer nSz=(list?.size()-listSize) + 1 List nList=list?.drop(nSz) nList?.push(val) list=nList }else if(list?.size() == listSize){ List nList=list?.drop(1) nList?.push(val) list=nList } return list } /* static Integer getAverageValue(items){ List tmpAvg=[] def val=0 if(!items){ return val } else if(items?.size() > 1){ tmpAvg=items if(tmpAvg){ val=(tmpAvg.sum().toDouble() / tmpAvg.size().toDouble()).round(0) } }else{ val=items } return val.toInteger() } */ /************************************************************************************************ | DYNAMIC NOTIFICATION PAGES | *************************************************************************************************/ @SuppressWarnings('unused') def setNotificationPage1(params){ //href "setNotificationPage1", title: titles("t_nt"), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? sCOMPLT : null), image: getAppImg("i_not") LogTrace("setNotificationPage1()") String pName=watchDogPrefix() def t0=["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] return setNotificationPage( t0 ) } @SuppressWarnings('unused') def setNotificationPage2(params){ //href "setNotificationPage2", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":false, "allowAlarm":false, "showSchedule":true], state: (pageDesc ? sCOMPLT : null) LogTrace("setNotificationPage2()") String pName=nModePrefix() def t0=["pName":"${pName}", "allowSpeech":false, "allowAlarm":false, "showSchedule":true] return setNotificationPage( t0 ) } @SuppressWarnings('unused') def setNotificationPage3(params){ //href "setNotificationPage3", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? sCOMPLT : null) String pName=leakWatPrefix() LogTrace("setNotificationPage3()") def t0=["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] return setNotificationPage( t0 ) } @SuppressWarnings('unused') def setNotificationPage4(params){ //href "setNotificationPage4, title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? sCOMPLT : null) String pName=conWatPrefix() LogTrace("setNotificationPage4()") def t0=["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] return setNotificationPage( t0 ) } @SuppressWarnings('unused') def setNotificationPage5(params){ //href "setNotificationPage5", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? sCOMPLT : null) String pName=extTmpPrefix() LogTrace("setNotificationPage5()") def t0=["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true] return setNotificationPage( t0 ) } def setNotificationPage(params){ String pName; pName=params?.pName Boolean allowSpeech Boolean allowAlarm Boolean showSched if(params?.pName){ state.t_notifD=params allowSpeech=params?.allowSpeech?.toBoolean(); showSched=params?.showSchedule?.toBoolean(); allowAlarm=params?.allowAlarm?.toBoolean() }else{ pName=state.t_notifD?.pName; allowSpeech=state.t_notifD?.allowSpeech; showSched=state.t_notifD?.showSchedule; allowAlarm=state.t_notifD?.allowAlarm } if(!pName){ return } dynamicPage(name: "setNotificationPage", title: "Configure Notification Options", uninstall: false){ section(sBLANK){ //section("Notification Preferences:"){ input "${pName}NotifOn", sBOOL, title: imgTitle(getAppImg("i_not"), inputTitleStr("Enable Notifications?")), description: (!settings["${pName}NotifOn"] ? "Enable Text, Voice, or Alarm Notifications" : sBLANK), required: false, defaultValue: false, submitOnChange: true Boolean fixSettings; fixSettings=false if((Boolean)settings["${pName}NotifOn"]){ // section("Use NST Manager Settings:"){ input "${pName}UseMgrNotif", sBOOL, title: imgTitle(getAppImg("notification_icon2.png"), inputTitleStr("Use Manager Settings?")), defaultValue: true, submitOnChange: true, required: false // } if(!(Boolean)settings."${pName}UseMgrNotif"){ settingRemove("${pName}NotifPhones") // section("Enable Text Messaging:"){ // input "${pName}NotifPhones", "phone", title: imgTitle(getAppImg("notification_icon2.png"), inputTitleStr("Send SMS to Number (Optional)")), required: false, submitOnChange: true // } // section("Enable Pushover Support:"){ input "${pName}PushoverEnabled", sBOOL, title: imgTitle(getAppImg("pushover_icon.png"), inputTitleStr("Notification Device")), required: false, submitOnChange: true if(settings."${pName}PushoverEnabled" == true){ input "${pName}PushoverDevices", "capability.notification", title: imgTitle(getAppImg("pushover_icon.png"), inputTitleStr("Notification Device")), required: false, submitOnChange: true } // } }else{ fixSettings=true } }else{ fixSettings=true } if(fixSettings){ settingRemove("${pName}NotifPhones") settingRemove("${pName}PushoverEnabled") settingRemove("${pName}PushoverDevices") //settingRemove("${pName}UseParentNotifRestrictions") } /* if(allowSpeech && settings."${pName}NotifOn"){ // section("Voice Notification Preferences:"){ input "${pName}AllowSpeechNotif", sBOOL, title: "Enable Voice Notifications?", description: "Media players, or Speech Devices", required: false, defaultValue: (settings."${pName}AllowSpeechNotif" ? true : false), submitOnChange: true, image: getAppImg("speech_icon.png") if(settings["${pName}AllowSpeechNotif"]){ setInitialVoiceMsgs(pName) input "${pName}SendToAskAlexaQueue", sBOOL, title: "Send to Ask Alexa Message Queue?", required: false, defaultValue: (settings."${pName}AllowSpeechNotif" ? false : true), submitOnChange: true, image: askAlexaImgUrl() input "${pName}SpeechMediaPlayer", "capability.musicPlayer", title: "Select Media Player(s)", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg("media_player.png") input "${pName}EchoDevices", "device.echoSpeaksDevice", title: "Select Alexa Devices(s)", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg('echo_speaks.png') input "${pName}SpeechDevices", "capability.speechSynthesis", title: "Select Speech Synthesizer(s)", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg("speech2_icon.png") if(settings["${pName}SpeechMediaPlayer"] || settings["${pName}EchoDevices"]){ input "${pName}SpeechVolumeLevel", "number", title: "Default Volume Level?", required: false, defaultValue: 30, range: "0::100", submitOnChange: true, image: getAppImg("volume_icon.png") if(settings["${pName}SpeechMediaPlayer"]){ input "${pName}SpeechAllowResume", sBOOL, title: "Can Resume Playing Media?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("resume_icon.png") } } def desc=sBLANK if(pName in ["conWat", "extTmp", "leakWat"]){ if( (settings["${pName}SpeechMediaPlayer"] || settings["${pName}SpeechDevices"] || settings["${pName}EchoDevices"] || settings["${pName}SendToAskAlexaQueue"]) ){ switch(pName){ case "conWat": desc="Contact Close" break case "extTmp": desc="External Temperature Threshold" break case "leakWat": desc="Water Dried" break } input "${pName}SpeechOnRestore", sBOOL, title: "Speak when restoring HVAC on (${desc})?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png") // TODO: There are more messages and errors than ON / OFF input "${pName}UseCustomSpeechNotifMsg", sBOOL, title: "Customize Notitification Message?", required: false, defaultValue: (settings."${pName}AllowSpeechNotif" ? false : true), submitOnChange: true, image: getAppImg("speech_icon.png") if(settings["${pName}UseCustomSpeechNotifMsg"]){ getNotifVariables(pName) input "${pName}CustomOffSpeechMessage", "text", title: "Turn Off Message?", required: false, defaultValue: state."${pName}OffVoiceMsg" , submitOnChange: true, image: getAppImg("speech_icon.png") state."${pName}OffVoiceMsg"=settings."${pName}CustomOffSpeechMessage" if(settings."${pName}CustomOffSpeechMessage"){ paragraph "Off Msg:\n" + voiceNotifString(state."${pName}OffVoiceMsg",pName) } input "${pName}CustomOnSpeechMessage", "text", title: "Restore On Message?", required: false, defaultValue: state."${pName}OnVoiceMsg", submitOnChange: true, image: getAppImg("speech_icon.png") state."${pName}OnVoiceMsg"=settings."${pName}CustomOnSpeechMessage" if(settings."${pName}CustomOnSpeechMessage"){ paragraph "Restore On Msg:\n" + voiceNotifString(state."${pName}OnVoiceMsg",pName) } }else{ state."${pName}OffVoiceMsg"=sBLANK state."${pName}OnVoiceMsg"=sBLANK } } } } //} } */ if(allowAlarm && settings."${pName}NotifOn"){ // section("Alarm/Siren Device Preferences:"){ input "${pName}AllowAlarmNotif", sBOOL, title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Enable Alarm | Siren?")), required: false, defaultValue: (settings."${pName}AllowAlarmNotif" ? true : false), submitOnChange: true if(settings["${pName}AllowAlarmNotif"]){ input "${pName}AlarmDevices", "capability.alarm", title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Select Alarm/Siren(s)")), multiple: true, required: settings["${pName}AllowAlarmNotif"], submitOnChange: true } // } } if(pName in ["conWat", "leakWat", "extTmp", "watchDog"] && settings["${pName}NotifOn"] && settings["${pName}AllowAlarmNotif"] && settings["${pName}AlarmDevices"]){ // section("Notification Alert Options (1):"){ input "${pName}_Alert_1_Delay", sENUM, title: imgTitle(getAppImg("alert_icon2.png"), inputTitleStr("First Alert Delay (in minutes)")), defaultValue: null, required: true, submitOnChange: true, options: longTimeSecEnum() if(settings."${pName}_Alert_1_Delay"){ input "${pName}_Alert_1_AlarmType", sENUM, title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Alarm Type to use?")), options: alarmActionsEnum(), defaultValue: null, submitOnChange: true, required: true if(settings."${pName}_Alert_1_AlarmType"){ input "${pName}_Alert_1_Alarm_Runtime", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Turn off Alarm After (in seconds)?")), options: shortTimeEnum(), defaultValue: 10, required: true, submitOnChange: true } } // } if(settings["${pName}_Alert_1_Delay"]){ // section("Notification Alert Options (2):"){ input "${pName}_Alert_2_Delay", sENUM, title: imgTitle(getAppImg("alert_icon2.png"), inputTitleStr("Second Alert Delay (in minutes)")), defaultValue: null, options: longTimeSecEnum(), required: false, submitOnChange: true if(settings."${pName}_Alert_2_Delay"){ input "${pName}_Alert_2_AlarmType", sENUM, title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Alarm Type to use?")), options: alarmActionsEnum(), defaultValue: null, submitOnChange: true, required: true if(settings."${pName}_Alert_2_AlarmType"){ input "${pName}_Alert_2_Alarm_Runtime", sENUM, title: imgTitle(getAppImg("i_dt"), inputTitleStr("Turn off Alarm After (in minutes)?")), options: shortTimeEnum(), defaultValue: 10, required: true, submitOnChange: true } } // } } } } } } /* def setInitialVoiceMsgs(pName){ if(settings["${pName}AllowSpeechNotif"]){ if(pName in ["conWat", "extTmp", "leakWat"]){ if(pName == "leakWat"){ if(!state."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]){ state."${pName}OffVoiceMsg"="ATTENTION: %devicename% has been turned OFF because %wetsensor% has reported it is WET" } if(!state."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]){ state."${pName}OnVoiceMsg"="Restoring %devicename% to %lastmode% Mode because ALL water sensors have been Dry again for (%ondelay%)" } } if(pName == "conWat"){ if(!state."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]){ state."${pName}OffVoiceMsg"="ATTENTION: %devicename% has been turned OFF because %opencontact% has been Opened for (%offdelay%)" } if(!state."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]){ state."${pName}OnVoiceMsg"="Restoring %devicename% to %lastmode% Mode because ALL contacts have been Closed again for (%ondelay%)" } } if(pName == "extTmp"){ if(!state."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]){ state."${pName}OffVoiceMsg"="ATTENTION: %devicename% has been turned to ECO because External Temp is above the temp threshold for (%offdelay%)" } if(!state."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]){ state."${pName}OnVoiceMsg"="Restoring %devicename% to %lastmode% Mode because External Temp has been above the temp threshold for (%ondelay%)" } } } } } */ //ERS /* def setCustomVoice(pName){ if(settings["${pName}AllowSpeechNotif"]){ if(pName in ["conWat", "extTmp", "leakWat"]){ if(settings["${pName}UseCustomSpeechNotifMsg"]){ state."${pName}OffVoiceMsg"=settings."${pName}CustomOffSpeechMessage" state."${pName}OnVoiceMsg"=settings."${pName}CustomOnSpeechMessage" } } } } */ /* def setNotificationTimePage(params){ def pName=params?.pName if(params?.pName){ state.curNotifTimePageData=params }else{ pName=state.curNotifTimePageData?.pName } dynamicPage(name: "setNotificationTimePage", title: "Prevent Notifications\nDuring these Days, Times or Modes", uninstall: false){ def timeReq=(settings["${pName}qStartTime"] || settings["${pName}qStopTime"]) ? true : false section(){ input "${pName}qStartInput", sENUM, title: "Starting at", options: ["A specific time", "Sunrise", "Sunset"], defaultValue: null, submitOnChange: true, required: false, image: getAppImg("start_time_icon.png") if(settings["${pName}qStartInput"] == "A specific time"){ input "${pName}qStartTime", sTIME, title: "Start time", required: timeReq, image: getAppImg("start_time_icon.png") } input "${pName}qStopInput", sENUM, title: "Stopping at", options: ["A specific time", "Sunrise", "Sunset"], defaultValue: null, submitOnChange: true, required: false, image: getAppImg("stop_time_icon.png") if(settings."${pName}qStopInput" == "A specific time"){ input "${pName}qStopTime", sTIME, title: "Stop time", required: timeReq, image: getAppImg("stop_time_icon.png") } input "${pName}quietDays", sENUM, title: "Prevent during these days of the week", multiple: true, required: false, image: getAppImg("day_calendar_icon.png"), options: timeDayOfWeekOptions() input "${pName}quietModes", sMODE, title: "Prevent when these Modes are Active", multiple: true, submitOnChange: true, required: false, image: getAppImg("i_mod") } } } String getNotifSchedDesc(pName){ def sun=getSunriseAndSunset() def startInput=settings."${pName}qStartInput" String startTime=settings."${pName}qStartTime" def stopInput=settings."${pName}qStopInput" String stopTime=settings."${pName}qStopTime" def dayInput=settings."${pName}quietDays" def modeInput=settings."${pName}quietModes" String notifDesc=sBLANK if(settings."${pName}UseParentNotifRestrictions" == false){ def getNotifTimeStartLbl=( (startInput == "Sunrise" || startInput == "Sunset") ? ( (startInput == "Sunset") ? epochToTime(sun?.sunset.time) : epochToTime(sun?.sunrise.time) ) : (startTime ? time2Str(startTime) : sBLANK) ) def getNotifTimeStopLbl=( (stopInput == "Sunrise" || stopInput == "Sunset") ? ( (stopInput == "Sunset") ? epochToTime(sun?.sunset.time) : epochToTime(sun?.sunrise.time) ) : (stopTime ? time2Str(stopTime) : sBLANK) ) notifDesc += (getNotifTimeStartLbl && getNotifTimeStopLbl) ? "• Silent Time: ${getNotifTimeStartLbl} - ${getNotifTimeStopLbl}" : sBLANK def days=getInputToStringDesc(dayInput) def modes=getInputToStringDesc(modeInput) notifDesc += days ? "${(getNotifTimeStartLbl || getNotifTimeStopLbl) ? "\n" : sBLANK}• Silent Day${isPluralString(dayInput)}: ${days}" : sBLANK notifDesc += modes ? "${(getNotifTimeStartLbl || getNotifTimeStopLbl || days) ? "\n" : sBLANK}• Silent Mode${isPluralString(modeInput)}: ${modes}" : sBLANK }else{ notifDesc += "• Using Manager Restrictions" } return (notifDesc != sBLANK) ? notifDesc : sNULL } def getOk2Notify(pName){ return ((settings["${pName}NotifOn"] == true) && (daysOk(settings."${pName}quietDays") == true) && (notificationTimeOk(pName) == true) && (modesOk(settings."${pName}quietModes") == true)) } Boolean notificationTimeOk(pName){ def strtTime=null def stopTime=null Date now=new Date() def sun=getSunriseAndSunset() // current based on geofence, previously was: def sun=getSunriseAndSunset(zipCode: zipCode) if(settings."${pName}qStartTime" && settings."${pName}qStopTime"){ if(settings."${pName}qStartInput" == "sunset"){ strtTime=sun.sunset } else if(settings."${pName}qStartInput" == "sunrise"){ strtTime=sun.sunrise } else if(settings."${pName}qStartInput" == "A specific time" && settings."${pName}qStartTime"){ strtTime=settings."${pName}qStartTime" } if(settings."${pName}qStopInput" == "sunset"){ stopTime=sun.sunset } else if(settings."${pName}qStopInput" == "sunrise"){ stopTime=sun.sunrise } else if(settings."${pName}qStopInput" == "A specific time" && settings."${pName}qStopTime"){ stopTime=settings."${pName}qStopTime" } }else{ return true } if(strtTime && stopTime){ return timeOfDayIsBetween(strtTime, stopTime, new Date(), getTimeZone()) ? false : true }else{ return true } } def getNotifVariables(pName){ String str=sBLANK str += "\n • DeviceName: %devicename%" str += "\n • Last Mode: %lastmode%" str += (pName == "leakWat") ? "\n • Wet Water Sensor: %wetsensor%" : sBLANK str += (pName == "conWat") ? "\n • Open Contact: %opencontact%" : sBLANK str += (pName in ["conWat", "extTmp"]) ? "\n • Off Delay: %offdelay%" : sBLANK str += "\n • On Delay: %ondelay%" str += (pName == "extTmp") ? "\n • Temp Threshold: %tempthreshold%" : sBLANK paragraph "These Variables are accepted: ${str}" } //process custom tokens to generate final voice message (Copied from BigTalker) def voiceNotifString(phrase, pName){ //LogTrace("conWatVoiceNotifString") try { if(phrase?.toLowerCase().contains("%devicename%")){ phrase=phrase?.toLowerCase().replace('%devicename%', (settings."schMotTstat"?.displayName.toString() ?: "unknown")) } if(phrase?.toLowerCase().contains("%lastmode%")){ phrase=phrase?.toLowerCase().replace('%lastmode%', (state."${pName}RestoreMode".toString() ?: "unknown")) } if(pName == "leakWat" && phrase?.toLowerCase().contains("%wetsensor%")){ phrase=phrase?.toLowerCase().replace('%wetsensor%', (getWetWaterSensors(leakWatSensors) ? getWetWaterSensors(leakWatSensors)?.join(", ").toString() : "a selected leak sensor")) } if(pName == "conWat" && phrase?.toLowerCase().contains("%opencontact%")){ phrase=phrase?.toLowerCase().replace('%opencontact%', (getOpenContacts(conWatContacts) ? getOpenContacts(conWatContacts)?.join(", ").toString() : "a selected contact")) } if(pName == "extTmp" && phrase?.toLowerCase().contains("%tempthreshold%")){ phrase=phrase?.toLowerCase().replace('%tempthreshold%', "${extTmpDiffVal.toString()}(${tUnitStr()})") } if(phrase?.toLowerCase().contains("%offdelay%")){ phrase=phrase?.toLowerCase().replace('%offdelay%', getEnumValue(longTimeSecEnum(), settings."${pName}OffDelay").toString()) } if(phrase?.toLowerCase().contains("%ondelay%")){ phrase=phrase?.toLowerCase().replace('%ondelay%', getEnumValue(longTimeSecEnum(), settings."${pName}OnDelay").toString()) } } catch (ex){ log.error "voiceNotifString Exception:", ex //parent?.sendExceptionData(ex, "voiceNotifString", true, getAutoType()) } return phrase } */ String getNotifConfigDesc(String pName){ //LogTrace("getNotifConfigDesc pName: $pName") String str; str=sBLANK if(settings."${pName}NotifOn"){ // str += "Notification Status:" // if(!getRecipientDesc(pName)){ // str += "\n • Contacts: Using Manager Settings" // } String t0 if(settings."${pName}UseMgrNotif" == false){ // str += (settings."${pName}NotifPhones") ? "${str != sBLANK ? "\n" : sBLANK} • SMS: (${settings."${pName}NotifPhones"?.size()})" : sBLANK str += (settings."${pName}PushoverEnabled") ? "${str != sBLANK ? "\n" : sBLANK}Pushover: (Enabled)" : sBLANK str += (settings."${pName}PushoverEnabled" && settings."${pName}PushoverDevices") ? "${str != sBLANK ? "\n" : sBLANK} • Pushover Devices: (${settings."${pName}PushoverDevices"})" : sBLANK //t0=getNotifSchedDesc(pName) //str += t0 ? "\n\nAlert Restrictions:\n${t0}" : sBLANK }else{ str += " • Enabled Using Manager Settings" } t0=str if(t0){ str="Notification Settings\n${t0}" } //t0=getVoiceNotifConfigDesc(pName) //str += t0 ? "\n\nVoice Status:${t0}" : sBLANK t0=getAlarmNotifConfigDesc(pName) str += t0 ? "\n\nAlarm Status:${t0}" : sBLANK t0=getAlertNotifConfigDesc(pName) str += t0 ? "\n\n${t0}" : sBLANK } return (str != sBLANK) ? str : sNULL } /* def getVoiceNotifConfigDesc(pName){ String str=sBLANK if(settings."${pName}NotifOn" && settings["${pName}AllowSpeechNotif"]){ def speaks=settings."${pName}SpeechDevices" def medias=settings."${pName}SpeechMediaPlayer" def echos=settings["${pName}EchoDevices"] str += settings["${pName}SendToAskAlexaQueue"] ? "\n• Send to Ask Alexa: (True)" : sBLANK str += speaks ? "\n • Speech Devices:" : sBLANK if(speaks){ def cnt=1 speaks?.each { str += it ? "\n ${cnt < speaks.size() ? "├" : "└"} $it" : sBLANK; cnt=cnt+1; } } str += echos ? "\n • Alexa Devices:" : sBLANK if(echos){ Integer cnt=1 echos?.each { str += it ? "\n ${cnt < echos.size() ? "├" : "└"} $it" : sBLANK; cnt=cnt+1; } str += (echos && settings."${pName}SpeechVolumeLevel") ? "\n└ Volume: (${settings."${pName}SpeechVolumeLevel"})" : sBLANK } str += medias ? "${(speaks || echos) ? "\n\n" : "\n"} • Media Players:" : sBLANK if(medias){ def cnt=1 medias?.sort { it?.displayName }?.each { str += it ? "\n│${cnt < medias.size() ? "├" : "└"} $it" : sBLANK; cnt=cnt+1; } } str += (medias && settings."${pName}SpeechVolumeLevel") ? "\n├ Volume: (${settings."${pName}SpeechVolumeLevel"})" : sBLANK str += (medias && settings."${pName}SpeechAllowResume") ? "\n└ Resume: (${strCapitalize(settings."${pName}SpeechAllowResume")})" : sBLANK str += (settings."${pName}UseCustomSpeechNotifMsg" && (medias || speaks)) ? "\n• Custom Message: (${strCapitalize(settings."${pName}UseCustomSpeechNotifMsg")})" : sBLANK } return (str != sBLANK) ? str : sNULL } */ String getAlarmNotifConfigDesc(String pName){ String str; str=sBLANK if(settings."${pName}NotifOn" && settings["${pName}AllowAlarmNotif"]){ def alarms=getInputToStringDesc((List)settings["${pName}AlarmDevices"], true) str += alarms ? "\n • Alarm Devices:${alarms.size() > 1 ? "\n" : sBLANK}${alarms}" : sBLANK } return (str != sBLANK) ? str : sNULL } String getAlertNotifConfigDesc(String pName){ String str; str=sBLANK //TODO not sure we do all these if(settings."${pName}NotifOn" && (settings["${pName}_Alert_1_Delay"] || settings["${pName}_Alert_2_Delay"]) && (settings["${pName}AllowSpeechNotif"] || settings["${pName}AllowAlarmNotif"])){ str += settings["${pName}_Alert_1_Delay"] ? "\nAlert (1) Status:\n • Delay: (${getEnumValue(longTimeSecEnum(), settings["${pName}_Alert_1_Delay"])})" : sBLANK // str += settings["${pName}_Alert_1_Send_Push"] ? "\n • Send Push: (${settings["${pName}_Alert_1_Send_Push"]})" : sBLANK // str += settings["${pName}_Alert_1_Use_Speech"] ? "\n • Use Speech: (${settings["${pName}_Alert_1_Use_Speech"]})" : sBLANK str += settings["${pName}_Alert_1_Use_Alarm"] ? "\n • Use Alarm: (${settings["${pName}_Alert_1_Use_Alarm"]})" : sBLANK str += (settings["${pName}_Alert_1_Use_Alarm"] && settings["${pName}_Alert_1_AlarmType"]) ? "\n ├ Alarm Type: (${getEnumValue(alarmActionsEnum(), settings["${pName}_Alert_1_AlarmType"])})" : sBLANK str += (settings["${pName}_Alert_1_Use_Alarm"] && settings["${pName}_Alert_1_Alarm_Runtime"]) ? "\n └ Alarm Runtime: (${getEnumValue(shortTimeEnum(), settings["${pName}_Alert_1_Alarm_Runtime"])})" : sBLANK str += settings["${pName}_Alert_2_Delay"] ? "${settings["${pName}_Alert_1_Delay"] ? "\n" : sBLANK}\nAlert (2) Status:\n • Delay: (${getEnumValue(longTimeSecEnum(), settings["${pName}_Alert_2_Delay"])})" : sBLANK // str += settings["${pName}_Alert_2_Send_Push"] ? "\n • Send Push: (${settings["${pName}_Alert_2_Send_Push"]})" : sBLANK // str += settings["${pName}_Alert_2_Use_Speech"] ? "\n • Use Speech: (${settings["${pName}_Alert_2_Use_Speech"]})" : sBLANK str += settings["${pName}_Alert_2_Use_Alarm"] ? "\n • Use Alarm: (${settings["${pName}_Alert_2_Use_Alarm"]})" : sBLANK str += (settings["${pName}_Alert_2_Use_Alarm"] && settings["${pName}_Alert_2_AlarmType"]) ? "\n ├ Alarm Type: (${getEnumValue(alarmActionsEnum(), settings["${pName}_Alert_2_AlarmType"])})" : sBLANK str += (settings["${pName}_Alert_2_Use_Alarm"] && settings["${pName}_Alert_2_Alarm_Runtime"]) ? "\n └ Alarm Runtime: (${getEnumValue(shortTimeEnum(), settings["${pName}_Alert_2_Alarm_Runtime"])})" : sBLANK } return (str != sBLANK) ? str : sNULL } static String getInputToStringDesc(List inpt, Boolean addSpace=false){ Integer cnt; cnt=0 String str; str=sBLANK if(inpt){ inpt.sort().each { item -> cnt=cnt+1 str += item ? (((cnt < 1) || (inpt.size() > 1)) ? "\n ${item}" : "${addSpace ? " " : sBLANK}${item}") : sBLANK } } //log.debug "str: $str" return (str != sBLANK) ? str : sNULL } static String isPluralString(List obj){ return (obj?.size() > 1) ? "(s)" : sBLANK } /* def getRecipientsNames(val){ String n=sBLANK Integer i=0 if(val){ //log.debug "val: $val" val?.each { r -> i=i + 1 n += i == val?.size() ? "${r}" : "${r}," } } return n?.toString().replaceAll("\\,", "\n") } def getRecipientDesc(pName){ return (settings."${pName}NotifPhones" || (settings."${pName}PushoverEnabled" && settings."${pName}PushoverDevices")) ? true : false } */ @SuppressWarnings('unused') def setDayModeTimePage1(params){ String mpName=nModePrefix() def t0=[pName:mpName ] return setDayModeTimePage( t0 ) } @SuppressWarnings('unused') def setDayModeTimePage2(params){ String mpName=fanCtrlPrefix() def t0=[pName:mpName ] return setDayModeTimePage( t0 ) } @SuppressWarnings('unused') def setDayModeTimePage3(params){ String mpName=conWatPrefix() def t0=[pName:mpName ] return setDayModeTimePage( t0 ) } @SuppressWarnings('unused') def setDayModeTimePage4(params){ String mpName=humCtrlPrefix() def t0=[pName:mpName ] return setDayModeTimePage( t0 ) } @SuppressWarnings('unused') def setDayModeTimePage5(params){ String mpName=extTmpPrefix() def t0=[pName:mpName ] return setDayModeTimePage( t0 ) } def setDayModeTimePage(params){ String pName; pName=params?.pName if(params?.pName){ state.t_setDayData=params }else{ pName=state.t_setDayData?.pName } dynamicPage(name: "setDayModeTimePage", title: "Select Days, Times or Modes", uninstall: false){ String secDesc=settings["${pName}DmtInvert"] ? "Not" : "Only" Boolean inverted=settings["${pName}DmtInvert"] ? true : false section(sBLANK){ String actIcon=settings."${pName}DmtInvert" ? "inactive" : "active" input "${pName}DmtInvert", sBOOL, title: imgTitle(getAppImg("${actIcon}_icon.png"), inputTitleStr("${secDesc} in These? (tap to invert)")), defaultValue: false, submitOnChange: true } section("${secDesc} During these Days, Times, or Modes:"){ Boolean timeReq= (settings."${pName}StartTime" || settings."${pName}StopTime") input "${pName}StartTime", sTIME, title: imgTitle(getAppImg("start_time_icon.png"), inputTitleStr("Start time")), required: timeReq input "${pName}StopTime", sTIME, title: imgTitle(getAppImg("stop_time_icon.png"), inputTitleStr("Stop time")), required: timeReq input "${pName}Days", sENUM, title: imgTitle(getAppImg("day_calendar_icon2.png"), inputTitleStr("${inverted ? "Not": "Only"} These Days")), multiple: true, required: false, options: timeDayOfWeekOptions() input "${pName}Modes", sMODE, title: imgTitle(getAppImg("i_mod"), inputTitleStr("${inverted ? "Not": "Only"} in These Modes")), multiple: true, required: false } section("Switches:"){ input "${pName}rstrctSWOn", "capability.switch", title: imgTitle(getAppImg("i_sw"), inputTitleStr("Only execute when these switches are all ON")), multiple: true, required: false input "${pName}rstrctSWOff", "capability.switch", title: imgTitle(getAppImg("switch_off_icon.png"), inputTitleStr("Only execute when these switches are all OFF")), multiple: true, required: false } } } String getDayModeTimeDesc(String pName){ String startTime=settings."${pName}StartTime" String stopTime=settings."${pName}StopTime" List dayInput=(List)settings."${pName}Days" List modeInput=(List)settings."${pName}Modes" Boolean inverted=settings."${pName}DmtInvert" ?: null List swOnInput=(List)settings."${pName}rstrctSWOn" List swOffInput=(List)settings."${pName}rstrctSWOff" String str; str=sBLANK String days=getInputToStringDesc(dayInput) String modes=getInputToStringDesc(modeInput) String swOn=getInputToStringDesc(swOnInput) String swOff=getInputToStringDesc(swOffInput) str += ((startTime && stopTime) || modes || days) ? "${!inverted ? "When" : "When Not"}:" : sBLANK str += (startTime && stopTime) ? "\n • Time: ${time2Str((String)settings."${pName}StartTime")} - ${time2Str((String)settings."${pName}StopTime")}" : sBLANK str += days ? "${(startTime && stopTime) ? "\n" : sBLANK}\n • Day${isPluralString(dayInput)}: ${days}" : sBLANK str += modes ? "${((startTime && stopTime) || days) ? "\n" : sBLANK}\n • Mode${isPluralString(modeInput)}: ${modes}" : sBLANK str += swOn ? "${((startTime && stopTime) || days || modes) ? "\n" : sBLANK}\n • Switch${isPluralString(swOnInput)} that must be on: ${getRestSwitch(swOnInput)}" : sBLANK str += swOff ? "${((startTime && stopTime) || days || modes || swOn) ? "\n" : sBLANK}\n • Switch${isPluralString(swOffInput)} that must be off: ${getRestSwitch(swOffInput)}" : sBLANK str += (str != sBLANK) ? descriptions("d_ttm") : sBLANK return str } String getRestSwitch(List swlist){ String swDesc; swDesc=sBLANK Integer swCnt; swCnt=0 Integer rmSwCnt=swlist?.size() ?: 0 swlist?.sort { it?.displayName }?.each { sw -> swCnt=swCnt+1 swDesc += "${swCnt >= 1 ? "${swCnt == rmSwCnt ? "\n └" : "\n ├"}" : "\n └"} ${sw?.label}: (${strCapitalize(sw?.currentSwitch)})" } return (swDesc == sBLANK) ? sNULL : swDesc } String getDmtSectionDesc(String autoType){ return settings["${autoType}DmtInvert"] ? "Do Not Act During these Days, Times, or Modes:" : "Only Act During these Days, Times, or Modes:" //TODO add switches to adjust schedule } /************************************************************************************************ | AUTOMATION SCHEDULE CHECK | *************************************************************************************************/ Boolean autoScheduleOk(String autoType){ try { Boolean inverted=settings."${autoType}DmtInvert" ? true : false Boolean modeOk modeOk= (!(List) settings."${autoType}Modes" || ((isInMode((List) settings."${autoType}Modes") && !inverted) || (!isInMode((List) settings."${autoType}Modes") && inverted))) //dayOk Boolean dayOk SimpleDateFormat dayFmt=new SimpleDateFormat("EEEE") dayFmt.setTimeZone(getTimeZone()) String today=dayFmt.format(new Date()) Boolean inDay= (today in (List) settings."${autoType}Days") dayOk= (!(List) settings."${autoType}Days" || ((inDay && !inverted) || (!inDay && inverted))) //scheduleTimeOk Boolean timeOk; timeOk=true if(settings."${autoType}StartTime" && settings."${autoType}StopTime"){ Date st1=timeToday(settings."${autoType}StartTime", getTimeZone()) Date end1=timeToday(settings."${autoType}StopTime", getTimeZone()) //def inTime=(timeOfDayIsBetween(settings."${autoType}StartTime", settings."${autoType}StopTime", new Date(), getTimeZone())) ? true : false Boolean inTime=timeOfDayIsBetween(st1, end1, new Date(), getTimeZone()) timeOk=(inTime && !inverted) || (!inTime && inverted) } Boolean soFarOk; soFarOk=modeOk && dayOk && timeOk Boolean swOk; swOk=true if(soFarOk && (List)settings."${autoType}rstrctSWOn"){ for(sw in (List)settings["${autoType}rstrctSWOn"]){ if(sw.currentValue(sSWIT) != sON){ swOk=false break } } } soFarOk= (modeOk && dayOk && timeOk && swOk) if(soFarOk && (List)settings."${autoType}rstrctSWOff"){ for(sw in (List)settings["${autoType}rstrctSWOff"]){ if(sw.currentValue(sSWIT) != sOFF){ swOk=false break } } } LogAction("autoScheduleOk( dayOk: $dayOk | modeOk: $modeOk | dayOk: ${dayOk} | timeOk: $timeOk | swOk: $swOk | inverted: ${inverted})", sINFO, false) return (modeOk && dayOk && timeOk && swOk) } catch (ex){ log.error "${autoType}-autoScheduleOk Exception: ${ex?.message}" //parent?.sendExceptionData(ex, "autoScheduleOk", true, getAutoType()) } } /************************************************************************************************ | SEND NOTIFICATIONS VIA PARENT APP | *************************************************************************************************/ void sendNofificationMsg(String msg, String msgType, String pName, lvl=null, pusho=null, sms=null){ LogAction("sendNofificationMsg($msg, $msgType, $pName, $sms, $pusho)", sDEBUG, false) if(settings."${pName}NotifOn" == true){ Integer nlvl=lvl ?: (sms || pusho) ? 5 : 4 if(settings."${pName}UseMgrNotif" == false){ // def mySms=sms ?: settings."${pName}NotifPhones" // if(mySms){ // parent.sendMsg(msgType, msg, nlvl, null, mySms) // } if(pusho && settings."${pName}PushoverDevices"){ parent.sendMsg(msgType, msg, nlvl, settings."${pName}PushoverDevices") } }else{ parent.sendMsg(msgType, msg, nlvl) } }else{ LogAction("sendMsg: Message Skipped as notifications off ($msg)", sINFO, true) } } /************************************************************************************************ | GLOBAL Code | Logging AND Diagnostic | *************************************************************************************************/ void sendEventPushNotifications(String message, String type, String pName){ LogTrace("sendEventPushNotifications($message, $type, $pName)") sendNofificationMsg(message, type, pName) } /* def sendEventVoiceNotifications(vMsg, pName, msgId, rmAAMsg=false, rmMsgId){ def allowNotif=settings."${pName}NotifOn" ? true : false def allowSpeech=allowNotif && settings."${pName}AllowSpeechNotif" ? true : false def ok2Notify=setting?."${pName}UseParentNotifRestrictions" != false ? getOk2Notify(pName) : getOk2Notify(pName) //parent?.getOk2Notify() LogAction("sendEventVoiceNotifications($vMsg, $pName) | ok2Notify: $ok2Notify", sINFO, false) if(allowNotif && allowSpeech){ if(ok2Notify && (settings["${pName}SpeechDevices"] || settings["${pName}SpeechMediaPlayer"] || settings["${pName}EchoDevices"])){ sendTTS(vMsg, pName) } if(settings["${pName}SendToAskAlexaQueue"]){ // we queue to Alexa regardless of quiet times if(rmMsgId != null && rmAAMsg == true){ removeAskAlexaQueueMsg(rmMsgId) } if(vMsg && msgId != null){ addEventToAskAlexaQueue(vMsg, msgId) } } } } */ /* def addEventToAskAlexaQueue(vMsg, msgId, queue=null){ if(false){ //parent?.getAskAlexaMQEn() == true) { if(parent.getAskAlexaMultiQueueEn()){ LogAction("sendEventToAskAlexaQueue: Adding this Message to the Ask Alexa Queue ($queues): ($vMsg)|${msgId}", sINFO, true) sendLocationEvent(name: "AskAlexaMsgQueue", value: "${app?.label}", isStateChange: true, descriptionText: "${vMsg}", unit: "${msgId}", data:queues) }else{ LogAction("sendEventToAskAlexaQueue: Adding this Message to the Ask Alexa Queue: ($vMsg)|${msgId}", sINFO, true) sendLocationEvent(name: "AskAlexaMsgQueue", value: "${app?.label}", isStateChange: true, descriptionText: "${vMsg}", unit: "${msgId}") } } } def removeAskAlexaQueueMsg(msgId, queue=null){ if(false){ //parent?.getAskAlexaMQEn() == true) { if(parent.getAskAlexaMultiQueueEn()){ LogAction("removeAskAlexaQueueMsg: Removing Message ID (${msgId}) from the Ask Alexa Queue ($queues)", sINFO, true) sendLocationEvent(name: "AskAlexaMsgQueueDelete", value: "${app?.label}", isStateChange: true, unit: msgId, data: queues) }else{ LogAction("removeAskAlexaQueueMsg: Removing Message ID (${msgId}) from the Ask Alexa Queue", sINFO, true) sendLocationEvent(name: "AskAlexaMsgQueueDelete", value: "${app?.label}", isStateChange: true, unit: msgId) } } } */ void scheduleAlarmOn(String autoType){ LogAction("scheduleAlarmOn: autoType: $autoType a1DelayVal: ${getAlert1DelayVal(autoType)}", sDEBUG, false) Integer timeVal=getAlert1DelayVal(autoType) Boolean ok2Notify=true //setting?."${autoType}UseParentNotifRestrictions" != false ? getOk2Notify(autoType) : getOk2Notify(autoType) //parent?.getOk2Notify() LogAction("scheduleAlarmOn timeVal: $timeVal ok2Notify: $ok2Notify", sINFO, false) if(ok2Notify){ if(timeVal > 0){ runIn(timeVal, "alarm0FollowUp", [data: ["autoType": autoType]]) LogAction("scheduleAlarmOn: Scheduling Alarm Followup 0 in timeVal: $timeVal", sINFO, false) state."${autoType}AlarmActive"=true }else{ LogAction("scheduleAlarmOn: Did not schedule ANY operation timeVal: $timeVal", sERR, true) } }else{ LogAction("scheduleAlarmOn: Could not schedule operation timeVal: $timeVal", sERR, true) } } @SuppressWarnings('unused') void alarm0FollowUp(Map val){ String autoType=val.autoType LogAction("alarm0FollowUp: autoType: $autoType 1 OffVal: ${getAlert1AlarmEvtOffVal(autoType)}", sDEBUG, false) Integer timeVal=getAlert1AlarmEvtOffVal(autoType) LogAction("alarm0FollowUp timeVal: $timeVal", sINFO, false) if(timeVal > 0 && sendEventAlarmAction(1, autoType)){ runIn(timeVal, "alarm1FollowUp", [data: ["autoType": autoType]]) LogAction("alarm0FollowUp: Scheduling Alarm Followup 1 in timeVal: $timeVal", sINFO, false) }else{ LogAction ("alarm0FollowUp: Could not schedule operation timeVal: $timeVal", sERR, true) } } @SuppressWarnings('unused') void alarm1FollowUp(Map val){ String autoType=val.autoType LogAction("alarm1FollowUp autoType: $autoType a2DelayVal: ${getAlert2DelayVal(autoType)}", sDEBUG, false) def aDev=settings["${autoType}AlarmDevices"] if(aDev){ aDev?.off() storeLastAction("Set Alarm OFF", getDtNow(), sBLANK) LogAction("alarm1FollowUp: Turning OFF ${aDev}", sINFO, false) } Integer timeVal=getAlert2DelayVal(autoType) //if(canSchedule() && (settings["${autoType}_Alert_2_Use_Alarm"] && timeVal > 0)){ if(timeVal > 0){ runIn(timeVal, "alarm2FollowUp", [data: ["autoType": autoType]]) LogAction("alarm1FollowUp: Scheduling Alarm Followup 2 in timeVal: $timeVal", sINFO, false) }else{ LogAction ("alarm1FollowUp: Could not schedule operation timeVal: $timeVal", sERR, true) } } @SuppressWarnings('unused') void alarm2FollowUp(Map val){ String autoType=val.autoType LogAction("alarm2FollowUp: autoType: $autoType 2 OffVal: ${getAlert2AlarmEvtOffVal(autoType)}", sDEBUG, false) Integer timeVal=getAlert2AlarmEvtOffVal(autoType) if(timeVal > 0 && sendEventAlarmAction(2, autoType)){ runIn(timeVal, "alarm3FollowUp", [data: ["autoType": autoType]]) LogAction("alarm2FollowUp: Scheduling Alarm Followup 3 in timeVal: $timeVal", sINFO, false) }else{ LogAction ("alarm2FollowUp: Could not schedule operation timeVal: $timeVal", sERR, true) } } void alarm3FollowUp(Map val){ String autoType=val.autoType LogAction("alarm3FollowUp: autoType: $autoType", sDEBUG, false) def aDev=settings["${autoType}AlarmDevices"] if(aDev){ aDev?.off() storeLastAction("Set Alarm OFF", getDtNow(), sBLANK) LogAction("alarm3FollowUp: Turning OFF ${aDev}", sINFO, false) } state."${autoType}AlarmActive"=false } def alarmEvtSchedCleanup(String autoType){ if(state."${autoType}AlarmActive"){ LogAction("Cleaning Up Alarm Event Schedules autoType: $autoType", sINFO, false) List items=["alarm0FollowUp","alarm1FollowUp", "alarm2FollowUp", "alarm3FollowUp"] items.each { unschedule("$it") } def val=[ "autoType": autoType ] alarm3FollowUp(val) } } Boolean sendEventAlarmAction(Integer evtNum, String autoType){ LogAction("sendEventAlarmAction evtNum: $evtNum autoType: $autoType", sINFO, false) Boolean resval; resval=false try { Boolean allowNotif=settings."${autoType}NotifOn" ? true : false Boolean allowAlarm=allowNotif && settings."${autoType}AllowAlarmNotif" def aDev=settings["${autoType}AlarmDevices"] if(allowNotif && allowAlarm && aDev){ //if(settings["${autoType}_Alert_${evtNum}_Use_Alarm"]){ resval=true def alarmType=settings["${autoType}_Alert_${evtNum}_AlarmType"].toString() switch (alarmType){ case "both": state."${autoType}alarmEvt${evtNum}StartDt"=getDtNow() aDev?.both() storeLastAction("Set Alarm BOTH ON", getDtNow(), autoType) break case "siren": state."${autoType}alarmEvt${evtNum}StartDt"=getDtNow() aDev?.siren() storeLastAction("Set Alarm SIREN ON", getDtNow(), autoType) break case "strobe": state."${autoType}alarmEvt${evtNum}StartDt"=getDtNow() aDev?.strobe() storeLastAction("Set Alarm STROBE ON", getDtNow(), autoType) break default: resval=false break } //} } } catch (ex){ log.error "sendEventAlarmAction Exception: ($evtNum) - ${ex?.message}" //parent?.sendExceptionData(ex, "sendEventAlarmAction", true, getAutoType()) } return resval } void alarmAlertEvt(evt){ LogAction("alarmAlertEvt: ${evt.displayName} Alarm State is Now (${evt.value})", sDEBUG, false) } Integer getAlert1DelayVal(String autoType){ return !settings["${autoType}_Alert_1_Delay"] ? 300 : (settings["${autoType}_Alert_1_Delay"].toInteger()) } Integer getAlert2DelayVal(String autoType){ return !settings["${autoType}_Alert_2_Delay"] ? 300 : (settings["${autoType}_Alert_2_Delay"].toInteger()) } Integer getAlert1AlarmEvtOffVal(String autoType){ return !settings["${autoType}_Alert_1_Alarm_Runtime"] ? 10 : (settings["${autoType}_Alert_1_Alarm_Runtime"].toInteger()) } Integer getAlert2AlarmEvtOffVal(String autoType){ return !settings["${autoType}_Alert_2_Alarm_Runtime"] ? 10 : (settings["${autoType}_Alert_2_Alarm_Runtime"].toInteger()) } /* Integer getAlarmEvt1RuntimeDtSec(){ return !state.alarmEvt1StartDt ? 100000 : GetTimeDiffSeconds(state.alarmEvt1StartDt).toInteger() } Integer getAlarmEvt2RuntimeDtSec(){ return !state.alarmEvt2StartDt ? 100000 : GetTimeDiffSeconds(state.alarmEvt2StartDt).toInteger() } */ /* void sendTTS(txt, pName){ LogAction("sendTTS(data: ${txt})", sDEBUG, false) try { def msg=txt?.toString()?.replaceAll("\\[|\\]|\\(|\\)|\\'|\\_", sBLANK) def spks=settings."${pName}SpeechDevices" def meds=settings."${pName}SpeechMediaPlayer" def echos=settings."${pName}EchoDevices" def res=settings."${pName}SpeechAllowResume" def vol=settings."${pName}SpeechVolumeLevel" LogAction("sendTTS msg: $msg | speaks: $spks | medias: $meds | echos: $echos| resume: $res | volume: $vol", sDEBUG, false) if(settings."${pName}AllowSpeechNotif"){ if(spks){ spks*.speak(msg) } if(meds){ meds?.each { if(res){ def currentStatus=it.latestValue('status') def currentTrack=it.latestState("trackData")?.jsonValue def currentVolume=it.latestState("level")?.integerValue ? it.currentState("level")?.integerValue : 0 if(vol){ it?.playTextAndResume(msg, vol?.toInteger()) }else{ it?.playTextAndResume(msg) } } else { it?.playText(msg) } } } if(echos){ echos*.setVolumeAndSpeak(settings."${pName}SpeechVolumeLevel", msg as String) } } } catch (ex){ log.error "sendTTS Exception:", ex //parent?.sendExceptionData(ex, "sendTTS", true, getAutoType()) } } */ def scheduleTimeoutRestore(String pName){ Integer timeOutVal=settings["${pName}OffTimeout"]?.toInteger() if(timeOutVal && !state."${pName}TimeoutScheduled"){ runIn(timeOutVal.toInteger(), "restoreAfterTimeOut", [data: [pName:pName]]) LogAction("Mode Restoration Timeout Scheduled ${pName} (${getEnumValue(longTimeSecEnum(), settings."${pName}OffTimeout")})", sINFO, true) state."${pName}TimeoutScheduled"=true } } def unschedTimeoutRestore(String pName){ Integer timeOutVal=settings["${pName}OffTimeout"]?.toInteger() if(timeOutVal && state."${pName}TimeoutScheduled"){ unschedule("restoreAfterTimeOut") LogAction("Cancelled Scheduled Mode Restoration Timeout ${pName}", sINFO, false) } state."${pName}TimeoutScheduled"=false } @SuppressWarnings('unused') def restoreAfterTimeOut(val){ String pName=val?.pName?.value if(pName && settings."${pName}OffTimeout"){ switch(pName){ case "conWat": state."${pName}TimeoutScheduled"=false conWatCheck(true) break //case "leakWat": //leakWatCheck(true) //break case "extTmp": state."${pName}TimeoutScheduled"=false extTmpTempCheck(true) break default: LogAction("restoreAfterTimeOut no pName match ${pName}", sERR, true) break } } } Boolean checkThermostatDupe(tstatOne, tstatTwo){ if(tstatOne && tstatTwo){ String pTstat=tstatOne?.deviceNetworkId?.toString() List mTstatAr=[] tstatTwo?.each { ts -> mTstatAr << ts.deviceNetworkId?.toString() } if(pTstat in mTstatAr){ return true } } return false } static Boolean checkModeDuplication(modeOne, modeTwo){ Boolean result; result=false if(modeOne && modeTwo){ modeOne?.each { dm -> if(dm in modeTwo){ result=true } } } return result } private List getDeviceSupportedCommands(dev){ return dev?.supportedCommands?.findAll { it as String } } Boolean checkFanSpeedSupport(dev){ List req=["setSpeed"] Integer devCnt; devCnt=0 List devData=getDeviceSupportedCommands(dev) devData.each { cmd -> if(cmd.name in req){ devCnt=devCnt+1 } } def t0=dev?.currentSpeed def speed=t0 ?: null //log.debug "checkFanSpeedSupport (speed: $speed | devCnt: $devCnt)" return speed && devCnt==1 } void getTstatCapabilities(tstat, String autoType, Boolean dyn=false){ try { if(tstat) { Boolean canCool= !!(tstat.currentCanCool?.toBoolean()) Boolean canHeat= !!(tstat.currentCanHeat?.toBoolean()) Boolean hasFan= !!(tstat.currentHasFan?.toBoolean()) state."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : sBLANK}TstatCanCool"=canCool state."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : sBLANK}TstatCanHeat"=canHeat state."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : sBLANK}TstatHasFan"=hasFan } } catch (ex){ log.error "getTstatCapabilities Exception: ${ex?.message}" } } Map getSafetyTemps(tstat, Boolean usedefault=true){ Double minTemp, maxTemp minTemp=tstat?.currentSafetyTempMin?.doubleValue maxTemp=tstat?.currentSafetyTempMax?.doubleValue if(minTemp == 0.0D){ if(usedefault){ minTemp=(getTemperatureScale() == "C") ? 7.0D : 45.0D } else { minTemp=null } } if(maxTemp == 0.0D){ maxTemp=null } if(minTemp || maxTemp){ return ["min":minTemp, "max":maxTemp] } return null } Double getComfortDewpoint(tstat, Boolean usedefault=true){ Double maxDew maxDew=tstat?.currentComfortDewpointMax?.doubleValue maxDew=maxDew ?: 0.0D if(maxDew == 0.0D){ if(usedefault){ maxDew=(getTemperatureScale() == "C") ? 19.0D : 66.0D return maxDew } return null } return maxDew } Boolean getSafetyTempsOk(tstat){ Map sTemps=getSafetyTemps(tstat) //log.debug "sTempsOk: $sTemps" if(sTemps && tstat){ Double curTemp=tstat.currentTemperature?.toDouble() //log.debug "curTemp: ${curTemp}" if( ((sTemps.min!=null && sTemps.min.toDouble() != 0.0D) && (curTemp < sTemps.min.toDouble())) || ((sTemps.max!=null && sTemps.max.toDouble() != 0.0D) && (curTemp > sTemps.max.toDouble())) ){ return false } } //else{ log.debug "getSafetyTempsOk: no safety Temps" } return true } Double getGlobalDesiredHeatTemp(){ Double t0=null //parent?.settings.locDesiredHeatTemp?.toDouble() return t0 ?: null } Double getGlobalDesiredCoolTemp(){ Double t0=null // parent?.settings.locDesiredCoolTemp?.toDouble() return t0 ?: null } /* def getClosedContacts(contacts){ if(contacts){ def cnts=contacts?.findAll { it?.currentContact == "closed" } return cnts ?: null } return null } */ List getOpenContacts(contacts){ if(contacts){ List cnts=contacts?.findAll { it?.currentContact == "open" } return cnts ?: null } return null } /* def getDryWaterSensors(sensors){ if(sensors){ def cnts=sensors?.findAll { it?.currentWater == "dry" } return cnts ?: null } return null } */ List getWetWaterSensors(List sensors){ if(sensors){ List cnts=sensors?.findAll { it?.currentWater == "wet" } return cnts ?: null } return null } /* Boolean isContactOpen(con){ Boolean res=false if(con){ if(con?.currentSwitch == sON){ res=true } } return res } */ Boolean allDevAttValsEqual(List devs, String att, val) { if(devs && att) { if(val instanceof List) return (devs.findAll { it?.currentValue(att) in val }?.size() == devs.size()) else return (devs.findAll { it?.currentValue(att) == val }?.size() == devs.size()) } return false } Boolean anyDevAttValsEqual(List devs, String att, val) { Boolean res=false if(devs && att) { Boolean isList = (val instanceof List) for(dev in devs) { if(isList) { if(dev.currentValue(att) in (List)val) { res=true; break } } else if (dev.currentValue(att) == val) { res=true; break } } } return res } Boolean devAttValEqual(dev, String att, val) { if(dev && att) { return (dev.currentValue(att) == val) } return false } Boolean isSwitchOn(dev){ return devAttValEqual(dev, sSWIT, sON) } Boolean isPresenceHome(List presSensor){ return anyDevAttValsEqual(presSensor, sPRESENCE, sPRESENT) } Boolean isSomebodyHome(List sensors){ return anyDevAttValsEqual(sensors, sPRESENCE, sPRESENT) } String getTstatPresence(tstat){ String pres pres="not present" if(tstat){ pres=tstat?.currentPresence } return pres } Boolean setTstatMode(tstat, String mode, String autoType=sNULL){ Boolean result result=false if(mode && tstat){ String curMode=tstat?.currentThermostatMode?.toString() if(curMode != mode){ try { if(mode == sAUTO){ tstat.auto(); result=true } else if(mode == sHEAT){ tstat.heat(); result=true } else if(mode == sCOOL){ tstat.cool(); result=true } else if(mode == sOFF){ tstat.off(); result=true } else { if(mode == sECO){ tstat.eco(); result=true LogTrace("setTstatMode mode action | type: $autoType") } } } catch (ex){ log.error "setTstatMode() Exception: ${tstat?.label} does not support mode ${mode}; check IDE and install instructions ${ex?.message}" //parent?.sendExceptionData(ex, "setTstatMode", true, getAutoType()) } } if(result){ LogAction("setTstatMode: '${tstat?.label}' Mode set to (${strCapitalize(mode)})", sINFO, false) } else { LogAction("setTstatMode() | No Mode change: ${mode}", sINFO, false) } }else{ LogAction("setTstatMode() | Invalid or Missing Mode received: ${mode}", sWARN, true) } return result } Boolean setMultipleTstatMode(tstats, String mode, String autoType=sNULL){ Boolean result result=false if(tstats && mode){ tstats?.each { ts -> Boolean retval // try { retval=setTstatMode(ts, mode, autoType) // } catch (ex){ // log.error "setMultipleTstatMode() Exception:", ex // parent.sendExceptionData(ex, "setMultipleTstatMode", true, getAutoType()) // } if(retval){ LogAction("Setting ${ts?.displayName} Mode to (${mode})", sINFO, false) storeLastAction("Set ${ts?.displayName} to (${mode})", getDtNow(), autoType) result=true }else{ LogAction("Failed Setting ${ts} Mode to (${mode})", sWARN, true) return false } } }else{ LogAction("setMultipleTstatMode(${tstats}, $mode, $autoType) | Invalid or Missing tstats or Mode received: ${mode}", sWARN, true) } return result } Boolean setTstatAutoTemps(tstat, Double coolSetpoint, Double heatSetpoint, String pName, mir=null){ Boolean retVal; retVal=false String setStr; setStr="No thermostat device" Boolean heatFirst Boolean setHeat; setHeat=null Boolean setCool; setCool=null String hvacMode; hvacMode="unknown" Double reqCool, reqHeat, curCoolSetpoint, curHeatSetpoint reqCool=null reqHeat=null curCoolSetpoint=null curHeatSetpoint=null String tempScaleStr=tUnitStr() if(tstat){ hvacMode=tstat.currentThermostatMode.toString() // LogTrace(tStr) retVal=true setStr="Error: " curCoolSetpoint=getTstatSetpoint(tstat, sCOOL) curHeatSetpoint=getTstatSetpoint(tstat, sHEAT) Double diff=getTemperatureScale() == "C" ? 2.0 : 3.0 reqCool=coolSetpoint ?: null reqHeat=heatSetpoint ?: null if(!reqCool && !reqHeat){ retVal=false; setStr += "Missing COOL and HEAT Setpoints" } if(hvacMode in [sAUTO]){ if(!reqCool && reqHeat){ reqCool=(Double) ((curCoolSetpoint > (reqHeat + diff)) ? curCoolSetpoint : (reqHeat + diff)) } if(!reqHeat && reqCool){ reqHeat=(Double) ((curHeatSetpoint < (reqCool - diff)) ? curHeatSetpoint : (reqCool - diff)) } if((reqCool && reqHeat) && (reqCool >= (reqHeat + diff))){ if(reqHeat <= curHeatSetpoint){ heatFirst=true } else if(reqCool >= curCoolSetpoint){ heatFirst=false } else if(reqHeat > curHeatSetpoint){ heatFirst=false } else { heatFirst=true } if(heatFirst){ if(reqHeat != curHeatSetpoint){ setHeat=true } if(reqCool != curCoolSetpoint){ setCool=true } }else{ if(reqCool != curCoolSetpoint){ setCool=true } if(reqHeat != curHeatSetpoint){ setHeat=true } } }else{ setStr += " or COOL/HEAT is not separated by ${diff}" retVal=false } }else if(hvacMode in [sCOOL] && reqCool){ if(reqCool != curCoolSetpoint){ setCool=true } }else if(hvacMode in [sHEAT] && reqHeat){ if(reqHeat != curHeatSetpoint){ setHeat=true } }else{ setStr += "incorrect HVAC Mode (${hvacMode})" retVal=false } } if(retVal){ setStr="Setting: " if(heatFirst && setHeat){ setStr += "heatSetpoint: (${reqHeat}${tempScaleStr}) " if(reqHeat != curHeatSetpoint){ tstat?.setHeatingSetpoint(reqHeat) storeLastAction("Set ${tstat} Heat Setpoint ${reqHeat}${tempScaleStr}".toString(), getDtNow(), pName) if(mir){ mir*.setHeatingSetpoint(reqHeat) } } } if(setCool){ setStr += "coolSetpoint: (${reqCool}${tempScaleStr}) " if(reqCool != curCoolSetpoint){ tstat?.setCoolingSetpoint(reqCool) storeLastAction("Set ${tstat} Cool Setpoint ${reqCool}".toString(), getDtNow(), pName) if(mir){ mir*.setCoolingSetpoint(reqCool) } } } if(!heatFirst && setHeat){ setStr += "heatSetpoint: (${reqHeat}${tempScaleStr})" if(reqHeat != curHeatSetpoint){ tstat?.setHeatingSetpoint(reqHeat) storeLastAction("Set ${tstat} Heat Setpoint ${reqHeat}${tempScaleStr}".toString(), getDtNow(), pName) if(mir){ mir*.setHeatingSetpoint(reqHeat) } } } } String tStr="setTstatAutoTemps: [tstat: ${tstat?.displayName} | Mode: ${hvacMode} | coolSetpoint: ${coolSetpoint}${tempScaleStr} | heatSetpoint: ${heatSetpoint}${tempScaleStr}] " LogAction(tStr+setStr, retVal ? sINFO : sWARN, true) return retVal } /****************************************************************************** * Keep These Methods * *******************************************************************************/ /* def switchEnumVals(){ return [0:"Off", 1:"On", 2:"On/Off"] } def longTimeMinEnum(){ def vals=[ 1:"1 Minute", 2:"2 Minutes", 3:"3 Minutes", 4:"4 Minutes", 5:"5 Minutes", 10:"10 Minutes", 15:"15 Minutes", 20:"20 Minutes", 25:"25 Minutes", 30:"30 Minutes", 45:"45 Minutes", 60:"1 Hour", 120:"2 Hours", 240:"4 Hours", 360:"6 Hours", 720:"12 Hours", 1440:"24 Hours" ] return vals } */ static Map fanTimeSecEnum(){ Map vals=[ 60:"1 Minute", 120:"2 Minutes", 180:"3 Minutes", 240:"4 Minutes", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes" ] return vals } static Map longTimeSecEnum(){ Map vals=[ 0:"Off", 60:"1 Minute", 120:"2 Minutes", 180:"3 Minutes", 240:"4 Minutes", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes", 1800:"30 Minutes", 2700:"45 Minutes", 3600:"1 Hour", 7200:"2 Hours", 14400:"4 Hours", 21600:"6 Hours", 43200:"12 Hours", 86400:"24 Hours", 10:"10 Seconds(Testing)" ] return vals } static Map shortTimeEnum(){ Map vals=[ 1:"1 Second", 2:"2 Seconds", 3:"3 Seconds", 4:"4 Seconds", 5:"5 Seconds", 6:"6 Seconds", 7:"7 Seconds", 8:"8 Seconds", 9:"9 Seconds", 10:"10 Seconds", 15:"15 Seconds", 30:"30 Seconds", 60:"60 Seconds" ] return vals } Map switchRunEnum(Boolean addAlways=false){ String pName=schMotPrefix() Boolean hasFan=(Boolean)state."${pName}TstatHasFan" Boolean canCool=(Boolean)state."${pName}TstatCanCool" Boolean canHeat=(Boolean)state."${pName}TstatCanHeat" Map vals=[ 1:"Any operation: Heating or Cooling" ] if(hasFan){ vals << [2:"With HVAC Fan Only"] } if(canHeat){ vals << [3:"Heating"] } if(canCool){ vals << [4:"Cooling"] } if(addAlways){ vals << [5:"Any Operating or non-operating State"] } return vals } Map fanModeTrigEnum(){ String pName=schMotPrefix() Boolean canCool=(Boolean)state."${pName}TstatCanCool" Boolean canHeat=(Boolean)state."${pName}TstatCanHeat" Map vals=[(sAUTO):"Auto", (sCOOL):"Cool", (sHEAT):"Heat", (sECO):"Eco", "any":"Any Mode"] if(!canHeat){ vals=[(sCOOL):"Cool", (sECO):"Eco", "any":"Any Mode"] } if(!canCool){ vals=[(sHEAT):"Heat", (sECO):"Eco", "any":"Any Mode"] } return vals } static Map tModeHvacEnum(Boolean canHeat, Boolean canCool, Boolean canRtn=false){ Map vals=[(sAUTO):"Auto", (sCOOL):"Cool", (sHEAT):"Heat", (sECO):"Eco"] if(!canHeat){ vals=[(sCOOL):"Cool", (sECO):"Eco"] } if(!canCool){ vals=[(sHEAT):"Heat", (sECO):"Eco"] } if(canRtn){ vals << ["rtnFromEco":"Return from ECO if in ECO"] } return vals } static Map alarmActionsEnum(){ Map vals=["siren":"Siren", "strobe":"Strobe", "both":"Both (Siren/Strobe)"] return vals } static def getEnumValue(Map enumName, inputName){ def result; result="unknown" List resultList=[] Boolean inputIsList= getObjType(inputName) == "List" if(enumName){ enumName?.each { item -> if(inputIsList){ inputName?.each { inp -> if(item.key.toString() == inp?.toString()){ resultList.push(item.value) } } }else if(item.key.toString() == inputName?.toString()){ result=item.value } } } if(inputIsList){ return resultList }else{ return result } } /* def getSunTimeState(){ def tz=TimeZone.getTimeZone(location.timeZone.ID) def sunsetTm=Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", location?.currentValue('sunsetTime')).format('h:mm a', tz) def sunriseTm=Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", location?.currentValue('sunriseTime')).format('h:mm a', tz) state.sunsetTm=sunsetTm state.sunriseTm=sunriseTm } def parseDt(String format, dt){ def result Date newDt=Date.parse(format, dt) result=formatDt(newDt) //log.debug "result: $result" return result } */ /****************************************************************************** * STATIC METHODS * *******************************************************************************/ //def getAutoAppChildName() { return "Nest Automations" } String getWatDogAppChildName() { return "Nest Location ${location.name} Watchdog" } //def getChildName(str) { return "${str}" } String getChildAppVer(appName){ return appName?.appVersion() ? "v${appName?.appVersion()}" : sBLANK } //Boolean getUse24Time() { return settings.useMilitaryTime } //Returns app State Info Integer getStateSize(){ String resultJson= JsonOutput.toJson((Map)state) return resultJson?.length() } Integer getStateSizePerc() { return (Integer) ((stateSize / 100000)*100).toDouble().round(0) } List getLocationModes(){ List result=[] location.modes.sort().each { if(it){ result.push("${it}") } } return result } static String getObjType(obj){ if(obj instanceof String){return "String"} else if(obj instanceof Map){return "Map"} else if(obj instanceof List){return "List"} else if(obj instanceof ArrayList){return "ArrayList"} else if(obj instanceof Integer){return "Integer"} else if(obj instanceof BigInteger){return "BigInteger"} else if(obj instanceof Long){return "Long"} else if(obj instanceof Boolean){return "Boolean"} else if(obj instanceof BigDecimal){return "BigDecimal"} else if(obj instanceof Float){return "Float"} else if(obj instanceof Byte){return "Byte"} else { return "unknown"} } //static Map preStrObj(){ [1:"•", 2:"│", 3:"├", 4:"└", 5:" "] } //def getShowHelp(){ return state.showHelp == false ? false : true } TimeZone getTimeZone(){ TimeZone tz; tz=null if(location?.timeZone){ tz=(TimeZone)location.timeZone } //else { tz=getNestTimeZone() ? TimeZone.getTimeZone(getNestTimeZone()) : null } if(!tz){ LogAction("getTimeZone: Hub or Nest TimeZone not found", sWARN, true) } return tz } String formatDt(Date dt){ SimpleDateFormat tf=new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy") if(getTimeZone()){ tf.setTimeZone(getTimeZone()) } else { LogAction("HE TimeZone is not set; Please open your location and Press Save", sWARN, true) } return (String)tf.format(dt) } String getGlobTitleStr(typ){ return "Desired Default ${typ} Temp (${tUnitStr()})" } String formatDt2(String tm){ //def formatVal=settings.useMilitaryTime ? "MMM d, yyyy - HH:mm:ss" : "MMM d, yyyy - h:mm:ss a" String formatVal="MMM d, yyyy - h:mm:ss a" SimpleDateFormat tf=new SimpleDateFormat(formatVal) if(getTimeZone()){ tf.setTimeZone(getTimeZone()) } return (String)tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", tm)) } String tUnitStr(){ return "\u00b0"+(String)getTemperatureScale() } /* void updTimestampMap(keyName, dt=null){ def data=state.timestampDtMap ?: [:] if(keyName){ data[keyName]=dt } state.timestampDtMap=data } def getTimestampVal(val){ def tsData=state.timestampDtMap if(val && tsData && tsData[val]){ return tsData[val] } return null } */ private Integer getTimeSeconds(String timeKey, Integer defVal, String meth){ String t0=state."${timeKey}" //=getTimestampVal(timeKey) return !t0 ? defVal : GetTimeDiffSeconds(t0, sNULL, meth).toInteger() } Long GetTimeDiffSeconds(String strtDate, String stpDate=sNULL, String methName=sNULL){ //LogTrace("[GetTimeDiffSeconds] StartDate: $strtDate | StopDate: ${stpDate ?: "Not Sent"} | MethodName: ${methName ?: "Not Sent"})") if((strtDate && !stpDate) || (strtDate && stpDate)){ //if(strtDate?.contains("dtNow")){ return 10000 } Date now=new Date() String stopVal=stpDate ? stpDate.toString() : formatDt(now) Long start=Date.parse("E MMM dd HH:mm:ss z yyyy", strtDate).getTime() Long stop=Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal).getTime() Long diff=Math.round((stop - start) / 1000L) LogTrace("[GetTimeDiffSeconds] Results for '$methName': ($diff seconds)") return diff }else{ return null } } /* Boolean daysOk(days){ if(days){ SimpleDateFormat dayFmt=new SimpleDateFormat("EEEE") if(getTimeZone()){ dayFmt.setTimeZone(getTimeZone()) } return days.contains(dayFmt.format(new Date())) ? false : true }else{ return true } } */ String time2Str(String time){ if(time){ Date t=(Date)timeToday(time, getTimeZone()) SimpleDateFormat f=new SimpleDateFormat("h:mm a") f.setTimeZone(getTimeZone() ?: TimeZone.getDefault()) //timeZone(time)) return (String)f.format(t) } return sNULL } /* String epochToTime(Long tm){ SimpleDateFormat tf=new SimpleDateFormat("h:mm a") tf?.setTimeZone(getTimeZone()) return tf.format(tm) } */ String getDtNow(){ Date now=new Date() return formatDt(now) } /* Boolean modesOk(List modeEntry){ Boolean res=true if(modeEntry){ modeEntry?.each { m -> if(m.toString() == location.mode.toString()){ res=false } } } return res } */ Boolean isInMode(List modeList){ if(modeList){ //log.debug "mode (${location.mode}) in list: ${modeList} | result: (${location.mode in modeList})" return location.mode.toString() in modeList } return false } /* static Map notifValEnum(Boolean allowCust=true){ Map valsC=[ 60:"1 Minute", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes", 1800:"30 Minutes", 3600:"1 Hour", 7200:"2 Hours", 14400:"4 Hours", 21600:"6 Hours", 43200:"12 Hours", 86400:"24 Hours", 1000000:"Custom" ] Map vals=[ 60:"1 Minute", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes", 1800:"30 Minutes", 3600:"1 Hour", 7200:"2 Hours", 14400:"4 Hours", 21600:"6 Hours", 43200:"12 Hours", 86400:"24 Hours" ] return allowCust ? valsC : vals } static Map pollValEnum(){ Map vals=[ 60:"1 Minute", 120:"2 Minutes", 180:"3 Minutes", 240:"4 Minutes", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes", 1800:"30 Minutes", 2700:"45 Minutes", 3600:"60 Minutes" ] return vals } static Map waitValEnum(){ Map vals=[ 1:"1 Second", 2:"2 Seconds", 3:"3 Seconds", 4:"4 Seconds", 5:"5 Seconds", 6:"6 Seconds", 7:"7 Seconds", 8:"8 Seconds", 9:"9 Seconds", 10:"10 Seconds", 15:"15 Seconds", 30:"30 Seconds" ] return vals } */ static String strCapitalize(str){ return str ? str.toString().capitalize() : sNULL } /* static String getInputEnumLabel(inputName, enumName){ String result="Not Set" if(inputName && enumName){ enumName.each { item -> if(item?.key.toString() == inputName?.toString()){ result=item?.value } } } return result } */ /* String toJson(Map m){ //return new org.json.JSONObject(m).toString() return new groovy.json.JsonOutput().toJson(m) } def toQueryString(Map m){ return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } */ /************************************************************************************************ | LOGGING AND Diagnostic | *************************************************************************************************/ static String lastN(String input, Integer n){ return n > input?.size() ? input : input[-n..-1] //return n > input?.size() ? input : n ? input[-n..-1] : '' } void LogTrace(GString msg, String logSrc=sNULL){ LogTrace(msg.toString(), logSrc) } void LogTrace(String msg, String logSrc=sNULL){ Boolean trOn=((Boolean)settings.showDebug && (Boolean)settings.advAppDebug) if(trOn){ Boolean logOn=((Boolean)settings.enRemDiagLogging && (Boolean)state.enRemDiagLogging) Logger(msg, sTRACE, logSrc, logOn) } } void LogAction(GString msg, String type=sDEBUG, Boolean showAlways=false, String logSrc=sNULL){ LogAction(msg.toString(), type, showAlways, logSrc) } void LogAction(String msg, String type=sDEBUG, Boolean showAlways=false, String logSrc=sNULL){ def isDbg=(Boolean)settings.showDebug if(showAlways || isDbg){ Logger(msg, type, logSrc) } } void Logger(String msg, String type=sDEBUG, String logSrc=sNULL, Boolean noSTlogger=false){ if(msg && type){ String labelstr; labelstr=sBLANK if((Boolean)state.dbgAppndName == null){ Boolean tval=parent ? parent.getSettingVal("dbgAppndName") : settings.dbgAppndName state.dbgAppndName=(tval || tval == null) } String t0=app.label if((Boolean)state.dbgAppndName){ labelstr=t0+' | ' } String themsg=labelstr+msg //log.debug "Logger remDiagTest: $msg | $type | $logSrc" if(state.enRemDiagLogging == null){ state.enRemDiagLogging=parent.getStateVal("enRemDiagLogging") if(state.enRemDiagLogging == null){ state.enRemDiagLogging=false } //log.debug "set enRemDiagLogging to ${state.enRemDiagLogging}" } if((Boolean)state.enRemDiagLogging){ String theId=lastN(app.id.toString(),5) String theLogSrc=(logSrc == sNULL) ? (parent ? 'Automation-'+theId : "NestManager") : logSrc parent.saveLogtoRemDiagStore(themsg, type, theLogSrc) }else{ if(!noSTlogger){ switch(type){ case sINFO: log.info sSPACE + logPrefix(themsg, "#0299b1") break case sTRACE: log.trace logPrefix(themsg, sCLRGRY) break case sERR: log.error logPrefix(themsg, sCLRRED) break case sWARN: log.warn sSPACE + logPrefix(themsg, sCLRORG) break case sDEBUG: default: log.debug logPrefix(themsg, "purple") } } } }else{ log.error "${labelstr}Logger Error - type: ${type} | msg: ${msg} | logSrc: ${logSrc}" } } static String logPrefix(String msg, String color = sNULL) { return span("Automation (v" + appVersion() + ") | ", sCLRGRY) + span(msg, color) } static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLANK}" : sBLANK } /////////////////////////////////////////////////////////////////////////////// /****************************************************************************** | Application Help and License Info Variables | *******************************************************************************/ /////////////////////////////////////////////////////////////////////////////// static String appName() { return appLabel() } static String appLabel() { return "NST Automations" } static String gitRepo() { return "tonesto7/nest-manager"} static String gitBranch() { return "master" } static String gitPath() { return gitRepo()+'/'+gitBranch() }