Skip to content

Instantly share code, notes, and snippets.

@imnotbob
Created September 27, 2019 12:42
Show Gist options
  • Save imnotbob/013c08d197eda567aa456725a9dd0839 to your computer and use it in GitHub Desktop.
Save imnotbob/013c08d197eda567aa456725a9dd0839 to your computer and use it in GitHub Desktop.
bug fix test
/********************************************************************************************
| 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 |
| |
| September 26, 2019 |
| License Info: https://github.com/tonesto7/nest-manager/blob/master/app_license.txt |
|********************************************************************************************/
import groovy.json.*
import java.text.SimpleDateFormat
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: "",
iconX2Url: "",
iconX3Url: "",
importUrl: "https://raw.githubusercontent.com/tonesto7/nst-manager-he/master/apps/nstAutomations.groovy")
String appVersion() { "2.0.6" }
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 != true) {
//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()
return
}
void updated() {
log.debug "${app.getLabel()} Updated...with settings: ${settings}"
state?.isInstalled = true
String appLbl = getCurAppLbl()
if(appLbl?.contains("Watchdog")) {
if(!state?.autoTyp) { state.autoTyp = "watchDog" }
}
initialize()
state?.lastUpdatedDt = getDtNow()
return
}
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 adj_temp(tempF) {
if(getTemperatureScale() == "C") {
return ((tempF - 32) * ( 5/9 )) as Double
} else {
return tempF
}
}
void setMyLockId(val) {
if(state?.myID == null && parent && val) {
state.myID = val
}
}
String getMyLockId() {
if(parent) { return state?.myID } else { return null }
}
/*
def fixState() {
def 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", "info", true)
state.resetAllData = false
}
if(result) {
state.resetAllData = true
LogAction("fixState: State Data: before: $before after: ${getStateSizePerc()}", "info", 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", "info", true)
getTstatCapabilities(tstat, schMotPrefix())
if(!getMyLockId()) {
setMyLockId(app.id)
}
if(settings?.schMotRemoteSensor) {
LogAction("finishFixState found remote sensor", "info", true)
if( parent?.remSenLock(tstat?.deviceNetworkId, getMyLockId()) ) { // lock new ID
state?.remSenTstat = tstat?.deviceNetworkId
}
if(isRemSenConfigured() && settings?.remSensorDay) {
LogAction("finishFixState found remote sensor configured", "info", true)
if(settings?.vthermostat != null) { parent?.addRemoveVthermostat(tstat.deviceNetworkId, vthermostat, getMyLockId()) }
}
}
}
if(!migrate) { initAutoApp() }
//updated()
}
}
*/
def selectAutoPage() {
//LogTrace("selectAutoPage()")
if(!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: ""//, 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: "" //, params: ["aTyp": "schMot"]
}
}
}
else { return mainAutoPage( [aTyp: state?.autoTyp]) }
}
String sectionTitleStr(title) { return "<h3>$title</h3>" }
String inputTitleStr(title) { return "<u>$title</u>" }
String pageTitleStr(title) { return "<h1>$title</h1>" }
String paraTitleStr(title) { return "<b>$title</b>" }
//def imgTitle(imgSrc, imgWidth=30, imgHeight=null, titleStr, color=null) {
String imgTitle(String imgSrc, String titleStr, String color=null, imgWidth=30, imgHeight=null) {
String imgStyle = ""
imgStyle += imgWidth ? "width: ${imgWidth}px !important;" : ""
imgStyle += imgHeight ? "${imgWidth ? " " : ""}height: ${imgHeight}px !important;" : ""
if(color) { return """<div style="color: ${color}; font-weight: bold;"><img style="${imgStyle}" src="${imgSrc}"> ${titleStr}</img></div>""" }
else { return """<img style="${imgStyle}" src="${imgSrc}"> ${titleStr}</img>""" }
}
// string table for titles
String titles(String name, Object... args) {
def 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
String descriptions(name, Object... args) {
def element_descriptions = [
"d_ttc": "Tap to configure",
"d_ttm": "\n\nTap to modify"
]
if (args)
return String.format(element_descriptions[name],args)
else
return element_descriptions[name]
}
String icons(String name, napp="App") {
def icon_names = [
"i_dt": "delay_time",
"i_not": "notification",
"i_calf": "cal_filter",
"i_set": "settings",
"i_sw": "switch_on",
"i_mod": "mode",
"i_hmod": "hvac_mode",
"i_inst": "instruct",
"i_err": "error",
"i_cfg": "configure",
"i_t": "temperature"
//ERS
]
//return icon_names[name]
String t0 = icon_names?."${name}"
//LogAction("t0 ${t0}", "warn", true)
if(t0) return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${t0}_icon.png"
else return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${name}"
}
String getAppImg(String imgName, on = null) {
//return (!disAppIcons || on) ? "https://raw.githubusercontent.com/${gitPath()}/Images/App/$imgName" : ""
return (!disAppIcons || on) ? icons(imgName) : ""
}
String getDevImg(String imgName, on = null) {
//return (!disAppIcons || on) ? "https://raw.githubusercontent.com/${gitPath()}/Images/Devices/$imgName" : ""
return (!disAppIcons || on) ? icons(imgName, "Devices") : ""
}
def mainAutoPage1(params) {
//LogTrace("mainAutoPage1()")
def t0 = [:]
t0.aTyp = "nMode"
return mainAutoPage( t0 ) //[autoType: "nMode"])
}
def mainAutoPage2(params) {
//LogTrace("mainAutoPage2()")
def t0 = [:]
t0.aTyp = "schMot"
return mainAutoPage( t0 ) //[autoType: "schMot"])
}
def mainAutoPage(params) {
//LogTrace("mainAutoPage()")
String t0 = getTemperatureScale()?.toString()
state.tempUnit = (t0 != null) ? t0 : state?.tempUnit
if(!state.autoDisabled) { state.autoDisabled = false }
String autoType = (String) null
//If params.aTyp is not null then save to state.
if(!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 = 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(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: "complete" }
}
if(!getIsAutomationDisabled()) {
if(autoType == "nMode") {
//paragraph paraTitleStr("Set Nest Presence Based on location Modes, Presence Sensor, or Switches:")
String nDesc = ""
nDesc += isNestModesConfigured() ? "Nest Mode:\n • Status: (${strCapitalize(getNestLocPres())})" : ""
if(((!nModePresSensor && !nModeSwitch) && (nModeAwayModes && nModeHomeModes))) {
nDesc += nModeHomeModes ? "\n • Home Modes: (${nModeHomeModes.size()})" : ""
nDesc += nModeAwayModes ? "\n • Away Modes: (${nModeAwayModes.size()})" : ""
}
nDesc += (nModePresSensor && !nModeSwitch) ? "\n\n${nModePresenceDesc()}" : ""
nDesc += (nModeSwitch && !nModePresSensor) ? "\n • Using Switch: (State: ${isSwitchOn(nModeSwitch) ? "ON" : "OFF"})" : ""
nDesc += (settings?.nModeDelay && settings?.nModeDelayVal) ? "\n • Change Delay: (${getEnumValue(longTimeSecEnum(), settings?.nModeDelayVal)})" : ""
nDesc += (isNestModesConfigured() ) ? "\n • Restrictions Active: (${autoScheduleOk(getAutoType()) ? "NO" : "YES"})" : ""
if(isNestModesConfigured()) {
nDesc += "\n • Set Thermostats to ECO: (${nModeSetEco ? "On" : "Off"})"
if(parent.getSettingVal("cameras")) {
nDesc += "\n • Cams On when Away: (${nModeCamOnAway ? "On" : "Off"})"
nDesc += "\n • Cams Off when Home: (${nModeCamOffHome ? "On" : "Off"})"
if(settings?.nModeCamsSel) {
nDesc += "\n • Nest Cams Selected: (${nModeCamsSel.size()})"
}
}
}
String t1 = getNotifConfigDesc("nMode")
nDesc += t1 ? "\n\n${t1}" : ""
nDesc += t1 || (nModePresSensor || nModeSwitch) || (!nModePresSensor && !nModeSwitch && (nModeAwayModes && nModeHomeModes)) ? descriptions("d_ttm") : ""
String nModeDesc = isNestModesConfigured() ? "${nDesc}" : null
//Logger("nModeDesc ${nModeDesc}")
href "nestModePresPage", title: imgTitle(getAppImg("mode_automation_icon.png"), inputTitleStr("Nest Mode Automation Config")), description: nModeDesc ?: descriptions("d_ttc"), state: (nModeDesc ? "complete" : null)
}
if(autoType == "schMot") {
//Logger("calling schMot config and page")
//Logger("in mainAutoPage7")
String sModeDesc = getSchMotConfigDesc()
href "schMotModePage", title: imgTitle(getAppImg("thermostat_automation_icon.png"), inputTitleStr("Thermostat Automation Config")), description: sModeDesc ?: descriptions("d_ttc"), state: (sModeDesc ? "complete" : null)
}
if(autoType == "watchDog") {
//paragraph paraTitleStr("Watch your Nest Location for Events:")
String watDesc = ""
String t1 = getNotifConfigDesc("watchDog")
if(t1) {
def tstats = parent.getSettingVal("thermostats")
def prots = parent.getSettingVal("protects")
def 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)) {
def camStreamNotif = parent.getSettingVal("camStreamNotifMsg")
if(camStreamNotif != false) {
t1 += "\n • Notify on Camera Streaming status changes"
}
}
}
}
def camStreamNotif = parent.getSettingVal("locPresChangeMsg")
if(camStreamNotif != false) {
t1 += "\n • Notify Nest Home/Away Status changes"
}
}
watDesc += t1 ? "${t1}" + descriptions("d_ttm") : ""
String watDogDesc = isWatchdogConfigured() ? "${watDesc}" : null
href "watchDogPage", title: imgTitle(getAppImg("watchdog_icon.png"), inputTitleStr(titles("t_nlw"))), description: watDogDesc ?: descriptions("d_ttc"), state: (watDogDesc ? "complete" : null)
}
}
}
section(sectionTitleStr("Automation Options:")) {
if(/* state?.isInstalled && */ (isNestModesConfigured() || isWatchdogConfigured() || isSchMotConfigured())) {
//paragraph paraTitleStr("Enable/Disable this Automation")
input "autoDisabledreq", "bool", title: imgTitle(getAppImg("disable_icon2.png"), inputTitleStr("Disable this Automation?")), required: false, defaultValue: false /* state?.autoDisabled */, submitOnChange: true
setAutomationStatus()
}
input ("showDebug", "bool", 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: "bool", title: imgTitle(getAppImg("list_icon.png"), inputTitleStr("Show Verbose Logs?")), required: false, defaultValue: false, submitOnChange: true)
} else {
settingUpdate("advAppDebug", "false", "bool")
}
}
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."
}
}
}
}
}
String getSchMotConfigDesc(retAsList=false) {
def list = []
if(settings?.schMotWaterOff) { list.push("Turn Off if Leak Detected") }
if(settings?.schMotContactOff) { list.push("Set ECO if Contact Open") }
if(settings?.schMotExternalTempOff) { list.push("Set ECO based on External Temp") }
if(settings?.schMotRemoteSensor) { list.push("Use Remote Temp Sensors") }
if(isTstatSchedConfigured()) { list.push("Setpoint Schedules Created") }
if(settings?.schMotOperateFan) { list.push("Control Fans with HVAC") }
if(settings?.schMotHumidityControl) { list.push("Control Humidifier") }
if(retAsList) {
return isSchMotConfigured() ? list : null
} else {
String sDesc = ""
sDesc += settings?.schMotTstat ? "${settings?.schMotTstat?.label}" : ""
list?.each { ls ->
sDesc += "\n • ${ls}"
}
String t1 = getNotifConfigDesc("schMot")
sDesc += t1 ? "\n\n${t1}" : ""
sDesc += settings?.schMotTstat ? descriptions("d_ttm") : ""
return isSchMotConfigured() ? "${sDesc}" : null
}
}
void setAutomationStatus(upd=false) {
Boolean myDis = (settings?.autoDisabledreq == true)
Boolean settingsReset = (parent.getSettingVal("disableAllAutomations") == true)
Boolean storAutoType = getAutoType() == "storage" ? true : false
if(settingsReset && !storAutoType) {
if(!myDis && settingsReset) { LogAction("setAutomationStatus: Nest Integrations forcing disable", "info", true) }
myDis = true
} else if(storAutoType) {
myDis = false
}
if(!getIsAutomationDisabled() && myDis) {
LogAction("Automation Disabled at (${getDtNow()})", "info", true)
state?.autoDisabledDt = getDtNow()
} else if(getIsAutomationDisabled() && !myDis) {
LogAction("Automation Enabled at (${getDtNow()})", "info", true)
state?.autoDisabledDt = null
}
state?.autoDisabled = myDis
if(upd) { app.update() }
}
void settingUpdate(String name, value, String type=null) {
//LogTrace("settingUpdate($name, $value, $type)...")
if(name) {
if(value == "" || value == null || value == []) {
settingRemove(name)
return
}
}
if(name && type) { app?.updateSetting("$name", [type: "$type", value: value]) }
else if (name && type == null) { app?.updateSetting(name.toString(), 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", "error", true); return false }
}
void stateRemove(String key) {
state.remove(key?.toString())
}
def 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()
def schedList = getScheduleList()
boolean timersActive = false
def sLbl
int cnt = 1
int numact = 0
schedList?.each { 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")
def newscd = [:]
def act = 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(settings["${sLbl}rstrctPHome"], "and"),
p0: buildDeviceNameList(settings["${sLbl}rstrctPAway"], "and"),
s1: buildDeviceNameList(settings["${sLbl}rstrctSWOn"], "and"),
s0: buildDeviceNameList(settings["${sLbl}rstrctSWOff"], "and"),
ctemp: roundTemp(settings["${sLbl}CoolTemp"]),
htemp: roundTemp(settings["${sLbl}HeatTemp"]),
hvacm: settings["${sLbl}HvacMode"],
sen0: settings["schMotRemoteSensor"] ? buildDeviceNameList(settings["${sLbl}remSensor"], "and") : null,
thres: settings["schMotRemoteSensor"] ? settings["${sLbl}remSenThreshold"] : null,
m0: buildDeviceNameList(settings["${sLbl}Motion"], "and"),
mctemp: settings["${sLbl}Motion"] ? roundTemp(settings["${sLbl}MCoolTemp"]) : null,
mhtemp: settings["${sLbl}Motion"] ? roundTemp(settings["${sLbl}MHeatTemp"]) : null,
mhvacm: settings["${sLbl}Motion"] ? settings["${sLbl}MHvacMode"] : null,
// mpresHome: settings["${sLbl}Motion"] ? settings["${sLbl}MPresHome"] : null,
// mpresAway: settings["${sLbl}Motion"] ? settings["${sLbl}MPresAway"] : null,
mdelayOn: settings["${sLbl}Motion"] ? settings["${sLbl}MDelayValOn"] : null,
mdelayOff: 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) ? true : false
state."schedule${cnt}PresEnabled" = (newscd?.p1 || newscd?.p0) ? true : false
state."schedule${cnt}MotionEnabled" = (newscd?.m0) ? true : false
state."schedule${cnt}SensorEnabled" = (newscd?.sen0) ? true : false
//state."schedule${cnt}FanCtrlEnabled" = (newscd?.fan0) ? true : false
state."schedule${cnt}TimeActive" = (newscd?.tf || newscd?.tfc || newscd?.tfo || newscd?.tt || newscd?.ttc || newscd?.tto || newscd?.w) ? true : false
def newact = isMotionActive(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") ? true : false
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()}", "info", 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
settingUpdate("showDebug", "true", "bool")
//settingUpdate("advAppDebug", "false", "bool")
stateRemove("detailEventHistory")
stateRemove("detailExecutionHistory")
scheduleAutomationEval(30)
if(settings?.showDebug || settings?.advAppDebug) { runIn(1800, logsOff) }
}
void logsOff() {
log.warn "debug logging disabled..."
settingUpdate("showDebug", "false", "bool")
settingUpdate("advAppDebug", "false", "bool")
}
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 = state?.autoTyp
String appLbl = getCurAppLbl()
String newName = appName() == "${appLabel()}" ? "NST Automations" : "${appName()}"
String typeLabel = ""
String newLbl
String dis = (getIsAutomationDisabled() == true) ? "\n(Disabled)" : ""
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 && 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() {
def sets = []
settings?.sort().each { st ->
sets << st
}
return sets
}
def getSettingVal(var) {
if(var == null) { return settings }
return settings[var] ?: null
}
def getStateVal(var) {
return state[var] ?: null
}
public automationsInst() {
state.isNestModesConfigured = (isNestModesConfigured() == true)
state.isWatchdogConfigured = (isWatchdogConfigured() == true)
state.isSchMotConfigured = (isSchMotConfigured() == true)
state.isLeakWatConfigured = (isLeakWatConfigured() == true)
state.isConWatConfigured = (isConWatConfigured() == true)
state.isHumCtrlConfigured = (isHumCtrlConfigured() == true)
state.isExtTmpConfigured = (isExtTmpConfigured() == true)
state.isRemSenConfigured = (isRemSenConfigured() == true)
state.isTstatSchedConfigured = (isTstatSchedConfigured() == true)
state.isFanCtrlConfigured = (isFanCtrlSwConfigured() == true)
state.isFanCircConfigured = (isFanCircConfigured() == true)
state?.isInstalled = true
}
List getAutomationsInstalled() {
List list = []
String aType = state?.autoTyp
switch(aType) {
case "nMode":
list.push(aType)
break
case "schMot":
def 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 state?.autoTyp ?: null
}
String getAutoType() { return !parent ? "" : state?.autoTyp }
boolean getIsAutomationDisabled() {
def dis = state?.autoDisabled
return (dis != null && dis == true) ? true : false
}
void subscribeToEvents() {
//Remote Sensor Subscriptions
String autoType = getAutoType()
List swlist = []
//Nest Mode Subscriptions
if(autoType == "nMode") {
if(isNestModesConfigured()) {
if(!settings?.nModePresSensor && !settings?.nModeSwitch && (settings?.nModeHomeModes || settings?.nModeAwayModes)) { subscribe(location, "mode", nModeGenericEvt) }
if(settings?.nModePresSensor && !settings?.nModeSwitch) { subscribe(nModePresSensor, "presence", nModeGenericEvt) }
if(settings?.nModeSwitch && !settings?.nModePresSensor) { subscribe(nModeSwitch, "switch", nModeGenericEvt) }
/*
def 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})", "debug", false)
//subscribe(d1, "ThermostatMode", automationGenericEvt) // this is not needed for nMode
//subscribe(d1, "presence", automationGenericEvt) // this is not needed, tracking only
}
return d1
}
}
*/
List t0 = []
if(settings["nModerstrctSWOn"]) { t0 = t0 + settings["nModerstrctSWOn"] }
if(settings["nModerstrctSWOff"]) { t0 = t0 + settings["nModerstrctSWOff"] }
for(sw in t0) {
if(swlist?.contains(sw)) {
//log.trace "found $sw"
} else {
swlist.push(sw)
subscribe(sw, "switch", automationGenericEvt)
}
}
}
}
//ST Thermostat Motion
if(autoType == "schMot") {
def needThermTemp
def needThermMode
def needThermPres
if(isSchMotConfigured()) {
if(settings?.schMotWaterOff) {
if(isLeakWatConfigured()) {
setInitialVoiceMsgs(leakWatPrefix())
setCustomVoice(leakWatPrefix())
subscribe(leakWatSensors, "water", leakWatSensorEvt)
}
}
if(settings?.schMotContactOff) {
if(isConWatConfigured()) {
setInitialVoiceMsgs(conWatPrefix())
setCustomVoice(conWatPrefix())
subscribe(conWatContacts, "contact", conWatContactEvt)
List t0 = []
if(settings["conWatrstrctSWOn"]) { t0 = t0 + settings["conWatrstrctSWOn"] }
if(settings["conWatrstrctSWOff"]) { t0 = t0 + settings["conWatrstrctSWOff"] }
for(sw in t0) {
if(swlist?.contains(sw)) {
//log.trace "found $sw"
} else {
swlist.push(sw)
subscribe(sw, "switch", automationGenericEvt)
}
}
}
}
if(settings?.schMotHumidityControl) {
if(isHumCtrlConfigured()) {
subscribe(humCtrlSwitches, "switch", automationGenericEvt)
subscribe(humCtrlHumidity, "humidity", automationGenericEvt)
if(!settings?.humCtrlUseWeather && settings?.humCtrlTempSensor) { subscribe(humCtrlTempSensor, "temperature", automationGenericEvt) }
if(settings?.humCtrlUseWeather) {
//state.needWeathUpd = true
def weather = parent.getSettingVal("weatherDevice")
if(weather) {
subscribe(weather, "temperature", extTmpGenericEvt)
} else { LogAction("No weather device found", "error", true) }
}
def t0 = []
if(settings["humCtrlrstrctSWOn"]) { t0 = t0 + settings["humCtrlrstrctSWOn"] }
if(settings["humCtrlrstrctSWOff"]) { t0 = t0 + settings["humCtrlrstrctSWOff"] }
for(sw in t0) {
if(swlist?.contains(sw)) {
//log.trace "found $sw"
} else {
swlist.push(sw)
subscribe(sw, "switch", automationGenericEvt)
}
}
}
}
if(settings?.schMotExternalTempOff) {
if(isExtTmpConfigured()) {
setInitialVoiceMsgs(extTmpPrefix())
setCustomVoice(extTmpPrefix())
if(settings?.extTmpUseWeather) {
//state.needWeathUpd = true
def weather = parent.getSettingVal("weatherDevice")
if(weather) {
subscribe(weather, "temperature", extTmpGenericEvt)
subscribe(weather, "humidity", extTmpGenericEvt)
} else { LogAction("No weather device found", "error", true) }
}
def t0 = []
if(settings["extTmprstrctSWOn"]) { t0 = t0 + settings["extTmprstrctSWOn"] }
if(settings["extTmprstrctSWOff"]) { t0 = t0 + settings["extTmprstrctSWOff"] }
for(sw in t0) {
if(swlist?.contains(sw)) {
//log.trace "found $sw"
} else {
swlist.push(sw)
subscribe(sw, "switch", automationGenericEvt)
}
}
if(!settings?.extTmpUseWeather && settings?.extTmpTempSensor) { subscribe(extTmpTempSensor, "temperature", extTmpGenericEvt) }
state.extTmpChgWhileOnDt = getDtNow()
state.extTmpChgWhileOffDt = getDtNow()
}
}
def senlist = []
if(settings?.schMotRemoteSensor) {
if(isRemSenConfigured()) {
if(settings?.remSensorDay) {
for(sen in settings?.remSensorDay) {
if(senlist?.contains(sen)) {
//log.trace "found $sen"
} else {
senlist.push(sen)
subscribe(sen, "temperature", automationGenericEvt)
subscribe(sen, "humidity", automationGenericEvt)
if(settings?.schMotExternalTempOff) {
if(isExtTmpConfigured()) {
subscribe(sen, "temperature", extTmpGenericEvt)
subscribe(sen, "humidity", extTmpGenericEvt)
}
}
}
}
}
}
}
if(isTstatSchedConfigured()) { }
if(settings?.schMotOperateFan) {
if(isFanCtrlSwConfigured() && fanCtrlFanSwitches) {
subscribe(fanCtrlFanSwitches, "switch", automationGenericEvt)
subscribe(fanCtrlFanSwitches, "level", automationGenericEvt)
}
def t0 = []
if(settings["fanCtrlrstrctSWOn"]) { t0 = t0 + settings["fanCtrlrstrctSWOn"] }
if(settings["fanCtrlrstrctSWOff"]) { t0 = t0 + settings["fanCtrlrstrctSWOff"] }
for(sw in t0) {
if(swlist?.contains(sw)) {
//log.trace "found $sw"
} else {
swlist.push(sw)
subscribe(sw, "switch", automationGenericEvt)
}
}
}
Boolean hasFan = (state?.schMotTstatHasFan == true)
if(hasFan && (settings?.schMotOperateFan || settings?.schMotRemoteSensor || settings?.schMotHumidityControl)) {
subscribe(settings.schMotTstat, "thermostatFanMode", automationGenericEvt)
}
List schedList = getScheduleList()
String sLbl
Integer cnt = 1
List prlist = []
List mtlist = []
schedList?.each { scd ->
sLbl = "schMot_${scd}_"
def restrict = state?."sched${cnt}restrictions"
def act = settings["${sLbl}SchedActive"]
if(act) {
if(state?."schedule${cnt}SwEnabled") {
if(restrict?.s1) {
for(sw in settings["${sLbl}rstrctSWOn"]) {
if(swlist?.contains(sw)) {
//log.trace "found $sw"
} else {
swlist.push(sw)
subscribe(sw, "switch", automationGenericEvt)
}
}
}
if(restrict?.s0) {
for(sw in settings["${sLbl}rstrctSWOff"]) {
if(swlist?.contains(sw)) {
//log.trace "found $sw"
} else {
swlist.push(sw)
subscribe(sw, "switch", automationGenericEvt)
}
}
}
}
if(state?."schedule${cnt}PresEnabled") {
if(restrict?.p1) {
for(pr in settings["${sLbl}rstrctPHome"]) {
if(prlist?.contains(pr)) {
//log.trace "found $pr"
} else {
prlist.push(pr)
subscribe(pr, "presence", automationGenericEvt)
}
}
}
if(restrict?.p0) {
for(pr in settings["${sLbl}rstrctPAway"]) {
if(prlist?.contains(pr)) {
//log.trace "found $pr"
} else {
prlist.push(pr)
subscribe(pr, "presence", automationGenericEvt)
}
}
}
}
if(state?."schedule${cnt}MotionEnabled") {
if(restrict?.m0) {
for(mt in 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 settings["${sLbl}remSensor"]) {
if(senlist?.contains(sen)) {
//log.trace "found $sen"
} else {
senlist.push(sen)
subscribe(sen, "temperature", automationGenericEvt)
}
}
}
}
}
cnt += 1
}
subscribe(settings.schMotTstat, "thermostatMode", automationGenericEvt)
subscribe(settings.schMotTstat, "thermostatOperatingState", automationGenericEvt)
subscribe(settings.schMotTstat, "temperature", automationGenericEvt)
subscribe(settings.schMotTstat, "presence", automationGenericEvt)
def canCool = state?.schMotTstatCanCool
if(canCool) {
subscribe(settings.schMotTstat, "coolingSetpoint", automationGenericEvt)
}
def canHeat = state?.schMotTstatCanHeat
if(canHeat) {
subscribe(settings.schMotTstat, "heatingSetpoint", automationGenericEvt)
}
subscribe(location, "sunset", automationGenericEvt)
subscribe(location, "sunrise", automationGenericEvt)
subscribe(location, "mode", automationGenericEvt)
}
}
//watchDog Subscriptions
if(autoType == "watchDog") {
// if(isWatchdogConfigured())
def 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})", "debug", false)
subscribe(d1, "temperature", automationGenericEvt)
subscribe(d1, "thermostatMode", automationGenericEvt)
subscribe(d1, "presence", automationGenericEvt)
subscribe(d1, "onlineStatus", automationGenericEvt)
subscribe(location, "mode", automationGenericEvt)
}
return d1
}
}
def prots = 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})", "debug", false)
subscribe(d1, "onlineStatus", automationGenericEvt)
}
return d1
}
}
def cams = 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})", "debug", 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()
int random_int = random.nextInt(60)
int random_dint = random.nextInt(9)
String autoType = getAutoType()
if(autoType == "schMot" && state?.scheduleActiveCnt && 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)
}
}
void heartbeatAutomation() {
String autoType = getAutoType()
String str = "heartbeatAutomation() ${autoType}"
int val = 900
if(autoType == "schMot") {
val = 220
}
if(getAutoRunInSec() > val) {
LogTrace("${str} RUN")
runAutomationEval()
} else {
LogTrace("${str} NOT NEEDED")
}
}
int defaultAutomationTime() {
return 20
}
void scheduleAutomationEval(schedtime = defaultAutomationTime()) {
int theTime = schedtime
if(theTime < defaultAutomationTime()) { theTime = defaultAutomationTime() }
String autoType = getAutoType()
def random = new Random()
int 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
}
int schWaitVal = settings?.schMotWaitVal?.toInteger() ?: 60
if(schWaitVal > 120) { schWaitVal = 120 }
int 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: "
def t0 = state?.evalSchedLastTime
if(t0 == null) { t0 = 0 }
int 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() }
int 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}", "warn", true)
break
}
}
/*
void sendAutoChgToDevice(dev, autoType, chgDesc) {
if(dev && autoType && chgDesc) {
try {
dev?.whoMadeChanges(autoType?.toString(), chgDesc?.toString(), getDtNow().toString())
} catch (ex) {
log.error "sendAutoChgToDevice Exception:", ex
}
}
}
*/
/*
def sendEcoActionDescToDevice(dev, desc) {
if(dev && desc) {
try {
dev?.ecoDesc(desc) // THIS ONLY WORKS ON NEST THERMOSTATS
} catch (ex) {
log.error "sendEcoActionDescToDevice Exception:", ex
}
}
}
*/
/*
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, dev=null) {
if(actionDesc && actionDt) {
def newVal = ["actionDesc":actionDesc, "dt":actionDt, "autoType":autoType]
state?.lastAutoActionData = newVal
def list = state?.detailActionHistory ?: []
int listSize = 30
if(list?.size() < listSize) {
list.push(newVal)
}
else if(list?.size() > listSize) {
def nSz = (list?.size()-listSize) + 1
def nList = list?.drop(nSz)
nList?.push(newVal)
list = nList
}
else if(list?.size() == listSize) {
def nList = list?.drop(1)
nList?.push(newVal)
list = nList
}
if(list) { state?.detailActionHistory = list }
/*
if(dev) {
sendAutoChgToDevice(dev, autoType, actionDesc) // THIS ONLY WORKS ON NEST THERMOSTATS
}
*/
}
}
/*
def getAutoActionData() {
if(state?.lastAutoActionData) {
return state?.lastAutoActionData
}
}
*/
def automationGenericEvt(evt) {
def startTime = now()
def eventDelay = startTime - evt.date.getTime()
LogAction("${evt?.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", "info", false)
/* if streaming, this is not needed
if(isRemSenConfigured() && settings?.vthermostat) {
state.needChildUpdate = true
}
if(settings?.humCtrlUseWeather && isHumCtrlConfigured()) {
state.needWeathUpd = true
}
*/
doTheEvent(evt)
}
def doTheEvent(evt) {
if(getIsAutomationDisabled()) { return }
else {
scheduleAutomationEval()
storeLastEventData(evt)
}
}
/******************************************************************************
| WATCHDOG AUTOMATION LOGIC CODE |
*******************************************************************************/
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") : ""
href "setNotificationPage1", title: imgTitle(getAppImg("i_not"), inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
//ERS
if(settings?."${pName}NotifOn") {
def tstats = parent.getSettingVal("thermostats")
def prots = parent.getSettingVal("protects")
def cams = parent.getSettingVal("cameras")
if(tstats || prots || cams) {
input "onlineStatMon", "bool", title: paraTitleStr("Notify When Devices are offline?"), required: false, defaultValue: true, submitOnChange: true
}
if(tstats && (settings?.onlineStatMon != false)) {
paragraph imgTitle(getAppImg("i_sw"), paraTitleStr("Temperature warnings on"))
input "thermMissedEco", "bool", title: paraTitleStr("Notify When Away and Thermostat Not in Eco Mode?"), required: false, defaultValue: true, submitOnChange: true
}
if(cams && (settings?.onlineStatMon != false)) {
def camStreamNotif = parent.getSettingVal("camStreamNotifMsg")
def mys = camStreamNotif == false ? false : true
def iiact = mys ? "i_sw" : "switch_off_icon.png"
//settingUpdate("camStNot", mys.toString(), "bool")
paragraph imgTitle(getAppImg(iiact), paraTitleStr("Stream Notification (setting from mgr) ${mys}"))
//input "camStNot", "bool", title: imgTitle(getAppImg("i_sw"), inputTitleStr("Stream Notification (setting from mgr)")), required: false, defaultValue: mys, submitOnChange: true
}
def locPres = parent.getSettingVal("locPresChangeMsg")
boolean myp = locPres == false ? false : true
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) {
def startTime = now()
def eventDelay = startTime - 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 == "true") {
scheduleAutomationEval()
}
}
storeLastEventData(evt)
}
*/
// Alarms will repeat every watDogRepeatMsgDelay (1 hr default) ALL thermostats
def watchDogCheck() {
if(getIsAutomationDisabled()) { return }
else {
long execTime = now()
state?.autoRunDt = getDtNow()
def tstats = 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}", "warn", 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") ? true : false
//def nestModeAway = (getNestLocPres() == "home") ? false : true
if(nestModeAway) {
String curMode = d1?.currentThermostatMode?.toString()
if(!(curMode in ["eco", "off" ])) {
watchDogAlarmActions(d1.displayName, dni, "eco")
def pres = d1?.currentPresence?.toString()
//LogAction("watchDogCheck: | Thermostat: ${d1?.displayName} is Away and Mode Is Not in ECO | CurMode: (${curMode}) | CurrentPresence: (${pres})", "warn", true)
}
}
}
return d1
}
}
}
def prots = parent.getSettingVal("protects")
def foundProts
if(prots) {
foundProts = prots?.collect { dni ->
checkOnline(dni)
return dni
}
}
def cams = 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, "stream", curStream, lastStr)
//LogAction("watchDogCheck: | ${d1?.displayName} streaming changed | CurStream: (${curStream}) | prev: (${lastStr})", "warn", true)
}
state?."lastStr${dni}" = curStream
}
return dni
}
}
}
}
def locPres = parent.getSettingVal("locPresChangeMsg")
boolean myp = locPres == false ? false : true
String curPres = parent.getLocationPresence() ?: ""
String lastPres = state?.lastPresence ?: ""
if(lastpres && (lastPres != curPres)) {
state.lastPresence = curPres
watchDogAlarmActions(location.name, "Location", "locPres", curPres, lastPres)
//LogAction("watchDogCheck: | Nest Location changed | Cur: (${curPres}) | prev: (${lastPres})", "info", true)
}
storeExecutionHistory((now()-execTime), "watchDogCheck")
}
}
boolean checkOnline(dni) {
def d1 = parent.getDevice(dni)
if(d1) {
def curOnline = d1?.currentOnlineStatus?.toString()
if(curOnline != "online") {
watchDogAlarmActions(d1.displayName, dni, "online")
//LogAction("watchDogCheck: | ${d1?.displayName} is not online | CurOnline: (${curOnline})", "warn", true)
return false
}
return true
}
return false
}
private void watchDogAlarmActions(dev, String dni, String actType, p1=null, p2=null) {
String pName = watchDogPrefix()
String evtNotifMsg = ""
String eventType = "Warning"
int lvl
switch(actType) {
case "temp":
evtNotifMsg = "Safety Temp exceeded on ${dev}."
break
case "eco":
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"] ? true : false
boolean canNotif = (allowNotif && (getWatDogSafetyAlertDtSec("${dni}") > getWatDogRepeatMsgDelayVal())) ? true : false
if(canNotif) {
sendNofificationMsg(evtNotifMsg, eventType, pName, lvl) // this uses parent
def allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
if(allowAlarm) {
scheduleAlarmOn(pName)
}
state?."watDogSafetyAlDt${dni}" = getDtNow()
}
String t0 = eventType == "Info" ? "info" : "warn"
LogAction("watchDogAlarmActions() | SENT: ${canNotif} | ${evtNotifMsg}", t0, true)
}
//def getWatDogSafetyAlertDtSec(dni) { return !state?."watDogSafetyAlDt${dni}" ? 10000 : GetTimeDiffSeconds(state?."watDogSafetyAlDt${dni}", null, "getWatDogSafetyAlertDtSec").toInteger() }
int getWatDogSafetyAlertDtSec(dni) { return getTimeSeconds("watDogSafetyAlDt${dni}", 10000, "getWatDogSafetyAlertDtSec") }
def getWatDogRepeatMsgDelayVal() { return !watDogRepeatMsgDelay ? 3600 : watDogRepeatMsgDelay.toInteger() }
boolean isWatchdogConfigured() {
return (state?.autoTyp == "watchDog") ? true : false
}
/////////////////////THERMOSTAT AUTOMATION CODE LOGIC ///////////////////////
/****************************************************************************
| REMOTE SENSOR AUTOMATION CODE |
*****************************************************************************/
String remSenPrefix() { return "remSen" }
void removeVstat(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", "info", false)
}
}
}
//Requirements Section
boolean remSenCoolTempsReq() { return (settings?.remSenRuleType in [ "Cool", "Heat_Cool", "Cool_Circ", "Heat_Cool_Circ" ]) ? true : false }
boolean remSenHeatTempsReq() { return (settings?.remSenRuleType in [ "Heat", "Heat_Cool", "Heat_Circ", "Heat_Cool_Circ" ]) ? true : false }
boolean remSenDayHeatTempOk() { return (!remSenHeatTempsReq() || (remSenHeatTempsReq() && remSenDayHeatTemp)) ? true : false }
boolean remSenDayCoolTempOk() { return (!remSenCoolTempsReq() || (remSenCoolTempsReq() && remSenDayCoolTemp)) ? true : false }
boolean isRemSenConfigured() {
boolean devOk = (settings?.remSensorDay) ? true : false
return (settings?.schMotRemoteSensor && devOk && settings?.remSenRuleType && remSenDayHeatTempOk() && remSenDayCoolTempOk() ) ? true : false
}
int getMotionActiveSec(mySched) {
def sLbl = "schMot_${mySched}_"
return !state?."${sLbl}MotionActiveDt" ? 0 : GetTimeDiffSeconds(state?."${sLbl}MotionActiveDt", null, "getMotionActiveSec").toInteger()
}
int getMotionInActiveSec(mySched) {
def sLbl = "schMot_${mySched}_"
return !state?."${sLbl}MotionInActiveDt" ? 0 : GetTimeDiffSeconds(state?."${sLbl}MotionInActiveDt", null, "getMotionInActiveSec").toInteger()
}
def 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", "info", false)
if(getIsAutomationDisabled()) { return }
else {
storeLastEventData(evt)
boolean dorunIn = false
int delay = 120
String sLbl
def mySched = getCurrentSchedule()
def schedList = getScheduleList()
String schName = ""
for (cnt in schedList) {
sLbl = "schMot_${cnt}_"
def act = settings["${sLbl}SchedActive"]
if(act && settings["${sLbl}Motion"]) {
def str = settings["${sLbl}Motion"].toString()
if(str.contains( evt.displayName)) {
def oldActive = state?."${sLbl}oldMotionActive"
def newActive = isMotionActive(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)", "info", 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) {
def 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 {
def 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)
}
}
}
def isMotionActive(sensors) {
def result
sensors?.each { sen ->
if(sen) {
def sval = sen?.currentMotion
if(sval == "active") { result = true }
}
}
return result
//return sensors?.currentMotion?.equals("active") ? true : false
}
def getDeviceVarAvg(items, var) {
def tmpAvg = []
def tempVal = 0
if(!items) { return tempVal }
else {
tmpAvg = items*."${var}"
if(tmpAvg && tmpAvg?.size() > 0) { tempVal = (tmpAvg?.sum().toDouble() / tmpAvg?.size().toDouble()).round(1) }
}
return tempVal.toDouble()
}
def getDeviceTempAvg(items) {
return getDeviceVarAvg(items, "currentTemperature")
}
def getDeviceTemp(dev) {
return getDeviceVarAvg(dev, "currentTemperature")
}
def remSenShowTempsPage() {
dynamicPage(name: "remSenShowTempsPage", uninstall: false) {
if(settings?.remSensorDay) {
def t0 = "${tUnitStr()}"
section("Default Sensor Temps: (Schedules can override)") {
def cnt = 0
def rCnt = settings?.remSensorDay?.size()
def str = ""
str += "Sensor Temp (average): (${getDeviceTempAvg(settings?.remSensorDay)}${t0})\n│"
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) ? " " : "│"}└ " : ""}(${getDeviceTemp(t)}${t0})"
}
paragraph imgTitle(getAppImg("i_t"), sectionTitleStr("${str}")), state: "complete"
}
}
}
}
def remSendoSetCool(chgval, onTemp, offTemp) {
def remSenTstat = settings?.schMotTstat
def remSenTstatMir = settings?.schMotTstatMir
try {
def hvacMode = remSenTstat ? remSenTstat?.currentThermostatMode?.toString() : null
def curCoolSetpoint = getTstatSetpoint(remSenTstat, "cool")
def curHeatSetpoint = getTstatSetpoint(remSenTstat, "heat")
def tempChangeVal = !remSenTstatTempChgVal ? 5.0 : Math.min(Math.max(remSenTstatTempChgVal.toDouble(), 2.0), 5.0)
def maxTempChangeVal = tempChangeVal * 3
chgval = (chgval > (onTemp + maxTempChangeVal)) ? onTemp + maxTempChangeVal : chgval
chgval = (chgval < (offTemp - maxTempChangeVal)) ? offTemp - maxTempChangeVal : chgval
def t0 = "${tUnitStr()}"
if(chgval != curCoolSetpoint) {
scheduleAutomationEval(70)
def cHeat = null
if(hvacMode in ["auto"]) {
if(curHeatSetpoint >= (offTemp-tempChangeVal)) {
cHeat = offTemp - tempChangeVal
LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to (${cHeat}${t0}) to allow COOL setting", "info", false)
if(remSenTstatMir) { remSenTstatMir*.setHeatingSetpoint(cHeat) }
}
}
if(setTstatAutoTemps(remSenTstat, chgval, cHeat, "remSen")) {
//LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to (${chgval}${t0}) ", "info", 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}) ", "info", false)
}
} catch (ex) {
log.error "remSendoSetCool Exception: ${ex?.message}"
}
return false
}
def remSendoSetHeat(chgval, onTemp, offTemp) {
def remSenTstat = settings?.schMotTstat
def remSenTstatMir = settings?.schMotTstatMir
try {
def hvacMode = remSenTstat ? remSenTstat?.currentThermostatMode?.toString() : null
def curCoolSetpoint = getTstatSetpoint(remSenTstat, "cool")
def curHeatSetpoint = getTstatSetpoint(remSenTstat, "heat")
def tempChangeVal = !remSenTstatTempChgVal ? 5.0 : Math.min(Math.max(remSenTstatTempChgVal.toDouble(), 2.0), 5.0)
def maxTempChangeVal = tempChangeVal * 3
chgval = (chgval < (onTemp - maxTempChangeVal)) ? onTemp - maxTempChangeVal : chgval
chgval = (chgval > (offTemp + maxTempChangeVal)) ? offTemp + maxTempChangeVal : chgval
def t0 = "${tUnitStr()}"
if(chgval != curHeatSetpoint) {
scheduleAutomationEval(70)
def cCool = null
if(hvacMode in ["auto"]) {
if(curCoolSetpoint <= (offTemp+tempChangeVal)) {
cCool = offTemp + tempChangeVal
LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to (${cCool}${t0}) to allow HEAT setting", "info", false)
if(remSenTstatMir) { remSenTstatMir*.setCoolingSetpoint(cCool) }
}
}
if(setTstatAutoTemps(remSenTstat, cCool, chgval, "remSen")) {
//LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to (${chgval}${t0})", "info", 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})", "info", false)
}
} catch (ex) {
log.error "remSendoSetHeat Exception: ${ex?.message}"
}
return false
}
/*
def getRemSenModeOk() {
def result = false
if(settings?.remSensorDay ) { result = true }
//log.debug "getRemSenModeOk: $result"
return result
}
*/
private remSenCheck() {
LogTrace("remSenCheck")
if(getIsAutomationDisabled()) { return }
try {
def remSenTstat = settings?.schMotTstat
def remSenTstatMir = settings?.schMotTstatMir
long execTime = now()
String noGoDesc = ""
if( !settings?.remSensorDay || !remSenTstat) {
noGoDesc += !settings?.remSensorDay ? "Missing Required Sensor Selections" : ""
noGoDesc += !remSenTstat ? "Missing Required Thermostat device" : ""
LogTrace("Remote Sensor NOT Evaluating Status: ${noGoDesc}")
} else {
//log.info "remSenCheck: Evaluating Event"
def tempScaleStr = "${tUnitStr()}"
def hvacMode = remSenTstat ? remSenTstat?.currentThermostatMode?.toString() : null
if(hvacMode in [ "off", "eco"] ) {
LogAction("Remote Sensor: Skipping Evaluation; The Current Thermostat Mode is '${strCapitalize(hvacMode)}'", "info", false)
disableOverrideTemps()
storeExecutionHistory((now() - execTime), "remSenCheck")
return
}
def reqSenHeatSetPoint = getRemSenHeatSetTemp(hvacMode)
def reqSenCoolSetPoint = getRemSenCoolSetTemp(hvacMode)
def threshold = getRemoteSenThreshold()
if(hvacMode in ["auto"]) {
// check that requested setpoints make sense & notify
def coolheatDiff = Math.abs(reqSenCoolSetPoint - reqSenHeatSetPoint)
if( !((reqSenCoolSetPoint > reqSenHeatSetPoint) && (coolheatDiff >= 2)) ) {
LogAction("remSenCheck: Invalid Setpoints with auto mode: (${reqSenCoolSetPoint})/(${reqSenHeatSetPoint}, ${threshold})", "warn", true)
storeExecutionHistory((now() - execTime), "remSenCheck")
return
}
}
def tempChangeVal = !remSenTstatTempChgVal ? 5.0 : Math.min(Math.max(remSenTstatTempChgVal.toDouble(), 2.0), 5.0)
def maxTempChangeVal = tempChangeVal * 3
def curTstatTemp = getDeviceTemp(remSenTstat).toDouble()
def curSenTemp = getRemoteSenTemp().toDouble()
def curTstatOperState = remSenTstat?.currentThermostatOperatingState.toString()
def curTstatFanMode = remSenTstat?.currentThermostatFanMode.toString()
def fanOn = (curTstatFanMode == "on" || curTstatFanMode == "circulate") ? true : false
def curCoolSetpoint = getTstatSetpoint(remSenTstat, "cool")
def curHeatSetpoint = getTstatSetpoint(remSenTstat, "heat")
def acRunning = (curTstatOperState == "cooling") ? true : false
def heatRunning = (curTstatOperState == "heating") ? true : false
/*
LogAction("remSenCheck: Rule Type: ${getEnumValue(remSenRuleEnum("heatcool"), settings?.remSenRuleType)}", "info", false)
LogAction("remSenCheck: Sensor Temp: ${curSenTemp}", "info", false)
LogAction("remSenCheck: Thermostat Info - ( Temperature: (${curTstatTemp}) | HeatSetpoint: (${curHeatSetpoint}) | CoolSetpoint: (${curCoolSetpoint}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) | FanMode: (${curTstatFanMode}) )", "info", false)
LogAction("remSenCheck: Desired Temps - Heat: ${reqSenHeatSetPoint} | Cool: ${reqSenCoolSetPoint}", "info", false)
LogAction("remSenCheck: Threshold Temp: ${threshold} | Change Temp Increments: ${tempChangeVal}", "info", false)
*/
def chg = false
def chgval = 0
if(hvacMode in ["cool","auto"]) {
//Changes Cool Setpoints
if(settings?.remSenRuleType in ["Cool", "Heat_Cool", "Heat_Cool_Circ"]) {
def onTemp = reqSenCoolSetPoint + threshold
def offTemp = reqSenCoolSetPoint
def turnOn = false
def 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", "info", 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", "info", 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
}
def coolDiff1 = Math.abs(curTstatTemp - curCoolSetpoint)
//LogAction("Remote Sensor: COOL - coolDiff1: ${coolDiff1} tempChangeVal: ${tempChangeVal}", "info", false)
if(coolDiff1 < (tempChangeVal / 2)) {
chg = true
LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to maintain state", "info", 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}) ", "info", false) }
}
}
chg = false
chgval = 0
//LogAction("remSenCheck: Thermostat Info - ( Temperature: (${curTstatTemp}) | HeatSetpoint: (${curHeatSetpoint}) | CoolSetpoint: (${curCoolSetpoint}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) | FanMode: (${curTstatFanMode}) )", "info", false)
//Heat Functions.
if(hvacMode in ["heat", "emergency heat", "auto"]) {
if(settings?.remSenRuleType in ["Heat", "Heat_Cool", "Heat_Cool_Circ"]) {
def onTemp = reqSenHeatSetPoint - threshold
def offTemp = reqSenHeatSetPoint
def turnOn = false
def turnOff = false
//LogAction("Remote Sensor: HEAT - (Sensor Temp: ${curSenTemp} - HeatSetpoint: ${reqSenHeatSetPoint})", "info", 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", "info", 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", "info", 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
}
def heatDiff1 = Math.abs(curTstatTemp - curHeatSetpoint)
//LogAction("Remote Sensor: HEAT - heatDiff1: ${heatDiff1} tempChangeVal: ${tempChangeVal}", "info", false)
if(heatDiff1 < (tempChangeVal / 2)) {
chg = true
LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to maintain state", "info", 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})", "info", 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())
}
}
def getRemSenTempsToList() {
def mySched = getCurrentSchedule()
def sensors
if(mySched) {
def sLbl = "schMot_${mySched}_"
if(settings["${sLbl}remSensor"]) {
sensors = settings["${sLbl}remSensor"]
}
}
if(!sensors) { sensors = settings?.remSensorDay }
if(sensors?.size() >= 1) {
def t0 = "${tUnitStr()}"
def info = []
sensors?.sort().each {
info.push("${it?.displayName}": " ${it?.currentTemperature.toString()}${t0}")
}
return info
}
}
def getTstatSetpoint(tstat, type) {
if(tstat) {
if(type == "cool") {
def coolSp = tstat?.currentCoolingSetpoint
//log.debug "getTstatSetpoint(cool): $coolSp"
return coolSp ? coolSp?.toDouble() : 0
} else {
def heatSp = tstat?.currentHeatingSetpoint
//log.debug "getTstatSetpoint(heat): $heatSp"
return heatSp ? heatSp?.toDouble() : 0
}
}
else { return 0 }
}
def getRemoteSenThreshold() {
def threshold = settings?.remSenTempDiffDegrees
def mySched = getCurrentSchedule()
if(mySched) {
def sLbl = "schMot_${mySched}_"
if(settings["${sLbl}remSenThreshold"]) {
threshold = settings["${sLbl}remSenThreshold"]
}
}
def theMin = getTemperatureScale() == "C" ? 0.3 : 0.6
threshold = !threshold ? 2.0 : Math.min(Math.max(threshold.toDouble(),theMin), 4.0)
return threshold.toDouble()
}
def getRemoteSenTemp() {
def mySched = getCurrentSchedule()
if(state?.remoteTempSourceStr != null) { state.remoteTempSourceStr = null }
if(state?.currentSchedNum != null) { state.currentSchedNum = null }
def sens
if(mySched) {
String sLbl = "schMot_${mySched}_"
if(settings["${sLbl}remSensor"]) {
state.remoteTempSourceStr = "Schedule"
state.currentSchedNum = mySched
sens = settings["${sLbl}remSensor"]
return getDeviceTempAvg(sens).toDouble()
}
}
if(isRemSenConfigured()) {
state.remoteTempSourceStr = "Remote Sensor"
state.currentSchedNum = null
return getDeviceTempAvg(settings?.remSensorDay).toDouble()
} else {
state.remoteTempSourceStr = "Thermostat"
state.currentSchedNum = null
return getDeviceTemp(settings?.schMotTstat).toDouble()
/*
else {
LogAction("getRemoteSenTemp: No Temperature Found!", "warn", true)
return 0.0
*/
}
}
def fixTempSetting(temp) {
Double newtemp = temp?.toDouble()
if(temp != null) {
if(getTemperatureScale() == "C") {
if(temp > 35) { // setting was done in F
newtemp = roundTemp( ((newtemp - 32.0) * (5 / 9)) as Double)
}
} else if(getTemperatureScale() == "F") {
if(temp < 40) { // setting was done in C
newtemp = roundTemp( (((newtemp * (9 / 5)) as Double) + 32.0) ).toInteger()
}
}
}
return newtemp
}
def setRemoteSenTstat(val) {
LogAction("setRemoteSenTstat $val", "info", false)
state.remSenTstat = val
}
def getRemSenCoolSetTemp(curMode=null, isEco=false, useCurrent=true) {
Double coolTemp
def theMode = curMode != null ? curMode : null
if(theMode == null) {
def tstat = settings?.schMotTstat
theMode = tstat ? tstat?.currentThermostatMode.toString() : null
}
state.remoteCoolSetSourceStr = ""
if(theMode != "eco") {
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) {
def mySched = getCurrentSchedule()
if(mySched) {
def useMotion = state?."motion${mySched}UseMotionSettings"
def hvacSettings = state?."sched${mySched}restrictions"
coolTemp = !useMotion ? hvacSettings?.ctemp : hvacSettings?.mctemp ?: hvacSettings?.ctemp
state.remoteCoolSetSourceStr = "Schedule"
}
// ERS if Remsensor is enabled
if(isRemSenConfigured()) {
if(theMode == "cool" && coolTemp == null /* && isEco */) {
if(state?.extTmpSavedTemp) {
coolTemp = state?.extTmpSavedTemp.toDouble()
state.remoteCoolSetSourceStr = "Last Desired Temp"
}
}
if(theMode == "auto" && coolTemp == null /* && isEco */) {
if(state?.extTmpSavedCTemp) {
coolTemp = state?.extTmpSavedCTemp.toDouble()
state.remoteCoolSetSourceStr = "Last Desired CTemp"
}
}
if(coolTemp == null && remSenDayCoolTemp) {
coolTemp = remSenDayCoolTemp.toDouble()
state.remoteCoolSetSourceStr = "RemSen Day Cool Temp"
}
if(coolTemp == null) {
def desiredCoolTemp = getGlobalDesiredCoolTemp()
if(desiredCoolTemp) {
coolTemp = desiredCoolTemp.toDouble()
state.remoteCoolSetSourceStr = "Global Desired Cool Temp"
}
}
if(coolTemp) {
coolTemp = fixTempSetting(coolTemp)
}
}
}
}
if(coolTemp == null && useCurrent) {
coolTemp = settings?.schMotTstat ? getTstatSetpoint(settings?.schMotTstat, "cool") : coolTemp
state.remoteCoolSetSourceStr = "Thermostat"
}
return coolTemp
}
def getRemSenHeatSetTemp(curMode=null, isEco=false, useCurrent=true) {
Double heatTemp
def theMode = curMode != null ? curMode : null
if(theMode == null) {
def tstat = settings?.schMotTstat
theMode = tstat ? tstat?.currentThermostatMode.toString() : null
}
state.remoteHeatSetSourceStr = ""
if(theMode != "eco") {
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) {
def mySched = getCurrentSchedule()
if(mySched) {
def useMotion = state?."motion${mySched}UseMotionSettings"
def hvacSettings = state?."sched${mySched}restrictions"
heatTemp = !useMotion ? hvacSettings?.htemp : hvacSettings?.mhtemp ?: hvacSettings?.htemp
state.remoteHeatSetSourceStr = "Schedule"
}
// ERS if Remsensor is enabled
if(isRemSenConfigured()) {
if(theMode == "heat" && heatTemp == null /* && isEco */) {
if(state?.extTmpSavedTemp) {
heatTemp = state?.extTmpSavedTemp.toDouble()
state.remoteHeatSetSourceStr = "Last Desired Temp"
}
}
if(theMode == "auto" && heatTemp == null /* && isEco */) {
if(state?.extTmpSavedHTemp) {
heatTemp = state?.extTmpSavedHTemp.toDouble()
state.remoteHeatSetSourceStr = "Last Desired HTemp"
}
}
if(heatTemp == null && remSenDayHeatTemp) {
heatTemp = remSenDayHeatTemp.toDouble()
state.remoteHeatSetSourceStr = "RemSen Day Heat Temp"
}
if(heatTemp == null) {
def 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, "heat") : 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
def getOverrideCoolSec() { return !state?.remSenCoverrideDt ? 100000 : GetTimeDiffSeconds(state?.remSenCoverrideDt, null, "getOverrideCoolSec").toInteger() }
def getOverrideHeatSec() { return !state?.remSenHoverrideDt ? 100000 : GetTimeDiffSeconds(state?.remSenHoverrideDt, null, "getOverrideHeatSec").toInteger() }
def disableOverrideTemps() {
if(state?.remSenHoverride || state?.remSenCoverride) {
stateRemove("remSenCoverride")
stateRemove("remSenHoverride")
stateRemove("remSenCoverrideDt")
stateRemove("remSenHoverrideDt")
LogAction("disableOverrideTemps: Disabling Override temps", "info", false)
}
}
def remSenTempUpdate(temp, mode) {
//LogAction("remSenTempUpdate(${temp}, ${mode})", "info", false)
def res = false
if(getIsAutomationDisabled()) { return res }
switch(mode) {
case "heat":
if(remSenHeatTempsReq()) {
//LogAction("remSenTempUpdate Set Heat Override to: ${temp} for 4 hours", "info", false)
state?.remSenHoverride = temp.toDouble()
state?.remSenHoverrideDt = getDtNow()
res = true
}
break
case "cool":
if(remSenCoolTempsReq()) {
//LogAction("remSenTempUpdate Set Cool Override to: ${temp} for 4 hours", "info", false)
state?.remSenCoverride = temp.toDouble()
state?.remSenCoverrideDt = getDtNow()
res = true
}
break
default:
LogAction("remSenTempUpdate Invalid Request: ${mode}, ${temp}", "warn", true)
break
}
if(res) {
scheduleAutomationEval()
LogAction("remSenTempUpdate Set ${mode} Override to: ${temp} for 4 hours", "info", false)
}
return res
}
def remSenRuleEnum(type=null) {
// Determines that available rules to display based on the selected thermostats capabilites.
def canCool = state?.schMotTstatCanCool ? true : false
def canHeat = state?.schMotTstatCanHeat ? true : false
def hasFan = state?.schMotTstatHasFan ? true : false
//log.debug "remSenRuleEnum -- hasFan: $hasFan (${state?.schMotTstatHasFan} | canCool: $canCool (${state?.schMotTstatCanCool} | canHeat: $canHeat (${state?.schMotTstatCanHeat}"
def 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)", "error", 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 |
*************************************************************************/
String fanCtrlPrefix() { return "fanCtrl" }
boolean isFanCtrlConfigured() {
return ( settings?.schMotOperateFan && (isFanCtrlSwConfigured() || isFanCircConfigured())) ? true : false
}
boolean isFanCtrlSwConfigured() {
return (settings?.schMotOperateFan && settings?.fanCtrlFanSwitches && settings?.fanCtrlFanSwitchTriggerType && settings?.fanCtrlFanSwitchHvacModeFilter) ? true : false
}
boolean isFanCircConfigured() {
return (settings?.schMotOperateFan && (settings?.schMotCirculateTstatFan || settings?.schMotCirculateExtFan) && settings?.schMotFanRuleType) ? true : false
}
String getFanSwitchDesc(boolean showOpt = true) {
String swDesc = ""
int swCnt = 0
String pName = fanCtrlPrefix()
if(showOpt) {
swDesc += (settings?."${pName}FanSwitches" && (settings?."${pName}FanSwitchSpeedCtrl" || settings?."${pName}FanSwitchTriggerType" || settings?."${pName}FanSwitchHvacModeFilter")) ? "Fan Switch Config:" : ""
}
swDesc += settings?."${pName}FanSwitches" ? "${showOpt ? "\n" : ""}• Fan Switches:" : ""
int 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()})" : ""
}
if(showOpt) {
if (settings?."${pName}FanSwitches") {
swDesc += (settings?."${pName}FanSwitchSpeedCtrl" || settings?."${pName}FanSwitchTriggerType" || settings?."${pName}FanSwitchHvacModeFilter") ? "\n\nFan Triggers:" : ""
swDesc += (settings?."${pName}FanSwitchSpeedCtrl") ? "\n • Fan Speed Support: (Active)" : ""
swDesc += (settings?."${pName}FanSwitchTriggerType") ? "\n • Fan Trigger:\n └(${getEnumValue(switchRunEnum(), settings?."${pName}FanSwitchTriggerType")})" : ""
swDesc += (settings?."${pName}FanSwitchHvacModeFilter") ? "\n • Hvac Mode Filter:\n └(${getEnumValue(fanModeTrigEnum(), settings?."${pName}FanSwitchHvacModeFilter")})" : ""
}
}
boolean t0 = isFanCircConfigured()
swDesc += (t0) ? "\n\nFan Circulation Enabled:" : ""
swDesc += (t0) ? "\n • Fan Circulation Rule:\n └(${getEnumValue(remSenRuleEnum("fan"), settings?.schMotFanRuleType)})" : ""
swDesc += (t0 && settings?.fanCtrlTempDiffDegrees) ? ("\n • Threshold: (${settings?.fanCtrlTempDiffDegrees}${tUnitStr()})") : ""
swDesc += (t0 && settings?.fanCtrlOnTime) ? ("\n • Circulate Time: (${getEnumValue(fanTimeSecEnum(), settings?.fanCtrlOnTime)})") : ""
swDesc += (t0 && settings?.fanCtrlTimeBetweenRuns) ? ("\n • Time Between Cycles:\n └ (${getEnumValue(longTimeSecEnum(), settings?.fanCtrlTimeBetweenRuns)})") : ""
swDesc += (settings?."${pName}FanSwitches" || t0) ? "\n\nRestrictions Active: (${autoScheduleOk(fanCtrlPrefix()) ? "No" : "Yes"})" : ""
return (swDesc == "") ? null : "${swDesc}"
}
boolean getFanSwitchesSpdChk() {
int devCnt = 0
String pName = fanCtrlPrefix()
if(settings?."${pName}FanSwitches") {
settings?."${pName}FanSwitches"?.each { sw ->
if(checkFanSpeedSupport(sw)) { devCnt = devCnt+1 }
}
}
return (devCnt >= 1) ? true : false
}
boolean fanCtrlScheduleOk() { return autoScheduleOk(fanCtrlPrefix()) }
def fanCtrlCheck() {
//LogAction("FanControl Event | Fan Switch Check", "info", 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() : null
boolean modeEco = (curMode in ["eco"]) ? true : false
def reqHeatSetPoint
def reqCoolSetPoint
if(!modeEco) {
reqHeatSetPoint = getRemSenHeatSetTemp(curMode)
reqCoolSetPoint = getRemSenCoolSetTemp(curMode)
}
def lastMode = settings?.schMotTstat ? settings?.schMotTstat?.currentpreviousthermostatMode?.toString() : null
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) { reqHeatSetPoint = state?.extTmpSavedHTemp }
if(!reqCoolSetPoint) { reqCoolSetPoint = state?.extTmpSavedCTemp }
}
LogAction("fanCtrlCheck: Using lastMode: ${lastMode} | extTmpTstatOffRequested: ${state?.extTmpTstatOffRequested} | curMode: ${curMode}", "info", false)
}
reqHeatSetPoint = reqHeatSetPoint ?: 0
reqCoolSetPoint = reqCoolSetPoint ?: 0
def curTstatTemp = getRemoteSenTemp().toDouble()
def t0 = getReqSetpointTemp(curTstatTemp, reqHeatSetPoint, reqCoolSetPoint).req
def curSetPoint = t0 ? t0.toDouble() : 0
def tempDiff = Math.abs(curSetPoint - curTstatTemp)
LogAction("fanCtrlCheck: Desired Temps - Heat: ${reqHeatSetPoint} | Cool: ${reqCoolSetPoint}", "info", false)
LogAction("fanCtrlCheck: Current Thermostat Sensor Temp: ${curTstatTemp} Temp Difference: (${tempDiff})", "info", false)
def circWantsOn = null
if(isFanCircConfigured()) {
def adjust = (getTemperatureScale() == "C") ? 0.5 : 1.0
def threshold = !settings?.fanCtrlTempDiffDegrees ? adjust : settings?.fanCtrlTempDiffDegrees.toDouble()
def hvacMode = curMode
/*
def curTstatFanMode = settings?.schMotTstat?.currentThermostatFanMode.toString()
def fanOn = (curTstatFanMode == "on" || curTstatFanMode == "circulate") ? true : false
if(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", "info", false)
storeLastAction("Turned ${settings?.schMotTstat} Fan to (Auto)", getDtNow(), "fanCtrl", settings?.schMotTstat)
settings?.schMotTstat?.fanAuto()
if(settings?.schMotTstatMir) { settings?.schMotTstatMir*.fanAuto() }
}
}
state.haveRunFan = false
}
*/
def sTemp = getReqSetpointTemp(curTstatTemp, reqHeatSetPoint, reqCoolSetPoint)
def resultMode = sTemp?.type?.toString()
def can_Circ = false
if(
!(hvacMode in ["off"]) && (
( hvacMode in ["cool"] && schMotFanRuleType in ["Cool_Circ"]) ||
( resultMode in ["cool"] && schMotFanRuleType in ["Cool_Circ", "Heat_Cool_Circ"]) ||
( hvacMode in ["heat"] && schMotFanRuleType in ["Heat_Circ"]) ||
( resultMode in ["heat"] && schMotFanRuleType in ["Heat_Circ", "Heat_Cool_Circ"]) ||
( hvacMode in ["auto"] && schMotFanRuleType in ["Heat_Cool_Circ"]) ||
( hvacMode in ["eco"] && schMotFanRuleType in ["Circ"])
)
) {
can_Circ = true
}
circWantsOn = circulateFanControl(resultMode, curTstatTemp, sTemp?.req?.toDouble(), 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())
}
}
def getReqSetpointTemp(curTemp, reqHeatSetPoint, reqCoolSetPoint) {
//LogAction("getReqSetpointTemp: Current Temp: ${curTemp} Req Heat: ${reqHeatSetPoint} Req Cool: ${reqCoolSetPoint}", "info", false)
def tstat = settings?.schMotTstat
//def modeEco = (curMode == "eco") ? true : false
//def modeAuto = (curMode == "auto") ? true : false
def canHeat = state?.schMotTstatCanHeat
def canCool = state?.schMotTstatCanCool
String hvacMode = tstat ? tstat?.currentThermostatMode.toString() : null
String operState = tstat ? tstat?.currentThermostatOperatingState.toString() : null
String opType = hvacMode.toString()
if(hvacMode == "off") {
return ["req":null, "type":"off"]
}
if((hvacMode == "cool") || (operState == "cooling") || (hvacMode == "eco" && !canHeat && canCool) ) {
opType = "cool"
} else if((hvacMode == "heat") || (operState == "heating")|| (hvacMode == "eco" && !canCool && canHeat) ) {
opType = "heat"
} else if(hvacMode == "auto" || hvacMode == "eco") {
def coolDiff = Math.abs(curTemp - reqCoolSetPoint)
def heatDiff = Math.abs(curTemp - reqHeatSetPoint)
opType = coolDiff < heatDiff ? "cool" : "heat"
}
def temp = (opType == "cool") ? reqCoolSetPoint?.toDouble() : reqHeatSetPoint?.toDouble()
return ["req":temp, "type":opType]
}
def doFanOperation(tempDiff, curTstatTemp, curHeatSetpoint, curCoolSetpoint, circWantsOn) {
String pName = fanCtrlPrefix()
//LogAction("doFanOperation: Temp Difference: (${tempDiff})", "info", false)
try {
def tstat = settings?.schMotTstat
/* def curTstatTemp = tstat ? getRemoteSenTemp().toDouble() : null
def curCoolSetpoint = getRemSenCoolSetTemp()
def curHeatSetpoint = getRemSenHeatSetTemp()
*/
def hvacMode = tstat ? tstat?.currentThermostatMode.toString() : null
def curTstatOperState = tstat?.currentThermostatOperatingState.toString()
def curTstatFanMode = tstat?.currentThermostatFanMode.toString()
//LogAction("doFanOperation: Thermostat Info - ( Temperature: (${curTstatTemp}) | HeatSetpoint: (${curHeatSetpoint}) | CoolSetpoint: (${curCoolSetpoint}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) | FanMode: (${curTstatFanMode}) )", "info", false)
if(state?.haveRunFan == null) { state.haveRunFan = false }
def savedHaveRun = state.haveRunFan
//def wantFanOn = circWantsOn != null ? circWantsOn ? false
def wantFanOn = false
// 1:"Heating/Cooling", 2:"With Fan Only", 3:"Heating", 4:"Cooling"
def validOperModes = []
switch ( settings?."${pName}FanSwitchTriggerType".toInteger() ) {
case 1:
validOperModes = ["heating", "cooling"]
wantFanOn = (curTstatOperState in validOperModes) ? true : false
break
case 2:
wantFanOn = (curTstatFanMode in ["on", "circulate"]) ? true : false
break
case 3:
validOperModes = ["heating"]
wantFanOn = (curTstatOperState in validOperModes) ? true : false
break
case 4:
validOperModes = ["cooling"]
wantFanOn = (curTstatOperState in validOperModes) ? true : false
break
default:
break
}
/*
if( settings?."${pName}FanSwitchTriggerType".toInteger() == 1) {
def validOperModes = ["heating", "cooling"]
wantFanOn = (curTstatOperState in ["heating", "cooling"]) ? true : false
}
if( settings?."${pName}FanSwitchTriggerType".toInteger() == 2) {
wantFanOn = (curTstatFanMode in ["on", "circulate"]) ? true : false
}
//if(settings?."${pName}FanSwitchHvacModeFilter" != "any" && (settings?."${pName}FanSwitchHvacModeFilter" != hvacMode)) {
*/
if( !( ("any" in settings?."${pName}FanSwitchHvacModeFilter") || (hvacMode in settings?."${pName}FanSwitchHvacModeFilter") ) ) {
if(savedHaveRun) {
LogAction("doFanOperation: Evaluating turn fans off; Thermostat Mode does not Match the required Mode", "info", false)
}
wantFanOn = false // force off of fans
}
boolean schedOk = fanCtrlScheduleOk()
if(!schedOk) {
if(savedHaveRun) {
LogAction("doFanOperation: Evaluating turn fans off; Schedule is restricted", "info", false)
}
wantFanOn = false // force off of fans
circWantsOn = false // force off of fans
}
def allOff = true
settings?."${pName}FanSwitches"?.each { sw ->
def swOn = (sw?.currentSwitch.toString() == "on") ? true : false
if(wantFanOn || circWantsOn) {
if(!swOn && !savedHaveRun) {
LogAction("doFanOperation: Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (ON)", "info", 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", "info", false)
}
}
if(swOn && state?.haveRunFan && checkFanSpeedSupport(sw)) {
def t0 = sw?.currentSpeed
def speed = t0 ?: null
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)", "info", 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)", "info", 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)", "info", 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)", "info", 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)", "info", 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", "info", false)
}
}
}
if(swOn) { allOff = false }
}
if(allOff) { state.haveRunFan = false }
} catch (ex) {
log.error "doFanOperation Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "doFanOperation", true, getAutoType())
}
}
def getFanCtrlFanRunDtSec() { return !state?.fanCtrlRunDt ? 100000 : GetTimeDiffSeconds(state?.fanCtrlRunDt, null, "getFanCtrlFanRunDtSec").toInteger() }
def getFanCtrlFanOffDtSec() { return !state?.fanCtrlFanOffDt ? 100000 : GetTimeDiffSeconds(state?.fanCtrlFanOffDt, null, "getFanCtrlFanOffDtSec").toInteger() }
// CONTROLS THE THERMOSTAT FAN
def circulateFanControl(operType, Double curSenTemp, Double reqSetpointTemp, Double threshold, can_Circ) {
String pName = fanCtrlPrefix()
def tstat = settings?.schMotTstat
def tstatsMir = settings?.schMotTstatMir
// input (name: "schMotCirculateTstatFan", type: "bool", 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: "bool", 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
def theFanIsOn = false
def hvacMode = tstat ? tstat?.currentThermostatMode.toString() : null
def curTstatFanMode = tstat?.currentThermostatFanMode.toString()
def fanOn = (curTstatFanMode == "on" || curTstatFanMode == "circulate") ? true : false
def returnToAuto = can_Circ ? false : true
if(hvacMode in ["off"]) { returnToAuto = true }
// Track approximate fan on / off times
if( !fanOn && state?.fanCtrlRunDt > state?.fanCtrlFanOffDt ) {
state?.fanCtrlFanOffDt = getDtNow()
returnToAuto = true
}
if( fanOn && state?.fanCtrlRunDt < state?.fanCtrlFanOffDt ) {
state?.lastfanCtrlFanRunDt = getDtNow()
}
boolean schedOk = fanCtrlScheduleOk()
if(!schedOk) {
returnToAuto = true
}
def curOperState = tstat?.currentnestThermostatOperatingState.toString()
def tstatOperStateOk = (curOperState == "idle") ? true : false
// if ac or heat is on, we should put fan back to auto
if(!tstatOperStateOk) {
if( state?.fanCtrlFanOffDt > state?.fanCtrlRunDt) { return false }
LogAction("Circulate Fan Run: The Thermostat OperatingState is Currently (${strCapitalize(curOperState)}) Skipping", "info", false)
state?.fanCtrlFanOffDt = getDtNow()
returnToAuto = true
}
def fanTempOk = getCirculateFanTempOk(curSenTemp, reqSetpointTemp, threshold, fanOn, operType)
if(hvacMode in ["heat", "auto", "cool", "eco"] && fanTempOk && !returnToAuto) {
if(!fanOn) {
def waitTimeVal = fanCtrlTimeBetweenRuns?.toInteger() ?: 1200
def timeSinceLastOffOk = (getFanCtrlFanOffDtSec() > waitTimeVal) ? true : false
if(!timeSinceLastOffOk) {
def remaining = waitTimeVal - getFanCtrlFanOffDtSec()
LogAction("Circulate Fan: Want to RUN Fan | Delaying for wait period ${waitTimeVal}, remaining ${remaining} seconds", "info", false)
def 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", "info", false)
tstat?.fanOn()
storeLastAction("Turned ${tstat} Fan 'On'", getDtNow(), pName, tstat)
state?.fanCtrlRunDt = getDtNow()
if(tstatsMir) {
tstatsMir?.each { mt ->
LogAction("Circulate Fan: Mirroring Primary Thermostat: Activating '${mt?.displayName}' Fan", "info", false)
mt?.fanOn()
storeLastAction("Turned ${mt.displayName} Fan 'On'", getDtNow(), pName, mt)
}
}
}
theFanIsOn = true
} else {
if(returnToAuto || !fanTempOk) {
if(fanOn && !returnToAuto) {
def fanOnTimeVal = fanCtrlOnTime?.toInteger() ?: 240
def timeSinceLastRunOk = (getFanCtrlFanRunDtSec() > fanOnTimeVal) ? true : false // fan left on for minimum
if(!timeSinceLastRunOk) {
def remaining = fanOnTimeVal - getFanCtrlFanRunDtSec()
LogAction("Circulate Fan Run: Want to STOP Fan | Delaying for run period ${fanOnTimeVal}, remaining ${remaining} seconds", "info", false)
def 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", "info", false)
tstat?.fanAuto()
storeLastAction("Turned ${tstat} Fan to 'Auto'", getDtNow(), pName, tstat)
state?.fanCtrlFanOffDt = getDtNow()
if(tstatsMir) {
tstatsMir?.each { mt ->
LogAction("Circulate Fan: Mirroring Primary Thermostat: Turning OFF '${mt?.displayName}' Fan", "info", false)
mt?.fanAuto()
storeLastAction("Turned ${mt.displayName} Fan 'Off'", getDtNow(), pName, mt)
}
}
}
}
theFanIsOn = false
}
if(theFanIsOn) {
scheduleAutomationEval(120)
}
return theFanIsOn
}
def getCirculateFanTempOk(Double senTemp, Double reqsetTemp, Double threshold, Boolean fanOn, operType) {
def turnOn = false
def tempScaleStr = "${tUnitStr()}"
/*
def 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}", "warn", true)
return false
}
LogAction(" ├ adjust: ${adjust}}${tUnitStr()}", "info", false)
*/
//LogAction(" ├ operType: (${strCapitalize(operType)}) | Temp Threshold: ${threshold}${tempScaleStr} | FanAlreadyOn: (${strCapitalize(fanOn)})", "info", false)
//LogAction(" ├ Sensor Temp: ${senTemp}${tempScaleStr} | Requested Setpoint Temp: ${reqsetTemp}${tempScaleStr}", "info", false)
if(!reqsetTemp) {
//LogAction("getCirculateFanTempOk: Bad reqsetTemp ${reqsetTemp}", "warn", true)
//LogAction("getCirculateFanTempOk:", "info", false)
return false
}
// def ontemp
def offtemp
if(operType == "cool") {
// ontemp = reqsetTemp + threshold
offtemp = reqsetTemp
if(senTemp >= (offtemp + threshold)) { turnOn = true }
// if((senTemp > offtemp) && (senTemp <= (ontemp - adjust))) { turnOn = true }
}
if(operType == "heat") {
// 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}", "info", false)
//LogAction(" ├ offTemp: ${offtemp}${tempScaleStr} | Temp Threshold: ${threshold}${tempScaleStr}", "info", false)
//LogAction(" ┌ Final Result: (${strCapitalize(turnOn)})", "info", false)
// LogAction("getCirculateFanTempOk: ", "info", false)
def resultStr = "getCirculateFanTempOk: The Temperature Difference is "
if(turnOn) {
resultStr += " within "
} else {
resultStr += " Outside "
}
def 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}", "info", disp)
return turnOn
}
/********************************************************************************
| HUMIDITY CONTROL AUTOMATION CODE |
*********************************************************************************/
String humCtrlPrefix() { return "humCtrl" }
boolean isHumCtrlConfigured() {
return (settings?.schMotHumidityControl && (settings?.humCtrlUseWeather || settings?.humCtrlTempSensor) && settings?.humCtrlHumidity && settings?.humCtrlSwitches) ? true : false
}
String humCtrlSwitchDesc(showOpt = true) {
if(settings?.humCtrlSwitches) {
int cCnt = settings?.humCtrlSwitches?.size() ?: 0
String str = ""
int cnt = 0
str += "Switch Status:"
settings?.humCtrlSwitches?.sort { it?.displayName }?.each { dev ->
cnt = cnt+1
def val = strCapitalize(dev?.currentSwitch) ?: "Not Set"
str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: (${val})"
}
if(showOpt) {
str += (settings?.humCtrlSwitchTriggerType || settings?.humCtrlSwitchHvacModeFilter) ? "\n\nSwitch Triggers:" : ""
str += (settings?.humCtrlSwitchTriggerType) ? "\n • Switch Trigger: (${getEnumValue(switchRunEnum(true), settings?.humCtrlSwitchTriggerType)})" : ""
str += (settings?.humCtrlSwitchHvacModeFilter) ? "\n • Hvac Mode Filter: (${getEnumValue(fanModeTrigEnum(), settings?.humCtrlSwitchHvacModeFilter).toString().replaceAll("\\[|\\]", "")})" : ""
}
return str
}
return null
}
String humCtrlHumidityDesc() {
if(settings?.humCtrlHumidity) {
int cCnt = settings?.humCtrlHumidity?.size() ?: 0
String str = ""
int cnt = 0
str += "Sensor Humidity (average): (${getDeviceVarAvg(settings.humCtrlHumidity, "currentHumidity")}%)"
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) ? " " : "│"}└ " : ""}(${val}%)"
}
return str
}
return null
}
def getHumCtrlTemperature() {
def extTemp = 0.0
if(!settings?.humCtrlUseWeather && settings?.humCtrlTempSensor) {
extTemp = getDeviceTemp(settings?.humCtrlTempSensor)
} else {
if(settings?.humCtrlUseWeather && (state?.curWeaTemp_f || state?.curWeaTemp_c)) {
if(getTemperatureScale() == "C") { extTemp = state?.curWeaTemp_c.toDouble() }
else { extTemp = state?.curWeaTemp_f.toDouble() }
}
}
return extTemp
}
int getMaxHumidity(curExtTemp) {
int maxhum = 15
if(curExtTemp != null) {
if(curExtTemp >= adj_temp(40)) {
maxhum = 45
} else if(curExtTemp >= adj_temp(32)) {
maxhum = 45 - ( (adj_temp(40) - curExtTemp)/(adj_temp(40)-adj_temp(32)) ) * 5
//maxhum = 40
} else if(curExtTemp >= adj_temp(20)) {
maxhum = 40 - ( (adj_temp(32) - curExtTemp)/(adj_temp(32)-adj_temp(20)) ) * 5
//maxhum = 35
} else if(curExtTemp >= adj_temp(10)) {
maxhum = 35 - ( (adj_temp(20) - curExtTemp)/(adj_temp(20)-adj_temp(10)) ) * 5
//maxhum = 30
} else if(curExtTemp >= adj_temp(0)) {
maxhum = 30 - ( (adj_temp(10) - curExtTemp)/(adj_temp(10)-adj_temp(0)) ) * 5
//maxhum = 25
} else if(curExtTemp >= adj_temp(-10)) {
maxhum = 25 - Math.abs( (adj_temp(0) - curExtTemp) / (adj_temp(0)-adj_temp(-10)) ) * 5
//maxhum = 20
} else if(curExtTemp >= adj_temp(-20)) {
maxhum = 15
}
}
return maxhum
}
boolean humCtrlScheduleOk() { return autoScheduleOk(humCtrlPrefix()) }
def humCtrlCheck() {
//LogAction("humCtrlCheck", "info", false)
String pName = humCtrlPrefix()
if(getIsAutomationDisabled()) { return }
try {
long execTime = now()
def tstat = settings?.schMotTstat
String hvacMode = tstat ? tstat?.currentThermostatMode.toString() : null
String curTstatOperState = tstat?.currentThermostatOperatingState.toString()
String curTstatFanMode = tstat?.currentThermostatFanMode.toString()
//def curHum = humCtrlHumidity?.currentHumidity
def curHum = getDeviceVarAvg(settings.humCtrlHumidity, "currentHumidity")
def curExtTemp = getHumCtrlTemperature()
def maxHum = getMaxHumidity(curExtTemp)
boolean schedOk = humCtrlScheduleOk()
LogAction("humCtrlCheck: ( Humidity: (${curHum}) | External Temp: (${curExtTemp}) | Max Humidity: (${maxHum}) | HvacMode: (${hvacMode}) | OperatingState: (${curTstatOperState}) )", "info", false)
if(state?.haveRunHumidifier == null) { state.haveRunHumidifier = false }
def savedHaveRun = state?.haveRunHumidifier
boolean humOn = false
if(curHum < maxHum) {
humOn = true
}
// 1:"Heating/Cooling", 2:"With Fan Only", 3:"Heating", 4:"Cooling" 5:"All Operating Modes"
def validOperModes = []
boolean validOperating = true
switch ( settings?.humCtrlSwitchTriggerType?.toInteger() ) {
case 1:
validOperModes = ["heating", "cooling"]
validOperating = (curTstatOperState in validOperModes) ? true : false
break
case 2:
validOperating = (curTstatFanMode in ["on", "circulate"]) ? true : false
break
case 3:
validOperModes = ["heating"]
validOperating = (curTstatOperState in validOperModes) ? true : false
break
case 4:
validOperModes = ["cooling"]
validOperating = (curTstatOperState in validOperModes) ? true : false
break
case 5:
break
default:
break
}
boolean validHvac = true
if( !( ("any" in settings?.humCtrlSwitchHvacModeFilter) || (hvacMode in settings?.humCtrlSwitchHvacModeFilter) ) ){
//LogAction("humCtrlCheck: Evaluating turn humidifier off; Thermostat Mode does not Match the required Mode", "info", 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}", "info", false)
settings?.humCtrlSwitches?.each { sw ->
boolean swOn = (sw?.currentSwitch.toString() == "on") ? true : false
if(turnOn) {
//if(!swOn && !savedHaveRun) {
if(!swOn) {
LogAction("humCtrlCheck: Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (ON)", "info", false)
sw.on()
swOn = true
state.haveRunHumidifier = true
storeLastAction("Turned On $sw)", getDtNow(), pName)
} else {
if(!swOn && savedHaveRun) {
LogAction("humCtrlCheck: savedHaveRun state shows switch ${sw} turned OFF outside of automation requests", "info", false)
}
}
} else {
//if(swOn && savedHaveRun) {
if(swOn) {
LogAction("humCtrlCheck: Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (OFF)", "info", false)
storeLastAction("Turned Off (${sw})", getDtNow(), pName)
sw.off()
state.haveRunHumidifier = false
} else {
if(swOn && !savedHaveRun) {
LogAction("humCtrlCheck: Saved have run state shows switch ${sw} turned ON outside of automation requests", "info", 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 |
*********************************************************************************/
String extTmpPrefix() { return "extTmp" }
boolean isExtTmpConfigured() {
return (settings?.schMotExternalTempOff && (settings?.extTmpUseWeather || settings?.extTmpTempSensor) && settings?.extTmpDiffVal) ? true : false
}
def getWeathUpdSec() { return !state?.weatherUpdDt ? 100000 : GetTimeDiffSeconds(state?.weatherUpdDt, null, "getWeathUpdSec").toInteger() }
def 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(state?.wDevInst) {
def weather = parent.getSettingVal("weatherDevice")
if(weather) {
def temp0
def hum0
if(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 = 0.0
long f_temp = 0
if(getTemperatureScale() == "C") {
c_temp = temp0
f_temp = ((c_temp * (9 / 5)) + 32)
} else {
f_temp = temp0
c_temp = ((f_temp - 32) * (5 / 9))
}
state?.curWeaTemp_f = Math.round(f_temp) as Integer
state?.curWeaTemp_c = Math.round(c_temp.round(1) * 2) / 2.0f
c_temp = estimateDewPoint(hum0, c_temp)
if(state.curWeaTemp_c < c_temp) { c_temp = state.curWeaTemp_c }
f_temp = c_temp * 9.0/5.0 + 32.0
state?.curWeatherDewpointTemp_c = Math.round(c_temp.round(1) * 2) / 2.0f
state?.curWeatherDewpointTemp_f = Math.round(f_temp) as Integer
}
}
storeExecutionHistory((now()-execTime), "getExtConditions")
}
private estimateDewPoint(double rh,double t) {
double L = Math.log(rh/100)
double M = 17.27 * t
double N = 237.3 + t
double B = (L + (M/N)) / 17.27
double dp = (237.3 * B) / (1 - B)
double dp1 = 243.04 * ( Math.log(rh / 100) + ( (17.625 * t) / (243.04 + t) ) ) / (17.625 - Math.log(rh / 100) - ( (17.625 * t) / (243.04 + t) ) )
double ave = (dp + dp1)/2
//LogAction("dp: ${dp.round(1)} dp1: ${dp1.round(1)} ave: ${ave.round(1)}")
ave = dp1
return ave.round(1)
}
def getExtTmpTemperature() {
def extTemp = 0.0
if(!settings?.extTmpUseWeather && settings?.extTmpTempSensor) {
extTemp = getDeviceTemp(settings?.extTmpTempSensor)
} else {
if(settings?.extTmpUseWeather && (state?.curWeaTemp_f || state?.curWeaTemp_c)) {
if(getTemperatureScale() == "C") { extTemp = state?.curWeaTemp_c.toDouble() }
else { extTemp = state?.curWeaTemp_f.toDouble() }
}
}
return extTemp
}
def getExtTmpDewPoint() {
def extDp = 0.0
if(settings?.extTmpUseWeather && (state?.curWeatherDewpointTemp_f || state?.curWeatherDewpointTemp_c)) {
if(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
}
def getDesiredTemp() {
def extTmpTstat = settings?.schMotTstat
String curMode = extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : null
boolean modeOff = (curMode in ["off"]) ? true : false
boolean modeEco = (curMode in ["eco"]) ? true : false
boolean modeCool = (curMode == "cool") ? true : false
boolean modeHeat = (curMode == "heat") ? true : false
boolean modeAuto = (curMode == "auto") ? true : false
def desiredHeatTemp = getRemSenHeatSetTemp(curMode)
def desiredCoolTemp = getRemSenCoolSetTemp(curMode)
String 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}", "info", false)
modeOff = (lastMode in ["off"]) ? true : false
modeCool = (lastMode == "cool") ? true : false
modeHeat = (lastMode == "heat") ? true : false
modeAuto = (lastMode == "auto") ? true : false
}
}
def desiredTemp = 0
if(!modeOff) {
if(desiredHeatTemp && modeHeat) { desiredTemp = desiredHeatTemp }
else if(desiredCoolTemp && modeCool) { desiredTemp = desiredCoolTemp }
else if(desiredHeatTemp && desiredCoolTemp && (desiredHeatTemp < desiredCoolTemp) && modeAuto ) {
desiredTemp = (desiredCoolTemp + desiredHeatTemp) / 2.0
}
//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}", "info", false)
}
return desiredTemp
}
boolean extTmpTempOk(disp=false, last=false) {
//LogTrace("extTmpTempOk")
String pName = extTmpPrefix()
try {
long execTime = now()
def extTmpTstat = settings?.schMotTstat
def extTmpTstatMir = settings?.schMotTstatMir
def intTemp = extTmpTstat ? getRemoteSenTemp().toDouble() : null