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
def extTemp = getExtTmpTemperature()
def dpLimit = getComfortDewpoint(extTmpTstat)
def curDp = getExtTmpDewPoint()
def diffThresh = Math.abs(getExtTmpTempDiffVal())
String curMode = extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : null
boolean modeOff = (curMode == "off") ? true : false
boolean modeCool = (curMode == "cool") ? true : false
boolean modeHeat = (curMode == "heat") ? true : false
boolean modeEco = (curMode == "eco") ? true : false
boolean modeAuto = (curMode == "auto") ? true : false
def canHeat = state?.schMotTstatCanHeat
def canCool = state?.schMotTstatCanCool
//LogAction("extTmpTempOk: Inside Temp: ${intTemp} | curMode: ${curMode} | modeOff: ${modeOff} | modeEco: ${modeEco} | modeAuto: ${modeAuto} || extTmpTstatOffRequested: ${state?.extTmpTstatOffRequested}", "info", false)
boolean retval = true
boolean externalTempOk = true
boolean internalTempOk = true
boolean dpOk = (curDp < dpLimit || !canCool) ? true : false
if(!dpOk) { retval = false }
String str
/*
def modeEco = (curMode in ["eco"]) ? true : false
def home = false
def away = false
if(extTmpTstat && getTstatPresence(extTmpTstat) == "present") { home = true }
else { away = true }
if(away && modeEco) { // we won't pull system out of ECO mode if we are away
retval = false
str = "Nest is away AND in ECO mode"
}
*/
if(!getSafetyTempsOk(extTmpTstat)) {
retval = false
externalTempOk = false
str = "within safety Temperatures "
LogAction("extTmpTempOk: Safety Temps not OK", "warn", true)
}
if(modeOff) {
retval = false
}
def desiredHeatTemp
def desiredCoolTemp
if(modeAuto && retval) {
desiredHeatTemp = getRemSenHeatSetTemp(curMode)
desiredCoolTemp = getRemSenCoolSetTemp(curMode)
}
def lastMode = extTmpTstat?.currentpreviousthermostatMode?.toString()
if(curMode == "eco") {
if(!lastMode && state?.extTmpTstatOffRequested && state?.extTmplastMode) {
lastMode = state?.extTmplastMode
//state?.extTmpSavedTemp
}
if(lastMode) {
//LogAction("extTmpTempOk: Resetting mode curMode: ${curMode} | to previous mode lastMode: ${lastMode} | extTmpTstatOffRequested: ${state?.extTmpTstatOffRequested}", "info", false)
desiredHeatTemp = getRemSenHeatSetTemp(lastMode, modeEco, false)
desiredCoolTemp = getRemSenCoolSetTemp(lastMode, modeEco, false)
if(!desiredHeatTemp) { desiredHeatTemp = state?.extTmpSavedHTemp }
if(!desiredCoolTemp) { desiredCoolTemp = state?.extTmpSavedCTemp }
//modeOff = (lastMode == "off") ? true : false
modeCool = (lastMode == "cool") ? true : false
modeHeat = (lastMode == "heat") ? true : false
modeEco = (lastMode == "eco") ? true : false
modeAuto = (lastMode == "auto") ? true : false
}
}
if(modeAuto && retval && desiredHeatTemp && desiredCoolTemp) {
if( !(extTemp >= (desiredHeatTemp+diffThresh) && extTemp <= (desiredCoolTemp-diffThresh)) ) {
retval = false
externalTempOk = false
str = "within range (${desiredHeatTemp} ${desiredCoolTemp})"
}
//ERS
state?.extTmpSavedHTemp = desiredHeatTemp
state?.extTmpSavedCTemp = desiredCoolTemp
}
def tempDiff
def desiredTemp
def insideThresh
if(!modeAuto && retval) {
desiredTemp = getDesiredTemp()
//ERS
if(desiredTemp) { state?.extTmpSavedTemp = desiredTemp }
if(!desiredTemp) {
desiredTemp = intTemp
if(!modeOff) {
LogAction("extTmpTempOk: No Desired Temp found, using interior Temp", "warn", true)
}
retval = false
} else {
tempDiff = Math.abs(extTemp - desiredTemp)
str = "enough different (${tempDiff})"
insideThresh = getExtTmpInsideTempDiffVal()
LogAction("extTmpTempOk: Outside Temp: ${extTemp} | Inside Temp: ${intTemp} | Desired Temp: ${desiredTemp} | Inside Temp Threshold: ${insideThresh} | Outside Temp Threshold: ${diffThresh} | Actual Difference: ${tempDiff} | Outside Dew point: ${curDp} | Dew point Limit: ${dpLimit}", "info", false)
if(diffThresh && tempDiff < diffThresh) {
retval = false
externalTempOk = false
}
boolean extTempHigh = (extTemp >= desiredTemp) ? true : false
boolean extTempLow = (extTemp <= desiredTemp) ? true : false
def oldMode = state?.extTmpRestoreMode
if(modeCool || oldMode == "cool" || (!canHeat && canCool)) {
str = "greater than"
if(extTempHigh) { retval = false; externalTempOk = false }
if(intTemp > desiredTemp+insideThresh) { retval = false; internalTempOk = false } // too hot inside
}
if(modeHeat || oldMode == "heat" || (!canCool && canHeat)) {
str = "less than"
if(extTempLow) { retval = false; externalTempOk = false }
if(intTemp < desiredTemp-insideThresh) { retval = false; internalTempOk = false } // too cold inside
}
//LogAction("extTmpTempOk: extTempHigh: ${extTempHigh} | extTempLow: ${extTempLow}", "info", false)
}
}
boolean showRes = disp ? (retval != last ? true : false) : false
if(!dpOk) {
LogAction("extTmpTempOk: ${retval} Dewpoint: (${curDp}${tUnitStr()}) is ${dpOk ? "ok" : "TOO HIGH"}", "info", showRes)
} else {
if(!modeAuto) {
LogAction("extTmpTempOk: ${retval} Desired Inside Temp: (${desiredTemp}${tUnitStr()}) is ${externalTempOk ? "" : "Not"} ${str} $diffThresh\u00b0 of Outside Temp: (${extTemp}${tUnitStr()}) ${retval ? "AND" : "OR"} Inside Temp: (${intTemp}) is ${internalTempOk ? "" : "Not"} within Inside Threshold: ${insideThresh} of desired (${desiredTemp})", "info", showRes)
} else {
LogAction("extTmpTempOk: ${retval} Exterior Temperature (${extTemp}${tUnitStr()}) is ${externalTempOk ? "" : "Not"} ${str} using $diffThresh\u00b0 offset | Inside Temp: (${intTemp}${tUnitStr()})", "info", showRes)
}
}
storeExecutionHistory((now() - execTime), "extTmpTempOk")
return retval
} catch (ex) {
log.error "extTmpTempOk Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "extTmpTempOk", true, getAutoType())
}
}
boolean extTmpScheduleOk() { return autoScheduleOk(extTmpPrefix()) }
def getExtTmpTempDiffVal() { return !settings?.extTmpDiffVal ? 1.0 : settings?.extTmpDiffVal.toDouble() }
def getExtTmpInsideTempDiffVal() { return !settings?.extTmpInsideDiffVal ? (getTemperatureScale() == "C" ? 2 : 4) : settings?.extTmpInsideDiffVal.toDouble() }
def getExtTmpWhileOnDtSec() { return !state?.extTmpChgWhileOnDt ? 100000 : GetTimeDiffSeconds(state?.extTmpChgWhileOnDt, null, "getExtTmpWhileOnDtSec").toInteger() }
def getExtTmpWhileOffDtSec() { return !state?.extTmpChgWhileOffDt ? 100000 : GetTimeDiffSeconds(state?.extTmpChgWhileOffDt, null, "getExtTmpWhileOffDtSec").toInteger() }
// allow override from schedule?
def getExtTmpOffDelayVal() { return !settings?.extTmpOffDelay ? 300 : settings?.extTmpOffDelay.toInteger() }
def getExtTmpOnDelayVal() { return !settings?.extTmpOnDelay ? 300 : settings?.extTmpOnDelay.toInteger() }
def extTmpTempCheck(cTimeOut = false) {
//LogAction("extTmpTempCheck", "info", false)
String pName = extTmpPrefix()
try {
if(getIsAutomationDisabled()) { return }
else {
def extTmpTstat = settings?.schMotTstat
def extTmpTstatMir = settings?.schMotTstatMir
long execTime = now()
//state?.autoRunDt = getDtNow()
if(state?."${pName}TimeoutOn" == null) { state."${pName}TimeoutOn" = false }
if(cTimeOut) { state."${pName}TimeoutOn" = true }
boolean timeOut = state."${pName}TimeoutOn" ?: false
String curMode = extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : null
boolean modeOff = (curMode in ["off"]) ? true : false
boolean modeInActive = (curMode in ["off", "eco"]) ? true : false
boolean modeEco = (curMode in ["eco"]) ? true : false
boolean modeAuto = (curMode == "auto") ? true : false
boolean allowNotif = settings?."${pName}NotifOn" ? true : false
// def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
boolean allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
// def speakOnRestore = allowSpeech && settings?."${pName}SpeechOnRestore" ? true : false
if(!modeInActive) { state."${pName}TimeoutOn" = false; timeOut = false }
// if we requested off; and someone switched us on or nMode took over...
if( state?.extTmpTstatOffRequested && (!modeEco || (modeEco && parent.setNModeActive(null))) ) { // reset timer and states
LogAction("extTmpTempCheck: | ${!modeEco ? "HVAC turned on when automation had OFF" : "Automation overridden by nMODE"}, resetting state to match", "warn", true)
state.extTmpChgWhileOnDt = getDtNow()
state.extTmpTstatOffRequested = false
state.extTmpChgWhileOffDt = getDtNow()
state?.extTmpRestoreMode = null
state."${pName}TimeoutOn" = false
unschedTimeoutRestore(pName)
}
if(modeOff) {
storeExecutionHistory((now() - execTime), "extTmpTempCheck")
return
}
String mylastMode = state?.extTmplastMode // when we state change that could change desired Temp ensure delays happen before off can happen again
def lastDesired = state?.extTmpSavedTemp // this catches scheduled temp or hvac mode changes
def desiredTemp = getDesiredTemp()
if( (mylastMode != curMode) || (desiredTemp && desiredTemp != lastDesired)) {
if(!modeInActive) {
state?.extTmplastMode = curMode
//ERS
if(desiredTemp) { state?.extTmpSavedTemp = desiredTemp }
def desiredHeatTemp
def desiredCoolTemp
if(modeAuto) {
desiredHeatTemp = getRemSenHeatSetTemp(curMode)
desiredCoolTemp = getRemSenCoolSetTemp(curMode)
if(desiredHeatTemp && desiredCoolTemp) {
state?.extTmpSavedHTemp = desiredHeatTemp
state?.extTmpSavedCTemp = desiredCoolTemp
}
}
state.extTmpChgWhileOnDt = getDtNow()
} else {
//state.extTmpChgWhileOffDt = getDtNow()
}
}
boolean safetyOk = getSafetyTempsOk(extTmpTstat)
boolean schedOk = extTmpScheduleOk()
boolean okToRestore = (modeEco && state?.extTmpTstatOffRequested && state?.extTmpRestoreMode) ? true : false
boolean tempWithinThreshold = extTmpTempOk( ((modeEco && okToRestore) || (!modeEco && !okToRestore)), okToRestore)
if(!tempWithinThreshold || timeOut || !safetyOk || !schedOk) {
if(allowAlarm) { alarmEvtSchedCleanup(extTmpPrefix()) }
String rmsg = ""
if(okToRestore) {
if(getExtTmpWhileOffDtSec() >= (getExtTmpOnDelayVal() - 5) || timeOut || !safetyOk) {
def lastMode = null
if(state?.extTmpRestoreMode) {
lastMode = extTmpTstat?.currentpreviousthermostatMode?.toString()
if(!lastMode) { lastMode = state?.extTmpRestoreMode }
}
if(lastMode && (lastMode != curMode || timeOut || !safetyOk || !schedOk)) {
scheduleAutomationEval(70)
if(setTstatMode(extTmpTstat, lastMode, pName)) {
storeLastAction("Restored Mode ($lastMode)", getDtNow(), pName, extTmpTstat)
state?.extTmpRestoreMode = null
state?.extTmpTstatOffRequested = false
state?.extTmpRestoredDt = getDtNow()
state.extTmpChgWhileOnDt = getDtNow()
state."${pName}TimeoutOn" = false
unschedTimeoutRestore(pName)
if(extTmpTstatMir) {
if(setMultipleTstatMode(extTmpTstatMir, lastMode, pName)) {
LogAction("Mirroring (${lastMode}) Restore to ${extTmpTstatMir}", "info", false)
}
}
rmsg = "extTmpTempCheck: Restoring '${extTmpTstat?.label}' to '${strCapitalize(lastMode)}' mode: "
boolean needAlarm = false
if(!safetyOk) {
rmsg += "External Temp Safety Temps reached"
needAlarm = true
} else if(!schedOk) {
rmsg += "the schedule does not allow automation control"
} else if(timeOut) {
rmsg += "the (${getEnumValue(longTimeSecEnum(), extTmpOffTimeout)}) Timeout reached"
} else {
rmsg += "External Temp above the Threshold for (${getEnumValue(longTimeSecEnum(), extTmpOnDelay)})"
}
LogAction(rmsg, (needAlarm ? "warn" : "info"), true)
if(allowNotif) {
if(!timeOut && safetyOk) {
sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times others do NOT
// if(speakOnRestore) { sendEventVoiceNotifications(voiceNotifString(state?."${pName}OnVoiceMsg", pName), pName, "nmExtTmpOn_${app?.id}", true, "nmExtTmpOff_${app?.id}") }
} else if(needAlarm) {
sendEventPushNotifications(rmsg, "Warning", pName)
if(allowAlarm) { scheduleAlarmOn(pName) }
}
}
storeExecutionHistory((now() - execTime), "extTmpTempCheck")
return
} else { LogAction("extTmpTempCheck: | There was problem restoring the last mode to '", "error", true) }
} else {
if(!lastMode) {
LogAction("extTmpTempCheck: | Unable to restore settings: previous mode not found. Likely other automation operation", "warn", true)
state?.extTmpTstatOffRequested = false
} else if(!timeOut && safetyOk) { LogAction("extTmpTstatCheck: | Skipping Restore: the Mode to Restore is same as Current Mode ${curMode}", "info", false) }
if(!safetyOk) { LogAction("extTmpTempCheck: | Unable to restore mode and safety temperatures are exceeded", "warn", true) }
// TODO check if timeout quickly cycles back
}
} else {
if(safetyOk) {
def remaining = getExtTmpOnDelayVal() - getExtTmpWhileOffDtSec()
LogAction("extTmpTempCheck: Delaying restore for wait period ${getExtTmpOnDelayVal()}, remaining ${remaining}", "info", false)
def val = Math.min( Math.max(remaining,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
}
}
} else {
if(modeInActive) {
if(timeout || !safetyOk) {
LogAction("extTmpTempCheck: | Timeout or Safety temps exceeded and Unable to restore settings okToRestore is false", "warn", true)
state."${pName}TimeoutOn" = false
}
else if( (!state?.extTmpRestoreMode && state?.extTmpTstatOffRequested) ||
(state?.extTmpRestoreMode && !state?.extTmpTstatOffRequested) ) {
LogAction("extTmpTempCheck: | Unable to restore settings: previous mode not found.", "warn", true)
state?.extTmpRestoreMode = null
state?.extTmpTstatOffRequested = false
}
}
}
}
if(tempWithinThreshold && !timeOut && safetyOk && schedOk && !modeEco) {
String rmsg = ""
if(!modeInActive) {
if(getExtTmpWhileOnDtSec() >= (getExtTmpOffDelayVal() - 2)) {
state."${pName}TimeoutOn" = false
state?.extTmpRestoreMode = curMode
LogAction("extTmpTempCheck: Saving ${extTmpTstat?.label} (${strCapitalize(state?.extTmpRestoreMode)}) mode", "info", false)
scheduleAutomationEval(70)
if(setTstatMode(extTmpTstat, "eco", pName)) {
storeLastAction("Set Thermostat ${extTmpTstat?.displayName} to ECO", getDtNow(), pName, extTmpTstat)
state?.extTmpTstatOffRequested = true
state.extTmpChgWhileOffDt = getDtNow()
scheduleTimeoutRestore(pName)
modeInActive = true
modeEco = true
rmsg = "${extTmpTstat.label} turned 'ECO': External Temp is at the temp threshold for (${getEnumValue(longTimeSecEnum(), extTmpOffDelay)})"
if(extTmpTstatMir) {
if(setMultipleTstatMode(extTmpTstatMir, "eco", pName)) {
LogAction("Mirroring (ECO) Mode to ${extTmpTstatMir}", "info", false)
}
}
LogAction(rmsg, "info", false)
if(allowNotif) {
sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT
// if(allowSpeech) { sendEventVoiceNotifications(voiceNotifString(state?."${pName}OffVoiceMsg",pName), pName, "nmExtTmpOff_${app?.id}", true, "nmExtTmpOn_${app?.id}") }
if(allowAlarm) { scheduleAlarmOn(pName) }
}
} else { LogAction("extTmpTempCheck: Error turning themostat to Eco", "warn", true) }
} else {
def remaining = getExtTmpOffDelayVal() - getExtTmpWhileOnDtSec()
LogAction("extTmpTempCheck: Delaying ECO for wait period ${getExtTmpOffDelayVal()} seconds | Wait time remaining: ${remaining} seconds", "info", false)
def val = Math.min( Math.max(remaining,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
}
} else {
LogAction("extTmpTempCheck: | Skipping: Exterior temperatures in range and '${extTmpTstat?.label}' mode is 'OFF or ECO'", "info", false)
}
} else {
if(timeOut) { LogAction("extTmpTempCheck: Skipping: active timeout", "info", false) }
else if(!safetyOk) { LogAction("extTmpTempCheck: Skipping: Safety Temps Exceeded", "info", false) }
else if(!schedOk) { LogAction("extTmpTempCheck: Skipping: Schedule Restrictions", "info", false) }
//else if(!tempWithinThreshold) { LogAction("extTmpTempCheck: Exterior temperatures not in range", "info", false) }
//else if(modeEco) { LogAction("extTmpTempCheck: Skipping: in ECO mode extTmpTstatOffRequested: (${state?.extTmpTstatOffRequested})", "info", false) }
}
storeExecutionHistory((now() - execTime), "extTmpTempCheck")
}
} catch (ex) {
log.error "extTmpTempCheck Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "extTmpTempCheck", true, getAutoType())
}
}
def extTmpGenericEvt(evt) {
long startTime = now()
int eventDelay = startTime - evt.date.getTime()
LogAction("${evt?.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", "debug", false)
storeLastEventData(evt)
extTmpDpOrTempEvt("${evt?.name}")
}
def extTmpDpOrTempEvt(type) {
if(getIsAutomationDisabled()) { return }
else {
//state.needWeathUpd = false
if(settings?.humCtrlUseWeather || settings?.extTmpUseWeather) {
state.needWeathUpd = false
state?.weatherUpdDt = getDtNow()
getExtConditions()
}
}
if(isExtTmpConfigured()) {
def extTmpTstat = settings?.schMotTstat
String curMode = extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : null
boolean modeOff = (curMode in ["off"]) ? true : false
if(modeOff) {
//LogAction("${type} | Thermostat is off HVAC mode: ${curMode}", "info", false)
return
}
def lastTempWithinThreshold = state?.extTmpWithinThreshold
boolean tempWithinThreshold = extTmpTempOk(false,false)
state?.extTmpWithinThreshold = tempWithinThreshold
if(lastTempWithinThreshold == null || tempWithinThreshold != lastTempWithinThreshold) {
//def extTmpTstat = settings?.schMotTstat
//def curMode = extTmpTstat ? extTmpTstat?.currentThermostatMode?.toString() : null
boolean modeActive = !(curMode in ["off", "eco"]) ? true : false
def offVal = getExtTmpOffDelayVal()
def onVal = getExtTmpOnDelayVal()
def timeVal
if(modeActive) {
state.extTmpChgWhileOnDt = getDtNow()
timeVal = ["valNum":offVal, "valLabel":getEnumValue(longTimeSecEnum(), offVal)]
} else {
state.extTmpChgWhileOffDt = getDtNow()
timeVal = ["valNum":onVal, "valLabel":getEnumValue(longTimeSecEnum(), onVal)]
}
def val = Math.min( Math.max(timeVal?.valNum,defaultAutomationTime()), 60)
LogAction("${type} | External Temp Check scheduled for (${timeVal.valLabel}) HVAC mode: ${curMode}", "info", false)
scheduleAutomationEval(val)
}
//else { LogAction("${type}: Skipping no state change | tempWithinThreshold: ${tempWithinThreshold}", "info", false) }
} else {
scheduleAutomationEval()
}
}
/******************************************************************************
| WATCH CONTACTS AUTOMATION CODE |
*******************************************************************************/
String conWatPrefix() { return "conWat" }
String autoStateDesc(autotype) {
String str = ""
def t0 = state?."${autotype}RestoreMode"
def t1 = state?."${autotype}TstatOffRequested"
str += "ECO State:"
str += "\n • Mode Adjusted: (${t0 != null ? "TRUE" : "FALSE"})"
str += "\n • Last Mode: (${strCapitalize(t0) ?: "Not Set"})"
str += t1 ? "\n • Last Eco Requested: (${t1})" : ""
return str != "" ? str : null
}
String conWatContactDesc() {
if(settings?.conWatContacts) {
int cCnt = settings?.conWatContacts?.size() ?: 0
String str = ""
int cnt = 0
str += "Contact Status:"
settings?.conWatContacts?.sort { it?.displayName }?.each { dev ->
cnt = cnt+1
String t0 = strCapitalize(dev?.currentContact)
String val = t0 ?: "Not Set"
str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: (${val})"
}
return str
}
return null
}
boolean isConWatConfigured() {
return (settings?.schMotContactOff && settings?.conWatContacts && settings?.conWatOffDelay) ? true : false
}
boolean getConWatContactsOk() { return settings?.conWatContacts?.currentContact?.contains("open") ? false : true }
//def conWatContactOk() { return (!settings?.conWatContacts) ? false : true }
boolean conWatScheduleOk() { return autoScheduleOk(conWatPrefix()) }
def getConWatOpenDtSec() { return !state?.conWatOpenDt ? 100000 : GetTimeDiffSeconds(state?.conWatOpenDt, null, "getConWatOpenDtSec").toInteger() }
def getConWatCloseDtSec() { return !state?.conWatCloseDt ? 100000 : GetTimeDiffSeconds(state?.conWatCloseDt, null, "getConWatCloseDtSec").toInteger() }
def getConWatRestoreDelayBetweenDtSec() { return !state?.conWatRestoredDt ? 100000 : GetTimeDiffSeconds(state?.conWatRestoredDt, null, "getConWatRestoreDelayBetweenDtSec").toInteger() }
// allow override from schedule?
def getConWatOffDelayVal() { return !settings?.conWatOffDelay ? 300 : (settings?.conWatOffDelay.toInteger()) }
def getConWatOnDelayVal() { return !settings?.conWatOnDelay ? 300 : (settings?.conWatOnDelay.toInteger()) }
def getConWatRestoreDelayBetweenVal() { return !settings?.conWatRestoreDelayBetween ? 600 : settings?.conWatRestoreDelayBetween.toInteger() }
def conWatCheck(cTimeOut = false) {
LogTrace("conWatCheck $cTimeOut")
//
// There should be monitoring of actual temps for min and max warnings given on/off automations
//
// Should have some check for stuck contacts
//
String pName = conWatPrefix()
def conWatTstat = settings?.schMotTstat
def conWatTstatMir = settings?.schMotTstatMir
try {
if(getIsAutomationDisabled()) { return }
else {
long execTime = now()
//state?.autoRunDt = getDtNow()
if(state?."${pName}TimeoutOn" == null) { state."${pName}TimeoutOn" = false }
if(cTimeOut) { state."${pName}TimeoutOn" = true }
boolean timeOut = state."${pName}TimeoutOn" ?: false
String curMode = conWatTstat ? conWatTstat?.currentThermostatMode.toString() : null
boolean modeEco = (curMode in ["eco"]) ? true : false
//def curNestPres = getTstatPresence(conWatTstat)
boolean modeOff = (curMode in ["off", "eco"]) ? true : false
boolean allowNotif = settings?."${pName}NotifOn" ? true : false
// def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
boolean allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
// def speakOnRestore = allowSpeech && settings?."${pName}SpeechOnRestore" ? true : false
//log.debug "curMode: $curMode | modeOff: $modeOff | conWatRestoreOnClose: $conWatRestoreOnClose | lastMode: $lastMode"
//log.debug "conWatTstatOffRequested: ${state?.conWatTstatOffRequested} | getConWatCloseDtSec(): ${getConWatCloseDtSec()}"
if(!modeEco) { state."${pName}TimeoutOn" = false; timeOut = false }
// if we requested off; and someone switched us on or nMode took over...
if( state?.conWatTstatOffRequested && (!modeEco || (modeEco && parent.setNModeActive(null))) ) { // so reset timer and states
LogAction("conWatCheck: | ${!modeEco ? "HVAC turned on when automation had OFF" : "Automation overridden by nMODE"}, resetting state to match", "warn", true)
state?.conWatRestoreMode = null
state?.conWatTstatOffRequested = false
state?.conWatOpenDt = getDtNow()
state."${pName}TimeoutOn" = false
unschedTimeoutRestore(pName)
}
def mylastMode = state?.conWatlastMode // when we state change modes, ensure delays happen before off can happen again
state?.conWatlastMode = curMode
if(!modeOff && (mylastMode != curMode)) { state?.conWatOpenDt = getDtNow() }
boolean safetyOk = getSafetyTempsOk(conWatTstat)
boolean schedOk = conWatScheduleOk()
boolean okToRestore = (modeEco && state?.conWatTstatOffRequested) ? true : false
boolean contactsOk = getConWatContactsOk()
if(contactsOk || timeOut || !safetyOk || !schedOk) {
if(allowAlarm) { alarmEvtSchedCleanup(conWatPrefix()) }
String rmsg = ""
if(okToRestore) {
if(getConWatCloseDtSec() >= (getConWatOnDelayVal() - 5) || timeOut || !safetyOk) {
def lastMode = null
if(state?.conWatRestoreMode) {
lastMode = conWatTstat?.currentpreviousthermostatMode?.toString()
if(!lastMode) { lastMode = state?.conWatRestoreMode }
}
if(lastMode && (lastMode != curMode || timeOut || !safetyOk || !schedOk)) {
scheduleAutomationEval(70)
if(setTstatMode(conWatTstat, lastMode, pName)) {
storeLastAction("Restored Mode ($lastMode) to $conWatTstat", getDtNow(), pName, conWatTstat)
state?.conWatRestoreMode = null
state?.conWatTstatOffRequested = false
state?.conWatRestoredDt = getDtNow()
state?.conWatOpenDt = getDtNow()
state."${pName}TimeoutOn" = false
unschedTimeoutRestore(pName)
modeEco = false
modeOff = false
if(conWatTstatMir) {
if(setMultipleTstatMode(conWatTstatMir, lastMode, pName)) {
LogAction("Mirroring (${lastMode}) Restore to ${conWatTstatMir}", "info", false)
}
}
rmsg = "Restoring '${conWatTstat?.label}' to '${strCapitalize(lastMode)}' mode: "
boolean needAlarm = false
if(!safetyOk) {
rmsg += "Global Safety Values reached"
needAlarm = true
} else if(timeOut) {
rmsg += "(${getEnumValue(longTimeSecEnum(), conWatOffTimeout)}) Timeout reached"
} else if(!schedOk) {
rmsg += "of Schedule restrictions"
} else {
rmsg += "ALL contacts 'Closed' for (${getEnumValue(longTimeSecEnum(), conWatOnDelay)})"
}
LogAction(rmsg, (needAlarm ? "warn" : "info"), true)
//ERS
if(allowNotif) {
if(!timeOut && safetyOk) {
sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT
// if(speakOnRestore) { sendEventVoiceNotifications(voiceNotifString(state?."${pName}OnVoiceMsg",pName), pName, "nmConWatOn_${app?.id}", true, "nmConWatOff_${app?.id}") }
} else if(needAlarm) {
sendEventPushNotifications(rmsg, "Warning", pName)
if(allowAlarm) { scheduleAlarmOn(pName) }
}
}
storeExecutionHistory((now() - execTime), "conWatCheck")
return
} else { LogAction("conWatCheck: | There was Problem Restoring the Last Mode to ($lastMode)", "error", true) }
} else {
if(!lastMode) {
LogAction("conWatCheck: | Unable to restore settings: previous mode not found. Likely other automation operation", "warn", true)
state?.conWatTstatOffRequested = false
} else if(!timeOut && safetyOk) { LogAction("conWatCheck: | Skipping Restore: the Mode to Restore is same as Current Mode ${curMode}", "info", false) }
if(!safetyOk) { LogAction("conWatCheck: | Unable to restore mode and safety temperatures are exceeded", "warn", true) }
}
} else {
if(safetyOk) {
def remaining = getConWatOnDelayVal() - getConWatCloseDtSec()
LogAction("conWatCheck: Delaying restore for wait period ${getConWatOnDelayVal()}, remaining ${remaining}", "info", false)
def val = Math.min( Math.max(remaining,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
}
}
} else {
if(modeOff) {
if(timeOut || !safetyOk) {
LogAction("conWatCheck: | Timeout or Safety temps exceeded and Unable to restore settings okToRestore is false", "warn", true)
state."${pName}TimeoutOn" = false
}
else if(!state?.conWatRestoreMode && state?.conWatTstatOffRequested) {
LogAction("conWatCheck: | Unable to restore settings: previous mode not found. Likely other automation operation", "warn", true)
state?.conWatTstatOffRequested = false
}
}
}
}
if(!contactsOk && safetyOk && !timeOut && schedOk && !modeEco) {
String rmsg = ""
if(!modeOff) {
if((getConWatOpenDtSec() >= (getConWatOffDelayVal() - 2)) && (getConWatRestoreDelayBetweenDtSec() >= (getConWatRestoreDelayBetweenVal() - 2))) {
state."${pName}TimeoutOn" = false
state?.conWatRestoreMode = curMode
def t0 = getOpenContacts(conWatContacts)
String openCtDesc = t0 ? " '${t0?.join(", ")}' " : " a selected contact "
LogAction("conWatCheck: Saving ${conWatTstat?.label} mode (${strCapitalize(state?.conWatRestoreMode)})", "info", false)
LogAction("conWatCheck: ${openCtDesc}${t0?.size() > 1 ? "are" : "is"} still Open: Turning 'OFF' '${conWatTstat?.label}'", "debug", false)
scheduleAutomationEval(70)
if(setTstatMode(conWatTstat, "eco", pName)) {
storeLastAction("Set $conWatTstat to 'ECO'", getDtNow(), pName, conWatTstat)
state?.conWatTstatOffRequested = true
state?.conWatCloseDt = getDtNow()
scheduleTimeoutRestore(pName)
if(conWatTstatMir) {
if(setMultipleTstatMode(conWatTstatMir, "eco", pName)) {
LogAction("Mirroring (ECO) Mode to ${conWatTstatMir}", "info", false)
}
}
rmsg = "${conWatTstat.label} turned to 'ECO': ${openCtDesc}Opened for (${getEnumValue(longTimeSecEnum(), conWatOffDelay)})"
LogAction(rmsg, "info", false)
if(allowNotif) {
sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT
// if(allowSpeech) { sendEventVoiceNotifications(voiceNotifString(state?."${pName}OffVoiceMsg",pName), pName, "nmConWatOff_${app?.id}", true, "nmConWatOn_${app?.id}") }
if(allowAlarm) { scheduleAlarmOn(pName) }
}
} else { LogAction("conWatCheck: Error turning themostat to ECO", "warn", true) }
} else {
if(getConWatRestoreDelayBetweenDtSec() < (getConWatRestoreDelayBetweenVal() - 2)) {
def remaining = getConWatRestoreDelayBetweenVal() - getConWatRestoreDelayBetweenDtSec()
//LogAction("conWatCheck: | Skipping ECO change: delay since last restore not met (${getEnumValue(longTimeSecEnum(), conWatRestoreDelayBetween)})", "info", false)
def val = Math.min( Math.max(remaining,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
} else {
def remaining = getConWatOffDelayVal() - getConWatOpenDtSec()
LogAction("conWatCheck: Delaying ECO for wait period ${getConWatOffDelayVal()} seconds | Wait time remaining: ${remaining} seconds", "info", false)
def val = Math.min( Math.max(remaining,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
}
}
} else {
LogAction("conWatCheck: | Skipping ECO change: '${conWatTstat?.label}' mode is '${curMode}'", "info", false)
}
} else {
if(timeOut) { LogAction("conWatCheck: Skipping: active timeout", "info", false) }
else if(!schedOk) { LogAction("conWatCheck: Skipping: Schedule Restrictions", "info", false) }
else if(!safetyOk) { LogAction("conWatCheck: Skipping: Safety Temps Exceeded", "warn", true) }
else if(contactsOk) { LogAction("conWatCheck: Contacts are closed", "info", false) }
//else if(modeEco) { LogAction("conWatTempCheck: Skipping: in ECO mode conWatTstatOffRequested: (${state?.conWatTstatOffRequested})", "info", false) }
}
storeExecutionHistory((now() - execTime), "conWatCheck")
}
} catch (ex) {
log.error "conWatCheck Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "conWatCheck", true, getAutoType())
}
}
def conWatContactEvt(evt) {
long startTime = now()
long eventDelay = startTime - (long)evt.date.getTime()
LogAction("${evt?.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", "debug", false)
if(getIsAutomationDisabled()) { return }
else {
def conWatTstat = settings?.schMotTstat
String curMode = conWatTstat ? conWatTstat?.currentThermostatMode?.toString() : null
boolean isModeOff = (curMode in ["eco"]) ? true : false
boolean conOpen = (evt?.value == "open") ? true : false
boolean canSched = false
def timeVal
if(conOpen) {
state?.conWatOpenDt = getDtNow()
timeVal = ["valNum":getConWatOffDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getConWatOffDelayVal())]
canSched = true
}
else if(!conOpen && getConWatContactsOk()) {
state.conWatCloseDt = getDtNow()
if(isModeOff) {
timeVal = ["valNum":getConWatOnDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getConWatOnDelayVal())]
canSched = true
}
}
storeLastEventData(evt)
if(canSched) {
//LogAction("conWatContactEvt: Contact Check scheduled for (${timeVal?.valLabel})", "info", false)
def val = Math.min( Math.max(timeVal?.valNum,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
} else {
LogAction("conWatContactEvt: Skipping Event", "info", false)
}
}
}
/******************************************************************************
| WATCH FOR LEAKS AUTOMATION LOGIC CODE |
******************************************************************************/
String leakWatPrefix() { return "leakWat" }
String leakWatSensorsDesc() {
if(settings?.leakWatSensors) {
int cCnt = settings?.leakWatSensors?.size() ?: 0
String str = ""
int cnt = 0
str += "Leak Sensors:"
settings?.leakWatSensors?.sort { it?.displayName }?.each { dev ->
cnt = cnt+1
String t0 = strCapitalize(dev?.currentWater)
String val = t0 ?: "Not Set"
str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: (${val})"
}
return str
}
return null
}
boolean isLeakWatConfigured() {
return (settings?.schMotWaterOff && settings?.leakWatSensors) ? true : false
}
boolean getLeakWatSensorsOk() { return settings?.leakWatSensors?.currentWater?.contains("wet") ? false : true }
//def leakWatSensorsOk() { return (!settings?.leakWatSensors) ? false : true }
//def leakWatScheduleOk() { return autoScheduleOk(leakWatPrefix()) }
// allow override from schedule?
def getLeakWatOnDelayVal() { return !settings?.leakWatOnDelay ? 300 : settings?.leakWatOnDelay.toInteger() }
def getLeakWatDryDtSec() { return !state?.leakWatDryDt ? 100000 : GetTimeDiffSeconds(state?.leakWatDryDt, null, "getLeakWatDryDtSec").toInteger() }
def leakWatCheck() {
//LogTrace("leakWatCheck")
//
// if we cannot save/restore settings, don't bother turning things off
//
String pName = leakWatPrefix()
try {
if(getIsAutomationDisabled()) { return }
else {
def leakWatTstat = settings?.schMotTstat
def leakWatTstatMir = settings?.schMotTstatMir
long execTime = now()
//state?.autoRunDt = getDtNow()
String curMode = leakWatTstat?.currentThermostatMode.toString()
//def curNestPres = getTstatPresence(leakWatTstat)
boolean modeOff = (curMode == "off") ? true : false
boolean allowNotif = settings?."${pName}NotifOn" ? true : false
// def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
boolean allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
// def speakOnRestore = allowSpeech && settings?."${pName}SpeechOnRestore" ? true : false
if(!modeOff && state?.leakWatTstatOffRequested) { // someone switched us on when we had turned things off, so reset timer and states
LogAction("leakWatCheck: | System turned on when automation had OFF, resetting state to match", "warn", true)
state?.leakWatRestoreMode = null
state?.leakWatTstatOffRequested = false
}
boolean safetyOk = getSafetyTempsOk(leakWatTstat)
//def schedOk = leakWatScheduleOk()
boolean okToRestore = (modeOff && state?.leakWatTstatOffRequested) ? true : false
boolean sensorsOk = getLeakWatSensorsOk()
if(sensorsOk || !safetyOk) {
if(allowAlarm) { alarmEvtSchedCleanup(leakWatPrefix()) }
String rmsg = ""
if(okToRestore) {
if(getLeakWatDryDtSec() >= (getLeakWatOnDelayVal() - 5) || !safetyOk) {
def lastMode = null
if(state?.leakWatRestoreMode) { lastMode = state?.leakWatRestoreMode }
if(lastMode && (lastMode != curMode || !safetyOk)) {
scheduleAutomationEval(70)
if(setTstatMode(leakWatTstat, lastMode, pName)) {
storeLastAction("Restored Mode ($lastMode) to $leakWatTstat", getDtNow(), pName, leakWatTstat)
state?.leakWatTstatOffRequested = false
state?.leakWatRestoreMode = null
state?.leakWatRestoredDt = getDtNow()
if(leakWatTstatMir) {
if(setMultipleTstatMode(leakWatTstatMir, lastmode, pName)) {
LogAction("leakWatCheck: Mirroring Restoring Mode (${lastMode}) to ${leakWatTstatMir}", "info", false)
}
}
rmsg = "Restoring '${leakWatTstat?.label}' to '${strCapitalize(lastMode)}' mode: "
boolean needAlarm = false
if(!safetyOk) {
rmsg += "External Temp Safety Temps reached"
needAlarm = true
} else {
rmsg += "ALL leak sensors 'Dry' for (${getEnumValue(longTimeSecEnum(), leakWatOnDelay)})"
}
LogAction(rmsg, needAlarm ? "warn" : "info", true)
if(allowNotif) {
if(safetyOk) {
sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT
// if(speakOnRestore) { sendEventVoiceNotifications(voiceNotifString(state?."${pName}OnVoiceMsg", pName), pName, "nmLeakWatOn_${app?.id}", true, "nmLeakWatOff_${app?.id}") }
} else if(needAlarm) {
sendEventPushNotifications(rmsg, "Warning", pName)
if(allowAlarm) { scheduleAlarmOn(pName) }
}
}
storeExecutionHistory((now() - execTime), "leakWatCheck")
return
} else { LogAction("leakWatCheck: | There was problem restoring the last mode to ${lastMode}", "error", true) }
} else {
if(!safetyOk) {
LogAction("leakWatCheck: | Unable to restore mode and safety temperatures are exceeded", "warn", true)
} else {
LogAction("leakWatCheck: | Skipping Restore: Mode to Restore (${lastMode}) is same as Current Mode ${curMode}", "info", false)
}
}
} else {
if(safetyOk) {
def remaining = getLeakWatOnDelayVal() - getLeakWatDryDtSec()
LogAction("leakWatCheck: Delaying restore for wait period ${getLeakWatOnDelayVal()}, remaining ${remaining}", "info", false)
def val = Math.min( Math.max(remaining,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
}
}
} else {
if(modeOff) {
if(!safetyOk) {
LogAction("leakWatCheck: | Safety temps exceeded and Unable to restore settings okToRestore is false", "warn", true)
}
else if(!state?.leakWatRestoreMode && state?.leakWatTstatOffRequested) {
LogAction("leakWatCheck: | Unable to restore settings: previous mode not found. Likely other automation operation", "warn", true)
state?.leakWatTstatOffRequested = false
}
}
}
}
// tough decision here: there is a leak, do we care about schedule ?
// if(!getLeakWatSensorsOk() && safetyOk && schedOk) {
if(!sensorsOk && safetyOk) {
String rmsg = ""
if(!modeOff) {
state?.leakWatRestoreMode = curMode
def t0 = getWetWaterSensors(leakWatSensors)
String wetCtDesc = t0 ? " '${t0?.join(", ")}' " : " a selected leak sensor "
LogAction("leakWatCheck: Saving ${leakWatTstat?.label} mode (${strCapitalize(state?.leakWatRestoreMode)})", "info", false)
LogAction("leakWatCheck: ${wetCtDesc}${t0?.size() > 1 ? "are" : "is"} Wet: Turning 'OFF' '${leakWatTstat?.label}'", "debug", false)
scheduleAutomationEval(70)
if(setTstatMode(leakWatTstat, "off", pName)) {
storeLastAction("Turned Off $leakWatTstat", getDtNow(), pName, leakWatTstat)
state?.leakWatTstatOffRequested = true
state?.leakWatDryDt = null // getDtNow()
if(leakWatTstatMir) {
if(setMultipleTstatMode(leakWatTstatMir, "off", pName)) {
LogAction("leakWatCheck: Mirroring (Off) Mode to ${leakWatTstatMir}", "info", false)
}
}
rmsg = "${leakWatTstat.label} turned 'OFF': ${wetCtDesc}has reported it's WET"
LogAction(rmsg, "warn", true)
if(allowNotif) {
sendEventPushNotifications(rmsg, "Warning", pName) // this uses parent and honors quiet times, others do NOT
// if(allowSpeech) { sendEventVoiceNotifications(voiceNotifString(state?."${pName}OffVoiceMsg",pName), pName, "nmLeakWatOff_${app?.id}", true, "nmLeakWatOn_${app?.id}") }
if(allowAlarm) { scheduleAlarmOn(pName) }
}
} else { LogAction("leakWatCheck: Error turning themostat Off", "warn", true) }
} else {
LogAction("leakWatCheck: | Skipping change: '${leakWatTstat?.label}' mode is already 'OFF'", "info", false)
}
} else {
//if(!schedOk) { LogAction("leakWatCheck: Skipping: Schedule Restrictions", "warn", true) }
if(!safetyOk) { LogAction("leakWatCheck: Skipping: Safety Temps Exceeded", "warn", true) }
if(sensorsOk) { LogAction("leakWatCheck: Sensors are ok", "info", false) }
}
storeExecutionHistory((now() - execTime), "leakWatCheck")
}
} catch (ex) {
log.error "leakWatCheck Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "leakWatCheck", true, getAutoType())
}
}
def leakWatSensorEvt(evt) {
long startTime = now()
long eventDelay = startTime - evt.date.getTime()
LogAction("${evt?.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", "debug", false)
if(getIsAutomationDisabled()) { return }
else {
String curMode = leakWatTstat?.currentThermostatMode.toString()
boolean isModeOff = (curMode == "off") ? true : false
boolean leakWet = (evt?.value == "wet") ? true : false
boolean canSched = false
def timeVal
if(leakWet) {
canSched = true
}
else if(!leakWet && getLeakWatSensorsOk()) {
if(isModeOff) {
state?.leakWatDryDt = getDtNow()
timeVal = ["valNum":getLeakWatOnDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getLeakWatOnDelayVal())]
canSched = true
}
}
storeLastEventData(evt)
if(canSched) {
LogAction("leakWatSensorEvt: Leak Check scheduled (${timeVal?.valLabel})", "info", false)
def val = Math.min( Math.max(timeVal?.valNum,defaultAutomationTime()), 60)
scheduleAutomationEval(val)
} else {
LogAction("leakWatSensorEvt: Skipping Event", "info", false)
}
}
}
/********************************************************************************
| MODE AUTOMATION CODE |
*********************************************************************************/
String nModePrefix() { return "nMode" }
def nestModePresPage() {
//Logger("in nestModePresPage")
String pName = nModePrefix()
dynamicPage(name: "nestModePresPage", title: "Nest Mode - Nest Home/Away Automation", uninstall: false, install: true) {
if(!nModePresSensor && !nModeSwitch) {
def modeReq = (nModeHomeModes && nModeAwayModes)
section(sectionTitleStr("Set Nest Presence with location Modes:")) {
input "nModeHomeModes", "mode", title: imgTitle(getAppImg("mode_home_icon.png"), inputTitleStr("Modes to Set Nest Location 'Home'")), multiple: true, submitOnChange: true, required: modeReq
if(checkModeDuplication(nModeHomeModes, nModeAwayModes)) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("ERROR:\nDuplicate Mode(s) were found under both the Home and Away Modes.\nPlease Correct to Proceed")), required: true, state: null
}
input "nModeAwayModes", "mode", title: imgTitle(getAppImg("mode_away_icon.png"), inputTitleStr("Modes to Set Nest Location 'Away'")), multiple: true, submitOnChange: true, required: modeReq
if(nModeHomeModes || nModeAwayModes) {
//Logger("in part 11")
def str = ""
def locPres = getNestLocPres()
str += location?.mode || locPres ? "Location Mode Status:" : ""
str += location?.mode ? "\n${locPres ? "├" : "└"} Hub: (${location?.mode})" : ""
str += locPres ? "\n└ Nest Location: (${locPres == "away" ? "Away" : "Home"})" : ""
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != "" ? "complete" : null)
}
}
}
if(!nModeHomeModes && !nModeAwayModes && !nModeSwitch) {
section(sectionTitleStr("(Optional) Set Nest Presence using Presence Sensor:")) {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Choose a Presence Sensor(s) to use to set your Nest to Home/Away"))
def t0 = nModePresenceDesc()
def presDesc = t0 ? "\n\n${t0}" + descriptions("d_ttm") : descriptions("d_ttc")
//Logger("in part 12")
input "nModePresSensor", "capability.presenceSensor", title: imgTitle(getAppImg("presence_icon.png"), inputTitleStr("Select Presence Sensor(s)")), description: presDesc, multiple: true, submitOnChange: true, required: false
if(nModePresSensor) {
if(nModePresSensor.size() > 1) {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("How this Works!"))
paragraph sectionTitleStr("Nest Location will be set to 'Away' when all Presence sensors leave and will return to 'Home' when someone arrives")
}
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${t0}")), state: "complete"
}
}
}
if(!nModePresSensor && !nModeHomeModes && !nModeAwayModes) {
//Logger("in part 13")
section(sectionTitleStr("(Optional) Set Nest Presence based on the state of a Switch:")) {
input "nModeSwitch", "capability.switch", title: imgTitle(getAppImg("i_sw"), inputTitleStr("Select a Switch")), required: false, multiple: false, submitOnChange: true
if(nModeSwitch) {
input "nModeSwitchOpt", "enum", title: imgTitle(getAppImg("i_set"), inputTitleStr("Switch State to Trigger 'Away'?")), required: true, defaultValue: "On", options: ["On", "Off"], submitOnChange: true
}
}
}
if(parent.getSettingVal("cameras")) {
section(sectionTitleStr("Nest Cam Options:")) {
input (name: "nModeCamOnAway", type: "bool", title: imgTitle(getAppImg("camera_green_icon.png"), inputTitleStr("Turn On Nest Cams when Away?")), required: false, defaultValue: false, submitOnChange: true)
input (name: "nModeCamOffHome", type: "bool", title: imgTitle(getAppImg("camera_gray_icon.png"), inputTitleStr("Turn Off Nest Cams when Home?")), required: false, defaultValue: false, submitOnChange: true)
if(settings?.nModeCamOffHome || settings?.nModeCamOnAway) {
paragraph paraTitleStr("Optional")
paragraph sectionTitleStr("You can choose which cameras are changed when Home/Away. If you don't select any devices all will be changed.")
input (name: "nModeCamsSel", type: "capability.soundSensor", title: imgTitle(getAppImg("camera_blue_icon.png"), inputTitleStr("Select your Nest Cams?")), required: false, multiple: true, submitOnChange: true)
}
}
}
if((nModeHomeModes && nModeAwayModes) || nModePresSensor || nModeSwitch) {
section(sectionTitleStr("Additional Settings:")) {
//Logger("in part 14")
input (name: "nModeSetEco", type: "bool", title: imgTitle(getDevImg("eco_icon.png"), inputTitleStr("Set ECO mode when away?")), required: false, defaultValue: false, submitOnChange: true)
input (name: "nModeDelay", type: "bool", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Changes?")), required: false, defaultValue: false, submitOnChange: true)
if(settings?.nModeDelay) {
input "nModeDelayVal", "enum", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Delay before change?")), required: false, defaultValue: 60, options:longTimeSecEnum(), submitOnChange: true
}
}
}
if(((nModeHomeModes && nModeAwayModes) && !nModePresSensor) || nModePresSensor) {
//Logger("in part 15")
section(getDmtSectionDesc(pName)) {
def pageDesc = getDayModeTimeDesc(pName)
href "setDayModeTimePage1", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName": "${pName}"]
}
section(sectionTitleStr(titles("t_nt"))) {
def t0 = getNotifConfigDesc(pName)
def pageDesc = t0 ? "${t0}" + descriptions("d_ttm") : ""
href "setNotificationPage2", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName":"${pName}", "allowSpeech":false, "allowAlarm":false, "showSchedule":true]
}
}
/*
if(state?.showHelp) {
section("Help:") {
href url:"${getAutoHelpPageUrl()}", style:"embedded", required:false, title:"Help and Instructions", description:"", image: getAppImg("info.png")
}
}
*/
}
}
String nModePresenceDesc() {
if(settings?.nModePresSensor) {
Integer cCnt = settings?.nModePresSensor?.size() ?: 0
String str = ""
Integer cnt = 0
str += "Presence Status:"
settings?.nModePresSensor?.sort { it?.displayName }?.each { dev ->
cnt = cnt+1
String t0 = strCapitalize(dev?.currentPresence)
String presState = t0 ?: "No State"
str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: ${(dev?.label?.toString()?.length() > 10) ? "\n${(cCnt == 1 || cnt == cCnt) ? " " : " │"} └ " : ""}(${presState})"
}
return str
}
return null
}
Boolean isNestModesConfigured() {
return ((!settings?.nModePresSensor && !settings?.nModeSwitch && (settings?.nModeHomeModes && settings?.nModeAwayModes)) || (settings?.nModePresSensor && !settings?.nModeSwitch) || (!settings?.nModePresSensor && settings?.nModeSwitch))
}
def nModeGenericEvt(evt) {
long startTime = now()
long eventDelay = startTime - evt.date.getTime()
LogAction("${evt?.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", "debug", false)
if(getIsAutomationDisabled()) { return }
storeLastEventData(evt)
if(settings?.nModeDelay) {
Integer delay = settings?.nModeDelayVal.toInteger() ?: 60
if(delay > defaultAutomationTime()) {
LogAction("Event | A Check is scheduled (${getEnumValue(longTimeSecEnum(), settings?.nModeDelayVal)})", "info", false)
scheduleAutomationEval(delay)
} else { scheduleAutomationEval() }
} else {
scheduleAutomationEval()
}
}
void adjustCameras(on, sendAutoType=null) {
def cams = parent?.getSettingVal("cameras")
if(cams) {
def foundCams
if(settings?.nModeCamsSel) {
foundCams = settings?.nModeCamsSel
} else {
foundCams = cams?.collect { parent.getDevice(it) } //parent.getCameraDevice(it) }
}
foundCams.each { dev ->
if(dev) {
def didstr = "On"
try {
if(on) {
dev?.on()
} else {
dev?.off()
didstr = "Off"
}
LogAction("adjustCameras: Turning Streaming ${didstr} for (${dev?.displayName})", "info", false)
storeLastAction("Turned ${didstr} Streaming ${dev?.displayName}", getDtNow(), sendAutoType)
}
catch (ex) {
log.error "adjustCameras() Exception: ${dev?.label} does not support commands on / off ${ex?.message}"
sendEventPushNotifications("Camera commands not found, check IDE logs and installation instructions", "Warning", nModePrefix())
//parent?.sendExceptionData(ex, "adjustCameras", true, getAutoType())
}
return dev
}
}
}
}
void adjustEco(boolean on, String senderAutoType) {
def tstats = parent.getSettingVal("thermostats")
def foundTstats
if(tstats) {
foundTstats = tstats?.collect { dni ->
//foundTstats = tstats?.each { d1 ->
def d1 = parent.getDevice(dni)
//def d1 = parent.getThermostatDevice(dni)
if(d1) {
def didstr = null
def tstatAction = null
def curMode = d1?.currentThermostatMode?.toString()
def prevMode = d1?.currentpreviousthermostatMode?.toString()
//LogAction("adjustEco: CURMODE: ${curMode} ON: ${on} PREVMODE: ${prevMode}", "info", false)
if(on && !(curMode in ["eco", "off"])) {
didstr = "ECO"
tstatAction = "eco"
}
if(!on && curMode in ["eco"]) {
if(prevMode && prevMode != curMode) {
didstr = "$prevMode"
tstatAction = prevMode
}
}
if(didstr) {
setTstatMode(d1, tstatAction, senderAutoType)
LogAction("adjustEco($on): | Thermostat: ${d1?.displayName} setting to HVAC mode $didstr was $curMode", "debug", false)
storeLastAction("Set ${d1?.displayName} to $didstr", getDtNow(), senderAutoType, d1)
} else {
/*
if(on && (curMode in ["eco"])) { // override device to know nMODE is active
if(senderAutoType) { sendEcoActionDescToDevice(d1, senderAutoType) } // THIS ONLY WORKS ON NEST THERMOSTATS
}
*/
LogAction("adjustEco: | Thermostat: ${d1?.displayName} NOCHANGES CURMODE: ${curMode} ON: ${on} PREVMODE: ${prevMode}", "debug", false)
}
return d1
} else { LogAction("adjustEco NO D1", "warn", true) }
}
}
}
void setAway(boolean away) {
def tstats = parent.getSettingVal("thermostats")
def didstr = away ? "AWAY" : "HOME"
def foundTstats
if(tstats) {
foundTstats = tstats?.collect { dni ->
//foundTstats = tstats?.each { d1 ->
def d1 = parent.getDevice(dni)
//def d1 = parent.getThermostatDevice(dni)
if(d1) {
if(away) {
d1?.away()
} else {
d1?.present()
}
LogAction("setAway($away): | Thermostat: ${d1?.displayName} setting to $didstr", "debug", false)
storeLastAction("Set ${d1?.displayName} to $didstr", getDtNow(), "nMode", d1)
return d1
} else { LogAction("setaway NO D1", "warn", true) }
}
} else {
if(away) {
parent?.setStructureAway(null, true)
} else {
parent?.setStructureAway(null, false)
}
LogAction("setAway($away): | Setting structure to $didstr", "debug", false)
storeLastAction("Set structure to $didstr", getDtNow(), "nMode")
}
}
boolean nModeScheduleOk() { return autoScheduleOk(nModePrefix()) }
def getnModeActionSec() { return !state?.nModeActionDt ? 100000 : GetTimeDiffSeconds(state?.nModeActionDt, null, "getnModeActionSec").toInteger() }
def checkNestMode() {
LogAction("checkNestMode", "debug", false)
//
// This automation only works with Nest as it toggles non-ST standard home/away
//
String pName = nModePrefix()
try {
if(getIsAutomationDisabled()) { return }
else if(!nModeScheduleOk()) {
LogAction("checkNestMode: Skipping: Schedule Restrictions", "info", false)
} else {
long execTime = now()
state?.autoRunDt = getDtNow()
def curStMode = location?.mode
def allowNotif = settings?."${nModePrefix()}NotifOn" ? true : false
boolean nestModeAway = (getNestLocPres() == "home") ? false : true
String awayPresDesc = (nModePresSensor && !nModeSwitch) ? "All Presence device(s) have left setting " : ""
String homePresDesc = (nModePresSensor && !nModeSwitch) ? "A Presence Device is Now Present setting " : ""
String awaySwitDesc = (nModeSwitch && !nModePresSensor) ? "${nModeSwitch} State is 'Away' setting " : ""
String homeSwitDesc = (nModeSwitch && !nModePresSensor) ? "${nModeSwitch} State is 'Home' setting " : ""
String modeDesc = ((!nModeSwitch && !nModePresSensor) && nModeHomeModes && nModeAwayModes) ? "The ST Mode (${curStMode}) has triggered" : ""
String awayDesc = "${awayPresDesc}${awaySwitDesc}${modeDesc}"
String homeDesc = "${homePresDesc}${homeSwitDesc}${modeDesc}"
boolean away = false
boolean home = false
// ERS figure out what state we are in
if(nModePresSensor && !nModeSwitch) {
if(!isPresenceHome(nModePresSensor)) {
away = true
} else {
home = true
}
} else if(nModeSwitch && !nModePresSensor) {
def swOptAwayOn = (nModeSwitchOpt == "On") ? true : false
if(swOptAwayOn) {
!isSwitchOn(nModeSwitch) ? (home = true) : (away = true)
} else {
!isSwitchOn(nModeSwitch) ? (away = true) : (home = true)
}
} else if(nModeHomeModes && nModeAwayModes) {
if(isInMode(nModeHomeModes)) {
home = true
} else {
if(isInMode(nModeAwayModes)) { away = true }
}
} else {
LogAction("checkNestMode: Nothing Matched", "info", true)
}
// Track changes that happen outside of nMode
// this won't attempt to reset Nest device eco or camera state - you chose to do it outside the automation
Boolean NMisEnabled = parent.automationNestModeEnabled(true)
Boolean NMecoisEnabled = parent.setNModeActive(null)
Boolean t0 = (!nModeSetEco)
Boolean t1 = (home && (!nestModeAway) )
if( (t0 || t1) && NMecoisEnabled) {
LogAction("checkNestMode adjusting manager state NM is not setting eco", "warn", true)
parent.setNModeActive(false) // clear nMode has it in manager
}
if(t1) { state?.nModeTstatLocAway = false }
def t2 = (away && nestModeAway) ? true : false
if(nModeSetEco && t2 && (!NMecoisEnabled)) {
LogAction("checkNestMode adjusting manager state NM will clear eco", "warn", true)
parent.setNModeActive(true) // set nMode has it in manager
}
if(t2) { state?.nModeTstatLocAway = true }
def homeChgd = false
def nestModeChgd = false
if(state?.nModeLastHome != home) {
homeChgd = true;
LogAction("NestMode Home Changed: ${homeChgd} Home: ${home}", "info", false)
state.nModeLastHome = home
}
def t5 = getNestLocPres()
if(state?.nModeLastNestMode != t5) {
nestModeChgd = true;
def t6 = "info"
if(!homeChgd) {
t6 = "warn"
}
LogAction("Nest location mode Changed: ${t5}", t6, true)
state.nModeLastNestMode = t5
}
def didsomething = false
// Manage state changes
if(away && !nestModeAway) {
LogAction("checkNestMode: ${awayDesc} Nest 'Away' ${away} ${nestModeAway}", "info", false)
if(getnModeActionSec() < 4*60) {
LogAction("checkNestMode did change recently - SKIPPING", "warn", true)
scheduleAutomationEval(90)
storeExecutionHistory((now() - execTime), "checkNestMode")
return
}
didsomething = true
setAway(true)
state.nModeLastNestMode = "away"
state?.nModeTstatLocAway = true
if(nModeSetEco) {
parent.setNModeActive(true) // set nMode has it in manager
adjustEco(true, pName)
}
if(allowNotif) {
sendEventPushNotifications("${awayDesc} Nest 'Away'", "Info", pName)
}
if(nModeCamOnAway) { adjustCameras(true, pName) }
} else if(home && nestModeAway) {
LogAction("checkNestMode: ${homeDesc} Nest 'Home' ${home} ${nestModeAway}", "info", false)
if(getnModeActionSec() < 4*60) {
LogAction("checkNestMode did change recently - SKIPPING", "warn", true)
scheduleAutomationEval(90)
storeExecutionHistory((now() - execTime), "checkNestMode")
return
}
didsomething = true
setAway(false)
parent.setNModeActive(false) // clear nMode has it in manager
state.nModeLastNestMode = "home"
state?.nModeTstatLocAway = false
if(nModeSetEco) { adjustEco(false, pName) }
if(allowNotif) {
sendEventPushNotifications("${homeDesc} Nest 'Home'", "Info", pName)
}
if(nModeCamOffHome) { adjustCameras(false, pName) }
}
else {
LogAction("checkNestMode: No Changes | ${nModePresSensor ? "isPresenceHome: ${isPresenceHome(nModePresSensor)} | " : ""}ST-Mode: ($curStMode) | NestModeAway: ($nestModeAway) | Away: ($away) | Home: ($home)", "info", false)
}
if(didsomething) {
state?.nModeActionDt = getDtNow()
scheduleAutomationEval(90)
}
storeExecutionHistory((now() - execTime), "checkNestMode")
}
} catch (ex) {
log.error "checkNestMode Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "checkNestMode", true, getAutoType())
}
}
def getNestLocPres() {
if(getIsAutomationDisabled()) { return }
else {
def plocationPresence = parent?.getLocationPresence()
if(!plocationPresence) { return null }
else {
return plocationPresence
}
}
}
/********************************************************************************
| SCHEDULE, MODE, or MOTION CHANGES ADJUST THERMOSTAT SETPOINTS |
| (AND THERMOSTAT MODE) AUTOMATION CODE |
*********************************************************************************/
def getTstatAutoDevId() {
if(settings?.schMotTstat) { return settings?.schMotTstat.deviceNetworkId.toString() }
return null
}
private tempRangeValues() {
return (getTemperatureScale() == "C") ? "10..32" : "50..90"
}
private timeComparisonOptionValues() {
return ["custom time", "midnight", "sunrise", "noon", "sunset"]
}
private timeDayOfWeekOptions() {
return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
}
private getDayOfWeekName(date = null) {
if (!date) {
date = adjustTime()
}
def theDay = date.day.toInteger()
def list = []
list = timeDayOfWeekOptions()
//LogAction("theDay: $theDay date.date: ${date.day}")
return(list[theDay].toString())
/*
switch (date.day) {
case 0: return "Sunday"
case 1: return "Monday"
case 2: return "Tuesday"
case 3: return "Wednesday"
case 4: return "Thursday"
case 5: return "Friday"
case 6: return "Saturday"
}
*/
return null
}
/*
private getDayOfWeekNumber(date = null) {
if (!date) {
date = adjustTime(now())
}
if (date instanceof Date) {
return date.day
}
switch (date) {
case "Sunday": return 0
case "Monday": return 1
case "Tuesday": return 2
case "Wednesday": return 3
case "Thursday": return 4
case "Friday": return 5
case "Saturday": return 6
}
return null
}
*/
//adjusts the time to local timezone
private adjustTime(time = null) {
if (time instanceof String) {
//get UTC time
time = timeToday(time, location.timeZone).getTime()
}
if (time instanceof Date) {
//get unix time
time = time.getTime()
}
if (!time) {
time = now()
}
if (time) {
return new Date(time + location.timeZone.getOffset(time))
}
return null
}
private formatLocalTime(time, format = "EEE, MMM d yyyy @ h:mm a z") {
if (time instanceof Long) {
time = new Date(time)
}
if (time instanceof String) {
//get UTC time
time = timeToday(time, location.timeZone)
}
if (!(time instanceof Date)) {
return null
}
def formatter = new java.text.SimpleDateFormat(format)
formatter.setTimeZone(location.timeZone)
return formatter.format(time)
}
private convertDateToUnixTime(date) {
if (!date) {
return null
}
if (!(date instanceof Date)) {
date = new Date(date)
}
return date.time - location.timeZone.getOffset(date.time)
}
private convertTimeToUnixTime(time) {
if (!time) {
return null
}
return time - location.timeZone.getOffset(time)
}
private formatTime(time, zone = null) {
//we accept both a Date or a settings' Time
return formatLocalTime(time, "h:mm a${zone ? " z" : ""}")
}
private formatHour(h) {
return (h == 0 ? "midnight" : (h < 12 ? "${h} AM" : (h == 12 ? "noon" : "${h-12} PM"))).toString()
}
private cleanUpMap(map) {
def washer = []
//find dirty laundry
for (item in map) {
if (item.value == null) washer.push(item.key)
}
//clean it
for (item in washer) {
map.remove(item)
}
washer = null
return map
}
private buildDeviceNameList(devices, suffix) {
def cnt = 1
def result = ""
for (device in devices) {
def label = getDeviceLabel(device)
result += "$label" + (cnt < devices.size() ? (cnt == devices.size() - 1 ? " $suffix " : ", ") : "")
cnt++
}
if(result == "") { result = null }
return result
}
private getDeviceLabel(device) {
return device instanceof String ? device : (device ? ( device.label ? device.label : (device.name ? device.name : "$device")) : "Unknown device")
}
def getCurrentSchedule() {
def noSched = false
def mySched
def schedList = getScheduleList()
def res1
int ccnt = 1
for (cnt in schedList) {
res1 = checkRestriction(cnt)
if(res1 == null) { break }
ccnt += 1
}
if(ccnt > schedList?.size()) { noSched = true }
else { mySched = ccnt }
if(mySched != null) {
LogTrace("getCurrentSchedule: mySched: $mySched noSched: $noSched ccnt: $ccnt res1: $res1")
}
return mySched
}
private checkRestriction(cnt) {
//LogTrace("checkRestriction:( $cnt )")
String sLbl = "schMot_${cnt}_"
def restriction
def act = settings["${sLbl}SchedActive"]
if(act) {
def apprestrict = state?."sched${cnt}restrictions"
if (apprestrict?.m && apprestrict?.m.size() && !(location.mode in apprestrict?.m)) {
restriction = "a HE MODE mismatch"
} else if (apprestrict?.w && apprestrict?.w.size() && !(getDayOfWeekName() in apprestrict?.w)) {
restriction = "a day of week mismatch"
} else if (apprestrict?.tf && apprestrict?.tt && !(checkTimeCondition(apprestrict?.tf, apprestrict?.tfc, apprestrict?.tfo, apprestrict?.tt, apprestrict?.ttc, apprestrict?.tto))) {
restriction = "a time of day mismatch"
} else {
if (settings["${sLbl}rstrctSWOn"]) {
for(sw in settings["${sLbl}rstrctSWOn"]) {
if (sw.currentValue("switch") != "on") {
restriction = "switch ${sw} being ${sw.currentValue("switch")}"
break
}
}
}
if (!restriction && settings["${sLbl}rstrctSWOff"]) {
for(sw in settings["${sLbl}rstrctSWOff"]) {
if (sw.currentValue("switch") != "off") {
restriction = "switch ${sw} being ${sw.currentValue("switch")}"
break
}
}
}
if (!restriction && settings["${sLbl}rstrctPHome"] && !isSomebodyHome(settings["${sLbl}rstrctPHome"])) {
for(pr in settings["${sLbl}rstrctPHome"]) {
if (!isPresenceHome(pr)) {
restriction = "presence ${pr} being ${pr.currentValue("presence")}"
break
}
}
}
if (!restriction && settings["${sLbl}rstrctPAway"] && isSomebodyHome(settings["${sLbl}rstrctPAway"])) {
for(pr in settings["${sLbl}rstrctPAway"]) {
if (isPresenceHome(pr)) {
restriction = "presence ${pr} being ${pr.currentValue("presence")}"
break
}
}
}
}
LogTrace("checkRestriction:( $cnt ) restriction: $restriction")
} else {
restriction = "an inactive schedule"
}
return restriction
}
def getActiveScheduleState() {
return state?.activeSchedData ?: null
}
boolean getSchRestrictDoWOk(cnt) {
def apprestrict = state?.activeSchedData
def result = true
apprestrict?.each { sch ->
if(sch?.key.toInteger() == cnt.toInteger()) {
if (!(getDayOfWeekName().toString() in sch?.value?.w)) {
result = false
}
}
}
return result
}
private checkTimeCondition(timeFrom, timeFromCustom, timeFromOffset, timeTo, timeToCustom, timeToOffset) {
def time = adjustTime()
//convert to minutes since midnight
def tc = time.hours * 60 + time.minutes
def tf
def tt
def i = 0
while (i < 2) {
def t = null
def h = null
def m = null
switch(i == 0 ? timeFrom : timeTo) {
case "custom time":
t = adjustTime(i == 0 ? timeFromCustom : timeToCustom)
if (i == 0) {
timeFromOffset = 0
} else {
timeToOffset = 0
}
break
case "sunrise":
t = getSunrise()
break
case "sunset":
t = getSunset()
break
case "noon":
h = 12
break
case "midnight":
h = (i == 0 ? 0 : 24)
break
}
if (h != null) {
m = 0
} else {
h = t.hours
m = t.minutes
}
switch (i) {
case 0:
tf = h * 60 + m + cast(timeFromOffset, "number")
break
case 1:
tt = h * 60 + m + cast(timeFromOffset, "number")
break
}
i += 1
}
//due to offsets, let's make sure all times are within 0-1440 minutes
while (tf < 0) tf += 1440
while (tf > 1440) tf -= 1440
while (tt < 0) tt += 1440
while (tt > 1440) tt -= 1440
if (tf < tt) {
return (tc >= tf) && (tc < tt)
} else {
return (tc < tt) || (tc >= tf)
}
}
private cast(value, dataType) {
def trueStrings = ["1", "on", "open", "locked", "active", "wet", "detected", "present", "occupied", "muted", "sleeping"]
def falseStrings = ["0", "false", "off", "closed", "unlocked", "inactive", "dry", "clear", "not detected", "not present", "not occupied", "unmuted", "not sleeping"]
switch (dataType) {
case "string":
case "text":
if (value instanceof Boolean) {
return value ? "true" : "false"
}
return value ? "$value" : ""
case "number":
if (value == null) return (int) 0
if (value instanceof String) {
if (value.isInteger())
return value.toInteger()
if (value.isFloat())
return (int) Math.floor(value.toFloat())
if (value in trueStrings)
return (int) 1
}
def result = (int) 0
try {
result = (int) value
} catch(all) {
result = (int) 0
}
return result ? result : (int) 0
case "long":
if (value == null) return (long) 0
if (value instanceof String) {
if (value.isInteger())
return (long) value.toInteger()
if (value.isFloat())
return (long) Math.round(value.toFloat())
if (value in trueStrings)
return (long) 1
}
def result = (long) 0
try {
result = (long) value
} catch(all) {
}
return result ? result : (long) 0
case "decimal":
if (value == null) return (float) 0
if (value instanceof String) {
if (value.isFloat())
return (float) value.toFloat()
if (value.isInteger())
return (float) value.toInteger()
if (value in trueStrings)
return (float) 1
}
def result = (float) 0
try {
result = (float) value
} catch(all) {
}
return result ? result : (float) 0
case "boolean":
if (value instanceof String) {
if (!value || (value in falseStrings))
return false
return true
}
return !!value
case "time":
return value instanceof String ? adjustTime(value).time : cast(value, "long")
case "vector3":
return value instanceof String ? adjustTime(value).time : cast(value, "long")
}
return value
}
//TODO is this expensive?
private getSunrise() {
def sunTimes = getSunriseAndSunset()
return adjustTime(sunTimes.sunrise)
}
private getSunset() {
def sunTimes = getSunriseAndSunset()
return adjustTime(sunTimes.sunset)
}
def isTstatSchedConfigured() {
//return (settings?.schMotSetTstatTemp && state?.activeSchedData?.size())
return (state.scheduleActiveCnt)
}
/* //NOT IN USE ANYMORE (Maybe we should keep for future use)
def isTimeBetween(start, end, now, tz) {
def startDt = Date.parse("E MMM dd HH:mm:ss z yyyy", start).getTime()
def endDt = Date.parse("E MMM dd HH:mm:ss z yyyy", end).getTime()
def nowDt = Date.parse("E MMM dd HH:mm:ss z yyyy", now).getTime()
def result = false
if(nowDt > startDt && nowDt < endDt) {
result = true
}
//def result = timeOfDayIsBetween(startDt, endDt, nowDt, tz) ? true : false
return result
}
*/
def checkOnMotion(mySched) {
LogTrace("checkOnMotion($mySched)")
def sLbl = "schMot_${mySched}_"
if(settings["${sLbl}Motion"] && state?."${sLbl}MotionActiveDt") {
def motionOn = isMotionActive(settings["${sLbl}Motion"])
def lastActiveMotionDt = Date.parse("E MMM dd HH:mm:ss z yyyy", state?."${sLbl}MotionActiveDt").getTime()
def lastActiveMotionSec = getMotionActiveSec(mySched)
def lastInactiveMotionDt = 1
def lastInactiveMotionSec
if(state?."${sLbl}MotionInActiveDt") {
lastInactiveMotionDt = Date.parse("E MMM dd HH:mm:ss z yyyy", state?."${sLbl}MotionInActiveDt").getTime()
lastInactiveMotionSec = getMotionInActiveSec(mySched)
}
LogAction("checkOnMotion: [ActiveDt: ${lastActiveMotionDt} (${lastActiveMotionSec} sec) | InActiveDt: ${lastInactiveMotionDt} (${lastInactiveMotionSec} sec) | MotionOn: ($motionOn)", "info", false)
def ontimedelay = (settings."${sLbl}MDelayValOn"?.toInteger() ?: 60) * 1000 // default to 60s
def offtimedelay = (settings."${sLbl}MDelayValOff"?.toInteger() ?: 30*60) * 1000 // default to 30 min
def ontimeNum = lastActiveMotionDt + ontimedelay
def offtimeNum = lastInactiveMotionDt + offtimedelay
def nowDt = now() // Date.parse("E MMM dd HH:mm:ss z yyyy", getDtNow()).getTime()
if(ontimeNum > offtimeNum) { // means motion is on now, so ensure offtime is in future
offtimeNum = nowDt + offtimedelay
}
def lastOnTime // if we are on now, backup ontime to not oscillate
if(state?."motion${mySched}UseMotionSettings" && state?."motion${mySched}TurnedOnDt") {
lastOnTime = Date.parse("E MMM dd HH:mm:ss z yyyy", state?."motion${mySched}TurnedOnDt").getTime()
if(ontimeNum > lastOnTime) {
ontimeNum = lastOnTime - ontimedelay
}
}
def ontime = formatDt( ontimeNum )
def offtime = formatDt( offtimeNum )
LogAction("checkOnMotion: [ActiveDt: (${state."${sLbl}MotionActiveDt"}) | OnTime: ($ontime) | InActiveDt: (${state?."${sLbl}MotionInActiveDt"}) | OffTime: ($offtime)]", "info", false)
def result = false
if(nowDt >= ontimeNum && nowDt <= offtimeNum) {
result = true
}
if(nowDt < ontimeNum || (result && !motionOn)) {
LogAction("checkOnMotion: (Schedule $mySched - ${getSchedLbl(mySched)}) Scheduling Motion Check (60 sec)", "info", false)
scheduleAutomationEval(60)
}
return result
}
return false
}
def setTstatTempCheck() {
LogAction("setTstatTempCheck", "debug", false)
/* NOTE:
// This automation only works with Nest as it checks non-ST presence & thermostat capabilities
// Presumes: That all thermostats in an automation are in the same Nest structure, so that all share home/away settings and tStat modes
*/
try {
if(getIsAutomationDisabled()) { return }
long execTime = now()
def tstat = settings?.schMotTstat
def tstatMir = settings?.schMotTstatMir
String pName = schMotPrefix()
String curMode = tstat ? tstat?.currentThermostatMode.toString() : null
def lastMode = state?.schMotlastMode
boolean samemode = lastMode == curMode ? true : false
def mySched = getCurrentSchedule()
LogAction("setTstatTempCheck | Current Schedule: (${mySched ? ("${mySched} - ${getSchedLbl(mySched)}") : "None Active"})", "debug", false)
boolean noSched = (mySched == null) ? true : false
def previousSched = state?.schedLast
boolean samesched = previousSched == mySched ? true : false
if((!samesched || !samemode) && previousSched) { // schedule change - set old schedule to not use motion
if(state?."motion${previousSched}UseMotionSettings") {
LogAction("setTstatTempCheck: Disabled Motion Settings Used for Previous Schedule (${previousSched} - ${getSchedLbl(previousSched)}", "info", false)
}
state?."motion${previousSched}UseMotionSettings" = false
state?."motion${previousSched}LastisBtwn" = false
}
if(!samesched || !samemode ) { // schedule change, clear out overrides
disableOverrideTemps()
}
LogAction("setTstatTempCheck: [Current Schedule: (${getSchedLbl(mySched)}) | Previous Schedule: (${previousSched} - ${getSchedLbl(previousSched)}) | None: ($noSched)]", "info", false)
if(noSched) {
LogAction("setTstatTempCheck: Skipping check [No matching Schedule]", "info", false)
} else {
def isBtwn = checkOnMotion(mySched)
def previousBtwn = state?."motion${mySched}LastisBtwn"
state?."motion${mySched}LastisBtwn" = isBtwn
if(!isBtwn) {
if(state?."motion${mySched}UseMotionSettings") {
LogAction("setTstatTempCheck: Disabled Use of Motion Settings for Schedule (${mySched} - ${getSchedLbl(mySched)})", "info", false)
}
state?."motion${mySched}UseMotionSettings" = false
}
def sLbl = "schMot_${mySched}_"
def motionOn = isMotionActive(settings["${sLbl}Motion"])
if(!state?."motion${mySched}UseMotionSettings" && isBtwn && !previousBtwn) { // transitioned to use Motion
if(motionOn) { // if motion is on use motion now
state?."motion${mySched}UseMotionSettings" = true
state?."motion${mySched}TurnedOnDt" = getDtNow()
disableOverrideTemps()
LogAction("setTstatTempCheck: Enabled Use of Motion Settings for schedule ${mySched}", "info", false)
} else {
state."${sLbl}MotionActiveDt" = null // this will clear isBtwn
state?."motion${mySched}LastisBtwn" = false
LogAction("setTstatTempCheck: Motion Sensors were NOT Active at Transition Time to Motion ON for Schedule (${mySched} - ${getSchedLbl(mySched)})", "info", false)
}
}
boolean samemotion = previousBtwn == isBtwn ? true : false
boolean schedMatch = (samesched && samemotion) ? true : false
String strv = "Using "
if(schedMatch) { strv = "" }
LogAction("setTstatTempCheck: ${strv}Schedule ${mySched} (${previousSched}) use Motion settings: ${state?."motion${mySched}UseMotionSettings"} | isBtwn: $isBtwn | previousBtwn: $previousBtwn | motionOn $motionOn", "debug", false)
if(tstat && !schedMatch) {
def hvacSettings = state?."sched${mySched}restrictions"
def useMotion = state?."motion${mySched}UseMotionSettings"
def newHvacMode = (!useMotion ? hvacSettings?.hvacm : (hvacSettings?.mhvacm ?: hvacSettings?.hvacm))
if(newHvacMode && (newHvacMode.toString() != curMode)) {
if(newHvacMode == "rtnFromEco") {
if(curMode == "eco") {
def t0 = tstat?.currentpreviousthermostatMode?.toString()
if(t0) {
newHvacMode = t0
}
} else {
newHvacMode = curMode
}
LogAction("setTstatTempCheck: New Mode is rtnFromEco; Setting Thermostat Mode to (${strCapitalize(newHvacMode)})", "info", false)
}
if(newHvacMode && (newHvacMode.toString() != curMode)) {
if(setTstatMode(settings?.schMotTstat, newHvacMode, pName)) {
storeLastAction("Set ${tstat} Mode to ${strCapitalize(newHvacMode)}", getDtNow(), pName, tstat)
LogAction("setTstatTempCheck: Setting ${tstat} Thermostat Mode to (${strCapitalize(newHvacMode)})", "info", false)
} else { LogAction("setTstatTempCheck: Error Setting ${tstat} Thermostat Mode to (${strCapitalize(newHvacMode)})", "warn", true) }
if(tstatMir) {
if(setMultipleTstatMode(tstatMir, newHvacMode, pName)) {
LogAction("Mirroring (${newHvacMode}) to ${tstatMir}", "info", false)
}
}
}
}
curMode = tstat?.currentThermostatMode?.toString()
// if remote sensor is on, let it handle temp changes (above took care of a mode change)
if(settings?.schMotRemoteSensor && isRemSenConfigured()) {
state.schedLast = mySched
state?.schMotlastMode = curMode
storeExecutionHistory((now() - execTime), "setTstatTempCheck")
return
}
boolean isModeOff = (curMode in ["off","eco"]) ? true : false
def tstatHvacMode = curMode
def heatTemp = null
def coolTemp = null
boolean needChg = false
if(!isModeOff && state?.schMotTstatCanHeat) {
def oldHeat = getTstatSetpoint(tstat, "heat")
heatTemp = getRemSenHeatSetTemp(curMode)
if(heatTemp && oldHeat != heatTemp) {
needChg = true
LogAction("setTstatTempCheck: Schedule Heat Setpoint '${heatTemp}${tUnitStr()}' on (${tstat}) | Old Setpoint: '${oldHeat}${tUnitStr()}'", "info", false)
//storeLastAction("Set ${settings?.schMotTstat} Heat Setpoint to ${heatTemp}", getDtNow(), pName, tstat)
} else { heatTemp = null }
}
if(!isModeOff && state?.schMotTstatCanCool) {
def oldCool = getTstatSetpoint(tstat, "cool")
coolTemp = getRemSenCoolSetTemp(curMode)
if(coolTemp && oldCool != coolTemp) {
needChg = true
LogAction("setTstatTempCheck: Schedule Cool Setpoint '${coolTemp}${tUnitStr()}' on (${tstat}) | Old Setpoint: '${oldCool}${tUnitStr()}'", "info", false)
//storeLastAction("Set ${settings?.schMotTstat} Cool Setpoint to ${coolTemp}", getDtNow(), pName, tstat)
} else { coolTemp = null }
}
if(needChg) {
if(setTstatAutoTemps(settings?.schMotTstat, coolTemp?.toDouble(), heatTemp?.toDouble(), pName, tstatMir)) {
//LogAction("setTstatTempCheck: [Temp Change | newHvacMode: $newHvacMode | tstatHvacMode: $tstatHvacMode | heatTemp: $heatTemp | coolTemp: $coolTemp ]", "info", false)
//storeLastAction("Set ${tstat} Cool Setpoint ${coolTemp} Heat Setpoint ${heatTemp}", getDtNow(), pName, tstat)
} else {
LogAction("setTstatTempCheck: Thermostat Set ERROR [ newHvacMode: $newHvacMode | tstatHvacMode: $tstatHvacMode | heatTemp: ${heatTemp}${tUnitStr()} | coolTemp: ${coolTemp}${tUnitStr()} ]", "warn", true)
}
}
}
}
state.schedLast = mySched
state?.schMotlastMode = curMode
storeExecutionHistory((now() - execTime), "setTstatTempCheck")
} catch (ex) {
log.error "setTstatTempCheck Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "setTstatTempCheck", true, getAutoType())
}
}
/********************************************************************************
| MASTER AUTOMATION FOR THERMOSTATS |
*********************************************************************************/
String schMotPrefix() { return "schMot" }
def schMotModePage() {
//Logger("in schmotModePage")
//def pName = schMotPrefix()
dynamicPage(name: "schMotModePage", title: "Thermostat Automation", uninstall: false, install: true) {
def dupTstat
def dupTstat1
def dupTstat2
def dupTstat3
def tStatPhys
//Logger("in schmotModePage0")
def tempScale = getTemperatureScale()
def tempScaleStr = "${tUnitStr()}"
section("Configure Thermostat") {
input name: "schMotTstat", type: "capability.thermostat", title: imgTitle(getAppImg("thermostat_icon.png"), inputTitleStr("Select Thermostat?")), multiple: false, submitOnChange: true, required: true
//log.debug "schMotTstat: ${schMotTstat}"
def tstat = settings?.schMotTstat
def tstatMir = settings?.schMotTstatMir
if(tstat) {
//Logger("in schmotModePage1")
getTstatCapabilities(tstat, schMotPrefix())
//Logger("in schmotModePage2")
def canHeat = state?.schMotTstatCanHeat
def canCool = state?.schMotTstatCanCool
tStatPhys = tstat?.currentNestType != "virtual" ? true : false
//Logger("in schmotModePage3")
String str = ""
def reqSenHeatSetPoint = getRemSenHeatSetTemp()
def reqSenCoolSetPoint = getRemSenCoolSetTemp()
def curZoneTemp = getRemoteSenTemp()
//Logger("in schmotModePage4")
def tempSrcStr = (getCurrentSchedule() && state?.remoteTempSourceStr == "Schedule") ? "Schedule ${getCurrentSchedule()} (${"${getSchedLbl(getCurrentSchedule())}" ?: "Not Found"})" : "(${state?.remoteTempSourceStr})"
//Logger("in schmotModePage5")
str += tempSrcStr ? "Zone Status:\n• Temp Source:${tempSrcStr?.toString().length() > 15 ? "\n └" : ""} ${tempSrcStr}" : ""
str += curZoneTemp ? "\n• Temperature: (${curZoneTemp}${tempScaleStr})" : ""
String hstr = canHeat ? "H: ${reqSenHeatSetPoint}${tempScaleStr}" : ""
String cstr = canHeat && canCool ? "/" : ""
cstr += canCool ? "C: ${reqSenCoolSetPoint}${tempScaleStr}" : ""
str += "\n• Setpoints: (${hstr}${cstr})\n"
str += "\nThermostat Status:\n• Temperature: (${getDeviceTemp(tstat)}${tempScaleStr})"
hstr = canHeat ? "H: ${getTstatSetpoint(tstat, "heat")}${tempScaleStr}" : ""
cstr = canHeat && canCool ? "/" : ""
cstr += canCool ? "C: ${getTstatSetpoint(tstat, "cool")}${tempScaleStr}" : ""
str += "\n• Setpoints: (${hstr}${cstr})"
str += "\n• Mode: (${tstat ? ("${strCapitalize(tstat?.currentThermostatOperatingState)}/${strCapitalize(tstat?.currentThermostatMode)}") : "unknown"})"
str += (state?.schMotTstatHasFan) ? "\n• FanMode: (${strCapitalize(tstat?.currentThermostatFanMode)})" : "\n• No Fan on HVAC system"
str += "\n• Presence: (${strCapitalize(getTstatPresence(tstat))})"
def safetyTemps = getSafetyTemps(tstat)
str += safetyTemps ? "\n• Safety Temps:\n └ Min: ${safetyTemps.min}${tempScaleStr}/Max: ${safetyTemps.max}${tempScaleStr}" : ""
str += "\n• Virtual: (${tstat?.currentNestType.toString() == "virtual" ? "True" : "False"})"
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("${tstat.displayName} Zone Status")), state: (str != "" ? "complete" : null)
paragraph sectionTitleStr("${str}"), state: (str != "" ? "complete" : null)
//Logger("in schmotModePage6")
String t0Str = "ERROR:\nThe "
String t1Str = "Primary Thermostat was found in Mirror Thermostat List.\nPlease Correct to Proceed"
if(!tStatPhys) { // if virtual thermostat, check if physical thermostat is in mirror list
def mylist = [ deviceNetworkId:"${tstat.deviceNetworkId.toString().replaceFirst("v", "")}" ]
dupTstat1 = checkThermostatDupe(mylist, tstatMir)
if(dupTstat1) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("${t0Str}${t1Str}")), required: true, state: null
}
} else { // if physcial thermostat, see if virtual version is in mirror list
def mylist = [ deviceNetworkId:"v${tstat.deviceNetworkId.toString()}" ]
dupTstat2 = checkThermostatDupe(mylist, tstatMir)
if(dupTstat2) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("${t0Str}Virtual version of the ${t1Str}")), required: true, state: null
}
}
dupTstat3 = checkThermostatDupe(tstat, tstatMir) // make sure thermostat is not in mirror list
if(dupTstat3) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("${t0Str}${t1Str}")), required: true, state: null
}
dupTstat = dupTstat1 || dupTstat2 || dupTstat3
if(!tStatPhys) {
}
input "schMotTstatMir", "capability.thermostat", title: imgTitle(getAppImg("thermostat_icon.png"), inputTitleStr("Mirror Changes to these Thermostats")), multiple: true, submitOnChange: true, required: false
if(tstatMir && !dupTstat) {
tstatMir?.each { t ->
paragraph "Thermostat Temp: ${getDeviceTemp(t)}${tempScaleStr}"//, image: " "
}
}
}
}
if(settings?.schMotTstat && !dupTstat) {
updateScheduleStateMap()
section {
paragraph paraTitleStr("Choose Automations:"), required: false
paragraph sectionTitleStr("The options below allow you to configure a thermostat with automations that will help save energy and maintain comfort"), required: false
}
section("Schedule Automation:") {
def actSch = state?.activeSchedData?.size()
def tDesc = (isTstatSchedConfigured() || state?.activeSchedData?.size()) ? "Tap to modify Schedules" : null
href "tstatConfigAutoPage1", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Use Schedules to adjust Temp Setpoints and HVAC mode?")), description: (tDesc != null ? tDesc : ""), state: (tDesc != null ? "complete" : "")//, params: ["configType":"tstatSch"]
if (actSch) {
def schInfo = getScheduleDesc()
def curSch = getCurrentSchedule()
if (schInfo?.size()) {
schInfo?.each { schItem ->
def schNum = schItem?.key
def schDesc = schItem?.value
def schInUse = (curSch?.toInteger() == schNum?.toInteger()) ? true : false
if(schNum && schDesc) {
href "schMotSchedulePage${schNum}", title: "", description: "${schDesc}\n\nTap to modify this Schedule", state: (schInUse ? "complete" : "")//, params: ["sNum":schNum]
}
}
}
}
}
String t3Str = " is not available on a VIRTUAL Thermostat"
String t4Str = "ERROR:\nThe Primary Thermostat is VIRTUAL and UNSUPPORTED for automation.\nPlease Correct to Proceed"
section("Fan Control:") {
if(tStatPhys || settings?.schMotOperateFan) {
String desc = ""
String titStr = ""
if(state?.schMotTstatHasFan) { titStr += "Use HVAC Fan for Circulation\nor\n" }
titStr += "Run External Fan while HVAC is Operating"
input (name: "schMotOperateFan", type: "bool", title: imgTitle(getAppImg("fan_control_icon.png"), inputTitleStr("${titStr}?")), description: desc, required: false, defaultValue: false, submitOnChange: true)
String fanCtrlDescStr = ""
String t0 = getFanSwitchDesc()
if(settings?.schMotOperateFan) {
fanCtrlDescStr += t0 ? "${t0}" : ""
String fanCtrlDesc = isFanCtrlConfigured() ? "${fanCtrlDescStr}" + descriptions("d_ttm") : null
href "tstatConfigAutoPage2", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Fan Control Config")), description: fanCtrlDesc ?: "Not Configured", state: (fanCtrlDesc ? "complete" : null), required: true//, params: ["configType":"fanCtrl"]
}
} else if(!tStatPhys) {
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Fan Control${t3Str}")), state: "complete"
}
if(!tStatPhys && settings?.schMotOperateFan) { paragraph imgTitle(getAppImg("i_err"), paraTitleStr("FAN ${t4Str}")), required: true, state: null }
}
section("Remote Sensor:") {
if(tStatPhys || settings?.schMotRemoteSensor) {
String desc = ""
input (name: "schMotRemoteSensor", type: "bool", title: imgTitle(getAppImg("remote_sensor_icon.png"), inputTitleStr("Use Alternate Temp Sensors to Control Zone temperature?")), description: desc, required: false, defaultValue: false, submitOnChange: true)
if(settings?.schMotRemoteSensor) {
String remSenDescStr = ""
remSenDescStr += settings?.remSenRuleType ? "Rule-Type: ${getEnumValue(remSenRuleEnum("heatcool"), settings?.remSenRuleType)}" : ""
remSenDescStr += settings?.remSenTempDiffDegrees ? ("\n • Threshold: (${settings?.remSenTempDiffDegrees}${tempScaleStr})") : ""
remSenDescStr += settings?.remSenTstatTempChgVal ? ("\n • Adjust Temp: (${settings?.remSenTstatTempChgVal}${tempScaleStr})") : ""
String hstr = remSenHeatTempsReq() ? "H: ${fixTempSetting(settings?.remSenDayHeatTemp) ?: 0}${tempScaleStr}" : ""
String cstr = remSenHeatTempsReq() && remSenCoolTempsReq() ? "/" : ""
cstr += remSenCoolTempsReq() ? "C: ${fixTempSetting(settings?.remSenDayCoolTemp) ?: 0}${tempScaleStr}" : ""
remSenDescStr += (settings?.remSensorDay && (settings?.remSenDayHeatTemp || settings?.remSenDayCoolTemp)) ? "\n • Default Temps:\n └ (${hstr}${cstr})" : ""
remSenDescStr += (settings?.vthermostat) ? "\n\nVirtual Thermostat:" : ""
remSenDescStr += (settings?.vthermostat) ? "\n• Enabled" : ""
//remote sensor/Day
String dayModeDesc = ""
dayModeDesc += settings?.remSensorDay ? "\n\nDefault Sensor${settings?.remSensorDay?.size() > 1 ? "s" : ""}:" : ""
def rCnt = settings?.remSensorDay?.size()
settings?.remSensorDay?.each { t ->
dayModeDesc += "\n ├ ${t?.label}: ${(t?.label?.toString()?.length() > 10) ? "\n │ └ " : ""}(${getDeviceTemp(t)}${tempScaleStr})"
}
dayModeDesc += settings?.remSensorDay ? "\n └ Temp${(settings?.remSensorDay?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings?.remSensorDay)}${tempScaleStr})" : ""
remSenDescStr += settings?.remSensorDay ? "${dayModeDesc}" : ""
String remSenDesc = isRemSenConfigured() ? "${remSenDescStr}" + descriptions("d_ttm") : null
href "tstatConfigAutoPage3", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Remote Sensor Config")), description: remSenDesc ?: "Not Configured", required: true, state: (remSenDesc ? "complete" : null)//, params: ["configType":"remSen"]
} else {
if(settings?.vthermostat != null) {
//ERS
settingRemove("vthermostat")
removeVstat("automation Selection")
}
}
} else if(!tStatPhys) { paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Remote Sensor${t3Str}")), state: "complete" }
if(!tStatPhys && settings?.schMotRemoteSensor) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Remote Sensor ${t4Str}")), required: true, state: null
}
}
section("Leak Detection:") {
if(tStatPhys || settings?.schMotWaterOff) {
String desc = ""
input (name: "schMotWaterOff", type: "bool", title: imgTitle(getAppImg("leak_icon.png"), inputTitleStr("Turn Off if Water Leak is detected?")), description: desc, required: false, defaultValue: false, submitOnChange: true)
if(settings?.schMotWaterOff) {
String leakDesc = ""
String t0 = leakWatSensorsDesc()
leakDesc += (settings?.leakWatSensors && t0) ? "${t0}" : ""
leakDesc += settings?.leakWatSensors ? "\n\n${autoStateDesc("leakWat")}" : ""
leakDesc += (settings?.leakWatSensors) ? "\n\nSettings:" : ""
leakDesc += settings?.leakWatOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings?.leakWatOnDelay)})" : ""
//leakDesc += (settings?.leakWatModes || settings?.leakWatDays || (settings?.leakWatStartTime && settings?.leakWatStopTime)) ?
//"\n • Restrictions Active: (${autoScheduleOk(leakWatPrefix()) ? "NO" : "YES"})" : ""
String t1 = getNotifConfigDesc(leakWatPrefix())
leakDesc += t1 ? "\n\n${t1}" : ""
leakDesc += (settings?.leakWatSensors) ? descriptions("d_ttm") : ""
String leakWatDesc = isLeakWatConfigured() ? "${leakDesc}" : null
href "tstatConfigAutoPage4", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Leak Sensor Automation")), description: leakWatDesc ?: descriptions("d_ttc"), required: true, state: (leakWatDesc ? "complete" : null)//, params: ["configType":"leakWat"]
}
} else if(!tStatPhys) {
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Leak Detection${t3Str}")), state: "complete"
}
if(!tStatPhys && settings?.schMotWaterOff) { paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Leak ${t4Str}")), required: true, state: null }
}
section("Contact Automation:") {
if(tStatPhys || settings?.schMotContactOff) {
String desc = ""
input (name: "schMotContactOff", type: "bool", title: imgTitle(getAppImg("open_window.png"), inputTitleStr("Set ECO if Door/Window Contact Open?")), description: desc, required: false, defaultValue: false, submitOnChange: true)
if(settings?.schMotContactOff) {
String conDesc = ""
String t0 = conWatContactDesc()
conDesc += (settings?.conWatContacts && t0) ? "${t0}" : ""
conDesc += settings?.conWatContacts ? "\n\n${autoStateDesc("conWat")}" : ""
conDesc += settings?.conWatContacts ? "\n\nSettings:" : ""
conDesc += settings?.conWatOffDelay ? "\n • Eco Delay: (${getEnumValue(longTimeSecEnum(), settings?.conWatOffDelay)})" : ""
conDesc += settings?.conWatOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings?.conWatOnDelay)})" : ""
conDesc += settings?.conWatRestoreDelayBetween ? "\n • Delay Between Restores:\n └ (${getEnumValue(longTimeSecEnum(), settings?.conWatRestoreDelayBetween)})" : ""
conDesc += (settings?.conWatContacts) ? "\n • Restrictions Active: (${autoScheduleOk(conWatPrefix()) ? "NO" : "YES"})" : ""
String t1 = getNotifConfigDesc(conWatPrefix())
conDesc += t1 ? "\n\n${t1}" : ""
conDesc += (settings?.conWatContacts) ? descriptions("d_ttm") : ""
String conWatDesc = isConWatConfigured() ? "${conDesc}" : null
href "tstatConfigAutoPage5", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Contact Sensors Config")), description: conWatDesc ?: descriptions("d_ttc"), required: true, state: (conWatDesc ? "complete" : null)//, params: ["configType":"conWat"]
}
} else if(!tStatPhys) {
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("Contact automation${t3Str}")), state: "complete"
}
if(!tStatPhys && settings?.schMotContactOff) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Contact ${t4Str}")), required: true, state: null
}
}
section("Humidity Control:") {
String desc = ""
input (name: "schMotHumidityControl", type: "bool", title: imgTitle(getAppImg("humidity_automation_icon.png"), inputTitleStr("Turn Humidifier On / Off?")), description: desc, required: false, defaultValue: false, submitOnChange: true)
if(settings?.schMotHumidityControl) {
String humDesc = ""
humDesc += (settings?.humCtrlSwitches) ? "${humCtrlSwitchDesc()}" : ""
humDesc += (settings?.humCtrlHumidity) ? "${settings?.humCtrlSwitches ? "\n\n" : ""}${humCtrlHumidityDesc()}" : ""
humDesc += (settings?.humCtrlUseWeather || settings?.humCtrlTempSensor) ? "\n\nSettings:" : ""
humDesc += (!settings?.humCtrlUseWeather && settings?.humCtrlTempSensor) ? "\n • Temp Sensor: (${getHumCtrlTemperature()}${tempScaleStr})" : ""
humDesc += (settings?.humCtrlUseWeather && !settings?.humCtrlTempSensor) ? "\n • Weather: (${getHumCtrlTemperature()}${tempScaleStr})" : ""
humDesc += (settings?.humCtrlSwitches) ? "\n • Restrictions Active: (${autoScheduleOk(humCtrlPrefix()) ? "NO" : "YES"})" : ""
//TODO need this in schedule
humDesc += ((settings?.humCtrlTempSensor || settings?.humCtrlUseWeather) ) ? descriptions("d_ttm") : ""
String humCtrlDesc = isHumCtrlConfigured() ? "${humDesc}" : null
href "tstatConfigAutoPage6", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("Humidifer Config")), description: humCtrlDesc ?: descriptions("d_ttc"), required: true, state: (humCtrlDesc ? "complete" : null)//, params: ["configType":"humCtrl"]
}
}
section("External Temp:") {
if(tStatPhys || settings?.schMotExternalTempOff) {
String desc = ""
input (name: "schMotExternalTempOff", type: "bool", title: imgTitle(getAppImg("external_temp_icon.png"), inputTitleStr("Set ECO if External Temp is near comfort settings?")), description: desc, required: false, defaultValue: false, submitOnChange: true)
if(settings?.schMotExternalTempOff) {
String extDesc = ""
extDesc += (settings?.extTmpUseWeather || settings?.extTmpTempSensor) ? "${autoStateDesc("extTmp")}\n\n" : ""
extDesc += (settings?.extTmpUseWeather || settings?.extTmpTempSensor) ? "Settings:" : ""
extDesc += (!settings?.extTmpUseWeather && settings?.extTmpTempSensor) ? "\n • Sensor: (${getExtTmpTemperature()}${tempScaleStr})" : ""
extDesc += (settings?.extTmpUseWeather && !settings?.extTmpTempSensor) ? "\n • Weather: (${getExtTmpTemperature()}${tempScaleStr})" : ""
//TODO need this in schedule
extDesc += settings?.extTmpDiffVal ? "\n • Outside Threshold: (${settings?.extTmpDiffVal}${tempScaleStr})" : ""
extDesc += settings?.extTmpInsideDiffVal ? "\n • Inside Threshold: (${settings?.extTmpInsideDiffVal}${tempScaleStr})" : ""
extDesc += settings?.extTmpOffDelay ? "\n • ECO Delay: (${getEnumValue(longTimeSecEnum(), settings?.extTmpOffDelay)})" : ""
extDesc += settings?.extTmpOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings?.extTmpOnDelay)})" : ""
extDesc += (settings?.extTmpTempSensor || settings?.extTmpUseWeather) ? "\n • Restrictions Active: (${autoScheduleOk(extTmpPrefix()) ? "NO" : "YES"})" : ""
String t0 = getNotifConfigDesc(extTmpPrefix())
extDesc += t0 ? "\n\n${t0}" : ""
extDesc += ((settings?.extTmpTempSensor || settings?.extTmpUseWeather) ) ? descriptions("d_ttm") : ""
String extTmpDesc = isExtTmpConfigured() ? "${extDesc}" : null
href "tstatConfigAutoPage7", title: imgTitle(getAppImg("i_cfg"), inputTitleStr("External Temps Config")), description: extTmpDesc ?: descriptions("d_ttc"), required: true, state: (extTmpDesc ? "complete" : null)//, params: ["configType":"extTmp"]
}
} else if(!tStatPhys) {
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("External Temp Automation${t3Str}")), state: "complete"
}
if(!tStatPhys && settings?.schMotExternalTempOff) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("External Temp ${t4Str}")), required: true, state: null
}
}
section("Settings:") {
input "schMotWaitVal", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Minimum Wait Time between Evaluations?")), required: false, defaultValue: 60, options: [30:"30 Seconds", 60:"60 Seconds",90:"90 Seconds",120:"120 Seconds"]
}
}
/*
if(state?.showHelp) {
section("Help:") {
href url:"${getAutoHelpPageUrl()}", style:"embedded", required:false, title:"Help and Instructions", description:"", image: getAppImg("info.png")
}
}
*/
}
}
String getSchedLbl(num) {
String result = ""
if(num) {
def schData = state?.activeSchedData
schData?.each { sch ->
if(num?.toInteger() == sch?.key.toInteger()) {
//log.debug "Label:(${sch?.value?.lbl})"
result = sch?.value?.lbl
}
}
}
return result
}
def getSchedData(num) {
if(!num) { return null }
def resData = [:]
def schData = state?.activeSchedData
schData?.each { sch ->
//log.debug "sch: $sch"
if(sch?.key != null && num?.toInteger() == sch?.key.toInteger()) {
// log.debug "Data:(${sch?.value})"
resData = sch?.value
}
}
return resData != [:] ? resData : null
}
/* NOTE
Schedule Rules:
You ALWAYS HAVE TEMPS in A SCHEDULE
• You ALWAYS OFFER OPTION OF MOTION TEMPS in A SCHEDULE
• If Motion is ENABLED, it MUST HAVE MOTION TEMPS
• You ALWAYS OFFER RESTRICTION OPTIONS in A SCHEDULE
• If REMSEN is ON, you offer remote sensors options
*/
def tstatConfigAutoPage1(params) { def t0 = [:]; t0.configType = "tstatSch"; return tstatConfigAutoPage( t0 ) }
def tstatConfigAutoPage2(params) { def t0 = [:]; t0.configType = "fanCtrl"; return tstatConfigAutoPage( t0 ) }
def tstatConfigAutoPage3(params) { def t0 = [:]; t0.configType = "remSen"; return tstatConfigAutoPage( t0 ) }
def tstatConfigAutoPage4(params) { def t0 = [:]; t0.configType = "leakWat"; return tstatConfigAutoPage( t0 ) }
def tstatConfigAutoPage5(params) { def t0 = [:]; t0.configType = "conWat"; return tstatConfigAutoPage( t0 ) }
def tstatConfigAutoPage6(params) { def t0 = [:]; t0.configType = "humCtrl"; return tstatConfigAutoPage( t0 ) }
def tstatConfigAutoPage7(params) { def t0 = [:]; t0.configType = "extTmp"; return tstatConfigAutoPage( t0 ) }
def tstatConfigAutoPage(params) {
def configType = params?.configType
if(params?.configType) {
//Logger("tstatConfigAutoPage got params")
state.tempTstatConfigPageData = params; configType = params?.configType;
} else { configType = state?.tempTstatConfigPageData?.configType }
String pName = ""
String pTitle = ""
String pDesc = null
switch(configType) {
case "tstatSch":
pName = schMotPrefix()
pTitle = "Thermostat Schedule Automation"
pDesc = "Configure Schedules and Setpoints"
break
case "fanCtrl":
pName = fanCtrlPrefix()
pTitle = "Fan Automation"
break
case "remSen":
pName = remSenPrefix()
pTitle = "Remote Sensor Automation"
break
case "leakWat":
pName = leakWatPrefix()
pTitle = "Thermostat/Leak Automation"
break
case "conWat":
pName = conWatPrefix()
pTitle = "Thermostat/Contact Automation"
break
case "humCtrl":
pName = humCtrlPrefix()
pTitle = "Humidifier Automation"
break
case "extTmp":
pName = extTmpPrefix()
pTitle = "Thermostat/External Temps Automation"
break
}
dynamicPage(name: "tstatConfigAutoPage", title: pTitle, description: pDesc, uninstall: false) {
def tstat = settings?.schMotTstat
if (tstat) {
def tempScale = getTemperatureScale()
String tempScaleStr = "${tUnitStr()}"
String tStatName = tstat?.displayName.toString()
def tStatHeatSp = getTstatSetpoint(tstat, "heat")
def tStatCoolSp = getTstatSetpoint(tstat, "cool")
String tStatMode = tstat?.currentThermostatMode
String tStatTemp = "${getDeviceTemp(tstat)}${tempScaleStr}"
boolean canHeat = state?.schMotTstatCanHeat
boolean canCool = state?.schMotTstatCanCool
def locMode = location?.mode
def hidestr = null
hidestr = ["fanCtrl"] // fan schedule is turned off
if(!settings?.schMotRemoteSensor) { // no remote sensors requested or used
hidestr = ["fanCtrl", "remSen"]
}
def params1 = ["sData":["hideStr":"${hidestr}"]]
state.t_tempSData = params1
if(!settings?.schMotOperateFan) {
}
//if(!settings?.schMotSetTstatTemp) { //motSen means no motion sensors offered restrict means no restrictions offered tstatTemp says no tstat temps offered
//"tstatTemp", "motSen" "restrict"
//}
if(!settings?.schMotExternalTempOff) {
}
if(configType == "tstatSch") {
section {
String str = ""
str += "• Temperature: (${tStatTemp})"
str += "\n• Setpoints: (H: ${canHeat ? "${tStatHeatSp}${tempScaleStr}" : "NA"} / C: ${canCool ? "${tStatCoolSp}${tempScaleStr}" : "NA"})"
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("${tStatName}\nSchedules and Setpoints:")), state: "complete"
paragraph sectionTitleStr("${str}"), state: "complete"
}
showUpdateSchedule(null, hidestr)
}
if(configType == "fanCtrl") {
def reqinp = !(settings["schMotCirculateTstatFan"] || settings["${pName}FanSwitches"])
section("Control Fans/Switches based on Thermostat\n(3-Speed Fans Supported)") {
input "${pName}FanSwitches", "capability.switch", title: imgTitle(getAppImg("fan_ventilation_icon.png"), inputTitleStr("Select Fan Switches?")), required: reqinp, submitOnChange: true, multiple: true
if(settings?."${pName}FanSwitches") {
String t0 = getFanSwitchDesc(false)
paragraph paraTitleStr("${t0}"), state: t0 ? "complete" : null
}
}
if(settings["${pName}FanSwitches"]) {
section("Fan Event Triggers") {
paragraph "Triggers are evaluated when Thermostat sends an operating event. Poll time may take 1 minute or more for fan to switch on.",
title: "What are these triggers?", image: getAppImg("i_inst")
input "${pName}FanSwitchTriggerType", "enum", title: imgTitle(getAppImg("${settings?."${pName}FanSwitchTriggerType" == 1 ? "thermostat" : "home_fan"}_icon.png"), inputTitleStr("Control Switches When?")), defaultValue: 1, options: switchRunEnum(), submitOnChange: true
input "${pName}FanSwitchHvacModeFilter", "enum", title: imgTitle(getAppImg("i_mod"), inputTitleStr("Thermostat Mode Triggers?")), defaultValue: "any", options: fanModeTrigEnum(), submitOnChange: true, multiple: true
}
if(getFanSwitchesSpdChk()) {
section("Fan Speed Options") {
input "${pName}FanSwitchSpeedCtrl", "bool", title: imgTitle(getAppImg("speed_knob_icon.png"), inputTitleStr("Enable Speed Control?")), defaultValue: true, submitOnChange: true
if(settings["${pName}FanSwitchSpeedCtrl"]) {
paragraph paraTitleStr("What do these values mean?")
paragraph sectionTitleStr("These threshold settings allow you to configure the speed of the fan based on it's closeness to the desired temp")
input "${pName}FanSwitchLowSpeed", "decimal", title: imgTitle(getAppImg("fan_low_speed.png"), inputTitleStr( "Low Speed Threshold (${tempScaleStr})")), required: true, defaultValue: 1.0, submitOnChange: true
input "${pName}FanSwitchMedSpeed", "decimal", title: imgTitle(getAppImg("fan_med_speed.png"), inputTitleStr("Medium Speed Threshold (${tempScaleStr})")), required: true, defaultValue: 2.0, submitOnChange: true
input "${pName}FanSwitchHighSpeed", "decimal", title: imgTitle(getAppImg("fan_high_speed.png"), inputTitleStr("High Speed Threshold (${tempScaleStr})")), required: true, defaultValue: 4.0, submitOnChange: true
}
}
}
}
if(state?.schMotTstatHasFan || settings["${pName}FanSwitches"]) { // ERS allow for external fans also?
section("Fan Circulation:") {
String desc = ""
if(state?.schMotTstatHasFan) {
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)
}
if(settings["${pName}FanSwitches"]) {
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)
} else {
settingRemove("schMotCirculateExtFan")
}
if(settings?.schMotCirculateTstatFan || settings?.schMotCirculateExtFan) {
input("schMotFanRuleType", "enum", title: imgTitle(getAppImg("rule_icon.png"), inputTitleStr("(Rule) Action Type")), options: remSenRuleEnum("fan"), required: true)
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What is the Action Threshold Temp?"))
paragraph sectionTitleStr("Temp difference to trigger Action Type.")
def adjust = (getTemperatureScale() == "C") ? 0.5 : 1.0
input "fanCtrlTempDiffDegrees", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Action Threshold Temp (${tempScaleStr})")), required: true, defaultValue: adjust
input name: "fanCtrlOnTime", type: "enum", title: imgTitle(getAppImg("timer_icon.png"), inputTitleStr("Minimum circulate Time\n(Optional)")), defaultValue: 240, options: fanTimeSecEnum(), required: true, submitOnChange: true
input "fanCtrlTimeBetweenRuns", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Between On/Off Cycles\n(Optional)")), required: true, defaultValue: 1200, options: longTimeSecEnum(), submitOnChange: true
}
}
}
section(getDmtSectionDesc(fanCtrlPrefix())) {
String pageDesc = getDayModeTimeDesc(pName)
href "setDayModeTimePage2", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName": "${pName}"]
}
if(settings?."${pName}FanSwitches") {
String schTitle
if(!state?.activeSchedData?.size()) {
schTitle = "Optionally create schedules to set temperatures based on schedule"
} else {
schTitle = "Temperature settings based on schedule"
}
section("${schTitle}") { // FANS USE TEMPS IN LOGIC
href "scheduleConfigPage", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Enable/Modify Schedules")), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["sData":["hideStr":"${hidestr}"]]
}
}
}
boolean cannotLock
double defHeat
double defCool
double curTemp
if(!getMyLockId()) {
setMyLockId(app.id)
}
//ERS
// this deals with changing the tstat on the automation
if(state?.remSenTstat) {
if(tstat.deviceNetworkId != state?.remSenTstat) {
removeVstat("settings pages")
}
}
if(settings?.schMotRemoteSensor) {
if( parent?.remSenLock(tstat?.deviceNetworkId, getMyLockId()) ) { // lock new ID
state?.remSenTstat = tstat?.deviceNetworkId
cannotLock = false
} else { cannotLock = true }
}
if(configType == "remSen") {
if(cannotLock) {
section("") {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Cannot Lock thermostat for remote sensor - thermostat may already be in use. Please Correct")), required: true, state: null
}
settingRemove("vthermostat")
}
if(!cannotLock) {
section("Select the Allowed (Rule) Action Type:") {
if(!settings?.remSenRuleType) {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What are Rule Actions?"))
paragraph sectionTitleStr("They determine the actions taken when the temperature threshold is reached, to balance temperatures")
}
input(name: "remSenRuleType", type: "enum", title: imgTitle(getAppImg("rule_icon.png"), inputTitleStr("(Rule) Action Type")), options: remSenRuleEnum("heatcool"), required: true, submitOnChange: true)
}
if(settings?.remSenRuleType) {
String senLblStr = "Default"
section("Choose Temperature Sensor(s) to use:") {
def daySenReq = (!settings?.remSensorDay) ? true : false
input "remSensorDay", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("${senLblStr} Temp Sensor(s)")), submitOnChange: true, required: daySenReq, multiple: true
if(settings?.remSensorDay) {
curTemp = getDeviceTempAvg(settings?.remSensorDay)
String tmpVal = "Temp${(settings?.remSensorDay?.size() > 1) ? " (avg):" : ":"} (${curTemp}${tempScaleStr})"
if(settings?.remSensorDay.size() > 1) {
href "remSenShowTempsPage", title: inputTitleStr("View ${senLblStr} Sensor Temps"), description: "${tmpVal}", state: "complete"
paragraph imgTitle(getAppImg("i_icon.png"), paraTitleStr("Multiple temp sensors will return the average of those sensors."))
} else {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Remote Sensor Temp")), state: "complete"
paragraph sectionTitleStr("${tmpVal}"), state: "complete"
}
}
}
if(settings?.remSensorDay) {
section("Desired Setpoints") {
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("What are these temps for?"))
paragraph sectionTitleStr("These temps are used when remote sensors are enabled and no schedules are created or active")
if(isTstatSchedConfigured()) {
// if(settings?.schMotSetTstatTemp) {
paragraph "If schedules are enabled and that schedule is in use it's setpoints will take precendence over the setpoints below", required: true, state: null
}
String tempStr = "Default "
if(remSenHeatTempsReq()) {
defHeat = fixTempSetting(getGlobalDesiredHeatTemp())
defHeat = defHeat ?: (tStatHeatSp ?: curTemp-1)
input "remSenDayHeatTemp", "decimal", title: imgTitle(getAppImg("heat_icon.png"), inputTitleStr("Desired ${tempStr}Heat Temp (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", range: tempRangeValues(), required: true, defaultValue: defHeat
}
if(remSenCoolTempsReq()) {
defCool = fixTempSetting(getGlobalDesiredCoolTemp())
defCool = defCool ?: (tStatCoolSp ?: curTemp+1)
input "remSenDayCoolTemp", "decimal", title: imgTitle(getAppImg("cool_icon.png"), inputTitleStr("Desired ${tempStr}Cool Temp (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", range: tempRangeValues(), required: true, defaultValue: defCool
}
}
section("Remote Sensor Settings") {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What is the Action Threshold Temp?"))
paragraph sectionTitleStr("Temp difference to trigger Actions.")
input "remSenTempDiffDegrees", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Action Threshold Temp (${tempScaleStr})")), required: true, defaultValue: 2.0
if(settings?.remSenRuleType != "Circ") {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("What are Temp Increments?"))
paragraph sectionTitleStr("Is the amount the thermostat temp is adjusted +/- to turn on/off the HVAC system.")
input "remSenTstatTempChgVal", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Change Temp Increments (${tempScaleStr})")), required: true, defaultValue: 5.0
}
}
section("(Optional) Create a Virtual Nest Thermostat:") {
input(name: "vthermostat", type: "bool", title: imgTitle(getAppImg("thermostat_icon.png"), inputTitleStr("Create Virtual Nest Thermostat")), required: false, submitOnChange: true)
if(settings?.vthermostat != null && !parent?.addRemoveVthermostat(tstat.deviceNetworkId, vthermostat, getMyLockId())) {
paragraph imgTitle(getAppImg("i_err"), paraTitleStr("Unable to ${(vthermostat ? "enable" : "disable")} Virtual Thermostat!. Please Correct"))
}
}
String schTitle
if(!state?.activeSchedData?.size()) {
schTitle = "Optionally create schedules to set temperatures, alternate sensors based on schedule"
} else {
schTitle = "Temperature settings and optionally alternate sensors based on schedule"
}
section("${schTitle}") {
href "scheduleConfigPage", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Enable/Modify Schedules")), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["sData":["hideStr":"${hidestr}"]]
}
}
}
}
}
if(configType == "leakWat") {
section("When Leak is Detected, Turn Off this Thermostat") {
def req = (settings?.leakWatSensors || settings?.schMotTstat) ? true : false
input name: "leakWatSensors", type: "capability.waterSensor", title: imgTitle(getAppImg("water_icon.png"), inputTitleStr("Which Leak Sensor(s)?")), multiple: true, submitOnChange: true, required: req
if(settings?.leakWatSensors) {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${leakWatSensorsDesc()}")), state: "complete"
}
}
if(settings?.leakWatSensors) {
section("Restore On when Dry:") {
input "${pName}OnDelay", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dr"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true // Delay Restore
}
section(sectionTitleStr(titles("t_nt"))) {
String t0 = getNotifConfigDesc(pName)
def pageDesc = t0 ? "${t0}" + descriptions("d_ttm") : ""
href "setNotificationPage3", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
}
}
}
if(configType == "conWat") {
section("When these Contacts are open, Set this Thermostat to ECO") {
boolean req = !settings?.conWatContacts ? true : false
input name: "conWatContacts", type: "capability.contactSensor", title: imgTitle(getAppImg("contact_icon.png"), inputTitleStr("Which Contact(s)?")), multiple: true, submitOnChange: true, required: req
if(settings?.conWatContacts) {
String str = ""
str += settings?.conWatContacts ? "${conWatContactDesc()}\n" : ""
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != "" ? "complete" : null)
}
}
if(settings?.conWatContacts) {
section("Delay Values:") {
input "${pName}OffDelay", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dtse"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true // Delay to set ECO
input "${pName}OnDelay", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dr"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true
input "conWatRestoreDelayBetween", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Between On/Eco Cycles\n(Optional)")), required: false, defaultValue: 600, options: longTimeSecEnum(), submitOnChange: true
}
section("Restoration Preferences:") {
input "${pName}OffTimeout", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Auto Restore after (Optional)")), defaultValue: 0, options: longTimeSecEnum(), required: false, submitOnChange: true
if(!settings?."${pName}OffTimeout") { state."${pName}TimeoutScheduled" = false }
}
section(getDmtSectionDesc(conWatPrefix())) {
String pageDesc = getDayModeTimeDesc(pName)
href "setDayModeTimePage3", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName": "${pName}"]
}
section(sectionTitleStr(titles("t_nt"))) {
String t0 = getNotifConfigDesc(pName)
String pageDesc = t0 ? "${t0}" + descriptions("d_ttm") : ""
href "setNotificationPage4", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
}
}
}
if(configType == "humCtrl") {
section("Switch for Humidifier") {
def reqinp = !(settings?.humCtrlSwitches)
// TODO needs new icon
input "humCtrlSwitches", "capability.switch", title: imgTitle(getAppImg("fan_ventilation_icon.png"), inputTitleStr("Select Switches?")), required: reqinp, submitOnChange: true, multiple: true
/*
this does not work...
*/
def t00 = settings[humCtrlSwitches].collect { it?.id }
def t1 = state?.oldhumCtrlSwitches
def t2 = t1.collect { it?.id }
if(t2 != t00) {
state.haveRunHumidifier = false
if(settings?.humCtrlSwitches) { humCtrlSwitches*.off() }
if(t1) { t1*.off() }
state?.oldhumCtrlSwitches = settings?.humCtrlSwitches
log.warn "found different oldhum vs. humctrlSwitches"
}
if(settings?.humCtrlSwitches) {
String t0 = humCtrlSwitchDesc(false)
paragraph paraTitleStr("${t0}"), state: t0 ? "complete" : null
}
}
if(settings?.humCtrlSwitches) {
section("Humidifier Triggers") {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr( "What are these triggers?"))
paragraph sectionTitleStr("Triggers are evaluated when Thermostat sends an operating event. Poll time may take 1 minute or more for fan to switch on.")
// TODO needs to fix icon
input "humCtrlSwitchTriggerType", "enum", title: imgTitle(getAppImg("${settings?.humCtrlSwitchTriggerType == 1 ? "thermostat" : "home_fan"}_icon.png"), inputTitleStr("Control Switches When?")), defaultValue: "5", options: switchRunEnum(true), submitOnChange: true
input "humCtrlSwitchHvacModeFilter", "enum", title: imgTitle(getAppImg("i_mod"), inputTitleStr("Thermostat Mode Triggers?")), defaultValue: "any", options: fanModeTrigEnum(),
submitOnChange: true, multiple: true
}
section("Indoor Humidity Measurement") {
boolean req = !settings?.humCtrlHumidity ? true : false
input name: "humCtrlHumidity", type: "capability.relativeHumidityMeasurement", title: imgTitle(getAppImg("humidity_icon.png"), inputTitleStr("Which Humidity Sensor(s)?")), multiple: true, submitOnChange: true, required: req
if(settings?.humCtrlHumidity) {
String str = ""
str += settings?.humCtrlHumidity ? "${humCtrlHumidityDesc()}\n" : ""
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != "" ? "complete" : null)
}
}
section("Select the External Temp Sensor to Use:") {
if(!parent.getSettingVal("weatherDevice")) {
paragraph "Please Enable the Weather Device under the Manager App before trying to use External Weather as the External Temperature Sensor!", required: true, state: null
} else {
if(!settings?.humCtrlTempSensor) {
input "humCtrlUseWeather", "bool", title: imgTitle(getAppImg("weather_icon.png"), inputTitleStr("Use Local Weather as External Sensor?")), required: false, defaultValue: false, submitOnChange: true
//state.needWeathUpd = true
if(settings?.humCtrlUseWeather) {
if(state?.curWeather == null) {
getExtConditions()
}
def tmpVal = (tempScale == "C") ? state?.curWeaTemp_c : state?.curWeaTemp_f
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Local Weather:\n• ${state?.curWeatherLoc}\n• Temp: (${tmpVal}${tempScaleStr})")), state: "complete"
}
}
}
if(!settings?.humCtrlUseWeather) {
state.curWeather = null // force refresh of weather if toggled
def senReq = (!settings?.humCtrlUseWeather && !settings?.humCtrlTempSensor) ? true : false
input "humCtrlTempSensor", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("Select a Temp Sensor?")), submitOnChange: true, multiple: false, required: senReq
if(settings?.humCtrlTempSensor) {
def str = ""
str += settings?.humCtrlTempSensor ? "Sensor Status:" : ""
str += settings?.humCtrlTempSensor ? "\n└ Temp: (${settings?.humCtrlTempSensor?.currentTemperature}${tempScaleStr})" : ""
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != "" ? "complete" : null)
}
}
}
section(getDmtSectionDesc(humCtrlPrefix())) {
def pageDesc = getDayModeTimeDesc(pName)
href "setDayModeTimePage4", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName": "${pName}"]
}
}
}
if(configType == "extTmp") {
section("Select the External Temps to Use:") {
if(!parent.getSettingVal("weatherDevice")) {
paragraph "Please Enable the Weather Device under the Manager App before trying to use External Weather as an External Sensor!", required: true, state: null
} else {
if(!settings?.extTmpTempSensor) {
input "extTmpUseWeather", "bool", title: imgTitle(getAppImg("weather_icon.png"), inputTitleStr("Use Local Weather as External Sensor?")), required: false, defaultValue: false, submitOnChange: true
//state.needWeathUpd = true
if(settings?.extTmpUseWeather) {
if(state?.curWeather == null) {
getExtConditions()
}
def tmpVal = (tempScale == "C") ? state?.curWeaTemp_c : state?.curWeaTemp_f
def curDp = getExtTmpDewPoint()
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Local Weather:")), state: "complete"
paragraph sectionTitleStr("• ${state?.curWeatherLoc}\n• Temp: (${tmpVal}${tempScaleStr})\n• Dewpoint: (${curDp}${tempScaleStr})"), state: "complete"
}
}
}
if(!settings?.extTmpUseWeather) {
state.curWeather = null // force refresh of weather if toggled
def senReq = (!settings?.extTmpUseWeather && !settings?.extTmpTempSensor) ? true : false
input "extTmpTempSensor", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("Select a Temp Sensor?")), submitOnChange: true, multiple: false, required: senReq
if(settings?.extTmpTempSensor) {
def str = ""
str += settings?.extTmpTempSensor ? "Sensor Status:" : ""
str += settings?.extTmpTempSensor ? "\n└ Temp: (${settings?.extTmpTempSensor?.currentTemperature}${tempScaleStr})" : ""
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${str}")), state: (str != "" ? "complete" : null)
}
}
}
if(settings?.extTmpUseWeather || settings?.extTmpTempSensor) {
section("When the threshold Temps are Reached\nSet the Thermostat to ECO") {
input name: "extTmpDiffVal", type: "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("When desired and external temp difference is at least this many degrees (${tempScaleStr})?")), defaultValue: 1.0, submitOnChange: true, required: true
input name: "extTmpInsideDiffVal", type: "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("AND When desired and internal temp difference is within this many degrees (${tempScaleStr})?")), defaultValue: getTemperatureScale() == "C" ? 2.0 : 4.0, submitOnChange: true, required: true
}
section("Delay Values:") {
input "${pName}OffDelay", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dtse"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true // Delay to set eco
input "${pName}OnDelay", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr(titles("t_dr"))), required: false, defaultValue: 300, options: longTimeSecEnum(), submitOnChange: true
}
section("Restoration Preferences:") {
input "${pName}OffTimeout", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Auto Restore after (Optional)")), defaultValue: 0, options: longTimeSecEnum(), required: false, submitOnChange: true
if(!settings?."${pName}OffTimeout") { state."${pName}TimeoutScheduled" = false }
}
section(getDmtSectionDesc(extTmpPrefix())) {
def pageDesc = getDayModeTimeDesc(pName)
href "setDayModeTimePage5", title: imgTitle(getAppImg("i_calf"),inputTitleStr(titles("t_cr"))), description: pageDesc, state: (pageDesc ? "complete" : null)//,params: ["pName": "${pName}"]
}
section(sectionTitleStr(titles("t_nt"))) {
def t0 = getNotifConfigDesc(pName)
def pageDesc = t0 ? "${t0}" + descriptions("d_ttm") : ""
href "setNotificationPage5", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
}
def schTitle
if(!state?.activeSchedData?.size()) {
schTitle = "Optionally create schedules to set temperatures based on schedule"
} else {
schTitle = "Temperature settings based on schedule"
}
section("${schTitle}") { // EXTERNAL TEMPERATURE has TEMP Setting
href "scheduleConfigPage", title: imgTitle(getAppImg("schedule_icon.png"), inputTitleStr("Enable/Modify Schedules")), description: pageDesc, state: (pageDesc ? "complete" : null)//, params: ["sData":["hideStr":"${hidestr}"]]
}
}
}
}
}
}
def scheduleConfigPage(params) {
//LogTrace("scheduleConfigPage ($params)")
def sData = params?.sData
if(params?.sData) {
state.t_tempSData = params
sData = params?.sData
} else {
sData = state?.t_tempSData?.sData
}
dynamicPage(name: "scheduleConfigPage", title: "Thermostat Schedule Page", description: "Configure/View Schedules", uninstall: false) {
if(settings?.schMotTstat) {
def tstat = settings?.schMotTstat
def canHeat = state?.schMotTstatCanHeat
def canCool = state?.schMotTstatCanCool
def str = ""
def reqSenHeatSetPoint = getRemSenHeatSetTemp()
def reqSenCoolSetPoint = getRemSenCoolSetTemp()
def curZoneTemp = getRemoteSenTemp()
def tempSrcStr = state?.remoteTempSourceStr
def tempScaleStr = "${tUnitStr()}"
section {
str += "Zone Status:\n• Temp Source: (${tempSrcStr})\n• Temperature: (${curZoneTemp}${tempScaleStr})"
def hstr = canHeat ? "H: ${reqSenHeatSetPoint}${tempScaleStr}" : ""
def cstr = canHeat && canCool ? "/" : ""
cstr += canCool ? "C: ${reqSenCoolSetPoint}${tempScaleStr}" : ""
str += "\n• Setpoints: (${hstr}${cstr})\n"
str += "\nThermostat Status:\n• Temperature: (${getDeviceTemp(tstat)}${tempScaleStr})"
str += "\n• Mode: (${tstat ? ("${strCapitalize(tstat?.currentThermostatOperatingState)}/${strCapitalize(tstat?.currentThermostatMode)}") : "unknown"})"
hstr = canHeat ? "H: ${getTstatSetpoint(tstat, "heat")}${tempScaleStr}" : ""
cstr = canHeat && canCool ? "/" : ""
cstr += canCool ? "C: ${getTstatSetpoint(tstat, "cool")}${tempScaleStr}" : ""
str += "\n• Setpoints: (${hstr}${cstr})"
str += (state?.schMotTstatHasFan) ? "\n• FanMode: (${strCapitalize(tstat?.currentThermostatFanMode)})" : "\n• No Fan on HVAC system"
str += "\n• Presence: (${strCapitalize(getTstatPresence(tstat))})"
paragraph imgTitle(getAppImg("info_icon2.png"), paraTitleStr("${tstat?.displayName}\nSchedules and Setpoints:")), state: "complete"
paragraph sectionTitleStr("${str}"), state: "complete"
}
showUpdateSchedule(null,sData?.hideStr)
}
}
}
//ERS
def schMotSchedulePage1(params) { def t0 = [:]; t0.sNum = 1; return schMotSchedulePage( t0 ) }
def schMotSchedulePage2(params) { def t0 = [:]; t0.sNum = 2; return schMotSchedulePage( t0 ) }
def schMotSchedulePage3(params) { def t0 = [:]; t0.sNum = 3; return schMotSchedulePage( t0 ) }
def schMotSchedulePage4(params) { def t0 = [:]; t0.sNum = 4; return schMotSchedulePage( t0 ) }
def schMotSchedulePage5(params) { def t0 = [:]; t0.sNum = 5; return schMotSchedulePage( t0 ) }
def schMotSchedulePage6(params) { def t0 = [:]; t0.sNum = 6; return schMotSchedulePage( t0 ) }
def schMotSchedulePage7(params) { def t0 = [:]; t0.sNum = 7; return schMotSchedulePage( t0 ) }
def schMotSchedulePage8(params) { def t0 = [:]; t0.sNum = 8; return schMotSchedulePage( t0 ) }
def schMotSchedulePage(params) {
//LogTrace("schMotSchedulePage($params)")
def sNum = params?.sNum
if(params?.sNum) {
state.t_schedData = params
sNum = params?.sNum
} else {
sNum = state?.t_schedData?.sNum
}
dynamicPage(name: "schMotSchedulePage", title: "Edit Schedule Page", description: "Modify Schedules", uninstall: false) {
if(sNum) {
showUpdateSchedule(sNum)
}
}
}
def getScheduleList() {
def cnt = null // parent ? parent?.state?.appData?.settings?.schedules?.count : null
def maxCnt = cnt ? cnt.toInteger() : 8
maxCnt = Math.min( Math.max(maxCnt,4), 8)
if(maxCnt < state?.scheduleList?.size()) {
maxCnt = state?.scheduleList?.size()
LogAction("A schedule size issue has occurred. The configured schedule size is smaller than the previous configuration restoring previous schedule size.", "warn", true)
}
def list = 1..maxCnt
state?.scheduleList = list
return list
}
def showUpdateSchedule(sNum=null,hideStr=null) {
updateScheduleStateMap()
def schedList = getScheduleList() // setting in initAutoApp adjust # of schedule slots
def lact
def act = 1
def sLbl
def cnt = 1
schedList?.each { scd ->
sLbl = "schMot_${scd}_"
if(sNum != null) {
if(sNum?.toInteger() == scd?.toInteger()) {
lact = act
act = settings["${sLbl}SchedActive"]
def schName = settings["${sLbl}name"]
editSchedule("secData":["scd":scd, "schName":schName, "hideable":(sNum ? false : true), "hidden":(act || (!act && scd == 1)) ? true : false, "hideStr":hideStr])
}
} else {
lact = act
act = settings["${sLbl}SchedActive"]
if (lact || act) {
def schName = settings["${sLbl}name"]
editSchedule("secData":["scd":scd, "schName":schName, "hideable":true, "hidden":(act || (!act && scd == 1)) ? true : false, "hideStr":hideStr])
}
}
}
}
def editSchedule(schedData) {
def cnt = schedData?.secData?.scd
LogTrace("editSchedule (${schedData?.secData})")
String sLbl = "schMot_${cnt}_"
boolean canHeat = state?.schMotTstatCanHeat
boolean canCool = state?.schMotTstatCanCool
String tempScaleStr = "${tUnitStr()}"
def act = settings["${sLbl}SchedActive"]
def actIcon = act ? "active" : "inactive"
def hideStr = schedData?.secData?.hideStr
def sectStr = schedData?.secData?.schName ? (act ? "Enabled" : "Disabled") : "Tap to Enable"
def titleStr = "Schedule ${schedData?.secData?.scd} (${sectStr})"
section(title: "${titleStr} ") {//, hideable:schedData?.secData?.hideable, hidden: schedData?.secData?.hidden) {
input "${sLbl}SchedActive", "bool", title: imgTitle(getAppImg("${actIcon}_icon.png"), inputTitleStr("Schedule Enabled")), description: ( (cnt == 1 && !act) ? "Enable to Edit Schedule" : null), required: true,
defaultValue: false, submitOnChange: true
if(act) {
input "${sLbl}name", "text", title: imgTitle(getAppImg("name_tag_icon.png"), inputTitleStr("Schedule Name")), required: true, defaultValue: "Schedule ${cnt}", multiple: false, submitOnChange: true
}
}
if(act) {
section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Setpoint Configuration: ", hideable: true, hidden: (settings["${sLbl}HeatTemp"] != null && settings["${sLbl}CoolTemp"] != null) ) {
paragraph paraTitleStr("Setpoints and Mode")
paragraph sectionTitleStr("Configure Setpoints and HVAC modes that will be set when this Schedule is in use")
if(canHeat) {
input "${sLbl}HeatTemp", "decimal", title: imgTitle(getAppImg("heat_icon.png"), inputTitleStr("Heat Set Point (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(),
submitOnChange: true
}
if(canCool) {
input "${sLbl}CoolTemp", "decimal", title: imgTitle(getAppImg("cool_icon.png"), inputTitleStr("Cool Set Point (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(),
submitOnChange: true
}
input "${sLbl}HvacMode", "enum", title: imgTitle(getAppImg("i_hmod"), inputTitleStr("Set Hvac Mode (Optional):")), required: false, description: "No change set", options: tModeHvacEnum(canHeat,canCool, true), multiple: false
}
if(settings?.schMotRemoteSensor && !("remSen" in hideStr)) {
section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Remote Sensor Options: ", hideable: true, hidden: (settings["${sLbl}remSensor"] == null && settings["${sLbl}remSenThreshold"] == null)) {
paragraph paraTitleStr("Alternate Remote Sensors\n(Optional)")
paragraph sectionTitleStr("Configure alternate Remote Temp sensors that are active with this schedule")
input "${sLbl}remSensor", "capability.temperatureMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("Alternate Temp Sensors")), description: "For Remote Sensor Automation", submitOnChange: true, required: false, multiple: true
if(settings?."${sLbl}remSensor" != null) {
def tmpVal = "Temp${(settings["${sLbl}remSensor"]?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings["${sLbl}remSensor"])}${tempScaleStr})"
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("${tmpVal}")), state: "complete"
}
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr("Alternate Action Threshold Temp\n(Optional)?"))
paragraph sectionTitleStr("Temp difference to trigger HVAC operations used with this schedule")
input "${sLbl}remSenThreshold", "decimal", title: imgTitle(getAppImg("temp_icon.png"), inputTitleStr("Action Threshold Temp (${tempScaleStr})")), required: false, defaultValue: 2.0
}
}
section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Motion Sensor Setpoints: ", hideable: true, hidden:(settings["${sLbl}Motion"] == null) ) {
paragraph paraTitleStr("Optional")
paragraph sectionTitleStr("Activate alternate HVAC settings with Motion")
def mmot = settings["${sLbl}Motion"]
input "${sLbl}Motion", "capability.motionSensor", title: imgTitle(getAppImg("motion_icon.png"), inputTitleStr("Motion Sensors")), description: "Select Sensors to Use", required: false, multiple: true, submitOnChange: true
if(settings["${sLbl}Motion"]) {
paragraph imgTitle(getAppImg("i_inst"), paraTitleStr(" • Motion State: (${isMotionActive(mmot) ? "Active" : "Not Active"})")), state: "complete"
if(canHeat) {
input "${sLbl}MHeatTemp", "decimal", title: imgTitle(getAppImg("heat_icon.png"), inputTitleStr("Heat Setpoint with Motion(${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues()
}
if(canCool) {
input "${sLbl}MCoolTemp", "decimal", title: imgTitle(getAppImg("cool_icon.png"), inputTitleStr("Cool Setpoint with Motion (${tempScaleStr})")), description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues()
}
input "${sLbl}MHvacMode", "enum", title: imgTitle(getAppImg("i_hmod"), inputTitleStr("Set Hvac Mode with Motion:")), required: false, description: "No change set", options: tModeHvacEnum(canHeat,canCool,true), multiple: false
// input "${sLbl}MRestrictionMode", "mode", title: "Ignore in these modes", description: "Any location mode", required: false, multiple: true, image: getAppImg("i_mod")
// input "${sLbl}MPresHome", "capability.presenceSensor", title: "Only act when these people are home", description: "Always", required: false, multiple: true, image: getAppImg("nest_dev_pres_icon.png")
// input "${sLbl}MPresAway", "capability.presenceSensor", title: "Only act when these people are away", description: "Always", required: false, multiple: true, image: getAppImg("nest_dev_away_icon.png")
input "${sLbl}MDelayValOn", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay Motion Setting Changes")), required: false, defaultValue: 60, options: longTimeSecEnum(), multiple: false
input "${sLbl}MDelayValOff", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Delay disabling Motion Settings")), required: false, defaultValue: 1800, options: longTimeSecEnum(), multiple: false
}
}
def timeFrom = settings["${sLbl}rstrctTimeFrom"]
def timeTo = settings["${sLbl}rstrctTimeTo"]
def showTime = (timeFrom || timeTo || settings?."${sLbl}rstrctTimeFromCustom" || settings?."${sLbl}rstrctTimeToCustom") ? true : false
def myShow = !(settings["${sLbl}rstrctMode"] || settings["${sLbl}restrictionDOW"] || showTime || settings["${sLbl}rstrctSWOn"] || settings["${sLbl}rstrctSWOff"] || settings["${sLbl}rstrctPHome"] || settings["${sLbl}rstrctPAway"] )
section("(${schedData?.secData?.schName ?: "Schedule ${cnt}"}) Schedule Restrictions: ", hideable: true, hidden: myShow) {
paragraph paraTitleStr("Optional")
paragraph sectionTitleStr("Restrict when this Schedule is in use")
input "${sLbl}rstrctMode", "mode", title: imgTitle(getAppImg("i_mod"), inputTitleStr("Only execute in these modes")), description: "Any location mode", required: false, multiple: true
input "${sLbl}restrictionDOW", "enum", options: timeDayOfWeekOptions(), title: imgTitle(getAppImg("day_calendar_icon2.png"), inputTitleStr("Only execute on these days")), description: "Any week day", required: false, multiple: true
input "${sLbl}rstrctTimeFrom", "enum", title: imgTitle(getAppImg("start_time_icon.png"), inputTitleStr((timeFrom ? "Only execute if time is between" : "Only execute during this time"))), options: timeComparisonOptionValues(), required: showTime, multiple: false, submitOnChange: true
if (showTime) {
if ((timeFrom && timeFrom.contains("custom")) || settings?."${sLbl}rstrctTimeFromCustom" != null) {
input "${sLbl}rstrctTimeFromCustom", "time", title: inputTitleStr("Custom time"), required: true, multiple: false
} else {
input "${sLbl}rstrctTimeFromOffset", "number", title: imgTitle(getAppImg("offset_icon.png"), inputTitleStr("Offset (+/- minutes)")), range: "*..*", required: true, multiple: false, defaultValue: 0
}
input "${sLbl}rstrctTimeTo", "enum", title: imgTitle(getAppImg("stop_time_icon.png"), inputTitleStr("And")), options: timeComparisonOptionValues(), required: true, multiple: false, submitOnChange: true
if ((timeTo && timeTo.contains("custom")) || settings?."${sLbl}rstrctTimeToCustom" != null) {
input "${sLbl}rstrctTimeToCustom", "time", title: inputTitleStr("Custom time"), required: true, multiple: false
} else {
input "${sLbl}rstrctTimeToOffset", "number", title: imgTitle(getAppImg("offset_icon.png"), inputTitleStr("Offset (+/- minutes)")), range: "*..*", required: true, multiple: false, defaultValue: 0
}
}
input "${sLbl}rstrctPHome", "capability.presenceSensor", title: imgTitle(getAppImg("nest_dev_pres_icon.png"), inputTitleStr("Only execute when one or more of these People are home")), description: "Always", required: false, multiple: true
input "${sLbl}rstrctPAway", "capability.presenceSensor", title: imgTitle(getAppImg("nest_dev_away_icon.png"), inputTitleStr("Only execute when all these People are away")), description: "Always", required: false, multiple: true
input "${sLbl}rstrctSWOn", "capability.switch", title: imgTitle(getAppImg("i_sw"), inputTitleStr("Only execute when these switches are all on")), description: "Always", required: false, multiple: true
input "${sLbl}rstrctSWOff", "capability.switch", title: imgTitle(getAppImg("switch_off_icon.png"), inputTitleStr("Only execute when these switches are all off")), description: "Always", required: false, multiple: true
}
}
}
def getScheduleDesc(num = null) {
def result = [:]
def schedData = state?.activeSchedData
def actSchedNum = getCurrentSchedule()
String tempScaleStr = "${tUnitStr()}"
def schNum
def schData
int sCnt = 1
def sData = schedData
if(num) {
sData = schedData?.find { it?.key.toInteger() == num.toInteger() }
}
if(sData?.size()) {
sData?.sort().each { scd ->
String str = ""
schNum = scd?.key
schData = scd?.value
String sLbl = "schMot_${schNum}_"
boolean isRestrict = (schData?.m || schData?.tf || schData?.tfc || schData?.tfo || schData?.tt || schData?.ttc || schData?.tto || schData?.w || schData?.s1 || schData?.s0 || schData?.p1 || schData?.p0)
boolean isTimeRes = (schData?.tf || schData?.tfc || schData?.tfo || schData?.tt || schData?.ttc || schData?.tto)
boolean isDayRes = schData?.w
boolean isTemp = (schData?.ctemp || schData?.htemp || schData?.hvacm)
boolean isSw = (schData?.s1 || schData?.s0)
boolean isPres = (schData?.p1 || schData?.p0)
boolean isMot = schData?.m0
boolean isRemSen = (schData?.sen0 || schData?.thres)
boolean isFanEn = schData?.fan0
String resPreBar = isSw || isPres || isTemp ? "│" : " "
String tempPreBar = isMot || isRemSen ? "│" : " "
boolean motPreBar = isRemSen
str += schData?.lbl ? " • ${schData?.lbl}${(actSchedNum?.toInteger() == schNum?.toInteger()) ? " (In Use)" : " (Not In Use)"}" : ""
//restriction section
str += isRestrict ? "\n ${isSw || isPres || isTemp ? "├" : "└"} Restrictions:" : ""
int mLen = schData?.m ? schData?.m?.toString().length() : 0
String mStr = ""
int mdSize = 1
schData?.m?.each { md ->
mStr += md ? "\n ${isSw || isPres || isTemp ? "│ ${(isDayRes || isTimeRes || isPres || isSw) ? "│" : " "}" : " "} ${mdSize < schData?.m.size() ? "├" : "└"} ${md.toString()}" : ""
mdSize = mdSize+1
}
str += schData?.m ? "\n ${resPreBar} ${(isTimeRes || schData?.w) ? "├" : "└"} Mode${schData?.m?.size() > 1 ? "s" : ""}:${isInMode(schData?.m) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
str += schData?.m ? "$mStr" : ""
String dayStr = getAbrevDay(schData?.w)
String timeDesc = getScheduleTimeDesc(schData?.tf, schData?.tfc, schData?.tfo, schData?.tt, schData?.ttc, schData?.tto, (isSw || isPres || isDayRes))
str += isTimeRes ? "\n │ ${isDayRes || isPres || isSw ? "├" : "└"} ${timeDesc}" : ""
str += isDayRes ? "\n │ ${isSw || isPres ? "├" : "└"} Days:${getSchRestrictDoWOk(schNum) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
str += isDayRes ? "\n │ ${isSw || isPres ? "│" :" "} └ ${dayStr}" : ""
// def p1Len = schData?.p1 ? schData?.p1?.toString().length() : 0
// def p1Str = ""
// def p1dSize = 1
// settings["${sLbl}rstrctPAway"]?.each { ps1 ->
// p1Str += ps1 ? "\n ${isSw || isPres || isTemp ? "│ " : " "} ${p1dSize < settings["${sLbl}rstrctPAway"].size() ? "├" : "└"} ${ps1.toString()}${!isPresenceHome(ps1) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
// p1dSize = p1dSize+1
// }
// def p0Len = schData?.p0 ? schData?.p0?.toString().length() : 0
// def p0Str = ""
// def p0dSize = 1
// settings["${sLbl}rstrctPHome"]?.each { ps0 ->
// p0Str += ps0 ? "\n ${isSw || isPres || isTemp ? "│ " : " "} ${p0dSize < settings["${sLbl}rstrctPHome"].size() ? "├" : "└"} ${ps0.toString()}" : ""
// p0dSize = p0dSize+1
// }
str += schData?.p1 ? "\n │ ${(schData?.p0 || isSw) ? "├" : "└"} Presence Home:${isSomebodyHome(settings["${sLbl}rstrctPHome"]) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
//str += schData?.p1 ? "$p1Str" : ""
str += schData?.p1 ? "\n │ ${(schData?.p0 || isSw) ? "│" : " "} └ (${schData?.p1.size()} Selected)" : ""
str += schData?.p0 ? "\n │ ${isSw ? "├" : "└"} Presence Away:${!isSomebodyHome(settings["${sLbl}rstrctPAway"]) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
//str += schData?.p0 ? "$p0Str" : ""
str += schData?.p0 ? "\n │ ${isSw ? "│" : " "} └ (${schData?.p0.size()} Selected)" : ""
str += schData?.s1 ? "\n │ ${schData?.s0 ? "├" : "└"} Switches On:${isSwitchOn(settings["${sLbl}rstrctSWOn"]) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
str += schData?.s1 ? "\n │ ${schData?.s0 ? "│" : " "} └ (${schData?.s1.size()} Selected)" : ""
str += schData?.s0 ? "\n │ └ Switches Off:${!isSwitchOn(settings["${sLbl}rstrctSWOff"]) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
str += schData?.s0 ? "\n │ └ (${schData?.s0.size()} Selected)" : ""
//Temp Setpoints
str += isTemp ? "${isRestrict ? "\n │\n" : "\n"} ${(isMot || isRemSen) ? "├" : "└"} Temp Setpoints:" : ""
str += schData?.ctemp ? "\n ${tempPreBar} ${schData?.htemp ? "├" : "└"} Cool Setpoint: (${fixTempSetting(schData?.ctemp)}${tempScaleStr})" : ""
str += schData?.htemp ? "\n ${tempPreBar} ${schData?.hvacm ? "├" : "└"} Heat Setpoint: (${fixTempSetting(schData?.htemp)}${tempScaleStr})" : ""
str += schData?.hvacm ? "\n ${tempPreBar} └ HVAC Mode: (${strCapitalize(schData?.hvacm)})" : ""
//Motion Info
// def m0Len = schData?.p0 ? schData?.p0?.toString().length() : 0
// def m0Str = ""
// def m0dSize = 1
// schData?.m0?.each { ms0 ->
// m0Str += ms0 ? "\n ${isTemp || isFanEn || isRemSen || isRestrict ? "│" : " "} ${m0dSize < schData?.m0.size() ? "├" : "└"} ${ms0.toString()}" : ""
// m0dSize = m0dSize+1
// }
str += isMot ? "${isTemp || isFanEn || isRemSen || isRestrict ? "\n │\n" : "\n"} ${isRemSen ? "├" : "└"} Motion Settings:" : ""
str += isMot ? "\n ${motPreBar ? "│" : " "} ${(schData?.mctemp || schData?.mhtemp) ? "├" : "└"} Motion Sensors: (${schData?.m0.size()})" : ""
//str += schData?.m0 ? "$m0Str" : ""
//str += isMot ? "\n ${motPreBar ? "│" : " "} ${schData?.mctemp || schData?.mhtemp ? "│" : ""} └ (${isMotionActive(settings["${sLbl}Motion"]) ? "Active" : "None Active"})" : ""
str += isMot && schData?.mctemp ? "\n ${motPreBar ? "│" : " "} ${(schData?.mctemp || schData?.mhtemp) ? "├" : "└"} Mot. Cool Setpoint: (${fixTempSetting(schData?.mctemp)}${tempScaleStr})" : ""
str += isMot && schData?.mhtemp ? "\n ${motPreBar ? "│" : " "} ${schData?.mdelayOn || schData?.mdelayOff ? "├" : "└"} Mot. Heat Setpoint: (${fixTempSetting(schData?.mhtemp)}${tempScaleStr})" : ""
str += isMot && schData?.mhvacm ? "\n ${motPreBar ? "│" : " "} ${(schData?.mdelayOn || schData?.mdelayOff) ? "├" : "└"} Mot. HVAC Mode: (${strCapitalize(schData?.mhvacm)})" : ""
str += isMot && schData?.mdelayOn ? "\n ${motPreBar ? "│" : " "} ${schData?.mdelayOff ? "├" : "└"} Mot. On Delay: (${getEnumValue(longTimeSecEnum(), schData?.mdelayOn)})" : ""
str += isMot && schData?.mdelayOff ? "\n ${motPreBar ? "│" : " "} └ Mot. Off Delay: (${getEnumValue(longTimeSecEnum(), schData?.mdelayOff)})" : ""
//Remote Sensor Info
str += isRemSen && schData?.sen0 ? "${isRemSen || isRestrict ? "\n │\n" : "\n"} └ Alternate Remote Sensor:" : ""
//str += isRemSen && schData?.sen0 ? "\n ├ Temp Sensors: (${schData?.sen0.size()})" : ""
settings["${sLbl}remSensor"]?.each { t ->
str += "\n ├ ${t?.label}: ${(t?.label?.toString()?.length() > 10) ? "\n │ └ " : ""}(${getDeviceTemp(t)}${tempScaleStr})"
}
str += isRemSen && schData?.sen0 ? "\n └ Temp${(settings["${sLbl}remSensor"]?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings["${sLbl}remSensor"])}${tempScaleStr})" : ""
str += isRemSen && schData?.thres ? "\n └ Threshold: (${settings["${sLbl}remSenThreshold"]}${tempScaleStr})" : ""
//log.debug "str: \n$str"
if(str != "") { result[schNum] = str }
}
}
return (result?.size() >= 1) ? result : null
}
String getScheduleTimeDesc(timeFrom, timeFromCustom, timeFromOffset, timeTo, timeToCustom, timeToOffset, showPreLine=false) {
def tf = new SimpleDateFormat("h:mm a")
tf.setTimeZone(location?.timeZone)
String spl = showPreLine == true ? "│" : ""
String timeToVal = null
String timeFromVal = null
int i = 0
if(timeFrom && timeTo) {
while (i < 2) {
switch(i == 0 ? timeFrom : timeTo) {
case "custom time":
if(i == 0) { timeFromVal = tf.format(Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", timeFromCustom)) }
else { timeToVal = tf.format(Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", timeToCustom)) }
break
case "sunrise":
def sunTime = ((timeFromOffset > 0 || timeToOffset > 0) ? getSunriseAndSunset(zipCode: location.zipCode, sunriseOffset: "00:${i == 0 ? timeFromOffset : timeToOffset}") : getSunriseAndSunset(zipCode: location.zipCode))
if(i == 0) { timeFromVal = "Sunrise: (" + tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunrise.toString())) + ")" }
else { timeToVal = "Sunrise: (" + tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunrise.toString())) + ")" }
break
case "sunset":
def sunTime = ((timeFromOffset > 0 || timeToOffset > 0) ? getSunriseAndSunset(zipCode: location.zipCode, sunriseOffset: "00:${i == 0 ? timeFromOffset : timeToOffset}") : getSunriseAndSunset(zipCode: location.zipCode))
if(i == 0) { timeFromVal = "Sunset: (" + tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunset.toString())) + ")" }
else { timeToVal = "Sunset: (" + tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", sunTime?.sunset.toString())) + ")" }
break
case "noon":
def rightNow = adjustTime().time
def offSet = (timeFromOffset != null || timeToOffset != null) ? (i == 0 ? (timeFromOffset * 60 * 1000) : (timeToOffset * 60 * 1000)) : 0
String res = "Noon: " + formatTime(convertDateToUnixTime((rightNow - rightNow.mod(86400000) + 43200000) + offSet))
if(i == 0) { timeFromVal = res }
else { timeToVal = res }
break
case "midnight":
def rightNow = adjustTime().time
def offSet = (timeFromOffset != null || timeToOffset != null) ? (i == 0 ? (timeFromOffset * 60 * 1000) : (timeToOffset * 60 * 1000)) : 0
String res = "Midnight: " + formatTime(convertDateToUnixTime((rightNow - rightNow.mod(86400000)) + offSet))
if(i == 0) { timeFromVal = res }
else { timeToVal = res }
break
}
i += 1
}
}
boolean timeOk = ((timeFrom && (timeFromCustom || timeFromOffset) && timeTo && (timeToCustom || timeToOffset)) && checkTimeCondition(timeFrom, timeFromCustom, timeFromOffset, timeTo, timeToCustom, timeToOffset)) ? true : false
String out = ""
out += (timeFromVal && timeToVal) ? "Time:${timeOk ? " (${okSym()})" : " (${notOkSym()})"}\n │ ${spl} ├ $timeFromVal\n │ ${spl} ├ to\n │ ${spl} └ $timeToVal" : ""
return out
}
void updSchedActiveState(String schNum, String active) {
LogTrace("updSchedActiveState(schNum: $schNum, active: $active)")
if(schNum && active) {
def sLbl = "schMot_${schNum}_SchedActive"
def curAct = settings["${sLbl}"]
if(curAct.toString() == active.toString()) { return }
LogAction("updateSchedActiveState | Setting Schedule (${schNum} - ${getSchedLbl(schNum)}) Active to ($active)", "info", false)
settingUpdate("${sLbl}", "${active}")
} else { return }
}
String okSym() {
return "✓"// ☑"
}
String notOkSym() {
return "✘"
}
def getRemSenTempSrc() {
return state?.remoteTempSourceStr ?: null
}
def getAbrevDay(vals) {
def list = []
if(vals) {
//log.debug "days: $vals | (${vals?.size()})"
def len = (vals?.toString().length() < 7) ? 3 : 2
vals?.each { d ->
list.push(d?.toString().substring(0, len))
}
}
return list
}
def roundTemp(Double temp) {
if(temp == null) { return null }
def newtemp
if( getTemperatureScale() == "C") {
newtemp = Math.round(temp.round(1) * 2) / 2.0f
} else {
if(temp instanceof Integer) {
//log.debug "roundTemp: ($temp) is Integer"
newTemp = temp.toInteger()
}
else if(temp instanceof Double) {
//log.debug "roundTemp: ($temp) is Double"
newtemp = temp.round(0).toInteger()
}
else if(temp instanceof BigDecimal) {
//log.debug "roundTemp: ($temp) is BigDecimal"
newtemp = temp.toInteger()
}
}
return newtemp
}
def updateScheduleStateMap() {
if(autoType == "schMot" && isSchMotConfigured()) {
def actSchedules = null
int numAct = 0
actSchedules = [:]
getScheduleList()?.each { scdNum ->
String sLbl = "schMot_${scdNum}_"
def newScd = [:]
def schActive = settings["${sLbl}SchedActive"]
if(schActive) {
actSchedules?."${scdNum}" = [:]
newScd = cleanUpMap([
lbl: settings["${sLbl}name"],
m: settings["${sLbl}rstrctMode"],
tf: settings["${sLbl}rstrctTimeFrom"],
tfc: settings["${sLbl}rstrctTimeFromCustom"],
tfo: settings["${sLbl}rstrctTimeFromOffset"],
tt: settings["${sLbl}rstrctTimeTo"],
ttc: settings["${sLbl}rstrctTimeToCustom"],
tto: settings["${sLbl}rstrctTimeToOffset"],
w: settings["${sLbl}restrictionDOW"],
p1: deviceInputToList(settings["${sLbl}rstrctPHome"]),
p0: deviceInputToList(settings["${sLbl}rstrctPAway"]),
s1: deviceInputToList(settings["${sLbl}rstrctSWOn"]),
s0: deviceInputToList(settings["${sLbl}rstrctSWOff"]),
ctemp: roundTemp(settings["${sLbl}CoolTemp"]),
htemp: roundTemp(settings["${sLbl}HeatTemp"]),
hvacm: settings["${sLbl}HvacMode"],
sen0: settings["schMotRemoteSensor"] ? deviceInputToList(settings["${sLbl}remSensor"]) : null,
thres: settings["schMotRemoteSensor"] ? settings["${sLbl}remSenThreshold"] : null,
m0: deviceInputToList(settings["${sLbl}Motion"]),
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
actSchedules?."${scdNum}" = newScd
//LogAction("updateScheduleMap [ ScheduleNum: $scdNum | PrefixLbl: $sLbl | SchedActive: $schActive | NewSchedData: $newScd ]", "info", false)
}
}
state.activeSchedData = actSchedules
}
}
def deviceInputToList(items) {
def list = []
if(items) {
items?.sort().each { d ->
list.push(d?.displayName.toString())
}
return list
}
return null
}
/*
def inputItemsToList(items) {
def list = []
if(items) {
items?.each { d ->
list.push(d)
}
return list
}
return null
}
*/
boolean isSchMotConfigured() {
return (settings?.schMotTstat && (
settings?.schMotOperateFan ||
settings?.schMotRemoteSensor ||
settings?.schMotWaterOff ||
settings?.schMotContactOff ||
settings?.schMotHumidityControl ||
settings?.schMotExternalTempOff)) ? true : false
}
def getAutoRunSec() { return !state?.autoRunDt ? 100000 : GetTimeDiffSeconds(state?.autoRunDt, null, "getAutoRunSec").toInteger() }
def schMotCheck() {
LogTrace("schMotCheck")
try {
if(getIsAutomationDisabled()) { return }
def schWaitVal = settings?.schMotWaitVal?.toInteger() ?: 60
if(schWaitVal > 120) { schWaitVal = 120 }
def t0 = getAutoRunSec()
if(t0 < schWaitVal) {
def schChkVal = ((schWaitVal - t0) < 30) ? 30 : (schWaitVal - t0)
scheduleAutomationEval(schChkVal)
LogAction("Too Soon to Evaluate Actions; Re-Evaluation in (${schChkVal} seconds)", "info", false)
return
}
long execTime = now()
state?.autoRunDt = getDtNow()
// This order is important
// turn system on/off, then update schedule mode/temps, then remote sensors, then update fans
def updatedWeather = false
if(settings?.schMotWaterOff) {
if(isLeakWatConfigured()) { leakWatCheck() }
}
if(settings?.schMotContactOff) {
if(isConWatConfigured()) { conWatCheck() }
}
if(settings?.schMotExternalTempOff) {
if(isExtTmpConfigured()) {
if(settings?.extTmpUseWeather && !updatedWeather) { updatedWeather = true; getExtConditions() }
extTmpTempCheck()
}
}
// if(settings?.schMotSetTstatTemp) {
if(isTstatSchedConfigured()) { setTstatTempCheck() }
// }
if(settings?.schMotRemoteSensor) {
if(isRemSenConfigured()) {
remSenCheck()
}
}
if(settings?.schMotHumidityControl) {
if(isHumCtrlConfigured()) {
if(settings?.humCtrlUseWeather && !updateWeather) { getExtConditions() }
humCtrlCheck()
}
}
if(settings?.schMotOperateFan) {
if(isFanCtrlConfigured()) {
fanCtrlCheck()
}
}
storeExecutionHistory((now() - execTime), "schMotCheck")
} catch (ex) {
log.error "schMotCheck Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "schMotCheck", true, getAutoType())
}
}
def storeLastEventData(evt) {
if(evt) {
def newVal = ["name":evt.name, "displayName":evt.displayName, "value":evt.value, "date":formatDt(evt.date), "unit":evt.unit]
state?.lastEventData = newVal
//log.debug "LastEvent: ${state?.lastEventData}"
def list = state?.detailEventHistory ?: []
int listSize = 15
if(list?.size() < listSize) {
list.push(newVal)
}
else if(list?.size() > listSize) {
int 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?.detailEventHistory = list }
}
}
void storeExecutionHistory(val, method = null) {
//log.debug "storeExecutionHistory($val, $method)"
// try {
if(method) {
LogTrace("${method} Execution Time: (${val} milliseconds)")
}
if(method in ["watchDogCheck", "checkNestMode", "schMotCheck"]) {
state?.autoExecMS = val ?: null
def list = state?.evalExecutionHistory ?: []
int listSize = 20
list = addToList(val, list, listSize)
if(list) { state?.evalExecutionHistory = list }
}
if(!(method in ["watchDogCheck", "checkNestMode"])) {
def list = state?.detailExecutionHistory ?: []
int listSize = 30
list = addToList([val, method, getDtNow()], list, listSize)
if(list) { state?.detailExecutionHistory = list }
}
// } catch (ex) {
// log.error "storeExecutionHistory Exception:", ex
//parent?.sendExceptionData(ex, "storeExecutionHistory", true, getAutoType())
// }
}
def addToList(val, list, listSize) {
if(list?.size() < listSize) {
list.push(val)
} else if(list?.size() > listSize) {
int nSz = (list?.size()-listSize) + 1
def nList = list?.drop(nSz)
nList?.push(val)
list = nList
} else if(list?.size() == listSize) {
def nList = list?.drop(1)
nList?.push(val)
list = nList
}
return list
}
def getAverageValue(items) {
def tmpAvg = []
def val = 0
if(!items) { return val }
else if(items?.size() > 1) {
tmpAvg = items
if(tmpAvg && tmpAvg?.size() > 1) { val = (tmpAvg?.sum().toDouble() / tmpAvg?.size().toDouble()).round(0) }
} else { val = item }
return val.toInteger()
}
/************************************************************************************************
| DYNAMIC NOTIFICATION PAGES |
*************************************************************************************************/
def setNotificationPage1(params) {
//href "setNotificationPage1", title: titles("t_nt"), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? "complete" : null), image: getAppImg("i_not")
LogTrace("setNotificationPage1()")
String pName = watchDogPrefix()
def t0 = ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
return setNotificationPage( t0 )
}
def setNotificationPage2(params) {
//href "setNotificationPage2", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":false, "allowAlarm":false, "showSchedule":true], state: (pageDesc ? "complete" : null)
LogTrace("setNotificationPage2()")
String pName = nModePrefix()
def t0 = ["pName":"${pName}", "allowSpeech":false, "allowAlarm":false, "showSchedule":true]
return setNotificationPage( t0 )
}
def setNotificationPage3(params) {
//href "setNotificationPage3", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? "complete" : null)
String pName = leakWatPrefix()
LogTrace("setNotificationPage3()")
def t0 = ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
return setNotificationPage( t0 )
}
def setNotificationPage4(params) {
//href "setNotificationPage4, title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? "complete" : null)
String pName = conWatPrefix()
LogTrace("setNotificationPage4()")
def t0 = ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
return setNotificationPage( t0 )
}
def setNotificationPage5(params) {
//href "setNotificationPage5", title: imgTitle(getAppImg("i_not"),inputTitleStr(titles("t_nt"))), description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true], state: (pageDesc ? "complete" : null)
String pName = extTmpPrefix()
LogTrace("setNotificationPage5()")
def t0 = ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true]
return setNotificationPage( t0 )
}
def setNotificationPage(params) {
String pName = params?.pName
boolean allowSpeech = false
boolean allowAlarm = false
boolean showSched = false
if(params?.pName) {
state.t_notifD = params
allowSpeech = params?.allowSpeech?.toBoolean(); showSched = params?.showSchedule?.toBoolean(); allowAlarm = params?.allowAlarm?.toBoolean()
} else {
pName = state?.t_notifD?.pName; allowSpeech = state?.t_notifD?.allowSpeech; showSched = state?.t_notifD?.showSchedule; allowAlarm = state?.t_notifD?.allowAlarm
}
if(pName == null) { return }
dynamicPage(name: "setNotificationPage", title: "Configure Notification Options", uninstall: false) {
section("") {
//section("Notification Preferences:") {
input "${pName}NotifOn", "bool", title: imgTitle(getAppImg("i_not"), inputTitleStr("Enable Notifications?")), description: (!settings["${pName}NotifOn"] ? "Enable Text, Voice, or Alarm Notifications" : ""), required: false, defaultValue: false, submitOnChange: true
boolean fixSettings = false
if(settings["${pName}NotifOn"]) {
// section("Use NST Manager Settings:") {
input "${pName}UseMgrNotif", "bool", title: imgTitle(getAppImg("notification_icon2.png"), inputTitleStr("Use Manager Settings?")), defaultValue: true, submitOnChange: true, required: false
// }
if(settings?."${pName}UseMgrNotif" == false) {
// section("Enable Text Messaging:") {
input "${pName}NotifPhones", "phone", title: imgTitle(getAppImg("notification_icon2.png"), inputTitleStr("Send SMS to Number (Optional)")), required: false, submitOnChange: true
// }
// section("Enable Pushover Support:") {
input "${pName}PushoverEnabled", "bool", title: imgTitle(getAppImg("pushover_icon.png"), inputTitleStr("Notification Device")), required: false, submitOnChange: true
if(settings?."${pName}PushoverEnabled" == true) {
input "${pName}PushoverDevices", "capability.notification", title: imgTitle(getAppImg("pushover_icon.png"), inputTitleStr("Notification Device")), required: false, submitOnChange: true
}
// }
} else {
fixSettings = true
}
} else {
fixSettings = true
}
if(fixSettings) {
settingRemove("${pName}NotifPhones")
settingRemove("${pName}PushoverEnabled")
settingRemove("${pName}PushoverDevices")
//settingRemove("${pName}UseParentNotifRestrictions")
}
/*
if(allowSpeech && settings?."${pName}NotifOn") {
// section("Voice Notification Preferences:") {
input "${pName}AllowSpeechNotif", "bool", title: "Enable Voice Notifications?", description: "Media players, or Speech Devices", required: false, defaultValue: (settings?."${pName}AllowSpeechNotif" ? true : false), submitOnChange: true, image: getAppImg("speech_icon.png")
if(settings["${pName}AllowSpeechNotif"]) {
setInitialVoiceMsgs(pName)
input "${pName}SendToAskAlexaQueue", "bool", title: "Send to Ask Alexa Message Queue?", required: false, defaultValue: (settings?."${pName}AllowSpeechNotif" ? false : true), submitOnChange: true,
image: askAlexaImgUrl()
input "${pName}SpeechMediaPlayer", "capability.musicPlayer", title: "Select Media Player(s)", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg("media_player.png")
input "${pName}EchoDevices", "device.echoSpeaksDevice", title: "Select Alexa Devices(s)", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg('echo_speaks.png')
input "${pName}SpeechDevices", "capability.speechSynthesis", title: "Select Speech Synthesizer(s)", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg("speech2_icon.png")
if(settings["${pName}SpeechMediaPlayer"] || settings["${pName}EchoDevices"]) {
input "${pName}SpeechVolumeLevel", "number", title: "Default Volume Level?", required: false, defaultValue: 30, range: "0::100", submitOnChange: true, image: getAppImg("volume_icon.png")
if(settings["${pName}SpeechMediaPlayer"]) {
input "${pName}SpeechAllowResume", "bool", title: "Can Resume Playing Media?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("resume_icon.png")
}
}
def desc = ""
if(pName in ["conWat", "extTmp", "leakWat"]) {
if( (settings["${pName}SpeechMediaPlayer"] || settings["${pName}SpeechDevices"] || settings["${pName}EchoDevices"] || settings["${pName}SendToAskAlexaQueue"]) ) {
switch(pName) {
case "conWat":
desc = "Contact Close"
break
case "extTmp":
desc = "External Temperature Threshold"
break
case "leakWat":
desc = "Water Dried"
break
}
input "${pName}SpeechOnRestore", "bool", title: "Speak when restoring HVAC on (${desc})?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png")
// TODO: There are more messages and errors than ON / OFF
input "${pName}UseCustomSpeechNotifMsg", "bool", title: "Customize Notitification Message?", required: false, defaultValue: (settings?."${pName}AllowSpeechNotif" ? false : true), submitOnChange: true,
image: getAppImg("speech_icon.png")
if(settings["${pName}UseCustomSpeechNotifMsg"]) {
getNotifVariables(pName)
input "${pName}CustomOffSpeechMessage", "text", title: "Turn Off Message?", required: false, defaultValue: state?."${pName}OffVoiceMsg" , submitOnChange: true, image: getAppImg("speech_icon.png")
state?."${pName}OffVoiceMsg" = settings?."${pName}CustomOffSpeechMessage"
if(settings?."${pName}CustomOffSpeechMessage") {
paragraph "Off Msg:\n" + voiceNotifString(state?."${pName}OffVoiceMsg",pName)
}
input "${pName}CustomOnSpeechMessage", "text", title: "Restore On Message?", required: false, defaultValue: state?."${pName}OnVoiceMsg", submitOnChange: true, image: getAppImg("speech_icon.png")
state?."${pName}OnVoiceMsg" = settings?."${pName}CustomOnSpeechMessage"
if(settings?."${pName}CustomOnSpeechMessage") {
paragraph "Restore On Msg:\n" + voiceNotifString(state?."${pName}OnVoiceMsg",pName)
}
} else {
state?."${pName}OffVoiceMsg" = ""
state?."${pName}OnVoiceMsg" = ""
}
}
}
}
//}
}
*/
if(allowAlarm && settings?."${pName}NotifOn") {
// section("Alarm/Siren Device Preferences:") {
input "${pName}AllowAlarmNotif", "bool", title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Enable Alarm | Siren?")), required: false, defaultValue: (settings?."${pName}AllowAlarmNotif" ? true : false), submitOnChange: true
if(settings["${pName}AllowAlarmNotif"]) {
input "${pName}AlarmDevices", "capability.alarm", title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Select Alarm/Siren(s)")), multiple: true, required: settings["${pName}AllowAlarmNotif"], submitOnChange: true
}
// }
}
if(pName in ["conWat", "leakWat", "extTmp", "watchDog"] && settings["${pName}NotifOn"] && settings["${pName}AllowAlarmNotif"] && settings["${pName}AlarmDevices"]) {
// section("Notification Alert Options (1):") {
input "${pName}_Alert_1_Delay", "enum", title: imgTitle(getAppImg("alert_icon2.png"), inputTitleStr("First Alert Delay (in minutes)")), defaultValue: null, required: true, submitOnChange: true, options: longTimeSecEnum()
if(settings?."${pName}_Alert_1_Delay") {
input "${pName}_Alert_1_AlarmType", "enum", title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Alarm Type to use?")), options: alarmActionsEnum(), defaultValue: null, submitOnChange: true, required: true
if(settings?."${pName}_Alert_1_AlarmType") {
input "${pName}_Alert_1_Alarm_Runtime", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Turn off Alarm After (in seconds)?")), options: shortTimeEnum(), defaultValue: 10, required: true, submitOnChange: true
}
}
// }
if(settings["${pName}_Alert_1_Delay"]) {
// section("Notification Alert Options (2):") {
input "${pName}_Alert_2_Delay", "enum", title: imgTitle(getAppImg("alert_icon2.png"), inputTitleStr("Second Alert Delay (in minutes)")), defaultValue: null, options: longTimeSecEnum(), required: false, submitOnChange: true
if(settings?."${pName}_Alert_2_Delay") {
input "${pName}_Alert_2_AlarmType", "enum", title: imgTitle(getAppImg("alarm_icon.png"), inputTitleStr("Alarm Type to use?")), options: alarmActionsEnum(), defaultValue: null, submitOnChange: true, required: true
if(settings?."${pName}_Alert_2_AlarmType") {
input "${pName}_Alert_2_Alarm_Runtime", "enum", title: imgTitle(getAppImg("i_dt"), inputTitleStr("Turn off Alarm After (in minutes)?")), options: shortTimeEnum(), defaultValue: 10, required: true, submitOnChange: true
}
}
// }
}
}
}
}
}
def setInitialVoiceMsgs(pName) {
/*
if(settings["${pName}AllowSpeechNotif"]) {
if(pName in ["conWat", "extTmp", "leakWat"]) {
if(pName == "leakWat") {
if(!state?."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) {
state?."${pName}OffVoiceMsg" = "ATTENTION: %devicename% has been turned OFF because %wetsensor% has reported it is WET" }
if(!state?."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) {
state?."${pName}OnVoiceMsg" = "Restoring %devicename% to %lastmode% Mode because ALL water sensors have been Dry again for (%ondelay%)" }
}
if(pName == "conWat") {
if(!state?."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) {
state?."${pName}OffVoiceMsg" = "ATTENTION: %devicename% has been turned OFF because %opencontact% has been Opened for (%offdelay%)" }
if(!state?."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) {
state?."${pName}OnVoiceMsg" = "Restoring %devicename% to %lastmode% Mode because ALL contacts have been Closed again for (%ondelay%)" }
}
if(pName == "extTmp") {
if(!state?."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) {
state?."${pName}OffVoiceMsg" = "ATTENTION: %devicename% has been turned to ECO because External Temp is above the temp threshold for (%offdelay%)" }
if(!state?."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) {
state?."${pName}OnVoiceMsg" = "Restoring %devicename% to %lastmode% Mode because External Temp has been above the temp threshold for (%ondelay%)" }
}
}
}
*/
}
//ERS
def setCustomVoice(pName) {
/*
if(settings["${pName}AllowSpeechNotif"]) {
if(pName in ["conWat", "extTmp", "leakWat"]) {
if(settings["${pName}UseCustomSpeechNotifMsg"]) {
state?."${pName}OffVoiceMsg" = settings?."${pName}CustomOffSpeechMessage"
state?."${pName}OnVoiceMsg" = settings?."${pName}CustomOnSpeechMessage"
}
}
}
*/
}
/*
def setNotificationTimePage(params) {
def pName = params?.pName
if(params?.pName) {
state.curNotifTimePageData = params
} else { pName = state?.curNotifTimePageData?.pName }
dynamicPage(name: "setNotificationTimePage", title: "Prevent Notifications\nDuring these Days, Times or Modes", uninstall: false) {
def timeReq = (settings["${pName}qStartTime"] || settings["${pName}qStopTime"]) ? true : false
section() {
input "${pName}qStartInput", "enum", title: "Starting at", options: ["A specific time", "Sunrise", "Sunset"], defaultValue: null, submitOnChange: true, required: false, image: getAppImg("start_time_icon.png")
if(settings["${pName}qStartInput"] == "A specific time") {
input "${pName}qStartTime", "time", title: "Start time", required: timeReq, image: getAppImg("start_time_icon.png")
}
input "${pName}qStopInput", "enum", title: "Stopping at", options: ["A specific time", "Sunrise", "Sunset"], defaultValue: null, submitOnChange: true, required: false, image: getAppImg("stop_time_icon.png")
if(settings?."${pName}qStopInput" == "A specific time") {
input "${pName}qStopTime", "time", title: "Stop time", required: timeReq, image: getAppImg("stop_time_icon.png")
}
input "${pName}quietDays", "enum", title: "Prevent during these days of the week", multiple: true, required: false, image: getAppImg("day_calendar_icon.png"), options: timeDayOfWeekOptions()
input "${pName}quietModes", "mode", title: "Prevent when these Modes are Active", multiple: true, submitOnChange: true, required: false, image: getAppImg("i_mod")
}
}
}
String getNotifSchedDesc(pName) {
def sun = getSunriseAndSunset()
def startInput = settings?."${pName}qStartInput"
def startTime = settings?."${pName}qStartTime"
def stopInput = settings?."${pName}qStopInput"
def stopTime = settings?."${pName}qStopTime"
def dayInput = settings?."${pName}quietDays"
def modeInput = settings?."${pName}quietModes"
def notifDesc = ""
if(settings?."${pName}UseParentNotifRestrictions" == false) {
def getNotifTimeStartLbl = ( (startInput == "Sunrise" || startInput == "Sunset") ? ( (startInput == "Sunset") ? epochToTime(sun?.sunset.time) : epochToTime(sun?.sunrise.time) ) : (startTime ? time2Str(startTime) : "") )
def getNotifTimeStopLbl = ( (stopInput == "Sunrise" || stopInput == "Sunset") ? ( (stopInput == "Sunset") ? epochToTime(sun?.sunset.time) : epochToTime(sun?.sunrise.time) ) : (stopTime ? time2Str(stopTime) : "") )
notifDesc += (getNotifTimeStartLbl && getNotifTimeStopLbl) ? "• Silent Time: ${getNotifTimeStartLbl} - ${getNotifTimeStopLbl}" : ""
def days = getInputToStringDesc(dayInput)
def modes = getInputToStringDesc(modeInput)
notifDesc += days ? "${(getNotifTimeStartLbl || getNotifTimeStopLbl) ? "\n" : ""}• Silent Day${isPluralString(dayInput)}: ${days}" : ""
notifDesc += modes ? "${(getNotifTimeStartLbl || getNotifTimeStopLbl || days) ? "\n" : ""}• Silent Mode${isPluralString(modeInput)}: ${modes}" : ""
} else {
notifDesc += "• Using Manager Restrictions"
}
return (notifDesc != "") ? "${notifDesc}" : null
}
def getOk2Notify(pName) {
return ((settings["${pName}NotifOn"] == true) && (daysOk(settings?."${pName}quietDays") == true) && (notificationTimeOk(pName) == true) && (modesOk(settings?."${pName}quietModes") == true))
}
def notificationTimeOk(pName) {
def strtTime = null
def stopTime = null
def now = new Date()
def sun = getSunriseAndSunset() // current based on geofence, previously was: def sun = getSunriseAndSunset(zipCode: zipCode)
if(settings?."${pName}qStartTime" && settings?."${pName}qStopTime") {
if(settings?."${pName}qStartInput" == "sunset") { strtTime = sun.sunset }
else if(settings?."${pName}qStartInput" == "sunrise") { strtTime = sun.sunrise }
else if(settings?."${pName}qStartInput" == "A specific time" && settings?."${pName}qStartTime") { strtTime = settings?."${pName}qStartTime" }
if(settings?."${pName}qStopInput" == "sunset") { stopTime = sun.sunset }
else if(settings?."${pName}qStopInput" == "sunrise") { stopTime = sun.sunrise }
else if(settings?."${pName}qStopInput" == "A specific time" && settings?."${pName}qStopTime") { stopTime = settings?."${pName}qStopTime" }
} else { return true }
if(strtTime && stopTime) {
return timeOfDayIsBetween(strtTime, stopTime, new Date(), getTimeZone()) ? false : true
} else { return true }
}
def getNotifVariables(pName) {
def str = ""
str += "\n • DeviceName: %devicename%"
str += "\n • Last Mode: %lastmode%"
str += (pName == "leakWat") ? "\n • Wet Water Sensor: %wetsensor%" : ""
str += (pName == "conWat") ? "\n • Open Contact: %opencontact%" : ""
str += (pName in ["conWat", "extTmp"]) ? "\n • Off Delay: %offdelay%" : ""
str += "\n • On Delay: %ondelay%"
str += (pName == "extTmp") ? "\n • Temp Threshold: %tempthreshold%" : ""
paragraph "These Variables are accepted: ${str}"
}
//process custom tokens to generate final voice message (Copied from BigTalker)
def voiceNotifString(phrase, pName) {
//LogTrace("conWatVoiceNotifString")
try {
if(phrase?.toLowerCase().contains("%devicename%")) { phrase = phrase?.toLowerCase().replace('%devicename%', (settings?."schMotTstat"?.displayName.toString() ?: "unknown")) }
if(phrase?.toLowerCase().contains("%lastmode%")) { phrase = phrase?.toLowerCase().replace('%lastmode%', (state?."${pName}RestoreMode".toString() ?: "unknown")) }
if(pName == "leakWat" && phrase?.toLowerCase().contains("%wetsensor%")) {
phrase = phrase?.toLowerCase().replace('%wetsensor%', (getWetWaterSensors(leakWatSensors) ? getWetWaterSensors(leakWatSensors)?.join(", ").toString() : "a selected leak sensor")) }
if(pName == "conWat" && phrase?.toLowerCase().contains("%opencontact%")) {
phrase = phrase?.toLowerCase().replace('%opencontact%', (getOpenContacts(conWatContacts) ? getOpenContacts(conWatContacts)?.join(", ").toString() : "a selected contact")) }
if(pName == "extTmp" && phrase?.toLowerCase().contains("%tempthreshold%")) {
phrase = phrase?.toLowerCase().replace('%tempthreshold%', "${extTmpDiffVal.toString()}(${tUnitStr()})") }
if(phrase?.toLowerCase().contains("%offdelay%")) { phrase = phrase?.toLowerCase().replace('%offdelay%', getEnumValue(longTimeSecEnum(), settings?."${pName}OffDelay").toString()) }
if(phrase?.toLowerCase().contains("%ondelay%")) { phrase = phrase?.toLowerCase().replace('%ondelay%', getEnumValue(longTimeSecEnum(), settings?."${pName}OnDelay").toString()) }
} catch (ex) {
log.error "voiceNotifString Exception:", ex
//parent?.sendExceptionData(ex, "voiceNotifString", true, getAutoType())
}
return phrase
}
*/
String getNotifConfigDesc(String pName) {
//LogTrace("getNotifConfigDesc pName: $pName")
String str = ""
if(settings?."${pName}NotifOn") {
// str += "Notification Status:"
// if(!getRecipientDesc(pName)) {
// str += "\n • Contacts: Using Manager Settings"
// }
String t0
if(settings?."${pName}UseMgrNotif" == false) {
str += (settings?."${pName}NotifPhones") ? "${str != "" ? "\n" : ""} • SMS: (${settings?."${pName}NotifPhones"?.size()})" : ""
str += (settings?."${pName}PushoverEnabled") ? "${str != "" ? "\n" : ""}Pushover: (Enabled)" : ""
str += (settings?."${pName}PushoverEnabled" && settings?."${pName}PushoverDevices") ? "${str != "" ? "\n" : ""} • Pushover Devices: (${settings?."${pName}PushoverDevices"})" : ""
//t0 = getNotifSchedDesc(pName)
//str += t0 ? "\n\nAlert Restrictions:\n${t0}" : ""
} else {
str += " • Enabled Using Manager Settings"
}
t0 = str
if(t0) {
str = "Notification Settings\n${t0}"
}
//t0 = getVoiceNotifConfigDesc(pName)
//str += t0 ? "\n\nVoice Status:${t0}" : ""
t0 = getAlarmNotifConfigDesc(pName)
str += t0 ? "\n\nAlarm Status:${t0}" : ""
t0 = getAlertNotifConfigDesc(pName)
str += t0 ? "\n\n${t0}" : ""
}
return (str != "") ? "${str}" : null
}
/*
def getVoiceNotifConfigDesc(pName) {
def str = ""
if(settings?."${pName}NotifOn" && settings["${pName}AllowSpeechNotif"]) {
def speaks = settings?."${pName}SpeechDevices"
def medias = settings?."${pName}SpeechMediaPlayer"
def echos = settings["${pName}EchoDevices"]
str += settings["${pName}SendToAskAlexaQueue"] ? "\n• Send to Ask Alexa: (True)" : ""
str += speaks ? "\n • Speech Devices:" : ""
if(speaks) {
def cnt = 1
speaks?.each { str += it ? "\n ${cnt < speaks.size() ? "├" : "└"} $it" : ""; cnt = cnt+1; }
}
str += echos ? "\n • Alexa Devices:" : ""
if(echos) {
def cnt = 1
echos?.each { str += it ? "\n ${cnt < echos.size() ? "├" : "└"} $it" : ""; cnt = cnt+1; }
str += (echos && settings?."${pName}SpeechVolumeLevel") ? "\n└ Volume: (${settings?."${pName}SpeechVolumeLevel"})" : ""
}
str += medias ? "${(speaks || echos) ? "\n\n" : "\n"} • Media Players:" : ""
if(medias) {
def cnt = 1
medias?.sort { it?.displayName }?.each { str += it ? "\n│${cnt < medias.size() ? "├" : "└"} $it" : ""; cnt = cnt+1; }
}
str += (medias && settings?."${pName}SpeechVolumeLevel") ? "\n├ Volume: (${settings?."${pName}SpeechVolumeLevel"})" : ""
str += (medias && settings?."${pName}SpeechAllowResume") ? "\n└ Resume: (${strCapitalize(settings?."${pName}SpeechAllowResume")})" : ""
str += (settings?."${pName}UseCustomSpeechNotifMsg" && (medias || speaks)) ? "\n• Custom Message: (${strCapitalize(settings?."${pName}UseCustomSpeechNotifMsg")})" : ""
}
return (str != "") ? "${str}" : null
}
*/
String getAlarmNotifConfigDesc(pName) {
String str = ""
if(settings?."${pName}NotifOn" && settings["${pName}AllowAlarmNotif"]) {
def alarms = getInputToStringDesc(settings["${pName}AlarmDevices"], true)
str += alarms ? "\n • Alarm Devices:${alarms.size() > 1 ? "\n" : ""}${alarms}" : ""
}
return (str != "") ? "${str}" : null
}
String getAlertNotifConfigDesc(pName) {
String str = ""
//TODO not sure we do all these
if(settings?."${pName}NotifOn" && (settings["${pName}_Alert_1_Delay"] || settings["${pName}_Alert_2_Delay"]) && (settings["${pName}AllowSpeechNotif"] || settings["${pName}AllowAlarmNotif"])) {
str += settings["${pName}_Alert_1_Delay"] ? "\nAlert (1) Status:\n • Delay: (${getEnumValue(longTimeSecEnum(), settings["${pName}_Alert_1_Delay"])})" : ""
// str += settings["${pName}_Alert_1_Send_Push"] ? "\n • Send Push: (${settings["${pName}_Alert_1_Send_Push"]})" : ""
// str += settings["${pName}_Alert_1_Use_Speech"] ? "\n • Use Speech: (${settings["${pName}_Alert_1_Use_Speech"]})" : ""
str += settings["${pName}_Alert_1_Use_Alarm"] ? "\n • Use Alarm: (${settings["${pName}_Alert_1_Use_Alarm"]})" : ""
str += (settings["${pName}_Alert_1_Use_Alarm"] && settings["${pName}_Alert_1_AlarmType"]) ? "\n ├ Alarm Type: (${getEnumValue(alarmActionsEnum(), settings["${pName}_Alert_1_AlarmType"])})" : ""
str += (settings["${pName}_Alert_1_Use_Alarm"] && settings["${pName}_Alert_1_Alarm_Runtime"]) ? "\n └ Alarm Runtime: (${getEnumValue(shortTimeEnum(), settings["${pName}_Alert_1_Alarm_Runtime"])})" : ""
str += settings["${pName}_Alert_2_Delay"] ? "${settings["${pName}_Alert_1_Delay"] ? "\n" : ""}\nAlert (2) Status:\n • Delay: (${getEnumValue(longTimeSecEnum(), settings["${pName}_Alert_2_Delay"])})" : ""
// str += settings["${pName}_Alert_2_Send_Push"] ? "\n • Send Push: (${settings["${pName}_Alert_2_Send_Push"]})" : ""
// str += settings["${pName}_Alert_2_Use_Speech"] ? "\n • Use Speech: (${settings["${pName}_Alert_2_Use_Speech"]})" : ""
str += settings["${pName}_Alert_2_Use_Alarm"] ? "\n • Use Alarm: (${settings["${pName}_Alert_2_Use_Alarm"]})" : ""
str += (settings["${pName}_Alert_2_Use_Alarm"] && settings["${pName}_Alert_2_AlarmType"]) ? "\n ├ Alarm Type: (${getEnumValue(alarmActionsEnum(), settings["${pName}_Alert_2_AlarmType"])})" : ""
str += (settings["${pName}_Alert_2_Use_Alarm"] && settings["${pName}_Alert_2_Alarm_Runtime"]) ? "\n └ Alarm Runtime: (${getEnumValue(shortTimeEnum(), settings["${pName}_Alert_2_Alarm_Runtime"])})" : ""
}
return (str != "") ? "${str}" : null
}
String getInputToStringDesc(inpt, addSpace = null) {
int cnt = 0
String str = ""
if(inpt) {
inpt.sort().each { item ->
cnt = cnt+1
str += item ? (((cnt < 1) || (inpt?.size() > 1)) ? "\n ${item}" : "${addSpace ? " " : ""}${item}") : ""
}
}
//log.debug "str: $str"
return (str != "") ? "${str}" : null
}
String isPluralString(obj) {
return (obj?.size() > 1) ? "(s)" : ""
}
/*
def getRecipientsNames(val) {
String n = ""
Integer i = 0
if(val) {
//log.debug "val: $val"
val?.each { r ->
i = i + 1
n += i == val?.size() ? "${r}" : "${r},"
}
}
return n?.toString().replaceAll("\\,", "\n")
}
def getRecipientDesc(pName) {
return (settings?."${pName}NotifPhones" || (settings?."${pName}PushoverEnabled" && settings?."${pName}PushoverDevices")) ? true : false
}
*/
def setDayModeTimePage1(params) {
String mpName = nModePrefix()
def t0 = [pName:"${mpName}" ]
return setDayModeTimePage( t0 )
}
def setDayModeTimePage2(params) {
String mpName = fanCtrlPrefix()
def t0 = [pName:"${mpName}" ]
return setDayModeTimePage( t0 )
}
def setDayModeTimePage3(params) {
String mpName = conWatPrefix()
def t0 = [pName:"${mpName}" ]
return setDayModeTimePage( t0 )
}
def setDayModeTimePage4(params) {
String mpName = humCtrlPrefix()
def t0 = [pName:"${mpName}" ]
return setDayModeTimePage( t0 )
}
def setDayModeTimePage5(params) {
String mpName = extTmpPrefix()
def t0 = [pName:"${mpName}" ]
return setDayModeTimePage( t0 )
}
def setDayModeTimePage(params) {
String pName = params?.pName
if(params?.pName) {
state.t_setDayData = params
} else {
pName = state?.t_setDayData?.pName
}
dynamicPage(name: "setDayModeTimePage", title: "Select Days, Times or Modes", uninstall: false) {
String secDesc = settings["${pName}DmtInvert"] ? "Not" : "Only"
boolean inverted = settings["${pName}DmtInvert"] ? true : false
section("") {
String actIcon = settings?."${pName}DmtInvert" ? "inactive" : "active"
input "${pName}DmtInvert", "bool", title: imgTitle(getAppImg("${actIcon}_icon.png"), inputTitleStr("${secDesc} in These? (tap to invert)")), defaultValue: false, submitOnChange: true
}
section("${secDesc} During these Days, Times, or Modes:") {
boolean timeReq = (settings?."${pName}StartTime" || settings."${pName}StopTime") ? true : false
input "${pName}StartTime", "time", title: imgTitle(getAppImg("start_time_icon.png"), inputTitleStr("Start time")), required: timeReq
input "${pName}StopTime", "time", title: imgTitle(getAppImg("stop_time_icon.png"), inputTitleStr("Stop time")), required: timeReq
input "${pName}Days", "enum", title: imgTitle(getAppImg("day_calendar_icon2.png"), inputTitleStr("${inverted ? "Not": "Only"} These Days")), multiple: true, required: false, options: timeDayOfWeekOptions()
input "${pName}Modes", "mode", title: imgTitle(getAppImg("i_mod"), inputTitleStr("${inverted ? "Not": "Only"} in These Modes")), multiple: true, required: false
}
section("Switches:") {
input "${pName}rstrctSWOn", "capability.switch", title: imgTitle(getAppImg("i_sw"), inputTitleStr("Only execute when these switches are all ON")), multiple: true, required: false
input "${pName}rstrctSWOff", "capability.switch", title: imgTitle(getAppImg("switch_off_icon.png"), inputTitleStr("Only execute when these switches are all OFF")), multiple: true, required: false
}
}
}
String getDayModeTimeDesc(String pName) {
def startTime = settings?."${pName}StartTime"
def stopTime = settings?."${pName}StopTime"
def dayInput = settings?."${pName}Days"
def modeInput = settings?."${pName}Modes"
def inverted = settings?."${pName}DmtInvert" ?: null
def swOnInput = settings?."${pName}rstrctSWOn"
def swOffInput = settings?."${pName}rstrctSWOff"
String str = ""
String days = getInputToStringDesc(dayInput)
String modes = getInputToStringDesc(modeInput)
String swOn = getInputToStringDesc(swOnInput)
String swOff = getInputToStringDesc(swOffInput)
str += ((startTime && stopTime) || modes || days) ? "${!inverted ? "When" : "When Not"}:" : ""
str += (startTime && stopTime) ? "\n • Time: ${time2Str(settings?."${pName}StartTime")} - ${time2Str(settings?."${pName}StopTime")}" : ""
str += days ? "${(startTime && stopTime) ? "\n" : ""}\n • Day${isPluralString(dayInput)}: ${days}" : ""
str += modes ? "${((startTime && stopTime) || days) ? "\n" : ""}\n • Mode${isPluralString(modeInput)}: ${modes}" : ""
str += swOn ? "${((startTime && stopTime) || days || modes) ? "\n" : ""}\n • Switch${isPluralString(swOnInput)} that must be on: ${getRestSwitch(swOnInput)}" : ""
str += swOff ? "${((startTime && stopTime) || days || modes || swOn) ? "\n" : ""}\n • Switch${isPluralString(swOffInput)} that must be off: ${getRestSwitch(swOffInput)}" : ""
str += (str != "") ? descriptions("d_ttm") : ""
return str
}
String getRestSwitch(swlist) {
String swDesc = ""
int swCnt = 0
int rmSwCnt = swlist?.size() ?: 0
swlist?.sort { it?.displayName }?.each { sw ->
swCnt = swCnt+1
swDesc += "${swCnt >= 1 ? "${swCnt == rmSwCnt ? "\n └" : "\n ├"}" : "\n └"} ${sw?.label}: (${strCapitalize(sw?.currentSwitch)})"
}
return (swDesc == "") ? null : "${swDesc}"
}
String getDmtSectionDesc(String autoType) {
return settings["${autoType}DmtInvert"] ? "Do Not Act During these Days, Times, or Modes:" : "Only Act During these Days, Times, or Modes:"
//TODO add switches to adjust schedule
}
/************************************************************************************************
| AUTOMATION SCHEDULE CHECK |
*************************************************************************************************/
boolean autoScheduleOk(autoType) {
try {
boolean inverted = settings?."${autoType}DmtInvert" ? true : false
boolean modeOk = true
modeOk = (!settings?."${autoType}Modes" || ((isInMode(settings?."${autoType}Modes") && !inverted) || (!isInMode(settings?."${autoType}Modes") && inverted))) ? true : false
//dayOk
boolean dayOk = true
def dayFmt = new SimpleDateFormat("EEEE")
dayFmt.setTimeZone(getTimeZone())
def today = dayFmt.format(new Date())
boolean inDay = (today in settings?."${autoType}Days") ? true : false
dayOk = (!settings?."${autoType}Days" || ((inDay && !inverted) || (!inDay && inverted))) ? true : false
//scheduleTimeOk
boolean timeOk = true
if(settings?."${autoType}StartTime" && settings?."${autoType}StopTime") {
def st1 = timeToday(settings?."${autoType}StartTime", getTimeZone())
def end1 = timeToday(settings?."${autoType}StopTime", getTimeZone())
//def inTime = (timeOfDayIsBetween(settings?."${autoType}StartTime", settings?."${autoType}StopTime", new Date(), getTimeZone())) ? true : false
boolean inTime = (timeOfDayIsBetween(st1, end1, new Date(), getTimeZone())) ? true : false
timeOk = ((inTime && !inverted) || (!inTime && inverted)) ? true : false
}
boolean soFarOk = (modeOk && dayOk && timeOk) ? true : false
boolean swOk = true
if(soFarOk && settings?."${autoType}rstrctSWOn") {
for(sw in settings["${autoType}rstrctSWOn"]) {
if (sw.currentValue("switch") != "on") {
swOk = false
break
}
}
}
soFarOk = (modeOk && dayOk && timeOk && swOk) ? true : false
if(soFarOk && settings?."${autoType}rstrctSWOff") {
for(sw in settings["${autoType}rstrctSWOff"]) {
if (sw.currentValue("switch") != "off") {
swOk = false
break
}
}
}
LogAction("autoScheduleOk( dayOk: $dayOk | modeOk: $modeOk | dayOk: ${dayOk} | timeOk: $timeOk | swOk: $swOk | inverted: ${inverted})", "info", false)
return (modeOk && dayOk && timeOk && swOk) ? true : false
} catch (ex) {
log.error "${autoType}-autoScheduleOk Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "autoScheduleOk", true, getAutoType())
}
}
/************************************************************************************************
| SEND NOTIFICATIONS VIA PARENT APP |
*************************************************************************************************/
void sendNofificationMsg(String msg, String msgType, String pName, lvl=null, pusho=null, sms=null) {
LogAction("sendNofificationMsg($msg, $msgType, $pName, $sms, $pusho)", "debug", false)
if(settings?."${pName}NotifOn" == true) {
int nlvl = lvl ?: (sms || pusho) ? 5 : 4
if(settings?."${pName}UseMgrNotif" == false) {
def mySms = sms ?: settings?."${pName}NotifPhones"
if(mySms) {
parent?.sendMsg(msgType, msg, nlvl, null, mySms)
}
if(pusho && settings?."${pName}PushoverDevices") {
parent?.sendMsg(msgType, msg, nlvl, settings?."${pName}PushoverDevices")
}
} else {
parent?.sendMsg(msgType, msg, nlvl)
}
} else {
LogAction("sendMsg: Message Skipped as notifications off ($msg)", "info", true)
}
}
/************************************************************************************************
| GLOBAL Code | Logging AND Diagnostic |
*************************************************************************************************/
void sendEventPushNotifications(String message, String type, String pName) {
LogTrace("sendEventPushNotifications($message, $type, $pName)")
sendNofificationMsg(message, type, pName)
}
/*
def sendEventVoiceNotifications(vMsg, pName, msgId, rmAAMsg=false, rmMsgId) {
def allowNotif = settings?."${pName}NotifOn" ? true : false
def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
def ok2Notify = setting?."${pName}UseParentNotifRestrictions" != false ? getOk2Notify(pName) : getOk2Notify(pName) //parent?.getOk2Notify()
LogAction("sendEventVoiceNotifications($vMsg, $pName) | ok2Notify: $ok2Notify", "info", false)
if(allowNotif && allowSpeech) {
if(ok2Notify && (settings["${pName}SpeechDevices"] || settings["${pName}SpeechMediaPlayer"] || settings["${pName}EchoDevices"])) {
sendTTS(vMsg, pName)
}
if(settings["${pName}SendToAskAlexaQueue"]) { // we queue to Alexa regardless of quiet times
if(rmMsgId != null && rmAAMsg == true) {
removeAskAlexaQueueMsg(rmMsgId)
}
if (vMsg && msgId != null) {
addEventToAskAlexaQueue(vMsg, msgId)
}
}
}
}
*/
/*
def addEventToAskAlexaQueue(vMsg, msgId, queue=null) {
if(false) { //parent?.getAskAlexaMQEn() == true) {
if(parent.getAskAlexaMultiQueueEn()) {
LogAction("sendEventToAskAlexaQueue: Adding this Message to the Ask Alexa Queue ($queues): ($vMsg)|${msgId}", "info", true)
sendLocationEvent(name: "AskAlexaMsgQueue", value: "${app?.label}", isStateChange: true, descriptionText: "${vMsg}", unit: "${msgId}", data:queues)
} else {
LogAction("sendEventToAskAlexaQueue: Adding this Message to the Ask Alexa Queue: ($vMsg)|${msgId}", "info", true)
sendLocationEvent(name: "AskAlexaMsgQueue", value: "${app?.label}", isStateChange: true, descriptionText: "${vMsg}", unit: "${msgId}")
}
}
}
def removeAskAlexaQueueMsg(msgId, queue=null) {
if(false) { //parent?.getAskAlexaMQEn() == true) {
if(parent.getAskAlexaMultiQueueEn()) {
LogAction("removeAskAlexaQueueMsg: Removing Message ID (${msgId}) from the Ask Alexa Queue ($queues)", "info", true)
sendLocationEvent(name: "AskAlexaMsgQueueDelete", value: "${app?.label}", isStateChange: true, unit: msgId, data: queues)
} else {
LogAction("removeAskAlexaQueueMsg: Removing Message ID (${msgId}) from the Ask Alexa Queue", "info", true)
sendLocationEvent(name: "AskAlexaMsgQueueDelete", value: "${app?.label}", isStateChange: true, unit: msgId)
}
}
}
*/
def scheduleAlarmOn(String autoType) {
LogAction("scheduleAlarmOn: autoType: $autoType a1DelayVal: ${getAlert1DelayVal(autoType)}", "debug", false)
int timeVal = getAlert1DelayVal(autoType).toInteger()
boolean ok2Notify = true //setting?."${autoType}UseParentNotifRestrictions" != false ? getOk2Notify(autoType) : getOk2Notify(autoType) //parent?.getOk2Notify()
LogAction("scheduleAlarmOn timeVal: $timeVal ok2Notify: $ok2Notify", "info", false)
if(ok2Notify) {
if(timeVal > 0) {
runIn(timeVal, "alarm0FollowUp", [data: [autoType: autoType]])
LogAction("scheduleAlarmOn: Scheduling Alarm Followup 0 in timeVal: $timeVal", "info", false)
state."${autoType}AlarmActive" = true
} else { LogAction("scheduleAlarmOn: Did not schedule ANY operation timeVal: $timeVal", "error", true) }
} else { LogAction("scheduleAlarmOn: Could not schedule operation timeVal: $timeVal", "error", true) }
}
void alarm0FollowUp(val) {
String autoType = val.autoType
LogAction("alarm0FollowUp: autoType: $autoType 1 OffVal: ${getAlert1AlarmEvtOffVal(autoType)}", "debug", false)
int timeVal = getAlert1AlarmEvtOffVal(autoType).toInteger()
LogAction("alarm0FollowUp timeVal: $timeVal", "info", false)
if(timeVal > 0 && sendEventAlarmAction(1, autoType)) {
runIn(timeVal, "alarm1FollowUp", [data: [autoType: autoType]])
LogAction("alarm0FollowUp: Scheduling Alarm Followup 1 in timeVal: $timeVal", "info", false)
} else { LogAction ("alarm0FollowUp: Could not schedule operation timeVal: $timeVal", "error", true) }
}
void alarm1FollowUp(val) {
String autoType = val.autoType
LogAction("alarm1FollowUp autoType: $autoType a2DelayVal: ${getAlert2DelayVal(autoType)}", "debug", false)
def aDev = settings["${autoType}AlarmDevices"]
if(aDev) {
aDev?.off()
storeLastAction("Set Alarm OFF", getDtNow(), "")
LogAction("alarm1FollowUp: Turning OFF ${aDev}", "info", false)
}
int timeVal = getAlert2DelayVal(autoType).toInteger()
//if(canSchedule() && (settings["${autoType}_Alert_2_Use_Alarm"] && timeVal > 0)) {
if(timeVal > 0) {
runIn(timeVal, "alarm2FollowUp", [data: [autoType: autoType]])
LogAction("alarm1FollowUp: Scheduling Alarm Followup 2 in timeVal: $timeVal", "info", false)
} else { LogAction ("alarm1FollowUp: Could not schedule operation timeVal: $timeVal", "error", true) }
}
void alarm2FollowUp(val) {
String autoType = val.autoType
LogAction("alarm2FollowUp: autoType: $autoType 2 OffVal: ${getAlert2AlarmEvtOffVal(autoType)}", "debug", false)
int timeVal = getAlert2AlarmEvtOffVal(autoType)
if(timeVal > 0 && sendEventAlarmAction(2, autoType)) {
runIn(timeVal, "alarm3FollowUp", [data: [autoType: autoType]])
LogAction("alarm2FollowUp: Scheduling Alarm Followup 3 in timeVal: $timeVal", "info", false)
} else { LogAction ("alarm2FollowUp: Could not schedule operation timeVal: $timeVal", "error", true) }
}
void alarm3FollowUp(val) {
String autoType = val.autoType
LogAction("alarm3FollowUp: autoType: $autoType", "debug", false)
def aDev = settings["${autoType}AlarmDevices"]
if(aDev) {
aDev?.off()
storeLastAction("Set Alarm OFF", getDtNow(), "")
LogAction("alarm3FollowUp: Turning OFF ${aDev}", "info", false)
}
state."${autoType}AlarmActive" = false
}
def alarmEvtSchedCleanup(String autoType) {
if(state?."${autoType}AlarmActive") {
LogAction("Cleaning Up Alarm Event Schedules autoType: $autoType", "info", false)
def items = ["alarm0FollowUp","alarm1FollowUp", "alarm2FollowUp", "alarm3FollowUp"]
items.each {
unschedule("$it")
}
def val = [ autoType: autoType ]
alarm3FollowUp(val)
}
}
boolean sendEventAlarmAction(evtNum, autoType) {
LogAction("sendEventAlarmAction evtNum: $evtNum autoType: $autoType", "info", false)
try {
boolean resval = false
boolean allowNotif = settings?."${autoType}NotifOn" ? true : false
boolean allowAlarm = allowNotif && settings?."${autoType}AllowAlarmNotif" ? true : false
def aDev = settings["${autoType}AlarmDevices"]
if(allowNotif && allowAlarm && aDev) {
//if(settings["${autoType}_Alert_${evtNum}_Use_Alarm"]) {
resval = true
def alarmType = settings["${autoType}_Alert_${evtNum}_AlarmType"].toString()
switch (alarmType) {
case "both":
state?."${autoType}alarmEvt${evtNum}StartDt" = getDtNow()
aDev?.both()
storeLastAction("Set Alarm BOTH ON", getDtNow(), autoType)
break
case "siren":
state?."${autoType}alarmEvt${evtNum}StartDt" = getDtNow()
aDev?.siren()
storeLastAction("Set Alarm SIREN ON", getDtNow(), autoType)
break
case "strobe":
state?."${autoType}alarmEvt${evtNum}StartDt" = getDtNow()
aDev?.strobe()
storeLastAction("Set Alarm STROBE ON", getDtNow(), autoType)
break
default:
resval = false
break
}
//}
}
} catch (ex) {
log.error "sendEventAlarmAction Exception: ($evtNum) - ${ex?.message}"
//parent?.sendExceptionData(ex, "sendEventAlarmAction", true, getAutoType())
}
return resval
}
def alarmAlertEvt(evt) {
LogAction("alarmAlertEvt: ${evt.displayName} Alarm State is Now (${evt.value})", "debug", false)
}
int getAlert1DelayVal(String autoType) { return !settings["${autoType}_Alert_1_Delay"] ? 300 : (settings["${autoType}_Alert_1_Delay"].toInteger()) }
int getAlert2DelayVal(String autoType) { return !settings["${autoType}_Alert_2_Delay"] ? 300 : (settings["${autoType}_Alert_2_Delay"].toInteger()) }
int getAlert1AlarmEvtOffVal(autoType) { return !settings["${autoType}_Alert_1_Alarm_Runtime"] ? 10 : (settings["${autoType}_Alert_1_Alarm_Runtime"].toInteger()) }
int getAlert2AlarmEvtOffVal(autoType) { return !settings["${autoType}_Alert_2_Alarm_Runtime"] ? 10 : (settings["${autoType}_Alert_2_Alarm_Runtime"].toInteger()) }
/*
def getAlarmEvt1RuntimeDtSec() { return !state?.alarmEvt1StartDt ? 100000 : GetTimeDiffSeconds(state?.alarmEvt1StartDt).toInteger() }
def getAlarmEvt2RuntimeDtSec() { return !state?.alarmEvt2StartDt ? 100000 : GetTimeDiffSeconds(state?.alarmEvt2StartDt).toInteger() }
*/
/*
void sendTTS(txt, pName) {
LogAction("sendTTS(data: ${txt})", "debug", false)
try {
def msg = txt?.toString()?.replaceAll("\\[|\\]|\\(|\\)|\\'|\\_", "")
def spks = settings?."${pName}SpeechDevices"
def meds = settings?."${pName}SpeechMediaPlayer"
def echos = settings?."${pName}EchoDevices"
def res = settings?."${pName}SpeechAllowResume"
def vol = settings?."${pName}SpeechVolumeLevel"
LogAction("sendTTS msg: $msg | speaks: $spks | medias: $meds | echos: $echos| resume: $res | volume: $vol", "debug", false)
if(settings?."${pName}AllowSpeechNotif") {
if(spks) {
spks*.speak(msg)
}
if(meds) {
meds?.each {
if(res) {
def currentStatus = it.latestValue('status')
def currentTrack = it.latestState("trackData")?.jsonValue
def currentVolume = it.latestState("level")?.integerValue ? it.currentState("level")?.integerValue : 0
if(vol) {
it?.playTextAndResume(msg, vol?.toInteger())
} else {
it?.playTextAndResume(msg)
}
}
else {
it?.playText(msg)
}
}
}
if(echos) {
echos*.setVolumeAndSpeak(settings?."${pName}SpeechVolumeLevel", msg as String)
}
}
} catch (ex) {
log.error "sendTTS Exception:", ex
//parent?.sendExceptionData(ex, "sendTTS", true, getAutoType())
}
}
*/
def scheduleTimeoutRestore(String pName) {
int timeOutVal = settings["${pName}OffTimeout"]?.toInteger()
if(timeOutVal && !state?."${pName}TimeoutScheduled") {
runIn(timeOutVal.toInteger(), "restoreAfterTimeOut", [data: [pName:pName]])
LogAction("Mode Restoration Timeout Scheduled ${pName} (${getEnumValue(longTimeSecEnum(), settings?."${pName}OffTimeout")})", "info", true)
state."${pName}TimeoutScheduled" = true
}
}
def unschedTimeoutRestore(String pName) {
int timeOutVal = settings["${pName}OffTimeout"]?.toInteger()
if(timeOutVal && state?."${pName}TimeoutScheduled") {
unschedule("restoreAfterTimeOut")
LogAction("Cancelled Scheduled Mode Restoration Timeout ${pName}", "info", false)
}
state."${pName}TimeoutScheduled" = false
}
def restoreAfterTimeOut(val) {
String pName = val?.pName.value
if(pName && settings?."${pName}OffTimeout") {
switch(pName) {
case "conWat":
state."${pName}TimeoutScheduled" = false
conWatCheck(true)
break
//case "leakWat":
//leakWatCheck(true)
//break
case "extTmp":
state."${pName}TimeoutScheduled" = false
extTmpTempCheck(true)
break
default:
LogAction("restoreAfterTimeOut no pName match ${pName}", "error", true)
break
}
}
}
boolean checkThermostatDupe(tstatOne, tstatTwo) {
boolean result = false
if(tstatOne && tstatTwo) {
String pTstat = tstatOne?.deviceNetworkId.toString()
def mTstatAr = []
tstatTwo?.each { ts ->
mTstatAr << ts?.deviceNetworkId.toString()
}
if(pTstat in mTstatAr) { return true }
}
return result
}
boolean checkModeDuplication(modeOne, modeTwo) {
boolean result = false
if(modeOne && modeTwo) {
modeOne?.each { dm ->
if(dm in modeTwo) {
result = true
}
}
}
return result
}
private getDeviceSupportedCommands(dev) {
return dev?.supportedCommands.findAll { it as String }
}
boolean checkFanSpeedSupport(dev) {
def req = ["setSpeed"]
int devCnt = 0
def devData = getDeviceSupportedCommands(dev)
devData.each { cmd ->
if(cmd.name in req) { devCnt = devCnt+1 }
}
def t0 = dev?.currentSpeed
def speed = t0 ?: null
//log.debug "checkFanSpeedSupport (speed: $speed | devCnt: $devCnt)"
return (speed && devCnt == 1) ? true : false
}
void getTstatCapabilities(tstat, String autoType, boolean dyn = false) {
try {
boolean canCool = true
boolean canHeat = true
boolean hasFan = true
if(tstat?.currentCanCool) { canCool = tstat?.currentCanCool.toBoolean() }
if(tstat?.currentCanHeat) { canHeat = tstat?.currentCanHeat.toBoolean() }
if(tstat?.currentHasFan) { hasFan = tstat?.currentHasFan.toBoolean() }
state?."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : ""}TstatCanCool" = canCool
state?."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : ""}TstatCanHeat" = canHeat
state?."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : ""}TstatHasFan" = hasFan
} catch (ex) {
log.error "getTstatCapabilities Exception: ${ex?.message}"
//parent?.sendExceptionData(ex, "getTstatCapabilities", true, getAutoType())
}
}
def getSafetyTemps(tstat, usedefault=true) {
def minTemp = tstat?.currentSafetyTempMin?.doubleValue
def maxTemp = tstat?.currentSafetyTempMax?.doubleValue
if(minTemp == 0) {
if(usedefault) { minTemp = (getTemperatureScale() == "C") ? 7 : 45 }
else { minTemp = null }
}
if(maxTemp == 0) { maxTemp = null }
if(minTemp || maxTemp) {
return ["min":minTemp, "max":maxTemp]
}
return null
}
def getComfortDewpoint(tstat, usedefault=true) {
def maxDew = tstat?.currentComfortDewpointMax?.doubleValue
maxDew = maxDew ?: 0.0
if(maxDew == 0.0) {
if(usedefault) {
maxDew = (getTemperatureScale() == "C") ? 19 : 66
return maxDew.toDouble()
}
return null
}
return maxDew
}
boolean getSafetyTempsOk(tstat) {
def sTemps = getSafetyTemps(tstat)
//log.debug "sTempsOk: $sTemps"
if(sTemps) {
def curTemp = tstat?.currentTemperature?.toDouble()
//log.debug "curTemp: ${curTemp}"
if( ((sTemps?.min != null && sTemps?.min.toDouble() != 0) && (curTemp < sTemps?.min.toDouble())) || ((sTemps?.max != null && sTemps?.max?.toDouble() != 0) && (curTemp > sTemps?.max?.toDouble())) ) {
return false
}
} // else { log.debug "getSafetyTempsOk: no safety Temps" }
return true
}
def getGlobalDesiredHeatTemp() {
Double t0 = null //parent?.settings?.locDesiredHeatTemp?.toDouble()
return t0 ?: null
}
def getGlobalDesiredCoolTemp() {
Double t0 = null // parent?.settings?.locDesiredCoolTemp?.toDouble()
return t0 ?: null
}
def getClosedContacts(contacts) {
if(contacts) {
def cnts = contacts?.findAll { it?.currentContact == "closed" }
return cnts ?: null
}
return null
}
def getOpenContacts(contacts) {
if(contacts) {
def cnts = contacts?.findAll { it?.currentContact == "open" }
return cnts ?: null
}
return null
}
def getDryWaterSensors(sensors) {
if(sensors) {
def cnts = sensors?.findAll { it?.currentWater == "dry" }
return cnts ?: null
}
return null
}
def getWetWaterSensors(sensors) {
if(sensors) {
def cnts = sensors?.findAll { it?.currentWater == "wet" }
return cnts ?: null
}
return null
}
boolean isContactOpen(con) {
boolean res = false
if(con) {
if(con?.currentSwitch == "on") { res = true }
}
return res
}
boolean isSwitchOn(dev) {
boolean res = false
if(dev) {
dev?.each { d ->
if(d?.currentSwitch == "on") { res = true }
}
}
return res
}
boolean isPresenceHome(presSensor) {
boolean res = false
if(presSensor) {
presSensor?.each { d ->
if(d?.currentPresence == "present") { res = true }
}
}
return res
}
boolean isSomebodyHome(sensors) {
if(sensors) {
def cnts = sensors?.findAll { it?.currentPresence == "present" }
return cnts ? true : false
}
return false
}
String getTstatPresence(tstat) {
String pres = "not present"
if(tstat) { pres = tstat?.currentPresence }
return pres
}
boolean setTstatMode(tstat, String mode, String autoType=null) {
boolean result = false
if(mode && tstat) {
String curMode = tstat?.currentThermostatMode?.toString()
if (curMode != mode) {
try {
if(mode == "auto") { tstat.auto(); result = true }
else if(mode == "heat") { tstat.heat(); result = true }
else if(mode == "cool") { tstat.cool(); result = true }
else if(mode == "off") { tstat.off(); result = true }
else {
if(mode == "eco") {
tstat.eco(); result = true
LogTrace("setTstatMode mode action | type: $autoType")
// if(autoType) { sendEcoActionDescToDevice(tstat, autoType) } // THIS ONLY WORKS ON NEST THERMOSTATS
}
}
}
catch (ex) {
log.error "setTstatMode() Exception: ${tstat?.label} does not support mode ${mode}; check IDE and install instructions ${ex?.message}"
//parent?.sendExceptionData(ex, "setTstatMode", true, getAutoType())
}
}
if(result) { LogAction("setTstatMode: '${tstat?.label}' Mode set to (${strCapitalize(mode)})", "info", false) }
else { LogAction("setTstatMode() | No Mode change: ${mode}", "info", false) }
} else {
LogAction("setTstatMode() | Invalid or Missing Mode received: ${mode}", "warn", true)
}
return result
}
boolean setMultipleTstatMode(tstats, String mode, String autoType=null) {
boolean result = false
if(tstats && mode) {
tstats?.each { ts ->
boolean retval
// try {
retval = setTstatMode(ts, mode, autoType)
// } catch (ex) {
// log.error "setMultipleTstatMode() Exception:", ex
// parent?.sendExceptionData(ex, "setMultipleTstatMode", true, getAutoType())
// }
if(retval) {
LogAction("Setting ${ts?.displayName} Mode to (${mode})", "info", false)
storeLastAction("Set ${ts?.displayName} to (${mode})", getDtNow(), autoType)
result = true
} else {
LogAction("Failed Setting ${ts} Mode to (${mode})", "warn", true)
return false
}
}
} else {
LogAction("setMultipleTstatMode(${tstats}, $mode, $autoType) | Invalid or Missing tstats or Mode received: ${mode}", "warn", true)
}
return result
}
def setTstatAutoTemps(tstat, coolSetpoint, heatSetpoint, pName, mir=null) {
boolean retVal = false
String setStr = "No thermostat device"
String tStr = "setTstatAutoTemps: [tstat: ${tstat?.displayName} | Mode: ${hvacMode} | coolSetpoint: ${coolSetpoint}${tempScaleStr} | heatSetpoint: ${heatSetpoint}${tempScaleStr}] "
def heatFirst
def setHeat
def setCool
String hvacMode = "unknown"
def reqCool
def reqHeat
def curCoolSetpoint
def curHeatSetpoint
String tempScaleStr = "${tUnitStr()}"
if(tstat) {
hvacMode = tstat?.currentThermostatMode.toString()
LogTrace(tStr)
retVal = true
setStr = "Error: "
curCoolSetpoint = getTstatSetpoint(tstat, "cool")
curHeatSetpoint = getTstatSetpoint(tstat, "heat")
def diff = getTemperatureScale() == "C" ? 2.0 : 3.0
reqCool = coolSetpoint?.toDouble() ?: null
reqHeat = heatSetpoint?.toDouble() ?: null
if(!reqCool && !reqHeat) { retVal = false; setStr += "Missing COOL and HEAT Setpoints" }
if(hvacMode in ["auto"]) {
if(!reqCool && reqHeat) { reqCool = (double) ((curCoolSetpoint > (reqHeat + diff)) ? curCoolSetpoint : (reqHeat + diff)) }
if(!reqHeat && reqCool) { reqHeat = (double) ((curHeatSetpoint < (reqCool - diff)) ? curHeatSetpoint : (reqCool - diff)) }
if((reqCool && reqHeat) && (reqCool >= (reqHeat + diff))) {
if(reqHeat <= curHeatSetpoint) { heatFirst = true }
else if(reqCool >= curCoolSetpoint) { heatFirst = false }
else if(reqHeat > curHeatSetpoint) { heatFirst = false }
else { heatFirst = true }
if(heatFirst) {
if(reqHeat != curHeatSetpoint) { setHeat = true }
if(reqCool != curCoolSetpoint) { setCool = true }
} else {
if(reqCool != curCoolSetpoint) { setCool = true }
if(reqHeat != curHeatSetpoint) { setHeat = true }
}
} else {
setStr += " or COOL/HEAT is not separated by ${diff}"
retVal = false
}
} else if(hvacMode in ["cool"] && reqCool) {
if(reqCool != curCoolSetpoint) { setCool = true }
} else if(hvacMode in ["heat"] && reqHeat) {
if(reqHeat != curHeatSetpoint) { setHeat = true }
} else {
setStr += "incorrect HVAC Mode (${hvacMode}"
retVal = false
}
}
if(retVal) {
setStr = "Setting: "
if(heatFirst && setHeat) {
setStr += "heatSetpoint: (${reqHeat}${tempScaleStr}) "
if(reqHeat != curHeatSetpoint) {
tstat?.setHeatingSetpoint(reqHeat)
storeLastAction("Set ${tstat} Heat Setpoint ${reqHeat}${tempScaleStr}", getDtNow(), pName, tstat)
if(mir) { mir*.setHeatingSetpoint(reqHeat) }
}
}
if(setCool) {
setStr += "coolSetpoint: (${reqCool}${tempScaleStr}) "
if(reqCool != curCoolSetpoint) {
tstat?.setCoolingSetpoint(reqCool)
storeLastAction("Set ${tstat} Cool Setpoint ${reqCool}", getDtNow(), pName, tstat)
if(mir) { mir*.setCoolingSetpoint(reqCool) }
}
}
if(!heatFirst && setHeat) {
setStr += "heatSetpoint: (${reqHeat}${tempScaleStr})"
if(reqHeat != curHeatSetpoint) {
tstat?.setHeatingSetpoint(reqHeat)
storeLastAction("Set ${tstat} Heat Setpoint ${reqHeat}${tempScaleStr}", getDtNow(), pName, tstat)
if(mir) { mir*.setHeatingSetpoint(reqHeat) }
}
}
//LogAction("setTstatAutoTemps() | Setting tstat [${tstat?.displayName} | mode: (${hvacMode}) | ${setStr}]", "info", false)
} else {
//LogAction("setTstatAutoTemps() | Setting tstat [${tstat?.displayName} | mode: (${hvacMode}) | ${setStr}]", "warn", true)
}
LogAction(tStr + setStr, retVal ? "info" : "warn", true)
//LogAction("setTstatAutoTemps() | Setting tstat [${tstat?.displayName} | mode: (${hvacMode}) | ${setStr}]", retVal ? "info" : "warn", true)
return retVal
}
/******************************************************************************
* Keep These Methods *
*******************************************************************************/
/*
def switchEnumVals() { return [0:"Off", 1:"On", 2:"On/Off"] }
def longTimeMinEnum() {
def vals = [
1:"1 Minute", 2:"2 Minutes", 3:"3 Minutes", 4:"4 Minutes", 5:"5 Minutes", 10:"10 Minutes", 15:"15 Minutes", 20:"20 Minutes", 25:"25 Minutes", 30:"30 Minutes",
45:"45 Minutes", 60:"1 Hour", 120:"2 Hours", 240:"4 Hours", 360:"6 Hours", 720:"12 Hours", 1440:"24 Hours"
]
return vals
}
*/
def fanTimeSecEnum() {
def vals = [
60:"1 Minute", 120:"2 Minutes", 180:"3 Minutes", 240:"4 Minutes", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes"
]
return vals
}
def longTimeSecEnum() {
def vals = [
0:"Off", 60:"1 Minute", 120:"2 Minutes", 180:"3 Minutes", 240:"4 Minutes", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes",
1800:"30 Minutes", 2700:"45 Minutes", 3600:"1 Hour", 7200:"2 Hours", 14400:"4 Hours", 21600:"6 Hours", 43200:"12 Hours", 86400:"24 Hours", 10:"10 Seconds(Testing)"
]
return vals
}
def shortTimeEnum() {
def vals = [
1:"1 Second", 2:"2 Seconds", 3:"3 Seconds", 4:"4 Seconds", 5:"5 Seconds", 6:"6 Seconds", 7:"7 Seconds",
8:"8 Seconds", 9:"9 Seconds", 10:"10 Seconds", 15:"15 Seconds", 30:"30 Seconds", 60:"60 Seconds"
]
return vals
}
def switchRunEnum(addAlways = false) {
String pName = schMotPrefix()
boolean hasFan = state?."${pName}TstatHasFan" ? true : false
boolean canCool = state?."${pName}TstatCanCool" ? true : false
boolean canHeat = state?."${pName}TstatCanHeat" ? true : false
def vals = [ 1:"Any operation: Heating or Cooling" ]
if(hasFan) {
vals << [2:"With HVAC Fan Only"]
}
if(canHeat) {
vals << [3:"Heating"]
}
if(canCool) {
vals << [4:"Cooling"]
}
if(addAlways) {
vals << [5:"Any Operating or non-operating State"]
}
return vals
}
def fanModeTrigEnum() {
String pName = schMotPrefix()
boolean canCool = state?."${pName}TstatCanCool" ? true : false
boolean canHeat = state?."${pName}TstatCanHeat" ? true : false
boolean hasFan = state?."${pName}TstatHasFan" ? true : false
def vals = ["auto":"Auto", "cool":"Cool", "heat":"Heat", "eco":"Eco", "any":"Any Mode"]
if(!canHeat) {
vals = ["cool":"Cool", "eco":"Eco", "any":"Any Mode"]
}
if(!canCool) {
vals = ["heat":"Heat", "eco":"Eco", "any":"Any Mode"]
}
return vals
}
def tModeHvacEnum(boolean canHeat, boolean canCool, boolean canRtn=false) {
def vals = ["auto":"Auto", "cool":"Cool", "heat":"Heat", "eco":"Eco"]
if(!canHeat) {
vals = ["cool":"Cool", "eco":"Eco"]
}
if(!canCool) {
vals = ["heat":"Heat", "eco":"Eco"]
}
if(canRtn) {
vals << ["rtnFromEco":"Return from ECO if in ECO"]
}
return vals
}
def alarmActionsEnum() {
def vals = ["siren":"Siren", "strobe":"Strobe", "both":"Both (Siren/Strobe)"]
return vals
}
def getEnumValue(enumName, inputName) {
def result = "unknown"
def resultList = []
def inputIsList = getObjType(inputName) == "List" ? true : false
if(enumName) {
enumName?.each { item ->
if(inputIsList) {
inputName?.each { inp ->
if(item?.key.toString() == inp?.toString()) {
resultList.push(item?.value)
}
}
} else
if(item?.key.toString() == inputName?.toString()) {
result = item?.value
}
}
}
if(inputIsList) {
return resultList
} else {
return result
}
}
/*
def getSunTimeState() {
def tz = TimeZone.getTimeZone(location.timeZone.ID)
def sunsetTm = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", location?.currentValue('sunsetTime')).format('h:mm a', tz)
def sunriseTm = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", location?.currentValue('sunriseTime')).format('h:mm a', tz)
state.sunsetTm = sunsetTm
state.sunriseTm = sunriseTm
}
*/
def parseDt(format, dt) {
def result
def newDt = Date.parse("$format", dt)
result = formatDt(newDt)
//log.debug "result: $result"
return result
}
/******************************************************************************
* STATIC METHODS *
*******************************************************************************/
//def getAutoAppChildName() { return "Nest Automations" }
String getWatDogAppChildName() { return "Nest Location ${location.name} Watchdog" }
//def getChildName(str) { return "${str}" }
String getChildAppVer(appName) { return appName?.appVersion() ? "v${appName?.appVersion()}" : "" }
//def getUse24Time() { return useMilitaryTime ? true : false }
//Returns app State Info
int getStateSize() {
def resultJson = new groovy.json.JsonOutput().toJson(state)
return resultJson?.toString().length()
}
def getStateSizePerc() { return (int) ((stateSize / 100000)*100).toDouble().round(0) }
def getLocationModes() {
def result = []
location?.modes.sort().each {
if(it) { result.push("${it}") }
}
return result
}
String getObjType(obj) {
if(obj instanceof String) {return "String"}
else if(obj instanceof Map) {return "Map"}
else if(obj instanceof List) {return "List"}
else if(obj instanceof ArrayList) {return "ArrayList"}
else if(obj instanceof Integer) {return "Integer"}
else if(obj instanceof BigInteger) {return "BigInteger"}
else if(obj instanceof Long) {return "Long"}
else if(obj instanceof Boolean) {return "Boolean"}
else if(obj instanceof BigDecimal) {return "BigDecimal"}
else if(obj instanceof Float) {return "Float"}
else if(obj instanceof Byte) {return "Byte"}
else { return "unknown"}
}
def preStrObj() { [1:"•", 2:"│", 3:"├", 4:"└", 5:" "] }
//def getShowHelp() { return state?.showHelp == false ? false : true }
def getTimeZone() {
def tz = null
if(location?.timeZone) { tz = location?.timeZone }
//else { tz = getNestTimeZone() ? TimeZone.getTimeZone(getNestTimeZone()) : null }
if(!tz) { LogAction("getTimeZone: Hub or Nest TimeZone not found", "warn", true) }
return tz
}
String formatDt(dt) {
def tf = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy")
if(getTimeZone()) { tf.setTimeZone(getTimeZone()) }
else {
LogAction("HE TimeZone is not set; Please open your location and Press Save", "warn", true)
}
return tf.format(dt)
}
def getGlobTitleStr(typ) {
return "Desired Default ${typ} Temp (${tUnitStr()})"
}
String formatDt2(tm) {
//def formatVal = settings?.useMilitaryTime ? "MMM d, yyyy - HH:mm:ss" : "MMM d, yyyy - h:mm:ss a"
def formatVal = "MMM d, yyyy - h:mm:ss a"
def tf = new SimpleDateFormat(formatVal)
if(getTimeZone()) { tf.setTimeZone(getTimeZone()) }
return tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", tm.toString()))
}
String tUnitStr() {
return "\u00b0${getTemperatureScale()}"
}
/*
void updTimestampMap(keyName, dt=null) {
def data = state?.timestampDtMap ?: [:]
if(keyName) { data[keyName] = dt }
state?.timestampDtMap = data
}
def getTimestampVal(val) {
def tsData = state?.timestampDtMap
if(val && tsData && tsData[val]) { return tsData[val] }
return null
}
*/
private int getTimeSeconds(timeKey, defVal, meth) {
def t0 = state."${timeKey}" //=getTimestampVal(timeKey)
return !t0 ? defVal : GetTimeDiffSeconds(t0, null, meth).toInteger()
}
def GetTimeDiffSeconds(String strtDate, String stpDate=null, String methName=null) {
//LogTrace("[GetTimeDiffSeconds] StartDate: $strtDate | StopDate: ${stpDate ?: "Not Sent"} | MethodName: ${methName ?: "Not Sent"})")
if((strtDate && !stpDate) || (strtDate && stpDate)) {
//if(strtDate?.contains("dtNow")) { return 10000 }
def now = new Date()
String stopVal = stpDate ? stpDate.toString() : formatDt(now)
long start = Date.parse("E MMM dd HH:mm:ss z yyyy", strtDate).getTime()
long stop = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal).getTime()
long diff = (stop - start) / 1000
LogTrace("[GetTimeDiffSeconds] Results for '$methName': ($diff seconds)")
return diff
} else { return null }
}
boolean daysOk(days) {
if(days) {
def dayFmt = new SimpleDateFormat("EEEE")
if(getTimeZone()) { dayFmt.setTimeZone(getTimeZone()) }
return days.contains(dayFmt.format(new Date())) ? false : true
} else { return true }
}
String time2Str(time) {
if(time) {
def t = timeToday(time, getTimeZone())
def f = new java.text.SimpleDateFormat("h:mm a")
f.setTimeZone(getTimeZone() ?: timeZone(time))
f.format(t)
}
}
String epochToTime(tm) {
def tf = new SimpleDateFormat("h:mm a")
tf?.setTimeZone(getTimeZone())
return tf.format(tm)
}
String getDtNow() {
def now = new Date()
return formatDt(now)
}
boolean modesOk(modeEntry) {
boolean res = true
if(modeEntry) {
modeEntry?.each { m ->
if(m.toString() == location?.mode.toString()) { res = false }
}
}
return res
}
boolean isInMode(modeList) {
if(modeList) {
//log.debug "mode (${location.mode}) in list: ${modeList} | result: (${location?.mode in modeList})"
return location.mode.toString() in modeList
}
return false
}
def notifValEnum(allowCust = true) {
def valsC = [
60:"1 Minute", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes", 1800:"30 Minutes",
3600:"1 Hour", 7200:"2 Hours", 14400:"4 Hours", 21600:"6 Hours", 43200:"12 Hours", 86400:"24 Hours", 1000000:"Custom"
]
def vals = [
60:"1 Minute", 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes",
1800:"30 Minutes", 3600:"1 Hour", 7200:"2 Hours", 14400:"4 Hours", 21600:"6 Hours", 43200:"12 Hours", 86400:"24 Hours"
]
return allowCust ? valsC : vals
}
def pollValEnum() {
def vals = [
60:"1 Minute", 120:"2 Minutes", 180:"3 Minutes", 240:"4 Minutes", 300:"5 Minutes",
600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes",
1800:"30 Minutes", 2700:"45 Minutes", 3600:"60 Minutes"
]
return vals
}
def waitValEnum() {
def vals = [
1:"1 Second", 2:"2 Seconds", 3:"3 Seconds", 4:"4 Seconds", 5:"5 Seconds", 6:"6 Seconds", 7:"7 Seconds",
8:"8 Seconds", 9:"9 Seconds", 10:"10 Seconds", 15:"15 Seconds", 30:"30 Seconds"
]
return vals
}
String strCapitalize(str) {
return str ? str?.toString().capitalize() : null
}
def getInputEnumLabel(inputName, enumName) {
def result = "Not Set"
if(inputName && enumName) {
enumName.each { item ->
if(item?.key.toString() == inputName?.toString()) {
result = item?.value
}
}
}
return result
}
String toJson(Map m) {
return new org.json.JSONObject(m).toString()
}
def toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
/************************************************************************************************
| LOGGING AND Diagnostic |
*************************************************************************************************/
String lastN(String input, n) {
return n > input?.size() ? input : input[-n..-1]
//return n > input?.size() ? input : n ? input[-n..-1] : ''
}
void LogTrace(String msg, String logSrc=(String)null) {
boolean trOn = (settings?.showDebug && settings?.advAppDebug) ? true : false
if(trOn) {
boolean logOn = (settings?.enRemDiagLogging && state?.enRemDiagLogging) ? true : false
Logger(msg, "trace", logSrc, logOn)
}
}
void LogAction(String msg, String type="debug", boolean showAlways=false, String logSrc=null) {
def isDbg = settings?.showDebug ? true : false
if(showAlways || (isDbg && !showAlways)) { Logger(msg, type, logSrc) }
}
void Logger(String msg, String type="debug", String logSrc=(String)null, boolean noSTlogger=false) {
if(msg && type) {
String labelstr = ""
if(state?.dbgAppndName == null) {
def tval = parent ? parent.getSettingVal("dbgAppndName") : settings?.dbgAppndName
state?.dbgAppndName = (tval || tval == null) ? true : false
}
String t0 = app.label
if(state?.dbgAppndName) { labelstr = "${t0} | " }
String themsg = "${labelstr}${msg}"
//log.debug "Logger remDiagTest: $msg | $type | $logSrc"
if(state?.enRemDiagLogging == null) {
state?.enRemDiagLogging = parent?.getStateVal("enRemDiagLogging")
if(state?.enRemDiagLogging == null) {
state?.enRemDiagLogging = false
}
//log.debug "set enRemDiagLogging to ${state?.enRemDiagLogging}"
}
if(state?.enRemDiagLogging) {
String theId = lastN(app?.id.toString(),5)
String theLogSrc = (logSrc == null) ? (parent ? "Automation-${theId}" : "NestManager") : logSrc
parent?.saveLogtoRemDiagStore(themsg, type, theLogSrc)
} else {
if(!noSTlogger) {
switch(type) {
case "debug":
log.debug "${themsg}"
break
case "info":
log.info "| ${themsg}"
break
case "trace":
log.trace "| ${themsg}"
break
case "error":
log.error "| ${themsg}"
break
case "warn":
log.warn "|| ${themsg}"
break
default:
log.debug "${themsg}"
break
}
}
}
}
else { log.error "${labelstr}Logger Error - type: ${type} | msg: ${msg} | logSrc: ${logSrc}" }
}
///////////////////////////////////////////////////////////////////////////////
/******************************************************************************
| Application Help and License Info Variables |
*******************************************************************************/
///////////////////////////////////////////////////////////////////////////////
String appName() { return "${appLabel()}" }
String appLabel() { return "NST Automations" }
String gitRepo() { return "tonesto7/nest-manager"}
String gitBranch() { return "master" }
String gitPath() { return "${gitRepo()}/${gitBranch()}"}
//def betaMarker() { return false }
//def appDevType() { return false }
//def appDevName() { return "" }
/*
def appInfoDesc() {
def cur = null //parent ? parent?.state?.appData?.updater?.versions?.autoapp?.ver.toString() : null
def beta = "" // betaMarker() ? " Beta" : ""
def str = ""
str += "${appName()}"
//str += isAppUpdateAvail() ? "\n• ${textVersion()} (Latest: v${cur})${beta}" : "\n• ${textVersion()}${beta}"
//str += "\n• ${textModified()}"
return str
}
def textVersion() { return "Version: ${appVersion()}" }
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment