diff --git a/smartapps/tonesto7/nest-manager.src/nest-manager.groovy b/smartapps/tonesto7/nest-manager.src/nest-manager.groovy
new file mode 100644
index 0000000..f1b4117
--- /dev/null
+++ b/smartapps/tonesto7/nest-manager.src/nest-manager.groovy
@@ -0,0 +1,11053 @@
+/********************************************************************************************
+| Application Name: Nest Manager and Automations |
+| Author: Anthony S. (@tonesto7), Eric S. (@E_sch) |
+| Contributors: Ben W. (@desertblade) |
+| |
+|*******************************************************************************************|
+| There maybe portions of the code that may resemble code from other apps in the |
+| community. I may have used some of it as a point of reference. |
+| Thanks go out to those Authors!!! |
+| I apologize if i've missed anyone. Please let me know and I will add your credits |
+| |
+| ### I really hope that we don't have a ton or forks being released to the community, |
+| ### I hope that we can collaborate and make app and device type that will accommodate |
+| ### every use case |
+*********************************************************************************************/
+
+import groovy.json.*
+import groovy.time.*
+import java.text.SimpleDateFormat
+import java.security.MessageDigest
+
+definition(
+ name: "${textAppName()}",
+ namespace: "${textNamespace()}",
+ author: "${textAuthor()}",
+ description: "${textDesc()}",
+ category: "Convenience",
+ iconUrl: "https://raw.githubusercontent.com/tonesto7/nest-manager/master/Images/App/nest_manager.png",
+ iconX2Url: "https://raw.githubusercontent.com/tonesto7/nest-manager/master/Images/App/nest_manager%402x.png",
+ iconX3Url: "https://raw.githubusercontent.com/tonesto7/nest-manager/master/Images/App/nest_manager%403x.png",
+ singleInstance: true,
+ oauth: true )
+
+{
+ appSetting "clientId"
+ appSetting "clientSecret"
+}
+
+include 'asynchttp_v1'
+
+def appVersion() { "4.0.0" }
+def appVerDate() { "10-14-2016" }
+def appVerInfo() {
+ def str = ""
+
+ str += "V4.0.0 (October 14th, 2016):"
+ str += "\n▔▔▔▔▔▔▔▔▔▔▔"
+ str += "\n • V4.0.0 Release"
+
+ return str
+}
+
+preferences {
+ //startPage
+ page(name: "startPage")
+
+ //Manager Pages
+ page(name: "authPage")
+ page(name: "mainPage")
+ page(name: "deviceSelectPage")
+ page(name: "reviewSetupPage")
+ page(name: "changeLogPage")
+ page(name: "prefsPage")
+ page(name: "infoPage")
+ page(name: "nestInfoPage")
+ page(name: "structInfoPage")
+ page(name: "tstatInfoPage")
+ page(name: "protInfoPage")
+ page(name: "camInfoPage")
+ page(name: "pollPrefPage")
+ page(name: "debugPrefPage")
+ page(name: "notifPrefPage")
+ page(name: "diagPage")
+ page(name: "appParamsDataPage")
+ page(name: "devNamePage")
+ page(name: "childAppDataPage")
+ page(name: "childDevDataPage")
+ page(name: "managAppDataPage")
+ page(name: "alarmTestPage")
+ page(name: "simulateTestEventPage")
+ page(name: "devNameResetPage")
+ page(name: "resetDiagQueuePage")
+ page(name: "devPrefPage")
+ page(name: "nestLoginPrefPage")
+ page(name: "nestTokenResetPage")
+ page(name: "uninstallPage")
+ page(name: "custWeatherPage")
+ page(name: "automationsPage")
+ page(name: "automationKickStartPage")
+ page(name: "automationGlobalPrefsPage")
+ page(name: "automationStatisticsPage")
+ page(name: "automationSchedulePage")
+ page(name: "feedbackPage")
+ page(name: "sendFeedbackPage")
+
+ //Automation Pages
+ page(name: "selectAutoPage" )
+ page(name: "mainAutoPage")
+ page(name: "remSenTstatFanSwitchPage")
+ page(name: "remSenShowTempsPage")
+ page(name: "nestModePresPage")
+ page(name: "tstatModePage")
+ page(name: "schMotModePage")
+ page(name: "setDayModeTimePage")
+ page(name: "watchDogPage")
+ page(name: "schMotSchedulePage")
+ page(name: "scheduleConfigPage")
+ page(name: "tstatConfigAutoPage")
+
+ //shared pages
+ page(name: "setNotificationPage")
+ page(name: "setNotificationTimePage")
+}
+
+
+mappings {
+ if(!parent) {
+ //used during Oauth Authentication
+ path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
+ path("/oauth/callback") {action: [GET: "callback"]}
+ //Renders Json Data
+ path("/renderInstallId") {action: [GET: "renderInstallId"]}
+ path("/renderInstallData") {action: [GET: "renderInstallData"]}
+ //path("/receiveEventData") {action: [POST: "receiveEventData"]}
+ }
+}
+
+//This Page is used to load either parent or child app interface code
+def startPage() {
+ if(parent) {
+ atomicState?.isParent = false
+ selectAutoPage()
+ } else {
+ atomicState?.isParent = true
+ authPage()
+ }
+}
+
+def authPage() {
+ //log.trace "authPage()"
+ getAccessToken()
+ def preReqOk = preReqCheck()
+ deviceHandlerTest()
+
+ if(!atomicState?.accessToken || (!atomicState?.isInstalled && (!atomicState?.devHandlersTested || !preReqOk))) {
+ return dynamicPage(name: "authPage", title: "Status Page", nextPage: "", install: false, uninstall: false) {
+ section ("Status Page:") {
+ def desc
+ if(!atomicState?.accessToken) {
+ desc = "OAuth is not Enabled for the Nest Manager application. Please click remove and review the installation directions again..."
+ }
+ else if(!atomicState?.devHandlersTested) {
+ desc = "Device Handlers are likely Missing or Not Published. Please read the installation instructions and verify all device handlers are present before continuing."
+ }
+ else if(!preReqOk) {
+ desc = "SmartThings Location is not returning (TimeZone: ${location?.timeZone}) or (ZipCode: ${location?.zipCode}) Please edit these settings under the IDE or Mobile App..."
+ }
+ else {
+ desc = "Application Status has not received any messages to display"
+ }
+ LogAction("Status Message: $desc", "warn", true)
+ paragraph "$desc", required: true, state: null
+ }
+ }
+ }
+ updateWebStuff(true)
+ setStateVar(true)
+ if(atomicState?.newSetupComplete) {
+ def result = ((atomicState?.appData?.updater?.setupVersion && !atomicState?.setupVersion) || (atomicState?.setupVersion?.toInteger() < atomicState?.appData?.updater?.setupVersion?.toInteger())) ? true : false
+ if(result) { atomicState?.newSetupComplete = null }
+ }
+
+ def description
+ def oauthTokenProvided = false
+
+ if(atomicState?.authToken) {
+ description = "You are connected."
+ oauthTokenProvided = true
+ } else { description = "Click to enter Nest Credentials" }
+
+ def redirectUrl = buildRedirectUrl
+ //log.debug "RedirectUrl = ${redirectUrl}"
+
+ if(!oauthTokenProvided && atomicState?.accessToken) {
+ LogAction("AuthToken not found: Directing to Login Page...", "info", true)
+ return dynamicPage(name: "authPage", title: "Login Page", nextPage: "mainPage", install: false, uninstall: false) {
+ section("") {
+ paragraph appInfoDesc(), image: getAppImg("nest_manager%402x.png", true)
+ }
+ section(""){
+ paragraph "Tap 'Login to Nest' below to authorize SmartThings to access your Nest Account.\n\nAfter login you will be taken to the 'Works with Nest' page. Read the info and if you 'Agree' press the 'Accept' button."
+ paragraph "❖ FYI: If you are using a Nest Family account please signin with the parent Nest account, family member accounts will not work correctly...", state: "complete"
+ href url: redirectUrl, style:"embedded", required: true, title: "Login to Nest", description: description
+ }
+ }
+ }
+ else {
+ return mainPage()
+ }
+}
+
+def mainPage() {
+ //log.trace "mainPage"
+ def setupComplete = (!atomicState?.newSetupComplete || !atomicState.isInstalled) ? false : true
+ return dynamicPage(name: "mainPage", title: "", nextPage: (!setupComplete ? "reviewSetupPage" : null), install: setupComplete, uninstall: false) {
+ section("") {
+ href "changeLogPage", title: "", description: "${appInfoDesc()}", image: getAppImg("nest_manager%402x.png", true)
+ if(atomicState?.appData && !appDevType() && isAppUpdateAvail()) {
+ href url: stIdeLink(), style:"external", required: false, title:"An Update is Available for ${appName()}!!!",
+ description:"Current: v${appVersion()} | New: ${atomicState?.appData?.updater?.versions?.app?.ver}\n\nTap to Open the IDE in your Mobile Browser...", state: "complete", image: getAppImg("update_icon.png")
+ }
+ if(atomicState?.appData && !appDevType() && atomicState?.clientBlacklisted) {
+ paragraph "This ID is blacklisted, please update software!!!\nIf software is up to date, contact developer...", required: true, state: null
+ }
+ }
+ if(atomicState?.isInstalled) {
+ section("Manage your Devices & Location:") {
+ def devDesc = getDevicesDesc() ? "Nest Location: (${locationPresence().toString().capitalize()})\n\nCurrent Devices: ${getDevicesDesc()}\n\nTap to Modify..." : "Tap to Configure..."
+ href "deviceSelectPage", title: "Devices & Location", description: devDesc, state: "complete", image: getAppImg("thermostat_icon.png")
+ }
+ }
+ if(!atomicState?.isInstalled) {
+ devicesPage()
+ }
+ if(atomicState?.isInstalled && atomicState?.structures && (atomicState?.thermostats || atomicState?.protects || atomicState?.cameras)) {
+ def autoDesc = getInstAutoTypesDesc() ? "${getInstAutoTypesDesc()}\n\nTap to Modify..." : null
+ section("Manage your Automations:") {
+ href "automationsPage", title: "Automations...", description: (autoDesc ? autoDesc : "Tap to Configure..."), state: (autoDesc ? "complete" : null), image: getAppImg("automation_icon.png")
+ }
+ }
+ if(atomicState?.isInstalled) {
+ section("Manage Your Login, Notification, and Polling Preferences:") {
+ def descStr = ""
+ def sz = descStr.size()
+ descStr += getAppNotifConfDesc() ?: ""
+ if(descStr.size() != sz) { descStr += "\n\n"; sz = descStr.size() }
+ descStr += getAppDebugDesc() ?: ""
+ if(descStr.size() != sz) { descStr += "\n\n"; sz = descStr.size() }
+ descStr += getPollingConfDesc() ?: ""
+ if(descStr.size() != sz) { descStr += "\n\n"; sz = descStr.size() }
+ def prefDesc = (descStr != "") ? "Tap to Modify..." : "Tap to Configure..."
+ href "prefsPage", title: "Preferences", description: prefDesc, state: (descStr ? "complete" : ""), image: getAppImg("settings_icon.png")
+ }
+ section("View Change Logs, Donation, License Info, and Leave Feedback:") {
+ href "infoPage", title: "Help, Info, Instructions and More", description: "", image: getAppImg("info.png")
+ }
+ if(atomicState?.isInstalled && atomicState?.structures && (atomicState?.thermostats || atomicState?.protects || atomicState?.weatherDevice)) {
+ section("View App and Device Data, and Perform Device Tests:") {
+ href "nestInfoPage", title: "API | Diagnostics | Testing...", description: "", image: getAppImg("api_diag_icon.png")
+ }
+ }
+ section("Remove All Apps, Automations, and Devices:") {
+ href "uninstallPage", title: "Uninstall this App", description: "", image: getAppImg("uninstall_icon.png")
+ }
+ }
+ }
+}
+
+def devicesPage() {
+ def structs = getNestStructures()
+ def structDesc = !structs?.size() ? "No Locations Found" : "Found (${structs?.size()}) Locations..."
+ LogAction("${structDesc} (${structs})", "info", false)
+ if (atomicState?.thermostats || atomicState?.protects || atomicState?.vThermostats || atomicState?.cameras || atomicState?.presDevice || atomicState?.weatherDevice ) { // if devices are configured, you cannot change the structure until they are removed
+ section("Your Location:") {
+ paragraph "Location: ${structs[atomicState?.structures]}\n\n(Remove All Devices to Change!)", image: getAppImg("nest_structure_icon.png")
+ }
+ } else {
+ section("Select your Location:") {
+ input(name: "structures", title:"Nest Locations", type: "enum", required: true, multiple: false, submitOnChange: true, metadata: [values:structs],
+ image: getAppImg("nest_structure_icon.png"))
+ }
+ }
+ if (settings?.structures) {
+ atomicState.structures = settings?.structures ?: null
+
+ def stats = getNestThermostats()
+ def statDesc = stats.size() ? "Found (${stats.size()}) Thermostats..." : "No Thermostats"
+ LogAction("${statDesc} (${stats})", "info", false)
+
+ def coSmokes = getNestProtects()
+ def coDesc = coSmokes.size() ? "Found (${coSmokes.size()}) Protects..." : "No Protects"
+ LogAction("${coDesc} (${coSmokes})", "info", false)
+
+ def cams = getNestCameras()
+ def camDesc = cams.size() ? "Found (${cams.size()}) Cameras..." : "No Cameras"
+ LogAction("${camDesc} (${cams})", "info", false)
+
+ section("Select your Devices:") {
+ if(!stats?.size() && !coSmokes.size() && !cams?.size()) { paragraph "No Devices were found..." }
+ if(stats?.size() > 0) {
+ input(name: "thermostats", title:"Nest Thermostats", type: "enum", required: false, multiple: true, submitOnChange: true, metadata: [values:stats],
+ image: getAppImg("thermostat_icon.png"))
+ }
+ atomicState.thermostats = settings?.thermostats ? statState(settings?.thermostats) : null
+ if(coSmokes.size() > 0) {
+ input(name: "protects", title:"Nest Protects", type: "enum", required: false, multiple: true, submitOnChange: true, metadata: [values:coSmokes],
+ image: getAppImg("protect_icon.png"))
+ }
+ atomicState.protects = settings?.protects ? coState(settings?.protects) : null
+ if(cams.size() > 0) {
+ input(name: "cameras", title:"Nest Cameras", type: "enum", required: false, multiple: true, submitOnChange: true, metadata: [values:cams],
+ image: getAppImg("camera_icon.png"))
+ }
+ atomicState.cameras = settings?.cameras ? camState(settings?.cameras) : null
+ input(name: "presDevice", title:"Add Presence Device?\n", type: "bool", default: false, required: false, submitOnChange: true, image: getAppImg("presence_icon.png"))
+ atomicState.presDevice = settings?.presDevice ?: null
+ input(name: "weatherDevice", title:"Add Weather Device?\n", type: "bool", default: false, required: false, submitOnChange: true, image: getAppImg("weather_icon.png"))
+ atomicState.weatherDevice = settings?.weatherDevice ?: null
+ }
+ }
+}
+
+def deviceSelectPage() {
+ return dynamicPage(name: "deviceSelectPage", title: "Device Selection", nextPage: "startPage", install: false, uninstall: false) {
+ devicesPage()
+ }
+}
+
+def reviewSetupPage() {
+ return dynamicPage(name: "reviewSetupPage", title: "Setup Review", install: true, uninstall: atomicState?.isInstalled) {
+ if(!atomicState?.newSetupComplete) { atomicState.newSetupComplete = true }
+ atomicState?.setupVersion = atomicState?.appData?.updater?.setupVersion?.toInteger() ?: 0
+ section("Device Summary:") {
+ def str = ""
+ str += !atomicState?.isInstalled ? "Devices to Install:" : "Installed Devices:"
+ str += getDevicesDesc() ?: ""
+ paragraph "${str}"
+ if(atomicState?.weatherDevice) {
+ def wmsg = ""
+ if(!getStZipCode() || getStZipCode() != getNestZipCode()) {
+ wmsg = "Please configure as zip codes do not match..."
+ }
+ href "custWeatherPage", title: "Customize Weather Location?", description: (getWeatherConfDesc() ? "${getWeatherConfDesc()}\n\nTap to Modify..." : "${wmsg}"), state: ((getWeatherConfDesc() || wmsg) ? "complete":""), image: getAppImg("weather_icon_grey.png")
+
+ input ("weathAlertNotif", "bool", title: "Notify on Weather Alerts?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("weather_icon.png"))
+ }
+ if(!atomicState?.isInstalled && (settings?.thermostats || settings?.protects || settings?.cameras || settings?.presDevice || settings?.weatherDevice)) {
+ href "devNamePage", title: "Customize Device Names?", description: (atomicState?.custLabelUsed || atomicState?.useAltNames) ? "Tap to Modify..." : "Tap to configure...", state: ((atomicState?.custLabelUsed || atomicState?.useAltNames) ? "complete" : null), image: getAppImg("device_name_icon.png")
+ }
+ }
+ section("Notifications:") {
+ href "notifPrefPage", title: "Notifications", description: (getAppNotifConfDesc() ? "${getAppNotifConfDesc()}\n\nTap to modify..." : "Tap to configure..."), state: (getAppNotifConfDesc() ? "complete" : null), image: getAppImg("notification_icon.png")
+ }
+ section("Polling:") {
+ href "pollPrefPage", title: "Polling Preferences", description: "${getPollingConfDesc()}\n\nTap to modify...", state: (getPollingConfDesc() != "" ? "complete" : null), image: getAppImg("timer_icon.png")
+ }
+ doShareDev()
+ if(atomicState?.showHelp) {
+ section(" ") {
+ href "infoPage", title: "Help, Info and Instructions", description: "Tap to view...", image: getAppImg("info.png")
+ }
+ }
+ if(!atomicState?.isInstalled) {
+ section(" ") {
+ href "uninstallPage", title: "Uninstall this App", description: "Tap to Remove...", image: getAppImg("uninstall_icon.png")
+ }
+ }
+ }
+}
+
+def doShareDev() {
+ section("Share Data with Developer:") {
+ paragraph "These options will send the developer non-identifiable app information as well as error data to help diagnose issues quicker and catch trending issues."
+ input ("optInAppAnalytics", "bool", title: "Send Install Data?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("app_analytics_icon.png"))
+ input ("optInSendExceptions", "bool", title: "Send Error Data?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("diag_icon.png"))
+ if(settings?.optInAppAnalytics != false) {
+ input(name: "mobileClientType", title:"Primary Mobile Device?", type: "enum", required: true, submitOnChange: true, metadata: [values:["android":"Android", "ios":"iOS", "winphone":"Windows Phone"]],
+ image: getAppImg("${(settings?.mobileClientType && settings?.mobileClientType != "decline") ? "${settings?.mobileClientType}_icon" : "mobile_device_icon"}.png"))
+ href url: getAppEndpointUrl("renderInstallData"), style:"embedded", title:"View the Data that will be Shared with the Developer", description: "Tap to view Data...", required:false, image: getAppImg("view_icon.png")
+ }
+ }
+}
+
+//Defines the Preference Page
+def prefsPage() {
+ def devSelected = (atomicState?.structures && (atomicState?.thermostats || atomicState?.protects || atomicState?.cameras || atomicState?.presDevice || atomicState?.weatherDevice))
+ dynamicPage(name: "prefsPage", title: "Application Preferences", nextPage: "", install: false, uninstall: false ) {
+ section("Polling:") {
+ href "pollPrefPage", title: "Polling Preferences", description: "${getPollingConfDesc()}\n\nTap to modify...", state: (getPollingConfDesc() != "" ? "complete" : null), image: getAppImg("timer_icon.png")
+ }
+ if(devSelected) {
+ section("Devices:") {
+ href "devPrefPage", title: "Device Customization", description: (devCustomizePageDesc() ? "${devCustomizePageDesc()}\n\nTap to Modify..." : "Tap to configure..."),
+ state: (devCustomizePageDesc() ? "complete" : null), image: getAppImg("device_pref_icon.png")
+ }
+ }
+ section("Notifications Options:") {
+ href "notifPrefPage", title: "Notifications", description: (getAppNotifConfDesc() ? "${getAppNotifConfDesc()}\n\nTap to modify..." : "Tap to configure..."), state: (getAppNotifConfDesc() ? "complete" : null),
+ image: getAppImg("notification_icon.png")
+ }
+ section("App and Device Logging:") {
+ href "debugPrefPage", title: "Logging", description: (getAppDebugDesc() ? "${getAppDebugDesc() ?: ""}\n\nTap to modify..." : "Tap to configure..."), state: ((isAppDebug() || isChildDebug()) ? "complete" : null),
+ image: getAppImg("log.png")
+ }
+ doShareDev()
+ section ("Misc. Options:") {
+ input ("useMilitaryTime", "bool", title: "Use Military Time (HH:mm)?", defaultValue: false, submitOnChange: true, required: false, image: getAppImg("military_time_icon.png"))
+ input ("disAppIcons", "bool", title: "Disable App Icons?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("no_icon.png"))
+ input ("debugAppendAppName", "bool", title: "Append App Name to Log Entries?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("log.png"))
+ atomicState.needChildUpd = true
+ }
+ section("Manage Your Nest Login:") {
+ href "nestLoginPrefPage", title: "Nest Login Preferences", description: "Tap to configure...", image: getAppImg("login_icon.png")
+ }
+ section("Customize the Label of the App:") {
+ label title:"Application Label (optional)", required:false
+ }
+ }
+}
+
+def automationsPage() {
+ return dynamicPage(name: "automationsPage", title: "Installed Automations", nextPage: !parent ? "" : "automationsPage", install: false) {
+ def autoApp = findChildAppByName( appName() )
+ def autoAppInst = isAutoAppInst()
+ if(autoApp) {
+ //Nothing to add here yet...
+ }
+ else {
+ section("") {
+ paragraph "You haven't created any Automations yet!!!\nTap Create New Automation to get Started..."
+ }
+ }
+ section("") {
+ app(name: "autoApp", appName: appName(), namespace: "tonesto7", multiple: true, title: "Create New Automation...", image: getAppImg("automation_icon.png"))
+ /*
+ def rText = "NOTICE:\nAutomations is still in BETA!!!\n" +
+ "We are not responsible for any damages caused by using this SmartApp.\n\n USE AT YOUR OWN RISK!!!"
+ paragraph "${rText}"//, required: true, state: null
+ */
+ }
+ if(autoAppInst) {
+ section("View Details:") {
+ def schEn = getChildApps()?.findAll { (!(it.getAutomationType() in ["nMode", "watchDog"]) && it?.getActiveScheduleState()) }
+ if(schEn?.size()) {
+ href "automationSchedulePage", title: "View Automation Schedule(s)", description: "", image: getAppImg("schedule_icon.png")
+ }
+ href "automationStatisticsPage", title: "View Automation Statistics", description: "", image: getAppImg("app_analytics_icon.png")
+ }
+
+ section("Advanced Options: (Tap + to Show) ", hideable: true, hidden: true) {
+ def descStr = ""
+ descStr += (settings?.locDesiredCoolTemp || settings?.locDesiredHeatTemp) ? "Comfort Settings:" : ""
+ descStr += settings?.locDesiredHeatTemp ? "\n • Desired Heat Temp: (${settings?.locDesiredHeatTemp}°${getTemperatureScale()})" : ""
+ descStr += settings?.locDesiredCoolTemp ? "\n • Desired Cool Temp: (${settings?.locDesiredCoolTemp}°${getTemperatureScale()})" : ""
+ descStr += (settings?.locDesiredComfortDewpointMax) ? "${(settings?.locDesiredCoolTemp || settings?.locDesiredHeatTemp) ? "\n\n" : ""}Dew Point:" : ""
+ descStr += settings?.locDesiredComfortDewpointMax ? "\n • Max Dew Point: (${settings?.locDesiredComfortDewpointMax}${getTemperatureScale()})" : ""
+ descStr += "${(settings?.locDesiredCoolTemp || settings?.locDesiredHeatTemp) ? "\n\n" : ""}${getSafetyValuesDesc()}" ?: ""
+ def prefDesc = (descStr != "") ? "${descStr}\n\nTap to Modify..." : "Tap to Configure..."
+ href "automationGlobalPrefsPage", title: "Global Automation Preferences", description: prefDesc, state: (descStr != "" ? "complete" : null), image: getAppImg("global_prefs_icon.png")
+ input "enTstatAutoSchedInfoReq", "bool", title: "Allow Other Smart Apps to Retrieve your Thermostat automation Schedule info?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("info_icon2.png")
+ href "automationKickStartPage", title: "Re-Initialize All Automations", description: "Tap to call the Update() action on each automation.\nTap to Begin...", image: getAppImg("reset_icon.png")
+ }
+ }
+ }
+}
+
+def automationSchedulePage() {
+ dynamicPage(name: "automationSchedulePage", title: "View Schedule Data..", uninstall: false) {
+ def schMap = []
+ getChildApps()?.each {
+ def actSch = it?.getScheduleDesc() ?: null
+ if (actSch?.size()) {
+ def schInfo = it?.getScheduleDesc()
+ def curSch = it?.getCurrentSchedule()
+ if (schInfo?.size()) {
+ section("${it?.label}") {
+ schInfo?.each { schItem ->
+ def schNum = schItem?.key
+ def schDesc = schItem?.value
+ def schInUse = (curSch?.toInteger() == schNum?.toInteger()) ? true : false
+ if(schNum && schDesc) {
+ paragraph "${schDesc}", state: schInUse ? "complete" : ""
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+def automationStatisticsPage() {
+ dynamicPage(name: "automationStatisticsPage", title: "Installed Automations Stats\n(Auto-Refresh Every 20 sec.)", refreshInterval: 20, uninstall: false) {
+ def cApps = getChildApps()
+ if(cApps) {
+ cApps?.sort()?.each { chld ->
+ def autoType = chld?.getAutomationType()
+ section(" ") {
+ paragraph "${chld?.label}", state: "complete", image: getAutoIcon(autoType)
+ def data = chld?.getAutomationStats()
+ def tf = new SimpleDateFormat("M/d/yyyy - h:mm a")
+ tf.setTimeZone(getTimeZone())
+ def lastModDt = data?.lastUpdatedDt ? tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", data?.lastUpdatedDt.toString())) : null
+ def lastEvtDt = data?.lastEvent?.date ? tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", data?.lastEvent?.date.toString())) : null
+ def lastActionDt = data?.lastActionData?.dt ? tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", data?.lastActionData?.dt.toString())) : null
+ def lastEvalDt = data?.lastEvalDt ? tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", data?.lastEvalDt.toString())) : null
+ def lastSchedDt = data?.lastSchedDt ? tf.format(Date.parse("E MMM dd HH:mm:ss z yyyy", data?.lastSchedDt.toString())) : null
+ def lastExecVal = data?.lastExecVal ?: null
+ def execAvgVal = data?.execAvgVal ?: null
+
+ def str = ""
+ str += lastModDt ? " • Last Modified:\n └ (${lastModDt})" : "\n • Last Modified: (Not Available)"
+ str += lastEvtDt ? "\n\n • Last Event:" : ""
+ str += lastEvtDt ? "${(data?.lastEvent?.displayName.length() > 20) ? "\n │ Device:\n │└ " : "\n ├ Device: "}${data?.lastEvent?.displayName}" : ""
+ str += lastEvtDt ? "\n ├ Type: (${data?.lastEvent?.name.toString().capitalize()})" : ""
+ str += lastEvtDt ? "\n ├ Value: (${data?.lastEvent?.value}${data?.lastEvent?.unit ? "${data?.lastEvent?.unit}" : ""})" : ""
+ str += lastEvtDt ? "\n └ DateTime: (${lastEvtDt})" : "\n\n • Last Event: (Not Available)"
+ str += lastEvalDt ? "\n\n • Last Evaluation:\n └ (${lastEvalDt})" : "\n\n • Last Evaluation: (Not Available)"
+ str += lastSchedDt ? "\n\n • Last Schedule:\n └ (${lastSchedDt})" : "\n\n • Last Schedule: (Not Available)"
+ str += lastActionDt ? "\n\n • Last Action:\n ├ DateTime: (${lastActionDt})\n └ Action: ${data?.lastActionData?.actionDesc}" : "\n\n • Last Action: (Not Available)"
+ str += lastExecVal ? "\n\n • Execution Info:\n ${execAvgVal ? "├" : "└"} Last Time: (${lastExecVal} ms)${execAvgVal ? "\n └ Avg. Time: (${execAvgVal} ms)" : ""}" : "\n\n • Execution Info: (Not Available)"
+ paragraph "${str}", state: "complete"
+ }
+ }
+ }
+ }
+}
+
+def automationKickStartPage() {
+ dynamicPage(name: "automationKickStartPage", title: "This Page is running Update() on all of your installed Automations", nextPage: "automationsPage", install: false, uninstall: false) {
+ def cApps = getChildApps()
+ section("Running Update All Automations:") {
+ if(cApps) {
+ cApps?.sort()?.each { chld ->
+ chld?.update()
+ paragraph "${chld?.label}\n\nUpdate() Completed Successfully!!!", state: "complete"
+ }
+ } else {
+ paragraph "No Automations Found..."
+ }
+ }
+ }
+}
+
+def automationGlobalPrefsPage() {
+ dynamicPage(name: "automationGlobalPrefsPage", title: "", nextPage: "", install: false) {
+ if(atomicState?.thermostats) {
+ section {
+ paragraph "These settings are applied if individual thermostat settings are not present"
+ }
+ section(title: "Comfort Preferences ", hideable: true, hidden: false) {
+ input "locDesiredHeatTemp", "decimal", title: "Desired Global Heat Temp (°${getTemperatureScale()})", description: "Range within ${tempRangeValues()}", range: tempRangeValues(),
+ required: false, image: getAppImg("heat_icon.png")
+ input "locDesiredCoolTemp", "decimal", title: "Desired Global Cool Temp (°${getTemperatureScale()})", description: "Range within ${tempRangeValues()}", range: tempRangeValues(),
+ required: false, image: getAppImg("cool_icon.png")
+ def tRange = (getTemperatureScale() == "C") ? "15..19" : "60..66"
+ def wDev = getChildDevice(getNestWeatherId())
+ def curDewPnt = wDev ? "${wDev?.currentDewpoint}°${getTemperatureScale()}" : 0
+ input "locDesiredComfortDewpointMax", "decimal", title: "Max. Dewpoint Desired (${tRange} °${getTemperatureScale()})", required: false, range: trange,
+ image: getAppImg("dewpoint_icon.png")
+ href url: "https://en.wikipedia.org/wiki/Dew_point#Relationship_to_human_comfort", style:"embedded", title: "What is Dew Point?",
+ description:"Tap to View Info", image: getAppImg("instruct_icon.png")
+ }
+ section(title: "Safety Preferences ", hideable:true, hidden: false) {
+ if(atomicState?.thermostats) {
+ atomicState?.thermostats?.each { ts ->
+ def dev = getChildDevice(ts?.key)
+ def canHeat = dev?.currentState("canHeat")?.stringValue == "false" ? false : true
+ def canCool = dev?.currentState("canCool")?.stringValue == "false" ? false : true
+
+ def defmin
+ def defmax
+ def safeTemp = getSafetyTemps(dev)
+ if(safeTemp) {
+ defmin = safeTemp.min
+ defmax = safeTemp.max
+ }
+ def dew_max = getComfortDewpoint(dev)
+
+ /*
+ TODO
+ need to check / default to current setting in dth
+ should have method in dth to set safety temps (today they are sent from nest manager polls..)
+ should have method in dth to clear safety temps
+ add global default
+ */
+ def str = ""
+ str += "Safety Values:"
+ str += safeTemp ? "\n• Safefy Temps:\n └ Min: ${safeTemp.min}°${getTemperatureScale()}/Max: ${safeTemp.max}°${getTemperatureScale()}" : "\n• Safefy Temps: (Not Set)"
+ str += dew_max ? "\n• Comfort Max Dewpoint:\n └Max: ${dew_max}°${getTemperatureScale()}" : "\n• Comfort Max Dewpoint: (Not Set)"
+ paragraph "${str}", title:"${dev?.displayName}", state: "complete", image: getAppImg("instruct_icon.png")
+ if(canHeat) {
+ input "${dev?.deviceNetworkId}_safety_temp_min", "decimal", title: "Min. Temp Desired °(${getTemperatureScale()})", description: "Range within ${tempRangeValues()}",
+ range: "0..90", submitOnChange: true, required: false, image: getAppImg("heat_icon.png")
+ }
+ if(canCool) {
+ input "${dev?.deviceNetworkId}_safety_temp_max", "decimal", title: "Max. Temp Desired (°${getTemperatureScale()})", description: "Range within ${tempRangeValues()}",
+ range: tempRangeValues(), submitOnChange: true, required: false, image: getAppImg("cool_icon.png")
+ }
+ def tmin = settings?."${dev?.deviceNetworkId}_safety_temp_min"
+ def tmax = settings?."${dev?.deviceNetworkId}_safety_temp_max"
+
+ def comparelow = getTemperatureScale() == "C" ? 10 : 50
+ def comparehigh = getTemperatureScale() == "C" ? 32 : 90
+ tmin = (tmin == null || tmin == 0 || (tmin >= comparelow && tmin <= comparehigh)) ? tmin : 0
+ tmax = (tmax == null || tmax == 0 || (tmax <= comparehigh && tmax >= comparelow)) ? tmax : 0
+ if (tmin && tmin != 0 && tmax && tmax != 0) {
+ tmin = tmin < tmax ? tmin : 0
+ tmax = tmax > tmin ? tmax : 0
+ }
+ atomicState?."${dev?.deviceNetworkId}_safety_temp_min" = tmin
+ atomicState?."${dev?.deviceNetworkId}_safety_temp_max" = tmax
+
+ def tRange = (getTemperatureScale() == "C") ? "15..19" : "60..66"
+ input "${dev?.deviceNetworkId}_comfort_dewpoint_max", "decimal", title: "Max. Dewpoint Desired (${tRange} °${getTemperatureScale()})", required: false, range: trange,
+ image: getAppImg("dewpoint_icon.png")
+ /*
+ def hrange = "10..80"
+ input "${dev?.deviceNetworkId}_comfort_humidity_max", "number", title: "Max. Humidity Desired (%)", description: "Range within ${hrange}", range: hrange,
+ required: false, image: getAppImg("humidity_icon.png")
+ */
+ }
+ }
+ }
+ }
+ }
+}
+
+def getSafetyValuesDesc() {
+ def str = ""
+ def tstats = atomicState?.thermostats
+ if(tstats) {
+ tstats?.each { ts ->
+ def dev = getChildDevice(ts?.key)
+ def defmin
+ def defmax
+ def safeTemp = getSafetyTemps(dev)
+ if(safeTemp) {
+ defmin = safeTemp.min
+ defmax = safeTemp.max
+ }
+ def dew_max = getComfortDewpoint(dev)
+ def minTemp = defmin
+ def maxTemp = defmax
+ def maxDew = dew_max ?: (getTemperatureScale() == "C") ? 19 : 66
+
+ if(minTemp == 0) { minTemp = null }
+ if(maxTemp == 0) { maxTemp = null }
+ if(maxDew == 0) { maxDew = null }
+
+ str += (ts && (minTemp || maxTemp)) ? "${dev?.displayName}\nSafety Values:" : ""
+ str += minTemp ? "\n• Min. Temp: ${minTemp}°${getTemperatureScale()}" : ""
+ str += maxTemp ? "\n• Max. Temp: ${maxTemp}°${getTemperatureScale()}" : ""
+ //str += maxHum ? "\n• Max. Humidity: ${maxHum}%" : ""
+ str += (ts && (minTemp || maxTemp) && (maxDew)) ? "\n\n" : ""
+ str += (ts && (maxDew)) ? "${dev?.displayName}\nComfort Values:" : ""
+ str += maxDew ? "\n• Max. Dewpnt: ${maxDew}°${getTemperatureScale()}" : ""
+ str += tstats?.size() > 1 ? "\n\n" : ""
+ }
+ }
+ return (str != "") ? "${str}" : null
+}
+
+def setMyLockId(val) {
+ if(atomicState?.myID == null && parent && val) {
+ atomicState.myID = val
+ }
+}
+
+def getMyLockId() {
+ if(parent) { return atomicState?.myID } else { return null }
+}
+
+def addRemoveVthermostat(tstatdni, tval, myID) {
+ def odevId = tstatdni
+ LogAction("addRemoveVthermostat() tstat: ${tstatdni} devid: ${odevId} tval: ${tval} myID: ${myID} atomicState.vThermostats: ${atomicState?.vThermostats} ", "trace", true)
+
+ if(parent || !myID || tval == null) { return false }
+ def tstat = tstatdni
+
+ def d1 = getChildDevice(odevId.toString())
+ if(!d1) {
+ LogAction("addRemoveVthermostat Error: Cannot find thermostat device child", "error", true)
+ if(tval) { return false } // if deleting (false), let it try to proceed
+ } else {
+ tstat = d1
+ }
+
+ def devId = "v${odevId}"
+
+ if(atomicState?."vThermostat${devId}" && myID != atomicState?."vThermostatChildAppId${devId}") {
+ LogAction("addRemoveVthermostat() not ours to play with ${myID} ${atomicState?."vThermostat${devId}"} ${atomicState?."vThermostatChildAppId${devId}"}", "trace", true)
+ //atomicState?."vThermostat${devId}" = false
+ //atomicState?."vThermostatChildAppId${devId}" = null
+ //atomicState?."vThermostatMirrorId${devId}" = null
+ //atomicState?.vThermostats = null
+ return false
+
+ } else if(tval && atomicState?."vThermostat${devId}" && myID == atomicState?."vThermostatChildAppId${devId}") {
+ LogAction("addRemoveVthermostat() already created with ${myID} ${atomicState?."vThermostat${devId}"} ${atomicState?."vThermostatChildAppId${devId}"}", "trace", true)
+ return true
+
+ } else if(!tval && !atomicState?."vThermostat${devId}") {
+ LogAction("addRemoveVthermostat() already removed with ${myID} ${atomicState?."vThermostat${devId}"} ${atomicState?."vThermostatChildAppId${devId}"}", "trace", true)
+ return true
+
+ } else {
+ atomicState."vThermostat${devId}" = tval
+ if(tval && !atomicState?."vThermostatChildAppId${devId}") {
+ LogAction("addRemoveVthermostat() creating virtual thermostat tracking ${tstat}", "trace", true)
+ atomicState."vThermostatChildAppId${devId}" = myID
+ atomicState?."vThermostatMirrorId${devId}" = odevId
+ def vtlist = atomicState?.vThermostats ?: [:]
+ vtlist[devId] = "v${tstat.toString()}"
+ atomicState.vThermostats = vtlist
+ runIn(10, "updated", [overwrite: true]) // create what is needed
+
+ } else if(!tval && atomicState?."vThermostatChildAppId${devId}") {
+ LogAction("addRemoveVthermostat() removing virtual thermostat tracking ${tstat}", "trace", true)
+ atomicState."vThermostatChildAppId${devId}" = null
+ atomicState?."vThermostatMirrorId${devId}" = null
+ def vtlist = atomicState?.vThermostats
+ def newlist = [:]
+ def vtstat
+ vtstat = vtlist.collect { dni ->
+ //LogAction("atomicState.vThermostats: ${atomicState.vThermostats} dni: ${dni} dni.key: ${dni.key.toString()} dni.value: ${dni.value.toString()} devId: ${devId}", "debug", true)
+ def ttkey = dni.key.toString()
+ if(ttkey == devId) { ; /*log.trace "skipping $dni"*/ }
+ else { newlist[ttkey] = dni.value }
+ return true
+ }
+ vtlist = newlist
+ atomicState.vThermostats = vtlist
+ runIn(10, "updated", [overwrite: true]) // delete what is needed
+ } else {
+ LogAction("addRemoveVthermostat() unexpected operation state ${myID} ${atomicState?."vThermostat${devId}"} ${atomicState?."vThermostatChildAppId${devId}"}", "warn", true)
+ return false
+ }
+ return true
+ }
+}
+
+def installed() {
+ log.debug "Installed with settings: ${settings}"
+ atomicState?.installData = ["initVer":appVersion(), "dt":getDtNow().toString()]
+ initialize()
+ sendNotificationEvent("${textAppName()} has been installed...")
+}
+
+def updated() {
+ log.debug "Updated with settings: ${settings}"
+ initialize()
+ sendNotificationEvent("${textAppName()} has updated settings...")
+ if(parent) {
+ atomicState?.lastUpdatedDt = getDtNow()
+ }
+}
+
+def uninstalled() {
+ //log.debug "uninstalled..."
+ if(parent) {
+ uninstAutomationApp()
+ } else {
+ uninstManagerApp()
+ }
+ sendNotificationEvent("${textAppName()} is uninstalled...")
+}
+
+def initialize() {
+ //log.debug "initialize..."
+ if(parent) {
+ runIn(23, "initAutoApp", [overwrite: true])
+ }
+ else {
+ initWatchdogApp()
+ initManagerApp()
+ }
+}
+
+def initManagerApp() {
+ setStateVar()
+ unschedule()
+ unsubscribe()
+ atomicState.pollingOn = false
+ atomicState.lastChildUpdDt = null // force child update on next poll
+ atomicState.lastForcePoll = null
+ atomicState.swVersion = appVersion()
+ if(addRemoveDevices()) { // if we changed devices, reset queues and polling
+ atomicState.cmdQlist = []
+ }
+ if(settings?.thermostats || settings?.protects || settings?.cameras || settings?.presDevice || settings?.weatherDevice) {
+ atomicState?.isInstalled = true
+ } else { atomicState.isInstalled = false }
+ subscriber()
+ setPollingState()
+ if(optInAppAnalytics) { runIn(4, "sendInstallData", [overwrite: true]) } //If analytics are enabled this will send non-user identifiable data to firebase server
+ runIn(20, "stateCleanup", [overwrite: true])
+}
+
+def uninstManagerApp() {
+ //log.trace "uninstManagerApp"
+ try {
+ if(addRemoveDevices(true)) {
+ //removes analytic data from the server
+ if(optInAppAnalytics) {
+ if(removeInstallData()) {
+ atomicState?.installationId = null
+ }
+ }
+ //Revokes Smartthings endpoint token...
+ revokeAccessToken()
+ //Revokes Nest Auth Token
+ if(atomicState?.authToken) { revokeNestToken() }
+ //sends notification of uninstall
+ sendNotificationEvent("${textAppName()} is uninstalled...")
+ }
+ } catch (ex) {
+ log.error "uninstManagerApp Exception:", ex
+ sendExceptionData(ex, "uninstManagerApp")
+ }
+}
+
+def initWatchdogApp() {
+ //log.trace "initWatchdogApp..."
+ def watDogApp = getChildApps()?.findAll { it?.getAutomationType() == "watchDog" }
+ if(watDogApp?.size() < 1) {
+ LogAction("Installing Nest Watchdog App...", "info", true)
+ addChildApp(textNamespace(), appName(), getWatchdogAppChildName(), [settings:[watchDogFlag: true]])
+ } else if (watDogApp?.size() >= 1) {
+ def cnt = 1
+ watDogApp?.each { chld ->
+ if(cnt == 1) {
+ cnt = cnt+1
+ //LogAction("Running Update Command on Watchdog...", "warn", true)
+ chld.update()
+ } else if (cnt > 1) {
+ LogAction("Deleting Extra Watchdog Instance(${chld})...", "warn", true)
+ deleteChildApp(chld)
+ }
+ }
+ }
+}
+
+def getChildAppVer(appName) { return appName?.appVersion() ? "v${appName?.appVersion()}" : "" }
+
+def appBtnDesc(val) {
+ return atomicState?.automationsActive ? (atomicState?.automationsActiveDesc ? "${atomicState?.automationsActiveDesc}\nTap to Modify..." : "Tap to Modify...") : "Tap to Install..."
+}
+
+def isAutoAppInst() {
+ def chldCnt = 0
+ childApps?.each { cApp ->
+ chldCnt = chldCnt + 1
+ }
+ return (chldCnt > 0) ? true : false
+}
+
+def autoAppInst(Boolean val) {
+ log.debug "${getAutoAppChildName()} is Installed?: ${val}"
+ atomicState.autoAppInstalled = val
+}
+
+def getInstAutoTypesDesc() {
+ def nModeCnt = 0
+ def schMotCnt = 0
+ def watchDogCnt = 0
+ def disCnt = 0
+ def schedAutoInst
+ def remSenCnt = 0
+ def fanCtrlCnt = 0
+ def fanCircCnt = 0
+ def conWatCnt = 0
+ def extTmpCnt = 0
+ def leakWatCnt = 0
+ def tSchedCnt = 0
+ childApps?.each { a ->
+ def type = a?.getAutomationType()
+ if(a?.getIsAutomationDisabled()) { disCnt = disCnt+1 }
+ else {
+ //log.debug "automation type: $type"
+ switch(type) {
+ case "nMode":
+ nModeCnt = nModeCnt+1
+ break
+ case "schMot":
+ schMotCnt = schMotCnt+1
+ def ai = a?.getAutomationsInstalled()
+ if(ai) {
+ ai?.each { aut ->
+ aut?.each { it2 ->
+ if(it2?.key == "schMot") {
+ //log.debug "aut data: ${aut}"
+ if("tSched" in it2?.value) { tSchedCnt = tSchedCnt+1}
+ if("remSen" in it2?.value) { remSenCnt = remSenCnt+1 }
+ if("fanCtrl" in it2?.value) { fanCtrlCnt = fanCtrlCnt+1 }
+ if("fanCirc" in it2?.value) { fanCircCnt = fanCircCnt+1 }
+ if("conWat" in it2?.value) { conWatCnt = conWatCnt+1 }
+ if("extTmp" in it2?.value) { extTmpCnt = extTmpCnt+1 }
+ if("leakWat" in it2?.value) { leakWatCnt = leakWatCnt+1 }
+ }
+ }
+ }
+ }
+ break
+ case "watchDog":
+ watchDogCnt = watchDogCnt+1
+ break
+ }
+ }
+ }
+/*
+ TODO I need to add the individual thermostat automation types installed to the analytics
+*/
+ def inAutoList = []
+ inAutoList?.push("nestMode":nModeCnt)
+ inAutoList?.push("watchDog":watchDogCnt)
+ if(schMotCnt > 0) {
+ inAutoList?.push("schMot":["tSched":tSchedCnt, "remSen":remSenCnt, "fanCtrl":fanCtrlCnt, "fanCirc":fanCircCnt, "conWat":conWatCnt, "extTmp":extTmpCnt, "leakWat":leakWatCnt])
+ } else {
+ inAutoList?.push("schMot":schMotCnt)
+ }
+ //log.debug "inAutoList: $inAutoList"
+ atomicState?.installedAutomations = inAutoList
+
+ def str = ""
+ str += (watchDogCnt > 0 || nModeCnt > 0 || schMotCnt > 0 || disCnt > 0) ? "Installed Automations:" : ""
+ str += (watchDogCnt > 0) ? "\n• Watchdog (Active)" : ""
+ str += (nModeCnt > 0) ? ((nModeCnt > 1) ? "\n• Nest Home/Away ($nModeCnt)" : "\n• Nest Home/Away (Active)") : ""
+ str += (schMotCnt > 0) ? "\n• Thermostat ($schMotCnt)" : ""
+ str += (disCnt > 0) ? "\n\nDisabled Automations ($disCnt)" : ""
+ return (str != "") ? str : null
+}
+
+def subscriber() {
+ subscribe(app, onAppTouch)
+}
+
+def reqSchedInfoRprt(child) {
+ //log.trace "reqSchedInfoRprt: (${child.device.label})"
+ def result = null
+ def tstat = getChildDevice(child.device.deviceNetworkId)
+ if (tstat) {
+ def str = ""
+ def chldSch = getChildApps()?.find { (!(it.getAutomationType() in ["nMode", "watchDog"]) && it?.getActiveScheduleState() && it?.getTstatAutoDevId() == tstat?.deviceNetworkId) }
+ if(chldSch) {
+ def actNum = chldSch?.getCurrentSchedule()
+ def tempScaleStr = " degrees"
+
+ def canHeat = tstat?.currentCanHeat.toString() == "true" ? true : false
+ def canCool = tstat?.currentCanCool.toString() == "true" ? true : false
+ def curMode = tstat?.currentThermostatMode.toString()
+ def curOper = tstat?.currentThermostatOperatingState.toString()
+ def curHum = tstat?.currentHumidity.toString()
+ def reqSenHeatSetPoint = chldSch?.getRemSenHeatSetTemp() ?: null
+ def reqSenCoolSetPoint = chldSch?.getRemSenCoolSetTemp() ?: null
+ def curZoneTemp = chldSch?.getRemoteSenTemp() ?: null
+
+ def schedData = chldSch?.getSchedData(actNum) ?: null
+ def tempSrc = chldSch?.getRemSenTempSrc() ?: null
+ def tempSrcStr = (actNum && tempSrc == "Schedule") ? "Schedule ${actNum}" : tempSrc
+ def schedDesc = schedVoiceDesc(actNum, schedData) ?: null
+
+ str += schedDesc ?: " There are No Schedules currently Active. "
+
+ if(tempSrcStr && curZoneTemp) {
+ def zTmp = curZoneTemp.toDouble()
+ str += "The ${tempSrcStr} has an ambient temperature of "
+ if(zTmp > 78.0 && zTmp <= 85.0) { str += "a scorching " }
+ else if(zTmp > 76.0 && zTmp <= 80.0) { str += "a roasting " }
+ else if(zTmp > 74.0 && zTmp <= 76.0) { str += "a balmy " }
+ else if(zTmp >= 68.0 && zTmp <= 74.0) { str += "a comfortable " }
+ else if(zTmp >= 64.0 && zTmp <= 68.0) { str += "a breezy " }
+ else if(zTmp >= 60.0 && zTmp < 64.0) { str += "a chilly " }
+ else if(zTmp < 60.0) { str += "a freezing " }
+ str += "${curZoneTemp}${tempScaleStr}"
+ str += curHum ? " with a humidity of ${curHum}%. " : ". "
+ if(zTmp < 64.0) { str += " (Make sure to dress warmly. " }
+ }
+
+ str += " The HVAC is currently "
+ str += curOper == "idle" ? " sitting idle " : " ${curOper} "
+ str += " in ${curMode} mode"
+ str += curMode != "off" ? " with " : ". "
+ str += canHeat && curMode in ["auto", "heat"] ? "the Heat set to ${reqSenHeatSetPoint}${tempScaleStr}" : ""
+ str += canHeat && canCool && curMode == "auto" ? " and " : ". "
+ str += canCool && curMode in ["auto", "cooling"] ? "the cool set to ${reqSenCoolSetPoint}${tempScaleStr}. " : ""
+
+ if (str != "") {
+ LogAction("reqSchedInfoRprt: Sending voice report for Zone info on (${tstat})", "info", true)
+ result = str
+ }
+ } else {
+ LogAction ("reqSchedInfoRprt: No Automation Schedules were found for the ${tstat} device", "warn", true)
+ result = "No Thermostat Automation Schedules were found for the ${tstat} device"
+ }
+ } else {
+ LogAction("reqSchedInfoRprt: The requested thermostat device was not found", "error", true)
+ result = "The requested thermostat device was not found"
+ }
+ return result
+}
+
+def schedVoiceDesc(num, data) {
+ def str = ""
+ str += data?.lbl ? " The active automation schedule is named ${data?.lbl}. " : ""
+ str += data?.ctemp || data?.htemp ? "The schedules desired temps" : ""
+ str += data?.ctemp ? " are a cool temp of ${data?.ctemp} degrees " : ""
+ str += data?.ctemp && data?.htemp ? " and " : ". "
+ str += data?.htemp ? " ${data?.ctemp ? "and" : "are"} a heat temp of ${data?.ctemp} degrees. " : ""
+
+ return str != "" ? str : null
+}
+
+def setPollingState() {
+ if(!atomicState?.thermostats && !atomicState?.protects && !atomicState?.weatherDevice && !atomicState?.cameras) {
+ LogAction("No Devices Selected...Polling is Off!!!", "info", true)
+ unschedule()
+ atomicState.pollingOn = false
+ } else {
+ if(!atomicState?.pollingOn) {
+ LogAction("Polling is Now ACTIVE!!!", "info", true)
+ atomicState.pollingOn = true
+ def pollTime = !settings?.pollValue ? 180 : settings?.pollValue.toInteger()
+ def pollStrTime = !settings?.pollStrValue ? 180 : settings?.pollStrValue.toInteger()
+ def weatherTimer = pollTime
+ if(atomicState?.weatherDevice) { weatherTimer = (settings?.pollWeatherValue ? settings?.pollWeatherValue.toInteger() : 900) }
+ def timgcd = gcd([pollTime, pollStrTime, weatherTimer])
+ def random = new Random()
+ def random_int = random.nextInt(60)
+ timgcd = (timgcd.toInteger() / 60) < 1 ? 1 : timgcd.toInteger()/60
+ def random_dint = random.nextInt(timgcd.toInteger())
+ LogAction("'Poll' scheduled using Cron (${random_int} ${random_dint}/${timgcd} * * * ?)", "info", true)
+ schedule("${random_int} ${random_dint}/${timgcd} * * * ?", poll) // this runs every timgcd minutes
+ poll(true)
+ }
+ }
+}
+
+private gcd(a, b) {
+ while (b > 0) {
+ long temp = b;
+ b = a % b;
+ a = temp;
+ }
+ return a;
+}
+
+private gcd(input = []) {
+ long result = input[0];
+ for(int i = 1; i < input.size; i++) result = gcd(result, input[i]);
+ return result;
+}
+
+def onAppTouch(event) {
+ poll(true)
+}
+
+def refresh(child = null) {
+ def devId = !child?.device?.deviceNetworkId ? child?.toString() : child?.device?.deviceNetworkId.toString()
+ LogAction("Refresh Received from Device...${devId}", "debug", true)
+ if(childDebug && child) { child?.log("refresh: ${devId}") }
+ return sendNestApiCmd(atomicState?.structures, "poll", "poll", 0, devId)
+}
+
+/************************************************************************************************
+| API/Device Polling Methods |
+*************************************************************************************************/
+
+def pollFollow() { if(isPollAllowed()) { poll() } }
+
+def pollWatcher(evt) {
+ if(isPollAllowed() && (ok2PollDevice() || ok2PollStruct())) { poll() }
+}
+
+def checkIfSwupdated() {
+ if(atomicState?.swVersion != appVersion()) {
+ def cApps = getChildApps()
+ if(cApps) {
+ cApps?.sort()?.each { chld ->
+ chld?.update()
+ }
+ }
+ updated()
+ }
+}
+
+def poll(force = false, type = null) {
+ if(isPollAllowed()) {
+ //unschedule("postCmd")
+ checkIfSwupdated()
+ def dev = false
+ def str = false
+ if(force == true) { forcedPoll(type) }
+ if( !force && !ok2PollDevice() && !ok2PollStruct() ) {
+ LogAction("No Device or Structure poll - Devices Last Updated: ${getLastDevicePollSec()} seconds ago... | Structures Last Updated ${getLastStructPollSec()} seconds ago...", "info", true)
+ }
+ else if(!force) {
+ def allowAsync = false
+ def metstr = "sync"
+ if(atomicState?.appData && atomicState?.appData?.pollMethod?.allowAsync) {
+ allowAsync = true
+ metstr = "async"
+ }
+ if(ok2PollStruct()) {
+ LogAction("Updating Structure Data...(Last Updated: ${getLastStructPollSec()} seconds ago) (${metstr})", "info", true)
+ if(allowAsync) {
+ str = queueGetApiData("str")
+ } else {
+ str = getApiData("str")
+ }
+ }
+ if(ok2PollDevice()) {
+ LogAction("Updating Device Data...(Last Updated: ${getLastDevicePollSec()} seconds ago) (${metstr})", "info", true)
+ if(allowAsync) {
+ dev = queueGetApiData("dev")
+ } else {
+ dev = getApiData("dev")
+ }
+ }
+ if(allowAsync) { return }
+ }
+ finishPoll(str, dev)
+ } else if(atomicState?.clientBlacklisted) {
+ LogAction("Client poll is blocked because it was blacklisted. Please contact the Developer to resolve the issue...", "warn", true)
+ finishPoll(false, true)
+ }
+}
+
+def finishPoll(str, dev) {
+ LogAction("finishPoll($str, $dev) received...", "info", false)
+ if(atomicState?.pollBlocked) { schedNextWorkQ(null); return }
+ if(dev || str || atomicState?.needChildUpd ) { updateChildData() }
+ updateWebStuff()
+ notificationCheck() //Checks if a notification needs to be sent for a specific event
+}
+
+def forcedPoll(type = null) {
+ LogAction("forcedPoll($type) received...", "warn", true)
+ def lastFrcdPoll = getLastForcedPollSec()
+ def pollWaitVal = !settings?.pollWaitVal ? 10 : settings?.pollWaitVal.toInteger()
+ if(lastFrcdPoll > pollWaitVal) { // This limits manual forces to 10 seconds or more
+ atomicState?.lastForcePoll = getDtNow()
+ atomicState?.pollBlocked = false
+ LogAction("Forcing Data Update... Last Forced Update was ${lastFrcdPoll} seconds ago.", "info", true)
+ if(type == "dev" || !type) {
+ LogAction("Forcing Update of Device Data...", "info", true)
+ getApiData("dev")
+ }
+ if(type == "str" || !type) {
+ LogAction("Forcing Update of Structure Data...", "info", true)
+ getApiData("str")
+ }
+ atomicState?.lastWebUpdDt = null
+ atomicState?.lastWeatherUpdDt = null
+ atomicState?.lastForecastUpdDt = null
+ schedNextWorkQ(null)
+ } else {
+ LogAction("Too Soon to Force Data Update!!!! It's only been (${lastFrcdPoll}) seconds of the minimum (${settings?.pollWaitVal})...", "debug", true)
+ atomicState.needStrPoll = true
+ atomicState.needDevPoll = true
+ }
+ updateChildData(true)
+}
+
+def postCmd() {
+ //log.trace "postCmd()"
+ poll()
+}
+
+def getApiData(type = null) {
+ //log.trace "getApiData($type)"
+ LogAction("getApiData($type)", "info", false)
+ def result = false
+ if(!type) { return result }
+
+ def tPath = (type == "str") ? "/structures" : "/devices"
+ try {
+ def params = [
+ uri: getNestApiUrl(),
+ path: "$tPath",
+ headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState?.authToken}"]
+ ]
+ if(type == "str") {
+ httpGet(params) { resp ->
+ if(resp?.status == 200) {
+ atomicState?.lastStrucDataUpd = getDtNow()
+ atomicState.needStrPoll = false
+ LogTrace("API Structure Resp.Data: ${resp?.data}")
+ apiIssueEvent(false)
+ if(!resp?.data?.equals(atomicState?.structData) || !atomicState?.structData) {
+ LogAction("API Structure Data HAS Changed... Updating State data...", "debug", true)
+ atomicState?.structData = resp?.data
+ atomicState.needChildUpd = true
+ result = true
+ }
+ } else {
+ LogAction("getApiStructureData - Received a diffent Response than expected: Resp (${resp?.status})", "error", true)
+ }
+ }
+ }
+ else if(type == "dev") {
+ httpGet(params) { resp ->
+ if(resp?.status == 200) {
+ atomicState?.lastDevDataUpd = getDtNow()
+ atomicState?.needDevPoll = false
+ LogTrace("API Device Resp.Data: ${resp?.data}")
+ apiIssueEvent(false)
+ if(!resp?.data.equals(atomicState?.deviceData) || !atomicState?.deviceData) {
+ LogAction("API Device Data HAS Changed... Updating State data...", "debug", true)
+ atomicState?.deviceData = resp?.data
+ result = true
+ }
+ } else {
+ LogAction("getApiDeviceData - Received a diffent Response than expected: Resp (${resp?.status})", "error", true)
+ }
+ }
+ }
+ }
+ catch(ex) {
+ apiIssueEvent(true)
+ atomicState.needChildUpd = true
+ if(ex instanceof groovyx.net.http.HttpResponseException) {
+ if(ex.message.contains("Too Many Requests")) {
+ log.warn "Received '${ex.message}' response code..."
+ }
+ } else {
+ log.error "getApiData (type: $type) Exception:", ex
+ if(type == "str") { atomicState.needStrPoll = true }
+ else if(type == "dev") { atomicState?.needDevPoll = true }
+ sendExceptionData(ex, "getApiData")
+ }
+ }
+ return result
+}
+
+def queueGetApiData(type = null, newUrl = null) {
+ //log.trace "queueGetApiData($type)"
+ LogAction("queueGetApiData($type,$newUrl)", "info", false)
+ def result = false
+ if(!type) { return result }
+
+ def tPath = (type == "str") ? "/structures" : "/devices"
+ try {
+ def theUrl = newUrl ?: getNestApiUrl()
+ def params = [
+ uri: theUrl,
+ path: "$tPath",
+ headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState?.authToken}"]
+ ]
+ if(type == "str") {
+ atomicState.qstrRequested = true
+ asynchttp_v1.get(processResponse, params, [ type: "str"])
+ result = true
+ }
+ else if(type == "dev") {
+ atomicState.qdevRequested = true
+ asynchttp_v1.get(processResponse, params, [ type: "dev"])
+ result = true
+ }
+ } catch(ex) {
+ log.error "queueGetApiData (type: $type) Exception:", ex
+ sendExceptionData(ex, "queueGetApiData")
+ }
+ return result
+}
+
+def processResponse(resp, data) {
+ LogAction("processResponse(${data?.type})", "info", false)
+ def str = false
+ def dev = false
+ def type = data?.type
+
+ try {
+ if(!type) { return }
+
+ if(resp?.status == 307) {
+ //log.trace "resp: ${resp.headers}"
+ def newUrl = resp?.headers?.Location?.split("\\?")
+ //LogTrace("NewUrl: ${newUrl[0]}")
+ queueGetApiData(type, newUrl[0])
+ return
+ }
+
+ if(resp?.status == 200) {
+ apiIssueEvent(false)
+ if(type == "str") {
+ atomicState?.lastStrucDataUpd = getDtNow()
+ atomicState.needStrPoll = false
+ LogTrace("API Structure Resp.Data: ${resp?.json}")
+ //log.trace "API Structure Resp.Data: ${resp?.json}"
+ if(!resp?.json?.equals(atomicState?.structData) || !atomicState?.structData) {
+ LogAction("API Structure Data HAS Changed... Updating State data...", "debug", true)
+ atomicState?.structData = resp?.json
+ atomicState.needChildUpd = true
+ str = true
+ }
+ atomicState.qstrRequested = false
+ }
+ if(type == "dev") {
+ atomicState?.lastDevDataUpd = getDtNow()
+ atomicState?.needDevPoll = false
+ LogTrace("API Device Resp.Data: ${resp?.json}")
+ //log.trace "API Device Resp.Data: ${resp?.json}"
+ if(!resp?.json?.equals(atomicState?.deviceData) || !atomicState?.deviceData) {
+ LogAction("API Device Data HAS Changed... Updating State data...", "debug", true)
+ atomicState?.deviceData = resp?.json
+ dev = true
+ }
+ atomicState.qdevRequested = false
+ }
+ } else {
+ def tstr = (type == "str") ? "Structure" : "Device"
+ LogAction("processResponse - Received a different Response than expected for $tstr poll: Resp (${resp?.status})", "error", true)
+ if(resp.hasError()) {
+ log.debug "raw response: $resp.errorData"
+ }
+ apiIssueEvent(true)
+ atomicState.needChildUpd = true
+ atomicState.qstrRequested = false
+ atomicState.qdevRequested = false
+ }
+ if((atomicState?.qdevRequested == false && atomicState?.qstrRequested == false) && (dev || atomicState?.needChildUpd)) { finishPoll(true, true) }
+
+ } catch (e) {
+ apiIssueEvent(true)
+ atomicState.needChildUpd = true
+ atomicState.qstrRequested = false
+ atomicState.qdevRequested = false
+ log.error "processResponse (type: $type) Exception:", e
+ if(type == "str") { atomicState.needStrPoll = true }
+ else if(type == "dev") { atomicState?.needDevPoll = true }
+ sendExceptionData(ex, "processResponse_${type}")
+ }
+}
+
+def schedUpdateChild() {
+ runIn(25, "updateChildData", [overwrite: true])
+}
+
+def generateMD5_A(String s) {
+ MessageDigest digest = MessageDigest.getInstance("MD5")
+ digest.update(s.bytes)
+ return digest.digest().toString()
+}
+
+def minDevVer2Str(val) {
+ def str = ""
+ def pCnt = 0
+ def list = []
+ str += "v"
+ val?.each {
+ list.add(it)
+ //str += "${it}"
+ }
+ log.debug "list: $list"
+}
+
+def updateChildData(force = false) {
+ LogAction("updateChildData()", "info", true)
+ if(atomicState?.pollBlocked) { return }
+ def nforce = atomicState?.needChildUpd
+ atomicState.needChildUpd = true
+ //log.warn "force: $force nforce: $nforce"
+ //unschedule("schedUpdateChild")
+ //runIn(40, "postCmd", [overwrite: true])
+ try {
+ atomicState?.lastChildUpdDt = getDtNow()
+ def useMt = !useMilitaryTime ? false : true
+ def dbg = !childDebug ? false : true
+ def nestTz = getNestTimeZone()?.toString()
+ def api = !apiIssues() ? false : true
+ def htmlInfo = getHtmlInfo()
+ def allowDbException = allowDbException()
+ def allowVoiceZoneRprt = allowVoiceZoneRprt()
+ def allowVoiceUsageRprt = allowVoiceUsageRprt()
+ def clientBl = atomicState?.clientBlacklisted == true ? true : false
+ getAllChildDevices()?.each {
+ def devId = it?.deviceNetworkId
+ if(atomicState?.thermostats && atomicState?.deviceData?.thermostats[devId]) {
+ def defmin = atomicState?."${physdevId}_safety_temp_min" ?: 0.0
+ def defmax = atomicState?."${physdevId}_safety_temp_max" ?: 0.0
+ def safetyTemps = [ "min":defmin, "max":defmax ]
+
+ def comfortDewpoint = settings?."${devId}_comfort_dewpoint_max" ?: 0.0
+ if(comfortDewpoint == 0) {
+ comfortDewpoint = settings?.locDesiredComfortDewpointMax ?: 0.0
+ }
+
+ def comfortHumidity = settings?."${devId}_comfort_humidity_max" ?: 80
+ def tData = ["data":atomicState?.deviceData?.thermostats[devId], "mt":useMt, "debug":dbg, "tz":nestTz, "apiIssues":api, "safetyTemps":safetyTemps, "comfortHumidity":comfortHumidity,
+ "comfortDewpoint":comfortDewpoint, "pres":locationPresence(), "childWaitVal":getChildWaitVal().toInteger(), "htmlInfo":htmlInfo, "allowDbException":allowDbException,
+ "latestVer":latestTstatVer()?.ver?.toString(), "allowVoiceUsageRprt":allowVoiceUsageRprt, "allowVoiceZoneRprt":allowVoiceZoneRprt, "clientBl":clientBl]
+ def oldTstatData = atomicState?."oldTstatData${devId}"
+ def tDataChecksum = generateMD5_A(tData.toString())
+ atomicState."oldTstatData${devId}" = tDataChecksum
+ tDataChecksum = atomicState."oldTstatData${devId}"
+ if(force || nforce || (oldTstatData != tDataChecksum)) {
+ atomicState?.tDevVer = it?.devVer() ?: ""
+ if(!atomicState?.tDevVer || (versionStr2Int(atomicState?.tDevVer) >= minDevVersions()?.thermostat?.val)) {
+ LogTrace("UpdateChildData >> Thermostat id: ${devId} | data: ${tData}")
+ //log.warn "oldTstatData: ${oldTstatData} tDataChecksum: ${tDataChecksum} force: $force nforce: $nforce"
+ it.generateEvent(tData)
+ } else {
+ LogAction("VERSION RESTRICTION: Your Thermostat Device Version (v${atomicState?.tDevVer}) is lower than the Minimum (v${minDevVersions()?.thermostat?.desc}) Required... Please Update the Device Code to latest version to resume operation!!!", "error", true)
+ return false
+ }
+ }
+ return true
+ }
+ else if(atomicState?.protects && atomicState?.deviceData?.smoke_co_alarms[devId]) {
+ def pData = ["data":atomicState?.deviceData?.smoke_co_alarms[devId], "mt":useMt, "debug":dbg, "showProtActEvts":(!showProtActEvts ? false : true),
+ "tz":nestTz, "htmlInfo":htmlInfo, "apiIssues":api, "allowDbException":allowDbException, "latestVer":latestProtVer()?.ver?.toString(), "clientBl":clientBl]
+ def oldProtData = atomicState?."oldProtData${devId}"
+ def pDataChecksum = generateMD5_A(pData.toString())
+ atomicState."oldProtData${devId}" = pDataChecksum
+ pDataChecksum = atomicState."oldProtData${devId}"
+ if(force || nforce || (oldProtData != pDataChecksum)) {
+ atomicState?.pDevVer = it?.devVer() ?: ""
+ if(!atomicState?.pDevVer || (versionStr2Int(atomicState?.pDevVer) >= minDevVersions()?.protect?.val)) {
+ LogTrace("UpdateChildData >> Protect id: ${devId} | data: ${pData}")
+ //log.warn "oldProtData: ${oldProtData} pDataChecksum: ${pDataChecksum} force: $force nforce: $nforce"
+ it.generateEvent(pData)
+ } else {
+ LogAction("VERSION RESTRICTION: Your Protect Device Version (v${atomicState?.pDevVer}) is lower than the Minimum of (v${minDevVersions()?.protect?.desc}) | Please Update the Device Code to latest version to resume operation!!!", "error", true)
+ return false
+ }
+ }
+ return true
+ }
+ else if(atomicState?.cameras && atomicState?.deviceData?.cameras[devId]) {
+ def camData = ["data":atomicState?.deviceData?.cameras[devId], "mt":useMt, "debug":dbg,
+ "tz":nestTz, "htmlInfo":htmlInfo, "apiIssues":api, "allowDbException":allowDbException, "latestVer":latestCamVer()?.ver?.toString(), "clientBl":clientBl]
+ def oldCamData = atomicState?."oldCamData${devId}"
+ def cDataChecksum = generateMD5_A(camData.toString())
+ if(force || nforce || (oldCamData != cDataChecksum)) {
+ atomicState?.camDevVer = it?.devVer() ?: ""
+ if(!atomicState?.camDevVer || (versionStr2Int(atomicState?.camDevVer) >= minDevVersions()?.camera?.val)) {
+ LogTrace("UpdateChildData >> Camera id: ${devId} | data: ${camData}")
+ it.generateEvent(camData)
+ atomicState."oldCamData${devId}" = cDataChecksum
+ } else {
+ LogAction("VERSION RESTRICTION: Your Camera Device Version (v${atomicState?.camDevVer}) is lower than the Minimum of (v${minDevVersions()?.camera?.desc}) | Please Update the Device Code to latest version to resume operation!!!", "error", true)
+ return false
+ }
+ }
+ return true
+ }
+ else if(atomicState?.presDevice && devId == getNestPresId()) {
+ def pData = ["debug":dbg, "tz":nestTz, "mt":useMt, "pres":locationPresence(), "apiIssues":api, "allowDbException":allowDbException, "latestVer":latestPresVer()?.ver?.toString(), "clientBl":clientBl]
+ def oldPresData = atomicState?."oldPresData${devId}"
+ def pDataChecksum = generateMD5_A(pData.toString())
+ atomicState."oldPresData${devId}" = pDataChecksum
+ pDataChecksum = atomicState."oldPresData${devId}"
+ if(force || nforce || (oldPresData != pDataChecksum)) {
+ atomicState?.presDevVer = it?.devVer() ?: ""
+ if(!atomicState?.presDevVer || (versionStr2Int(atomicState?.presDevVer) >= minDevVersions()?.presence?.val)) {
+ LogTrace("UpdateChildData >> Presence id: ${devId}")
+ //log.warn "oldPresData: ${oldPresData} pDataChecksum: ${pDataChecksum} force: $force nforce: $nforce"
+ it.generateEvent(pData)
+ } else {
+ LogAction("VERSION RESTRICTION: Your Presence Device Version (v${atomicState?.presDevVer}) is lower than the Minimum of (v${minDevVersions()?.presence?.desc}) | Please Update the Device Code to latest version to resume operation!!!", "error", true)
+ return false
+ }
+ }
+ return true
+ }
+ else if(atomicState?.weatherDevice && devId == getNestWeatherId()) {
+ def wData = ["weatCond":getWData(), "weatForecast":getWForecastData(), "weatAstronomy":getWAstronomyData(), "weatAlerts":getWAlertsData()]
+ def oldWeatherData = atomicState?."oldWeatherData${devId}"
+ def wDataChecksum = generateMD5_A(wData.toString())
+ atomicState."oldWeatherData${devId}" = wDataChecksum
+ wDataChecksum = atomicState."oldWeatherData${devId}"
+ if(force || nforce || (oldWeatherData != wDataChecksum)) {
+ atomicState?.weatDevVer = it?.devVer() ?: ""
+ if(!atomicState?.weatDevVer || (versionStr2Int(atomicState?.weatDevVer) >= minDevVersions()?.weather?.val)) {
+ //log.warn "oldWeatherData: ${oldWeatherData} wDataChecksum: ${wDataChecksum} force: $force nforce: $nforce"
+ LogTrace("UpdateChildData >> Weather id: ${devId}")
+ it.generateEvent(["data":wData, "tz":nestTz, "mt":useMt, "debug":dbg, "apiIssues":api, "htmlInfo":htmlInfo, "allowDbException":allowDbException, "weathAlertNotif":weathAlertNotif, "latestVer":latestWeathVer()?.ver?.toString(), "clientBl":clientBl])
+ } else {
+ LogAction("VERSION RESTRICTION: Your Weather Device Version (v${atomicState?.weatDevVer}) is lower than the Required Minimum (v${minDevVersions()?.weather?.desc}) | Please Update the Device Code to latest version to resume operation!!!", "error", true)
+ return false
+ }
+ }
+ return true
+ }
+
+ else if(atomicState?.vThermostats && atomicState?."vThermostat${devId}") {
+ def physdevId = atomicState?."vThermostatMirrorId${devId}"
+
+ if(atomicState?.thermostats && atomicState?.deviceData?.thermostats[physdevId]) {
+ def data = atomicState?.deviceData?.thermostats[physdevId]
+ def defmin = atomicState?."${physdevId}_safety_temp_min" ?: 0.0
+ def defmax = atomicState?."${physdevId}_safety_temp_max" ?: 0.0
+ def safetyTemps = [ "min":defmin, "max":defmax ]
+
+ def comfortDewpoint = settings?."${physdevId}_comfort_dewpoint_max" ?: 0.0
+ if(comfortDewpoint == 0) {
+ comfortDewpoint = settings?.locDesiredComfortDewpointMax ?: 0.0
+ }
+ def comfortHumidity = settings?."${physdevId}_comfort_humidity_max" ?: 80
+
+ def automationChildApp = getChildApps().find{ it.id == atomicState?."vThermostatChildAppId${devId}" }
+
+ if(automationChildApp.getRemoteSenAutomationEnabled()) {
+ def tempC = 0.0
+ def tempF = 0
+ if(getTemperatureScale() == "C") {
+ tempC = automationChildApp.getRemoteSenTemp()
+ tempF = (tempC * 9/5 + 32) as Integer
+ } else {
+ tempF = automationChildApp.getRemoteSenTemp()
+ tempC = (tempF - 32) * 5/9 as Double
+ }
+ data?.ambient_temperature_c = tempC
+ data?.ambient_temperature_f = tempF
+
+ def ctempC = 0.0
+ def ctempF = 0
+ if(getTemperatureScale() == "C") {
+ ctempC = automationChildApp.getRemSenCoolSetTemp()
+ ctempF = (ctempC * 9/5 + 32.0) as Integer
+ } else {
+ ctempF = automationChildApp.getRemSenCoolSetTemp()
+ ctempC = (ctempF - 32.0) * 5/9 as Double
+ }
+
+ def htempC = 0.0
+ def htempF = 0
+ if(getTemperatureScale() == "C") {
+ htempC = automationChildApp.getRemSenHeatSetTemp()
+ htempF = (htempC * 9/5 + 32.0) as Integer
+ } else {
+ htempF = automationChildApp.getRemSenHeatSetTemp()
+ htempC = (htempF - 32.0) * 5/9 as Double
+ }
+
+ if(data?.hvac_mode.toString() == "heat-cool") {
+ data?.target_temperature_high_f = ctempF
+ data?.target_temperature_low_f = htempF
+ data?.target_temperature_high_c = ctempC
+ data?.target_temperature_low_c = htempC
+ } else if(data?.hvac_mode.toString() == "cool") {
+ data?.target_temperature_f = ctempF
+ data?.target_temperature_c = ctempC
+ } else if(data?.hvac_mode.toString() == "heat") {
+ data?.target_temperature_f = htempF
+ data?.target_temperature_c = htempC
+ }
+ }
+
+ def tData = ["data":data, "mt":useMt, "debug":dbg, "tz":nestTz, "apiIssues":api, "safetyTemps":safetyTemps, "comfortHumidity":comfortHumidity,
+ "comfortDewpoint":comfortDewpoint, "pres":locationPresence(), "childWaitVal":getChildWaitVal().toInteger(), "htmlInfo":htmlInfo, "allowDbException":allowDbException,
+ "latestVer":latestvStatVer()?.ver?.toString(), "clientBl":clientBl]
+ def oldTstatData = atomicState?."oldvStatData${devId}"
+ def tDataChecksum = generateMD5_A(tData.toString())
+ atomicState."oldvStatData${devId}" = tDataChecksum
+ tDataChecksum = atomicState."oldvStatData${devId}"
+ if(force || nforce || (oldTstatData != tDataChecksum)) {
+ atomicState?.vtDevVer = it?.devVer() ?: ""
+ if(!atomicState?.vtDevVer || (versionStr2Int(atomicState?.vtDevVer) >= minDevVersions()?.vthermostat?.val)) {
+ LogTrace("UpdateChildData >> vThermostat id: ${devId} | data: ${tData}")
+ //log.warn "oldvStatData: ${oldvStatData} tDataChecksum: ${tDataChecksum} force: $force nforce: $nforce"
+ it.generateEvent(tData)
+ } else {
+ LogAction("VERSION RESTRICTION: Your vThermostat Device Version (v${atomicState?.vtDevVer}) is lower than the Minimum of (v${minDevVersions()?.vthermostat?.desc}) | Please Update the Device Code to latest version to resume operation!!!", "error", true)
+ return false
+ }
+ }
+ return true
+ }
+ }
+
+ else if(devId == getNestPresId()) {
+ return true
+ }
+ else if(devId == getNestWeatherId()) {
+ return true
+ }
+/* This causes NP exceptions depending if child has not finished being deleted or if items are removed from Nest
+ else if(!atomicState?.deviceData?.thermostats[devId] && !atomicState?.deviceData?.smoke_co_alarms[devId] && !atomicState?.deviceData?.cameras[devId]) {
+ LogAction("Device found ${devId} and connection removed", "warn", true)
+ return null
+ }
+*/
+ else {
+ LogAction("updateChildData() Device ${devId} found without claimed configuration", "warn", true)
+ return true
+ }
+ }
+ atomicState.needChildUpd = false
+ }
+ catch (ex) {
+ log.error "updateChildData Exception:", ex
+ sendExceptionData(ex, "updateChildData")
+ atomicState?.lastChildUpdDt = null
+ return
+ }
+ //unschedule("postCmd")
+ atomicState.needChildUpd = false
+}
+
+def locationPresence() {
+ if(atomicState?.structData[atomicState?.structures]) {
+ def data = atomicState?.structData[atomicState?.structures]
+ LogAction("Location Presence: ${data?.away}", "debug", false)
+ return data?.away.toString()
+ }
+ else { return null }
+}
+
+def apiIssues() {
+ def result = state?.apiIssuesList.toString().contains("true") ? true : false
+ if(result) {
+ LogAction("Nest API Issues Detected... (${getDtNow()})", "warn", true)
+ }
+ return result
+}
+
+def apiIssueEvent(issue, cmd = null) {
+ def list = state?.apiIssuesList ?: []
+ //log.debug "listIn: $list (${list?.size()})"
+ def listSize = 3
+ if(list?.size() < listSize) {
+ list.push(issue)
+ }
+ else if(list?.size() > listSize) {
+ def nSz = (list?.size()-listSize) + 1
+ //log.debug ">listSize: ($nSz)"
+ def nList = list?.drop(nSz)
+ //log.debug "nListIn: $list"
+ nList?.push(issue)
+ //log.debug "nListOut: $nList"
+ list = nList
+ }
+ else if(list?.size() == listSize) {
+ def nList = list?.drop(1)
+ nList?.push(issue)
+ list = nList
+ }
+
+ if(list) { state?.apiIssuesList = list }
+ //log.debug "listOut: $list"
+}
+
+def ok2PollDevice() {
+ if(atomicState?.pollBlocked) { return false }
+ if(atomicState?.needDevPoll) { return true }
+ def pollTime = !settings?.pollValue ? 180 : settings?.pollValue.toInteger()
+ def val = pollTime/3
+ if(val > 60) { val = 50 }
+ return ( ((getLastDevicePollSec() + val) > pollTime) ? true : false )
+}
+
+def ok2PollStruct() {
+ if(atomicState?.pollBlocked) { return false }
+ if(atomicState?.needStrPoll) { return true }
+ def pollStrTime = !settings?.pollStrValue ? 180 : settings?.pollStrValue.toInteger()
+ def val = pollStrTime/3
+ if(val > 60) { val = 50 }
+ return ( ((getLastStructPollSec() + val) > pollStrTime || !atomicState?.structData) ? true : false )
+}
+
+
+def isPollAllowed() {
+ return (atomicState?.pollingOn &&
+ !atomicState?.clientBlacklisted &&
+ (atomicState?.thermostats || atomicState?.protects || atomicState?.weatherDevice || atomicState?.cameras)) ? true : false
+}
+def getLastDevicePollSec() { return !atomicState?.lastDevDataUpd ? 840 : GetTimeDiffSeconds(atomicState?.lastDevDataUpd, null, "getLastDevicePollSec").toInteger() }
+def getLastStructPollSec() { return !atomicState?.lastStrucDataUpd ? 1000 : GetTimeDiffSeconds(atomicState?.lastStrucDataUpd, null, "getLastStructPollSec").toInteger() }
+def getLastForcedPollSec() { return !atomicState?.lastForcePoll ? 1000 : GetTimeDiffSeconds(atomicState?.lastForcePoll, null, "getLastForcedPollSec").toInteger() }
+def getLastChildUpdSec() { return !atomicState?.lastChildUpdDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastChildUpdDt, null, "getLastChildUpdSec").toInteger() }
+
+/************************************************************************************************
+| Nest API Commands |
+*************************************************************************************************/
+
+private cmdProcState(Boolean value) { atomicState?.cmdIsProc = value }
+private cmdIsProc() { return !atomicState?.cmdIsProc ? false : true }
+private getLastProcSeconds() { return atomicState?.cmdLastProcDt ? GetTimeDiffSeconds(atomicState?.cmdLastProcDt, null, "getLastProcSeconds") : 0 }
+
+def apiVar() {
+ def api = [
+ rootTypes: [ struct:"structures", cos:"devices/smoke_co_alarms", tstat:"devices/thermostats", cam:"devices/cameras", meta:"metadata" ],
+ cmdObjs: [ targetF:"target_temperature_f", targetC:"target_temperature_c", targetLowF:"target_temperature_low_f", setLabel:"label",
+ targetLowC:"target_temperature_low_c", targetHighF:"target_temperature_high_f", targetHighC:"target_temperature_high_c",
+ fanActive:"fan_timer_active", fanTimer:"fan_timer_timeout", hvacMode:"hvac_mode", away:"away", streaming:"is_streaming" ],
+ hvacModes: [ heat:"heat", cool:"cool", heatCool:"heat-cool", off:"off" ]
+ ]
+ return api
+}
+
+def setCamStreaming(child, streamOn) {
+ def devId = !child?.device?.deviceNetworkId ? child?.toString() : child?.device?.deviceNetworkId.toString()
+ def val = streamOn.toBoolean() ? true : false
+ LogAction("Nest Manager(setCamStreaming) - Setting Camera${!devId ? "" : " ${devId}"} Streaming to: (${val ? "On" : "Off"})", "debug", true)
+ if(childDebug && child) { child?.log("setCamStreaming( devId: ${devId}, StreamOn: ${val})") }
+ return sendNestApiCmd(devId, apiVar().rootTypes.cam, apiVar().cmdObjs.streaming, val, devId)
+}
+
+def setStructureAway(child, value, virtual=false) {
+ def devId = !child?.device?.deviceNetworkId ? null : child?.device?.deviceNetworkId.toString()
+ def val = value?.toBoolean()
+ def virt = virtual.toBoolean()
+
+ if(virt && atomicState?.vThermostats && devId) {
+ if(atomicState?."vThermostat${devId}") {
+ def pdevId = atomicState?."vThermostatMirrorId${devId}"
+ def pChild
+ if(pdevId) { pChild = getChildDevice(pdevId) }
+
+ if(pChild) {
+ if(val) {
+ pChild.away()
+ } else {
+ pChild.home()
+ }
+ } else { LogAction("setStructureAway - CANNOT Set Thermostat${pdevId} Presence to: (${val}) with child ${pChild}", "warn", true) }
+ }
+ } else {
+ LogAction("setStructureAway - Setting Nest Location:${!devId ? "" : " ${devId}"} (${val ? "Away" : "Home"})", "debug", true)
+ if(childDebug && child) { child?.log("setStructureAway: ${devId} | (${val})") }
+ if(val) {
+ return sendNestApiCmd(atomicState?.structures, apiVar().rootTypes.struct, apiVar().cmdObjs.away, "away", devId)
+ }
+ else {
+ return sendNestApiCmd(atomicState?.structures, apiVar().rootTypes.struct, apiVar().cmdObjs.away, "home", devId)
+ }
+ }
+}
+
+def setTstatLabel(child, label, virtual=false) {
+ def devId = !child?.device?.deviceNetworkId ? null : child?.device?.deviceNetworkId.toString()
+ def val = label
+ def virt = virtual.toBoolean()
+
+// This is not used anywhere. A command to set label is not available in the dth for a callback
+
+ LogAction("Nest Manager(setTstatLabel) - Setting Thermostat${!devId ? "" : " ${devId}"} Label to: (${val ? "On" : "Auto"})", "debug", true)
+ if(childDebug && child) { child?.log("setTstatLabel( devId: ${devId}, newLabel: ${val})") }
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.setLabel, val, devId)
+}
+
+def setFanMode(child, fanOn, virtual=false) {
+ def devId = !child?.device?.deviceNetworkId ? null : child?.device?.deviceNetworkId.toString()
+ def val = fanOn.toBoolean()
+ def virt = virtual.toBoolean()
+
+ if(virt && atomicState?.vThermostats && devId) {
+ if(atomicState?."vThermostat${devId}") {
+ def pdevId = atomicState?."vThermostatMirrorId${devId}"
+ def pChild
+ if(pdevId) { pChild = getChildDevice(pdevId) }
+
+ if(pChild) {
+ if(val) {
+ pChild.fanOn()
+ } else {
+ pChild.fanAuto()
+ }
+ } else { LogAction("setFanMode - CANNOT Set Thermostat${pdevId} FanMode to: (${fanOn}) with child ${pChild}", "warn", true) }
+ }
+ } else {
+ LogAction("setFanMode - Setting Thermostat${!devId ? "" : " ${devId}"} Fan Mode to: (${val ? "On" : "Auto"})", "debug", true)
+ if(childDebug && child) { child?.log("setFanMode( devId: ${devId}, fanOn: ${val})") }
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.fanActive, val, devId)
+ }
+}
+
+def setHvacMode(child, mode, virtual=false) {
+ def devId = !child?.device?.deviceNetworkId ? null : child?.device?.deviceNetworkId.toString()
+ def virt = virtual.toBoolean()
+
+ if(virt && atomicState?.vThermostats && devId) {
+ if(atomicState?."vThermostat${devId}") {
+ def pdevId = atomicState?."vThermostatMirrorId${devId}"
+ def pChild
+ if(pdevId) { pChild = getChildDevice(pdevId) }
+
+ if(pChild) {
+ switch (mode) {
+ case "auto":
+ pChild.auto()
+ break
+ case "heat":
+ pChild.heat()
+ break
+ case "cool":
+ pChild.cool()
+ break
+ case "off":
+ pChild.off()
+ break
+ case "emergency heat":
+ pChild.emergencyHeat()
+ break
+ default:
+ LogAction("setHvacMode Received an Invalid Request: ${mode}", "warn", true)
+ break
+ }
+ } else { LogAction("setHvacMode - CANNOT Set Thermostat${pdevId} Mode to: (${mode}) with child ${pChild}", "warn", true) }
+ }
+ } else {
+ LogAction("setHvacMode - Setting Thermostat${!devId ? "" : " ${devId}"} Mode to: (${mode})", "debug", true)
+ if(childDebug && child) { child?.log("setHvacMode( devId: ${devId}, mode: ${mode})") }
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.hvacMode, mode.toString(), devId)
+ }
+}
+
+def setTargetTemp(child, unit, temp, mode, virtual=false) {
+ def devId = !child?.device?.deviceNetworkId ? null : child?.device?.deviceNetworkId.toString()
+ def virt = virtual.toBoolean()
+
+ if(virt && atomicState?.vThermostats && devId) {
+ if(atomicState?."vThermostat${devId}") {
+ def pdevId = atomicState?."vThermostatMirrorId${devId}"
+ def pChild
+ if(pdevId) { pChild = getChildDevice(pdevId) }
+ def appId = atomicState?."vThermostatChildAppId${devId}"
+ def automationChildApp
+ if(appId) { automationChildApp = getChildApps().find{ it.id == appId } }
+ if(automationChildApp) {
+ def res = automationChildApp.remSenTempUpdate(temp,mode)
+ if(res) { return }
+ }
+ if(pChild) {
+ if(mode == 'cool') {
+ pChild.setCoolingSetpoint(temp)
+ } else if(mode == 'heat') {
+ pChild.setHeatingSetpoint(temp)
+ } else { LogAction("setTargetTemp - UNKNOWN MODE (${mode}) with child ${pChild}", "warn", true) }
+ } else { LogAction("setTargetTemp - CANNOT Set Thermostat${pdevId} Temp to: (${temp})${unit} Mode: (${mode}) with child ${pChild}", "warn", true) }
+ }
+ } else {
+ LogAction("setTargetTemp: ${devId} | (${temp})${unit} | virtual ${virtual}", "debug", true)
+ if(childDebug && child) { child?.log("setTargetTemp: ${devId} | (${temp})${unit}") }
+ if(unit == "C") {
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetC, temp, devId)
+ }
+ else {
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetF, temp, devId)
+ }
+ }
+}
+
+def setTargetTempLow(child, unit, temp, virtual=false) {
+ def devId = !child?.device?.deviceNetworkId ? null : child?.device?.deviceNetworkId.toString()
+ def virt = virtual.toBoolean()
+
+ if(virt && atomicState?.vThermostats && devId) {
+ if(atomicState?."vThermostat${devId}") {
+ def pdevId = atomicState?."vThermostatMirrorId${devId}"
+ def pChild
+ if(pdevId) { pChild = getChildDevice(pdevId) }
+
+ def appId = atomicState?."vThermostatChildAppId${devId}"
+ def automationChildApp
+ if(appId) { automationChildApp = getChildApps().find{ it.id == appId } }
+
+ if(automationChildApp) {
+ def res = automationChildApp.remSenTempUpdate(temp,"heat")
+ if(res) { return }
+ }
+
+ if(pChild) {
+ pChild.setHeatingSetpoint(temp)
+ } else { LogAction("setTargetTemp - CANNOT Set Thermostat${pdevId} HEAT Temp to: (${temp})${unit} with child ${pChild}", "warn", true) }
+ }
+ } else {
+ LogAction("setTargetTempLow: ${devId} | (${temp})${unit} | virtual ${virtual}", "debug", true)
+ if(childDebug && child) { child?.log("setTargetTempLow: ${devId} | (${temp})${unit}") }
+ if(unit == "C") {
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetLowC, temp, devId)
+ }
+ else {
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetLowF, temp, devId)
+ }
+ }
+}
+
+def setTargetTempHigh(child, unit, temp, virtual=false) {
+ def devId = !child?.device?.deviceNetworkId ? null : child?.device?.deviceNetworkId.toString()
+ def virt = virtual.toBoolean()
+
+ if(virt && atomicState?.vThermostats && devId) {
+ if(atomicState?."vThermostat${devId}") {
+ def pdevId = atomicState?."vThermostatMirrorId${devId}"
+ def pChild
+ if(pdevId) { pChild = getChildDevice(pdevId) }
+
+ def appId = atomicState?."vThermostatChildAppId${devId}"
+ def automationChildApp
+ if(appId) { automationChildApp = getChildApps().find{ it.id == appId } }
+
+ if(automationChildApp) {
+ def res = automationChildApp.remSenTempUpdate(temp,"cool")
+ if(res) { return }
+ }
+
+ if(pChild) {
+ pChild.setCoolingSetpoint(temp)
+ } else { LogAction("setTargetTemp - CANNOT Set Thermostat${pdevId} COOL Temp to: (${temp})${unit} with child ${pChild}", "warn", true) }
+ }
+ } else {
+ LogAction("setTargetTempHigh: ${devId} | (${temp})${unit} | virtual ${virtual}", "debug", true)
+ if(childDebug && child) { child?.log("setTargetTempHigh: ${devId} | (${temp})${unit}") }
+ if(unit == "C") {
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetHighC, temp, devId)
+ }
+ else {
+ return sendNestApiCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetHighF, temp, devId)
+ }
+ }
+}
+
+def sendNestApiCmd(cmdTypeId, cmdType, cmdObj, cmdObjVal, childId) {
+ def childDev = getChildDevice(childId)
+ if(childDebug && childDev) { childDev?.log("sendNestApiCmd... $cmdTypeId, $cmdType, $cmdObj, $cmdObjVal, $childId") }
+ try {
+ if(cmdTypeId) {
+ def qnum = getQueueNumber(cmdTypeId, childId)
+ if(qnum == -1 ) { return false }
+
+ if(!atomicState?."cmdQ${qnum}" ) { atomicState."cmdQ${qnum}" = [] }
+ def cmdQueue = atomicState?."cmdQ${qnum}"
+ def cmdData = [cmdTypeId?.toString(), cmdType?.toString(), cmdObj?.toString(), cmdObjVal]
+
+ if(cmdQueue?.contains(cmdData)) {
+ LogAction("Command Exists in queue... Skipping...", "warn", true)
+ if(childDev) { childDev?.log("Command Exists in queue ${qnum}... Skipping...", "warn") }
+ schedNextWorkQ(childId)
+ } else {
+ LogAction("Adding Command to Queue ${qnum}: $cmdTypeId, $cmdType, $cmdObj, $cmdObjVal, $childId", "info", false)
+ if(childDebug && childDev) { childDev?.log("Adding Command to Queue ${qnum}: $cmdData") }
+ atomicState?.pollBlocked = true
+ cmdQueue = atomicState?."cmdQ${qnum}"
+ cmdQueue << cmdData
+ atomicState."cmdQ${qnum}" = cmdQueue
+ atomicState?.lastQcmd = cmdData
+ schedNextWorkQ(childId)
+ }
+ return true
+
+ } else {
+ if(childDebug && childDev) { childDev?.log("sendNestApiCmd null cmdTypeId... $cmdTypeId, $cmdType, $cmdObj, $cmdObjVal, $childId") }
+ return false
+ }
+ }
+ catch (ex) {
+ log.error "sendNestApiCmd Exception:", ex
+ sendExceptionData(ex, "sendNestApiCmd")
+ if(childDebug && childDev) { childDev?.log("sendNestApiCmd Exception: ${ex.message}", "error") }
+ return false
+ }
+}
+
+private getQueueNumber(cmdTypeId, childId) {
+ def childDev = getChildDevice(childId)
+ if(!atomicState?.cmdQlist) { atomicState.cmdQlist = [] }
+ def cmdQueueList = atomicState?.cmdQlist
+ def qnum = cmdQueueList.indexOf(cmdTypeId)
+ if(qnum == -1) {
+ cmdQueueList = atomicState?.cmdQlist
+ cmdQueueList << cmdTypeId
+ atomicState.cmdQlist = cmdQueueList
+ qnum = cmdQueueList.indexOf(cmdTypeId)
+ atomicState?."cmdQ${qnum}" = null
+ setLastCmdSentSeconds(qnum, null)
+ setRecentSendCmd(qnum, null)
+ }
+ qnum = cmdQueueList.indexOf(cmdTypeId)
+ if(qnum == -1 ) { if(childDev) { childDev?.log("getQueueNumber: NOT FOUND" ) } }
+ if(childDebug && childDev) { childDev?.log("getQueueNumber: cmdTypeId ${cmdTypeId} is queue ${qnum}" ) }
+ return qnum
+}
+
+void schedNextWorkQ(childId) {
+ def childDev = getChildDevice(childId)
+ def cmdDelay = getChildWaitVal()
+ //
+ // This is throttling the rate of commands to the Nest service for this access token.
+ // If too many commands are sent Nest throttling could shut all write commands down for 1 hour to the device or structure
+ // This allows up to 3 commands if none sent in the last hour, then only 1 per 60 seconds. Nest could still
+ // throttle this if the battery state on device is low.
+ //
+
+ if(!atomicState?.cmdQlist) { atomicState.cmdQlist = [] }
+ def cmdQueueList = atomicState?.cmdQlist
+ def done = false
+ def nearestQ = 100
+ def qnum = -1
+ cmdQueueList.eachWithIndex { val, idx ->
+ if(done || !atomicState?."cmdQ${idx}" ) { return }
+ else {
+ if( (getRecentSendCmd(idx) > 0 ) || (getLastCmdSentSeconds(idx) > 60) ) {
+ runIn(cmdDelay*2, "workQueue", [overwrite: true])
+ qnum = idx
+ done = true
+ return
+ } else {
+ if((60 - getLastCmdSentSeconds(idx) + cmdDelay) < nearestQ) {
+ nearestQ = (60 - getLastCmdSentSeconds(idx) + cmdDelay)
+ qnum = idx
+ }
+ }
+ }
+ }
+ if(!done) {
+ runIn(nearestQ, "workQueue", [overwrite: true])
+ }
+ //if(childDebug && childDev) { childDev?.log("schedNextWorkQ queue: ${qnum} | recentSendCmd: ${getRecentSendCmd(qnum)} | last seconds: ${getLastCmdSentSeconds(qnum)} | cmdDelay: ${cmdDelay}") }
+ //if(childDev) { childDev?.log("schedNextWorkQ queue: ${qnum} | recentSendCmd: ${getRecentSendCmd(qnum)} | last seconds: ${getLastCmdSentSeconds(qnum)} | cmdDelay: ${cmdDelay}") }
+ LogAction("schedNextWorkQ queue: ${qnum} | recentSendCmd: ${getRecentSendCmd(qnum)} | last seconds: ${getLastCmdSentSeconds(qnum)} | cmdDelay: ${cmdDelay}", "info", true)
+}
+
+private getRecentSendCmd(qnum) {
+ return atomicState?."recentSendCmd${qnum}"
+}
+
+private setRecentSendCmd(qnum, val) {
+ atomicState."recentSendCmd${qnum}" = val
+ return
+}
+
+private getLastCmdSentSeconds(qnum) { return atomicState?."lastCmdSentDt${qnum}" ? GetTimeDiffSeconds(atomicState?."lastCmdSentDt${qnum}", null, "getLastCmdSentSeconds") : 3601 }
+
+private setLastCmdSentSeconds(qnum, val) {
+ atomicState."lastCmdSentDt${qnum}" = val
+ atomicState.lastCmdSentDt = val
+}
+
+void workQueue() {
+ //log.trace "workQueue..."
+ def cmdDelay = getChildWaitVal()
+
+ if(!atomicState?.cmdQlist) { atomicState?.cmdQlist = [] }
+ def cmdQueueList = atomicState?.cmdQlist
+
+ def qnum = 0
+ def done = false
+ def nearestQ = 100
+ cmdQueueList?.eachWithIndex { val, idx ->
+ if(done || !atomicState?."cmdQ${idx}" ) { return }
+ else {
+ if( (getRecentSendCmd(idx) > 0 ) || (getLastCmdSentSeconds(idx) > 60) ) {
+ qnum = idx
+ done = true
+ return
+ } else {
+ if((60 - getLastCmdSentSeconds(idx) + cmdDelay) < nearestQ) {
+ nearestQ = (60 - getLastCmdSentSeconds(idx) + cmdDelay)
+ qnum = idx
+ }
+ }
+ }
+ }
+
+ def allowAsync = false
+ def metstr = "sync"
+ if(atomicState?.appData && atomicState?.appData?.pollMethod?.allowAsync) {
+ allowAsync = true
+ metstr = "async"
+ }
+
+ LogAction("workQueue Run queue: ${qnum} $metstr", "trace", true)
+ if(!atomicState?."cmdQ${qnum}") { atomicState."cmdQ${qnum}" = [] }
+ def cmdQueue = atomicState?."cmdQ${qnum}"
+
+ try {
+ if(cmdQueue?.size() > 0) {
+ runIn(60, "workQueue", [overwrite: true]) // lost schedule catchall
+
+ if(!cmdIsProc()) {
+ cmdProcState(true)
+ atomicState?.pollBlocked = true
+ def cmd = cmdQueue?.remove(0)
+ atomicState?."cmdQ${qnum}" = cmdQueue
+ def cmdres
+
+ if(getLastCmdSentSeconds(qnum) > 3600) { setRecentSendCmd(qnum, 3) } // if nothing sent in last hour, reset 3 command limit
+
+ if(cmd[1] == "poll") {
+ atomicState.needStrPoll = true
+ atomicState.needDevPoll = true
+ atomicState.needChildUpd = true
+ cmdres = true
+ } else {
+ if(allowAsync) {
+ cmdres = queueProcNestApiCmd(getNestApiUrl(), cmd[0], cmd[1], cmd[2], cmd[3], qnum, cmd)
+ return
+ } else {
+ cmdres = procNestApiCmd(getNestApiUrl(), cmd[0], cmd[1], cmd[2], cmd[3], qnum)
+ }
+ }
+ finishWorkQ(cmd, cmdres)
+ } else { LogAction("workQueue: command is processing already", "warn", true) }
+
+ } else { atomicState.pollBlocked = false }
+ }
+ catch (ex) {
+ log.error "workQueue Exception Error:", ex
+ sendExceptionData(ex, "workQueue")
+ cmdProcState(false)
+ atomicState.needDevPoll = true
+ atomicState.needStrPoll = true
+ atomicState.needChildUpd = true
+ atomicState?.pollBlocked = false
+ runIn(60, "workQueue", [overwrite: true])
+ runIn((60 + 4), "postCmd", [overwrite: true])
+ return
+ }
+}
+
+def finishWorkQ(cmd, result) {
+ //log.trace "finishWorkQ..."
+ def cmdDelay = getChildWaitVal()
+
+ if(!atomicState?.cmdQlist) { atomicState?.cmdQlist = [] }
+ def cmdQueueList = atomicState?.cmdQlist
+
+ cmdProcState(false)
+ if( !result ) {
+ atomicState.needChildUpd = true
+ atomicState.pollBlocked = false
+ runIn((cmdDelay * 3), "postCmd", [overwrite: true])
+ }
+
+ atomicState.needDevPoll = true
+ if(cmd[1] == apiVar().rootTypes.struct.toString()) {
+ atomicState.needStrPoll = true
+ atomicState.needChildUpd = true
+ }
+
+ def qnum = 0
+ def done = false
+ def nearestQ = 100
+ cmdQueueList?.eachWithIndex { val, idx ->
+ if(done || !atomicState?."cmdQ${idx}" ) { return }
+ else {
+ if( (getRecentSendCmd(idx) > 0 ) || (getLastCmdSentSeconds(idx) > 60) ) {
+ qnum = idx
+ done = true
+ return
+ } else {
+ if((60 - getLastCmdSentSeconds(idx) + cmdDelay) < nearestQ) {
+ nearestQ = (60 - getLastCmdSentSeconds(idx) + cmdDelay)
+ qnum = idx
+ }
+ }
+ }
+ }
+
+ if(!atomicState?."cmdQ${qnum}") { atomicState?."cmdQ${qnum}" = [] }
+ def cmdQueue = atomicState?."cmdQ${qnum}"
+ if(cmdQueue?.size() == 0) {
+ atomicState.pollBlocked = false
+ atomicState.needChildUpd = true
+ runIn(cmdDelay * 2, "postCmd", [overwrite: true])
+ }
+ else { schedNextWorkQ(null) }
+
+ atomicState?.cmdLastProcDt = getDtNow()
+ if(cmdQueue?.size() > 10) {
+ sendMsg("Warning", "There is now ${cmdQueue?.size()} events in the Command Queue. Something must be wrong...")
+ LogAction("There is now ${cmdQueue?.size()} events in the Command Queue. Something must be wrong...", "warn", true)
+ }
+ return
+}
+
+def queueProcNestApiCmd(uri, typeId, type, obj, objVal, qnum, cmd, redir = false) {
+ LogTrace("procNestApiCmd: typeId: ${typeId}, type: ${type}, obj: ${obj}, objVal: ${objVal}, qnum: ${qnum}, isRedirUri: ${redir}")
+ def result = false
+ try {
+ def urlPath = "/${type}/${typeId}"
+ def data = new JsonBuilder("${obj}":objVal)
+ def params = [
+ uri: uri,
+ path: urlPath,
+ requestContentType: "application/json",
+ headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState?.authToken}"],
+ body: data.toString()
+ ]
+ LogAction("queueProcNestApiCmd Url: $uri | params: ${params}", "trace", true)
+ atomicState?.lastCmdSent = "$type: (${obj}: ${objVal})"
+
+ if(!redir && (getRecentSendCmd(qnum) > 0) && (getLastCmdSentSeconds(qnum) < 60)) {
+ def val = getRecentSendCmd(qnum)
+ val -= 1
+ setRecentSendCmd(qnum, val)
+ }
+ setLastCmdSentSeconds(qnum, getDtNow())
+
+ //log.trace "queueProcNestApiCmd time update recentSendCmd: ${getRecentSendCmd(qnum)} last seconds:${getLastCmdSentSeconds(qnum)} queue: ${qnum}"
+ def asyncargs = [
+ typeId: typeId,
+ type: type,
+ obj: obj,
+ objVal: objVal,
+ qnum: qnum,
+ cmd: cmd ]
+
+ asynchttp_v1.put(nestResponse, params, asyncargs)
+
+ } catch(ex) {
+ log.error "queueProcNestApiCmd (command: $cmd) Exception:", ex
+ sendExceptionData(ex, "queueProcNestApiCmd")
+ }
+}
+
+def nestResponse(resp, data) {
+ LogAction("nestResponse(${data?.cmd})", "info", false)
+ def typeId = data?.typeId
+ def type = data?.type
+ def obj = data?.obj
+ def objVal = data?.objVal
+ def qnum = data?.qnum
+ def command = data?.cmd
+ def result = false
+ try {
+ if(!command) { return }
+
+ if(resp?.status == 307) {
+ //log.trace "resp: ${resp.headers}"
+ def newUrl = resp?.headers?.Location?.split("\\?")
+ //LogTrace("NewUrl: ${newUrl[0]}")
+ queueProcNestApiCmd(newUrl[0], typeId, type, obj, objVal, qnum, command, true)
+ return
+ }
+
+ if(resp?.status == 200) {
+ LogAction("nestResponse Processed queue: ${qnum} ($type | ($obj:$objVal)) Successfully!!!", "info", true)
+ apiIssueEvent(false)
+ increaseCmdCnt()
+ atomicState?.lastCmdSentStatus = "ok"
+ result = true
+ } else {
+ apiIssueEvent(true)
+ atomicState?.lastCmdSentStatus = "failed"
+ if(resp?.status == 400) {
+ LogAction("nestResponse 'Bad Request' Exception: ${resp?.status} ($command)", "error", true)
+ } else {
+ LogAction("processResponse - Received a different Response than expected: Resp (${resp?.status})", "error", true)
+ }
+ if(resp.hasError()) {
+ log.debug "raw response: $resp.errorData"
+ }
+ }
+ finishWorkQ(command, result)
+
+ } catch (ex) {
+ log.error "nestResponse (command: $command) Exception:", ex
+ sendExceptionData(ex, "nestResponse")
+ apiIssueEvent(true)
+ atomicState?.lastCmdSentStatus = "failed"
+ }
+}
+
+def procNestApiCmd(uri, typeId, type, obj, objVal, qnum, redir = false) {
+ LogTrace("procNestApiCmd: typeId: ${typeId}, type: ${type}, obj: ${obj}, objVal: ${objVal}, qnum: ${qnum}, isRedirUri: ${redir}")
+
+ def result = false
+ try {
+ def urlPath = redir ? "" : "/${type}/${typeId}"
+ def data = new JsonBuilder("${obj}":objVal)
+ def params = [
+ uri: uri,
+ path: urlPath,
+ contentType: "application/json",
+ query: [ "auth": atomicState?.authToken ],
+ body: data.toString()
+ ]
+ LogAction("procNestApiCmd Url: $uri | params: ${params}", "trace", true)
+ atomicState?.lastCmdSent = "$type: (${obj}: ${objVal})"
+
+ if(!redir && (getRecentSendCmd(qnum) > 0) && (getLastCmdSentSeconds(qnum) < 60)) {
+ def val = getRecentSendCmd(qnum)
+ val -= 1
+ setRecentSendCmd(qnum, val)
+ }
+ setLastCmdSentSeconds(qnum, getDtNow())
+
+ //log.trace "procNestApiCmd time update recentSendCmd: ${getRecentSendCmd(qnum)} last seconds:${getLastCmdSentSeconds(qnum)} queue: ${qnum}"
+
+ httpPutJson(params) { resp ->
+ if(resp?.status == 307) {
+ def newUrl = resp?.headers?.location?.split("\\?")
+ LogTrace("NewUrl: ${newUrl[0]}")
+ if( procNestApiCmd(newUrl[0], typeId, type, obj, objVal, qnum, true) ) {
+ result = true
+ }
+ }
+ else if( resp?.status == 200) {
+ LogAction("procNestApiCmd Processed queue: ${qnum} ($type | ($obj:$objVal)) Successfully!!!", "info", true)
+ apiIssueEvent(false)
+ result = true
+ increaseCmdCnt()
+ atomicState?.lastCmdSentStatus = "ok"
+ } else {
+ if(resp?.status == 400) {
+ LogAction("procNestApiCmd 'Bad Request' Exception: ${resp?.status} ($type | $obj:$objVal)", "error", true)
+ } else {
+ LogAction("procNestApiCmd 'Unexpected' Response: ${resp?.status}", "warn", true)
+ }
+ apiIssueEvent(true)
+ atomicState?.lastCmdSentStatus = "failed"
+ }
+ }
+ }
+ catch (ex) {
+ log.error "procNestApiCmd Exception: ($type | $obj:$objVal)", ex
+ sendExceptionData(ex, "procNestApiCmd")
+ apiIssueEvent(true)
+ atomicState?.lastCmdSentStatus = "failed"
+ }
+ return result
+}
+
+def increaseCmdCnt() {
+ def cmdCnt = atomicState?.apiCommandCnt ?: 0
+ cmdCnt = cmdCnt?.toInteger()+1
+ LogAction("Api CmdCnt: $cmdCnt", "info", false)
+ atomicState?.apiCommandCnt = cmdCnt?.toInteger()
+}
+
+
+/************************************************************************************************
+| Push Notification Functions |
+*************************************************************************************************/
+def pushStatus() { return (settings?.recipients || settings?.phone || settings?.usePush) ? (settings?.usePush ? "Push Enabled" : "Enabled") : null }
+def getLastMsgSec() { return !atomicState?.lastMsgDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastMsgDt, null, "getLastMsgSec").toInteger() }
+def getLastUpdMsgSec() { return !atomicState?.lastUpdMsgDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastUpdMsgDt, null, "getLastUpdMsgSec").toInteger() }
+def getLastMisPollMsgSec() { return !atomicState?.lastMisPollMsgDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastMisPollMsgDt, null, "getLastMisPollMsgSec").toInteger() }
+def getRecipientsSize() { return !settings.recipients ? 0 : settings?.recipients.size() }
+
+//this is parent only method
+def getOk2Notify() { return (daysOk(settings?.quietDays) && notificationTimeOk() && modesOk(settings?.quietModes)) }
+def isMissedPoll() { return (getLastDevicePollSec() > atomicState?.misPollNotifyWaitVal.toInteger()) ? true : false }
+
+def notificationCheck() {
+ if((settings?.recipients || settings?.usePush) && getOk2Notify()) {
+ if(sendMissedPollMsg) { missedPollNotify() }
+ if(sendAppUpdateMsg && !appDevType()) { appUpdateNotify() }
+ }
+}
+
+def missedPollNotify() {
+ if(isMissedPoll()) {
+ if(getOk2Notify() && (getLastMisPollMsgSec() > atomicState?.misPollNotifyMsgWaitVal.toInteger())) {
+ sendMsg("Warning", "${app.name} has not refreshed data in the last (${getLastDevicePollSec()}) seconds. Please try refreshing manually or refresh Nest Authentication settings.")
+ atomicState?.lastMisPollMsgDt = getDtNow()
+ }
+ }
+}
+
+def appUpdateNotify() {
+ def appUpd = isAppUpdateAvail()
+ def protUpd = atomicState?.protects ? isProtUpdateAvail() : null
+ def presUpd = atomicState?.presDevice ? isPresUpdateAvail() : null
+ def tstatUpd = atomicState?.thermostats ? isTstatUpdateAvail() : null
+ def vtstatUpd = atomicState?.vThermostats ? isvTstatUpdateAvail() : null
+ def weatherUpd = atomicState?.weatherDevice ? isWeatherUpdateAvail() : null
+ def camUpd = atomicState?.cameras ? isCamUpdateAvail() : null
+ if((appUpd || protUpd || presUpd || tstatUpd || weatherUpd || camUpd || vtstatUpd) && (getLastUpdMsgSec() > atomicState?.updNotifyWaitVal.toInteger())) {
+ def str = ""
+ str += !appUpd ? "" : "\nManager App: v${atomicState?.appData?.updater?.versions?.app?.ver?.toString()}"
+ str += !protUpd ? "" : "\nProtect: v${atomicState?.appData?.updater?.versions?.protect?.ver?.toString()}"
+ str += !camUpd ? "" : "\nCamera: v${atomicState?.appData?.updater?.versions?.camera?.ver?.toString()}"
+ str += !presUpd ? "" : "\nPresence: v${atomicState?.appData?.updater?.versions?.presence?.ver?.toString()}"
+ str += !tstatUpd ? "" : "\nThermostat: v${atomicState?.appData?.updater?.versions?.thermostat?.ver?.toString()}"
+ str += !vtstatUpd ? "" : "\nVirtual Thermostat: v${atomicState?.appData?.updater?.versions?.vthermostat?.ver?.toString()}"
+ str += !weatherUpd ? "" : "\nWeather App: v${atomicState?.appData?.updater?.versions?.weather?.ver?.toString()}"
+ sendMsg("Info", "Nest Manager Update(s) are Available:${str}... \n\nPlease visit the IDE to Update your code...")
+ atomicState?.lastUpdMsgDt = getDtNow()
+ }
+}
+
+def updateHandler() {
+ //log.trace "updateHandler..."
+ if(atomicState?.isInstalled) {
+ if(atomicState?.appData?.updater?.updateType.toString() == "critical" && atomicState?.lastCritUpdateInfo?.ver.toInteger() != atomicState?.appData?.updater?.updateVer.toInteger()) {
+ sendMsg("Critical", "There are Critical Updates available for the Nest Manager Application!!! Please visit the IDE and make sure to update the App and Devices Code...")
+ atomicState?.lastCritUpdateInfo = ["dt":getDtNow(), "ver":atomicState?.appData?.updater?.updateVer?.toInteger()]
+ }
+ if(atomicState?.appData?.updater?.updateMsg != "" && atomicState?.appData?.updater?.updateMsg != atomicState?.lastUpdateMsg) {
+ if(getLastUpdateMsgSec() > 86400) {
+ sendMsg("Info", "${atomicState?.updater?.updateMsg}")
+ atomicState?.lastUpdateMsgDt = getDtNow()
+ }
+ }
+ }
+}
+
+// parent only method
+def sendMsg(msgType, msg, people = null, sms = null, push = null, brdcast = null) {
+ //log.trace "sendMsg..."
+ try {
+ if(!getOk2Notify()) {
+ LogAction("No Notifications will be sent during Quiet Time...", "info", true)
+ } else {
+ def newMsg = "${msgType}: ${msg}"
+ if(!brdcast) {
+ def who = people ? people : settings?.recipients
+ if(location.contactBookEnabled) {
+ if(who) {
+ sendNotificationToContacts(newMsg, who)
+ atomicState?.lastMsg = newMsg
+ atomicState?.lastMsgDt = getDtNow()
+ LogAction("Push Message Sent: ${atomicState?.lastMsgDt}", "debug", true)
+ }
+ } else {
+ LogAction("ContactBook is NOT Enabled on your SmartThings Account...", "warn", true)
+ if(push) {
+ sendPush(newMsg)
+ atomicState?.lastMsg = newMsg
+ atomicState?.lastMsgDt = getDtNow()
+ LogAction("Push Message Sent: ${atomicState?.lastMsgDt}", "debug", true)
+ }
+ else if(sms) {
+ sendSms(sms, newMsg)
+ atomicState?.lastMsg = newMsg
+ atomicState?.lastMsgDt = getDtNow()
+ LogAction("SMS Message Sent: ${atomicState?.lastMsgDt}", "debug", true)
+ }
+ }
+ } else {
+ sendPushMessage(newMsg)
+ LogAction("Broadcast Message Sent: ${newMsg} - ${atomicState?.lastMsgDt}", "debug", true)
+ }
+ }
+ } catch (ex) {
+ log.error "sendMsg Exception:", ex
+ sendExceptionData(ex, "sendMsg")
+ }
+}
+
+def getLastWebUpdSec() { return !atomicState?.lastWebUpdDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastWebUpdDt, null, "getLastWebUpdSec").toInteger() }
+def getLastWeatherUpdSec() { return !atomicState?.lastWeatherUpdDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastWeatherUpdDt, null, "getLastWeatherUpdSec").toInteger() }
+def getLastForecastUpdSec() { return !atomicState?.lastForecastUpdDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastForecastUpdDt, null, "getLastForecastUpdSec").toInteger() }
+def getLastAnalyticUpdSec() { return !atomicState?.lastAnalyticUpdDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastAnalyticUpdDt, null, "getLastAnalyticUpdSec").toInteger() }
+def getLastUpdateMsgSec() { return !atomicState?.lastUpdateMsgDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastUpdateMsgDt, null, "getLastUpdateMsgSec").toInteger() }
+
+def getStZipCode() { return location?.zipCode.toString() }
+def getNestZipCode() { return atomicState?.structData[atomicState?.structures].postal_code ? atomicState?.structData[atomicState?.structures]?.postal_code.toString() : "" }
+def getNestTimeZone() { return atomicState?.structData[atomicState?.structures].time_zone ? atomicState?.structData[atomicState?.structures].time_zone : null}
+
+def updateWebStuff(now = false) {
+ //log.trace "updateWebStuff..."
+ if(!atomicState?.appData || (getLastWebUpdSec() > (3600*4))) {
+ if(now) {
+ getWebFileData()
+ } else {
+ //if(canSchedule()) { runIn(45, "getWebFileData", [overwrite: true]) } //This reads a JSON file from a web server with timing values and version numbers
+ getWebFileData(false)
+ }
+ }
+ if(optInAppAnalytics && atomicState?.isInstalled) {
+ if(getLastAnalyticUpdSec() > (3600*24)) {
+ sendInstallData()
+ }
+ }
+ if(atomicState?.weatherDevice && getLastWeatherUpdSec() > (settings?.pollWeatherValue ? settings?.pollWeatherValue.toInteger() : 900)) {
+ if(now) {
+ getWeatherConditions(now)
+ } else {
+ if(canSchedule()) { runIn(20, "getWeatherConditions", [overwrite: true]) }
+ }
+ }
+ if(atomicState?.feedbackPending) {
+ runIn(37, "sendFeedbackData", [overwrite: true])
+ }
+}
+
+def getWeatherConditions(force = false) {
+ //log.trace "getWeatherConditions..."
+ if(atomicState?.weatherDevice) {
+ try {
+ LogAction("Retrieving Latest Local Weather Conditions", "info", true)
+ def loc = ""
+ def curWeather = ""
+ def curForecast = ""
+ def curAstronomy = ""
+ def curAlerts = ""
+ def err = false
+ if(custLocStr) {
+ loc = custLocStr
+ curWeather = getWeatherFeature("conditions", loc)
+ curAlerts = getWeatherFeature("alerts", loc)
+ } else {
+ curWeather = getWeatherFeature("conditions")
+ curAlerts = getWeatherFeature("alerts")
+ }
+ if(getLastForecastUpdSec() > (1800)) {
+ if(custLocStr) {
+ loc = custLocStr
+ curForecast = getWeatherFeature("forecast", loc)
+ curAstronomy = getWeatherFeature("astronomy", loc)
+ } else {
+ curForecast = getWeatherFeature("forecast")
+ curAstronomy = getWeatherFeature("astronomy")
+ }
+ if(curForecast && curAstronomy) {
+ atomicState?.curForecast = curForecast
+ atomicState?.curAstronomy = curAstronomy
+ atomicState?.lastForecastUpdDt = getDtNow()
+ } else {
+ LogAction("Could Not Retrieve Latest Local Forecast or astronomy Conditions", "warn", true)
+ err = true
+ }
+ }
+ if(curWeather && curAlerts) {
+ atomicState?.curWeather = curWeather
+ atomicState?.curAlerts = curAlerts
+ if(!err) { atomicState?.lastWeatherUpdDt = getDtNow() }
+ } else {
+ LogAction("Could Not Retrieve Latest Local Weather Conditions or alerts", "warn", true)
+ return false
+ }
+ if(curWeather || curAstronomy || curForecast || curAlerts) {
+ atomicState.needChildUpd = true
+ if(!force) { runIn(21, "postCmd", [overwrite: true]) }
+ return true
+ }
+ }
+ catch (ex) {
+ log.error "getWeatherConditions Exception:", ex
+ sendExceptionData(ex, "getWeatherConditions")
+ return false
+ }
+ } else { return false }
+}
+
+def getWData() {
+ if(atomicState?.curWeather) {
+ return atomicState?.curWeather
+ } else {
+ if(getWeatherConditions(true)) {
+ return atomicState?.curWeather
+ }
+ }
+
+}
+
+def getWForecastData() {
+ if(atomicState?.curForecast) {
+ return atomicState?.curForecast
+ } else {
+ if(getWeatherConditions(true)) {
+ return atomicState?.curForecast
+ }
+ }
+}
+
+def getWAstronomyData() {
+ if(atomicState?.curAstronomy) {
+ return atomicState?.curAstronomy
+ } else {
+ if(getWeatherConditions(true)) {
+ return atomicState?.curAstronomy
+ }
+ }
+}
+
+def getWAlertsData() {
+ if(atomicState?.curAlerts) {
+ return atomicState?.curAlerts
+ } else {
+ if(getWeatherConditions(true)) {
+ return atomicState?.curAlerts
+ }
+ }
+}
+
+def getWeatherDeviceInst() {
+ return atomicState?.weatherDevice ? true : false
+}
+
+def getWebFileData(now = true) {
+ //log.trace "getWebFileData..."
+ def params = [ uri: "https://raw.githubusercontent.com/tonesto7/nest-manager/${gitBranch()}/Data/appData.json", contentType: 'application/json' ]
+ def result = false
+ try {
+ def allowAsync = false
+ def metstr = "sync"
+ if(!now && atomicState?.appData && atomicState?.appData?.pollMethod?.allowAsync) {
+ allowAsync = true
+ metstr = "async"
+ }
+
+ LogAction("Getting Latest Data from appData.json File...(${metstr})", "info", true)
+
+ if(now || !allowAsync) {
+ httpGet(params) { resp ->
+ result = webResponse(resp, [type:null])
+ }
+ } else {
+ asynchttp_v1.get(webResponse, params, [type:"async"])
+ }
+ }
+ catch (ex) {
+ if(ex instanceof groovyx.net.http.HttpResponseException) {
+ log.warn "appParams.json file not found..."
+ } else {
+ log.error "getWebFileData Exception:", ex
+ }
+ sendExceptionData(ex, "getWebFileData")
+ }
+ return result
+}
+
+def webResponse(resp, data) {
+ LogAction("webResponse(${data?.type})", "info", false)
+ def result = false
+ if(resp?.status == 200) {
+ def newdata = resp?.data
+ if(data?.type == "async") { newdata = resp?.json }
+ LogTrace("webResponse Resp: ${newdata}")
+ //log.debug "appdata: ${newdata}"
+ if(!newdata?.equals(atomicState?.appData)) {
+ LogAction("appData.json File HAS Changed...", "info", true)
+ atomicState?.appData = newdata
+ clientBlacklisted()
+ updateHandler()
+ broadcastCheck()
+ helpHandler()
+ } else { LogAction("appData.json did not change....", "info", true) }
+ atomicState?.lastWebUpdDt = getDtNow()
+ result = true
+ } else {
+ LogAction("Get failed of appData.json File... status: ${resp?.status}", "warn", true)
+ }
+ return result
+}
+
+def clientBlacklisted() {
+ if(atomicState?.clientBlacklisted == null) { atomicState?.clientBlacklisted == false }
+ def curBlState = atomicState?.clientBlacklisted
+ if(atomicState?.isInstalled && atomicState?.appData?.clientBL) {
+ def clientList = atomicState?.appData?.clientBL?.clients
+ if(clientList != null || clientList != []) {
+ def isBL = (atomicState?.installationId in clientList) ? true : false
+ if(curBlState != isBL) {
+ atomicState?.clientBlacklisted = isBL
+ }
+ }
+ }
+}
+
+def broadcastCheck() {
+ if(atomicState?.isInstalled && atomicState?.appData.broadcast) {
+ if(atomicState?.appData?.broadcast?.msgId != null && atomicState?.lastBroadcastId != atomicState?.appData?.broadcast?.msgId) {
+ sendMsg(atomicState?.appData?.broadcast?.type.toString().capitalize(), atomicState?.appData?.broadcast?.message.toString(), null, null, null, true)
+ atomicState?.lastBroadcastId = atomicState?.appData?.broadcast?.msgId
+ }
+ }
+}
+
+def helpHandler() {
+ if(atomicState?.appData?.help) {
+ atomicState.showHelp = atomicState?.appData?.help?.showHelp == "false" ? false : true
+ }
+}
+
+def getHtmlInfo() {
+ if(atomicState?.appData?.html?.cssUrl && atomicState?.appData?.html?.cssVer && atomicState?.appData?.html?.chartJsUrl && atomicState?.appData?.html?.chartJsVer ) {
+ return ["cssUrl":atomicState?.appData?.html?.cssUrl, "cssVer":atomicState?.appData?.html?.cssVer, "chartJsUrl":atomicState?.appData?.html?.chartJsUrl, "chartJsVer":atomicState?.appData?.html?.chartJsVer]
+ } else {
+ if(getWebFileData()) {
+ return ["cssUrl":atomicState?.appData?.html?.cssUrl, "cssVer":atomicState?.appData?.html?.cssVer, "chartJsUrl":atomicState?.appData?.html?.chartJsUrl, "chartJsVer":atomicState?.appData?.html?.chartJsVer]
+ }
+ }
+}
+
+def allowDbException() {
+ if(atomicState?.appData?.database?.disableExceptions != null) {
+ return atomicState?.appData?.database?.disableExceptions == true ? false : true
+ } else {
+ if(getWebFileData()) {
+ return atomicState?.appData?.database?.disableExceptions == true ? false : true
+ }
+ }
+}
+
+def ver2IntArray(val) {
+ def ver = val?.split("\\.")
+ return [maj:"${ver[0]?.toInteger()}",min:"${ver[1]?.toInteger()}",rev:"${ver[2]?.toInteger()}"]
+}
+def versionStr2Int(str) { return str ? str.toString().replaceAll("\\.", "").toInteger() : null }
+
+def getChildWaitVal() { return settings?.tempChgWaitVal ? settings?.tempChgWaitVal.toInteger() : 4 }
+
+def getAskAlexaQueueEnabled() {
+ if(!parent) { return (atomicState?.appData?.aaPrefs?.enAaMsgQueue == true) ? true : false }
+}
+
+def allowVoiceUsageRprt() {
+ if(!parent) { return (atomicState?.appData?.reportPrefs?.disVoiceUsageRprt == true || settings?.disableVoiceUsageRprt == true) ? false : true }
+}
+
+def allowVoiceZoneRprt() {
+ if(!parent) { return (atomicState?.appData?.reportPrefs?.disVoiceZoneRprt == true || settings?.disableVoiceZoneRprt == true) ? false : true }
+}
+
+def isCodeUpdateAvailable(newVer, curVer, type) {
+ def result = false
+ def latestVer
+ if(newVer && curVer) {
+ def versions = [newVer, curVer]
+ if(newVer != curVer) {
+ latestVer = versions?.max { a, b ->
+ def verA = a?.tokenize('.')
+ def verB = b?.tokenize('.')
+ def commonIndices = Math.min(verA?.size(), verB?.size())
+ for (int i = 0; i < commonIndices; ++i) {
+ //log.debug "comparing $numA and $numB"
+ if(verA[i]?.toInteger() != verB[i]?.toInteger()) {
+ return verA[i]?.toInteger() <=> verB[i]?.toInteger()
+ }
+ }
+ verA?.size() <=> verB?.size()
+ }
+ result = (latestVer == newVer) ? true : false
+ }
+ }
+ //log.debug "type: $type | newVer: $newVer | curVer: $curVer | newestVersion: ${latestVer} | result: $result"
+ return result
+}
+
+def isAppUpdateAvail() {
+ if(isCodeUpdateAvailable(atomicState?.appData?.updater?.versions?.app?.ver, appVersion(), "manager")) { return true }
+ return false
+}
+
+def isPresUpdateAvail() {
+ if(isCodeUpdateAvailable(atomicState?.appData?.updater?.versions?.presence?.ver, atomicState?.presDevVer, "presence")) { return true }
+ return false
+}
+
+def isProtUpdateAvail() {
+ if(isCodeUpdateAvailable(atomicState?.appData?.updater?.versions?.protect?.ver, atomicState?.pDevVer, "protect")) { return true }
+ return false
+}
+
+def isCamUpdateAvail() {
+ if(isCodeUpdateAvailable(atomicState?.appData?.updater?.versions?.camera?.ver, atomicState?.camDevVer, "camera")) { return true }
+ return false
+}
+
+def isTstatUpdateAvail() {
+ if(isCodeUpdateAvailable(atomicState?.appData?.updater?.versions?.thermostat?.ver, atomicState?.tDevVer, "thermostat")) { return true }
+ return false
+}
+
+def isvTstatUpdateAvail() {
+ if(isCodeUpdateAvailable(atomicState?.appData?.updater?.versions?.vthermostat?.ver, atomicState?.vtDevVer, "vthermostat")) { return true }
+ return false
+}
+
+def isWeatherUpdateAvail() {
+ if(isCodeUpdateAvailable(atomicState?.appData?.updater?.versions?.weather?.ver, atomicState?.weatDevVer, "weather")) { return true }
+ return false
+}
+
+/************************************************************************************************
+| This Section Discovers all structures and devices on your Nest Account. |
+| It also Adds/Removes Devices from ST |
+*************************************************************************************************/
+
+def getNestStructures() {
+ LogTrace("Getting Nest Structures")
+ def struct = [:]
+ def thisstruct = [:]
+ try {
+ if(ok2PollStruct()) { getApiData("str") }
+ if(atomicState?.structData) {
+ def structs = atomicState?.structData
+ structs?.eachWithIndex { struc, index ->
+ def strucId = struc?.key
+ def strucData = struc?.value
+
+ def dni = [strucData?.structure_id].join('.')
+ struct[dni] = strucData?.name.toString()
+
+ if(strucData?.structure_id.toString() == settings?.structures.toString()) {
+ thisstruct[dni] = strucData?.name.toString()
+ } else {
+ if(atomicState?.structures) {
+ if(strucData?.structure_id?.toString() == atomicState?.structures?.toString()) {
+ thisstruct[dni] = strucData?.name.toString()
+ }
+ } else {
+ if(!settings?.structures) {
+ thisstruct[dni] = strucData?.name.toString()
+ }
+ }
+ }
+ }
+ if(atomicState?.thermostats || atomicState?.protects || atomicState?.cameras || atomicState?.vThermostats || atomicState?.presDevice || atomicState?.weatherDevice || isAutoAppInst() ) { // if devices are configured, you cannot change the structure until they are removed
+ struct = thisstruct
+ }
+ if(ok2PollDevice()) { getApiData("dev") }
+ } else { LogAction("Missing: atomicState.structData ${atomicState?.structData}", "warn", true) }
+
+ } catch (ex) {
+ log.error "getNestStructures Exception:", ex
+ sendExceptionData(ex, "getNestStructures")
+ }
+ return struct
+}
+
+def getNestThermostats() {
+ LogTrace("Getting Thermostat list")
+ def stats = [:]
+ def tstats = atomicState?.deviceData?.thermostats
+ LogTrace("Found ${tstats?.size()} Thermostats...")
+ tstats.each { stat ->
+ def statId = stat?.key
+ def statData = stat?.value
+
+ def adni = [statData?.device_id].join('.')
+ if(statData?.structure_id == settings?.structures) {
+ stats[adni] = getThermostatDisplayName(statData)
+ }
+ }
+ return stats
+}
+
+def getNestProtects() {
+ LogTrace("Getting Nest Protect List...")
+ def protects = [:]
+ def nProtects = atomicState?.deviceData?.smoke_co_alarms
+ LogTrace("Found ${nProtects?.size()} Nest Protects...")
+ nProtects.each { dev ->
+ def devId = dev?.key
+ def devData = dev?.value
+
+ def bdni = [devData?.device_id].join('.')
+ if(devData?.structure_id == settings?.structures) {
+ protects[bdni] = getProtectDisplayName(devData)
+ }
+ }
+ return protects
+}
+
+def getNestCameras() {
+ LogTrace("Getting Nest Camera List...")
+ def cameras = [:]
+ def nCameras = atomicState?.deviceData?.cameras
+ LogTrace("Found ${nCameras?.size()} Nest Cameras...")
+ nCameras.each { dev ->
+ def devId = dev?.key
+ def devData = dev?.value
+
+ def bdni = [devData?.device_id].join('.')
+ if(devData?.structure_id == settings?.structures) {
+ cameras[bdni] = getCameraDisplayName(devData)
+ }
+ }
+ return cameras
+}
+
+def statState(val) {
+ def stats = [:]
+ def tstats = getNestThermostats()
+ tstats.each { stat ->
+ def statId = stat?.key
+ def statData = stat?.value
+ val.each { st ->
+ if(statId == st) {
+ def adni = [statId].join('.')
+ stats[adni] = statData
+ }
+ }
+ }
+ return stats
+}
+
+def coState(val) {
+ def protects = [:]
+ def nProtects = getNestProtects()
+ nProtects.each { dev ->
+ val.each { pt ->
+ if(dev?.key == pt) {
+ def bdni = [dev?.key].join('.')
+ protects[bdni] = dev?.value
+ }
+ }
+ }
+ return protects
+}
+
+def camState(val) {
+ def cams = [:]
+ def nCameras = getNestCameras()
+ nCameras.each { dev ->
+ val.each { cm ->
+ if(dev?.key == cm) {
+ def bdni = [dev?.key].join('.')
+ cams[bdni] = dev?.value
+ }
+ }
+ }
+ return cams
+}
+
+def getThermostatDisplayName(stat) {
+ if(stat?.name) { return stat.name.toString() }
+}
+
+def getProtectDisplayName(prot) {
+ if(prot?.name) { return prot.name.toString() }
+}
+
+def getCameraDisplayName(cam) {
+ if(cam?.name) { return cam.name.toString() }
+}
+
+def getNestTstatDni(dni) {
+ //log.debug "getNestTstatDni: $dni"
+ def d1 = getChildDevice(dni?.key.toString())
+ if(d1) { return dni?.key.toString() }
+ else {
+ def devt = appDevName()
+ return "NestThermostat-${dni?.value.toString()}${devt} | ${dni?.key.toString()}"
+ }
+ LogAction("getNestTstatDni Issue...", "warn", true)
+}
+
+def getNestProtDni(dni) {
+ def d2 = getChildDevice(dni?.key.toString())
+ if(d2) { return dni?.key.toString() }
+ else {
+ def devt = appDevName()
+ return "NestProtect-${dni?.value.toString()}${devt} | ${dni?.key.toString()}"
+ }
+ LogAction("getNestProtDni Issue...", "warn", true)
+}
+
+def getNestCamDni(dni) {
+ def d5 = getChildDevice(dni?.key.toString())
+ if(d5) { return dni?.key.toString() }
+ else {
+ def devt = appDevName()
+ return "NestCam-${dni?.value.toString()}${devt} | ${dni?.key.toString()}"
+ }
+ LogAction("getNestCamDni Issue...", "warn", true)
+}
+
+def getNestvStatDni(dni) {
+ def d6 = getChildDevice(dni.key.toString())
+ if(d6) { return dni?.key.toString() }
+ else {
+ def devt = appDevName()
+ return "NestvThermostat-${dni?.value.toString()}${devt} | ${dni?.key.toString()}"
+ }
+ LogAction("getNestvStatDni Issue...", "warn", true)
+}
+
+def getNestPresId() {
+ def dni = "Nest Presence Device" // old name 1
+ def d3 = getChildDevice(dni)
+ if(d3) { return dni }
+ else {
+ if(atomicState?.structures) {
+ dni = "NestPres${atomicState.structures}" // old name 2
+ d3 = getChildDevice(dni)
+ if(d3) { return dni }
+ }
+ def retVal = ""
+ def devt = appDevName()
+ if(settings?.structures) { retVal = "NestPres${devt} | ${settings?.structures}" }
+ else if(atomicState?.structures) { retVal = "NestPres${devt} | ${atomicState?.structures}" }
+ else {
+ LogAction("getNestPresID No structures ${atomicState?.structures}", "warn", true)
+ return ""
+ }
+ return retVal
+ }
+}
+
+def getNestWeatherId() {
+ def dni = "Nest Weather Device (${location?.zipCode})"
+ def d4 = getChildDevice(dni)
+ if(d4) { return dni }
+ else {
+ if(atomicState?.structures) {
+ dni = "NestWeather${atomicState.structures}"
+ d4 = getChildDevice(dni)
+ if(d4) { return dni }
+ }
+ def retVal = ""
+ def devt = appDevName()
+ if(settings?.structures) { retVal = "NestWeather${devt} | ${settings?.structures}" }
+ else if(atomicState?.structures) { retVal = "NestWeather${devt} | ${atomicState?.structures}" }
+ else {
+ LogAction("getNestWeatherId No structures ${atomicState?.structures}", "warn", true)
+ return ""
+ }
+ return retVal
+ }
+}
+
+def getNestTstatLabel(name) {
+ //log.trace "getNestTstatLabel: ${name}"
+ def devt = appDevName()
+ def defName = "Nest Thermostat${devt} - ${name}"
+ if(atomicState?.useAltNames) { defName = "${location.name}${devt} - ${name}" }
+ if(atomicState?.custLabelUsed) {
+ return settings?."tstat_${name}_lbl" ? settings?."tstat_${name}_lbl" : defName
+ }
+ else { return defName }
+}
+
+def getNestProtLabel(name) {
+ def devt = appDevName()
+ def defName = "Nest Protect${devt} - ${name}"
+ if(atomicState?.useAltNames) { defName = "${location.name}${devt} - ${name}" }
+ if(atomicState?.custLabelUsed) {
+ return settings?."prot_${name}_lbl" ? settings?."prot_${name}_lbl" : defName
+ }
+ else { return defName }
+}
+
+def getNestCamLabel(name) {
+ def devt = appDevName()
+ def defName = "Nest Camera${devt} - ${name}"
+ if(atomicState?.useAltNames) { defName = "${location.name}${devt} - ${name}" }
+ if(atomicState?.custLabelUsed) {
+ return settings?."cam_${name}_lbl" ? settings?."cam_${name}_lbl" : defName
+ }
+ else { return defName }
+}
+
+def getNestvStatLabel(name) {
+ def devt = appDevName()
+ def defName = "Nest vThermostat${devt} - ${name}"
+ if(atomicState?.useAltNames) { defName = "${location.name}${devt} - ${name}" }
+ if(atomicState?.custLabelUsed) {
+ return settings?."vtsat_${name}_lbl" ? settings?."vtstat_${name}_lbl" : defName
+ }
+ else { return defName }
+}
+
+def getNestPresLabel() {
+ def devt = appDevName()
+ def defName = "Nest Presence Device${devt}"
+ if(atomicState?.useAltNames) { defName = "${location.name}${devt} - Nest Presence Device" }
+ if(atomicState?.custLabelUsed) {
+ return settings?.presDev_lbl ? settings?.presDev_lbl.toString() : defName
+ }
+ else { return defName }
+}
+
+def getNestWeatherLabel() {
+ def devt = appDevName()
+ def wLbl = custLocStr ? custLocStr.toString() : "${getStZipCode()}"
+ def defName = "Nest Weather${devt} (${wLbl})"
+ if(atomicState?.useAltNames) { defName = "${location.name}${devt} - Nest Weather Device" }
+ if(atomicState?.custLabelUsed) {
+ return settings?.weathDev_lbl ? settings?.weathDev_lbl.toString() : defName
+ }
+ else { return defName }
+}
+
+def getWeatherDevice() {
+ def res = null
+ def d = getChildDevice(getNestWeatherId())
+ if(d) { return d }
+ return res
+}
+
+def getTstats() {
+ return atomicState?.thermostats
+}
+
+def getThermostatDevice(dni) {
+ def d = getChildDevice(getNestTstatDni(dni))
+ if(d) { return d }
+ return null
+}
+
+def addRemoveDevices(uninst = null) {
+ //log.trace "addRemoveDevices..."
+ def retVal = false
+ try {
+ def devsInUse = []
+ def tstats
+ def nProtects
+ def nCameras
+ def nVstats
+ def devsCrt = 0
+ if(!uninst) {
+ //LogAction("addRemoveDevices() Nest Thermostats ${atomicState?.thermostats}", "debug", false)
+ if(atomicState?.thermostats) {
+ tstats = atomicState?.thermostats.collect { dni ->
+ def d1 = getChildDevice(getNestTstatDni(dni))
+ if(!d1) {
+ def d1Label = getNestTstatLabel("${dni?.value}")
+ d1 = addChildDevice(app.namespace, getThermostatChildName(), dni?.key, null, [label: "${d1Label}"])
+ d1.take()
+ devsCrt = devsCrt + 1
+ LogAction("Created: ${d1?.displayName} with (Id: ${dni?.key})", "debug", true)
+ } else {
+ LogAction("Found: ${d1?.displayName} with (Id: ${dni?.key}) already exists", "debug", true)
+ }
+ devsInUse += dni.key
+ return d1
+ }
+ }
+ //LogAction("addRemoveDevices Nest Protects ${atomicState?.protects}", "debug", false)
+ if(atomicState?.protects) {
+ nProtects = atomicState?.protects.collect { dni ->
+ def d2 = getChildDevice(getNestProtDni(dni).toString())
+ if(!d2) {
+ def d2Label = getNestProtLabel("${dni.value}")
+ d2 = addChildDevice(app.namespace, getProtectChildName(), dni.key, null, [label: "${d2Label}"])
+ d2.take()
+ devsCrt = devsCrt + 1
+ LogAction("Created: ${d2?.displayName} with (Id: ${dni?.key})", "debug", true)
+ } else {
+ LogAction("Found: ${d2?.displayName} with (Id: ${dni?.key}) already exists", "debug", true)
+ }
+ devsInUse += dni.key
+ return d2
+ }
+ }
+
+ if(atomicState?.presDevice) {
+ try {
+ def dni = getNestPresId()
+ def d3 = getChildDevice(dni)
+ if(!d3) {
+ def d3Label = getNestPresLabel()
+ d3 = addChildDevice(app.namespace, getPresenceChildName(), dni, null, [label: "${d3Label}"])
+ d3.take()
+ devsCrt = devsCrt + 1
+ LogAction("Created: ${d3.displayName} with (Id: ${dni})", "debug", true)
+ } else {
+ LogAction("Found: ${d3.displayName} with (Id: ${dni}) already exists", "debug", true)
+ }
+ devsInUse += dni
+ } catch (ex) {
+ LogAction("Nest Presence Device Type is Likely not installed/published", "warn", true)
+ retVal = false
+ }
+ }
+
+ if(atomicState?.weatherDevice) {
+ try {
+ def dni = getNestWeatherId()
+ def d4 = getChildDevice(dni)
+ if(!d4) {
+ def d4Label = getNestWeatherLabel()
+ d4 = addChildDevice(app.namespace, getWeatherChildName(), dni, null, [label: "${d4Label}"])
+ d4.take()
+ atomicState?.lastWeatherUpdDt = null
+ atomicState?.lastForecastUpdDt = null
+ devsCrt = devsCrt + 1
+ LogAction("Created: ${d4.displayName} with (Id: ${dni})", "debug", true)
+ } else {
+ LogAction("Found: ${d4.displayName} with (Id: ${dni}) already exists", "debug", true)
+ }
+ devsInUse += dni
+ } catch (ex) {
+ LogAction("Nest Weather Device Type is Likely not installed/published", "warn", true)
+ retVal = false
+ }
+ }
+ if(atomicState?.cameras) {
+ nCameras = atomicState?.cameras.collect { dni ->
+ def d5 = getChildDevice(getNestCamDni(dni).toString())
+ if(!d5) {
+ def d5Label = getNestCamLabel("${dni.value}")
+ d5 = addChildDevice(app.namespace, getCameraChildName(), dni.key, null, [label: "${d5Label}"])
+ d5.take()
+ devsCrt = devsCrt + 1
+ LogAction("Created: ${d5?.displayName} with (Id: ${dni?.key})", "debug", true)
+ } else {
+ LogAction("Found: ${d5?.displayName} with (Id: ${dni?.key}) already exists", "debug", true)
+ }
+ devsInUse += dni.key
+ return d5
+ }
+ }
+ if(atomicState?.vThermostats) {
+ nVstats = atomicState?.vThermostats.collect { dni ->
+ //LogAction("atomicState.vThermostats: ${atomicState.vThermostats} dni: ${dni} dni.key: ${dni.key.toString()} dni.value: ${dni.value.toString()}", "debug", true)
+ def d6 = getChildDevice(getNestvStatDni(dni).toString())
+ if(!d6) {
+ def d6Label = getNestvStatLabel("${dni.value}")
+ //LogAction("CREATED: ${d6Label} with (Id: ${dni.key})", "debug", true)
+ d6 = addChildDevice(app.namespace, getvThermostatChildName(), dni.key, null, [label: "${d6Label}"])
+ d6.take()
+ devsCrt = devsCrt + 1
+ LogAction("Created: ${d6?.displayName} with (Id: ${dni?.key})", "debug", true)
+ } else {
+ LogAction("Found: ${d6?.displayName} with (Id: ${dni?.key}) already exists", "debug", true)
+ }
+ devsInUse += dni.key
+ return d6
+ }
+ }
+
+ def presCnt = 0
+ def weathCnt = 0
+ if(atomicState?.presDevice) { presCnt = 1 }
+ if(atomicState?.weatherDevice) { weathCnt = 1 }
+ if(devsCrt > 0) {
+ LogAction("Created Devices; Current Devices: (${tstats?.size()}) Thermostat(s), (${nVstats?.size()}) Virtual Thermostat(s), (${nProtects?.size()}) Protect(s), (${nCameras?.size()}) Cameras(s), ${presCnt} Presence Device and ${weathCnt} Weather Device", "debug", true)
+ }
+ }
+
+ if(uninst) {
+ atomicState.thermostats = []
+ atomicState.vThermostats = []
+ atomicState.protects = []
+ atomicState.cameras = []
+ atomicState.presDevice = false
+ atomicState.weatherDevice = false
+ }
+
+ if(!atomicState?.weatherDevice) {
+ atomicState?.curWeather = null
+ atomicState?.curForecast = null
+ atomicState?.curAstronomy = null
+ atomicState?.curAlerts = null
+ }
+
+ def delete
+ LogAction("devicesInUse: ${devsInUse}", "debug", false)
+ delete = getChildDevices().findAll { !devsInUse?.toString()?.contains(it?.deviceNetworkId) }
+
+ if(delete?.size() > 0) {
+ LogAction("Deleting: ${delete}, Removing ${delete.size()} devices", "debug", true)
+ delete.each { deleteChildDevice(it.deviceNetworkId) }
+ }
+ retVal = true
+ } catch (ex) {
+ if(ex instanceof physicalgraph.exception.ConflictException) {
+ def msg = "Error: Can't Delete App because Devices are still in use in other Apps, Routines, or Rules. Please double check before trying again."
+ sendPush(msg)
+ LogAction("addRemoveDevices Exception | $msg", "warn", true)
+ }
+ else if(ex instanceof physicalgraph.app.exception.UnknownDeviceTypeException) {
+ def msg = "Error: Device Handlers are likely Missing or Not Published. Please verify all device handlers are present before continuing."
+ sendPush(msg)
+ LogAction("addRemoveDevices Exception | $msg", "warn", true)
+ }
+ else { log.error "addRemoveDevices Exception:", ex }
+ sendExceptionData(ex, "addRemoveDevices")
+ retVal = false
+ }
+ return retVal
+}
+
+def devNamePage() {
+ def pagelbl = atomicState?.isInstalled ? "Device Labels" : "Custom Device Labels"
+ dynamicPage(name: "devNamePage", title: pageLbl, nextPage: "", install: false) {
+ def altName = (atomicState?.useAltNames) ? true : false
+ def custName = (atomicState?.custLabelUsed) ? true : false
+ section("Settings:") {
+ if(atomicState?.isInstalled) {
+ paragraph "Changes to device names are only allowed with new devices before they are installed. Existing devices can only be edited in the devices settings page in the mobile app or in the IDE."
+ } else {
+ if(!useCustDevNames) {
+ input (name: "useAltNames", type: "bool", title: "Use Location Name as Prefix?", required: false, defaultValue: altName, submitOnChange: true, image: "" )
+ }
+ if(!useAltNames) {
+ input (name: "useCustDevNames", type: "bool", title: "Assign Custom Names?", required: false, defaultValue: custName, submitOnChange: true, image: "" )
+ }
+ }
+ if(atomicState?.custLabelUsed) {
+ paragraph "Custom Labels Are Active", state: "complete"
+ }
+ if(atomicState?.useAltNames) {
+ paragraph "Using Location Name as Prefix is Active", state: "complete"
+ }
+ //paragraph "Current Device Handler Names", image: ""
+ }
+ def str1 = "\n\nName does not match what is expected.\nName Should be:"
+ def str2 = "\n\nName cannot be customized"
+ atomicState.useAltNames = useAltNames ? true : false
+ atomicState.custLabelUsed = useCustDevNames ? true : false
+
+ def found = false
+ if(atomicState?.thermostats || atomicState?.vThermostats) {
+ section ("Thermostat Device(s):") {
+ atomicState?.thermostats?.each { t ->
+ found = true
+ def d = getChildDevice(getNestTstatDni(t))
+ def dstr = ""
+ if(d) {
+ dstr += "Found: ${d.displayName}"
+ if(d.displayName != getNestTstatLabel(t.value)) {
+ dstr += "$str1 ${getNestTstatLabel(t.value)}"
+ }
+ else if(atomicState?.custLabelUsed || atomicState?.useAltNames) { dstr += "$str2" }
+ } else {
+ dstr += "New Name: ${getNestTstatLabel(t.value)}"
+ }
+ paragraph "${dstr}", state: "complete", image: (atomicState?.custLabelUsed && !d) ? " " : getAppImg("thermostat_icon.png")
+ if(atomicState.custLabelUsed && !d) {
+ input "tstat_${t.value}_lbl", "text", title: "Custom name for ${t.value}", defaultValue: getNestTstatLabel("${t.value}"), submitOnChange: true,
+ image: getAppImg("thermostat_icon.png")
+ }
+ }
+ atomicState?.vThermostats?.each { t ->
+ found = true
+ def d = getChildDevice(getNestvStatDni(t))
+ def dstr = ""
+ if(d) {
+ dstr += "Found: ${d.displayName}"
+ if(d.displayName != getNestvStatLabel(t.value)) {
+ dstr += "$str1 ${getNestvStatLabel(t.value)}"
+ }
+ else if(atomicState?.custLabelUsed || atomicState?.useAltNames) { dstr += "$str2" }
+ } else {
+ dstr += "New Name: ${getNestvStatLabel(t.value)}"
+ }
+ paragraph "${dstr}", state: "complete", image: (atomicState?.custLabelUsed && !d) ? " " : getAppImg("thermostat_icon.png")
+ if(atomicState.custLabelUsed && !d) {
+ input "tstat_${t.value}_lbl", "text", title: "Custom name for ${t.value}", defaultValue: getNestTstatLabel("${t.value}"), submitOnChange: true,
+ image: getAppImg("thermostat_icon.png")
+ }
+ }
+ }
+ }
+ if(atomicState?.protects) {
+ section ("Protect Device Names:") {
+ atomicState?.protects?.each { p ->
+ found = true
+ def dstr = ""
+ def d1 = getChildDevice(getNestProtDni(p))
+ if(d1) {
+ dstr += "Found: ${d1.displayName}"
+ if(d1.displayName != getNestProtLabel(p.value)) {
+ dstr += "$str1 ${getNestProtLabel(p.value)}"
+ }
+ else if(atomicState?.custLabelUsed || atomicState?.useAltNames) { dstr += "$str2" }
+ } else {
+ dstr += "New Name: ${getNestProtLabel(p.value)}"
+ }
+ paragraph "${dstr}", state: "complete", image: (atomicState.custLabelUsed && !d1) ? " " : getAppImg("protect_icon.png")
+ if(atomicState.custLabelUsed && !d1) {
+ input "prot_${p.value}_lbl", "text", title: "Custom name for ${p.value}", defaultValue: getNestProtLabel("${p.value}"), submitOnChange: true,
+ image: getAppImg("protect_icon.png")
+ }
+ }
+ }
+ }
+ if(atomicState?.cameras) {
+ section ("Camera Device Names:") {
+ atomicState?.cameras?.each { c ->
+ found = true
+ def dstr = ""
+ def d1 = getChildDevice(getNestCamDni(c))
+ if(d1) {
+ dstr += "Found: ${d1.displayName}"
+ if(d1.displayName != getNestCamLabel(c.value)) {
+ dstr += "$str1 ${getNestCamLabel(c.value)}"
+ }
+ else if(atomicState?.custLabelUsed || atomicState?.useAltNames) { dstr += "$str2" }
+ } else {
+ dstr += "New Name: ${getNestCamLabel(c.value)}"
+ }
+ paragraph "${dstr}", state: "complete", image: (atomicState.custLabelUsed && !d1) ? " " : getAppImg("camera_icon.png")
+ if(atomicState.custLabelUsed && !d1) {
+ input "cam_${c.value}_lbl", "text", title: "Custom name for ${c.value}", defaultValue: getNestCamLabel("${c.value}"), submitOnChange: true,
+ image: getAppImg("camera_icon.png")
+ }
+ }
+ }
+ }
+ if(atomicState?.presDevice) {
+ section ("Presence Device Name:") {
+ found = true
+ def pLbl = getNestPresLabel()
+ def dni = getNestPresId()
+ def d3 = getChildDevice(dni)
+ def dstr = ""
+ if(d3) {
+ dstr += "Found: ${d3.displayName}"
+ if(d3.displayName != pLbl) {
+ dstr += "$str1 ${pLbl}"
+ }
+ else if(atomicState?.custLabelUsed || atomicState?.useAltNames) { dstr += "$str2" }
+ } else {
+ dstr += "New Name: ${pLbl}"
+ }
+ paragraph "${dstr}", state: "complete", image: (atomicState.custLabelUsed && !d3) ? " " : getAppImg("presence_icon.png")
+ if(atomicState.custLabelUsed && !d3) {
+ input "presDev_lbl", "text", title: "Custom name for Nest Presence Device", defaultValue: pLbl, submitOnChange: true, image: getAppImg("presence_icon.png")
+ }
+ }
+ }
+ if(atomicState?.weatherDevice) {
+ section ("Weather Device Name:") {
+ found = true
+ def wLbl = getNestWeatherLabel()
+ def dni = getNestWeatherId()
+ def d4 = getChildDevice(dni)
+ def dstr = ""
+ if(d4) {
+ dstr += "Found: ${d4.displayName}"
+ if(d4.displayName != wLbl) {
+ dstr += "$str1 ${wLbl}"
+ }
+ else if(atomicState?.custLabelUsed || atomicState?.useAltNames) { dstr += "$str2" }
+ } else {
+ dstr += "New Name: ${wLbl}"
+ }
+ paragraph "${dstr}", state: "complete", image: (atomicState.custLabelUsed && !d4) ? " " : getAppImg("weather_icon.png")
+ if(atomicState.custLabelUsed && !d4) {
+ input "weathDev_lbl", "text", title: "Custom name for Nest Weather Device", defaultValue: wLbl, submitOnChange: true, image: getAppImg("weather_icon.png")
+ }
+ }
+ }
+ if(!found) {
+ paragraph "No Devices Selected"
+ }
+ }
+}
+
+def deviceHandlerTest() {
+ //log.trace "deviceHandlerTest()"
+ atomicState.devHandlersTested = true
+ return true
+
+ if(atomicState?.devHandlersTested || atomicState?.isInstalled || (atomicState?.thermostats && atomicState?.protects && atomicState?.cameras && atomicState?.vThermostats && atomicState?.presDevice && atomicState?.weatherDevice)) {
+ atomicState.devHandlersTested = true
+ return true
+ }
+ try {
+ def d1 = addChildDevice(app.namespace, getThermostatChildName(), "testNestThermostat-Install123", null, [label:"Nest Thermostat:InstallTest"])
+ def d2 = addChildDevice(app.namespace, getPresenceChildName(), "testNestPresence-Install123", null, [label:"Nest Presence:InstallTest"])
+ def d3 = addChildDevice(app.namespace, getProtectChildName(), "testNestProtect-Install123", null, [label:"Nest Protect:InstallTest"])
+ def d4 = addChildDevice(app.namespace, getWeatherChildName(), "testNestWeather-Install123", null, [label:"Nest Weather:InstallTest"])
+ def d5 = addChildDevice(app.namespace, getCameraChildName(), "testNestCamera-Install123", null, [label:"Nest Camera:InstallTest"])
+ def d6 = addChildDevice(app.namespace, getvThermostatChildName(), "testNestvThermostat-Install123", null, [label:"Nest vThermostat:InstallTest"])
+
+ log.debug "d1: ${d1.label} | d2: ${d2.label} | d3: ${d3.label} | d4: ${d4.label} | d5: ${d5.label} | d6: ${d6.label}"
+ atomicState.devHandlersTested = true
+ removeTestDevs()
+ //runIn(4, "removeTestDevs")
+ return true
+ }
+ catch (ex) {
+ if(ex instanceof physicalgraph.app.exception.UnknownDeviceTypeException) {
+ LogAction("Device Handlers are missing: ${getThermostatChildName()}, ${getPresenceChildName()}, and ${getProtectChildName()}, Verify the Device Handlers are installed and Published via the IDE", "error", true)
+ } else {
+ log.error "deviceHandlerTest Exception:", ex
+ sendExceptionData(ex, "deviceHandlerTest")
+ }
+ atomicState.devHandlersTested = false
+ return false
+ }
+}
+
+def removeTestDevs() {
+ try {
+ def names = [ "testNestThermostat-Install123", "testNestPresence-Install123", "testNestProtect-Install123", "testNestWeather-Install123", "testNestCamera-Install123", "testNestvThermostat-Install123" ]
+ names?.each { dev ->
+ //log.debug "dev: $dev"
+ def delete = getChildDevices().findAll { it?.deviceNetworkId == dev }
+ //log.debug "delete: ${delete}"
+ if(delete) {
+ delete.each { deleteChildDevice(it.deviceNetworkId) }
+ }
+ }
+ } catch (ex) {
+ log.error "deviceHandlerTest Exception:", ex
+ sendExceptionData(ex, "removeTestDevs")
+ }
+}
+
+def preReqCheck() {
+ //log.trace "preReqCheckTest()"
+ generateInstallId()
+ if(!location?.timeZone || !location?.zipCode) {
+ atomicState.preReqTested = false
+ LogAction("SmartThings Location is not returning (TimeZone: ${location?.timeZone}) or (ZipCode: ${location?.zipCode}) Please edit these settings under the IDE...", "warn", true)
+ return false
+ }
+ else {
+ atomicState.preReqTested = true
+ return true
+ }
+}
+
+//This code really does nothing at the moment but return the dynamic url of the app's endpoints
+def getEndpointUrl() {
+ def params = [
+ uri: "https://graph.api.smartthings.com/api/smartapps/endpoints",
+ query: ["access_token": atomicState?.accessToken],
+ contentType: 'application/json'
+ ]
+ try {
+ httpGet(params) { resp ->
+ LogAction("EndPoint URL: ${resp?.data?.uri}", "trace", false, false, true)
+ return resp?.data?.uri
+ }
+ } catch (ex) {
+ log.error "getEndpointUrl Exception:", ex
+ sendExceptionData(ex, "getEndpointUrl")
+ }
+}
+
+def getAccessToken() {
+ try {
+ if(!atomicState?.accessToken) { atomicState?.accessToken = createAccessToken() }
+ else { return true }
+ }
+ catch (ex) {
+ def msg = "Error: OAuth is not Enabled for the Nest Manager application!!!. Please click remove and Enable Oauth under the SmartApp App Settings in the IDE..."
+ sendPush(msg)
+ LogAction("getAccessToken Exception | $msg", "warn", true)
+ sendExceptionData(ex, "getAccessToken")
+ return false
+ }
+}
+
+def generateInstallId() {
+ if(!atomicState?.installationId) { atomicState?.installationId = UUID?.randomUUID().toString() }
+}
+
+/************************************************************************************************
+| Below This line handle SmartThings >> Nest Token Authentication |
+*************************************************************************************************/
+
+//These are the Nest OAUTH Methods to aquire the auth code and then Access Token.
+def oauthInitUrl() {
+ //log.debug "oauthInitUrl with callback: ${callbackUrl}"
+ atomicState.oauthInitState = UUID?.randomUUID().toString()
+ def oauthParams = [
+ response_type: "code",
+ client_id: clientId(),
+ state: atomicState?.oauthInitState,
+ redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback"
+ ]
+ redirect(location: "https://home.nest.com/login/oauth2?${toQueryString(oauthParams)}")
+}
+
+def callback() {
+ try {
+ LogTrace("callback()>> params: $params, params.code ${params.code}")
+ def code = params.code
+ LogTrace("Callback Code: ${code}")
+ def oauthState = params.state
+ LogTrace("Callback State: ${oauthState}")
+
+ if(oauthState == atomicState?.oauthInitState){
+ def tokenParams = [
+ code: code.toString(),
+ client_id: clientId(),
+ client_secret: clientSecret(),
+ grant_type: "authorization_code",
+ ]
+ def tokenUrl = "https://api.home.nest.com/oauth2/access_token?${toQueryString(tokenParams)}"
+ httpPost(uri: tokenUrl) { resp ->
+ atomicState.tokenExpires = resp?.data.expires_in
+ atomicState.authToken = resp?.data.access_token
+ if(atomicState?.authToken) { atomicState?.tokenCreatedDt = getDtNow() }
+ }
+
+ if(atomicState?.authToken) {
+ LogAction("Nest AuthToken Generated Successfully...", "info", true)
+ generateInstallId
+ success()
+ } else {
+ LogAction("There was a Failure Generating the Nest AuthToken!!!", "error", true)
+ fail()
+ }
+ }
+ else { LogAction("callback() failed oauthState != atomicState.oauthInitState", "error", true) }
+ }
+ catch (ex) {
+ log.error "Callback Exception:", ex
+ sendExceptionData(ex, "callback")
+ }
+}
+
+def revokeNestToken() {
+ def params = [
+ uri: "https://api.home.nest.com",
+ path: "/oauth2/access_tokens/${atomicState?.authToken}",
+ contentType: 'application/json'
+ ]
+ try {
+ httpDelete(params) { resp ->
+ if(resp?.status == 204) {
+ atomicState?.authToken = null
+ LogAction("Your Nest Token has been revoked successfully...", "warn", true)
+ return true
+ }
+ }
+ }
+ catch (ex) {
+ log.error "revokeNestToken Exception:", ex
+ sendExceptionData(ex, "revokeNestToken")
+ return false
+ }
+}
+
+//HTML Connections Pages
+def success() {
+ def message = """
+
Your SmartThings Account is now connected to Nest!
+ Click 'Done' to finish setup.
+ """
+ connectionStatus(message)
+}
+
+def fail() {
+ def message = """
+ The connection could not be established!
+ Click 'Done' to return to the menu.
+ """
+ connectionStatus(message)
+}
+
+def connectionStatus(message, redirectUrl = null) {
+ def redirectHtml = ""
+ if(redirectUrl) { redirectHtml = """""" }
+
+ def html = """
+
+
+
+
+ SmartThings & Nest connection
+
+
+
+
+
+
+ """
+ render contentType: 'text/html', data: html
+}
+
+def 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("&")
+}
+
+def clientId() {
+ if(!appSettings.clientId) {
+ def tokenNum = atomicState?.appData?.token?.tokenNum?.toInteger() ?: 1
+ switch(tokenNum) {
+ case 1:
+ return "63e9befa-dc62-4b73-aaf4-dcf3826dd704" // Original Token Updated with Cam/Image Support
+ break
+ case 2:
+ return "31aea46c-4048-4c2b-b6be-cac7fe305d4c" //token v2 with cam support
+ break
+ case 3:
+ return "665dbbb1-2765-4145-b3ae-36cb986c309d" //Added a 3rd token with 50 available slots
+ break
+ }
+ } else {
+ return appSettings.clientId
+ }
+}
+
+def clientSecret() {
+ if(!appSettings.clientSecret) {
+ def tokenNum = atomicState?.appData?.token?.tokenNum?.toInteger() ?: 1
+ switch(tokenNum) {
+ case 1:
+ return "8iqT8X46wa2UZnL0oe3TbyOa0" // Original Token Updated with Cam/Image Support
+ break
+ case 2:
+ return "FmO469GXfdSVjn7PhKnjGWZlm" //token v2 with cam support
+ break
+ case 3:
+ return "jzARJspM2bmXETMVWXeGTYBDJ" //Added a 3rd token with 50 available slots
+ break
+ }
+ } else {
+ return appSettings.clientSecret
+ }
+}
+
+/************************************************************************************************
+| LOGGING AND Diagnostic |
+*************************************************************************************************/
+def LogTrace(msg) {
+ def trOn = advAppDebug ? true : false
+ if(trOn) { Logger(msg, "trace") }
+}
+
+def LogAction(msg, type = "debug", showAlways = false) {
+ def isDbg = parent ? ((atomicState?.showDebug || showDebug) ? true : false) : (appDebug ? true : false)
+ if(showAlways) { Logger(msg, type) }
+
+ else if(isDbg && !showAlways) { Logger(msg, type) }
+}
+
+def Logger(msg, type) {
+ if(msg && type) {
+ def labelstr = ""
+ if(!atomicState?.debugAppendAppName) {
+ atomicState?.debugAppendAppName = (parent ? parent?.settings?.debugAppendAppName : settings?.debugAppendAppName) ? true : false
+ }
+ if(atomicState?.debugAppendAppName) { labelstr = "${app.label} | " }
+ switch(type) {
+ case "debug":
+ log.debug "${labelstr}${msg}"
+ break
+ case "info":
+ log.info "${labelstr}${msg}"
+ break
+ case "trace":
+ log.trace "${labelstr}${msg}"
+ break
+ case "error":
+ log.error "${labelstr}${msg}"
+ break
+ case "warn":
+ log.warn "${labelstr}${msg}"
+ break
+ default:
+ log.debug "${labelstr}${msg}"
+ break
+ }
+ }
+ else { log.error "Logger Error - type: ${type} | msg: ${msg}" }
+}
+
+def setStateVar(frc = false) {
+ //log.trace "setStateVar..."
+ //If the developer changes the version in the web appParams JSON it will trigger
+ //the app to create any new state values that might not exist or reset those that do to prevent errors
+ def stateVer = 3
+ def stateVar = !atomicState?.stateVarVer ? 0 : atomicState?.stateVarVer.toInteger()
+ if(!atomicState?.stateVarUpd || frc || (stateVer < atomicState?.appData.state.stateVarVer.toInteger())) {
+ if(!atomicState?.newSetupComplete) { atomicState.newSetupComplete = false }
+ if(!atomicState?.setupVersion) { atomicState?.setupVersion = 0 }
+ if(!atomicState?.misPollNotifyWaitVal) { atomicState.misPollNotifyWaitVal = 900 }
+ if(!atomicState?.misPollNotifyMsgWaitVal) { atomicState.misPollNotifyMsgWaitVal = 3600 }
+ if(!atomicState?.updNotifyWaitVal) { atomicState.updNotifyWaitVal = 7200 }
+ if(!atomicState?.custLabelUsed) { atomicState?.custLabelUsed = false }
+ if(!atomicState?.useAltNames) { atomicState.useAltNames = false }
+ if(!atomicState?.apiCommandCnt) { atomicState?.apiCommandCnt = 0 }
+ atomicState?.stateVarUpd = true
+ atomicState?.stateVarVer = atomicState?.appData?.state?.stateVarVer ? atomicState?.appData?.state?.stateVarVer?.toInteger() : 0
+ }
+}
+
+//Things that I need to clear up on updates go here
+//IMPORTANT: This must be run in it's own thread, and exit after running as the cleanup occurs on exit
+def stateCleanup() {
+ LogAction("stateCleanup...", "trace", true)
+
+ state.remove("exLogs")
+ state.remove("pollValue")
+ state.remove("pollStrValue")
+ state.remove("pollWaitVal")
+ state.remove("tempChgWaitVal")
+ state.remove("cmdDelayVal")
+ state.remove("testedDhInst")
+ state.remove("missedPollNotif")
+ state.remove("updateMsgNotif")
+ state.remove("updChildOnNewOnly")
+ state.remove("disAppIcons")
+ state.remove("showProtAlarmStateEvts")
+ state.remove("showAwayAsAuto")
+ state.remove("cmdQ")
+ state.remove("recentSendCmd")
+ state.remove("currentWeather")
+ state.remove("altNames")
+ state.remove("locstr")
+ state.remove("custLocStr")
+ state.remove("autoAppInstalled")
+ state.remove("nestStructures")
+ if(!atomicState?.cmdQlist) {
+ state.remove("cmdQ2")
+ state.remove("cmdQ3")
+ state.remove("cmdQ4")
+ state.remove("cmdQ5")
+ state.remove("cmdQ6")
+ state.remove("cmdQ7")
+ state.remove("cmdQ8")
+ state.remove("cmdQ9")
+ state.remove("cmdQ10")
+ state.remove("cmdQ11")
+ state.remove("cmdQ12")
+ state.remove("cmdQ13")
+ state.remove("cmdQ14")
+ state.remove("cmdQ15")
+ state.remove("lastCmdSentDt2")
+ state.remove("lastCmdSentDt3")
+ state.remove("lastCmdSentDt4")
+ state.remove("lastCmdSentDt5")
+ state.remove("lastCmdSentDt6")
+ state.remove("lastCmdSentDt7")
+ state.remove("lastCmdSentDt8")
+ state.remove("lastCmdSentDt9")
+ state.remove("lastCmdSentDt10")
+ state.remove("lastCmdSentDt11")
+ state.remove("lastCmdSentDt12")
+ state.remove("lastCmdSentDt13")
+ state.remove("lastCmdSentDt14")
+ state.remove("lastCmdSentDt15")
+ state.remove("recentSendCmd2")
+ state.remove("recentSendCmd3")
+ state.remove("recentSendCmd4")
+ state.remove("recentSendCmd5")
+ state.remove("recentSendCmd6")
+ state.remove("recentSendCmd7")
+ state.remove("recentSendCmd8")
+ state.remove("recentSendCmd9")
+ state.remove("recentSendCmd10")
+ state.remove("recentSendCmd11")
+ state.remove("recentSendCmd12")
+ state.remove("recentSendCmd13")
+ state.remove("recentSendCmd14")
+ state.remove("recentSendCmd15")
+ }
+}
+
+/******************************************************************************
+* Keep These Methods *
+*******************************************************************************/
+def getThermostatChildName() { return getChildName("Nest Thermostat") }
+def getProtectChildName() { return getChildName("Nest Protect") }
+def getPresenceChildName() { return getChildName("Nest Presence") }
+def getWeatherChildName() { return getChildName("Nest Weather") }
+def getCameraChildName() { return getChildName("Nest Camera") }
+def getvThermostatChildName() { return getChildName("Nest Virtual Thermostat") }
+
+def getAutoAppChildName() { return getChildName("Nest Automations") }
+def getWatchdogAppChildName(){ return getChildName("Nest Location ${location.name} Watchdog") }
+
+def getChildName(str) { return "${str}${appDevName()}" }
+
+def getServerUrl() { return "https://graph.api.smartthings.com" }
+def getShardUrl() { return getApiServerUrl() }
+def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" }
+def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState?.accessToken}&apiServerUrl=${shardUrl}" }
+def getNestApiUrl() { return "https://developer-api.nest.com" }
+def getAppEndpointUrl(subPath) { return "${apiServerUrl("/api/smartapps/installations/${app.id}/${subPath}?access_token=${atomicState.accessToken}")}" }
+def getHelpPageUrl() { return "https://rawgit.com/tonesto7/nest-manager/${gitBranch()}/Documents/help-page.html" }
+def getReadmePageUrl() { return "https://rawgit.com/tonesto7/nest-manager/${gitBranch()}/README.html" }
+def getAutoHelpPageUrl() { return "https://rawgit.com/tonesto7/nest-manager/${gitBranch()}/Documents/help/nest-automations.html" }
+def getFirebaseAppUrl() { return "https://st-nest-manager.firebaseio.com" }
+def getAppImg(imgName, on = null) { return (!disAppIcons || on) ? "https://raw.githubusercontent.com/tonesto7/nest-manager/${gitBranch()}/Images/App/$imgName" : "" }
+def getDevImg(imgName, on = null) { return (!disAppIcons || on) ? "https://raw.githubusercontent.com/tonesto7/nest-manager/${gitBranch()}/Images/Devices/$imgName" : "" }
+
+def latestTstatVer() { return atomicState?.appData?.updater?.versions?.thermostat ?: "unknown" }
+def latestProtVer() { return atomicState?.appData?.updater?.versions?.protect ?: "unknown" }
+def latestPresVer() { return atomicState?.appData?.updater?.versions?.presence ?: "unknown" }
+def latestWeathVer() { return atomicState?.appData?.updater?.versions?.weather ?: "unknown" }
+def latestCamVer() { return atomicState?.appData?.updater?.versions?.camera ?: "unknown" }
+def latestvStatVer() { return atomicState?.appData?.updater?.versions?.vthermostat ?: "unknown" }
+def getUse24Time() { return useMilitaryTime ? true : false }
+
+//Returns app State Info
+def getStateSize() { return state?.toString().length() }
+def getStateSizePerc() { return (int) ((stateSize/100000)*100).toDouble().round(0) }
+
+def debugStatus() { return !appDebug ? "Off" : "On" }
+def deviceDebugStatus() { return !childDebug ? "Off" : "On" }
+def isAppDebug() { return !appDebug ? false : true }
+def isChildDebug() { return !childDebug ? false : true }
+
+def getLocationModes() {
+ def result = []
+ location?.modes.sort().each {
+ if(it) { result.push("${it}") }
+ }
+ return result
+}
+
+def getShowHelp() { return atomicState?.showHelp == false ? false : true }
+
+def getTimeZone() {
+ def tz = null
+ if(location?.timeZone) { tz = location?.timeZone }
+ else { tz = TimeZone.getTimeZone(getNestTimeZone()) }
+ if(!tz) { LogAction("getTimeZone: Hub or Nest TimeZone is not found ...", "warn", true) }
+ return tz
+}
+
+def formatDt(dt) {
+ def tf = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy")
+ if(getTimeZone()) { tf.setTimeZone(getTimeZone()) }
+ else {
+ LogAction("SmartThings TimeZone is not found or is not set... Please Try to open your ST location and Press Save...", "warn", true)
+ }
+ return tf.format(dt)
+}
+
+def GetTimeDiffSeconds(strtDate, stpDate=null, methName=null) {
+ LogAction("[GetTimeDiffSeconds] StartDate: $strtDate | StopDate: ${stpDate ?: "Not Sent"} | MethodName: ${methName ?: "Not Sent"})", "warn", false)
+ if((strtDate && !stpDate) || (strtDate && stpDate)) {
+ if(strtDate?.contains("dtNow")) { return 10000 }
+ def now = new Date()
+ def stopVal = stpDate ? stpDate.toString() : formatDt(now)
+ def startDt = Date.parse("E MMM dd HH:mm:ss z yyyy", strtDate)
+ def stopDt = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal)
+ def start = Date.parse("E MMM dd HH:mm:ss z yyyy", formatDt(startDt)).getTime()
+ def stop = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal).getTime()
+ def diff = (int) (long) (stop - start) / 1000
+ //log.trace "[GetTimeDiffSeconds] Results for '$methName': ($diff seconds)"
+ return diff
+ } else { return null }
+}
+
+def 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 }
+}
+
+// parent only Method
+def notificationTimeOk() {
+ try {
+ 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?.qStartTime && settings?.qStopTime) {
+ if(settings?.qStartInput == "sunset") { strtTime = sun.sunset }
+ else if(settings?.qStartInput == "sunrise") { strtTime = sun.sunrise }
+ else if(settings?.qStartInput == "A specific time" && settings?.qStartTime) { strtTime = settings?.qStartTime }
+
+ if(settings?.qStopInput == "sunset") { stopTime = sun.sunset }
+ else if(settings?.qStopInput == "sunrise") { stopTime = sun.sunrise }
+ else if(settings?.qStopInput == "A specific time" && settings?.qStopTime) { stopTime = settings?.qStopTime }
+ } else { return true }
+ if(strtTime && stopTime) {
+ return timeOfDayIsBetween(strtTime, stopTime, new Date(), getTimeZone()) ? false : true
+ } else { return true }
+ } catch (ex) {
+ log.error "notificationTimeOk Exception:", ex
+ sendExceptionData(ex, "notificationTimeOk")
+ }
+}
+
+def 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)
+ }
+}
+
+def epochToTime(tm) {
+ def tf = new SimpleDateFormat("h:mm a")
+ tf?.setTimeZone(getTimeZone())
+ return tf.format(tm)
+}
+
+def getDtNow() {
+ def now = new Date()
+ return formatDt(now)
+}
+
+def modesOk(modeEntry) {
+ def res = true
+ if(modeEntry) {
+ modeEntry?.each { m ->
+ if(m.toString() == location?.mode.toString()) { res = false }
+ }
+ }
+ return res
+}
+
+def 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 minDevVersions() {
+ return [
+ "thermostat":["val":400, "desc":"4.0.0"],
+ "protect":["val":400, "desc":"4.0.0"],
+ "presence":["val":400, "desc":"4.0.0"],
+ "weather":["val":400, "desc":"4.0.0"],
+ "camera":["val":200 , "desc":"2.0.0"],
+ "vthermostat":["val":400, "desc":"4.0.0"]
+ ]
+}
+
+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
+}
+
+def strCapitalize(str) {
+ return str ? str?.toString().capitalize() : null
+}
+
+def getInputEnumLabel(inputName, enumName) {
+ def result = "Not Set"
+ if(input && enumName) {
+ enumName.each { item ->
+ if(item?.key.toString() == inputName?.toString()) {
+ result = item?.value
+ }
+ }
+ }
+ return result
+}
+
+def getShowProtAlarmEvts() { return showProtAlarmStateEvts ? true : false }
+
+/******************************************************************************
+| Application Pages |
+*******************************************************************************/
+def pollPrefPage() {
+ dynamicPage(name: "pollPrefPage", install: false) {
+ section("") {
+ paragraph "Polling Preferences", image: getAppImg("timer_icon.png")
+ }
+ section("Device Polling:") {
+ input ("pollValue", "enum", title: "Device Poll Rate", required: false, defaultValue: 180, metadata: [values:pollValEnum()],
+ submitOnChange: true)
+ }
+ section("Location Polling:") {
+ input ("pollStrValue", "enum", title: "Location Poll Rate", required: false, defaultValue: 180, metadata: [values:pollValEnum()],
+ submitOnChange: true)
+ }
+ if(atomicState?.weatherDevice) {
+ section("Weather Polling:") {
+ input ("pollWeatherValue", "enum", title: "Weather Refresh Rate", required: false, defaultValue: 900, metadata: [values:notifValEnum()],
+ submitOnChange: true)
+ }
+ }
+ section("Wait Values:") {
+ input ("pollWaitVal", "enum", title: "Forced Poll Refresh Limit", required: false, defaultValue: 10, metadata: [values:waitValEnum()],
+ submitOnChange: true)
+ }
+ }
+}
+
+def getPollingConfDesc() {
+ def pollValDesc = (!pollValue || pollValue == 180) ? "" : " (Custom)"
+ def pollStrValDesc = (!pollStrValue || pollStrValue == 180) ? "" : " (Custom)"
+ def pollWeatherValDesc = (!pollWeatherValue || pollWeatherValue == 900) ? "" : " (Custom)"
+ def pollWaitValDesc = (!pollWaitVal || pollWaitVal == 10) ? "" : " (Custom)"
+ def pStr = ""
+ pStr += "Polling: (${!atomicState?.pollingOn ? "Not Active" : "Active"})"
+ pStr += "\n • Device: (${getInputEnumLabel(pollValue?:180, pollValEnum())})"
+ pStr += "\n • Structure: (${getInputEnumLabel(pollStrValue?:180, pollValEnum())})"
+ pStr += atomicState?.weatherDevice ? "\n • Weather Polling: (${getInputEnumLabel(pollWeatherValue?:900, notifValEnum())})" : ""
+ pStr += "\n • Forced Poll Refresh Limit:\n └ (${getInputEnumLabel(pollWaitVal ?: 10, waitValEnum())})"
+ return ((pollValDesc || pollStrValDesc || pollWEatherValDesc || pollWaitValDesc) ? pStr : "")
+}
+
+def notifPrefPage() {
+ dynamicPage(name: "notifPrefPage", install: false) {
+ def sectDesc = !location.contactBookEnabled ? "Enable push notifications below..." : "Select People or Devices to Receive Notifications..."
+ section(sectDesc) {
+ if(!location.contactBookEnabled) {
+ input(name: "usePush", type: "bool", title: "Send Push Notitifications", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("notification_icon.png"))
+ } else {
+ input(name: "recipients", type: "contact", title: "Send notifications to", required: false, submitOnChange: true, image: getAppImg("recipient_icon.png")) {
+ input ("phone", "phone", title: "Phone Number to send SMS to...", required: false, submitOnChange: true, image: getAppImg("notification_icon.png"))
+ }
+ }
+ }
+
+ if(settings?.recipients || settings?.phone || settings?.usePush) {
+ if(settings?.recipients && !atomicState?.pushTested) {
+ sendMsg("Info", "Push Notification Test Successful... Notifications have been Enabled for ${textAppName()}")
+ atomicState.pushTested = true
+ } else { atomicState.pushTested = true }
+
+ section(title: "Time Restrictions") {
+ href "setNotificationTimePage", title: "Silence Notifications...", description: (getNotifSchedDesc() ?: "Tap to configure..."), params: [pName: ""], state: (getNotifSchedDesc() ? "complete" : null),
+ image: getAppImg("quiet_time_icon.png")
+ }
+ section("Missed Poll Notification:") {
+ input (name: "sendMissedPollMsg", type: "bool", title: "Send Missed Poll Messages?", defaultValue: true, submitOnChange: true, image: getAppImg("late_icon.png"))
+ if(sendMissedPollMsg == null || sendMissedPollMsg) {
+ def misPollNotifyWaitValDesc = !misPollNotifyWaitVal ? "Default: 15 Minutes" : misPollNotifyWaitVal
+ input (name: "misPollNotifyWaitVal", type: "enum", title: "Time Past the missed Poll?", required: false, defaultValue: 900, metadata: [values:notifValEnum()], submitOnChange: true)
+ if(misPollNotifyWaitVal) {
+ atomicState.misPollNotifyWaitVal = !misPollNotifyWaitVal ? 900 : misPollNotifyWaitVal.toInteger()
+ if(misPollNotifyWaitVal.toInteger() == 1000000) {
+ input (name: "misPollNotifyWaitValCust", type: "number", title: "Custom Missed Poll Value in Seconds", range: "60..86400", required: false, defaultValue: 900, submitOnChange: true)
+ if(misPollNotifyWaitValCust) { atomicState?.misPollNotifyWaitVal = misPollNotifyWaitValCust ? misPollNotifyWaitValCust.toInteger() : 900 }
+ }
+ } else { atomicState.misPollNotifyWaitVal = !misPollNotifyWaitVal ? 900 : misPollNotifyWaitVal.toInteger() }
+
+ def misPollNotifyMsgWaitValDesc = !misPollNotifyMsgWaitVal ? "Default: 1 Hour" : misPollNotifyMsgWaitVal
+ input (name: "misPollNotifyMsgWaitVal", type: "enum", title: "Delay before sending again?", required: false, defaultValue: 3600, metadata: [values:notifValEnum()], submitOnChange: true)
+ if(misPollNotifyMsgWaitVal) {
+ atomicState.misPollNotifyMsgWaitVal = !misPollNotifyMsgWaitVal ? 3600 : misPollNotifyMsgWaitVal.toInteger()
+ if(misPollNotifyMsgWaitVal.toInteger() == 1000000) {
+ input (name: "misPollNotifyMsgWaitValCust", type: "number", title: "Custom Msg Wait Value in Seconds", range: "60..86400", required: false, defaultValue: 3600, submitOnChange: true)
+ if(misPollNotifyMsgWaitValCust) { atomicState.misPollNotifyMsgWaitVal = misPollNotifyMsgWaitValCust ? misPollNotifyMsgWaitValCust.toInteger() : 3600 }
+ }
+ } else { atomicState.misPollNotifyMsgWaitVal = !misPollNotifyMsgWaitVal ? 3600 : misPollNotifyMsgWaitVal.toInteger() }
+ }
+ }
+ section("App and Device Updates:") {
+ input (name: "sendAppUpdateMsg", type: "bool", title: "Send for Updates...", defaultValue: true, submitOnChange: true, image: getAppImg("update_icon.png"))
+ if(sendMissedPollMsg == null || sendAppUpdateMsg) {
+ def updNotifyWaitValDesc = !updNotifyWaitVal ? "Default: 2 Hours" : updNotifyWaitVal
+ input (name: "updNotifyWaitVal", type: "enum", title: "Send reminders every?", required: false, defaultValue: 7200, metadata: [values:notifValEnum()], submitOnChange: true)
+ if(updNotifyWaitVal) {
+ atomicState.updNotifyWaitVal = !updNotifyWaitVal ? 7200 : updNotifyWaitVal.toInteger()
+ if(updNotifyWaitVal.toInteger() == 1000000) {
+ input (name: "updNotifyWaitValCust", type: "number", title: "Custom Missed Poll Value in Seconds", range: "30..86400", required: false, defaultValue: 7200, submitOnChange: true)
+ if(updNotifyWaitValCust) { atomicState.updNotifyWaitVal = updNotifyWaitValCust ? updNotifyWaitValCust.toInteger() : 7200 }
+ }
+ } else { atomicState.updNotifyWaitVal = !updNotifyWaitVal ? 7200 : updNotifyWaitVal.toInteger() }
+ }
+ }
+ } else { atomicState.pushTested = false }
+ }
+}
+
+// Parent only method
+def getNotifSchedDesc() {
+ def sun = getSunriseAndSunset()
+ //def schedInverted = settings?.DmtInvert
+ def startInput = settings?.qStartInput
+ def startTime = settings?.qStartTime
+ def stopInput = settings?.qStopInput
+ def stopTime = settings?.qStopTime
+ def dayInput = settings?.quietDays
+ def modeInput = settings?.quietModes
+ def notifDesc = ""
+ 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}" : ""
+ return (notifDesc != "") ? "${notifDesc}" : null
+}
+
+// Parent only method
+def setNotificationTimePage() {
+ dynamicPage(name: "setNotificationTimePage", title: "Prevent Notifications\nDuring these Days, Times or Modes", uninstall: false) {
+ def timeReq = (settings["qStartTime"] || settings["qStopTime"]) ? true : false
+ section() {
+ input "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["qStartInput"] == "A specific time") {
+ input "qStartTime", "time", title: "Start time", required: timeReq, image: getAppImg("start_time_icon.png")
+ }
+ input "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?."qStopInput" == "A specific time") {
+ input "qStopTime", "time", title: "Stop time", required: timeReq, image: getAppImg("stop_time_icon.png")
+ }
+ input "quietDays", "enum", title: "Only on these days of the week", multiple: true, required: false, image: getAppImg("day_calendar_icon.png"), options: timeDayOfWeekOptions()
+ input "quietModes", "mode", title: "When these Modes are Active", multiple: true, submitOnChange: true, required: false, image: getAppImg("mode_icon.png")
+ }
+ }
+}
+
+def getAppNotifConfDesc() {
+ def str = ""
+ str += pushStatus() ? "Notifications:" : ""
+ str += (pushStatus() && settings?.recipients) ? "\n • Contacts: (${settings?.recipients?.size()})" : ""
+ str += (pushStatus() && settings?.usePush) ? "\n • Push Messages: Enabled" : ""
+ str += (pushStatus() && sms) ? "\n • SMS: (${sms?.size()})" : ""
+ str += (pushStatus() && settings?.phone) ? "\n • SMS: (${settings?.phone?.size()})" : ""
+ str += (pushStatus() && getNotifSchedDesc()) ? "\n${getNotifSchedDesc()}" : ""
+ return pushStatus() ? "${str}" : null
+}
+
+def devPrefPage() {
+ dynamicPage(name: "devPrefPage", title: "Device Preferences", uninstall: false) {
+ if(settings?.thermostats || settings?.protects || settings?.presDevice || settings?.weatherDevice) {
+ section("Device Name Customization:") {
+ def devDesc = (atomicState?.custLabelUsed || atomicState?.useAltNames) ? "Custom Labels Set...\n\nTap to Modify..." : "Tap to Configure..."
+ href "devNamePage", title: "Device Names...", description: devDesc, state:(atomicState?.custLabelUsed || atomicState?.useAltNames) ? "complete" : "", image: getAppImg("device_name_icon.png")
+ }
+ }
+ if(atomicState?.thermostats) {
+ section("Thermostat Devices:") {
+ input ("tempChgWaitVal", "enum", title: "Manual Temp Change Delay", required: false, defaultValue: 4, metadata: [values:waitValEnum()], submitOnChange: true, image: getAppImg("temp_icon.png"))
+ paragraph "The options below will allow you to disable voice report access from voice apps like Ask Alexa"
+ input ("disableVoiceZoneRprt", "bool", title: "Disable Thermostat Zone Voice Reports?", description: "This will return the current thermostat automations zone information details.",
+ required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png"))
+ input ("disableVoiceUsageRprt", "bool", title: "Disable Thermostat Usage Voice Reports?", description: "This will return the thermostats elapsed runtime details.",
+ required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png"))
+ atomicState.needChildUpd = true
+ }
+ }
+ if(atomicState?.protects) {
+ section("Protect Devices:") {
+ input "showProtActEvts", "bool", title: "Show Non-Alarm Events in Device Activity Feed?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("list_icon.png")
+ atomicState.needChildUpd = true
+ }
+ }
+ if(atomicState?.vThermostats) {
+ section("Virtual Thermostat Devices:") {
+ paragraph "Nothing to Show!!!"
+ }
+ }
+ if(atomicState?.presDevice) {
+ section("Presence Device:") {
+ paragraph "Nothing to Show!!!"
+ }
+ }
+ if(atomicState?.weatherDevice) {
+ section("Weather Device:") {
+ href "custWeatherPage", title: "Customize Weather Location?", description: (getWeatherConfDesc() ? "${getWeatherConfDesc()}\n\nTap to Modify..." : ""), state: (getWeatherConfDesc() ? "complete":""), image: getAppImg("weather_icon_grey.png")
+ input ("weathAlertNotif", "bool", title: "Notify on Weather Alerts?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("weather_icon.png"))
+ }
+ }
+ }
+}
+
+def custWeatherPage() {
+ dynamicPage(name: "custWeatherPage", title: "", nextPage: "", install: false) {
+ section("Set Custom Weather Location") {
+ def validEnt = "\n\nWeather Stations: [pws:station_id]\nZipCodes: [90250]"
+ href url:"https://www.wunderground.com/weatherstation/ListStations.asp", style:"embedded", required:false, title:"Weather Station ID Lookup",
+ description: "Lookup Weather Station ID...", image: getAppImg("search_icon.png")
+ def defZip = getStZipCode() ? getStZipCode() : getNestZipCode()
+ input("custLocStr", "text", title: "Set Custom Weather Location?", required: false, defaultValue: defZip, submitOnChange: true,
+ image: getAppImg("weather_icon_grey.png"))
+ paragraph "Valid location entries are:${validEnt}", image: getAppImg("blank_icon.png")
+ atomicState.lastWeatherUpdDt = 0
+ atomicState?.lastForecastUpdDt = 0
+ }
+ }
+}
+
+def getWeatherConfDesc() {
+ def str = ""
+ def defZip = getStZipCode() ? getStZipCode() : getNestZipCode()
+ str += custLocStr ? " • Weather Location: (${custLocStr})" : " • Default Weather Location: (${defZip})"
+ return (str != "") ? "${str}" : null
+}
+
+def devCustomizePageDesc() {
+ def tempChgWaitValDesc = (!tempChgWaitVal || tempChgWaitVal == 4) ? "" : tempChgWaitVal
+ def wstr = weathAlertNotif ? "Enabled" : "Disabled"
+ def str = "Device Customizations:"
+ str += "\n • Man. Temp Change Delay:\n └ (${getInputEnumLabel(tempChgWaitVal ?: 4, waitValEnum())})"
+ str += "\n${getWeatherConfDesc()}"
+ str += "\n • Weather Alerts: (${wstr})"
+ return ((tempChgWaitValDesc || custLocStr || weathAlertNotif) ? str : "")
+}
+
+def getDevicesDesc() {
+ def str = ""
+ str += settings?.thermostats ? "\n • [${settings?.thermostats?.size()}] Thermostat${(settings?.thermostats?.size() > 1) ? "s" : ""}" : ""
+ str += settings?.protects ? "\n • [${settings?.protects?.size()}] Protect${(settings?.protects?.size() > 1) ? "s" : ""}" : ""
+ str += settings?.cameras ? "\n • [${settings?.cameras?.size()}] Camera${(settings?.cameras?.size() > 1) ? "s" : ""}" : ""
+ str += settings?.presDevice ? "\n • [1] Presence Device" : ""
+ str += settings?.weatherDevice ? "\n • [1] Weather Device" : ""
+ str += (!settings?.thermostats && !settings?.protects && !settings?.presDevice && !settings?.weatherDevice) ? "\n • No Devices Selected..." : ""
+ return (str != "") ? str : null
+}
+
+def b64Action(String str, dec=false) {
+ if (str) {
+ if(dec) {
+ return (String) str?.bytes?.decodeBase64()
+ } else {
+ return (String) str?.bytes?.encodeBase64(true)
+ }
+ }
+}
+
+def debugPrefPage() {
+ dynamicPage(name: "debugPrefPage", install: false) {
+ section ("Application Logs") {
+ input (name: "appDebug", type: "bool", title: "Show App Logs in the IDE?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("log.png"))
+ if(appDebug) {
+ input (name: "advAppDebug", type: "bool", title: "Show Verbose Logs?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("list_icon.png"))
+ LogAction("Debug Logs are Enabled...", "info", false)
+ }
+ else { LogAction("Debug Logs are Disabled...", "info", false) }
+ }
+ section ("Child Device Logs") {
+ input (name: "childDebug", type: "bool", title: "Show Device Logs in the IDE?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("log.png"))
+ if(childDebug) { LogAction("Device Debug Logs are Enabled...", "info", false) }
+ else { LogAction("Device Debug Logs are Disabled...", "info", false) }
+ }
+ atomicState.needChildUpd = true
+ }
+}
+
+def getAppDebugDesc() {
+ def str = ""
+ str += isAppDebug() ? "App Debug: (${debugStatus()})${advAppDebug ? "(Trace)" : ""}" : ""
+ str += isChildDebug() ? "${isAppDebug() ? "\n" : ""}Device Debug: (${deviceDebugStatus()})" : ""
+ return (str != "") ? "${str}" : null
+}
+
+def infoPage () {
+ dynamicPage(name: "infoPage", title: "Help, Info and Instructions", install: false) {
+ section("About this App:") {
+ paragraph appInfoDesc(), image: getAppImg("nest_manager%402x.png", true)
+ }
+ section("Help, Instructions, and Feedback:") {
+ href url: getReadmePageUrl(), style:"embedded", required:false, title:"Readme File",
+ description:"View the Projects Readme File...", state: "complete", image: getAppImg("readme_icon.png")
+ href url: getHelpPageUrl(), style:"embedded", required:false, title:"Help Pages",
+ description:"View the Help and Instructions Page...", state: "complete", image: getAppImg("help_icon.png")
+ href "feedbackPage", title: "Send Developer Feedback", description: "", image: getAppImg("feedback_icon.png")
+ }
+ section("Donations:") {
+ href url: textDonateLink(), style:"external", required: false, title:"Donations",
+ description:"Tap to Open in Mobile Browser...", state: "complete", image: getAppImg("donate_icon.png")
+ }
+ section("Created by:") {
+ paragraph "Anthony S. (@tonesto7)", state: "complete"
+ }
+ section("Collaborators:") {
+ paragraph "Ben W. (@desertblade)\nEric S. (@E_Sch)", state: "complete"
+ }
+ section("App Revision History:") {
+ href "changeLogPage", title: "View App Change Log Info", description: "Tap to View...", image: getAppImg("change_log_icon.png")
+ }
+ if(atomicState?.installationId) {
+ section("InstallationID:") {
+ paragraph "InstallationID:\n${atomicState?.installationId}"
+ }
+ }
+ section("Licensing Info:") {
+ paragraph "${textCopyright()}\n${textLicense()}"
+ }
+ }
+}
+
+def changeLogPage () {
+ dynamicPage(name: "changeLogPage", title: "View Change Info", install: false) {
+ section("App Revision History:") {
+ paragraph appVerInfo()
+ }
+ }
+}
+
+def uninstallPage() {
+ dynamicPage(name: "uninstallPage", title: "Uninstall", uninstall: true) {
+ section("") {
+ if(parent) {
+ paragraph "This will uninstall the ${app?.label} Automation!!!"
+ } else {
+ paragraph "This will uninstall the App, All Automation Apps and Child Devices.\n\nPlease make sure that any devices created by this app are removed from any routines/rules/smartapps before tapping Remove."
+ }
+ }
+ }
+}
+
+/******************************************************************************
+* NEST LOGIN PAGES *
+*******************************************************************************/
+def nestLoginPrefPage () {
+ if(!atomicState?.authToken) {
+ return authPage()
+ } else {
+ return dynamicPage(name: "nestLoginPrefPage", nextPage: atomicState?.authToken ? "" : "authPage", install: false) {
+ section("Authorization Info:") {
+ paragraph "Token Created:\n• ${atomicState?.tokenCreatedDt.toString() ?: "Not Found..."}"
+ paragraph "Token Expires:\n• ${atomicState?.tokenExpires ? "Never" : "Not Found..."}"
+ paragraph "Last Connection:\n• ${atomicState.lastDevDataUpd ? atomicState?.lastDevDataUpd.toString() : ""}"
+ }
+ section("Nest Login Preferences:") {
+ href "nestTokenResetPage", title: "Log Out and Reset your Nest Token", description: "Tap to Reset the Token...", required: true, state: null, image: getAppImg("reset_icon.png")
+ }
+ }
+ }
+}
+
+def nestTokenResetPage() {
+ return dynamicPage(name: "nestTokenResetPage", install: false) {
+ section ("Resetting Nest Token...") {
+ revokeNestToken()
+ atomicState.authToken = null
+ paragraph "Token has been reset...\nPress Done to return to Login page..."
+ }
+ }
+}
+
+
+/******************************************************************************
+* NEST API INFO PAGES *
+*******************************************************************************/
+
+def nestInfoPage () {
+ dynamicPage(name: "nestInfoPage", install: false) {
+ section("View All API Data Received from Nest:") {
+ if(atomicState?.structures) {
+ href "structInfoPage", title: "Nest Location(s) Info...", description: "Tap to view...", image: getAppImg("nest_structure_icon.png")
+ }
+ if(atomicState?.thermostats) {
+ href "tstatInfoPage", title: "Nest Thermostat(s) Info...", description: "Tap to view...", image: getAppImg("nest_like.png")
+ }
+ if(atomicState?.protects) {
+ href "protInfoPage", title: "Nest Protect(s) Info...", description: "Tap to view...", image: getAppImg("protect_icon.png")
+ }
+ if(atomicState?.cameras) {
+ href "camInfoPage", title: "Nest Camera(s) Info...", description: "Tap to view...", image: getAppImg("camera_icon.png")
+ }
+ if(!atomicState?.structures && !atomicState?.thermostats && !atomicState?.protects && !atomicState?.cameras) {
+ paragraph "There is nothing to show here...", image: getAppImg("instruct_icon.png")
+ }
+ }
+
+ if(atomicState?.protects) {
+ section("Nest Protect Alarm Simulation:") {
+ if(atomicState?.protects) {
+ href "alarmTestPage", title: "Test your Protect Devices\nBy Simulating Alarm Events", required: true , image: getAppImg("test_icon.png"), state: null, description: "Tap to Begin"
+ }
+ }
+ }
+ section("Diagnostics") {
+ href "diagPage", title: "View Diagnostic Info...", description: null, image: getAppImg("diag_icon.png")
+ }
+ }
+}
+
+def structInfoPage () {
+ dynamicPage(name: "structInfoPage", refreshInterval: 30, install: false) {
+ def noShow = [ "wheres", "cameras", "thermostats", "smoke_co_alarms", "structure_id" ]
+ section("") {
+ paragraph "Locations", state: "complete", image: getAppImg("nest_structure_icon.png")
+ }
+ atomicState?.structData?.each { struc ->
+ if(struc?.key == atomicState?.structures) {
+ def str = ""
+ def cnt = 0
+ section("Location Name: ${struc?.value?.name}") {
+ def data = struc?.value.findAll { !(it.key in noShow) }
+ data?.sort().each { item ->
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n"}• ${item?.key?.toString()}: (${item?.value})"
+ }
+ paragraph "${str}"
+ }
+ }
+ }
+ }
+}
+
+def tstatInfoPage () {
+ dynamicPage(name: "tstatInfoPage", refreshInterval: 30, install: false) {
+ def noShow = [ "where_id", "device_id", "structure_id" ]
+ section("") {
+ paragraph "Thermostats", state: "complete", image: getAppImg("nest_like.png")
+ }
+ atomicState?.thermostats?.sort().each { tstat ->
+ def str = ""
+ def cnt = 0
+ section("Thermostat Name: ${tstat?.value}") {
+ def data = atomicState?.deviceData?.thermostats[tstat?.key].findAll { !(it.key in noShow) }
+ data?.sort().each { item ->
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n"}• ${item?.key?.toString()}: (${item?.value})"
+ }
+ paragraph "${str}"
+ }
+ }
+ }
+}
+
+def protInfoPage () {
+ dynamicPage(name: "protInfoPage", refreshInterval: 30, install: false) {
+ def noShow = [ "where_id", "device_id", "structure_id" ]
+ section("") {
+ paragraph "Protects", state: "complete", image: getAppImg("protect_icon.png")
+ }
+ atomicState?.protects.sort().each { prot ->
+ def str = ""
+ def cnt = 0
+ section("Protect Name: ${prot?.value}") {
+ def data = atomicState?.deviceData?.smoke_co_alarms[prot?.key].findAll { !(it.key in noShow) }
+ data?.sort().each { item ->
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n"}• ${item?.key?.toString()}: (${item?.value})"
+ }
+ paragraph "${str}"
+ }
+ }
+ }
+}
+
+def camInfoPage () {
+ dynamicPage(name: "camInfoPage", refreshInterval: 30, install: false) {
+ def noShow = [ "where_id", "device_id", "structure_id" ]
+ section("") {
+ paragraph "Cameras", state: "complete", image: getAppImg("camera_icon.png")
+ }
+ atomicState?.cameras.sort().each { cam ->
+ def str = ""
+ def evtStr = ""
+ def cnt = 0
+ def cnt2 = 0
+ section("Camera Name: ${cam?.value}") {
+ def data = atomicState?.deviceData?.cameras[cam?.key].findAll { !(it.key in noShow) }
+ data?.sort().each { item ->
+ if(item?.key != "last_event") {
+ if(item?.key in ["app_url", "web_url"]) {
+ href url: item?.value, style:"external", required: false, title: item?.key.toString().replaceAll("\\_", " ").capitalize(), description:"Tap to View in Mobile Browser...", state: "complete"
+ } else {
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n"}• ${item?.key?.toString()}: (${item?.value})"
+ }
+ } else {
+ item?.value?.sort().each { item2 ->
+ if(item2?.key in ["app_url", "web_url", "image_url", "animated_image_url"]) {
+ href url: item2?.value, style:"external", required: false, title: "LastEvent: ${item2?.key.toString().replaceAll("\\_", " ").capitalize()}", description:"Tap to View in Mobile Browser...", state: "complete"
+ }
+ else {
+ cnt2 = cnt2+1
+ evtStr += "${(cnt2 <= 1) ? "" : "\n\n"} • (LastEvent) ${item2?.key?.toString()}: (${item2?.value})"
+ }
+ }
+ }
+ }
+ paragraph "${str}"
+ if(evtStr != "") {
+ paragraph "Last Event Data:\n\n${evtStr}"
+ }
+ }
+ }
+ }
+}
+
+def alarmTestPage () {
+ dynamicPage(name: "alarmTestPage", install: false, uninstall: false) {
+ if(atomicState?.protects) {
+ section("Select Carbon/Smoke Device to Test:") {
+ input(name: "alarmCoTestDevice", title:"Select the Protect to Test", type: "enum", required: false, multiple: false, submitOnChange: true,
+ metadata: [values:atomicState?.protects], image: getAppImg("protect_icon.png"))
+ }
+ if(settings?.alarmCoTestDevice) {
+ section("Select the Events to Generate:") {
+ input "alarmCoTestDeviceSimSmoke", "bool", title: "Simulate a Smoke Event?", defaultValue: false, submitOnChange: true, image: getDevImg("smoke_emergency.png")
+ input "alarmCoTestDeviceSimCo", "bool", title: "Simulate a Carbon Event?", defaultValue: false, submitOnChange: true, image: getDevImg("co_emergency.png")
+ input "alarmCoTestDeviceSimLowBatt", "bool", title: "Simulate a Low Battery Event?", defaultValue: false, submitOnChange: true, image: getDevImg("battery_low.png")
+ }
+ if(settings?.alarmCoTestDeviceSimLowBatt || settings?.alarmCoTestDeviceSimCo || settings?.alarmCoTestDeviceSimSmoke) {
+ section("Execute Selected Tests from Above:") {
+ if(!atomicState?.isAlarmCoTestActive) {
+ paragraph "WARNING: If your protect devices are used Smart Home Monitor (SHM) it will not see these as a test and will trigger any action/alarms you have configured...",
+ required: true, state: null
+ }
+ if(settings?.alarmCoTestDeviceSimSmoke && !settings?.alarmCoTestDeviceSimCo && !settings?.alarmCoTestDeviceSimLowBatt) {
+ href "simulateTestEventPage", title: "Simulate Smoke Event", params: ["testType":"smoke"], description: "Tap to Execute Test", required: true, state: null
+ }
+
+ if(settings?.alarmCoTestDeviceSimCo && !settings?.alarmCoTestDeviceSimSmoke && !settings?.alarmCoTestDeviceSimLowBatt) {
+ href "simulateTestEventPage", title: "Simulate Carbon Event", params: ["testType":"co"], description: "Tap to Execute Test", required: true, state: null
+ }
+
+ if(settings?.alarmCoTestDeviceSimLowBatt && !settings?.alarmCoTestDeviceSimCo && !settings?.alarmCoTestDeviceSimSmoke) {
+ href "simulateTestEventPage", title: "Simulate Battery Event", params: ["testType":"battery"], description: "Tap to Execute Test", required: true, state: null
+ }
+ }
+ }
+
+ if(atomicState?.isAlarmCoTestActive && (settings?.alarmCoTestDeviceSimLowBatt || settings?.alarmCoTestDeviceSimCo || settings?.alarmCoTestDeviceSimSmoke)) {
+ section("Instructions") {
+ paragraph "FYI: Clear ALL Selected Tests to Reset for New Alarm Test", required: true, state: null
+ }
+ if(!settings?.alarmCoTestDeviceSimLowBatt && !settings?.alarmCoTestDeviceSimCo && !settings?.alarmCoTestDeviceSimSmoke) {
+ atomicState?.isAlarmCoTestActive = false
+ atomicState?.curProtTestPageData = null
+ }
+ }
+ }
+ }
+ }
+}
+
+def simulateTestEventPage(params) {
+ def pName = getAutoType()
+ def testType
+ if(params?.testType) {
+ atomicState.curProtTestType = params?.testType
+ testType = params?.testType
+ } else {
+ testType = atomicState?.curProtTestType
+ }
+ dynamicPage(name: "simulateTestEventPage", refreshInterval: 10, install: false, uninstall: false) {
+ if(settings?.alarmCoTestDevice) {
+ def dev = getChildDevice(settings?.alarmCoTestDevice)
+ def testText
+ if(dev) {
+ section("Testing ${dev}...") {
+ def isRun = false
+ if(!atomicState?.isAlarmCoTestActive) {
+ atomicState?.isAlarmCoTestActive = true
+ if(testType == "co") {
+ testText = "Carbon 'Detected'"
+ dev?.runCoTest()
+ }
+ else if(testType == "smoke") {
+ testText = "Smoke 'Detected'"
+ dev?.runSmokeTest()
+ }
+ else if(testType == "co") {
+ testText = "Battery 'Replace'"
+ dev?.runBatteryTest()
+ }
+ LogAction("Sending ${testText} Event to '$dev'", "info", true)
+ paragraph "Sending ${testText} Event to '$dev'", state: "complete"
+ } else {
+ paragraph "Skipping... A Test is already Running...", required: true, state: null
+ }
+ }
+ }
+ }
+ }
+}
+
+def diagPage () {
+ dynamicPage(name: "diagPage", install: false) {
+ section("") {
+ paragraph "This page will allow you to view all diagnostic data related to the apps/devices in order to assist the developer in troubleshooting...", image: getAppImg("diag_icon.png")
+ }
+ section("State Size Info:") {
+ paragraph "Current State Usage:\n${getStateSizePerc()}% (${getStateSize()} bytes)", required: true, state: (getStateSizePerc() <= 70 ? "complete" : null),
+ image: getAppImg("progress_bar.png")
+ }
+ section("View Apps & Devices Data") {
+ href "managAppDataPage", title:"View Manager Data", description:"Tap to view...", image: getAppImg("view_icon.png")
+ href "childAppDataPage", title:"View Automations Data", description:"Tap to view...", image: getAppImg("view_icon.png")
+ href "childDevDataPage", title:"View Device Data", description:"Tap to view...", image: getAppImg("view_icon.png")
+ href "appParamsDataPage", title:"View AppParams Data", description:"Tap to view...", image: getAppImg("view_icon.png")
+ }
+ if(settings?.optInAppAnalytics || settings?.optInSendExceptions) {
+ section("Analytics Data") {
+ if(settings?.optInAppAnalytics) {
+ href url: getAppEndpointUrl("renderInstallData"), style:"embedded", required: false, title:"View Shared Install Data", description:"Tap to view Data...", image: getAppImg("app_analytics_icon.png")
+ }
+ href url: getAppEndpointUrl("renderInstallId"), style:"embedded", required: false, title:"View Your Installation ID", description:"Tap to view...", image: getAppImg("view_icon.png")
+ }
+ }
+ section("Recent Nest Command") {
+ def cmdDesc = ""
+ cmdDesc += "Last Command Details:"
+ cmdDesc += "\n • DateTime: (${atomicState?.lastCmdSentDt ?: "Nothing found..."})"
+ cmdDesc += "\n • Cmd Sent: (${atomicState?.lastCmdSent ?: "Nothing found..."})"
+ cmdDesc += "\n • Cmd Result: (${atomicState?.lastCmdSentStatus ?: "Nothing found..."})"
+
+ cmdDesc += "\n\n • Totals Commands Sent: (${!atomicState?.apiCommandCnt ? 0 : atomicState?.apiCommandCnt})"
+ paragraph "${cmdDesc}"
+ }
+ }
+}
+
+def appParamsDataPage() {
+ dynamicPage(name: "appParamsDataPage", refreshInterval: 30, install: false) {
+ if(atomicState?.appData) {
+ atomicState?.appData?.sort().each { sec ->
+ section("${sec?.key.toString().capitalize()}:") {
+ def str = ""
+ def cnt = 0
+ sec?.value.each { par ->
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n"}• ${par?.key.toString()}: ${par?.value}"
+ }
+ paragraph "${str}"
+ }
+ }
+ }
+ }
+}
+
+def managAppDataPage() {
+ dynamicPage(name: "managAppDataPage", refreshInterval:30, install: false) {
+ def noShow = ["accessToken", "authToken" /*, "curAlerts", "curAstronomy", "curForecast", "curWeather"*/]
+ section("SETTINGS DATA:") {
+ def str = ""
+ def cnt = 0
+ def data = settings?.findAll { !(it.key in noShow) }
+ data?.sort().each { item ->
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n"}• ${item?.key.toString()}: (${item?.value})"
+ }
+ paragraph "${str}"
+ }
+ section("STATE DATA:") {
+ def str = ""
+ def cnt = 0
+ def data = state?.findAll { !(it.key in noShow) }
+ data?.sort().each { item ->
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n"}• ${item?.key.toString()}: (${item?.value})"
+ }
+ paragraph "${str}"
+ }
+ section("APP METADATA:") {
+ def str = ""
+ def cnt = 0
+ getMetadata()?.sort().each { item ->
+ cnt = cnt+1
+ str += "${(cnt <= 1) ? "" : "\n\n\n"}${item?.key.toString().toUpperCase()}:\n\n"
+ def cnt2 = 0
+ item?.value.sort().each { vals ->
+ cnt2 = cnt2+1
+ str += "${(cnt2 <= 1) ? "" : "\n\n"}• ${vals?.key.toString()}: (${vals?.value})"
+ }
+ }
+ paragraph "${str}"
+ }
+ }
+}
+
+def childAppDataPage() {
+ dynamicPage(name: "childAppDataPage", refreshInterval:30, install:false) {
+ def apps = getChildApps()
+ if(apps) {
+ apps?.each { ca ->
+ def str = ""
+ section("${ca?.label.toString().capitalize()}:") {
+ str += " ─────SETTINGS DATA─────"
+ def setData = ca?.getSettingsData()
+ setData?.sort().each { sd ->
+ str += "\n\n• ${sd?.key.toString()}: (${sd?.value})"
+ }
+ def appData = ca?.getAppStateData()
+ str += "\n\n\n ───────STATE DATA──────"
+ appData?.sort().each { par ->
+ str += "\n\n• ${par?.key.toString()}: (${par?.value})"
+ }
+ paragraph "${str}"
+ }
+ }
+ } else {
+ section("") { paragraph "No Child Apps Installed..." }
+ }
+ }
+}
+
+def childDevDataPage() {
+ dynamicPage(name: "childDevDataPage", refreshInterval:180, install: false) {
+ getAllChildDevices().each { dev ->
+ def str = ""
+ section("${dev?.displayName.toString().capitalize()}:") {
+ str += " ───────STATE DATA──────"
+ dev?.getDeviceStateData()?.sort().each { par ->
+ str += "\n\n• ${par?.key.toString()}: (${par?.value})"
+ }
+ str += "\n\n\n ────SUPPORTED ATTRIBUTES────"
+ def devData = dev?.supportedAttributes.collect { it as String }
+ devData?.sort().each {
+ str += "\n\n• ${"$it" as String}: (${dev.currentValue("$it")})"
+ }
+ str += "\n\n\n ────SUPPORTED COMMANDS────"
+ dev?.supportedCommands?.sort().each { cmd ->
+ str += "\n\n• ${cmd.name}(${!cmd?.arguments ? "" : cmd?.arguments.toString().toLowerCase().replaceAll("\\[|\\]", "")})"
+ }
+
+ str += "\n\n\n ─────DEVICE CAPABILITIES─────"
+ dev?.capabilities?.sort().each { cap ->
+ str += "\n\n• ${cap}"
+ }
+ paragraph "${str}"
+ }
+ }
+ }
+}
+
+def feedbackPage() {
+ def fbData = atomicState?.lastFeedbackData
+ def fbNoDup = (settings?.feedbackMsg && fbData?.lastMsg == settings?.feedbackMsg) ? false : true
+ def fbLenOk = (settings?.feedbackMsg && settings?.feedbackMsg?.toString().length() > 20) ? true : false
+ def msgOk = (settings?.feedbackMsg && fbLenOk && fbNoDup) ? true : false
+ //log.debug "msgOk: ($msgOk) | [fbNoDup: $fbNoDup, fbLenOk: $fbLenOk]"
+ dynamicPage(name: "feedbackPage", install: false, nextPage: (msgOk ? "sendFeedbackPage" : ""), uninstall: false) {
+ section {
+ paragraph "Submit your feedback to the developer"
+ input "feedbackMsg", "text", title: "Enter your Feedback here...", submitOnChange: true, defaultValue: null
+ if (settings?.feedbackMsg != null || settings?.feedbackMsg != "") {
+ if (!fbLenOk) {
+ paragraph "The current feedback message is too short!!!.\n\n(minimum 20 Char.)...", required: true, state: null
+ }
+ if (!fbNoDup) {
+ paragraph "You've already sent the same feedback on (${fbData?.lastMsgDt}).\n\nPlease edit before trying again...", required: true, state: null
+ }
+ }
+ }
+ if (fbData) {
+ section {
+ paragraph "You Last Sent Feedback on:\n${fbData?.lastMsgDt}\nFor App Version: ${fbData?.lastAppVer}"
+ }
+ }
+ if (msgOk) {
+ section {
+ paragraph "Tap Next to Send...", state: "complete"
+ }
+ }
+ }
+}
+
+def sendFeedbackPage() {
+ dynamicPage(name: "sendFeedbackPage", install: false, nextPage: "mainPage", uninstall: false) {
+ def fbData = atomicState?.lastFeedbackData
+ section("Feedback Status:") {
+ if (settings?.feedbackMsg) {
+ def ok2send = true
+ if (fbData) {
+ if (fbData?.lastMsg == settings?.feedbackMsg) {
+ ok2send = false
+ paragraph "SKIPPING...\nYou've already sent the same feedback on (${fbData?.lastMsgDt}).\n\nPlease go back and edit before trying again...", required: true, state: null
+ }
+ }
+ if(ok2send) {
+ atomicState?.feedbackPending = true
+ runIn(5, "sendFeedbackData", [overwrite: true])
+ paragraph "Your feedback has been queued and will be submitted shortly...", title: "Thank You", state: "complete"
+ }
+ } else {
+ paragraph "It appears that your are missing feedback text on the previous page...", required: true, state: null
+ }
+ }
+ }
+}
+
+/*
+ NOTE: Keep this for furture backup/restore reference
+ setSettings(theSettingNameHer: [type: "capability.contactSensor", value: settingValueToSet])
+*/
+
+/******************************************************************************
+* Firebase Analytics Functions *
+*******************************************************************************/
+def createInstallDataJson() {
+ try {
+ generateInstallId()
+ def tsVer = atomicState?.tDevVer ?: "Not Installed"
+ def ptVer = atomicState?.pDevVer ?: "Not Installed"
+ def cdVer = atomicState?.camDevVer ?: "Not Installed"
+ def pdVer = atomicState?.presDevVer ?: "Not Installed"
+ def wdVer = atomicState?.weatDevVer ?: "Not Installed"
+ def vtsVer = atomicState?.vtDevVer ?: "Not Installed"
+ def versions = ["apps":["manager":appVersion()?.toString()], "devices":["thermostat":tsVer, "vthermostat":vtsVer, "protect":ptVer, "camera":cdVer, "presence":pdVer, "weather":wdVer]]
+
+ def tstatCnt = atomicState?.thermostats?.size() ?: 0
+ def protCnt = atomicState?.protects?.size() ?: 0
+ def camCnt = atomicState?.cameras?.size() ?: 0
+ def vstatCnt = atomicState?.vThermostats?.size() ?: 0
+ def automations = !atomicState?.installedAutomations ? "No Automations Installed" : atomicState?.installedAutomations
+ def tz = getTimeZone()?.ID?.toString()
+ def apiCmdCnt = !atomicState?.apiCommandCnt ? 0 : atomicState?.apiCommandCnt
+ def cltType = !mobileClientType ? "Not Configured" : mobileClientType?.toString()
+ def appErrCnt = !atomicState?.appExceptionCnt ? 0 : atomicState?.appExceptionCnt
+ def devErrCnt = !atomicState?.childExceptionCnt ? 0 : atomicState?.childExceptionCnt
+ def data = [
+ "guid":atomicState?.installationId, "versions":versions, "thermostats":tstatCnt, "protects":protCnt, "vthermostats":vstatCnt, "cameras":camCnt, "appErrorCnt":appErrCnt, "devErrorCnt":devErrCnt,
+ "automations":automations, "timeZone":tz, "apiCmdCnt":apiCmdCnt, "stateUsage":"${getStateSizePerc()}%", "mobileClient":cltType, "datetime":getDtNow()?.toString()
+ ]
+ def resultJson = new groovy.json.JsonOutput().toJson(data)
+ return resultJson
+
+ } catch (ex) {
+ log.error "createInstallDataJson: Exception:", ex
+ sendExceptionData(ex, "createInstallDataJson")
+ }
+}
+
+def renderInstallData() {
+ try {
+ def resultJson = createInstallDataJson()
+ def resultString = new groovy.json.JsonOutput().prettyPrint(resultJson)
+ render contentType: "application/json", data: resultString
+ } catch (ex) { log.error "renderInstallData Exception:", ex }
+}
+
+def renderInstallId() {
+ try {
+ def resultJson = new groovy.json.JsonOutput().toJson(atomicState?.installationId)
+ render contentType: "application/json", data: resultJson
+ } catch (ex) { log.error "renderInstallId Exception:", ex }
+}
+
+def sendInstallData() {
+ if(settings?.optInAppAnalytics) {
+ sendFirebaseData(createInstallDataJson(), "installData/clients/${atomicState?.installationId}.json")
+ }
+}
+
+def removeInstallData() {
+ if(settings?.optInAppAnalytics) {
+ return removeFirebaseData("installData/clients/${atomicState?.installationId}.json")
+ }
+}
+
+def sendExceptionData(ex, methodName, isChild = false, autoType = null) {
+ if(atomicState?.appData?.database?.disableExceptions == true) {
+ return
+ } else {
+ def exCnt = 0
+ def exString
+ if(ex instanceof java.lang.NullPointerException) {// || ex instanceof java.lang.SecurityException) {
+ //LogAction("sendExceptionData: NullPointerException was caught successfully...", "info", true)
+ return
+ } else {
+ exString = ex.message.toString()
+ //log.debug "sendExceptionData: Exception Message (${exString})"
+ }
+ exCnt = atomicState?.appExceptionCnt ? atomicState?.appExceptionCnt + 1 : 1
+ atomicState?.appExceptionCnt = exCnt ?: 1
+ if(settings?.optInSendExceptions) {
+ def appType = isChild && autoType ? "automationApp/${autoType}" : "managerApp"
+ def exData
+ if(isChild) {
+ exData = ["methodName":methodName, "automationType":autoType, "appVersion":(appVersion() ?: "Not Available"),"errorMsg":exString, "errorDt":getDtNow().toString()]
+ } else {
+ exData = ["methodName":methodName, "appVersion":(appVersion() ?: "Not Available"),"errorMsg":exString, "errorDt":getDtNow().toString()]
+ }
+ def results = new groovy.json.JsonOutput().toJson(exData)
+ sendFirebaseData(results, "errorData/${appType}/${methodName}.json", "post", "Exception")
+ }
+ }
+}
+
+def sendChildExceptionData(devType, devVer, ex, methodName) {
+ def exCnt = 0
+ def exString
+ if(ex instanceof java.lang.NullPointerException) {// || ex instanceof java.lang.SecurityException) {
+ return
+ } else {
+ exString = ex.message.toString()
+ }
+ exCnt = atomicState?.childExceptionCnt ? atomicState?.childExceptionCnt + 1 : 1
+ atomicState?.childExceptionCnt = exCnt ?: 1
+ if(settings?.optInSendExceptions) {
+ def exData = ["deviceType":devType, "devVersion":(devVer ?: "Not Available"), "methodName":methodName, "errorMsg":exString, "errorDt":getDtNow().toString()]
+ def results = new groovy.json.JsonOutput().toJson(exData)
+ sendFirebaseData(results, "errorData/${devType}/${methodName}.json", "post", "Exception")
+ }
+}
+
+def sendFeedbackData(msg) {
+ def cltId = atomicState?.installationId
+ def exData = ["guid":atomicState?.installationId, "version":appVersion(), "feedbackMsg":(msg ? msg : (settings?.feedbackMsg ?: "No Text")), "msgDt":getDtNow().toString()]
+ def results = new groovy.json.JsonOutput().toJson(exData)
+ if(sendFirebaseData(results, "feedback/data.json", "post", "Feedback")) {
+ atomicState?.feedbackPending = false
+ if(!msg) { atomicState?.lastFeedbackData = ["lastMsg":settings?.feedbackMsg, "lastMsgDt":getDtNow().toString(), "lastAppVer":appVersion()] }
+ }
+}
+
+def sendFirebaseData(data, pathVal, cmdType=null, type=null) {
+ LogAction("sendFirebaseData(${data}, ${pathVal}, $cmdType, $type", "trace", false)
+ def json = new groovy.json.JsonOutput().prettyPrint(data)
+ def result = false
+ def params = [ uri: "${getFirebaseAppUrl()}/${pathVal}", body: json.toString() ]
+ def typeDesc = type ? "${type}" : "Data"
+ def respData
+ try {
+ if(!cmdType || cmdType == "put") {
+ httpPutJson(params) { resp ->
+ respData = resp
+ }
+ }
+ else if (cmdType == "post") {
+ httpPostJson(params) { resp ->
+ respData = resp
+ }
+ }
+ if(respData) {
+ //log.debug "respData: ${respData}"
+ if( respData?.status == 200) {
+ LogAction("sendFirebaseData: ${typeDesc} Data Sent Successfully!!!", "info", true)
+ atomicState?.lastAnalyticUpdDt = getDtNow()
+ result = true
+ }
+ else if(respData?.status == 400) {
+ LogAction("sendFirebaseData: 'Bad Request' Exception: ${respData?.status}", "error", true)
+ }
+ else {
+ LogAction("sendFirebaseData: 'Unexpected' Response: ${respData?.status}", "warn", true)
+ }
+ }
+ }
+ catch (ex) {
+ if(ex instanceof groovyx.net.http.HttpResponseException) {
+ LogAction("sendFirebaseData: 'HttpResponseException' Exception: ${ex.message}", "error", true)
+ }
+ else { log.error "sendFirebaseData: ([$data, $pathVal, $cmdType, $type]) Exception:", ex }
+ sendExceptionData(ex, "sendFirebaseData")
+ }
+ return result
+}
+
+def removeFirebaseData(pathVal) {
+ log.trace "removeFirebaseData(${pathVal}"
+ def result = true
+ try {
+ httpDelete(uri: "${getFirebaseAppUrl()}/${pathVal}") { resp ->
+ log.debug "resp status: ${resp?.status}"
+ }
+ }
+ catch (ex) {
+ if(ex instanceof groovyx.net.http.ResponseParseException) {
+ LogAction("removeFirebaseData: Response: ${ex.message}", "info", true)
+ } else {
+ LogAction("removeFirebaseData: Exception: ${ex.message}", "error", true)
+ sendExceptionData(ex, "removeFirebaseData")
+ result = false
+ }
+ }
+ return result
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+/********************************************************************************************
+| Application Name: Nest Automations |
+| Author: Anthony S. (@tonesto7) | Eric S. (@E_Sch) |
+|********************************************************************************************/
+/////////////////////////////////////////////////////////////////////////////////////////////
+
+def selectAutoPage() {
+ //log.trace "selectAutoPage()..."
+ if(!atomicState?.automationType) {
+ return dynamicPage(name: "selectAutoPage", title: "Choose an Automation Type...", uninstall: false, install: false, nextPage: null) {
+ def thereIsChoice = !parent.automationNestModeEnabled(null)
+ if(thereIsChoice) {
+ section("Set Nest Presence Based on ST Modes, Presence Sensor, or Switches:") {
+ href "mainAutoPage", title: "Nest Mode Automations", description: "", params: [autoType: "nMode"], image: getAppImg("mode_automation_icon.png")
+ }
+ }
+ section("Thermostat Automations: Setpoints, Remote Sensor, External Temp, Contact Sensor, Leak Sensor, Fan Control") {
+ href "mainAutoPage", title: "Thermostat Automations", description: "", params: [autoType: "schMot"], image: getAppImg("thermostat_automation_icon.png")
+ }
+ }
+ }
+ else { return mainAutoPage( [autoType: atomicState?.automationType]) }
+}
+
+def mainAutoPage(params) {
+ //log.trace "mainAutoPage()"
+ if(!atomicState?.tempUnit) { atomicState?.tempUnit = getTemperatureScale()?.toString() }
+ if(!atomicState?.disableAutomation) { atomicState.disableAutomation = false }
+ atomicState?.showHelp = (parent?.getShowHelp() != null) ? parent?.getShowHelp() : true
+ def autoType = null
+ //If params.autoType is not null then save to atomicState.
+ if(!params?.autoType) { autoType = atomicState?.automationType }
+ else { atomicState.automationType = params?.autoType; autoType = params?.autoType }
+
+ // If the selected automation has not been configured take directly to the config page. Else show main page
+ if(autoType == "nMode" && !isNestModesConfigured()) { return nestModePresPage() }
+ else if(autoType == "watchDog" && !isWatchdogConfigured()) { return watchDogPage() }
+ else if(autoType == "schMot" && !isSchMotConfigured()) { return schMotModePage() }
+
+ else {
+ // Main Page Entries
+ //return dynamicPage(name: "mainAutoPage", title: "Automation Configuration", uninstall: false, install: false, nextPage: "nameAutoPage" ) {
+ return dynamicPage(name: "mainAutoPage", title: "Automation Configuration", uninstall: false, install: true, nextPage:null ) {
+ section() {
+ if(disableAutomationreq) {
+ paragraph "This Automation is currently disabled!!!\nTurn it back on to to make changes or resume operation...", required: true, state: null, image: getAppImg("instruct_icon.png")
+ }
+ if(autoType == "nMode" && !atomicState?.disableAutomation) {
+ //paragraph title:"Set Nest Presence Based on ST Modes, Presence Sensor, or Switches:", ""
+ def nDesc = ""
+ nDesc += isNestModesConfigured() ? "Nest Mode:\n • Status: (${getNestLocPres().toString().capitalize()})" : ""
+ 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 += (nModeDelay && nModeDelayVal) ? "\n • Delay: ${getEnumValue(longTimeSecEnum(), nModeDelayVal)}" : ""
+ nDesc += (settings?."${getAutoType()}Modes" || settings?."${getAutoType()}Days" || (settings?."${getAutoType()}StartTime" && settings?."${getAutoType()}StopTime")) ?
+ "\n • Evaluation Allowed: (${autoScheduleOk(getAutoType()) ? "ON" : "OFF"})" : ""
+ nDesc += (nModePresSensor || nModeSwitch) || (!nModePresSensor && !nModeSwitch && (nModeAwayModes && nModeHomeModes)) ? "\n\nTap to Modify..." : ""
+ def nModeDesc = isNestModesConfigured() ? "${nDesc}" : null
+ href "nestModePresPage", title: "Nest Mode Automation Config", description: nModeDesc ?: "Tap to Configure...", state: (nModeDesc ? "complete" : null), image: getAppImg("mode_automation_icon.png")
+ }
+
+ if(autoType == "schMot" && !atomicState?.disableAutomation) {
+ //paragraph title:"Thermostat Automation:", ""
+ def sDesc = ""
+ sDesc += settings?.schMotTstat ? "${settings?.schMotTstat?.label}" : ""
+ //sDesc += settings?.schMotTstat ? getTstatModeDesc() : ""
+
+ if(settings?.schMotWaterOff) {
+ sDesc += "\n • Turn Off if Leak Detected"
+ }
+ if(settings?.schMotContactOff) {
+ sDesc += "\n • Turn Off if Contact Open"
+ }
+ if(settings?.schMotExternalTempOff) {
+ sDesc += "\n • Turn Off based on External Temp"
+ }
+ if(settings?.schMotRemoteSensor) {
+ sDesc += "\n • Use Remote Temp Sensors"
+ }
+ if(isTstatSchedConfigured()) {
+ sDesc += "\n • Setpoint Schedules Created"
+ }
+ if(settings?.schMotOperateFan) {
+ sDesc += "\n • Control Fans with HVAC"
+ }
+
+ sDesc += settings?.schMotTstat ? "\n\nTap to Modify..." : ""
+ def sModeDesc = isSchMotConfigured() ? "${sDesc}" : null
+ href "schMotModePage", title: "Thermostat Automation Config", description: sModeDesc ?: "Tap to Configure...", state: (sModeDesc ? "complete" : null), image: getAppImg("thermostat_automation_icon.png")
+ }
+
+ if(autoType == "watchDog" && !atomicState?.disableAutomation) {
+ //paragraph title:"Watch your Nest Location for Events:", ""
+ def watDesc = ""
+ watDesc += (settings["${getAutoType()}AllowSpeechNotif"] && (settings["${getAutoType()}SpeechDevices"] || settings["${getAutoType()}SpeechMediaPlayer"]) && getVoiceNotifConfigDesc("watchDog")) ?
+ "\n\nVoice Notifications:${getVoiceNotifConfigDesc("watchDog")}" : ""
+ def watDogDesc = isWatchdogConfigured() ? "${watDesc}" : null
+ href "watchDogPage", title: "Nest Location Watchdog...", description: watDogDesc ?: "Tap to Configure...", state: (watDogDesc ? "complete" : null), image: getAppImg("watchdog_icon.png")
+ }
+ }
+ section("Automation Options:") {
+ if(atomicState?.isInstalled && (isNestModesConfigured() || isWatchdogConfigured() || isSchMotConfigured())) {
+ //paragraph title:"Enable/Disable this Automation", ""
+ input "disableAutomationreq", "bool", title: "Disable this Automation?", required: false, defaultValue: disableAutomation, submitOnChange: true, image: getAppImg("disable_icon.png")
+ if(!atomicState?.disableAutomation && disableAutomationreq) {
+ LogAction("This Automation was Disabled at (${getDtNow()})", "info", true)
+ atomicState?.disableAutomationDt = getDtNow()
+ } else if(atomicState?.disableAutomation && !disableAutomationreq) {
+ LogAction("This Automation was Restored at (${getDtNow()})", "info", true)
+ atomicState?.disableAutomationDt = null
+ }
+ atomicState.disableAutomation = disableAutomationreq
+ }
+ input ("showDebug", "bool", title: "Debug Option", description: "Show App Logs in the IDE?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("log.png"))
+ atomicState?.showDebug = showDebug
+ }
+ section("Automation Name:") {
+ if(autoType == "watchDog") {
+ paragraph "${app?.label}"
+ } else {
+ def newName = getAutoTypeLabel()
+ label title: "Label this Automation:", description: "Suggested Name: ${newName}", defaultValue: newName, required: true, wordWrap: true, image: getAppImg("name_tag_icon.png")
+ if(!atomicState?.isInstalled) {
+ paragraph "FYI:\nMake sure to name it something that will help you easily identify the automation later."
+ }
+ }
+ }
+ }
+ }
+}
+
+// parent only method
+def automationNestModeEnabled(val) {
+LogAction("automationNestModeEnabled: val: $val", "info", true)
+ if(val == null) {
+ return atomicState?.automationNestModeEnabled ?: false
+ } else {
+ atomicState.automationNestModeEnabled = val.toBoolean()
+ }
+ return atomicState?.automationNestModeEnabled ?: false
+}
+
+def initAutoApp() {
+ if(settings["watchDogFlag"]) {
+ atomicState?.automationType = "watchDog"
+ }
+ def autoType = getAutoType()
+ if(autoType == "nMode") {
+ parent.automationNestModeEnabled(true)
+ }
+ unschedule()
+ unsubscribe()
+ automationsInst()
+
+ if(autoType == "schMot" && isSchMotConfigured()) {
+ atomicState.scheduleList = [ 1,2,3,4 ]
+ def schedList = atomicState.scheduleList
+ def timersActive = false
+ def sLbl
+ def cnt = 1
+ def numact = 0
+ schedList?.each { scd ->
+ sLbl = "schMot_${scd}_"
+ atomicState."schedule${cnt}SwEnabled" = null
+ atomicState."schedule${cnt}MotionEnabled" = null
+ atomicState."schedule${cnt}SensorEnabled" = null
+
+ def newscd = []
+ def act = settings["${sLbl}SchedActive"]
+ if(act) {
+ newscd = cleanUpMap([
+ m: settings["${sLbl}restrictionMode"],
+ tf: settings["${sLbl}restrictionTimeFrom"],
+ tfc: settings["${sLbl}restrictionTimeFromCustom"],
+ tfo: settings["${sLbl}restrictionTimeFromOffset"],
+ tt: settings["${sLbl}restrictionTimeTo"],
+ ttc: settings["${sLbl}restrictionTimeToCustom"],
+ tto: settings["${sLbl}restrictionTimeToOffset"],
+ w: settings["${sLbl}restrictionDOW"],
+ s1: buildDeviceNameList(settings["${sLbl}restrictionSwitchOn"], "and"),
+ s0: buildDeviceNameList(settings["${sLbl}restrictionSwitchOff"], "and"),
+ ctemp: roundTemp(settings["${sLbl}CoolTemp"]),
+ htemp: roundTemp(settings["${sLbl}HeatTemp"]),
+ hvacm: settings["${sLbl}HvacMode"],
+ sen0: settings["schMotRemoteSensor"] ? buildDeviceNameList(settings["${sLbl}remSensor"], "and") : 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,
+ mdelayOn: settings["${sLbl}Motion"] ? settings["${sLbl}MDelayValOn"] : null,
+ mdelayOff: settings["${sLbl}Motion"] ? settings["${sLbl}MDelayValOff"] : null
+ ])
+
+ numact += 1
+ }
+ LogAction("initAutoApp: [Schedule: $scd | sLbl: $sLbl | act: $act | newscd: $newscd]", "info", true)
+ atomicState."sched${cnt}restrictions" = newscd
+ atomicState."schedule${cnt}SwEnabled" = (newscd?.s1 || newscd?.s0) ? true : false
+ atomicState."schedule${cnt}MotionEnabled" = (newscd?.m0) ? true : false
+ atomicState."schedule${cnt}SensorEnabled" = (newscd?.sen0) ? true : false
+ //atomicState."schedule${cnt}FanCtrlEnabled" = (newscd?.fan0) ? true : false
+ atomicState."schedule${cnt}TimeActive" = (newscd?.tf || newscd?.tfc || newscd?.tfo || newscd?.tt || newscd?.ttc || newscd?.tto || newscd?.w) ? true : false
+
+ atomicState."${sLbl}MotionActiveDt" = null
+ atomicState."${sLbl}MotionInActiveDt" = null
+
+ def newact = isMotionActive(settings["${sLbl}Motion"])
+ if(newact) { atomicState."${sLbl}MotionActiveDt" = getDtNow() }
+ else { atomicState."${sLbl}MotionInActiveDt" = getDtNow() }
+
+ atomicState."${sLbl}oldMotionActive" = newact
+ atomicState?."motion${cnt}UseMotionSettings" = null // clear automation state of schedule in use motion state
+ atomicState?."motion${mySched}LastisBtwn" = false
+
+ timersActive = (timersActive || atomicState."schedule${cnt}TimeActive") ? true : false
+
+ cnt += 1
+ }
+ atomicState.scheduleTimersActive = timersActive
+ atomicState.lastSched = null // clear automation state of schedule in use
+ atomicState.scheduleSchedActiveCount = numact
+ }
+
+ subscribeToEvents()
+ scheduler()
+ app.updateLabel(getAutoTypeLabel())
+ atomicState?.lastAutomationSchedDt = null
+ watchDogAutomation()
+}
+
+def uninstAutomationApp() {
+ //log.trace "uninstAutomationApp..."
+ def autoType = getAutoType()
+ if(autoType == "schMot") {
+ def myID = getMyLockId()
+ if(schMotTstat && myID && parent) {
+ if(parent?.addRemoveVthermostat(schMotTstat.deviceNetworkId, false, myID)) {
+ LogAction("cleanup check for virtual thermostat", "debug", true)
+ }
+ if( parent?.remSenUnlock(atomicState?.remSenTstat, myID) ) { // attempt unlock old ID
+ LogAction("Released remote sensor lock", "debug", true)
+ }
+ }
+ }
+ if(autoType == "nMode") {
+ parent?.automationNestModeEnabled(false)
+ }
+}
+
+def getAutoTypeLabel() {
+ //LogAction("getAutoTypeLabel:","trace", true)
+ def type = atomicState?.automationType
+ def appLbl = app?.label?.toString()
+ def newName = appName() == "Nest Manager" ? "Nest Automations" : "${appName()}"
+ def typeLabel = ""
+ def newLbl
+ def dis = atomicState?.disableAutomation ? "\n(Disabled)" : ""
+ if(type == "nMode") { typeLabel = "${newName} (NestMode)" }
+ else if(type == "watchDog") { typeLabel = "Nest Location ${location.name} Watchdog"}
+ else if(type == "schMot") { typeLabel = "${newName} (${schMotTstat?.label})" }
+
+ if(appLbl != "Nest Manager") {
+ 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) {
+ return settings[var] ?: null
+}
+
+def getStateVal(var) {
+ return state[var] ?: null
+}
+
+def getAutoType() { return !parent ? "" : atomicState?.automationType }
+
+def getAutoIcon(type) {
+ if(type) {
+ switch(type) {
+ case "remSen":
+ return getAppImg("remote_sensor_icon.png")
+ break
+ case "fanCtrl":
+ return getAppImg("fan_control_icon.png")
+ break
+ case "conWat":
+ return getAppImg("open_window.png")
+ break
+ case "leakWat":
+ return getAppImg("leak_icon.png")
+ break
+ case "extTmp":
+ return getAppImg("external_temp_icon.png")
+ break
+ case "nMode":
+ return getAppImg("mode_automation_icon.png")
+ break
+ case "schMot":
+ return getAppImg("thermostat_automation_icon.png")
+ break
+ case "tMode":
+ return getAppImg("mode_setpoints_icon.png")
+ break
+ case "watchDog":
+ return getAppImg("watchdog_icon.png")
+ break
+ }
+ }
+}
+
+def automationsInst() {
+ atomicState.isNestModesConfigured = isNestModesConfigured() ? true : false
+ atomicState.isSchMotConfigured = isSchMotConfigured() ? true : false
+ atomicState.isWatchdogConfigured = isWatchdogConfigured() ? true : false
+ atomicState.isFanCtrlConfigured = isFanCtrlConfigured() ? true : false
+ atomicState.isTstatSchedConfigured = isTstatSchedConfigured() ? true : false
+ atomicState.isExtTmpConfigured = isExtTmpConfigured() ? true : false
+ atomicState.isConWatConfigured = isConWatConfigured() ? true : false
+ atomicState.isLeakWatConfigured = isLeakWatConfigured() ? true : false
+ atomicState.isFanCircConfigured = isFanCircConfigured() ? true : false
+ atomicState?.isInstalled = true
+}
+
+def getAutomationsInstalled() {
+ def list = []
+ def aType = atomicState?.automationType
+ switch(aType) {
+ case "nMode":
+ list.push(aType)
+ break
+ case "schMot":
+ def tmp = [:]
+ tmp[aType] = []
+ if(isFanCtrlConfigured()) { tmp[aType].push("fanCtrl") }
+ if(isFanCircConfigured()) { tmp[aType].push("fanCirc") }
+ if(isTstatSchedConfigured()) { tmp[aType].push("tSched") }
+ if(isExtTmpConfigured()) { tmp[aType].push("extTmp") }
+ if(isConWatConfigured()) { tmp[aType].push("conWat") }
+ if(isLeakWatConfigured()) { tmp[aType].push("leakWat") }
+ if(tmp?.size()) { list.push(tmp) }
+ break
+ case "watchDog":
+ list.push(aType)
+ break
+ }
+ LogAction("getAutomationsInstalled List: $list", "debug", false)
+ return list
+}
+
+def getAutomationType() {
+ return atomicState?.automationType ?: null
+}
+
+def getIsAutomationDisabled() {
+ return atomicState?.disableAutomation ? true : false
+}
+
+def subscribeToEvents() {
+ //Remote Sensor Subscriptions
+ def autoType = getAutoType()
+
+ //Nest Mode Subscriptions
+ if(autoType == "nMode") {
+ if(isNestModesConfigured()) {
+ if(!settings?.nModePresSensor && !settings?.nModeSwitch && (settings?.nModeHomeModes || settings?.nModeAwayModes)) { subscribe(location, "mode", nModeSTModeEvt, [filterEvents: false]) }
+ if(settings?.nModePresSensor && !settings?.nModeSwitch) { subscribe(nModePresSensor, "presence", nModePresEvt) }
+ if(settings?.nModeSwitch && !settings?.nModePresSensor) { subscribe(nModeSwitch, "switch", nModeSwitchEvt) }
+ }
+ }
+
+ //ST Thermostat Motion
+ if(autoType == "schMot") {
+ def needThermTemp
+ def needThermMode
+ def needThermPres
+
+ if(isSchMotConfigured()) {
+ if(settings?.schMotWaterOff) {
+ if(isLeakWatConfigured()) { subscribe(leakWatSensors, "water", leakWatSensorEvt) }
+ }
+ if(settings?.schMotContactOff) {
+ if(isConWatConfigured()) { subscribe(conWatContacts, "contact", conWatContactEvt) }
+ }
+ if(settings?.schMotExternalTempOff) {
+ if(isExtTmpConfigured()) {
+ if(!settings?.extTmpUseWeather && settings?.extTmpTempSensor) { subscribe(extTmpTempSensor, "temperature", extTmpTempEvt, [filterEvents: false]) }
+ if(settings?.extTmpUseWeather) {
+ atomicState.NeedwUpd = true
+ if(parent?.getWeatherDeviceInst()) {
+ def weather = parent?.getWeatherDevice()
+ if(weather) {
+ subscribe(weather, "temperature", extTmpTempEvt)
+ subscribe(weather, "dewpoint", extTmpDpEvt)
+ }
+ } else { LogAction("No weather device found", "error", true) }
+ }
+ atomicState.extTmpChgWhileOnDt = getDtNow()
+ atomicState.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", automationTempSenEvt)
+ }
+ }
+ }
+ }
+ }
+ if(isTstatSchedConfigured()) {
+ }
+ if(settings?.schMotOperateFan) {
+ if(isFanCtrlConfigured() && fanCtrlFanSwitches) {
+ subscribe(fanCtrlFanSwitches, "switch", automationFanSwitchEvt)
+ subscribe(fanCtrlFanSwitches, "level", automationFanSwitchEvt)
+ }
+ }
+ if(settings?.schMotOperateFan || settings?.schMotRemoteSensor) {
+ subscribe(schMotTstat, "thermostatFanMode", automationTstatFanEvt)
+ }
+
+ def schedList = atomicState?.scheduleList
+ def sLbl
+ def cnt = 1
+ def swlist = []
+ def mtlist = []
+ schedList?.each { scd ->
+ sLbl = "schMot_${scd}_"
+ def restrict = atomicState?."sched${cnt}restrictions"
+ def act = settings["${sLbl}SchedActive"]
+ if(act) {
+ if(atomicState?."schedule${cnt}SwEnabled") {
+ if(restrict?.s1) {
+ for(sw in settings["${sLbl}restrictionSwitchOn"]) {
+ if(swlist?.contains(sw)) {
+ //log.trace "found $sw"
+ } else {
+ swlist.push(sw)
+ subscribe(sw, "switch", automationSwitchEvt)
+ }
+ }
+ }
+ if(restrict?.s0) {
+ for(sw in settings["${sLbl}restrictionSwitchOff"]) {
+ if(swlist?.contains(sw)) {
+ //log.trace "found $sw"
+ } else {
+ swlist.push(sw)
+ subscribe(sw, "switch", automationSwitchEvt)
+ }
+ }
+ }
+ }
+ if(atomicState?."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(atomicState?."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", automationTempSenEvt)
+ }
+ }
+ }
+ }
+ }
+ cnt += 1
+ }
+ subscribe(schMotTstat, "thermostatMode", automationTstatModeEvt)
+ subscribe(schMotTstat, "thermostatOperatingState", automationTstatOperEvt)
+ subscribe(schMotTstat, "temperature", automationTstatTempEvt)
+ subscribe(schMotTstat, "presence", automationPresenceEvt)
+ subscribe(schMotTstat, "coolingSetpoint", automationTstatCTempEvt)
+ subscribe(schMotTstat, "heatingSetpoint", automationTstatHTempEvt)
+ subscribe(schMotTstat, "safetyTempExceeded", automationSafetyTempEvt)
+ subscribe(location, "sunset", automationSunEvtHandler)
+ subscribe(location, "sunrise", automationSunEvtHandler)
+ subscribe(location, "mode", automationSTModeEvt, [filterEvents: false])
+ }
+ }
+ //watchDog Subscriptions
+ if(autoType == "watchDog") {
+ // if(isWatchdogConfigured()) {
+ def tstats = parent?.getTstats()
+ def foundTstats
+
+ if(tstats) {
+ foundTstats = tstats?.collect { dni ->
+ def d1 = parent.getThermostatDevice(dni)
+ if(d1) {
+ LogAction("Found: ${d1?.displayName} with (Id: ${dni?.key})", "debug", true)
+
+ // temperature is for DEBUG
+ subscribe(d1, "temperature", automationTstatTempEvt)
+ subscribe(d1, "safetyTempExceeded", automationSafetyTempEvt)
+ }
+ return d1
+ }
+ }
+ //Alarm status monitoring
+ 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)
+ }
+ }
+ }
+}
+
+def scheduler() {
+ def random = new Random()
+ def random_int = random.nextInt(60)
+ def random_dint = random.nextInt(9)
+
+ def autoType = getAutoType()
+ if(autoType == "schMot" && atomicState?.scheduleSchedActiveCount && atomicState?.scheduleTimersActive) {
+ LogAction("${autoType} scheduled using Cron (${random_int} ${random_dint}/5 * * * ?)", "info", true)
+ schedule("${random_int} ${random_dint}/5 * * * ?", watchDogAutomation)
+ } else {
+ LogAction("${autoType} scheduled using Cron (${random_int} ${random_dint}/30 * * * ?)", "info", true)
+ schedule("${random_int} ${random_dint}/30 * * * ?", watchDogAutomation)
+ }
+}
+
+def watchDogAutomation() {
+ LogAction("Heartbeat: watchDogAutomation()...", "trace", false)
+ def autoType = getAutoType()
+ def val = 900
+ if(autoType == "schMot") {
+ val = 220
+ }
+ if(getLastAutomationSchedSec() > val) {
+ LogAction("${autoType} Heartbeat run requested...", "trace", true)
+ runAutomationEval()
+ }
+}
+
+def scheduleAutomationEval(schedtime = 20) {
+ if(schedtime < 20) { schedtime = 20 }
+ if(getLastAutomationSchedSec() > 14) {
+ atomicState?.lastAutomationSchedDt = getDtNow()
+ runIn(schedtime, "runAutomationEval", [overwrite: true])
+ }
+}
+
+def getLastAutomationSchedSec() { return !atomicState?.lastAutomationSchedDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastAutomationSchedDt, null, "getLastAutomationSchedSec").toInteger() }
+
+def runAutomationEval() {
+ LogAction("runAutomationEval...", "trace", false)
+ def autoType = getAutoType()
+ switch(autoType) {
+ case "nMode":
+ if(isNestModesConfigured()) {
+ checkNestMode()
+ }
+ break
+ case "schMot":
+ if(isSchMotConfigured()) {
+ schMotCheck()
+ }
+ break
+ case "watchDog":
+ if(isWatchdogConfigured()) {
+ watchDogCheck()
+ }
+ break
+ default:
+ LogAction("runAutomationEval: Invalid Option Received... ${autoType}", "warn", true)
+ break
+ }
+}
+
+def getAutomationStats() {
+ return [
+ "lastUpdatedDt":atomicState?.lastUpdatedDt,
+ "lastEvalDt":atomicState?.lastEvalDt,
+ "lastEvent":atomicState?.lastEventData,
+ "lastActionData":getAutoActionData(),
+ "lastSchedDt":atomicState?.lastAutomationSchedDt,
+ "lastExecVal":atomicState?.lastExecutionTime,
+ "execAvgVal":(atomicState?.evalExecutionHistory != [] ? getAverageValue(atomicState?.evalExecutionHistory) : null)
+ ]
+}
+
+def storeLastAction(actionDesc, actionDt) {
+ if(actionDesc && actionDt) {
+ atomicState?.lastAutoActionData = ["actionDesc":actionDesc, "dt":actionDt]
+ }
+}
+
+def getAutoActionData() {
+ if(atomicState?.lastAutoActionData) {
+ return atomicState?.lastAutoActionData
+ }
+}
+
+def automationTempSenEvt(evt) {
+ LogAction("Event | Sensor Temp: ${evt?.displayName} - Temperature is (${evt?.value}°${getTemperatureScale()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationTstatTempEvt(evt) {
+ LogAction("Event | Thermostat Temp: ${evt?.displayName} - Temperature is (${evt?.value}°${getTemperatureScale()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationTstatModeEvt(evt) {
+ LogAction("Event | Thermostat Mode: ${evt?.displayName} - Mode is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def modeOff = (evt?.value == "off") ? true : false
+ if(!modeOff) { atomicState?.TstatTurnedOff = false }
+ else { atomicState?.TstatTurnedOff = true }
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationPresenceEvt(evt) {
+ LogAction("Event | Presence: ${evt?.displayName} - Presence is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationSwitchEvt(evt) {
+ LogAction("Event | Switch: ${evt?.displayName} - is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationFanSwitchEvt(evt) {
+ LogAction("Event | Fan Switch: ${evt?.displayName} - is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationTstatFanEvt(evt) {
+ LogAction("Event | Thermostat Fan: ${evt?.displayName} - Fan is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationTstatOperEvt(evt) {
+ LogAction("Event | Thermostat Operating State: ${evt?.displayName} - OperatingState is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationTstatCTempEvt(evt) {
+ LogAction("Event | Thermostat Cooling Setpoint: ${evt?.displayName} - Cooling Setpoint is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationTstatHTempEvt(evt) {
+ LogAction("Event | Thermostat Heating Setpoint: ${evt?.displayName} - Heating Setpoint is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationSTModeEvt(evt) {
+ LogAction("Event | ST Mode is (${evt?.value.toString().toUpperCase()})", "trace", false)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+ }
+}
+
+def automationSunEvtHandler(evt) {
+ LogAction("Event | ST Sunrise / Sunset is (${evt?.value.toString().toUpperCase()})", "trace", false)
+ if(atomicState?.disableAutomation) { return }
+ scheduleAutomationEval()
+ storeLastEventData(evt)
+}
+
+
+/******************************************************************************
+| WATCHDOG AUTOMATION LOGIC CODE |
+*******************************************************************************/
+def watchDogPrefix() { return "watchDog" }
+
+def watchDogPage() {
+ def pName = watchDogPrefix()
+ dynamicPage(name: "watchDogPage", title: "Nest Location Watchdog", uninstall: true, install: true) {
+ section("Notifications:") {
+ def pageDesc = getNotifConfigDesc(pName)
+ href "setNotificationPage", title: "Configured Alerts...", description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true],
+ state: (pageDesc ? "complete" : null), image: getAppImg("notification_icon.png")
+ }
+ }
+}
+
+
+def automationSafetyTempEvt(evt) {
+ LogAction("Event | Thermostat Safety Temp Exceeded: '${evt.displayName}' (${evt.value})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ if(evt?.value == "true") {
+ scheduleAutomationEval()
+ }
+ }
+ storeLastEventData(evt)
+}
+
+// Alarms will repeat every watDogRepeatMsgDelay (1 hr default) ALL thermostats
+def watchDogCheck() {
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def execTime = now()
+ atomicState?.lastEvalDt = getDtNow()
+ def tstats = parent?.getTstats()
+ def foundTstats
+ if(tstats) {
+ foundTstats = tstats?.collect { dni ->
+ def d1 = parent.getThermostatDevice(dni)
+ if(d1) {
+ def exceeded = d1?.currentValue("safetyTempExceeded")?.toString()
+ if(exceeded == "true") {
+ watchDogAlarmActions(d1.displayName, dni, "temp")
+ LogAction("watchDogCheck() | Thermostat: ${d1?.displayName} Temp Exceeded: ${exceeded}", "trace", true)
+ }
+ return d1
+ }
+ }
+ }
+ storeExecutionHistory((now()-execTime), "watchDogCheck")
+ }
+}
+
+def watchDogAlarmActions(dev, dni, actType) {
+ def pName = watchDogPrefix()
+ //def allowNotif = (settings["${pName}NotificationsOn"] && (settings["${pName}NotifRecips"] || settings["${pName}NotifPhones"] || settings["${pName}UsePush"])) ? true : false
+ def allowNotif = settings["${pName}NotificationsOn"] ? true : false
+ def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
+ def allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
+ def evtNotifMsg = ""
+ def evtVoiceMsg = ""
+ switch(actType) {
+ case "temp":
+ evtNotifMsg = "Safety Temp has been exceeded on ${dev}."
+ evtVoiceMsg = "Safety Temp has been exceeded on ${dev}."
+ break
+ }
+ if(getLastWatDogSafetyAlertDtSec(dni) > getWatDogRepeatMsgDelayVal()) {
+ LogAction("watchDogAlarmActions() | ${evtNotifMsg}", "trace", true)
+
+ if(allowNotif) {
+ sendEventPushNotifications(evtNotifMsg, "Warning", pName)
+ if(allowSpeech) {
+ sendEventVoiceNotifications(voiceNotifString(evtVoiceMsg, pName), pName, "nmWatDogEvt_${app?.id}", true, "nmWatDogEvt_${app?.id}")
+ }
+ if(allowAlarm) {
+ scheduleAlarmOn(pName)
+ }
+ } else {
+ sendNofificationMsg("Warning", evtNotifMsg)
+ }
+ atomicState?."lastWatDogSafetyAlertDt${dni}" = getDtNow()
+ }
+}
+
+def getLastWatDogSafetyAlertDtSec(dni) { return !atomicState?."lastWatDogSafetyAlertDt{$dni}" ? 10000 : GetTimeDiffSeconds(atomicState?."lastWatDogSafetyAlertDt${dni}", null, "getLastWatDogSafetyAlertDtSec").toInteger() }
+def getWatDogRepeatMsgDelayVal() { return !watDogRepeatMsgDelay ? 3600 : watDogRepeatMsgDelay.toInteger() }
+
+def isWatchdogConfigured() {
+ return (atomicState?.automationType == "watchDog") ? true : false
+}
+
+/////////////////////THERMOSTAT AUTOMATION CODE LOGIC ///////////////////////
+
+/****************************************************************************
+| REMOTE SENSOR AUTOMATION CODE |
+*****************************************************************************/
+
+def remSenPrefix() { return "remSen" }
+
+def remSenLock(val, myId) {
+ def res = false
+ if(val && myId && !parent) {
+ def lval = atomicState?."remSenLock${val}"
+ if(!lval) {
+ atomicState?."remSenLock${val}" = myId
+ res = true
+ } else if(lval == myId) { res = true }
+ }
+ return res
+}
+
+def remSenUnlock(val, myId) {
+ def res = false
+ if(val && myId && !parent) {
+ def lval = atomicState?."remSenLock${val}"
+ if(lval) {
+ if(lval == myId) {
+ atomicState?."remSenLock${val}" = null
+ res = true
+ }
+ } else { res = true }
+ }
+ return res
+}
+
+//Requirements Section
+def remSenCoolTempsReq() { return (settings?.remSenRuleType in [ "Cool", "Heat_Cool", "Cool_Circ", "Heat_Cool_Circ" ]) ? true : false }
+def remSenHeatTempsReq() { return (settings?.remSenRuleType in [ "Heat", "Heat_Cool", "Heat_Circ", "Heat_Cool_Circ" ]) ? true : false }
+def remSenDayHeatTempOk() { return (!remSenHeatTempsReq() || (remSenHeatTempsReq() && remSenDayHeatTemp)) ? true : false }
+def remSenDayCoolTempOk() { return (!remSenCoolTempsReq() || (remSenCoolTempsReq() && remSenDayCoolTemp)) ? true : false }
+
+def isRemSenConfigured() {
+ def devOk = (settings?.remSensorDay) ? true : false
+ return (devOk && settings?.remSenRuleType && remSenDayHeatTempOk() && remSenDayCoolTempOk() ) ? true : false
+}
+
+def getLastMotionActiveSec(mySched) {
+ def sLbl = "schMot_${mySched}_"
+ return !atomicState?."${sLbl}MotionActiveDt" ? 0 : GetTimeDiffSeconds(atomicState?."${sLbl}MotionActiveDt", null, "getLastMotionActiveSec").toInteger()
+}
+
+def getLastMotionInActiveSec(mySched) {
+ def sLbl = "schMot_${mySched}_"
+ return !atomicState?."${sLbl}MotionInActiveDt" ? 0 : GetTimeDiffSeconds(atomicState?."${sLbl}MotionInActiveDt", null, "getLastMotionInActiveSec").toInteger()
+}
+
+def automationMotionEvt(evt) {
+ LogAction("Event | Motion Sensor: '${evt?.displayName}' Motion is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ storeLastEventData(evt)
+ def dorunIn = false
+ def delay
+ def sLbl
+
+ def mySched = getCurrentSchedule()
+
+ def schedList = atomicState?.scheduleList
+ 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 = atomicState?."${sLbl}oldMotionActive"
+ def newActive = isMotionActive(settings["${sLbl}Motion"])
+ atomicState."${sLbl}oldMotionActive" = newActive
+ if(oldActive != newActive) {
+ if(newActive) {
+ if(cnt == mySched) { delay = settings."${sLbl}MDelayValOn"?.toInteger() ?: 60 }
+ atomicState."${sLbl}MotionActiveDt" = getDtNow()
+ } else {
+ if(cnt == mySched) { delay = settings."${sLbl}MDelayValOff"?.toInteger() ?: 30*60 }
+ atomicState."${sLbl}MotionInActiveDt" = getDtNow()
+ }
+ }
+ LogAction("Updating Schedule Motion Sensor State: [ Schedule: (${cnt}) | Previous Active: (${oldActive}) | Current Status: ({$newActive}) ]", "trace", true)
+ if(cnt == mySched) { dorunIn = true }
+ }
+ }
+ }
+
+ if(dorunIn) {
+ LogAction(" Motion: [ Scheduling for Delay: ($delay) | Schedule ${mySched} ]", "trace", true)
+ delay = delay > 20 ? delay : 20
+ delay = delay < 60 ? delay : 60
+ scheduleAutomationEval(delay)
+ } else {
+ def str = "Event | Skipping Motion Check because "
+ if(mySched) {
+ str += "motion sensor not in used in active schedule (${mySched})"
+ } else {
+ str += "no active schedule"
+ }
+ LogAction(str, "info", true)
+ }
+ }
+}
+
+def isMotionActive(sensors) {
+ def result
+ sensors?.each { sen ->
+ if(sen) {
+ def sval = sen?.currentState("motion").value
+ if(sval == "active") { result = true }
+ }
+ }
+ return result
+ //return sensors?.currentState("motion")?.value.equals("active") ? true : false
+}
+
+def getDeviceTempAvg(items) {
+ def tmpAvg = []
+ def tempVal = 0
+ if(!items) { return tempVal }
+ else if(items?.size() > 1) {
+ tmpAvg = items*.currentTemperature
+ if(tmpAvg && tmpAvg?.size() > 1) { tempVal = (tmpAvg?.sum().toDouble() / tmpAvg?.size().toDouble()).round(1) }
+ }
+ else { tempVal = getDeviceTemp(items) }
+ return tempVal.toDouble()
+}
+
+def remSenShowTempsPage() {
+ dynamicPage(name: "remSenShowTempsPage", uninstall: false) {
+ if(settings?.remSensorDay) {
+ section("Default Sensor Temps: (Schedules can override)") {
+ def cnt = 0
+ def rCnt = settings?.remSensorDay?.size()
+ def str = ""
+ str += "Sensor Temp (average): (${getDeviceTempAvg(settings?.remSensorDay)}°${getTemperatureScale()})\n│"
+ settings?.remSensorDay?.each { t ->
+ cnt = cnt+1
+ str += "${(cnt >= 1) ? "${(cnt == rCnt) ? "\n└" : "\n├"}" : "\n└"} ${t?.label}: ${(t?.label.length() > 10) ? "\n${(rCnt == 1 || cnt == rCnt) ? " " : "│"}└ " : ""}(${getDeviceTemp(t)}°${getTemperatureScale()})"
+ }
+ paragraph "${str}", state: "complete", image: getAppImg("temperature_icon.png")
+ }
+ }
+ }
+}
+
+def remSendoSetCool(chgval, onTemp, offTemp) {
+ def remSenTstat = settings?.schMotTstat
+ def remSenTstatMir = settings?.schMotTstatMir
+
+ def hvacMode = remSenTstat ? remSenTstat?.currentThermostatMode.toString() : null
+ def curCoolSetpoint = getTstatSetpoint(remSenTstat, "cool")
+ def curHeatSetpoint = getTstatSetpoint(remSenTstat, "heat")
+ def tempChangeVal = !remSenTstatTempChgVal ? 5.0 : remSenTstatTempChgVal.toDouble()
+ def maxTempChangeVal = tempChangeVal * 3
+
+ chgval = (chgval > (onTemp + maxTempChangeVal)) ? onTemp + maxTempChangeVal : chgval
+ chgval = (chgval < (offTemp - maxTempChangeVal)) ? offTemp - maxTempChangeVal : chgval
+ if(chgval != curCoolSetpoint) {
+ scheduleAutomationEval(60)
+ def cHeat = null
+ if(hvacMode in ["auto"]) {
+ if(curHeatSetpoint > (chgval-5.0)) {
+ cHeat = chgval - 5.0
+ LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to (${cHeat}°${getTemperatureScale()}) to allow COOL setting", "info", true)
+ if(remSenTstatMir) { remSenTstatMir*.setHeatingSetpoint(cHeat) }
+ }
+ }
+ if(setTstatAutoTemps(remSenTstat, chgval, cHeat)) {
+ //LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to (${chgval}°${getTemperatureScale()}) ", "info", true)
+ storeLastAction("Adjusted Cool Setpoint to (${chgval}°${getTemperatureScale()}) Heat Setpoint to (${cHeat}°${getTemperatureScale()})", getDtNow())
+ if(remSenTstatMir) { remSenTstatMir*.setCoolingSetpoint(chgval) }
+ }
+ return true // let all this take effect
+ } else {
+ LogAction("Remote Sensor: COOL - CoolSetpoint is already (${chgval}°${getTemperatureScale()}) ", "info", true)
+ }
+ return false
+}
+
+def remSendoSetHeat(chgval, onTemp, offTemp) {
+ def remSenTstat = schMotTstat
+ def remSenTstatMir = schMotTstatMir
+
+ def hvacMode = remSenTstat ? remSenTstat?.currentThermostatMode.toString() : null
+ def curCoolSetpoint = getTstatSetpoint(remSenTstat, "cool")
+ def curHeatSetpoint = getTstatSetpoint(remSenTstat, "heat")
+ def tempChangeVal = !remSenTstatTempChgVal ? 5.0 : remSenTstatTempChgVal.toDouble()
+ def maxTempChangeVal = tempChangeVal * 3
+
+ chgval = (chgval < (onTemp - maxTempChangeVal)) ? onTemp - maxTempChangeVal : chgval
+ chgval = (chgval > (offTemp + maxTempChangeVal)) ? offTemp + maxTempChangeVal : chgval
+ if(chgval != curHeatSetpoint) {
+ scheduleAutomationEval(60)
+ def cCool = null
+ if(hvacMode in ["auto"]) {
+ if(curCoolSetpoint < (chgval+5)) {
+ cCool = chgval + 5.0
+ LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to (${cCool}°${getTemperatureScale()}) to allow HEAT setting", "info", true)
+ if(remSenTstatMir) { remSenTstatMir*.setCoolingSetpoint(cCool) }
+ }
+ }
+ if(setTstatAutoTemps(remSenTstat, cCool, chgval)) {
+ //LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to (${chgval}°${getTemperatureScale()})", "info", true)
+ storeLastAction("Adjusted Heat Setpoint to (${chgval}°${getTemperatureScale()}) Cool Setpoint to (${cCool}°${getTemperatureScale()})", getDtNow())
+ if(remSenTstatMir) { remSenTstatMir*.setHeatingSetpoint(chgval) }
+ }
+ return true // let all this take effect
+ } else {
+ LogAction("Remote Sensor: HEAT - HeatSetpoint is already (${chgval}°${getTemperatureScale()})", "info", true)
+ }
+ return false
+}
+
+private remSenCheck() {
+ LogAction("remSenCheck.....", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ try {
+ def remSenTstat = settings?.schMotTstat
+ def remSenTstatMir = settings?.schMotTstatMir
+
+ def execTime = now()
+ //atomicState?.lastEvalDt = getDtNow()
+
+ def home = false
+ def away = false
+ if(remSenTstat && getTstatPresence(remSenTstat) == "present") { home = true }
+ else { away = true }
+
+ def noGoDesc = ""
+ if( !settings?.remSensorDay || !remSenTstat || !home) {
+ noGoDesc += !settings?.remSensorDay ? "Missing Required Sensor Selections..." : ""
+ noGoDesc += !remSenTstat ? "Missing Required Thermostat device" : ""
+ noGoDesc += !home ? "Ignoring because thermostat is in away mode." : ""
+ LogAction("Remote Sensor NOT Evaluating...Evaluation Status: ${noGoDesc}", "warn", true)
+ } else if(home) {
+ //log.info "remSenCheck: Evaluating Event..."
+
+ def hvacMode = remSenTstat ? remSenTstat?.currentThermostatMode.toString() : null
+ if(hvacMode == "off") {
+ LogAction("Remote Sensor: Skipping Evaluation... The Current Thermostat Mode is 'OFF'...", "info", true)
+ disableOverrideTemps()
+ storeExecutionHistory((now() - execTime), "remSenCheck")
+ return
+ }
+
+ def reqSenHeatSetPoint = getRemSenHeatSetTemp()
+ def reqSenCoolSetPoint = getRemSenCoolSetTemp()
+
+ 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 Requested Setpoints with auto mode: (${reqSenCoolSetPoint})/(${reqSenHeatSetPoint})...", "warn", true)
+ storeExecutionHistory((now() - execTime), "remSenCheck")
+ return
+ }
+ }
+
+ def threshold = !remSenTempDiffDegrees ? 2.0 : remSenTempDiffDegrees.toDouble()
+ def tempChangeVal = !remSenTstatTempChgVal ? 5.0 : remSenTstatTempChgVal.toDouble()
+ def maxTempChangeVal = tempChangeVal * 3
+ def curTstatTemp = getDeviceTemp(remSenTstat).toDouble()
+ def curSenTemp = (settings?.remSensorDay) ? getRemoteSenTemp().toDouble() : null
+
+ 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: Remote Sensor Rule Type: ${getEnumValue(remSenRuleEnum("heatcool"), settings?.remSenRuleType)}", "info", false)
+ LogAction("remSenCheck: Remote 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 modeOk = true
+ if(!modeOk || !getRemSenModeOk()) {
+ noGoDesc = ""
+ noGoDesc += (!modeOk && getRemSenModeOk()) ? "Mode Filters were set and the current mode was not selected for Evaluation" : ""
+ noGoDesc += (!getRemSenModeOk() && modeOk) ? "This mode is not one of those selected for evaluation..." : ""
+
+// if we have heat on, ac on, or fan on, turn them off once
+
+ if(atomicState?.haveRun) {
+ if(settings?.remSenRuleType in ["Cool", "Heat_Cool", "Heat_Cool_Circ"]
+ && atomicState?.remSenCoolOn != null && !atomicState.remSenCoolOn
+ && (hvacMode in ["cool","auto"])
+ && acRunning) {
+ def onTemp = reqSenCoolSetPoint + threshold
+ def offTemp = reqSenCoolSetPoint
+ chgval = curTstatTemp + tempChangeVal
+ if(remSendoSetCool(chgval, onTemp, offTemp)) {
+ noGoDesc += " Turning off COOL due to mode change"
+ }
+ atomicState?.remSenCoolOn = false
+ }
+
+ if(settings?.remSenRuleType in ["Heat", "Heat_Cool", "Heat_Cool_Circ"]
+ && atomicState?.remSenHeatOn != null && !atomicState.remSenHeatOn
+ && (hvacMode in ["heat", "emergency heat", "auto"])
+ && heatRunning) {
+ def onTemp = reqSenHeatSetPoint - threshold
+ def offTemp = reqSenHeatSetPoint
+ chgval = curTstatTemp - tempChangeVal
+ if(remSendoSetHeat(chgval, onTemp, offTemp)) {
+ noGoDesc += " Turning off HEAT due to mode change"
+ }
+ atomicState?.remSenHeatOn = false
+ }
+
+ atomicState.haveRun = false
+ }
+ LogAction("Remote Sensor: Skipping Evaluation...Remote Sensor Evaluation Status: ${noGoDesc}", "info", true)
+ storeExecutionHistory((now() - execTime), "remSenCheck")
+ return
+ }
+
+ atomicState.haveRun = true
+
+ 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
+
+ LogAction("Remote Sensor: COOL - (Sensor Temp: ${curSenTemp} - CoolSetpoint: ${reqSenCoolSetPoint})", "trace", true)
+ 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", true)
+ acRunning = false
+ atomicState?.remSenCoolOn = false
+ } else if(turnOn && !acRunning) {
+ chgval = curTstatTemp - tempChangeVal
+ chg = true
+ acRunning = true
+ atomicState.remSenCoolOn = true
+ LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to Turn On Thermostat", "info", true)
+ } else {
+ // logic to decide if we need to nudge thermostat to keep it on or off
+ if(acRunning) {
+ chgval = curTstatTemp - tempChangeVal
+ atomicState.remSenCoolOn = true
+ } else {
+ chgval = curTstatTemp + tempChangeVal
+ atomicState?.remSenCoolOn = false
+ }
+ def coolDiff1 = Math.abs(curTstatTemp - curCoolSetpoint)
+ LogAction("Remote Sensor: COOL - coolDiff1: ${coolDiff1} tempChangeVal: ${tempChangeVal}", "trace", false)
+ if(coolDiff1 < (tempChangeVal / 2)) {
+ chg = true
+ LogAction("Remote Sensor: COOL - Adjusting CoolSetpoint to maintain state", "info", true)
+ }
+ }
+ 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}°${getTemperatureScale()}) ", "info", true)
+ }
+ }
+ }
+
+ 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})", "trace", 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", true)
+ heatRunning = false
+ atomicState.remSenHeatOn = false
+ } else if(turnOn && !heatRunning) {
+ chgval = curTstatTemp + tempChangeVal
+ chg = true
+ LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to Turn On Thermostat", "info", true)
+ atomicState.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
+ atomicState.remSenHeatOn = true
+ } else {
+ chgval = curTstatTemp - tempChangeVal
+ atomicState.remSenHeatOn = false
+ }
+ def heatDiff1 = Math.abs(curTstatTemp - curHeatSetpoint)
+ LogAction("Remote Sensor: HEAT - heatDiff1: ${heatDiff1} tempChangeVal: ${tempChangeVal}", "trace", false)
+ if(heatDiff1 < (tempChangeVal / 2)) {
+ chg = true
+ LogAction("Remote Sensor: HEAT - Adjusting HeatSetpoint to maintain state", "info", true)
+ }
+ }
+ 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}°${getTemperatureScale()})", "info", true)
+ }
+ }
+ }
+ }
+ else {
+ //
+ // if all thermostats (primary and mirrors) are Nest, then AC/HEAT & fan will be off (or set back) with away mode.
+ // 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 react to home/away changes
+ //
+ LogAction("Remote Sensor: Skipping Evaluation... Thermostat is set to away...", "info", true)
+ }
+ storeExecutionHistory((now() - execTime), "remSenCheck")
+ } catch (ex) {
+ log.error "remSenCheck Exception:", ex
+ 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 info = []
+ sensors?.sort().each {
+ info.push("${it?.displayName}": " ${it?.currentTemperature.toString()}°${getTemperatureScale()}")
+ }
+ return info
+ }
+}
+
+def getRemSenModeOk() {
+ def result = false
+ if(settings?.remSensorDay ) { result = true }
+ //log.debug "getRemSenModeOk: $result"
+ return result
+}
+
+def getDeviceTemp(dev) {
+ return dev ? dev?.currentValue("temperature")?.toString().replaceAll("\\[|\\]", "").toDouble() : 0
+}
+
+def getTstatSetpoint(tstat, type) {
+ if(tstat) {
+ if(type == "cool") {
+ def coolSp = tstat?.currentCoolingSetpoint
+ return coolSp ? coolSp.toDouble() : 0
+ } else {
+ def heatSp = tstat?.currentHeatingSetpoint
+ return heatSp ? heatSp.toDouble() : 0
+ }
+ }
+ else { return 0 }
+}
+
+def getRemoteSenTemp() {
+ def mySched = getCurrentSchedule()
+ if(!atomicState.remoteTempSourceStr) { atomicState.remoteTempSourceStr = null }
+ if(!atomicState.currentSchedNum) { atomicState.currentSchedNum = null }
+ def sens
+ if(mySched) {
+ def sLbl = "schMot_${mySched}_"
+ if(settings["${sLbl}remSensor"]) {
+ atomicState.remoteTempSourceStr = "Schedule"
+ atomicState.currentSchedNum = mySched
+ sens = settings["${sLbl}remSensor"]
+ return getDeviceTempAvg(sens).toDouble()
+ }
+ }
+ if(settings?.remSensorDay) {
+ atomicState.remoteTempSourceStr = "Remote Sensor"
+ atomicState.currentSchedNum = null
+ return getDeviceTempAvg(settings?.remSensorDay).toDouble()
+ }
+ else {
+ atomicState.remoteTempSourceStr = "Thermostat"
+ atomicState.currentSchedNum = null
+ return getDeviceTemp(schMotTstat).toDouble()
+/*
+ else {
+ log.warn "getRemoteSenTemp: No Temperature Found!!!"
+ return 0.0
+*/
+ }
+}
+
+def getRemSenCoolSetTemp() {
+ if(getLastOverrideCoolSec() < (3600 * 4)) {
+ if(atomicState?.coolOverride != null) {
+ return atomicState?.coolOverride.toDouble()
+ }
+ } else { atomicState?.coolOverride = null }
+
+ def mySched = getCurrentSchedule()
+ def coolTemp
+ if(mySched) {
+ def useMotion = atomicState?."motion${mySched}UseMotionSettings"
+ def hvacSettings = atomicState?."sched${mySched}restrictions"
+ coolTemp = !useMotion ? hvacSettings?.ctemp : hvacSettings?.mctemp ?: hvacSettings?.ctemp
+ }
+ if (coolTemp) {
+ return coolTemp.toDouble()
+ } else if(remSenDayCoolTemp) {
+ return remSenDayCoolTemp.toDouble()
+ }
+ else {
+ def desiredCoolTemp = getGlobalDesiredCoolTemp()
+ if(desiredCoolTemp) { return desiredCoolTemp.toDouble() }
+ else { return schMotTstat ? getTstatSetpoint(schMotTstat, "cool") : 0 }
+ }
+}
+
+def getRemSenHeatSetTemp() {
+ if(getLastOverrideHeatSec() < (3600 * 4)) {
+ if(atomicState?.heatOverride != null) {
+ return atomicState.heatOverride.toDouble()
+ }
+ } else { atomicState?.heatOverride = null }
+
+ def mySched = getCurrentSchedule()
+ def heatTemp
+ if(mySched) {
+ def useMotion = atomicState?."motion${mySched}UseMotionSettings"
+ def hvacSettings = atomicState?."sched${mySched}restrictions"
+ heatTemp = !useMotion ? hvacSettings?.htemp : hvacSettings?.mhtemp ?: hvacSettings?.htemp
+ }
+ if (heatTemp) {
+ return heatTemp.toDouble()
+ } else if(remSenDayHeatTemp) {
+ return remSenDayHeatTemp.toDouble()
+ }
+ else {
+ def desiredHeatTemp = getGlobalDesiredHeatTemp()
+ if(desiredHeatTemp) { return desiredHeatTemp.toDouble() }
+ else { return schMotTstat ? getTstatSetpoint(schMotTstat, "heat") : 0 }
+ }
+}
+
+def getRemoteSenAutomationEnabled() {
+ return atomicState?.disableAutomation ? false : true
+}
+
+// TODO When a temp change is sent to virtual device, it lasts for 4 hours, or next turn off, 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, or have next schedule change clear override
+
+def getLastOverrideCoolSec() { return !atomicState?.lastOverrideCoolDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastOverrideCoolDt, null, "getLastOverrideCoolSec").toInteger() }
+def getLastOverrideHeatSec() { return !atomicState?.lastOverrideHeatDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastOverrideHeatDt, null, "getLastOverrideHeatSec").toInteger() }
+
+def disableOverrideTemps() {
+ if(atomicState?.heatOverride || atomicState?.coolOverride) {
+ atomicState?.coolOverride = null
+ atomicState?.heatOverride = null
+ atomicState?.lastOverrideCoolDt = null
+ atomicState?.lastOverrideHeatDt = null
+ LogAction("disableOverrideTemps: Disabling Override temps", "trace", true)
+ }
+}
+
+def remSenTempUpdate(temp, mode) {
+ LogAction("remSenTempUpdate(${temp}, ${mode})", "trace", true)
+
+ def res = false
+ if(atomicState?.disableAutomation) { return res }
+ switch(mode) {
+ case "heat":
+ if(remSenHeatTempsReq()) {
+ LogAction("remSenTempUpdate Set Heat Override to: ${temp} for 4 hours", "trace", true)
+ atomicState?.heatOverride = temp.toDouble()
+ atomicState?.lastOverrideHeatDt = getDtNow()
+ scheduleAutomationEval()
+ res = true
+ }
+ break
+ case "cool":
+ if(remSenCoolTempsReq()) {
+ LogAction("remSenTempUpdate Set Cool Override to: ${temp} for 4 hours", "trace", true)
+ atomicState?.coolOverride = temp.toDouble()
+ atomicState?.lastOverrideCoolDt = getDtNow()
+ scheduleAutomationEval()
+ res = true
+ }
+ break
+ default:
+ LogAction("remSenTempUpdate Received an Invalid Request: ${mode}", "warn", true)
+ break
+ }
+ return res
+}
+
+def remSenRuleEnum(type=null) {
+ // Determines that available rules to display based on the selected thermostats capabilites.
+ def canCool = atomicState?.schMotTstatCanCool ? true : false
+ def canHeat = atomicState?.schMotTstatCanHeat ? true : false
+ def hasFan = atomicState?.schMotTstatHasFan ? true : false
+
+ //log.debug "remSenRuleEnum -- hasFan: $hasFan (${atomicState?.schMotTstatHasFan} | canCool: $canCool (${atomicState?.schMotTstatCanCool} | canHeat: $canHeat (${atomicState?.schMotTstatCanHeat}"
+ def vals = []
+ if (type) {
+ if (type == "fan") {
+ if (canCool && !canHeat && hasFan) { vals = ["Circ":"Circulate(Fan)", "Cool_Circ":"Cool/Circulate(Fan)"] }
+ else if (!canCool && canHeat && hasFan) { vals = ["Circ":"Circulate(Fan)", "Heat_Circ":"Heat/Circulate(Fan)"] }
+ else if (!canCool && !canHeat && hasFan) { vals = ["Circ":"Circulate(Fan)"] }
+ else { vals = [ "Circ":"Circulate(Fan)", "Heat_Cool_Circ":"Auto/Circulate(Fan)", "Heat_Circ":"Heat/Circulate(Fan)", "Cool_Circ":"Cool/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 Received ($type)", "error", true) }
+ }
+ else {
+ if (canCool && !canHeat && hasFan) { vals = ["Cool":"Cool", "Circ":"Circulate(Fan)", "Cool_Circ":"Cool/Circulate(Fan)"] }
+ else if (canCool && !canHeat && !hasFan) { vals = ["Cool":"Cool"] }
+ else if (!canCool && canHeat && hasFan) { vals = ["Circ":"Circulate(Fan)", "Heat":"Heat", "Heat_Circ":"Heat/Circulate(Fan)"] }
+ else if (!canCool && canHeat && !hasFan) { vals = ["Heat":"Heat"] }
+ else if (!canCool && !canHeat && hasFan) { vals = ["Circ":"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":"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 |
+*************************************************************************/
+
+def fanCtrlPrefix() { return "fanCtrl" }
+
+def isFanCtrlConfigured() {
+ return ( (settings?.fanCtrlFanSwitches && settings?.fanCtrlFanSwitchTriggerType && settings?.fanCtrlFanSwitchHvacModeFilter) || (isFanCircConfigured())) ? true : false
+}
+
+def isFanCircConfigured() {
+ return (settings?.schMotCirculateTstatFan && settings?.schMotFanRuleType) ? true : false
+}
+
+def getFanSwitchDesc(showOpt = true) {
+ def swDesc = ""
+ def swCnt = 0
+ def 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:" : ""
+ def rmSwCnt = settings?."${pName}FanSwitches"?.size() ?: 0
+ settings?."${pName}FanSwitches"?.each { sw ->
+ swCnt = swCnt+1
+ swDesc += "${swCnt >= 1 ? "${swCnt == rmSwCnt ? "\n └" : "\n ├"}" : "\n └"} ${sw?.label}: (${sw?.currentSwitch?.toString().capitalize()})"
+ swDesc += checkFanSpeedSupport(sw) ? "\n └ Current Spd: (${sw?.currentValue("currentState").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: (${getEnumValue(switchRunEnum(), settings?."${pName}FanSwitchTriggerType")})" : ""
+ swDesc += (settings?."${pName}FanSwitchHvacModeFilter") ? "\n • Hvac Mode Filter: (${getEnumValue(fanModeTrigEnum(), settings?."${pName}FanSwitchHvacModeFilter")})" : ""
+ }
+ }
+
+ swDesc += (settings?.schMotCirculateTstatFan) ? "\n • Fan Circulation Enabled" : ""
+ swDesc += (settings?.schMotCirculateTstatFan) ? "\n • Fan Circulation Rule: (${getEnumValue(remSenRuleEnum("fan"), settings?.schMotCirculateTstatFan)})" : ""
+
+ return (swDesc == "") ? null : "${swDesc}"
+}
+
+def getSchFanSwitchDesc(devs, showOpt = false) {
+ def swDesc = ""
+ def swCnt = 0
+ def pName = fanCtrlPrefix()
+ if(showOpt) {
+ swDesc += (devs /*&& (settings?."${pName}FanSwitchSpeedCtrl" || settings?."${pName}FanSwitchTriggerType" || settings?."${pName}FanSwitchHvacModeFilter")*/) ? "Fan Switch Config:" : ""
+ }
+ swDesc += devs ? "${showOpt ? "\n" : ""}• Fan Switches:" : ""
+ def rmSwCnt = devs?.size() ?: 0
+ devs?.each { sw ->
+ swCnt = swCnt+1
+ swDesc += "${swCnt >= 1 ? "${swCnt == rmSwCnt ? "\n └" : "\n ├"}" : "\n └"} ${sw?.label}: (${sw?.currentSwitch?.toString().capitalize()})"
+ swDesc += "${checkFanSpeedSupport(sw) ? "\n └ Fan Speed (${sw?.currentValue("currentState").toString()})" : ""}"
+ }
+ if(showOpt) {
+ if (devs) {
+ //swDesc += (settings?."${pName}FanSwitchSpeedCtrl" || settings?."${pName}FanSwitchTriggerType" || settings?."${pName}FanSwitchHvacModeFilter") ? "\n\nFan Triggers:" : ""
+ //swDesc += (settings?."${pName}FanSwitchSpeedCtrl") ? "\n • 3-Speed Fan Support: (Active)" : ""
+ //swDesc += (settings?."${pName}FanSwitchTriggerType") ? "\n • Fan Trigger: (${getEnumValue(switchRunEnum(), settings?."${pName}FanSwitchTriggerType")})" : ""
+ //swDesc += (settings?."${pName}FanSwitchHvacModeFilter") ? "\n • Hvac Mode Filter: (${getEnumValue(fanModeTrigEnum(), settings?."${pName}FanSwitchHvacModeFilter")})" : ""
+ }
+ }
+ return (swDesc == "") ? null : "${swDesc}"
+}
+
+def getFanSwitchesSpdChk() {
+ def devCnt = 0
+ def pName = fanCtrlPrefix()
+ if(settings?."${pName}FanSwitches") {
+ settings?."${pName}FanSwitches"?.each { sw ->
+ if(checkFanSpeedSupport(sw)) { devCnt = devCnt+1 }
+ }
+ }
+ return (devCnt >= 1) ? true : false
+}
+
+def fanCtrlCheck() {
+ //LogAction("FanControl Event | Fan Switch Check", "trace", false)
+ try {
+ def fanCtrlTstat = schMotTstat
+
+ if(atomicState?.disableAutomation) { return }
+ if(!fanCtrlFanSwitches) { return }
+
+ def execTime = now()
+ //atomicState?.lastEvalDt = getDtNow()
+
+ def reqHeatSetPoint = getRemSenHeatSetTemp()
+ def reqCoolSetPoint = getRemSenCoolSetTemp()
+
+ //def curTstatTemp = getDeviceTemp(fanCtrlTstat).toDouble()
+
+ def curTstatTemp = getRemoteSenTemp().toDouble()
+
+ def curSetPoint = getReqSetpointTemp(curTstatTemp, reqHeatSetPoint, reqCoolSetPoint).req.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)
+
+ doFanOperation(tempDiff)
+
+ if(schMotCirculateTstatFan) {
+ def threshold = !remSenTempDiffDegrees ? 2.0 : remSenTempDiffDegrees.toDouble()
+ def curTstatFanMode = schMotTstat?.currentThermostatFanMode.toString()
+ def fanOn = (curTstatFanMode == "on" || curTstatFanMode == "circulate") ? true : false
+ def hvacMode = schMotTstat ? schMotTstat?.currentThermostatMode.toString() : null
+/*
+ if(atomicState?.haveRunFan) {
+ if(schMotFanRuleType in ["Circ", "Cool_Circ", "Heat_Circ", "Heat_Cool_Circ"]) {
+ if(fanOn) {
+ LogAction("fantCtrlCheck: Turning OFF '${schMotTstat?.displayName}' Fan as modes do not match evaluation", "info", true)
+ storeLastAction("Turned ${schMotTstat} Fan to (Auto)", getDtNow())
+ schMotTstat?.fanAuto()
+ if(schMotTstatMir) { schMotTstatMir*.fanAuto() }
+ }
+ }
+ atomicState.haveRunFan = false
+ }
+*/
+ if( (hvacMode in ["cool"] && schMotFanRuleType in ["Cool_Circ"]) ||
+ (hvacMode in ["heat"] && schMotFanRuleType in ["Heat_Circ"]) ||
+ (hvacMode in ["auto"] && schMotFanRuleType in ["Heat_Cool_Circ"]) ||
+ (hvacMode in ["off"] && schMotFanRuleType in ["Circ"]) ) {
+
+ def sTemp = getReqSetpointTemp(curTstatTemp, reqHeatSetPoint, reqCoolSetPoint)
+ circulateFanControl(sTemp?.type?.toString(), curTstatTemp, sTemp?.req?.toDouble(), threshold, fanOn)
+ }
+ }
+ storeExecutionHistory((now()-execTime), "fanCtrlCheck")
+
+ } catch (ex) {
+ log.error "fanCtrlCheck Exception:", ex
+ parent?.sendExceptionData(ex, "fanCtrlCheck", true, getAutoType())
+ }
+}
+
+// similar to getFanAutoModeTemp
+def getReqSetpointTemp(curTemp, reqHeatSetPoint, reqCoolSetPoint) {
+ LogAction("getReqSetpointTemp: Current Temp: ${curTemp} Req Heat: ${reqHeatSetPoint} Req Cool: ${reqCoolSetPoint}", "info", false)
+ def tstat = schMotTstat
+
+ def hvacMode = tstat ? tstat?.currentThermostatMode.toString() : null
+ def operState = tstat ? tstat?.currentThermostatOperatingState.toString() : null
+ def opType = hvacMode.toString()
+
+ if((hvacMode == "cool") || (operState == "cooling")) {
+ opType = "cool"
+ } else if((hvacMode == "heat") || (operState == "heating")) {
+ opType = "heat"
+ } else if(hvacMode == "auto") {
+ 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]
+ //return temp
+}
+
+def doFanOperation(tempDiff) {
+ def pName = fanCtrlPrefix()
+ LogAction("doFanOperation: Temp Difference: (${tempDiff})", "info", false)
+ try {
+ def tstat = 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)
+
+ def hvacFanOn = false
+ //1:"Heating/Cooling", 2:"With Fan Only"
+
+ if( settings?."${pName}FanSwitchTriggerType".toInteger() == 1) {
+ hvacFanOn = (curTstatOperState in ["heating", "cooling"]) ? true : false
+ }
+ if( settings?."${pName}FanSwitchTriggerType".toInteger() == 2) {
+ hvacFanOn = (curTstatFanMode in ["on", "circulate"]) ? true : false
+ }
+ if(settings?."${pName}FanSwitchHvacModeFilter" != "any" && (settings?."${pName}FanSwitchHvacModeFilter" != hvacMode)) {
+ LogAction("doFanOperation: Evaluating turn fans off Because Thermostat Mode does not Match the required Mode to Run Fans", "info", true)
+ hvacFanOn = false // force off of fans
+ }
+ if(atomicState?.haveRunFan == null) { atomicState.haveRunFan = false }
+ def savedHaveRun = atomicState.haveRunFan
+
+ settings?."${pName}FanSwitches"?.each { sw ->
+ def swOn = (sw?.currentSwitch.toString() == "on") ? true : false
+ if(hvacFanOn) {
+ if(!swOn && !savedHaveRun) {
+ LogAction("doFanOperation: Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (ON)", "info", true)
+ sw.on()
+ swOn = true
+ atomicState.haveRunFan = true
+ storeLastAction("Turned On $sw)", getDtNow())
+ } else {
+ if(!swOn && savedHaveRun) {
+ LogAction("doFanOperation: Saved have run state shows switch ${sw} turned OFF outside of automation requests", "info", true)
+ }
+ }
+ if(swOn && atomicState?.haveRunFan && checkFanSpeedSupport(sw)) {
+ def speed = sw?.currentValue("currentState") ?: null
+ if(settings?."${pName}FanSwitchSpeedCtrl" && settings?."${pName}FanSwitchHighSpeed" && settings?."${pName}FanSwitchMedSpeed" && settings?."${pName}FanSwitchLowSpeed") {
+ if(tempDiff < settings?."${pName}FanSwitchMedSpeed".toDouble()) {
+ if(speed != "LOW") {
+ sw.lowSpeed()
+ LogAction("doFanOperation: Temp Difference (${tempDiff}°${getTemperatureScale()}) is BELOW the Medium Speed Threshold of (${settings?."${pName}FanSwitchMedSpeed"}) | Turning '${sw}' Fan Switch on (LOW SPEED)", "info", true)
+ storeLastAction("Set Fan $sw to Low Speed", getDtNow())
+ }
+ }
+ else if(tempDiff >= settings?."${pName}FanSwitchMedSpeed".toDouble() && tempDiff < settings?."${pName}FanSwitchHighSpeed".toDouble()) {
+ if(speed != "MED") {
+ sw.medSpeed()
+ LogAction("doFanOperation: Temp Difference (${tempDiff}°${getTemperatureScale()}) is ABOVE the Medium Speed Threshold of (${settings?."${pName}FanSwitchMedSpeed"}) | Turning '${sw}' Fan Switch on (MEDIUM SPEED)", "info", true)
+ storeLastAction("Set Fan $sw to Medium Speed", getDtNow())
+ }
+ }
+ else if(tempDiff >= settings?."${pName}FanSwitchHighSpeed".toDouble()) {
+ if(speed != "HIGH") {
+ sw.highSpeed()
+ LogAction("doFanOperation: Temp Difference (${tempDiff}°${getTemperatureScale()}) is ABOVE the High Speed Threshold of (${settings?."${pName}FanSwitchHighSpeed"}) | Turning '${sw}' Fan Switch on (HIGH SPEED)", "info", true)
+ storeLastAction("Set Fan $sw to High Speed", getDtNow())
+ }
+ }
+ } else {
+ if(speed != "HIGH") {
+ sw.highSpeed()
+ LogAction("doFanOperation: Fan supports multiple speeds, with speed control disabled | Turning '${sw}' Fan Switch on (HIGH SPEED)", "info", true)
+ storeLastAction("Set Fan $sw to High Speed", getDtNow())
+ }
+ }
+ }
+ } else {
+ if(swOn && savedHaveRun) {
+ LogAction("doFanOperation: Fan Switch (${sw?.displayName}) is (${swOn ? "ON" : "OFF"}) | Turning '${sw}' Switch (OFF)", "info", true)
+ storeLastAction("Turned Off (${sw})", getDtNow())
+ sw.off()
+ atomicState.haveRunFan = false
+ } else {
+ if(swOn && !savedHaveRun) {
+ LogAction("doFanOperation: Saved have run state shows switch ${sw} turned ON outside of automation requests", "info", true)
+ }
+ }
+ }
+ }
+ } catch (ex) {
+ log.error "doFanOperation Exception:", ex
+ parent?.sendExceptionData(ex, "doFanOperation", true, getAutoType())
+ }
+}
+
+def getLastRemSenFanRunDtSec() { return !atomicState?.lastRemSenFanRunDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastRemSenFanRunDt, null, "getLastRemSenFanRunDtSec").toInteger() }
+def getLastRemSenFanOffDtSec() { return !atomicState?.lastRemSenFanOffDt ? 100000 : GetTimeDiffSeconds(atomicState?.lastRemSenFanOffDt, null, "getLastRemSenFanOffDtSec").toInteger() }
+
+
+// CONTROLS THE THERMOSTAT FAN
+def circulateFanControl(operType, Double curSenTemp, Double reqSetpointTemp, Double threshold, Boolean fanOn) {
+ def tstat = schMotTstat
+ def tstatsMir = schMotTstatMir
+
+ def hvacMode = tstat ? tstat?.currentThermostatMode.toString() : null
+ def home = false
+ def away = false
+ if(tstat && getTstatPresence(tstat) == "present") { home = true }
+ else { away = true }
+
+ if(home && hvacMode in ["heat", "auto", "cool"]) {
+ def curOperState = tstat?.currentThermostatOperatingState.toString()
+ def curFanMode = tstat?.currentThermostatFanMode.toString()
+
+ def returnToAuto = false
+ def tstatOperStateOk = (curOperState == "idle") ? true : false
+ // if ac or heat is on, we should put fan back to auto
+ if(!tstatOperStateOk) {
+ LogAction("Circulate Fan Run: The Thermostat OperatingState is Currently (${curOperState?.toString().toUpperCase()})... Skipping!!!", "info", true)
+
+ if( atomicState?.lastRemSenFanOffDt > atomicState?.lastRemSenFanRunDt) { return }
+ returnToAuto = true
+ }
+
+ def fanTempOk = getCirculateFanTempOk(curSenTemp, reqSetpointTemp, threshold, fanOn, operType)
+ if(fanTempOk && !fanOn && !returnToAuto) {
+ def waitTimeVal = remSenTimeBetweenRuns?.toInteger() ?: 3600
+ def timeSinceLastOffOk = (getLastRemSenFanOffDtSec() > waitTimeVal) ? true : false
+ if(!timeSinceLastOffOk) {
+ def remaining = waitTimeVal - getLastRemSenFanOffDtSec()
+ LogAction("Circulate Fan: Want to RUN Fan | Delaying for wait period ${waitTimeVal}, remaining ${remaining} seconds", "info", true)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ return
+ }
+ LogAction("Circulate Fan: Activating '${tstat?.displayName}'' Fan for ${operType.toString().toUpperCase()}ING Circulation...", "debug", true)
+ tstat?.fanOn()
+ storeLastAction("Turned ${tstat} Fan 'On'", getDtNow())
+ if(tstatsMir) {
+ tstatsMir?.each { mt ->
+ LogAction("Circulate Fan: Mirroring Primary Thermostat: Activating '${mt?.displayName}' Fan for ${operType.toString().toUpperCase()}ING Circulation", "debug", true)
+ mt?.fanOn()
+ }
+ }
+ atomicState?.lastRemSenFanRunDt = getDtNow()
+
+ } else {
+ if(fanOn && (returnToAuto || !fanTempOk)) {
+ if(!returnToAuto) {
+ def timeSinceLastRunOk = (getLastRemSenFanRunDtSec() > 600) ? true : false
+ if(!timeSinceLastRunOk) {
+ def remaining = 600 - getLastRemSenFanRunDtSec()
+ LogAction("Circulate Fan Run: Want to STOP Fan Delaying for wait period 600, remaining ${remaining} seconds", "info", true)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ return
+ }
+ }
+ LogAction("Circulate Fan: Turning OFF '${remSenTstat?.displayName}' Fan that was used for ${operType.toString().toUpperCase()}ING Circulation", "info", true)
+ tstat?.fanAuto()
+ storeLastAction("Turned ${tstat} Fan to 'Auto'", getDtNow())
+ if(tstatsMir) {
+ tstatsMir?.each { mt ->
+ LogAction("Circulate Fan: Mirroring Primary Thermostat: Turning OFF '${mt?.displayName}' Fan that was used for ${operType.toString().toUpperCase()}ING Circulation", "info", true)
+ mt?.fanAuto()
+ }
+ }
+ atomicState?.lastRemSenFanOffDt = getDtNow()
+ }
+ }
+ }
+}
+
+def getCirculateFanTempOk(Double senTemp, Double reqsetTemp, Double threshold, Boolean fanOn, operType) {
+ LogAction("RemSenFanTempOk Debug:", "debug", false)
+
+ def turnOn = false
+ def adjust = (getTemperatureScale() == "C") ? 0.5 : 1.0
+ if(threshold > (adjust * 2.0)) {
+ adjust = adjust * 2.0
+ }
+
+ if(adjust >= threshold) {
+ LogAction("Circulate Fan Temp: Bad threshold setting ${threshold} <= ${adjust}", "warn", true)
+ return false
+ }
+
+ LogAction(" ├ adjust: ${adjust}}°${getTemperatureScale()}", "debug", false)
+ LogAction(" ├ operType: (${operType.toString().toUpperCase()})", "debug", false)
+ LogAction(" ├ Sensor Temp: ${senTemp}°${getTemperatureScale()} | Requested Setpoint Temp: ${reqsetTemp}°${getTemperatureScale()}", "debug", false)
+
+ def ontemp
+ def offtemp
+
+ if(operType == "cool") {
+ ontemp = reqsetTemp + threshold
+ offtemp = reqsetTemp
+ if((senTemp > offtemp) && (senTemp <= (ontemp - adjust))) { turnOn = true }
+ }
+ if(operType == "heat") {
+ ontemp = reqsetTemp - threshold
+ offtemp = reqsetTemp
+ if((senTemp < offtemp) && (senTemp >= (ontemp + adjust))) { turnOn = true }
+ }
+
+ LogAction(" ├ onTemp: ${ontemp} | offTemp: ${offtemp}}°${getTemperatureScale()}", "debug", false)
+ LogAction(" ├ FanAlreadyOn: (${fanOn.toString().toUpperCase()})", "debug", false)
+ LogAction(" ┌ Final Result: (${turnOn.toString().toUpperCase()})", "debug", false)
+ LogAction("getCirculateFanTempOk: ", "debug", false)
+
+ if(!turnOn && fanOn) {
+ LogAction("getCirculateFanTempOk: The Temperature Difference is Outside of Threshold Limits | Turning Thermostat Fan OFF", "info", true)
+ }
+ if(turnOn && !fanOn) {
+ LogAction("getCirculateFanTempOk: The Temperature Difference is within the Threshold Limit | Turning Thermostat Fan ON", "info", true)
+ }
+
+ return turnOn
+}
+
+
+/********************************************************************************
+| EXTERNAL TEMP AUTOMATION CODE |
+*********************************************************************************/
+def extTmpPrefix() { return "extTmp" }
+
+def isExtTmpConfigured() {
+ return ((settings?.extTmpUseWeather || settings?.extTmpTempSensor) && settings?.extTmpDiffVal) ? true : false
+}
+
+def getExtConditions( doEvent = false ) {
+ //log.trace "getExtConditions..."
+ if(atomicState?.NeedwUpd && parent?.getWeatherDeviceInst()) {
+ def cur = parent?.getWData()
+ def weather = parent.getWeatherDevice()
+
+ atomicState?.curWeather = cur?.current_observation
+ atomicState?.curWeatherTemp_f = Math.round(cur?.current_observation?.temp_f).toInteger()
+ atomicState?.curWeatherTemp_c = Math.round(cur?.current_observation?.temp_c.toDouble())
+ atomicState?.curWeatherLoc = cur?.current_observation?.display_location?.full.toString() // This is not available as attribute in dth
+ //atomicState?.curWeatherHum = cur?.current_observation?.relative_humidity?.toString().replaceAll("\\%", "")
+
+ def dp = 0.0
+ if(weather) { // Dewpoint is calculated in dth
+ dp = weather?.currentValue("dewpoint")?.toString().replaceAll("\\[|\\]", "").toDouble()
+ }
+ def c_temp = 0.0
+ def f_temp = 0 as Integer
+ if(getTemperatureScale() == "C") {
+ c_temp = dp as Double
+ f_temp = c_temp * 9/5 + 32
+ } else {
+ f_temp = dp as Integer
+ c_temp = (f_temp - 32) * 5/9 as Double
+ }
+ atomicState?.curWeatherDewpointTemp_c = Math.round(c_temp.round(1) * 2) / 2.0f
+ atomicState?.curWeatherDewpointTemp_f = Math.round(f_temp) as Integer
+
+ atomicState.NeedwUpd = false
+ }
+}
+
+def getExtTmpTemperature() {
+ def extTemp = 0.0
+ if(!settings?.extTmpUseWeather && settings?.extTmpTempSensor) {
+ extTemp = getDeviceTemp(settings?.extTmpTempSensor)
+ } else {
+ if(settings?.extTmpUseWeather && (atomicState?.curWeatherTemp_f || atomicState?.curWeatherTemp_c)) {
+ if(location?.temperatureScale == "C" && atomicState?.curWeatherTemp_c) { extTemp = atomicState?.curWeatherTemp_c.toDouble() }
+ else { extTemp = atomicState?.curWeatherTemp_f.toDouble() }
+ }
+ }
+ return extTemp
+}
+
+def getExtTmpDewPoint() {
+ def extDp = 0.0
+ if(settings?.extTmpUseWeather && (atomicState?.curWeatherDewpointTemp_f || atomicState?.curWeatherDewpointTemp_c)) {
+ if(location?.temperatureScale == "C" && atomicState?.curWeatherDewpointTemp_c) { extDp = roundTemp(atomicState?.curWeatherDewpointTemp_c.toDouble()) }
+ else { extDp = roundTemp(atomicState?.curWeatherDewpointTemp_f.toDouble()) }
+ }
+//TODO if an external sensor, if it has temp and humidity, we can calculate DP
+ return extDp
+}
+
+def getDesiredTemp(curMode) {
+ def modeOff = (curMode == "off") ? true : false
+ def modeCool = (curMode == "cool") ? true : false
+ def modeHeat = (curMode == "heat") ? true : false
+ def modeAuto = (curMode == "auto") ? true : false
+
+ def desiredHeatTemp = getRemSenHeatSetTemp()
+ def desiredCoolTemp = getRemSenCoolSetTemp()
+ def desiredTemp = 0
+ if(desiredHeatTemp && modeHeat) { desiredTemp = desiredHeatTemp }
+ if(desiredCoolTemp && modeCool) { desiredTemp = desiredCoolTemp }
+ if(desiredHeatTemp && desiredCoolTemp && (desiredHeatTemp < desiredCoolTemp) && modeAuto) { desiredTemp = (desiredCoolTemp + desiredHeatTemp)/2.0 }
+ if(modeOff && !desiredTemp && atomicState?.extTmpLastDesiredTemp) { desiredTemp = atomicState?.extTmpLastDesiredTemp }
+
+ LogAction("extTmpTempOk: Desired Temp: ${desiredTemp} | Desired Heat Temp: ${desiredHeatTemp} | Desired Cool Temp: ${desiredCoolTemp} atomicState.extTmpLastDesiredTemp: ${atomicState?.extTmpLastDesiredTemp}", "info", false)
+
+ return desiredTemp
+}
+
+def extTmpTempOk() {
+ //log.trace "extTmpTempOk..."
+ def pName = extTmpPrefix()
+ try {
+ def extTmpTstat = settings?.schMotTstat
+ def extTmpTstatMir = settings?.schMotTstatMir
+
+ //def execTime = now()
+ def intTemp = extTmpTstat ? getRemoteSenTemp().toDouble() : null
+ def extTemp = getExtTmpTemperature()
+ def curMode = extTmpTstat.currentThermostatMode.toString()
+ def dpLimit = getComfortDewpoint(extTmpTstat) ?: (getTemperatureScale() == "C" ? 19 : 66)
+ def curDp = getExtTmpDewPoint()
+ def diffThresh = getExtTmpTempDiffVal()
+ def dpOk = (curDp < dpLimit) ? true : false
+
+ def modeOff = (curMode == "off") ? true : false
+ def modeCool = (curMode == "cool") ? true : false
+ def modeHeat = (curMode == "heat") ? true : false
+ def modeAuto = (curMode == "auto") ? true : false
+
+ def desiredTemp = getDesiredTemp(curMode)
+/*
+ def desiredHeatTemp = getRemSenHeatSetTemp()
+ def desiredCoolTemp = getRemSenCoolSetTemp()
+ def desiredTemp = 0
+ if(desiredHeatTemp && modeHeat) { desiredTemp = desiredHeatTemp }
+ if(desiredCoolTemp && modeCool) { desiredTemp = desiredCoolTemp }
+ if(desiredHeatTemp && desiredCoolTemp && (desiredHeatTemp < desiredCoolTemp) && modeAuto) { desiredTemp = (desiredCoolTemp + desiredHeatTemp)/2.0 }
+ if(modeOff && !desiredTemp && atomicState?.extTmpLastDesiredTemp) { desiredTemp = atomicState?.extTmpLastDesiredTemp }
+ LogAction("extTmpTempOk: Desired Temp: ${desiredTemp} | Desired Heat Temp: ${desiredHeatTemp} | Desired Cool Temp: ${desiredCoolTemp} atomicState.extTmpLastDesiredTemp: ${atomicState?.extTmpLastDesiredTemp}", "info", false)
+
+ if(!modeOff && desiredTemp) { atomicState?.extTmpLastDesiredTemp = desiredTemp }
+*/
+
+ LogAction("extTmpTempOk: Inside Temp: ${intTemp} | curMode: ${curMode} | modeOff: ${modeOff} | atomicState.extTmpTstatOffRequested: ${atomicState?.extTmpTstatOffRequested}", "debug", false)
+
+ if(!desiredTemp) {
+ desiredTemp = intTemp
+ LogAction("extTmpTempOk: No Desired Temp found, using interior Temp", "warn", true)
+ }
+ intTemp = desiredTemp
+
+ def tempDiff = Math.abs(extTemp - intTemp)
+ //LogAction("extTmpTempOk: Outside Temp: ${extTemp} | Temp Threshold: ${diffThresh} | Actual Difference: ${tempDiff} | Outside Dew point: ${curDp} | Dew point Limit: ${dpLimit}", "debug", false)
+
+ def retval = true
+ def tempOk = true
+ def str = "enough different (${tempDiff})"
+ if(intTemp && extTemp && diffThresh) {
+ if(!modeAuto && tempDiff < diffThresh) {
+ retval = false
+ tempOk = false
+ }
+ def extTempHigh = (extTemp > intTemp - diffThresh) ? true : false
+ def extTempLow = (extTemp < intTemp + diffThresh) ? true : false
+ def oldMode = atomicState?.extTmpRestoreMode
+ if(modeCool || oldMode == "cool") {
+ str = "greater than"
+ if(extTempHigh) { retval = false; tempOk = false }
+ }
+ if(modeHeat || oldMode == "heat") {
+ str = "less than"
+ if(extTempLow) { retval = false; tempOk = false }
+ }
+ if(modeAuto) { retval = false; str = "in supported mode" } // no point in turning off if in auto mode
+ if(!dpOk) { retval = false }
+ LogAction("extTmpTempOk: extTempHigh: ${extTempHigh} | extTempLow: ${extTempLow} | dpOk: ${dpOk}", "debug", false)
+ }
+ LogAction("extTmpTempOk: ${retval} Desired Inside Temp: (${intTemp}°${getTemperatureScale()}) is ${tempOk ? "" : "Not"} ${str} $diffThresh° of Outside Temp: (${extTemp}°${getTemperatureScale()}) or Dewpoint: (${curDp}°${getTemperatureScale()}) is ${dpOk ? "ok" : "TOO HIGH"}", "info", false)
+ //storeExecutionHistory((now() - execTime), "getExtTmpTempOk")
+ return retval
+ } catch (ex) {
+ log.error "getExtTmpTempOk Exception:", ex
+ parent?.sendExceptionData(ex, "extTmpTempOk", true, getAutoType())
+ }
+}
+
+def extTmpScheduleOk() { return autoScheduleOk(extTmpPrefix()) }
+def getExtTmpTempDiffVal() { return !settings?.extTmpDiffVal ? 1.0 : settings?.extTmpDiffVal.toDouble() }
+def getExtTmpWhileOnDtSec() { return !atomicState?.extTmpChgWhileOnDt ? 100000 : GetTimeDiffSeconds(atomicState?.extTmpChgWhileOnDt, null, "getExtTmpWhileOnDtSec").toInteger() }
+def getExtTmpWhileOffDtSec() { return !atomicState?.extTmpChgWhileOffDt ? 100000 : GetTimeDiffSeconds(atomicState?.extTmpChgWhileOffDt, null, "getExtTmpWhileOffDtSec").toInteger() }
+
+// TODO 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...", "trace", false)
+ def pName = extTmpPrefix()
+
+ try {
+ if(atomicState?.disableAutomation) { return }
+ else {
+
+ def extTmpTstat = settings?.schMotTstat
+ def extTmpTstatMir = settings?.schMotTstatMir
+
+ def execTime = now()
+ //atomicState?.lastEvalDt = getDtNow()
+
+ if(!atomicState?."${pName}timeOutOn") { atomicState."${pName}timeOutOn" = false }
+ if(cTimeOut) { atomicState."${pName}timeOutOn" = true }
+ def timeOut = atomicState."${pName}timeOutOn" ?: false
+
+ def curMode = extTmpTstat?.currentThermostatMode?.toString()
+ def modeOff = (curMode == "off") ? true : false
+ def safetyOk = getSafetyTempsOk(extTmpTstat)
+ def schedOk = extTmpScheduleOk()
+ def allowNotif = settings?."${pName}NotificationsOn" ? true : false
+ def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
+ def allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
+ def speakOnRestore = allowSpeech && settings?."${pName}SpeechOnRestore" ? true : false
+
+ def home = false
+ def away = false
+ if(extTmpTstat && getTstatPresence(extTmpTstat) == "present") { home = true }
+ else { away = true }
+
+ if(!modeOff) { atomicState."${pName}timeOutOn" = false; timeOut = false }
+ if(!modeOff && atomicState.extTmpTstatOffRequested) { // someone switched us on when we had turned things off, so reset timer and states
+ LogAction("extTmpTempCheck() | System turned on when automation had OFF, resetting state to match", "warn", true)
+ atomicState.extTmpChgWhileOnDt = getDtNow()
+ atomicState.extTmpTstatOffRequested = false
+ atomicState?.extTmpRestoreMode = null
+ atomicState."${pName}timeOutOn" = false
+ unschedTimeoutRestore(pName)
+ }
+
+ def lastaway = atomicState?."${pName}lastaway" // when we state change that could change desired Temp ensure delays happen before off can happen again
+ atomicState?."${pName}lastaway" = home
+
+ def lastDesired = atomicState?.extTmpLastDesiredTemp // this catches scheduled temp or hvac mode changes
+ def desiredTemp = getDesiredTemp(curMode)
+ if(desiredTemp) { atomicState?.extTmpLastDesiredTemp = desiredTemp }
+
+ if(!modeOff && ( (home && lastaway != home) || (desiredTemp && desiredTemp != lastDesired)) ) { atomicState.extTmpChgWhileOnDt = getDtNow() }
+
+ def okToRestore = (modeOff && atomicState?.extTmpTstatOffRequested && atomicState?.extTmpRestoreMode) ? true : false
+ def tempWithinThreshold = extTmpTempOk()
+
+ if(!tempWithinThreshold || timeOut || !safetyOk || away) {
+ if(allowAlarm) { alarmEvtSchedCleanup(extTmpPrefix()) }
+ def rmsg = ""
+ if(okToRestore) {
+ if(getExtTmpWhileOffDtSec() >= (getExtTmpOnDelayVal() - 5) || timeOut || !safetyOk) {
+ def lastMode = null
+ if(atomicState?.extTmpRestoreMode) { lastMode = atomicState?.extTmpRestoreMode }
+ if(lastMode && (lastMode != curMode || timeOut || !safetyOk)) {
+ scheduleAutomationEval(60)
+ if(setTstatMode(extTmpTstat, lastMode)) {
+ storeLastAction("Restored Mode ($lastMode)", getDtNow())
+ atomicState?.extTmpRestoreMode = null
+ atomicState?.extTmpTstatOffRequested = false
+ atomicState?.extTmpRestoredDt = getDtNow()
+ atomicState.extTmpChgWhileOnDt = getDtNow()
+ atomicState."${pName}timeOutOn" = false
+ unschedTimeoutRestore(pName)
+ modeOff = false
+
+ if(extTmpTstatMir) {
+ if(setMultipleTstatMode(extTmpTstatMir, lastMode)) {
+ LogAction("Mirroring (${lastMode}) Restore to ${extTmpTstatMir}", "info", true)
+ }
+ }
+
+ rmsg = "extTmpTempCheck: Restoring '${extTmpTstat?.label}' to '${lastMode.toUpperCase()}' mode "
+ def needAlarm = false
+ if(!safetyOk) {
+ rmsg += "because External Temp Safefy Temps have been reached..."
+ needAlarm = true
+ } else if(timeOut) {
+ rmsg += "because the (${getEnumValue(longTimeSecEnum(), extTmpOffTimeout)}) Timeout has been reached..."
+ } else if(away) {
+ rmsg += "because of AWAY Nest mode..."
+ } else {
+ rmsg += "because External Temp has been 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(atomicState?."${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 because previous mode was not found. Likely due to other automation making changes.", "warn", true)
+ atomicState?.extTmpTstatOffRequested = false
+ } else if(!timeOut && safetyOk) { LogAction("extTmpTstatCheck() | Skipping Restore because the Mode to Restore is same as Current Mode ${curMode}", "info", true) }
+ 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", true)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ }
+ }
+ } else {
+ if(modeOff) {
+ if(timeout || !safetyOk) {
+ LogAction("extTmpTempCheck() | Timeout or Safety temps exceeded and Unable to restore settings okToRestore is false", "warn", true)
+ atomicState."${pName}timeOutOn" = false
+ }
+ else if(!atomicState?.extTmpRestoreMode && atomicState?.extTmpTstatOffRequested) {
+ LogAction("extTmpTempCheck() | Unable to restore settings because previous mode was not found. Likely due to other automation making changes.", "warn", true)
+ atomicState?.extTmpTstatOffRequested = false
+ }
+ }
+ }
+ }
+
+ if(tempWithinThreshold && !timeOut && safetyOk && schedOk && home) {
+ def rmsg = ""
+ if(!modeOff) {
+ if(getExtTmpWhileOnDtSec() >= (getExtTmpOffDelayVal() - 2)) {
+ atomicState."${pName}timeOutOn" = false
+ atomicState?.extTmpRestoreMode = curMode
+ LogAction("extTmpTempCheck: Saving ${extTmpTstat?.label} (${atomicState?.extTmpRestoreMode.toString().toUpperCase()}) mode for Restore later.", "info", true)
+ scheduleAutomationEval(60)
+ if(setTstatMode(extTmpTstat, "off")) {
+ storeLastAction("Turned Off Thermostat", getDtNow())
+ atomicState?.extTmpTstatOffRequested = true
+ atomicState.extTmpChgWhileOffDt = getDtNow()
+ scheduleTimeoutRestore(pName)
+ modeOff = true
+ rmsg = "${extTmpTstat.label} has been turned 'Off' because External Temp is at the temp threshold for (${getEnumValue(longTimeSecEnum(), extTmpOffDelay)})!!!"
+ if(extTmpTstatMir) {
+ setMultipleTstatMode(extTmpTstatMir, "off") {
+ LogAction("Mirroring (Off) Mode to ${extTmpTstatMir}", "info", true)
+ }
+ }
+ LogAction(rmsg, "info", true)
+ if(allowNotif) {
+ sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT
+ if(allowSpeech) { sendEventVoiceNotifications(voiceNotifString(atomicState?."${pName}OffVoiceMsg",pName), pName, "nmExtTmpOff_${app?.id}", true, "nmExtTmpOn_${app?.id}") }
+ if(allowAlarm) { scheduleAlarmOn(pName) }
+ }
+ } else { LogAction("extTmpTempCheck(): Error turning themostat Off", "warn", true) }
+ } else {
+ def remaining = getExtTmpOffDelayVal() - getExtTmpWhileOnDtSec()
+ LogAction("extTmpTempCheck: Delaying OFF for wait period ${getExtTmpOffDelayVal()}, remaining ${remaining}", "info", true)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ }
+ } else {
+ LogAction("extTmpTempCheck() | Skipping because Exterior temperatures in range and '${extTmpTstat?.label}' mode is already 'OFF'", "info", true)
+ }
+ } else {
+ if(!home) { LogAction("extTmpTempCheck: Skipping because of AWAY Nest mode...", "info", true) }
+ else if(!schedOk) { LogAction("extTmpTempCheck: Skipping because of Schedule Restrictions...", "info", true) }
+ else if(!safetyOk) { LogAction("extTmpTempCheck: Skipping because of Safety Temps Exceeded...", "info", true) }
+ else if(timeOut) { LogAction("extTmpTempCheck: Skipping because of active timeout...", "info", true) }
+ else if(!tempWithinThreshold) { LogAction("extTmpTempCheck: Exterior temperatures not in range...", "info", true) }
+ }
+ storeExecutionHistory((now() - execTime), "extTmpTempCheck")
+ }
+ } catch (ex) {
+ log.error "extTmpTempCheck Exception:", ex
+ parent?.sendExceptionData(ex, "extTmpTempCheck", true, getAutoType())
+ }
+}
+
+def extTmpTempEvt(evt) {
+ LogAction("extTmp Event | External Sensor Temperature: ${evt?.displayName} - Temperature is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ storeLastEventData(evt)
+ extTmpDpOrTempEvt("extTmpTempEvt()")
+}
+
+def extTmpDpEvt(evt) {
+ LogAction("extTmp Event | External Sensor Dew point: ${evt?.displayName} - Dew point Temperature is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ storeLastEventData(evt)
+ extTmpDpOrTempEvt("extTmpDpEvt()")
+}
+
+def extTmpDpOrTempEvt(type) {
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def extTmpTstat = settings?.schMotTstat
+ def extTmpTstatMir = settings?.schMotTstatMir
+
+ def curMode = extTmpTstat?.currentThermostatMode.toString()
+ def modeOff = (curMode == "off") ? true : false
+ def offVal = getExtTmpOffDelayVal()
+ def onVal = getExtTmpOnDelayVal()
+ def timeVal
+
+ atomicState.NeedwUpd = true
+ getExtConditions()
+
+ def lastTempWithinThreshold = atomicState?.extTmpLastWithinThreshold
+ def tempWithinThreshold = extTmpTempOk()
+ atomicState?.extTmpLastWithinThreshold = tempWithinThreshold
+
+ if(lastTempWithinThreshold != null && tempWithinThreshold != lastTempWithinThreshold) {
+ if(!modeOff) {
+ atomicState.extTmpChgWhileOnDt = getDtNow()
+ timeVal = ["valNum":offVal, "valLabel":getEnumValue(longTimeSecEnum(), offVal)]
+ } else {
+ atomicState.extTmpChgWhileOffDt = getDtNow()
+ timeVal = ["valNum":onVal, "valLabel":getEnumValue(longTimeSecEnum(), onVal)]
+ }
+ def val = timeVal?.valNum > 20 ? timeVal?.valNum : 20
+ val = timeVal?.valNum < 60 ? timeVal?.valNum : 60
+ LogAction("${type} | External Temp Check scheduled for (${timeVal.valLabel}) HVAC mode: ${curMode}...", "info", true)
+ scheduleAutomationEval(val)
+ } else {
+ LogAction("${type}: Skipping Event...no state change | tempWithinThreshold: ${tempWithinThreshold}", "info", true)
+ }
+ }
+}
+
+/******************************************************************************
+| WATCH CONTACTS AUTOMATION CODE |
+*******************************************************************************/
+def conWatPrefix() { return "conWat" }
+
+def conWatContactDesc() {
+ if(settings?.conWatContacts) {
+ def cCnt = settings?.conWatContacts?.size() ?: 0
+ def str = ""
+ def cnt = 0
+ str += "Contact Status:"
+ settings?.conWatContacts?.each { dev ->
+ cnt = cnt+1
+ str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: (${dev?.currentContact?.toString().capitalize()})"
+ }
+ return str
+ }
+ return null
+}
+
+def isConWatConfigured() {
+ return (settings?.conWatContacts && settings?.conWatOffDelay) ? true : false
+}
+
+def getConWatContactsOk() { return settings?.conWatContacts?.currentState("contact")?.value.contains("open") ? false : true }
+def conWatContactOk() { return (!settings?.conWatContacts) ? false : true }
+def conWatScheduleOk() { return autoScheduleOk(conWatPrefix()) }
+def getConWatOpenDtSec() { return !atomicState?.conWatOpenDt ? 100000 : GetTimeDiffSeconds(atomicState?.conWatOpenDt, null, "getConWatOpenDtSec").toInteger() }
+def getConWatCloseDtSec() { return !atomicState?.conWatCloseDt ? 100000 : GetTimeDiffSeconds(atomicState?.conWatCloseDt, null, "getConWatCloseDtSec").toInteger() }
+def getConWatRestoreDelayBetweenDtSec() { return !atomicState?.conWatRestoredDt ? 100000 : GetTimeDiffSeconds(atomicState?.conWatRestoredDt, null, "getConWatRestoreDelayBetweenDtSec").toInteger() }
+
+// TODO 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) {
+ //log.trace "conWatCheck..."
+ //
+ // Should consider not turning thermostat off, as much as setting it more toward away settings?
+ // There should be monitoring of actual temps for min and max warnings given on/off automations
+ //
+ // Should have some check for stuck contacts
+ // if we cannot save/restore settings, don't bother turning things off
+ //
+ def pName = conWatPrefix()
+
+ def conWatTstat = settings?.schMotTstat
+ def conWatTstatMir = settings?.schMotTstatMir
+
+ try {
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def execTime = now()
+ //atomicState?.lastEvalDt = getDtNow()
+
+ if(!atomicState?."${pName}timeOutOn") { atomicState."${pName}timeOutOn" = false }
+ if(cTimeOut) { atomicState."${pName}timeOutOn" = true }
+ def timeOut = atomicState."${pName}timeOutOn" ?: false
+ def curMode = conWatTstat?.currentState("thermostatMode")?.value.toString()
+ def curNestPres = getTstatPresence(conWatTstat)
+ def modeOff = (curMode == "off") ? true : false
+ def openCtDesc = getOpenContacts(conWatContacts) ? " '${getOpenContacts(conWatContacts)?.join(", ")}' " : " a selected contact "
+ def safetyOk = getSafetyTempsOk(conWatTstat)
+ def schedOk = conWatScheduleOk()
+ def allowNotif = settings?."${pName}NotificationsOn" ? true : false
+ def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
+ def allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
+ def speakOnRestore = allowSpeech && settings?."${pName}SpeechOnRestore" ? true : false
+
+ def home = false
+ def away = false
+ if(conWatTstat && getTstatPresence(conWatTstat) == "present") { home = true }
+ else { away = true }
+
+ //log.debug "curMode: $curMode | modeOff: $modeOff | conWatRestoreOnClose: $conWatRestoreOnClose | lastMode: $lastMode"
+ //log.debug "conWatTstatOffRequested: ${atomicState?.conWatTstatOffRequested} | getConWatCloseDtSec(): ${getConWatCloseDtSec()}"
+
+ if(!modeOff) { atomicState."${pName}timeOutOn" = false; timeOut = false }
+
+ if(!modeOff && atomicState.conWatTstatOffRequested) { // someone switched us on when we had turned things off, so reset timer and states
+ LogAction("conWatCheck() | System turned on when automation had OFF, resetting state to match", "warn", true)
+ atomicState?.conWatRestoreMode = null
+ atomicState?.conWatTstatOffRequested = false
+ atomicState?.conWatOpenDt = getDtNow()
+ atomicState."${pName}timeOutOn" = false
+ unschedTimeoutRestore(pName)
+ }
+
+ def lastaway = atomicState?."${pName}lastaway" // when we state change from away to home, ensure delays happen before off can happen again
+ if(!modeOff && (home && lastaway != home)) { atomicState?.conWatOpenDt = getDtNow() }
+ atomicState?."${pName}lastaway" = home
+
+ def okToRestore = (modeOff && atomicState?.conWatTstatOffRequested) ? true : false
+ def contactsOk = getConWatContactsOk()
+
+ if(contactsOk || timeOut || !safetyOk || away) {
+ if(allowAlarm) { alarmEvtSchedCleanup(conWatPrefix()) }
+ def rmsg = ""
+ if(okToRestore) {
+ if(getConWatCloseDtSec() >= (getConWatOnDelayVal() - 5) || timeOut || !safetyOk) {
+ def lastMode = null
+ if(atomicState?.conWatRestoreMode) { lastMode = atomicState?.conWatRestoreMode }
+ if(lastMode && (lastMode != curMode || timeOut || !safetyOk)) {
+ scheduleAutomationEval(60)
+ if(setTstatMode(conWatTstat, lastMode)) {
+ storeLastAction("Restored Mode ($lastMode) to $conWatTstat", getDtNow())
+ atomicState?.conWatRestoreMode = null
+ atomicState?.conWatTstatOffRequested = false
+ atomicState?.conWatRestoredDt = getDtNow()
+ atomicState?.conWatOpenDt = getDtNow()
+ atomicState."${pName}timeOutOn" = false
+ unschedTimeoutRestore(pName)
+ modeOff = false
+
+ if(conWatTstatMir) {
+ if(setMultipleTstatMode(conWatTstatMir, lastMode)) {
+ LogAction("Mirroring (${lastMode}) Restore to ${conWatTstatMir}", "info", true)
+ }
+ }
+ rmsg = "Restoring '${conWatTstat?.label}' to '${lastMode?.toString().toUpperCase()}' mode "
+ def needAlarm = false
+ if(!safetyOk) {
+ rmsg += "because Global Safefy Values have been reached..."
+ needAlarm = true
+ } else if(timeOut) {
+ rmsg += "because the (${getEnumValue(longTimeSecEnum(), conWatOffTimeout)}) Timeout has been reached..."
+ } else if(away) {
+ rmsg += "because of AWAY Nest mode..."
+ } else {
+ rmsg += "because ALL contacts have been 'Closed' again 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(atomicState?."${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 because previous mode was not found. Likely due to other automation making changes.", "warn", true)
+ atomicState?.conWatTstatOffRequested = false
+ } else if(!timeOut && safetyOk) { LogAction("conWatCheck() | Skipping Restore because the Mode to Restore is same as Current Mode ${curMode}", "info", true) }
+ 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", true)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ }
+ }
+ } else {
+ if(modeOff) {
+ if(timeOut || !safetyOk) {
+ LogAction("conWatCheck() | Timeout or Safety temps exceeded and Unable to restore settings okToRestore is false", "warn", true)
+ atomicState."${pName}timeOutOn" = false
+ }
+ else if(!atomicState?.conWatRestoreMode && atomicState?.conWatTstatOffRequested) {
+ LogAction("conWatCheck() | Unable to restore settings because previous mode was not found. Likely due to other automation making changes.", "warn", true)
+ atomicState?.conWatTstatOffRequested = false
+ }
+ }
+ }
+ }
+
+ if(!contactsOk && safetyOk && !timeOut && schedOk && home) {
+ def rmsg = ""
+ if(!modeOff) {
+ if((getConWatOpenDtSec() >= (getConWatOffDelayVal() - 2)) && (getConWatRestoreDelayBetweenDtSec() >= (getConWatRestoreDelayBetweenVal() - 2))) {
+ atomicState."${pName}timeOutOn" = false
+ atomicState?.conWatRestoreMode = curMode
+ LogAction("conWatCheck: Saving ${conWatTstat?.label} mode (${atomicState?.conWatRestoreMode.toString().toUpperCase()}) for Restore later.", "info", true)
+ LogAction("conWatCheck: ${openCtDesc}${getOpenContacts(conWatContacts).size() > 1 ? "are" : "is"} still Open: Turning 'OFF' '${conWatTstat?.label}'", "debug", true)
+ scheduleAutomationEval(60)
+ if(setTstatMode(conWatTstat, "off")) {
+ storeLastAction("Turned Off $conWatTstat", getDtNow())
+ atomicState?.conWatTstatOffRequested = true
+ atomicState?.conWatCloseDt = getDtNow()
+ scheduleTimeoutRestore(pName)
+ if(conWatTstatMir) {
+ setMultipleTstatMode(conWatTstatMir, "off") {
+ LogAction("Mirroring (Off) Mode to ${conWatTstatMir}", "info", true)
+ }
+ }
+ rmsg = "${conWatTstat.label} has been turned 'OFF' because${openCtDesc}has been Opened for (${getEnumValue(longTimeSecEnum(), conWatOffDelay)})..."
+ LogAction(rmsg, "info", true)
+ if(allowNotif) {
+ sendEventPushNotifications(rmsg, "Info", pName) // this uses parent and honors quiet times, others do NOT
+ if(allowSpeech) { sendEventVoiceNotifications(voiceNotifString(atomicState?."${pName}OffVoiceMsg",pName), pName, "nmConWatOff_${app?.id}", true, "nmConWatOn_${app?.id}") }
+ if(allowAlarm) { scheduleAlarmOn(pName) }
+ }
+ } else { LogAction("conWatCheck(): Error turning themostat Off", "warn", true) }
+ } else {
+ if(getConWatRestoreDelayBetweenDtSec() < (getConWatRestoreDelayBetweenVal() - 2)) {
+ def remaining = getConWatRestoreDelayBetweenVal() - getConWatRestoreDelayBetweenDtSec()
+ LogAction("conWatCheck() | Skipping OFF change because the delay since last restore has been less than (${getEnumValue(longTimeSecEnum(), conWatRestoreDelayBetween)})", "info", false)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ } else {
+ def remaining = getConWatOffDelayVal() - getConWatOpenDtSec()
+ LogAction("conWatCheck: Delaying OFF for wait period ${getConWatOffDelayVal()}, remaining ${remaining}", "info", true)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ }
+ }
+ } else {
+ LogAction("conWatCheck() | Skipping OFF change because '${conWatTstat?.label}' mode is already 'OFF'", "info", true)
+ }
+ } else {
+ if(!home) { LogAction("conWatCheck: Skipping because of AWAY Nest mode...", "info", true) }
+ else if(!schedOk) { LogAction("conWatCheck: Skipping because of Schedule Restrictions...", "info", true) }
+ else if(!safetyOk) { LogAction("conWatCheck: Skipping because of Safety Temps Exceeded...", "warn", true) }
+ else if(timeOut) { LogAction("conWatCheck: Skipping because of active timeout...", "info", true) }
+ else if(contactsOk) { LogAction("conWatCheck: Contacts are closed...", "info", true) }
+ }
+ storeExecutionHistory((now() - execTime), "conWatCheck")
+ }
+ } catch (ex) {
+ log.error "conWatCheck Exception:", ex
+ parent?.sendExceptionData(ex, "conWatCheck", true, getAutoType())
+ }
+}
+
+def conWatContactEvt(evt) {
+ LogAction("ContactWatch Contact Event | '${evt?.displayName}' is now (${evt?.value.toString().toUpperCase()})", "trace", false)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def conWatTstat = settings?.schMotTstat
+ def curMode = conWatTstat?.currentThermostatMode.toString()
+ def isModeOff = (curMode == "off") ? true : false
+ def conOpen = (evt?.value == "open") ? true : false
+ def canSched = false
+ def timeVal
+ if(conOpen) {
+ atomicState?.conWatOpenDt = getDtNow()
+ timeVal = ["valNum":getConWatOffDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getConWatOffDelayVal())]
+ canSched = true
+ }
+ else if(!conOpen && getConWatContactsOk()) {
+ if(isModeOff) {
+ atomicState.conWatCloseDt = getDtNow()
+ timeVal = ["valNum":getConWatOnDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getConWatOnDelayVal())]
+ canSched = true
+ }
+ }
+ storeLastEventData(evt)
+ if(canSched) {
+ LogAction("conWatContactEvt: ${!evt ? "A monitored Contact is " : "'${evt?.displayName}' is "} '${evt?.value.toString().toUpperCase()}' | Contact Check scheduled for (${timeVal?.valLabel})...", "info", true)
+ def val = timeVal?.valNum > 20 ? timeVal?.valNum : 20
+ val = timeVal?.valNum < 60 ? timeVal?.valNum : 60
+ scheduleAutomationEval(val)
+ } else {
+ LogAction("conWatContactEvt: Skipping Event...", "info", true)
+ }
+ }
+}
+
+/******************************************************************************
+| WATCH FOR LEAKS AUTOMATION LOGIC CODE |
+******************************************************************************/
+def leakWatPrefix() { return "leakWat" }
+
+def leakWatSensorsDesc() {
+ if(settings?.leakWatSensors) {
+ def cCnt = settings?.leakWatSensors?.size() ?: 0
+ def str = ""
+ def cnt = 0
+ str += "Leak Sensors:"
+ settings?.leakWatSensors?.each { dev ->
+ cnt = cnt+1
+ str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: ${dev?.currentWater?.toString().capitalize()}"
+ }
+ return str
+ }
+ return null
+}
+
+def isLeakWatConfigured() {
+ return (settings?.leakWatSensors) ? true : false
+}
+
+def getLeakWatSensorsOk() { return settings?.leakWatSensors?.currentState("water")?.value.contains("wet") ? false : true }
+def leakWatSensorsOk() { return (!settings?.leakWatSensors) ? false : true }
+def leakWatScheduleOk() { return autoScheduleOk(leakWatPrefix()) }
+
+// TODO allow override from schedule?
+def getLeakWatOnDelayVal() { return !settings?.leakWatOnDelay ? 300 : settings?.leakWatOnDelay.toInteger() }
+def getLeakWatDryDtSec() { return !atomicState?.leakWatDryDt ? 100000 : GetTimeDiffSeconds(atomicState?.leakWatDryDt, null, "getLeakWatDryDtSec").toInteger() }
+
+def leakWatCheck() {
+ //log.trace "leakWatCheck..."
+//
+// TODO Should have some check for stuck contacts
+// if we cannot save/restore settings, don't bother turning things off
+//
+ def pName = leakWatPrefix()
+ try {
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def leakWatTstat = settings?.schMotTstat
+ def leakWatTstatMir = settings?.schMotTstatMir
+
+ def execTime = now()
+ //atomicState?.lastEvalDt = getDtNow()
+
+ def curMode = leakWatTstat?.currentState("thermostatMode")?.value.toString()
+ def curNestPres = getTstatPresence(leakWatTstat)
+ def modeOff = (curMode == "off") ? true : false
+ def wetCtDesc = getWetWaterSensors(leakWatSensors) ? " '${getWetWaterSensors(leakWatSensors)?.join(", ")}' " : " a selected leak sensor "
+ def safetyOk = getSafetyTempsOk(leakWatTstat)
+ def schedOk = leakWatScheduleOk()
+ def allowNotif = settings?."${pName}NotificationsOn" ? true : false
+ def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
+ def allowAlarm = allowNotif && settings?."${pName}AllowAlarmNotif" ? true : false
+ def speakOnRestore = allowSpeech && settings?."${pName}SpeechOnRestore" ? true : false
+
+ if(!modeOff && atomicState.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)
+ atomicState?.leakWatRestoreMode = null
+ atomicState?.leakWatTstatOffRequested = false
+ }
+
+ def okToRestore = (modeOff && atomicState?.leakWatTstatOffRequested) ? true : false
+ def sensorsOk = getLeakWatSensorsOk()
+
+ if(sensorsOk || !safetyOk) {
+ if(allowAlarm) { alarmEvtSchedCleanup(leakWatPrefix()) }
+ def rmsg = ""
+
+ if(okToRestore) {
+ if(getLeakWatDryDtSec() >= (getLeakWatOnDelayVal() - 5) || !safetyOk) {
+ def lastMode = null
+ if(atomicState?.leakWatRestoreMode) { lastMode = atomicState?.leakWatRestoreMode }
+ if(lastMode && (lastMode != curMode || !safetyOk)) {
+ scheduleAutomationEval(60)
+ if(setTstatMode(leakWatTstat, lastMode)) {
+ storeLastAction("Restored Mode ($lastMode) to $leakWatTstat", getDtNow())
+ atomicState?.leakWatTstatOffRequested = false
+ atomicState?.leakWatRestoreMode = null
+ atomicState?.leakWatRestoredDt = getDtNow()
+
+ if(leakWatTstatMir) {
+ if(setMultipleTstatMode(leakWatTstatMir, lastmode)) {
+ LogAction("leakWatCheck: Mirroring Restoring Mode (${lastMode}) to ${tstat}", "info", true)
+ }
+ }
+ rmsg = "Restoring '${leakWatTstat?.label}' to '${lastMode.toUpperCase()}' mode "
+ def needAlarm = false
+ if(!safetyOk) {
+ rmsg += "because External Temp Safefy Temps have been reached..."
+ needAlarm = true
+ } else {
+ rmsg += "because ALL leak sensors have been 'Dry' again 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(atomicState?."${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 because the Mode to Restore is same as Current Mode ${curMode}", "info", true)
+ }
+ }
+ } else {
+ if(safetyOk) {
+ def remaining = getLeakWatOnDelayVal() - getLeakWatDryDtSec()
+ LogAction("leakWatCheck: Delaying restore for wait period ${getLeakWatOnDelayVal()}, remaining ${remaining}", "info", true)
+ remaining = remaining > 20 ? remaining : 20
+ remaining = remaining < 60 ? remaining : 60
+ scheduleAutomationEval(remaining)
+ }
+ }
+ } else {
+ if(modeOff) {
+ if(!safetyOk) {
+ LogAction("leakWatCheck() | Safety temps exceeded and Unable to restore settings okToRestore is false", "warn", true)
+ }
+ else if(!atomicState?.leakWatRestoreMode && atomicState?.leakWatTstatOffRequested) {
+ LogAction("leakWatCheck() | Unable to restore settings because previous mode was not found. Likely due to other automation making changes.", "warn", true)
+ atomicState?.leakWatTstatOffRequested = false
+ }
+ }
+ }
+ }
+
+// tough decision here: there is a leak, do we care about schedule ?
+// if(!getLeakWatSensorsOk() && safetyOk && schedOk) {
+ if(!sensorsOk && safetyOk) {
+ def rmsg = ""
+ if(!modeOff) {
+ atomicState?.leakWatRestoreMode = curMode
+ LogAction("leakWatCheck: Saving ${leakWatTstat?.label} mode (${atomicState?.leakWatRestoreMode.toString().toUpperCase()}) for Restore later.", "info", true)
+ LogAction("leakWatCheck: ${wetCtDesc}${getWetWaterSensors(leakWatSensors).size() > 1 ? "are" : "is"} Wet: Turning 'OFF' '${leakWatTstat?.label}'", "debug", true)
+ scheduleAutomationEval(60)
+ if(setTstatMode(leakWatTstat, "off")) {
+ storeLastAction("Turned Off $leakWatTstat", getDtNow())
+ atomicState?.leakWatTstatOffRequested = true
+ atomicState?.leakWatDryDt = getDtNow()
+
+ if(leakWatTstatMir) {
+ if(setMultipleTstatMode(leakWatTstatMir, "off")) {
+ LogAction("leakWatCheck: Mirroring (Off) Mode to ${tstat}", "info", true)
+ }
+ }
+ rmsg = "${leakWatTstat.label} has been turned 'OFF' because${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(atomicState?."${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 because '${leakWatTstat?.label}' mode is already 'OFF'", "info", true)
+ }
+ } else {
+ //if(!schedOk) { LogAction("leakWatCheck: Skipping because of Schedule Restrictions...", "warn", true) }
+ if(!safetyOk) { LogAction("leakWatCheck: Skipping because of Safety Temps Exceeded...", "warn", true) }
+ if(sensorsOk) { LogAction("leakWatCheck: Sensors are ok...", "info", true) }
+ }
+ storeExecutionHistory((now() - execTime), "leakWatCheck")
+ }
+ } catch (ex) {
+ log.error "leakWatCheck Exception:", ex
+ parent?.sendExceptionData(ex, "leakWatCheck", true, getAutoType())
+ }
+}
+
+def leakWatSensorEvt(evt) {
+ LogAction("LeakWatch Sensor Event | '${evt?.displayName}' is now (${evt?.value.toString().toUpperCase()})", "trace", false)
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def curMode = leakWatTstat?.currentThermostatMode.toString()
+ def isModeOff = (curMode == "off") ? true : false
+ def leakWet = (evt?.value == "wet") ? true : false
+
+ def canSched = false
+ def timeVal
+ if(leakWet) {
+ canSched = true
+ }
+ else if(!leakWet && getLeakWatSensorsOk()) {
+ if(isModeOff) {
+ atomicState?.leakWatDryDt = getDtNow()
+ timeVal = ["valNum":getLeakWatOnDelayVal(), "valLabel":getEnumValue(longTimeSecEnum(), getLeakWatOnDelayVal())]
+ canSched = true
+ }
+ }
+
+ storeLastEventData(evt)
+ if(canSched) {
+ LogAction("leakWatSensorEvt: ${!evt ? "A monitored Leak Sensor is " : "'${evt?.displayName}' is "} '${evt?.value.toString().toUpperCase()}' | Leak Check scheduled for (${timeVal?.valLabel})...", "info", true)
+ def val = timeVal?.valNum > 20 ? timeVal?.valNum : 20
+ val = timeVal?.valNum < 60 ? timeVal?.valNum : 60
+ scheduleAutomationEval(val)
+ } else {
+ LogAction("leakWatSensorEvt: Skipping Event...", "info", true)
+ }
+ }
+}
+
+/********************************************************************************
+| MODE AUTOMATION CODE |
+*********************************************************************************/
+def nModePrefix() { return "nMode" }
+
+def nestModePresPage() {
+ def pName = nModePrefix()
+ dynamicPage(name: "nestModePresPage", title: "Nest Mode - Nest Home/Away Automation", uninstall: false, install: false) {
+ if(!nModePresSensor && !nModeSwitch) {
+ def modeReq = (!nModePresSensor && (nModeHomeModes || nModeAwayModes))
+ section("Set Nest Presence with ST Modes:") {
+ input "nModeHomeModes", "mode", title: "Modes to Set Nest Location 'Home'", multiple: true, submitOnChange: true, required: modeReq,
+ image: getAppImg("mode_home_icon.png")
+ if(checkModeDuplication(nModeHomeModes, nModeAwayModes)) {
+ paragraph "ERROR:\nDuplicate Mode(s) were found under both the Home and Away Modes!!!\nPlease Correct to Proceed...", required: true, state: null, image: getAppImg("error_icon.png")
+ }
+ input "nModeAwayModes", "mode", title: "Modes to Set Nest Location 'Away'", multiple: true, submitOnChange: true, required: modeReq,
+ image: getAppImg("mode_away_icon.png")
+ if(nModeHomeModes && nModeAwayModes) {
+ def str = ""
+ def pLocationPresence = getNestLocPres()
+ str += location?.mode && plocationPresence ? "Location Status:" : ""
+ str += location?.mode ? "\n ├ SmartThings Mode: ${location?.mode}" : ""
+ str += plocationPresence ? "\n └ Nest Location: (${plocationPresence == "away" ? "Away" : "Home"})" : ""
+ paragraph "${str}", state: (str != "" ? "complete" : null), image: getAppImg("instruct_icon.png")
+ }
+ }
+ }
+ if(!nModeHomeModes && !nModeAwayModes && !nModeSwitch) {
+ section("(Optional) Set Nest Presence using Presence Sensor:") {
+ //paragraph "Choose a Presence Sensor(s) to use to set your Nest to Home/Away", image: getAppImg("instruct_icon")
+ def presDesc = nModePresenceDesc() ? "\n\n${nModePresenceDesc()}\n\nTap to Modify" : "Tap to Configure"
+ input "nModePresSensor", "capability.presenceSensor", title: "Select a Presence Sensor", description: presDesc, multiple: true, submitOnChange: true, required: false,
+ image: getAppImg("presence_icon.png")
+ if(nModePresSensor) {
+ if(nModePresSensor.size() > 1) {
+ paragraph "Nest Location will be set to 'Away' when all Presence sensors leave and will return to 'Home' when someone arrives", title: "How this Works!", image: getAppImg("instruct_icon.png")
+ }
+ paragraph "${nModePresenceDesc()}", state: "complete", image: getAppImg("instruct_icon.png")
+ }
+ }
+ }
+ if(!nModePresSensor && !nModeHomeModes && !nModeAwayModes) {
+ section("(Optional) Set Nest Presence based on the state of a Switch:") {
+ input "nModeSwitch", "capability.switch", title: "Select a Switch", required: false, multiple: false, submitOnChange: true, image: getAppImg("switch_on_icon.png")
+ if(nModeSwitch) {
+ input "nModeSwitchOpt", "enum", title: "Switch State to Trigger 'Away'?", required: true, defaultValue: "On", options: ["On", "Off"], submitOnChange: true, image: getAppImg("settings_icon.png")
+ }
+ }
+ }
+ if((nModeHomeModes && nModeAwayModes) || nModePresSensor || nModeSwitch) {
+ section("Delay Changes:") {
+ input (name: "nModeDelay", type: "bool", title: "Delay Changes?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("delay_time_icon.png"))
+ if(nModeDelay) {
+ input "nModeDelayVal", "enum", title: "Delay before change?", required: false, defaultValue: 60, metadata: [values:longTimeSecEnum()],
+ submitOnChange: true, image: getAppImg("configure_icon.png")
+ }
+ if(parent?.settings?.cameras) {
+ input (name: "nModeCamOnAway", type: "bool", title: "Turn On Nest Cams when Away?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("camera_green_icon.png"))
+ input (name: "nModeCamOffHome", type: "bool", title: "Turn Off Nest Cams when Home?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("camera_gray_icon.png"))
+ }
+ }
+ }
+ if(((nModeHomeModes && nModeAwayModes) && !nModePresSensor) || nModePresSensor) {
+ section(getDmtSectionDesc(nModePrefix())) {
+ def pageDesc = getDayModeTimeDesc(pName)
+ href "setDayModeTimePage", title: "Configured Restrictions", description: pageDesc, params: ["pName": "${pName}"], state: (pageDesc ? "complete" : null),
+ image: getAppImg("cal_filter_icon.png")
+ }
+ section("Notifications:") {
+ def pageDesc = getNotifConfigDesc(pName)
+ href "setNotificationPage", title: "Configured Alerts...", description: pageDesc, params: ["pName":"${pName}", "allowSpeech":false, "allowAlarm":false, "showSchedule":true],
+ state: (pageDesc ? "complete" : null), image: getAppImg("notification_icon.png")
+ }
+ }
+ if(atomicState?.showHelp) {
+ section("Help:") {
+ href url:"${getAutoHelpPageUrl()}", style:"embedded", required:false, title:"Help and Instructions...", description:"", image: getAppImg("info.png")
+ }
+ }
+ }
+}
+
+def nModePresenceDesc() {
+ if(settings?.nModePresSensor) {
+ def cCnt = nModePresSensor?.size() ?: 0
+ def str = ""
+ def cnt = 0
+ str += "Presence Status:"
+ settings?.nModePresSensor?.each { dev ->
+ cnt = cnt+1
+ def presState = dev?.currentPresence ? dev?.currentPresence?.toString().capitalize() : "No State"
+ str += "${(cnt >= 1) ? "${(cnt == cCnt) ? "\n└" : "\n├"}" : "\n└"} ${dev?.label}: ${(dev?.label.length() > 10) ? "\n${(cCnt == 1 || cnt == cCnt) ? " " : "│"}└ " : ""}(${presState})"
+ }
+ return str
+ }
+ return null
+}
+
+def isNestModesConfigured() {
+ def devOk = ((!nModePresSensor && !nModeSwitch && (nModeHomeModes && nModeAwayModes)) || (nModePresSensor && !nModeSwitch) || (!nModePresSensor && nModeSwitch)) ? true : false
+ return devOk
+}
+
+def nModeSTModeEvt(evt) {
+ LogAction("Event | ST Mode is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else if(!nModePresSensor && !nModeSwitch) {
+ storeLastEventData(evt)
+ if(nModeDelay) {
+ def delay = nModeDelayVal.toInteger()
+
+ if(delay > 20) {
+ LogAction("Event | A Mode Check is scheduled for (${getEnumValue(longTimeSecEnum(), nModeDelayVal)})", "info", true)
+ scheduleAutomationEval(delay)
+ } else { scheduleAutomationEval() }
+ } else {
+ scheduleAutomationEval()
+ }
+ }
+}
+
+def nModePresEvt(evt) {
+ LogAction("Event | Presence: ${evt?.displayName} - Presence is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else if(nModeDelay) {
+ storeLastEventData(evt)
+ def delay = nModeDelayVal.toInteger()
+
+ if(delay > 20) {
+ LogAction("Event | A Presence Check is scheduled for (${getEnumValue(longTimeSecEnum(), nModeDelayVal)})", "info", true)
+ scheduleAutomationEval(delay)
+ } else { scheduleAutomationEval() }
+ } else {
+ scheduleAutomationEval()
+ }
+}
+
+def nModeSwitchEvt(evt) {
+ LogAction("Event | Switch: ${evt?.displayName} - is (${evt?.value.toString().toUpperCase()})", "trace", true)
+ if(atomicState?.disableAutomation) { return }
+ else if(nModeSwitch && !nModePresSensor) {
+ storeLastEventData(evt)
+ if(nModeDelay) {
+ def delay = nModeDelayVal.toInteger()
+ if(delay > 20) {
+ LogAction("Event | A Switch Check is scheduled for (${getEnumValue(longTimeSecEnum(), nModeDelayVal)})", "info", true)
+ scheduleAutomationEval(delay)
+ } else { scheduleAutomationEval() }
+ } else {
+ scheduleAutomationEval()
+ }
+ }
+}
+
+def nModeScheduleOk() { return autoScheduleOk(nModePrefix()) }
+
+def checkNestMode() {
+ LogAction("checkNestMode...", "trace", false)
+//
+// This automation only works with Nest as it toggles non-ST standard home/away
+//
+ def pName = nModePrefix()
+ try {
+ if(atomicState?.disableAutomation) { return }
+ else if(!nModeScheduleOk()) {
+ LogAction("checkNestMode: Skipping because of Schedule Restrictions...", "info", true)
+ atomicState.lastStMode = null
+ atomicState.lastPresSenAway = null
+ } else {
+ def execTime = now()
+ //atomicState?.lastEvalDt = getDtNow()
+
+ def curStMode = location?.mode
+ def allowNotif = settings?."${nModePrefix()}NotificationsOn" ? true : false
+ def nestModeAway = (getNestLocPres() == "home") ? false : true
+ def awayPresDesc = (nModePresSensor && !nModeSwitch) ? "All Presence device(s) have left setting " : ""
+ def homePresDesc = (nModePresSensor && !nModeSwitch) ? "A Presence Device is Now Present setting " : ""
+ def awaySwitDesc = (nModeSwitch && !nModePresSensor) ? "${nModeSwitch} State is 'Away' setting " : ""
+ def homeSwitDesc = (nModeSwitch && !nModePresSensor) ? "${nModeSwitch} State is 'Home' setting " : ""
+ def modeDesc = ((!nModeSwitch && !nModePresSensor) && nModeHomeModes && nModeAwayModes) ? "The mode (${curStMode}) has triggered " : ""
+ def awayDesc = "${awayPresDesc}${awaySwitDesc}${modeDesc}"
+ def homeDesc = "${homePresDesc}${homeSwitDesc}${modeDesc}"
+
+ def previousStMode = atomicState?.lastStMode
+ def previousPresSenAway = atomicState?.lastPresSenAway
+
+ def away = false
+ def home = false
+
+ 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)
+ }
+
+ def modeMatch = false // these check that we only change once per ST or presence change
+ if(nModeHomeModes && nModeAwayModes) {
+ if(previousStMode == curStMode) {
+ modeMatch = true
+ }
+ }
+ if(nModePresSensor && !nModeSwitch) {
+ if(previousPresSenAway != null && previousPresSenAway == away) {
+ modeMatch = true
+ }
+ }
+
+ LogAction("checkNestMode: isPresenceHome: (${nModePresSensor ? "${isPresenceHome(nModePresSensor)}" : "Presence Not Used"}) | ST-Mode: ($curStMode) | NestModeAway: ($nestModeAway) | Away?: ($away) | Home?: ($home) | modeMatch: ($modeMatch)", "info", true)
+
+ if(away && !nestModeAway && !modeMatch) {
+ LogAction("${awayDesc} Nest 'Away'", "info", true)
+ if(parent?.setStructureAway(null, true)) {
+ storeLastAction("Set Nest Location (Away)", getDtNow())
+ atomicState?.nModeTstatLocAway = true
+ atomicState.lastStMode = curStMode
+ atomicState.lastPresSenAway = away
+ if(allowNotif) {
+ sendEventPushNotifications("${awayDesc} Nest 'Away'", "Info", pName)
+ }
+ if(nModeCamOnAway) {
+ def cams = parent?.settings?.cameras
+ cams?.each { cam ->
+ def dev = getChildDevice(cam)
+ if(dev) {
+ //storeLastAction("Turned On Streaming for $cam", getDtNow())
+ dev?.on()
+ LogAction("checkNestMode: Turning Streaming On for (${dev}) because Location is now Away...", "info", true)
+ }
+ }
+ }
+ } else {
+ LogAction("checkNestMode: There was an issue sending the AWAY command to Nest", "error", true)
+ }
+ scheduleAutomationEval(60)
+ }
+ else if(home && nestModeAway && !modeMatch) {
+ LogAction("${homeDesc} Nest 'Home'", "info", true)
+ if(parent?.setStructureAway(null, false)) {
+ storeLastAction("Set Nest Location (Home)", getDtNow())
+ atomicState?.nModeTstatLocAway = false
+ atomicState.lastStMode = curStMode
+ atomicState.lastPresSenAway = away
+ if(allowNotif) {
+ sendEventPushNotifications("${homeDesc} Nest 'Home'", "Info", pName)
+ }
+ if(nModeCamOffHome) {
+ def cams = parent?.settings?.cameras
+ cams?.each { cam ->
+ def dev = getChildDevice(cam)
+ if(dev) {
+ dev?.off()
+ LogAction("checkNestMode: Turning Streaming Off for (${dev}) because Location is now Home...", "info", true)
+ //storeLastAction("Turned Streaming Off for $cam", getDtNow())
+ }
+ }
+ }
+ } else {
+ LogAction("checkNestMode: There was an issue sending the AWAY command to Nest", "error", true)
+ }
+ scheduleAutomationEval(60)
+ }
+ else {
+ LogAction("checkNestMode: Conditions are not valid to change mode | isPresenceHome: (${nModePresSensor ? "${isPresenceHome(nModePresSensor)}" : "Presence Not Used"}) | ST-Mode: ($curStMode) | NestModeAway: ($nestModeAway) | Away?: ($away) | Home?: ($home) | modeMatch: ($modeMatch)", "info", true)
+ }
+ storeExecutionHistory((now() - execTime), "checkNestMode")
+ }
+ } catch (ex) {
+ log.error "checkNestMode Exception:", ex
+ parent?.sendExceptionData(ex, "checkNestMode", true, getAutoType())
+ }
+}
+
+def getNestLocPres() {
+ if(atomicState?.disableAutomation) { return }
+ else {
+ def plocationPresence = parent?.locationPresence()
+ if(!plocationPresence) { return null }
+ else {
+ return plocationPresence
+ }
+ }
+}
+
+/********************************************************************************
+| SCHEDULE, MODE, or MOTION CHANGES ADJUST THERMOSTAT SETPOINTS |
+| (AND THERMOSTAT MODE) AUTOMATION CODE |
+*********************************************************************************/
+def tModePrefix() { return "tMode" }
+
+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 ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
+}
+
+private getDayOfWeekName(date = null) {
+ if (!date) {
+ date = adjustTime(now())
+ }
+ 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 = atomicState?.scheduleList
+ def res1
+ def ccnt = 1
+ for (cnt in schedList) {
+ res1 = checkRestriction(cnt)
+ if(res1 == null) { break }
+ ccnt += 1
+ }
+ if(ccnt > schedList?.size()) { noSched = true }
+ else { mySched = ccnt }
+ //LogAction("getCurrentSchedule: mySched: $mySched noSched: $noSched ccnt: $ccnt res1: $res1", "trace", false)
+ return mySched
+}
+
+private checkRestriction(cnt) {
+// LogAction("checkRestriction:( $cnt )...", "trace", false)
+ def sLbl = "schMot_${cnt}_"
+
+ def restriction
+
+ def act = settings["${sLbl}SchedActive"]
+ if(act) {
+ def apprestrict = atomicState?."sched${cnt}restrictions"
+
+ if (apprestrict?.m && apprestrict?.m.size() && !(location.mode in apprestrict?.m)) {
+ restriction = "a 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}restrictionSwitchOn"]) {
+ for(sw in settings["${sLbl}restrictionSwitchOn"]) {
+ if (sw.currentValue("switch") != "on") {
+ restriction = "switch ${sw} being ${sw.currentValue("switch")}"
+ break
+ }
+ }
+ }
+ if (!restriction && settings["${sLbl}restrictionSwitchOff"]) {
+ for(sw in settings["${sLbl}restrictionSwitchOff"]) {
+ if (sw.currentValue("switch") != "off") {
+ restriction = "switch ${sw} being ${sw.currentValue("switch")}"
+ break
+ }
+ }
+ }
+ }
+ } else {
+ restriction = "an inactive schedule"
+ }
+ //LogAction("checkRestriction:( $cnt ) restriction: $restriction", "trace", false)
+ return restriction
+}
+
+def getActiveScheduleState() {
+ return atomicState?.activeSchedData ?: null
+}
+
+def getSchRestrictDoWOk(cnt) {
+ def apprestrict = atomicState?.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 in ST?
+private getSunrise() {
+ def sunTimes = getSunriseAndSunset()
+ return adjustTime(sunTimes.sunrise)
+}
+
+private getSunset() {
+ def sunTimes = getSunriseAndSunset()
+ return adjustTime(sunTimes.sunset)
+}
+
+def isTstatSchedConfigured() {
+ //return (settings?.schMotSetTstatTemp && atomicState?.activeSchedData?.size())
+ return (atomicState.scheduleSchedActiveCount)
+}
+
+/* //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) {
+ //log.trace "checkOnMotion($mySched)"
+ def sLbl = "schMot_${mySched}_"
+
+ if(settings["${sLbl}Motion"] && atomicState?."${sLbl}MotionActiveDt") {
+ def motionOn = isMotionActive(settings["${sLbl}Motion"])
+
+ def lastActiveMotionDt = Date.parse("E MMM dd HH:mm:ss z yyyy", atomicState?."${sLbl}MotionActiveDt").getTime()
+ def lastActiveMotionSec = getLastMotionActiveSec(mySched)
+
+ def lastInactiveMotionDt = 1
+ def lastInactiveMotionSec
+
+ if(atomicState?."${sLbl}MotionInActiveDt") {
+ lastInactiveMotionDt = Date.parse("E MMM dd HH:mm:ss z yyyy", atomicState?."${sLbl}MotionInActiveDt").getTime()
+ lastInactiveMotionSec = getLastMotionInActiveSec(mySched)
+ }
+
+ LogAction("checkOnMotion: [ Active Dt: $lastActiveMotionDt ($lastActiveMotionSec sec.) | Inactive Dt: $lastInactiveMotionDt ($lastInactiveMotionSec sec.) | MotionOn: ($motionOn) ]", "trace", true)
+
+ 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 = 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(atomicState?."motion${mySched}UseMotionSettings" && atomicState?."motion${mySched}TurnedOnDt") {
+ lastOnTime = Date.parse("E MMM dd HH:mm:ss z yyyy", atomicState?."motion${mySched}TurnedOnDt").getTime()
+ if(ontimeNum > lastOnTime) {
+ ontimeNum = lastOnTime - ontimedelay
+ }
+ }
+
+ def ontime = formatDt( ontimeNum )
+ def offtime = formatDt( offtimeNum )
+
+ LogAction("checkOnMotion: [ Active Dt: (${atomicState."${sLbl}MotionActiveDt"}) | OnTime: ($ontime) | Inactive Dt: (${atomicState?."${sLbl}MotionInActiveDt"}) | OffTime: ($offtime) ]", "info", true)
+ def result = false
+ if(nowDt >= ontimeNum && nowDt <= offtimeNum) {
+ result = true
+ }
+ if(nowDt < ontimeNum || (result && !motionOn)) {
+ LogAction("checkOnMotion($mySched): scheduling motion check", "trace", true)
+ scheduleAutomationEval(60)
+ }
+ return result
+ }
+ return false
+}
+
+def setTstatTempCheck() {
+ LogAction("setTstatTempCheck...", "trace", 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 react to home/away changes
+ */
+ try {
+ def tstat = settings?.schMotTstat
+ def tstatMir = settings?.schMotTstatMir
+
+ if(atomicState?.disableAutomation) { return }
+ def execTime = now()
+
+ def away = (getNestLocPres() == "home") ? false : true
+// ERSERS
+ def mySched = getCurrentSchedule()
+ def noSched = (mySched == null) ? true : false
+
+ def previousSched = atomicState?.lastSched
+ def samesched = previousSched == mySched ? true : false
+
+ if((!samesched || away ) && previousSched) { // schedule change - set old schedule to not use motion
+ if(atomicState?."motion${previousSched}UseMotionSettings") {
+ LogAction("setTstatTempCheck: Disabled Use of Motion Settings for previous schedule ${previousSched}", "info", true)
+ }
+ atomicState?."motion${previousSched}UseMotionSettings" = false
+ atomicState?."motion${previousSched}LastisBtwn" = false
+ }
+
+ if(!samesched || away ) { // schedule change, clear out overrides
+ disableOverrideTemps()
+ }
+
+ LogAction("setTstatTempCheck: [Current Schedule: ($mySched) | Previous Schedule: (${previousSched}) | No Schedule: ($noSched)]", "trace", false)
+
+ if(noSched || away) {
+ if(away) {
+ LogAction("setTstatTempCheck: Skipping check because [Nest is set AWAY]", "info", true)
+ mySched = null
+ }
+ else { LogAction("setTstatTempCheck: Skipping check because [No matching Schedule]", "info", true) }
+ } else {
+ def isBtwn = checkOnMotion(mySched)
+ def previousBtwn = atomicState?."motion${mySched}LastisBtwn"
+ atomicState?."motion${mySched}LastisBtwn" = isBtwn
+
+ if(!isBtwn) {
+ if(atomicState?."motion${mySched}UseMotionSettings") {
+ LogAction("setTstatTempCheck: Disabled Use of Motion Settings for schedule ${mySched}", "info", true)
+ }
+ atomicState?."motion${mySched}UseMotionSettings" = false
+ }
+
+ def sLbl = "schMot_${mySched}_"
+ def motionOn = isMotionActive(settings["${sLbl}Motion"])
+
+ if(!atomicState?."motion${mySched}UseMotionSettings" && isBtwn && !previousBtwn) { // transitioned to use Motion
+ if(motionOn) { // if motion is on use motion now
+ atomicState?."motion${mySched}UseMotionSettings" = true
+ atomicState?."motion${mySched}TurnedOnDt" = getDtNow()
+ disableOverrideTemps()
+ LogAction("setTstatTempCheck: Enabled Use of Motion Settings for schedule ${mySched}", "info", true)
+ } else {
+ atomicState."${sLbl}MotionActiveDt" = null // this will clear isBtwn
+ atomicState?."motion${mySched}LastisBtwn" = false
+ LogAction("setTstatTempCheck: Motion Sensors not active at transition time to motion ON for schedule ${mySched}", "info", true)
+ }
+ }
+
+ def samemotion = previousBtwn == isBtwn ? true : false
+ def schedMatch = (samesched && samemotion) ? true : false
+
+ def strv = "Using "
+ if(schedMatch) { strv = "" }
+ LogAction("setTstatTempCheck: ${strv}Schedule ${mySched} (${previousSched}) use Motion settings: ${atomicState?."motion${mySched}UseMotionSettings"} | isBtwn: $isBtwn | previousBtwn: $previousBtwn | motionOn $motionOn", "trace", true)
+
+ if(tstat && !schedMatch) {
+ def hvacSettings = atomicState?."sched${mySched}restrictions"
+ def useMotion = atomicState?."motion${mySched}UseMotionSettings"
+
+ def newHvacMode = (!useMotion ? hvacSettings?.hvacm : (hvacSettings?.mhvacm ?: hvacSettings?.hvacm))
+ def tstatHvacMode = tstat?.currentThermostatMode?.toString()
+ if(newHvacMode && (newHvacMode.toString() != tstatHvacMode)) {
+ if(setTstatMode(schMotTstat, newHvacMode)) {
+ storeLastAction("Set ${tstat} Mode to ${strCapitalize(newHvacMode)}", getDtNow())
+ LogAction("setTstatTempCheck: Setting Thermostat Mode to '${strCapitalize(newHvacMode)}' on (${tstat})", "info", true)
+ } else { LogAction("setTstatTempCheck: Error Setting Thermostat Mode to '${strCapitalize(newHvacMode)}' on (${tstat})", "warn", true) }
+ }
+
+ // if remote sensor is on, let it handle temp changes (above took care of a mode change)
+ if(settings?.schMotRemoteSensor && isRemSenConfigured()) {
+ atomicState.lastSched = mySched
+ storeExecutionHistory((now() - execTime), "setTstatTempCheck")
+ return
+ }
+
+ def curMode = tstat?.currentThermostatMode?.toString()
+ def isModeOff = (curMode == "off") ? true : false
+ tstatHvacMode = curMode
+
+ def heatTemp = null
+ def coolTemp = null
+ def needChg = false
+
+ if(!isModeOff && atomicState?.schMotTstatCanHeat) {
+ def oldHeat = getTstatSetpoint(tstat, "heat")
+ heatTemp = getRemSenHeatSetTemp()
+ if(oldHeat != heatTemp) {
+ needChg = true
+ LogAction("setTstatTempCheck: Schedule Setpoint is '${heatTemp}' on (${tstat}) | Old Heat Setpoint: '${oldHeat}'", "info", false)
+ //storeLastAction("Set ${settings?.schMotTstat} Heat Setpoint to ${heatTemp}", getDtNow())
+ } else { heatTemp = null }
+ }
+
+ if(!isModeOff && atomicState?.schMotTstatCanCool) {
+ def oldCool = getTstatSetpoint(tstat, "cool")
+ coolTemp = getRemSenCoolSetTemp()
+ if(oldCool != coolTemp) {
+ needChg = true
+ LogAction("setTstatTempCheck: Schedule Cool Setpoint is '${coolTemp}' on (${tstat}) | Old Cool Setpoint: '${oldCool}'", "info", false)
+ //storeLastAction("Set ${settings?.schMotTstat} Cool Setpoint to ${coolTemp}", getDtNow())
+ } else { coolTemp = null }
+ }
+ if(needChg) {
+ if(setTstatAutoTemps(settings?.schMotTstat, coolTemp?.toDouble(), heatTemp?.toDouble())) {
+ //LogAction("setTstatTempCheck: [Temp Change | newHvacMode: $newHvacMode | tstatHvacMode: $tstatHvacMode | heatTemp: $heatTemp | coolTemp: $coolTemp ]", "info", true)
+ storeLastAction("Set ${tstat} Cool Setpoint to ${coolTemp} Set Heat Setpoint to ${heatTemp}", getDtNow())
+ } else {
+ LogAction("setTstatTempCheck: Thermostat Set ERROR [ newHvacMode: $newHvacMode | tstatHvacMode: $tstatHvacMode | heatTemp: $heatTemp | coolTemp: $coolTemp ]", "info", true)
+ }
+ }
+ }
+ }
+ atomicState.lastSched = mySched
+ storeExecutionHistory((now() - execTime), "setTstatTempCheck")
+ } catch (ex) {
+ log.error "setTstatTempCheck Exception:", ex
+ parent?.sendExceptionData(ex, "setTstatTempCheck", true, getAutoType())
+ }
+}
+
+/********************************************************************************
+| MASTER AUTOMATION FOR THERMOSTATS |
+*********************************************************************************/
+def schMotPrefix() { return "schMot" }
+
+def schMotModePage() {
+ //def pName = schMotPrefix()
+ dynamicPage(name: "schMotModePage", title: "Thermostat Automation", uninstall: false) {
+ def dupTstat
+ def dupTstat1
+ def dupTstat2
+ def dupTstat3
+ def tStatPhys
+ def tempScale = getTemperatureScale()
+ def tempScaleStr = "°${tempScale}"
+ section("Configure your Thermostat") {
+ input name: "schMotTstat", type: "capability.thermostat", title: "Select your Thermostat?", multiple: false, submitOnChange: true, required: true, image: getAppImg("thermostat_icon.png")
+ def tstat = settings?.schMotTstat
+ def tstatMir = settings?.schMotTstatMir
+ if(tstat) {
+ getTstatCapabilities(tstat, schMotPrefix())
+ def canHeat = atomicState?.schMotTstatCanHeat
+ def canCool = atomicState?.schMotTstatCanCool
+ tStatPhys = tstat?.currentNestType == "physical" ? true : false
+
+ def str = ""
+ def reqSenHeatSetPoint = getRemSenHeatSetTemp()
+ def reqSenCoolSetPoint = getRemSenCoolSetTemp()
+ def curZoneTemp = getRemoteSenTemp()
+ def tempSrcStr = (getCurrentSchedule() && atomicState?.remoteTempSourceStr == "Schedule") ? "Schedule ${getCurrentSchedule()} (${"${getSchedLbl(getCurrentSchedule())}" ?: "Not Found"})" : "(${atomicState?.remoteTempSourceStr})"
+
+ str += tempSrcStr ? "Zone Status:\n• Temp Source:${tempSrcStr?.toString().length() > 15 ? "\n └" : ""} ${tempSrcStr}" : ""
+ str += curZoneTemp ? "\n• Temperature: (${curZoneTemp}°${getTemperatureScale()})" : ""
+
+ def hstr = canHeat ? "H: ${reqSenHeatSetPoint}°${getTemperatureScale()}" : ""
+ def cstr = canHeat && canCool ? "/" : ""
+ cstr += canCool ? "C: ${reqSenCoolSetPoint}°${getTemperatureScale()}" : ""
+ 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 ? ("${tstat?.currentThermostatOperatingState.toString().capitalize()}/${tstat?.currentThermostatMode.toString().capitalize()}") : "unknown"})"
+ str += (atomicState?.schMotTstatHasFan) ? "\n• FanMode: (${tstat?.currentThermostatFanMode.toString().capitalize()})" : "\n• No Fan on HVAC system"
+ str += "\n• Presence: (${getTstatPresence(tstat).toString().capitalize()})"
+ def safetyTemps = getSafetyTemps(tstat)
+ str += safetyTemps ? "\n• Safefy Temps: \n └ Min: ${safetyTemps.min}°${getTemperatureScale()}/Max: ${safetyTemps.max}${tempScaleStr}" : ""
+ str += "\n• Virtual: (${tstat?.currentNestType.toString() == "virtual" ? "True" : "False"})"
+ paragraph "${str}", title: "${tstat.displayName} Zone Status", state: (str != "" ? "complete" : null), image: getAppImg("info_icon2.png")
+
+ 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 "ERROR:\nThe Virtual version of the Primary Thermostat was found in Mirror Thermostat List!!!\nPlease Correct to Proceed...", required: true, state: null, image: getAppImg("error_icon.png")
+ }
+ } 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 "ERROR:\nThe Virtual version of the Primary Thermostat was found in Mirror Thermostat List!!!\nPlease Correct to Proceed...", required: true, state: null, image: getAppImg("error_icon.png")
+ }
+ }
+ dupTstat3 = checkThermostatDupe(tstat, tstatMir) // make sure thermostat is not in mirror list
+ dupTstat = dupTstat1 || dupTstat2 || dupTstat3
+
+ if(dupTstat) {
+ paragraph "ERROR:\nThe Primary Thermostat was also found in the Mirror Thermostat List!!!\nPlease Correct to Proceed...", required: true, state: null, image: getAppImg("error_icon.png")
+ }
+ if(!tStatPhys) {
+ }
+ input "schMotTstatMir", "capability.thermostat", title: "Mirror Changes to these Thermostats", multiple: true, submitOnChange: true, required: false, image: getAppImg("thermostat_icon.png")
+ if(tstatMir && !dupTstat) {
+ tstatMir?.each { t ->
+ paragraph "Thermostat Temp: ${getDeviceTemp(t)}${tempScaleStr}", image: " "
+ }
+ }
+ }
+ }
+
+ if(settings?.schMotTstat && !dupTstat) {
+ updateScheduleStateMap()
+ section {
+ paragraph "The options below allow you to configure your thermostat with automations that will help you save energy and keep your home feeling more comfortable", title: "Choose Automations:", required: false
+ }
+
+ section("Schedule Automation:") {
+ def actSch = atomicState?.activeSchedData?.size()
+ def tDesc = (isTstatSchedConfigured() || atomicState?.activeSchedData?.size()) ? "Tap to Modify Schedules..." : null
+ href "tstatConfigAutoPage", title: "Use Schedules to adjust Temp Setpoints and HVAC mode?", description: (tDesc != null ? tDesc : ""), params: ["configType":"tstatSch"], state: (tDesc != null ? "complete" : ""), image: getAppImg("schedule_icon.png")
+ 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", title: "", description: "${schDesc}\n\nTap to Modify this Schedule...", params: ["sNum":schNum], state: (schInUse ? "complete" : "")
+ }
+ }
+ }
+ }
+ }
+
+ section("Fan Control:") {
+ def desc = ""
+ def titStr = "Run External Fan while HVAC is Operating"
+ if(atomicState?.schMotTstatHasFan) { titStr += " or Use HVAC Fan for Circulation" }
+ input (name: "schMotOperateFan", type: "bool", title: "${titStr}?", description: desc, required: false, defaultValue: false, submitOnChange: true, image: getAppImg("fan_control_icon.png"))
+ if(settings?.schMotOperateFan) {
+ def fanCtrlDescStr = ""
+ //fanCtrlDescStr += (atomicState?.schMotTstatHasFan) ? "\n • Current Fan Mode: (${schMotTstat?.currentThermostatFanMode.toString().capitalize()})" : ""
+ fanCtrlDescStr += getFanSwitchDesc() ? "${getFanSwitchDesc()}" : ""
+ def fanCtrlDesc = isFanCtrlConfigured() ? "${fanCtrlDescStr}\n\nTap to Modify..." : null
+ href "tstatConfigAutoPage", title: "Fan Control Config...", description: fanCtrlDesc ?: "Not Configured...", params: ["configType":"fanCtrl"], state: (fanCtrlDesc ? "complete" : null),
+ required: true, image: getAppImg("configure_icon.png")
+ }
+ }
+
+ section("Remote Sensor:") {
+ def desc = ""
+ input (name: "schMotRemoteSensor", type: "bool", title: "Use Alternate Temp Sensors Control Zone temperature?", description: desc, required: false, defaultValue: false, submitOnChange: true,
+ image: getAppImg("remote_sensor_icon.png"))
+ if(settings?.schMotRemoteSensor) {
+ def 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})") : ""
+
+ def hstr = remSenHeatTempsReq() ? "H: ${settings?.remSenDayHeatTemp ?: 0}${tempScaleStr}" : ""
+ def cstr = remSenHeatTempsReq() && remSenCoolTempsReq() ? "/" : ""
+ cstr += remSenCoolTempsReq() ? "C: ${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
+ def dayModeDesc = ""
+ dayModeDesc += settings?.remSensorDay ? "\n\nDefault Sensor${settings?.remSensorDay?.size() > 1 ? "s" : ""}:" : ""
+ //dayModeDesc += settings?.remSensorDay ? "\n ${settings.remSensorDay}" : ""
+ def rCnt = settings?.remSensorDay?.size()
+ settings?.remSensorDay?.each { t ->
+ dayModeDesc += "\n ├ ${t?.label}: ${(t?.label.length() > 10) ? "\n │ └ " : ""}(${getDeviceTemp(t)}${tempScaleStr})"
+ }
+ dayModeDesc += settings?.remSensorDay ? "\n └ Temp${(settings?.remSensorDay?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings?.remSensorDay)}${tempScaleStr})" : ""
+ //dayModeDesc += settings?.remSensorDay ? "\n • Temp${(settings?.remSensorDay?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings?.remSensorDay)}${tempScaleStr})" : ""
+ remSenDescStr += settings?.remSensorDay ? "${dayModeDesc}" : ""
+
+ def remSenDesc = isRemSenConfigured() ? "${remSenDescStr}\n\nTap to Modify..." : null
+ href "tstatConfigAutoPage", title: "Remote Sensor Config", description: remSenDesc ?: "Not Configured...", params: ["configType":"remSen"], required: true, state: (remSenDesc ? "complete" : null),
+ image: getAppImg("configure_icon.png")
+ }
+ }
+
+ section("Leak Detection:") {
+ def desc = ""
+ input (name: "schMotWaterOff", type: "bool", title: "Turn Off if Water Leak is detected?", description: desc, required: false, defaultValue: false, submitOnChange: true, image: getAppImg("leak_icon.png"))
+ if(settings?.schMotWaterOff) {
+ def leakDesc = ""
+ leakDesc += (settings?.leakWatSensors && leakWatSensorsDesc()) ? "${leakWatSensorsDesc()}" : ""
+ leakDesc += (settings?.leakWatSensors) ? "\n\nSettings:" : ""
+ leakDesc += settings?.leakWatOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings?.leakWatOnDelay)})" : ""
+ leakDesc += "\n • Last Mode: (${atomicState?.leakWatRestoreMode ? atomicState?.leakWatRestoreMode.toString().capitalize() : "Not Set"})"
+ leakDesc += (settings?.leakWatModes || settings?.leakWatDays || (settings?.leakWatStartTime && settings?.leakWatStopTime)) ?
+ "\n • Evaluation Allowed: (${autoScheduleOk(leakWatPrefix()) ? "ON" : "OFF"})" : ""
+ leakDesc += getNotifConfigDesc(leakWatPrefix()) ? "\n\n${getNotifConfigDesc(leakWatPrefix())}" : ""
+ leakDesc += (settings?.leakWatSensors) ? "\n\nTap to Modify..." : ""
+ def leakWatDesc = isLeakWatConfigured() ? "${leakDesc}" : null
+ href "tstatConfigAutoPage", title: "Leak Sensor Automation...", description: leakWatDesc ?: "Tap to Configure...", params: ["configType":"leakWat"], required: true, state: (leakWatDesc ? "complete" : null),
+ image: getAppImg("configure_icon.png")
+ }
+ }
+
+ section("Contact Automation:") {
+ def desc = ""
+ input (name: "schMotContactOff", type: "bool", title: "Turn Off if Door/Window Contact Open?", description: desc, required: false, defaultValue: false, submitOnChange: true, image: getAppImg("open_window.png"))
+ if(settings?.schMotContactOff) {
+ def conDesc = ""
+ conDesc += (settings?.conWatContacts && conWatContactDesc()) ? "${conWatContactDesc()}" : ""
+ conDesc += settings?.conWatContacts ? "\n\nSettings:" : ""
+ conDesc += settings?.conWatOffDelay ? "\n • Off 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 += "\n • Last Mode: (${atomicState?.conWatRestoreMode ? atomicState?.conWatRestoreMode.toString().capitalize() : "Not Set"})"
+ conDesc += (settings?."${conWatPrefix()}Modes" || settings?."${conWatPrefix()}Days" || (settings?."${conWatPrefix()}StartTime" && settings?."${conWatPrefix()}StopTime")) ?
+ "\n • Evaluation Allowed: (${autoScheduleOk(conWatPrefix()) ? "ON" : "OFF"})" : ""
+ conDesc += getNotifConfigDesc(conWatPrefix()) ? "\n\n${getNotifConfigDesc(conWatPrefix())}" : ""
+ conDesc += (settings?.conWatContacts) ? "\n\nTap to Modify..." : ""
+ def conWatDesc = isConWatConfigured() ? "${conDesc}" : null
+ href "tstatConfigAutoPage", title: "Contact Sensors Config...", description: conWatDesc ?: "Tap to Configure...", params: ["configType":"conWat"], required: true, state: (conWatDesc ? "complete" : null),
+ image: getAppImg("configure_icon.png")
+ }
+ }
+
+ section("External Temp:") {
+ def desc = ""
+ input (name: "schMotExternalTempOff", type: "bool", title: "Turn Off if External Temp is near comfort settings?", description: desc, required: false, defaultValue: false, submitOnChange: true, image: getAppImg("external_temp_icon.png"))
+ if(settings?.schMotExternalTempOff) {
+ def extDesc = ""
+ 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 • Threshold: (${settings?.extTmpDiffVal}${tempScaleStr})" : ""
+ extDesc += settings?.extTmpOffDelay ? "\n • Off Delay: (${getEnumValue(longTimeSecEnum(), settings?.extTmpOffDelay)})" : ""
+ extDesc += settings?.extTmpOnDelay ? "\n • On Delay: (${getEnumValue(longTimeSecEnum(), settings?.extTmpOnDelay)})" : ""
+ extDesc += "\n • Last Mode: (${atomicState?.extTmpRestoreMode ? atomicState?.extTmpRestoreMode.toString().capitalize() : "Not Set"})"
+ extDesc += (settings?."${extTmpPrefix()}Modes" || settings?."${extTmpPrefix()}Days" || (settings?."${extTmpPrefix()}StartTime" && settings?."${extTmpPrefix()}StopTime")) ?
+ "\n • Evaluation Allowed: (${autoScheduleOk(extTmpPrefix()) ? "ON" : "OFF"})" : ""
+ extDesc += getNotifConfigDesc(extTmpPrefix()) ? "\n\n${getNotifConfigDesc(extTmpPrefix())}" : ""
+ extDesc += ((settings?.extTmpTempSensor || settings?.extTmpUseWeather) ) ? "\n\nTap to Modify..." : ""
+ def extTmpDesc = isExtTmpConfigured() ? "${extDesc}" : null
+ href "tstatConfigAutoPage", title: "External Temps Config...", description: extTmpDesc ?: "Tap to Configure...", params: ["configType":"extTmp"], required: true, state: (extTmpDesc ? "complete" : null),
+ image: getAppImg("configure_icon.png")
+ }
+ }
+
+ section("Settings:") {
+ input "schMotWaitVal", "enum", title: "Minimum Wait Time between Evaluations?", required: false, defaultValue: 60, metadata: [values:[30:"30 Seconds", 60:"60 Seconds"]], image: getAppImg("delay_time_icon.png")
+ }
+ }
+ if(atomicState?.showHelp) {
+ section("Help:") {
+ href url:"${getAutoHelpPageUrl()}", style:"embedded", required:false, title:"Help and Instructions...", description:"", image: getAppImg("info.png")
+ }
+ }
+ }
+}
+
+def getSchedLbl(num) {
+ def result = ""
+ if(num) {
+ def schData = atomicState?.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 = atomicState?.activeSchedData
+ schData?.each { sch ->
+ //log.debug "sch: $sch"
+ if(num?.toInteger() == sch?.key.toInteger()) {
+ //log.debug "Data:(${sch?.value})"
+ resData = sch?.value
+ }
+ }
+ return resData != [:] ? resData : null
+}
+
+/* NOTE
+ Shedule 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 tstatConfigAutoPage(params) {
+ def configType = params.configType
+ if(params?.configType) {
+ atomicState.tempTstatConfigPageData = params; configType = params?.configType;
+ } else { configType = atomicState?.tempTstatConfigPageData?.configType }
+ def pName = ""
+ def pTitle = ""
+ def pDesc = null
+ switch(configType) {
+ case "tstatSch":
+ pName = schMotPrefix()
+ pTitle = "Thermostat Schedule Automation"
+ pDesc = "Configure Schedules and Setpoints"
+ break
+ case "fanCtrl":
+ pName = fanCtrlPrefix()
+ 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 "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()
+ def tempScaleStr = "°${tempScale}"
+ def tStatName = tstat?.displayName.toString()
+ def tStatHeatSp = getTstatSetpoint(tstat, "heat")
+ def tStatCoolSp = getTstatSetpoint(tstat, "cool")
+ def tStatMode = tstat?.currentThermostatMode
+ def tStatTemp = "${getDeviceTemp(tstat)}${tempScaleStr}"
+ def canHeat = atomicState?.schMotTstatCanHeat
+ def canCool = atomicState?.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"]
+ }
+ 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 {
+ def str = ""
+ str += "• Temperature: (${tStatTemp})"
+ str += "\n• Setpoints: (H: ${canHeat ? "${tStatHeatSp}${tempScaleStr}" : "NA"}/C: ${canCool ? "${tStatCoolSp}${tempScaleStr}" : "NA"})"
+ paragraph title: "${tStatName}\nSchedules and Setpoints:", "${str}", state: "complete", image: getAppImg("info_icon2.png")
+ }
+ showUpdateSchedule(null, hidestr)
+ }
+
+ if(configType == "fanCtrl") {
+ def reqinp = !(settings["schMotCirculateTstatFan"] || settings["${pName}FanSwitches"])
+ section("Control Fans/Switches based on your Thermostat\n(3-Speed Fans Supported)") {
+ input "${pName}FanSwitches", "capability.switch", title: "Select Fan Switches?", required: reqinp, submitOnChange: true, multiple: true,
+ image: getAppImg("fan_ventilation_icon.png")
+ if(settings?."${pName}FanSwitches") {
+ paragraph "${getFanSwitchDesc(false)}", state: getFanSwitchDesc() ? "complete" : null, image: getAppImg("blank_icon.png")
+ }
+ }
+ if(settings["${pName}FanSwitches"]) {
+ section("Fan Event Triggers") {
+ paragraph "Triggers are evaluated when your Thermostat sends an operating event. Depending on your configured Poll time it may take 1 minute or more for your fan to switch on.",
+ title: "What are these triggers?", image: getAppImg("instruct_icon.png")
+ input "${pName}FanSwitchTriggerType", "enum", title: "Control Switches When?", defaultValue: 1, metadata: [values:switchRunEnum()],
+ submitOnChange: true, image: getAppImg("${settings?."${pName}FanSwitchTriggerType" == 1 ? "thermostat" : "home_fan"}_icon.png")
+ input "${pName}FanSwitchHvacModeFilter", "enum", title: "Thermostat Mode Triggers?", defaultValue: "any", metadata: [values:fanModeTrigEnum()],
+ submitOnChange: true, image: getAppImg("mode_icon.png")
+ }
+ if(getFanSwitchesSpdChk()) {
+ section("Fan Speed Options") {
+ input "${pName}FanSwitchSpeedCtrl", "bool", title: "Enable Speed Control?", defaultValue: true, submitOnChange: true, image: getAppImg("speed_knob_icon.png")
+ if(settings["${pName}FanSwitchSpeedCtrl"]) {
+ paragraph "These threshold settings allow you to configure the speed of the fan based on it's closeness to the desired temp", title: "What do these values mean?"
+ input "${pName}FanSwitchLowSpeed", "decimal", title: "Low Speed Threshold (${tempScaleStr})", required: true, defaultValue: 1.0, submitOnChange: true, image: getAppImg("fan_low_speed.png")
+ input "${pName}FanSwitchMedSpeed", "decimal", title: "Medium Speed Threshold (${tempScaleStr})", required: true, defaultValue: 2.0, submitOnChange: true, image: getAppImg("fan_med_speed.png")
+ input "${pName}FanSwitchHighSpeed", "decimal", title: "High Speed Threshold (${tempScaleStr})", required: true, defaultValue: 4.0, submitOnChange: true, image: getAppImg("fan_high_speed.png")
+ }
+ }
+ }
+ }
+
+ if(atomicState?.schMotTstatHasFan) {
+ section("Fan Circulation:") {
+ def desc = ""
+ input (name: "schMotCirculateTstatFan", type: "bool", title: "Run HVAC Fan for Circulation?", description: desc, required: reqinp, defaultValue: false, submitOnChange: true, image: getAppImg("fan_circulation_icon.png"))
+ if(settings?.schMotCirculateTstatFan) {
+ input("schMotFanRuleType", "enum", title: "(Rule) Action Type", options: remSenRuleEnum("fan"), required: true, image: getAppImg("rule_icon.png"))
+ }
+ }
+ }
+ if(settings?."${pName}FanSwitches") {
+ def schTitle
+ if(!atomicState?.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: "Enable/Modify Schedules...", description: pageDesc, params: ["sData":["hideStr":"${hideStr}"]], state: (pageDesc ? "complete" : null), image: getAppImg("schedule_icon.png")
+ }
+ }
+ }
+
+ def cannotLock
+ def defHeat
+ def defCool
+ if(!getMyLockId()) {
+ setMyLockId(app.id)
+ }
+ if(atomicState?.remSenTstat) {
+ if(tstat.deviceNetworkId != atomicState?.remSenTstat) {
+ parent?.addRemoveVthermostat(atomicState.remSenTstat, false, getMyLockId())
+ if( parent?.remSenUnlock(atomicState.remSenTstat, getMyLockId()) ) { // attempt unlock old ID
+ atomicState.oldremSenTstat = atomicState?.remSenTstat
+ atomicState?.remSenTstat = null
+ }
+ }
+ }
+ if(settings?.schMotRemoteSensor) {
+ if( parent?.remSenLock(tstat?.deviceNetworkId, getMyLockId()) ) { // lock new ID
+ atomicState?.remSenTstat = tstat?.deviceNetworkId
+ cannotLock = false
+ } else { cannotLock = true }
+ }
+
+ if(configType == "remSen") {
+ // can check if any vthermostat is owned by us, and delete it
+ // have issue request for vthermostat is still on as input below
+
+ if(cannotLock) {
+ section("") {
+ paragraph "Cannot Lock thermostat for remote sensor - thermostat may already be in use. Please Correct...", required: true, state: null, image: getAppImg("error_icon.png")
+ }
+ }
+
+ if(!cannotLock) {
+ section("Select the Allowed (Rule) Action Type:") {
+ if(!settings?.remSenRuleType) {
+ paragraph "They determine the actions taken when the temperature threshold is reached, to balance temperatures...", title: "What are Rule Actions?", image: getAppImg("instruct_icon.png")
+ }
+ input(name: "remSenRuleType", type: "enum", title: "(Rule) Action Type", options: remSenRuleEnum("heatcool"), required: true, submitOnChange: true, image: getAppImg("rule_icon.png"))
+ }
+ if(settings?.remSenRuleType) {
+ def senLblStr = "Default"
+ section("Choose Sensor(s) to use for Temperature instead of the Thermostats:") {
+ def daySenReq = (!settings?.remSensorDay) ? true : false
+ input "remSensorDay", "capability.temperatureMeasurement", title: "${senLblStr} Temp Sensor(s)", submitOnChange: true, required: daySenReq,
+ multiple: true, image: getAppImg("temperature_icon.png")
+ if(settings?.remSensorDay) {
+ def tmpVal = "Temp${(settings?.remSensorDay?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings?.remSensorDay)}${tempScaleStr})"
+ if(settings?.remSensorDay.size() > 1) {
+ href "remSenShowTempsPage", title: "View ${senLblStr} Sensor Temps...", description: "${tmpVal}", state: "complete", image: getAppImg("blank_icon.png")
+ //paragraph "Multiple temp sensors will return the average of those sensors.", image: getAppImg("i_icon.png")
+ } else { paragraph "${tmpVal}", title: "Remote Sensor Temp", state: "complete", image: getAppImg("instruct_icon.png") }
+ }
+ }
+ if(settings?.remSensorDay) {
+ section("Desired Setpoints...") {
+ paragraph "These temps are used when remote sensors are enabled and no schedules are created or active", title: "What are these temps for?", image: getAppImg("info_icon2.png")
+ 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
+ }
+ def tempStr = "Default "
+ if(remSenHeatTempsReq()) {
+ defHeat = getGlobalDesiredHeatTemp()
+ defHeat = defHeat ?: tStatHeatSp
+ input "remSenDayHeatTemp", "decimal", title: "Desired ${tempStr}Heat Temp (${tempScaleStr})", description: "Range within ${tempRangeValues()}", range: tempRangeValues(),
+ required: true, defaultValue: defHeat, image: getAppImg("heat_icon.png")
+ }
+ if(remSenCoolTempsReq()) {
+ defCool = getGlobalDesiredCoolTemp()
+ defCool = defCool ?: tStatCoolSp
+ input "remSenDayCoolTemp", "decimal", title: "Desired ${tempStr}Cool Temp (${tempScaleStr})", description: "Range within ${tempRangeValues()}", range: tempRangeValues(),
+ required: true, defaultValue: defCool, image: getAppImg("cool_icon.png")
+ }
+ }
+ section("Remote Sensor Settings...") {
+ paragraph "Is the temp difference trigger for Action Type.", title: "What is the Action Threshold Temp?", image: getAppImg("instruct_icon.png")
+ input "remSenTempDiffDegrees", "decimal", title: "Action Threshold Temp (${tempScaleStr})", required: true, defaultValue: 2.0, image: getAppImg("temp_icon.png")
+ if(settings?.remSenRuleType != "Circ") {
+ paragraph "Is the amount the thermostat temp is adjusted +/- to enable the HVAC system.", title: "What are Temp Increments?", image: getAppImg("instruct_icon.png")
+ input "remSenTstatTempChgVal", "decimal", title: "Change Temp Increments (${tempScaleStr})", required: true, defaultValue: 5.0, image: getAppImg("temp_icon.png")
+ }
+ }
+
+ section("(Optional) Create a Virtual Nest Thermostat:") {
+ input(name: "vthermostat", type: "bool", title:"Create Virtual Nest Thermostat", required: false, submitOnChange: true, image: getAppImg("thermostat_icon.png"))
+ if(settings?.vthermostat != null && !parent?.addRemoveVthermostat(tstat.deviceNetworkId, vthermostat, getMyLockId())) {
+ paragraph "Unable to ${(vthermostat ? "enable" : "disable")} Virtual Thermostat!!!. Please Correct...", image: getAppImg("error_icon.png")
+ }
+ }
+
+ def schTitle
+ if(!atomicState?.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: "Enable/Modify Schedules...", description: pageDesc, params: ["sData":["hideStr":"${hideStr}"]], state: (pageDesc ? "complete" : null), image: getAppImg("schedule_icon.png")
+ }
+ }
+ }
+ }
+ }
+
+ if(configType == "leakWat") {
+ section("When Leak is Detected, Turn Off this Thermostat") {
+ def req = (settings?.leakWatSensors || setting?.schMotTstat) ? true : false
+ input name: "leakWatSensors", type: "capability.waterSensor", title: "Which Leak Sensor(s)?", multiple: true, submitOnChange: true, required: req,
+ image: getAppImg("water_icon.png")
+ if(settings?.leakWatSensors) {
+ paragraph "${leakWatSensorsDesc()}", state: "complete", image: getAppImg("instruct_icon.png")
+ }
+ }
+ if(settings?.leakWatSensors) {
+ section("Restore On when Dry:") {
+ input name: "leakWatOnDelay", type: "enum", title: "Delay Restore (in minutes)", defaultValue: 300, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ }
+ section("Notifications:") {
+ def pageDesc = getNotifConfigDesc(pName)
+ href "setNotificationPage", title: "Configured Alerts...", description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true],
+ state: (pageDesc ? "complete" : null), image: getAppImg("notification_icon.png")
+ }
+ }
+ }
+
+ if(configType == "conWat") {
+ section("When these Contacts are open, Turn Off this Thermostat") {
+ def req = !settings?.conWatContacts ? true : false
+ input name: "conWatContacts", type: "capability.contactSensor", title: "Which Contact(s)?", multiple: true, submitOnChange: true, required: req,
+ image: getAppImg("contact_icon.png")
+ if(settings?.conWatContacts) {
+ def str = ""
+ str += settings?.conWatContacts ? "${conWatContactDesc()}\n" : ""
+ paragraph "${str}", state: (str != "" ? "complete" : null), image: getAppImg("instruct_icon.png")
+ }
+ }
+ if(settings?.conWatContacts) {
+ section("Trigger Actions:") {
+ // TODO can these delays be set to 0?
+ input name: "conWatOffDelay", type: "enum", title: "Delay Off (in Minutes)", defaultValue: 300, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+
+ input name: "conWatOnDelay", type: "enum", title: "Delay Restore (in Minutes)", defaultValue: 300, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ input name: "conWatRestoreDelayBetween", type: "enum", title: "Delay Between On/Off Cycles\n(Optional)", defaultValue: 600, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ }
+ section("Restoration Preferences (Optional):") {
+ // TODO can these delays be set to 0? to turn back off?
+ input "${pName}OffTimeout", "enum", title: "Auto Restore after...", defaultValue: 3600, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ if(!settings?."${pName}OffTimeout") { atomicState."${pName}timeOutScheduled" = false }
+ }
+
+ section(getDmtSectionDesc(conWatPrefix())) {
+ def pageDesc = getDayModeTimeDesc(pName)
+ href "setDayModeTimePage", title: "Configured Restrictions", description: pageDesc, params: ["pName": "${pName}"], state: (pageDesc ? "complete" : null),
+ image: getAppImg("cal_filter_icon.png")
+ }
+ section("Notifications:") {
+ def pageDesc = getNotifConfigDesc(pName)
+ href "setNotificationPage", title: "Configured Alerts...", description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true],
+ state: (pageDesc ? "complete" : null), image: getAppImg("notification_icon.png")
+ }
+ }
+ }
+
+ if(configType == "extTmp") {
+ section("Select the External Temps to Use:") {
+ if(!parent?.getWeatherDeviceInst()) {
+ 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: "Use Local Weather as External Sensor?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("weather_icon.png")
+ if(settings?.extTmpUseWeather){
+ if(atomicState?.curWeather == null) {
+ atomicState.NeedwUpd = true
+ getExtConditions()
+ }
+ def tmpVal = (tempScale == "C") ? atomicState?.curWeatherTemp_c : atomicState?.curWeatherTemp_f
+ def curDp = getExtTmpDewPoint()
+ paragraph "Local Weather:\n• ${atomicState?.curWeatherLoc}\n• Temp: (${tmpVal}${tempScaleStr})\n• Dewpoint: (${curDp}${tempScaleStr})", state: "complete", image: getAppImg("instruct_icon.png")
+ }
+ }
+ }
+ if(!settings?.extTmpUseWeather) {
+ atomicState.curWeather = null // force refresh of weather if toggled
+ def senReq = (!settings?.extTmpUseWeather && !settings?.extTmpTempSensor) ? true : false
+ input "extTmpTempSensor", "capability.temperatureMeasurement", title: "Select a Temp Sensor?", submitOnChange: true, multiple: false, required: senReq, image: getAppImg("temperature_icon.png")
+ if(settings?.extTmpTempSensor) {
+ def str = ""
+ str += settings?.extTmpTempSensor ? "Sensor Status:" : ""
+ str += settings?.extTmpTempSensor ? "\n└ Temp: (${settings?.extTmpTempSensor?.currentTemperature}${tempScaleStr})" : ""
+ paragraph "${str}", state: (str != "" ? "complete" : null), image: getAppImg("instruct_icon.png")
+ }
+ }
+ }
+ if(settings?.extTmpUseWeather || settings?.extTmpTempSensor) {
+ section("When the threshold Temp is Reached\nTurn Off the Thermostat...") {
+ input name: "extTmpDiffVal", type: "decimal", title: "When internal and external temp difference is within this many degrees (${tempScaleStr})?", defaultValue: 1.0, submitOnChange: true, required: true,
+ image: getAppImg("temp_icon.png")
+ }
+ section("Delay Values:") {
+ // TODO can these delays be set to 0?
+ input name: "extTmpOffDelay", type: "enum", title: "Delay Off (in minutes)", defaultValue: 300, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ input name: "extTmpOnDelay", type: "enum", title: "Delay Restore (in minutes)", defaultValue: 300, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ }
+ section("Restoration Preferences (Optional):") {
+ // TODO can these delays be set to 0? to turn back off?
+ input "${pName}OffTimeout", "enum", title: "Auto Restore after (Optional)", defaultValue: 43200, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ if(!settings?."${pName}OffTimeout") { atomicState."${pName}timeOutScheduled" = false }
+ }
+ section(getDmtSectionDesc(extTmpPrefix())) {
+ def pageDesc = getDayModeTimeDesc(pName)
+ href "setDayModeTimePage", title: "Configured Restrictions", description: pageDesc, params: ["pName": "${pName}"], state: (pageDesc ? "complete" : null),
+ image: getAppImg("cal_filter_icon.png")
+ }
+ section("Notifications:") {
+ def pageDesc = getNotifConfigDesc(pName)
+ href "setNotificationPage", title: "Configured Alerts...", description: pageDesc, params: ["pName":"${pName}", "allowSpeech":true, "allowAlarm":true, "showSchedule":true],
+ state: (pageDesc ? "complete" : null), image: getAppImg("notification_icon.png")
+ }
+ def schTitle
+ if(!atomicState?.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: "Enable/Modify Schedules...", description: pageDesc, params: ["sData":["hideStr":"${hideStr}"]], state: (pageDesc ? "complete" : null), image: getAppImg("schedule_icon.png")
+ }
+ }
+ }
+ }
+ }
+}
+
+def scheduleConfigPage(params) {
+ LogTrace("scheduleConfigPage... ($params)")
+ def sData = params?.sData
+ if(params?.sData) {
+ atomicState.tempSchPageData = params
+ sData = params?.sData
+ } else {
+ sData = atomicState?.tempSchPageData?.sData
+ }
+ dynamicPage(name: "scheduleConfigPage", title: "Thermostat Schedule Page", description: "Configure/View Schedules", uninstall: false) {
+ if(settings?.schMotTstat) {
+ def tstat = settings?.schMotTstat
+ def canHeat = atomicState?.schMotTstatCanHeat
+ def canCool = atomicState?.schMotTstatCanCool
+ def str = ""
+ def reqSenHeatSetPoint = getRemSenHeatSetTemp()
+ def reqSenCoolSetPoint = getRemSenCoolSetTemp()
+ def curZoneTemp = getRemoteSenTemp()
+ def tempSrcStr = atomicState?.remoteTempSourceStr
+ section {
+ str += "Zone Status:\n• Temp Source: (${tempSrcStr})\n• Temperature: (${curZoneTemp}°${getTemperatureScale()})"
+
+ def hstr = canHeat ? "H: ${reqSenHeatSetPoint}°${getTemperatureScale()}" : ""
+ def cstr = canHeat && canCool ? "/" : ""
+ cstr += canCool ? "C: ${reqSenCoolSetPoint}°${getTemperatureScale()}" : ""
+ str += "\n• Setpoints: (${hstr}${cstr})\n"
+
+ str += "\nThermostat Status:\n• Temperature: (${getDeviceTemp(tstat)}°${getTemperatureScale()})"
+ hstr = canHeat ? "H: ${getTstatSetpoint(tstat, "heat")}°${getTemperatureScale()}" : ""
+ cstr = canHeat && canCool ? "/" : ""
+ cstr += canCool ? "C: ${getTstatSetpoint(tstat, "cool")}°${getTemperatureScale()}" : ""
+ str += "\n• Setpoints: (${hstr}${cstr})"
+
+ str += "\n• Mode: (${tstat ? ("${tstat?.currentThermostatOperatingState.toString().capitalize()}/${tstat?.currentThermostatMode.toString().capitalize()}") : "unknown"})"
+ str += (atomicState?.schMotTstatHasFan) ? "\n• FanMode: (${tstat?.currentThermostatFanMode.toString().capitalize()})" : "\n• No Fan on HVAC system"
+ str += "\n• Presence: (${getTstatPresence(tstat).toString().capitalize()})"
+ paragraph title: "${tstat?.displayName}\nSchedules and Setpoints:", "${str}", state: "complete", image: getAppImg("info_icon2.png")
+ }
+ showUpdateSchedule(null,sData?.hideStr)
+ }
+ }
+}
+
+def schMotSchedulePage(params) {
+ LogTrace("schMotSchedulePage($params)")
+ def sNum = params?.sNum
+ if(params?.sNum) {
+ atomicState.tempMotSchPageData = params
+ sNum = params?.sNum
+ } else {
+ sNum = atomicState?.tempMotSchPageData?.sNum
+ }
+ dynamicPage(name: "schMotSchedulePage", title: "Edit Schedule Page", description: "Modify Schedules", uninstall: false) {
+ if(sNum) {
+ showUpdateSchedule(sNum)
+ }
+ }
+}
+
+def showUpdateSchedule(sNum=null,hideStr=null) {
+ updateScheduleStateMap()
+ def schedList = atomicState?.scheduleList // setting in initAutoApp adjust # of schedule slots
+ if(schedList == null) { atomicState.scheduleList = [ 1,2,3,4 ]; schedList = atomicState?.scheduleList }
+ 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 || scd == 1) ? false : true), "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 || scd == 1) ? false : true), "hideStr":hideStr])
+ }
+ }
+ }
+}
+
+def editSchedule(schedData) {
+ def cnt = schedData?.secData?.scd
+ LogAction("editSchedule (${schedData?.secData})", "trace", false)
+
+ def sLbl = "schMot_${cnt}_"
+ def canHeat = atomicState?.schMotTstatCanHeat
+ def canCool = atomicState?.schMotTstatCanCool
+ def tempScaleStr = "°${getTemperatureScale()}"
+ def act = settings["${sLbl}SchedActive"]
+ def actIcon = act ? "active" : "inactive"
+ def sectStr = schedData?.secData?.schName ? (act ? "Enabled" : "Disabled") : "Tap to Enable"
+ def titleStr = "Schedule ${schedData?.secData?.scd} (${sectStr})"
+
+
+ section(title: "\n${titleStr}\n ", hideable:schedData?.secData?.hideable, hidden: schedData?.secData?.hidden) {
+ input "${sLbl}SchedActive", "bool", title: "Schedule Enabled", description: (cnt == 1 && !settings?."${sLbl}SchedActive" ? "Enable to Edit Schedule..." : null), required: true,
+ defaultValue: false, submitOnChange: true, image: getAppImg("${actIcon}_icon.png")
+ if(act) {
+ input "${sLbl}name", "text", title: "Schedule Name", required: true, defaultValue: "Schedule ${cnt}", multiple: false, submitOnChange: true, image: getAppImg("name_tag_icon.png")
+ }
+ }
+ if(act) {
+ //if(settings?.schMotSetTstatTemp && !("tstatTemp" in hideStr)) {
+ section("(${schedData?.secData?.schName}) Setpoint Configuration:") {
+ paragraph "Configure Setpoints and HVAC modes that will be set when this Schedule is in use...", title: "Setpoints and Mode"
+ if(canHeat) {
+ input "${sLbl}HeatTemp", "decimal", title: "Heat Set Point (${tempScaleStr})", description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(),
+ submitOnChange: true, image: getAppImg("heat_icon.png")
+ }
+ if(canCool) {
+ input "${sLbl}CoolTemp", "decimal", title: "Cool Set Point (${tempScaleStr})", description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(),
+ submitOnChange: true, image: getAppImg("cool_icon.png")
+ }
+ input "${sLbl}HvacMode", "enum", title: "Set Hvac Mode:", required: false, description: "No change set", metadata: [values:tModeHvacEnum(canHeat,canCool)], multiple: false, image: getAppImg("hvac_mode_icon.png")
+ }
+
+ if(settings?.schMotRemoteSensor && !("remSen" in hideStr)) {
+ section("(${schedData?.secData?.schName}) Remote Sensor Options:") {
+ paragraph "Configure alternate Remote Temp sensors that are active with this schedule...", title: "Alternate Remote Sensors\n(Optional)"
+ input "${sLbl}remSensor", "capability.temperatureMeasurement", title: "Alternate Temp Sensors", description: "For Remote Sensor Automation", submitOnChange: true, required: false, multiple: true, image: getAppImg("temperature_icon.png")
+ if(settings?."${sLbl}remSensor" != null) {
+ def tmpVal = "Temp${(settings["${sLbl}remSensor"]?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings["${sLbl}remSensor"])}${tempScaleStr})"
+ paragraph "${tmpVal}", state: "complete", image: getAppImg("instruct_icon.png")
+ }
+ }
+ }
+ //if(!("motSen" in hideStr)) {
+ section("(${schedData?.secData?.schName}) Motion Sensor Setpoints:") {
+ paragraph "Activate alternate HVAC settings with Motion...", title: "(Optional)"
+ def mmot = settings["${sLbl}Motion"]
+ input "${sLbl}Motion", "capability.motionSensor", title: "Motion Sensors", description: "Select Sensors to Configure...", required: false, multiple: true, submitOnChange: true, image: getAppImg("motion_icon.png")
+ if(settings["${sLbl}Motion"]) {
+ paragraph " • Motion State: (${isMotionActive(mmot) ? "Active" : "Not Active"})", state: "complete", image: getAppImg("instruct_icon.png")
+ if(canHeat) {
+ input "${sLbl}MHeatTemp", "decimal", title: "Heat Setpoint with Motion(${tempScaleStr})", description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(), image: getAppImg("heat_icon.png")
+ }
+ if(canCool) {
+ input "${sLbl}MCoolTemp", "decimal", title: "Cool Setpoint with Motion (${tempScaleStr})", description: "Range within ${tempRangeValues()}", required: true, range: tempRangeValues(), image: getAppImg("cool_icon.png")
+ }
+ input "${sLbl}MHvacMode", "enum", title: "Set Hvac Mode with Motion:", required: false, description: "No change set", metadata: [values:tModeHvacEnum(canHeat,canCool)], multiple: false, image: getAppImg("hvac_mode_icon.png")
+ //input "${sLbl}MRestrictionMode", "mode", title: "Ignore in these modes", description: "Any location mode", required: false, multiple: true, image: getAppImg("mode_icon.png")
+ input "${sLbl}MDelayValOn", "enum", title: "Delay Motion Setting Changes", required: false, defaultValue: 60, metadata: [values:longTimeSecEnum()], multiple: false, image: getAppImg("delay_time_icon.png")
+ input "${sLbl}MDelayValOff", "enum", title: "Delay disabling Motion Settings", required: false, defaultValue: 1800, metadata: [values:longTimeSecEnum()], multiple: false, image: getAppImg("delay_time_icon.png")
+ }
+ }
+
+ section("(${schedData?.secData?.schName}) Schedule Restrictions:") {
+ paragraph "Restrict when this Schedule is in use...", title: "(Optional)"
+ input "${sLbl}restrictionMode", "mode", title: "Only execute in these modes", description: "Any location mode", required: false, multiple: true, image: getAppImg("mode_icon.png")
+ input "${sLbl}restrictionDOW", "enum", options: timeDayOfWeekOptions(), title: "Only execute on these days", description: "Any week day", required: false, multiple: true, image: getAppImg("day_calendar_icon2.png")
+ def timeFrom = settings["${sLbl}restrictionTimeFrom"]
+ def timeTo = settings["${sLbl}restrictionTimeTo"]
+ def showTime = (timeFrom || timeTo || settings?."${sLbl}restrictionTimeFromCustom" || settings?."${sLbl}restrictionTimeToCustom") ? true : false
+ input "${sLbl}restrictionTimeFrom", "enum", title: (timeFrom ? "Only execute if time is between" : "Only execute during this time"), options: timeComparisonOptionValues(), required: showTime, multiple: false, submitOnChange: true, image: getAppImg("start_time_icon.png")
+ if (showTime) {
+ if ((timeFrom && timeFrom.contains("custom")) || settings?."${sLbl}restrictionTimeFromCustom" != null) {
+ input "${sLbl}restrictionTimeFromCustom", "time", title: "Custom time", required: true, multiple: false
+ } else {
+ input "${sLbl}restrictionTimeFromOffset", "number", title: "Offset (+/- minutes)", range: "*..*", required: true, multiple: false, defaultValue: 0, image: getAppImg("offset_icon.png")
+ }
+ input "${sLbl}restrictionTimeTo", "enum", title: "And", options: timeComparisonOptionValues(), required: true, multiple: false, submitOnChange: true, image: getAppImg("stop_time_icon.png")
+ if ((timeTo && timeTo.contains("custom")) || settings?."${sLbl}restrictionTimeToCustom" != null) {
+ input "${sLbl}restrictionTimeToCustom", "time", title: "Custom time", required: true, multiple: false
+ } else {
+ input "${sLbl}restrictionTimeToOffset", "number", title: "Offset (+/- minutes)", range: "*..*", required: true, multiple: false, defaultValue: 0, image: getAppImg("offset_icon.png")
+ }
+ }
+ input "${sLbl}restrictionSwitchOn", "capability.switch", title: "Only execute when these switches are all on", description: "Always", required: false, multiple: true, image: getAppImg("switch_on_icon.png")
+ input "${sLbl}restrictionSwitchOff", "capability.switch", title: "Only execute when these switches are all off", description: "Always", required: false, multiple: true, image: getAppImg("switch_off_icon.png")
+ }
+ }
+}
+
+def getScheduleDesc(num = null) {
+ def result = [:]
+ def schedData = atomicState?.activeSchedData
+ def actSchedNum = getCurrentSchedule()
+ def tempScaleStr = "°${getTemperatureScale()}"
+ def schNum
+ def schData
+
+ def sCnt = 1
+ def sData = schedData
+ if(num) {
+ sData = schedData?.find { it?.key.toInteger() == num.toInteger() }
+ }
+ if(sData?.size()) {
+ sData?.sort().each { scd ->
+ def str = ""
+ schNum = scd?.key
+ schData = scd?.value
+ def sLbl = "schMot_${schNum}_"
+ def isRestrict = (schData?.m || schData?.tf || schData?.tfc || schData?.tfo || schData?.tt || schData?.ttc || schData?.tto || schData?.w || schData?.s1 || schData?.s0)
+ def isTimeRes = (schData?.tf || schData?.tfc || schData?.tfo || schData?.tt || schData?.ttc || schData?.tto)
+ def isDayRes = schData?.w
+ def isTemp = (schData?.ctemp || schData?.htemp || schData?.hvacm)
+ def isSw = (schData?.s1 || schData?.s0)
+ def isMot = schData?.m0
+ def isRemSen = schData?.sen0
+ def isFanEn = schData?.fan0
+ def resPreBar = isSw || isTemp ? "│" : " "
+ def tempPreBar = isMot || isRemSen ? "│" : " "
+ def motPreBar = isRemSen
+
+ str += schData?.lbl ? " • ${schData?.lbl}${(actSchedNum?.toInteger() == schNum?.toInteger()) ? " (In Use)" : " (Not In Use)"}" : ""
+
+ //restriction section
+ str += isRestrict ? "\n ${isSw || isTemp ? "├" : "└"} Restrictions:" : ""
+ def mLen = schData?.m ? schData?.m?.toString().length() : 0
+ def mStr = ""
+ if (mLen > 15) {
+ def mdSize = 1
+ schData?.m?.each { md ->
+ mStr += md ? "\n ${isSw || isTemp ? "│ ${(isDayRes || isTimeRes || isSw) ? "│" : " "}" : " "} ${mdSize < schData?.m.size() ? "├" : "└"} ${md.toString()}" : ""
+ mdSize = mdSize+1
+ }
+ } else {
+ mStr += schData?.m.toString()
+ }
+ str += schData?.m ? "\n ${resPreBar} ${(isTimeRes || schData?.w) ? "├" : "└"} Mode${schData?.m?.size() > 1 ? "s" : ""}:${isInMode(schData?.m) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
+ str += schData?.m ? "$mStr" : ""
+
+ def dayStr = getAbrevDay(schData?.w)
+ def timeDesc = getScheduleTimeDesc(schData?.tf, schData?.tfc, schData?.tfo, schData?.tt, schData?.ttc, schData?.tto, (isSw || isDayRes))
+ str += isTimeRes ? "\n │ ${isDayRes || isSw ? "├" : "└"} ${timeDesc}" : ""
+ str += isDayRes ? "\n │ ${schData?.s1 ? "├" : "└"} Days:${getSchRestrictDoWOk(schNum) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
+ str += isDayRes ? "\n │ ${isSw ? "│" :" "} └ ${dayStr}" : ""
+ str += schData?.s1 ? "\n │ ${schData?.s0 ? "├" : "└"} Switches On:${isSwitchOn(settings["${sLbl}restrictionSwitchOn"]) ? " (${okSym()})" : " (${notOkSym()})"}" : ""
+ str += schData?.s1 ? "\n │ ${schData?.s0 ? "│" : " "} └ (${schData?.s1.size()} Selected)" : ""
+ str += schData?.s0 ? "\n │ └ Switches Off:${!isSwitchOn(settings["${sLbl}restrictionSwitchOff"]) ? " (${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: (${schData?.ctemp}${tempScaleStr})" : ""
+ str += schData?.htemp ? "\n ${tempPreBar} ${schData?.hvacm ? "├" : "└"} Heat Setpoint: (${schData?.htemp}${tempScaleStr})" : ""
+ str += schData?.hvacm ? "\n ${tempPreBar} └ HVAC Mode: (${schData?.hvacm.toString().capitalize()})" : ""
+
+ //Motion Info
+ 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 += 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: (${schData?.mctemp}${tempScaleStr})" : ""
+ str += isMot && schData?.mhtemp ? "\n ${motPreBar ? "│" : " "} ${schData?.mdelayOn || schData?.mdelayOff ? "├" : "└"} Mot. Heat Setpoint: (${schData?.mhtemp}${tempScaleStr})" : ""
+ str += isMot && schData?.mhvacm ? "\n ${motPreBar ? "│" : " "} ${(schData?.mdelayOn || schData?.mdelayOff) ? "├" : "└"} Mot. HVAC Mode: (${schData?.mhvacm.toString().capitalize()})" : ""
+ 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 ? "${isRemSen || isRestrict ? "\n │\n" : "\n"} └ Alternate Remote Sensor:" : ""
+ //str += isRemSen ? "\n ├ Temp Sensors: (${schData?.sen0.size()})" : ""
+ settings["${sLbl}remSensor"]?.each { t ->
+ str += "\n ├ ${t?.label}: ${(t?.label.length() > 10) ? "\n │ └ " : ""}(${getDeviceTemp(t)}°${getTemperatureScale()})"
+ }
+ str += isRemSen && schData?.sen0 ? "\n └ Temp${(settings["${sLbl}remSensor"]?.size() > 1) ? " (avg):" : ":"} (${getDeviceTempAvg(settings["${sLbl}remSensor"])}${tempScaleStr})" : ""
+ //log.debug "str: \n$str"
+ if(str != "") { result[schNum] = str }
+ }
+ }
+ return (result?.size() >= 1) ? result : null
+}
+
+def getScheduleTimeDesc(timeFrom, timeFromCustom, timeFromOffset, timeTo, timeToCustom, timeToOffset, showPreLine=false) {
+ def tf = new SimpleDateFormat("h:mm a")
+ tf.setTimeZone(location?.timeZone)
+ def spl = showPreLine == true ? "│" : ""
+ def timeToVal = null
+ def timeFromVal = null
+ def 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
+ def 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
+ def res = "Midnight: " + formatTime(convertDateToUnixTime((rightNow - rightNow.mod(86400000)) + offSet))
+ if(i == 0) { timeFromVal = res }
+ else { timeToVal = res }
+ break
+ }
+ i += 1
+ }
+ }
+ def timeOk = ((timeFrom && (timeFromCustom || timeFromOffset) && timeTo && (timeToCustom || timeToOffset)) && checkTimeCondition(timeFrom, timeFromCustom, timeFromOffset, timeTo, timeToCustom, timeToOffset)) ? true : false
+ def out = ""
+ out += (timeFromVal && timeToVal && ((timeFromVal.length() + timeToVal.length()) > 15)) ? "Time:${timeOk ? " (${okSym()})" : " (${notOkSym()})"}\n │ ${spl} ├ $timeFromVal\n │ ${spl} ├ to\n │ ${spl} └ $timeToVal" : "Time: $timeFromVal to $timeToVal"
+ return out
+}
+
+def okSym() {
+ return "✓"// ☑"
+}
+def notOkSym() {
+ return "✘"
+}
+
+
+def getRemSenTempSrc() {
+ return atomicState?.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
+ def numAct = 0
+ actSchedules = [:]
+ atomicState?.scheduleList?.each { scdNum ->
+ def sLbl = "schMot_${scdNum}_"
+ def newScd = []
+ def schActive = settings["${sLbl}SchedActive"]
+
+ if(schActive) {
+ actSchedules?."${scdNum}" = [:]
+ newScd = cleanUpMap([
+ lbl: settings["${sLbl}name"],
+ m: settings["${sLbl}restrictionMode"],
+ tf: settings["${sLbl}restrictionTimeFrom"],
+ tfc: settings["${sLbl}restrictionTimeFromCustom"],
+ tfo: settings["${sLbl}restrictionTimeFromOffset"],
+ tt: settings["${sLbl}restrictionTimeTo"],
+ ttc: settings["${sLbl}restrictionTimeToCustom"],
+ tto: settings["${sLbl}restrictionTimeToOffset"],
+ w: settings["${sLbl}restrictionDOW"],
+ s1: deviceInputToList(settings["${sLbl}restrictionSwitchOn"]),
+ s0: deviceInputToList(settings["${sLbl}restrictionSwitchOff"]),
+ ctemp: roundTemp(settings["${sLbl}CoolTemp"]),
+ htemp: roundTemp(settings["${sLbl}HeatTemp"]),
+ hvacm: settings["${sLbl}HvacMode"],
+ sen0: settings["schMotRemoteSensor"] ? deviceInputToList(settings["${sLbl}remSensor"]) : 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,
+ 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", true)
+ }
+ }
+ atomicState.activeSchedData = actSchedules
+ //atomicState.scheduleSchedActiveCount = numAct
+ }
+}
+
+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
+}
+
+
+def isSchMotConfigured() {
+ return settings?.schMotTstat ? true : false
+}
+
+def getLastschMotEvalSec() { return !atomicState?.lastschMotEval ? 100000 : GetTimeDiffSeconds(atomicState?.lastschMotEval, null, "getLastschMotEvalSec").toInteger() }
+
+def schMotCheck() {
+ LogAction("schMotCheck...", "trace", false)
+ try {
+ if(atomicState?.disableAutomation) { return }
+ def schWaitVal = settings?.schMotWaitVal?.toInteger() ?: 60
+ if(schWaitVal > 60) { schWaitVal = 60 }
+ if(getLastschMotEvalSec() < schWaitVal) {
+ def schChkVal = ((schWaitVal - getLastschMotEvalSec()) < 30) ? 30 : (schWaitVal - getLastschMotEvalSec())
+ scheduleAutomationEval(schChkVal)
+ LogAction("Remote Sensor: Too Soon to Evaluate Actions...Scheduling Re-Evaluation in (${schChkVal} seconds)", "info", true)
+ return
+ }
+
+ def execTime = now()
+ atomicState?.lastEvalDt = getDtNow()
+ atomicState?.lastschMotEval = getDtNow()
+
+ // This order is important...
+ // turn system on/off, then update schedule mode/temps, then remote sensors, then update fans
+
+ if(settings?.schMotWaterOff) {
+ if(isLeakWatConfigured()) { leakWatCheck() }
+ }
+ if(settings?.schMotContactOff) {
+ if(isConWatConfigured()) { conWatCheck() }
+ }
+ if(settings?.schMotExternalTempOff) {
+ if(isExtTmpConfigured()) {
+ if(setting?.extTmpUseWeather) { getExtConditions() }
+ extTmpTempCheck()
+ }
+ }
+// if(settings?.schMotSetTstatTemp) {
+ if(isTstatSchedConfigured()) { setTstatTempCheck() }
+// }
+ if(settings?.schMotRemoteSensor) {
+ if(isRemSenConfigured()) {
+ remSenCheck()
+ }
+ }
+ if(settings?.schMotOperateFan) {
+ if(isFanCtrlConfigured()) {
+ fanCtrlCheck()
+ }
+ }
+
+ storeExecutionHistory((now() - execTime), "schMotCheck")
+ } catch (ex) {
+ log.error "schMotCheck Exception:", ex
+ parent?.sendExceptionData(ex, "schMotCheck", true, getAutoType())
+ }
+}
+
+def storeLastEventData(evt) {
+ if(evt) {
+ atomicState?.lastEventData = ["name":evt.name, "displayName":evt.displayName, "value":evt.value, "date":formatDt(evt.date), "unit":evt.unit]
+ //log.debug "LastEvent: ${atomicState?.lastEventData}"
+ }
+}
+
+def storeExecutionHistory(val, method = null) {
+ //log.debug "storeExecutionHistory($val, $method)"
+ try {
+ if(method) {
+ LogAction("${method} Execution Time: (${val} milliseconds)", "trace", false)
+ }
+ atomicState?.lastExecutionTime = val ?: null
+ def list = atomicState?.evalExecutionHistory ?: []
+ def listSize = 30
+ if(list?.size() < listSize) {
+ list.push(val)
+ }
+ else if(list?.size() > listSize) {
+ def 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
+ }
+ if(list) { atomicState?.evalExecutionHistory = list }
+ } catch (ex) {
+ log.error "storeExecutionHistory Exception:", ex
+ parent?.sendExceptionData(ex, "storeExecutionHistory", true, getAutoType())
+ }
+}
+
+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 setNotificationPage(params) {
+ def pName = params.pName
+ def allowSpeech = false
+ def allowAlarm = false
+ def showSched = false
+ if(params?.pName) {
+ atomicState.curNotifPageData = params
+ allowSpeech = params?.allowSpeech?.toBoolean(); showSched = params?.showSchedule?.toBoolean(); allowAlarm = params?.allowAlarm?.toBoolean()
+ } else {
+ pName = atomicState?.curNotifPageData?.pName; allowSpeech = atomicState?.curNotifPageData?.allowSpeech; showSched = atomicState?.curNotifPageData?.showSchedule; allowAlarm = atomicState?.curNotifPageData?.allowAlarm
+ }
+ dynamicPage(name: "setNotificationPage", title: "Configure Notification Options", uninstall: false) {
+ section("Notification Preferences:") {
+ input "${pName}NotificationsOn", "bool", title: "Enable Notifications?", description: (!settings["${pName}NotificationsOn"] ? "Enable Text, Voice, Ask Alexa, or Alarm Notifications..." : ""), required: false, defaultValue: false, submitOnChange: true,
+ image: getAppImg("notification_icon.png")
+ }
+ if(settings["${pName}NotificationsOn"]) {
+ def notifDesc = !location.contactBookEnabled ? "Enable Push Messages Below..." : "(Manager App Recipients are used by default)"
+ section("${notifDesc}") {
+ if(!location.contactBookEnabled) {
+ input "${pName}UsePush", "bool", title: "Send Push Notitifications\n(Optional)", required: false, submitOnChange: true, defaultValue: false, image: getAppImg("notification_icon.png")
+ } else {
+ input("${pName}NotifRecips", "contact", title: "Select Recipients...\n(Optional)", required: false, multiple: true, submitOnChange: true, image: getAppImg("recipient_icon.png")) {
+ input ("${pName}NotifPhones", "phone", title: "Phone Number to Send SMS to...\n(Optional)", submitOnChange: true, required: false)
+ }
+ }
+ }
+ }
+
+/*
+this is a parent only method today
+
+ if(showSched && settings["${pName}NotificationsOn"]) {
+ section(title: "Time Restrictions") {
+ href "setNotificationTimePage", title: "Silence Notifications...", description: (getNotifSchedDesc(pName) ?: "Tap to configure..."), params: [pName: "${pName}"],
+ state: (getNotifSchedDesc(pName) ? "complete" : null), image: getAppImg("quiet_time_icon.png")
+ }
+ }
+*/
+ if(allowSpeech && settings?."${pName}NotificationsOn") {
+ section("Voice Notification Preferences:") {
+ input "${pName}AllowSpeechNotif", "bool", title: "Enable Voice Notifications?", description: "Media players, Speech Devices, or Ask Alexa", required: false, defaultValue: (settings?."${pName}AllowSpeechNotif" ? true : false), submitOnChange: true, image: getAppImg("speech_icon.png")
+ if(settings["${pName}AllowSpeechNotif"]) {
+ if(pName == "leakWat") {
+ if(!atomicState?."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) { atomicState?."${pName}OffVoiceMsg" = "ATTENTION: %devicename% has been turned OFF because %wetsensor% has reported it is WET" }
+ if(!atomicState?."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) { atomicState?."${pName}OnVoiceMsg" = "Restoring %devicename% to %lastmode% Mode because ALL water sensors have been Dry again for (%ondelay%)" }
+ }
+ if(pName == "conWat") {
+ if(!atomicState?."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) { atomicState?."${pName}OffVoiceMsg" = "ATTENTION: %devicename% has been turned OFF because %opencontact% has been Opened for (%offdelay%)" }
+ if(!atomicState?."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) { atomicState?."${pName}OnVoiceMsg" = "Restoring %devicename% to %lastmode% Mode because ALL contacts have been Closed again for (%ondelay%)" }
+ }
+ if(pName == "extTmp") {
+ if(!atomicState?."${pName}OffVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) { atomicState?."${pName}OffVoiceMsg" = "ATTENTION: %devicename% has been turned OFF because External Temp is above the temp threshold for (%offdelay%)" }
+ if(!atomicState?."${pName}OnVoiceMsg" || !settings["${pName}UseCustomSpeechNotifMsg"]) { atomicState?."${pName}OnVoiceMsg" = "Restoring %devicename% to %lastmode% Mode because External Temp has been above the temp threshold for (%ondelay%)" }
+ }
+ 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 Devices", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg("media_player.png")
+ input "${pName}SpeechDevices", "capability.speechSynthesis", title: "Select Speech Synthesis Devices", hideWhenEmpty: true, multiple: true, required: false, submitOnChange: true, image: getAppImg("speech2_icon.png")
+ if(settings["${pName}SpeechMediaPlayer"]) {
+ input "${pName}SpeechVolumeLevel", "number", title: "Default Volume Level?", required: false, defaultValue: 30, range: "0::100", submitOnChange: true, image: getAppImg("volume_icon.png")
+ 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}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: atomicState?."${pName}OffVoiceMsg" , submitOnChange: true, image: getAppImg("speech_icon.png")
+ atomicState?."${pName}OffVoiceMsg" = settings?."${pName}CustomOffSpeechMessage"
+ if(settings?."${pName}CustomOffSpeechMessage") {
+ paragraph "Off Msg:\n" + voiceNotifString(atomicState?."${pName}OffVoiceMsg",pName)
+ }
+ input "${pName}CustomOnSpeechMessage", "text", title: "Restore On Message?", required: false, defaultValue: atomicState?."${pName}OnVoiceMsg", submitOnChange: true, image: getAppImg("speech_icon.png")
+ atomicState?."${pName}OnVoiceMsg" = settings?."${pName}CustomOnSpeechMessage"
+ if(settings?."${pName}CustomOnSpeechMessage") {
+ paragraph "Restore On Msg:\n" + voiceNotifString(atomicState?."${pName}OnVoiceMsg",pName)
+ }
+ } else {
+ atomicState?."${pName}OffVoiceMsg" = ""
+ atomicState?."${pName}OnVoiceMsg" = ""
+ }
+ }
+ }
+ }
+ }
+ }
+ if(allowAlarm && settings?."${pName}NotificationsOn") {
+ section("Alarm/Siren Device Preferences:") {
+ input "${pName}AllowAlarmNotif", "bool", title: "Enable Alarm|Siren?", required: false, defaultValue: (settings?."${pName}AllowAlarmNotif" ? true : false), submitOnChange: true,
+ image: getAppImg("alarm_icon.png")
+ if(settings["${pName}AllowAlarmNotif"]) {
+ input "${pName}AlarmDevices", "capability.alarm", title: "Select Alarm/Siren Devices", multiple: true, required: settings["${pName}AllowAlarmNotif"], submitOnChange: true, image: getAppImg("alarm_icon.png")
+ }
+ }
+ }
+ //if(pName in ["conWat", "leakWat", "extTmp", "watchDog"] && settings["${pName}NotificationsOn"] && (settings["${pName}AllowSpeechNotif"] || settings["${pName}AllowAlarmNotif"])) {
+ if(pName in ["conWat", "leakWat", "extTmp", "watchDog"] && settings["${pName}NotificationsOn"] && settings["${pName}AllowAlarmNotif"] && settings["${pName}AlarmDevices"]) {
+ section("Notification Alert Options (1):") {
+ input "${pName}_Alert_1_Delay", "enum", title: "First Alert Delay (in minutes)", defaultValue: null, required: true, submitOnChange: true, metadata: [values:longTimeSecEnum()],
+ image: getAppImg("alert_icon2.png")
+ if(settings?."${pName}_Alert_1_Delay") {
+/*
+TODO These are not in use. They could only be used in alarming, but the other data needed is not available
+
+ if(settings?."${pName}NotificationsOn" && (settings["${pName}UsePush"] || settings["${pName}NotifRecips"] || settings["${pName}NotifPhones"])) {
+ input "${pName}_Alert_1_Send_Push", "bool", title: "Send Push Notification?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("notification_icon.png")
+ if(settings["${pName}_Alert_1_Send_Push"]) {
+ input "${pName}_Alert_1_Send_Custom_Push", "bool", title: "Custom Push Message?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("notification_icon.png")
+ if(settings["${pName}_Alert_1_Send_Custom_Push"]) {
+ input "${pName}_Alert_1_CustomPushMessage", "text", title: "Push Message to Send?", required: false, defaultValue: atomicState?."${pName}OffVoiceMsg" , submitOnChange: true, image: getAppImg("speech_icon.png")
+ }
+ }
+ }
+ if(settings?."${pName}AllowSpeechNotif") {
+ input "${pName}_Alert_1_Use_Speech", "bool", title: "Send Voice Notification?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png")
+ if(settings?."${pName}_Alert_1_Use_Speech") {
+ input "${pName}_Alert_1_Send_Custom_Speech", "bool", title: "Custom Speech Message?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png")
+ if(settings["${pName}_Alert_1_Send_Custom_Speech"]) {
+ input "${pName}_Alert_1_CustomSpeechMessage", "text", title: "Push Message to Send?", required: false, defaultValue: atomicState?."${pName}OffVoiceMsg" , submitOnChange: true, image: getAppImg("speech_icon.png")
+ }
+ }
+ }
+ if(settings?."${pName}AllowAlarmNotif") {
+*/
+ //input "${pName}_Alert_1_Use_Alarm", "bool", title: "Use Alarm Device", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("alarm_icon.png")
+ //if(settings?."${pName}_Alert_1_Use_Alarm" && settings?."${pName}AlarmDevices") {
+ input "${pName}_Alert_1_AlarmType", "enum", title: "Alarm Type to use?", metadata: [values:alarmActionsEnum()], defaultValue: null, submitOnChange: true, required: true, image: getAppImg("alarm_icon.png")
+ if(settings?."${pName}_Alert_1_AlarmType") {
+ input "${pName}_Alert_1_Alarm_Runtime", "enum", title: "Turn off Alarm After (in seconds)?", metadata: [values:shortTimeEnum()], defaultValue: 10, required: true, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ }
+ //}
+/*
+ }
+ if(settings["${pName}_Alert_1_Send_Custom_Speech"] || settings["${pName}_Alert_1_Send_Custom_Push"]) {
+ if(pName in ["leakWat", "conWat", "extTmp"]) {
+ getNotifVariables(pName)
+ }
+ }
+*/
+ }
+ }
+ if(settings["${pName}_Alert_1_Delay"]) {
+ section("Notification Alert Options (2):") {
+ input "${pName}_Alert_2_Delay", "enum", title: "Second Alert Delay (in minutes)", defaultValue: null, metadata: [values:longTimeSecEnum()], required: false, submitOnChange: true, image: getAppImg("alert_icon2.png")
+ if(settings?."${pName}_Alert_2_Delay") {
+/*
+TODO These are not in use. They could only be used in alarming, but the other data needed is not available
+ if(settings?."${pName}NotificationsOn" && (settings["${pName}UsePush"] || settings["${pName}NotifRecips"] || settings["${pName}NotifPhones"])) {
+ input "${pName}_Alert_2_Send_Push", "bool", title: "Send Push Notification?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("notification_icon.png")
+ if(settings["${pName}_Alert_2_Send_Push"]) {
+ input "${pName}_Alert_2_Send_Custom_Push", "bool", title: "Custom Push Message?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("notification_icon.png")
+ if(settings["${pName}_Alert_2_Send_Custom_Push"]) {
+ input "${pName}_Alert_2_CustomPushMessage", "text", title: "Push Message to Send?", required: false, defaultValue: atomicState?."${pName}OffVoiceMsg" , submitOnChange: true, image: getAppImg("speech_icon.png")
+ }
+ }
+ }
+ if(settings?."${pName}AllowSpeechNotif") {
+ input "${pName}_Alert_2_Use_Speech", "bool", title: "Send Voice Notification?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png")
+ if(settings?."${pName}_Alert_2_Use_Speech") {
+ input "${pName}_Alert_2_Send_Custom_Speech", "bool", title: "Custom Speech Message?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("speech_icon.png")
+ if(settings["${pName}_Alert_2_Send_Custom_Speech"]) {
+ input "${pName}_Alert_2_CustomSpeechMessage", "text", title: "Push Message to Send?", required: false, defaultValue: atomicState?."${pName}OffVoiceMsg" , submitOnChange: true, image: getAppImg("speech_icon.png")
+ }
+ }
+ }
+ if(settings?."${pName}AllowAlarmNotif") {
+*/
+ //input "${pName}_Alert_2_Use_Alarm", "bool", title: "Use Alarm Device?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("alarm_icon.png")
+ //if(settings?."${pName}_Alert_2_Use_Alarm" && settings?."${pName}AlarmDevices") {
+ input "${pName}_Alert_2_AlarmType", "enum", title: "Alarm Type to use?", metadata: [values:alarmActionsEnum()], defaultValue: null, submitOnChange: true, required: true, image: getAppImg("alarm_icon.png")
+ if(settings?."${pName}_Alert_2_AlarmType") {
+ input "${pName}_Alert_2_Alarm_Runtime", "enum", title: "Turn off Alarm After (in minutes)?", metadata: [values:shortTimeEnum()], defaultValue: 10, required: true, submitOnChange: true,
+ image: getAppImg("delay_time_icon.png")
+ }
+ //}
+/*
+ }
+ if(settings["${pName}_Alert_2_Send_Custom_Speech"] || settings["${pName}_Alert_2_Send_Custom_Push"]) {
+ if(pName in ["leakWat", "conWat", "extTmp"]) {
+ getNotifVariables(pName)
+ }
+ }
+*/
+ }
+ }
+ }
+ }
+ }
+}
+
+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) {
+ //log.trace "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%', (atomicState?."${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()}(°${getTemperatureScale()})") }
+ 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
+}
+
+def getNotificationOptionsConf(pName) {
+ LogAction("getNotificationOptionsConf pName: $pName", "trace", false)
+ def res = (settings?."${pName}NotificationsOn" &&
+ (getRecipientDesc(pName) ||
+ (settings?."${pName}AllowSpeechNotif" && (settings?."${pName}SpeechDevices" || settings?."${pName}SpeechMediaPlayer")) ||
+ (settings?."${pName}AllowAlarmNofif" && settings?."${pName}AlarmDevices")
+ ) ) ? true : false
+ return res
+}
+
+def getNotifConfigDesc(pName) {
+ LogAction("getNotifConfigDesc pName: $pName", "trace", false)
+ def str = ""
+ if(settings?."${pName}NotificationsOn") {
+ str += ( getRecipientDesc(pName) || (settings?."${pName}AllowSpeechNotif" && (settings?."${pName}SpeechDevices" || settings?."${pName}SpeechMediaPlayer"))) ?
+ "Notification Status:" : ""
+ str += (settings?."${pName}NotifRecips") ? "${str != "" ? "\n" : ""} • Contacts: (${settings?."${pName}NotifRecips"?.size()})" : ""
+ str += (settings?."${pName}UsePush") ? "\n • Push Messages: Enabled" : ""
+ str += (settings?."${pName}NotifPhones") ? "\n • SMS: (${settings?."${pName}NotifPhones"?.size()})" : ""
+ str += getVoiceNotifConfigDesc(pName) ? ("${(str != "") ? "\n\n" : "\n"}Voice Status:${getVoiceNotifConfigDesc(pName)}") : ""
+ str += getAlarmNotifConfigDesc(pName) ? ("${(str != "") ? "\n\n" : "\n"}Alarm Status:${getAlarmNotifConfigDesc(pName)}") : ""
+ str += getAlertNotifConfigDesc(pName) ? "\n${getAlertNotifConfigDesc(pName)}" : ""
+ }
+ return (str != "") ? "${str}" : null
+}
+
+def getVoiceNotifConfigDesc(pName) {
+ def str = ""
+ if(settings?."${pName}NotificationsOn" && settings["${pName}AllowSpeechNotif"]) {
+ def speaks = getInputToStringDesc(settings?."${pName}SpeechDevices", true)
+ def medias = getInputToStringDesc(settings?."${pName}SpeechMediaPlayer", true)
+ str += settings["${pName}SendToAskAlexaQueue"] ? "\n • Send to Ask Alexa: (True)" : ""
+ str += speaks ? "\n • Speech Devices:${speaks.size() > 1 ? "\n" : ""}${speaks}" : ""
+ str += medias ? "\n • Media Players:${medias.size() > 1 ? "" : ""}${medias}" : ""
+ str += (medias && settings?."${pName}SpeechVolumeLevel") ? "\n ├ Volume: (${settings?."${pName}SpeechVolumeLevel"})" : ""
+ str += (medias && settings?."${pName}SpeechAllowResume") ? "\n └ Resume: (${settings?."${pName}SpeechAllowResume".toString().capitalize()})" : ""
+ str += (settings?."${pName}UseCustomSpeechNotifMsg" && (medias || speaks)) ? "\n • Custom Message: (${settings?."${pName}UseCustomSpeechNotifMsg".toString().capitalize()})" : ""
+ }
+ return (str != "") ? "${str}" : null
+}
+
+def getAlarmNotifConfigDesc(pName) {
+ def str = ""
+ if(settings?."${pName}NotificationsOn" && settings["${pName}AllowAlarmNotif"]) {
+ def alarms = getInputToStringDesc(settings["${pName}AlarmDevices"], true)
+ str += alarms ? "\n • Alarm Devices:${alarms.size() > 1 ? "\n" : ""}${alarms}" : ""
+ }
+ return (str != "") ? "${str}" : null
+}
+
+def getAlertNotifConfigDesc(pName) {
+ def str = ""
+//TODO not sure we do all these...
+ if(settings?."${pName}NotificationsOn" && (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
+}
+
+def getInputToStringDesc(inpt, addSpace = null) {
+ def cnt = 0
+ def 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
+}
+
+def isPluralString(obj) {
+ return (obj?.size() > 1) ? "(s)" : ""
+}
+
+def getRecipientsNames(val) {
+ def n = ""
+ def i = 0
+ if(val) {
+ //def valLabel =
+ 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}NotifRecips") || (settings?."${pName}NotifPhones" || settings?."${pName}NotifUsePush")) ? getRecipientsNames(settings?."${pName}NotifRecips") : null
+}
+
+def setDayModeTimePage(params) {
+ def pName = params.pName
+ if(params?.pName) {
+ atomicState.cursetDayModeTimePageData = params
+ } else {
+ pName = atomicState?.cursetDayModeTimePageData?.pName
+ }
+ dynamicPage(name: "setDayModeTimePage", title: "Select Days, Times or Modes", uninstall: false) {
+ def secDesc = settings["${pName}DmtInvert"] ? "Not" : "Only"
+ def inverted = settings["${pName}DmtInvert"] ? true : false
+ section("") {
+ input "${pName}DmtInvert", "bool", title: "When Not in Any of These?...", defaultValue: false, submitOnChange: true, image: getAppImg("switch_icon.png")
+ }
+ section("${secDesc} During these Days, Times, or Modes:") {
+ def timeReq = (settings?."${pName}StartTime" || settings."${pName}StopTime") ? true : false
+ input "${pName}StartTime", "time", title: "Start time", required: timeReq, image: getAppImg("start_time_icon.png")
+ input "${pName}StopTime", "time", title: "Stop time", required: timeReq, image: getAppImg("stop_time_icon.png")
+ input "${pName}Days", "enum", title: "${inverted ? "Not": "Only"} These Days", multiple: true, required: false, options: timeDayOfWeekOptions(), image: getAppImg("day_calendar_icon2.png")
+ input "${pName}Modes", "mode", title: "${inverted ? "Not": "Only"} in These Modes...", multiple: true, required: false, image: getAppImg("mode_icon.png")
+ }
+ }
+}
+
+def getDayModeTimeDesc(pName) {
+ def startTime = settings?."${pName}StartTime"
+ def stopInput = settings?."${pName}StopInput"
+ def stopTime = settings?."${pName}StopTime"
+ def dayInput = settings?."${pName}Days"
+ def modeInput = settings?."${pName}Modes"
+ def inverted = settings?."${pName}DmtInvert" ?: null
+ def str = ""
+ def days = getInputToStringDesc(dayInput)
+ def modes = getInputToStringDesc(modeInput)
+ 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 += (str != "") ? "\n\nTap to Modify..." : ""
+ return str
+}
+
+def getDmtSectionDesc(autoType) {
+ return settings["${autoType}DmtInvert"] ? "Do Not Act During these Days, Times, or Modes:" : "Only Act During these Days, Times, or Modes:"
+}
+
+/************************************************************************************************
+| AUTOMATION SCHEDULE CHECK |
+*************************************************************************************************/
+
+def autoScheduleOk(autoType) {
+ try {
+ def inverted = settings?."${autoType}DmtInvert" ? true : false
+ def modeOk = true
+ modeOk = (!settings?."${autoType}Modes" || ((isInMode(settings?."${autoType}Modes") && !inverted) || (!isInMode(settings?."${autoType}Modes") && inverted))) ? true : false
+ //dayOk
+ def dayOk = true
+ def dayFmt = new SimpleDateFormat("EEEE")
+ dayFmt.setTimeZone(getTimeZone())
+ def today = dayFmt.format(new Date())
+ def inDay = (today in settings?."${autoType}Days") ? true : false
+ dayOk = (!settings?."${autoType}Days" || ((inDay && !inverted) || (!inDay && inverted))) ? true : false
+
+ //scheduleTimeOk
+ def timeOk = true
+ if(settings?."${autoType}StartTime" && settings?."${autoType}StopTime") {
+ def inTime = (timeOfDayIsBetween(settings?."${autoType}StartTime", settings?."${autoType}StopTime", new Date(), getTimeZone())) ? true : false
+ timeOk = ((inTime && !inverted) || (!inTime && inverted)) ? true : false
+ }
+
+ //LogAction("autoScheduleOk( dayOk: $dayOk | modeOk: $modeOk | dayOk: ${dayOk} | timeOk: $timeOk | inverted: ${inverted})", "info", false)
+ return (modeOk && dayOk && timeOk) ? true : false
+ } catch (ex) {
+ log.error "${autoType}-autoScheduleOk Exception:", ex
+ parent?.sendExceptionData(ex, "${autoType}-autoScheduleOk", true, getAutoType())
+ }
+}
+
+/************************************************************************************************
+| SEND NOTIFICATIONS VIA PARENT APP |
+*************************************************************************************************/
+def sendNofificationMsg(msg, msgType, recips = null, sms = null, push = null) {
+ LogAction("sendNofificationMsg...($msg, $msgType, $recips, $sms, $push)", "trace", false)
+ if(recips || sms || push) {
+ parent?.sendMsg(msgType, msg, recips, sms, push)
+ //LogAction("Send Push Notification to $recips...", "info", true)
+ } else {
+ parent?.sendMsg(msgType, msg)
+ }
+}
+
+/************************************************************************************************
+| GLOBAL Code | Logging AND Diagnostic |
+*************************************************************************************************/
+
+def sendEventPushNotifications(message, type, pName) {
+ //log.trace "sendEventPushNotifications...($message, $type, $pName)"
+ if(settings["${pName}_Alert_1_Send_Push"] || settings["${pName}_Alert_2_Send_Push"]) {
+//TODO this portion is never reached
+ if(settings["${pName}_Alert_1_CustomPushMessage"]) {
+ sendNofificationMsg(settings["${pName}_Alert_1_CustomPushMessage"].toString(), type, settings?."${pName}NotifRecips", settings?."${pName}NotifPhones", settings?."${pName}UsePush")
+ } else {
+ sendNofificationMsg(message, type, settings?."${pName}NotifRecips", settings?."${pName}NotifPhones", settings?."${pName}UsePush")
+ }
+ } else {
+ sendNofificationMsg(message, type, settings?."${pName}NotifRecips", settings?."${pName}NotifPhones", settings?."${pName}UsePush")
+ }
+}
+
+def sendEventVoiceNotifications(vMsg, pName, msgId, rmAAMsg=false, rmMsgId) {
+ def allowNotif = settings?."${pName}NotificationsOn" ? true : false
+ def allowSpeech = allowNotif && settings?."${pName}AllowSpeechNotif" ? true : false
+ def ok2Notify = parent.getOk2Notify()
+
+ LogAction("sendEventVoiceNotifications...($vMsg, $pName) ok2Notify: $ok2Notify", "trace", false)
+ if(allowNotif && allowSpeech) {
+ if(ok2Notify && (settings["${pName}SpeechDevices"] || settings["${pName}SpeechMediaPlayer"])) {
+ 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) {
+ if(parent?.getAskAlexaQueueEnabled() == true) {
+ 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) {
+ if(parent?.getAskAlexaQueueEnabled() == true) {
+ 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(autoType) {
+ LogAction("scheduleAlarmOn: autoType: $autoType a1DelayVal: ${getAlert1DelayVal(autoType)}", "debug", true)
+ def timeVal = getAlert1DelayVal(autoType).toInteger()
+ def ok2Notify = parent.getOk2Notify()
+
+ log.debug "scheduleAlarmOn timeVal: $timeVal ok2Notify: $ok2Notify"
+ if(canSchedule() && ok2Notify) {
+ if(timeVal > 0) {
+ runIn(timeVal, "alarm0FollowUp", [data: [autoType: autoType]])
+ LogAction("scheduleAlarmOn: Scheduling Alarm Followup 0...in timeVal: $timeVal", "info", true)
+ atomicState."${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) }
+}
+
+def alarm0FollowUp(val) {
+ def autoType = val.autoType
+ LogAction("alarm0FollowUp: autoType: $autoType 1 OffVal: ${getAlert1AlarmEvtOffVal(autoType)}", "debug", true)
+ def timeVal = getAlert1AlarmEvtOffVal(autoType).toInteger()
+ log.debug "alarm0FollowUp timeVal: $timeVal"
+ if(canSchedule() && timeVal > 0 && sendEventAlarmAction(1, autoType)) {
+ runIn(timeVal, "alarm1FollowUp", [data: [autoType: autoType]])
+ LogAction("alarm0FollowUp: Scheduling Alarm Followup 1...in timeVal: $timeVal", "info", true)
+ } else { LogAction ("alarm0FollowUp: Could not schedule operation timeVal: $timeVal", "error", true) }
+}
+
+def alarm1FollowUp(val) {
+ def autoType = val.autoType
+ LogAction("alarm1FollowUp autoType: $autoType a2DelayVal: ${getAlert2DelayVal(autoType)}", "debug", true)
+ def aDev = settings["${autoType}AlarmDevices"]
+ if(aDev) {
+ aDev?.off()
+ storeLastAction("Set Alarm OFF", getDtNow())
+ LogAction("alarm1FollowUp: Turning OFF ${aDev}", "info", true)
+ }
+ def timeVal = getAlert2DelayVal(autoType).toInteger()
+ //if(canSchedule() && (settings["${autoType}_Alert_2_Use_Alarm"] && timeVal > 0)) {
+ if(canSchedule() && timeVal > 0) {
+ runIn(timeVal, "alarm2FollowUp", [data: [autoType: autoType]])
+ LogAction("alarm1FollowUp: Scheduling Alarm Followup 2...in timeVal: $timeVal", "info", true)
+ } else { LogAction ("alarm1FollowUp: Could not schedule operation timeVal: $timeVal", "error", true) }
+}
+
+def alarm2FollowUp(val) {
+ def autoType = val.autoType
+ LogAction("alarm2FollowUp: autoType: $autoType 2 OffVal: ${getAlert2AlarmEvtOffVal(autoType)}", "debug", true)
+ def timeVal = getAlert2AlarmEvtOffVal(autoType)
+ if(canSchedule() && timeVal > 0 && sendEventAlarmAction(2, autoType)) {
+ runIn(timeVal, "alarm3FollowUp", [data: [autoType: autoType]])
+ LogAction("alarm2FollowUp: Scheduling Alarm Followup 3...in timeVal: $timeVal", "info", true)
+ } else { LogAction ("alarm2FollowUp: Could not schedule operation timeVal: $timeVal", "error", true) }
+}
+
+def alarm3FollowUp(val) {
+ def autoType = val.autoType
+ LogAction("alarm3FollowUp: autoType: $autoType", "debug", true)
+ def aDev = settings["${autoType}AlarmDevices"]
+ if(aDev) {
+ aDev?.off()
+ storeLastAction("Set Alarm OFF", getDtNow())
+ LogAction("alarm3FollowUp: Turning OFF ${aDev}", "info", true)
+ }
+ atomicState."${autoType}AlarmActive" = false
+}
+
+def alarmEvtSchedCleanup(autoType) {
+ if(atomicState?."${autoType}AlarmActive") {
+ LogAction("Cleaning Up Alarm Event Schedules... autoType: $autoType", "info", true)
+ def items = ["alarm0FollowUp","alarm1FollowUp", "alarm2FollowUp", "alarm3FollowUp"]
+ items.each {
+ unschedule("$it")
+ }
+ def val = [ autoType: autoType ]
+ alarm3FollowUp(val)
+ }
+}
+
+def sendEventAlarmAction(evtNum, autoType) {
+ LogAction("sendEventAlarmAction evtNum: $evtNum autoType: $autoType", "info", true)
+ try {
+ def resval = false
+ def allowNotif = settings?."${autoType}NotificationsOn" ? true : false
+ def 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":
+ atomicState?."${autoType}alarmEvt${evtNum}StartDt" = getDtNow()
+ aDev?.both()
+ storeLastAction("Set Alarm BOTH ON", getDtNow())
+ break
+ case "siren":
+ atomicState?."${autoType}alarmEvt${evtNum}StartDt" = getDtNow()
+ aDev?.siren()
+ storeLastAction("Set Alarm SIREN ON", getDtNow())
+ break
+ case "strobe":
+ atomicState?."${autoType}alarmEvt${evtNum}StartDt" = getDtNow()
+ aDev?.strobe()
+ storeLastAction("Set Alarm STROBE ON", getDtNow())
+ break
+ default:
+ resval = false
+ break
+ }
+ //}
+ }
+ } catch (ex) {
+ log.error "sendEventAlarmAction Exception: ($evtNum) - ", ex
+ parent?.sendExceptionData(ex, "sendEventAlarmAction", true, getAutoType())
+ }
+ return resval
+}
+
+def alarmAlertEvt(evt) {
+ log.trace "alarmAlertEvt: ${evt.displayName} Alarm State is Now (${evt.value})"
+}
+
+def getAlert1DelayVal(autoType) { return !settings["${autoType}_Alert_1_Delay"] ? 300 : (settings["${autoType}_Alert_1_Delay"].toInteger()) }
+def getAlert2DelayVal(autoType) { return !settings["${autoType}_Alert_2_Delay"] ? 300 : (settings["${autoType}_Alert_2_Delay"].toInteger()) }
+
+def getAlert1AlarmEvtOffVal(autoType) { return !settings["${autoType}_Alert_1_Alarm_Runtime"] ? 10 : (settings["${autoType}_Alert_1_Alarm_Runtime"].toInteger()) }
+def getAlert2AlarmEvtOffVal(autoType) { return !settings["${autoType}_Alert_2_Alarm_Runtime"] ? 10 : (settings["${autoType}_Alert_2_Alarm_Runtime"].toInteger()) }
+
+/*
+def getAlarmEvt1RuntimeDtSec() { return !atomicState?.alarmEvt1StartDt ? 100000 : GetTimeDiffSeconds(atomicState?.alarmEvt1StartDt).toInteger() }
+def getAlarmEvt2RuntimeDtSec() { return !atomicState?.alarmEvt2StartDt ? 100000 : GetTimeDiffSeconds(atomicState?.alarmEvt2StartDt).toInteger() }
+*/
+
+void sendTTS(txt, pName) {
+ log.trace "sendTTS(data: ${txt})"
+ try {
+ def msg = txt.toString().replaceAll("\\[|\\]|\\(|\\)|\\'|\\_", "")
+ def spks = settings?."${pName}SpeechDevices"
+ def meds = settings?."${pName}SpeechMediaPlayer"
+ def res = settings?."${pName}SpeechAllowResume"
+ def vol = settings?."${pName}SpeechVolumeLevel"
+ log.debug "msg: $msg | speaks: $spks | medias: $meds | resume: $res | volume: $vol"
+ 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)
+ }
+ }
+ }
+ }
+ } catch (ex) {
+ log.error "sendTTS Exception:", ex
+ parent?.sendExceptionData(ex, "sendTTS", true, getAutoType())
+ }
+}
+
+def scheduleTimeoutRestore(pName) {
+ def timeOutVal = settings["${pName}OffTimeout"]?.toInteger()
+ if(timeOutVal && !atomicState?."${pName}timeOutScheduled") {
+ runIn(timeOutVal.toInteger(), "restoreAfterTimeOut", [data: [pName:pName]])
+ LogAction("Mode Restoration Timeout Scheduled for ${pName} (${getEnumValue(longTimeSecEnum(), settings?."${pName}OffTimeout")})", "info", true)
+ atomicState."${pName}timeOutScheduled" = true
+ }
+}
+
+def unschedTimeoutRestore(pName) {
+ def timeOutVal = settings["${pName}OffTimeout"]?.toInteger()
+ if(timeOutVal && atomicState?."${pName}timeOutScheduled") {
+ unschedule("restoreAfterTimeOut")
+ LogAction("The Scheduled Mode Restoration Timeout for ${pName} has been cancelled because all Triggers are now clear...", "info", true)
+ }
+ atomicState."${pName}timeOutScheduled" = false
+}
+
+def restoreAfterTimeOut(val) {
+ def pName = val?.pName.value
+ if(pName && settings?."${pName}OffTimeout") {
+ switch(pName) {
+ case "conWat":
+ atomicState."${pName}timeOutScheduled" = false
+ conWatCheck(true)
+ break
+ case "leakWat":
+ //leakWatCheck(true)
+ break
+ case "extTmp":
+ atomicState."${pName}timeOutScheduled" = false
+ extTmpTempCheck(true)
+ break
+ default:
+ LogAction("restoreAfterTimeOut no pName match ${pName}", "error", true)
+ break
+ }
+ }
+}
+
+def checkThermostatDupe(tstatOne, tstatTwo) {
+ def result = false
+ if(tstatOne && tstatTwo) {
+ def pTstat = tstatOne?.deviceNetworkId.toString()
+ def mTstatAr = []
+ tstatTwo?.each { ts ->
+ mTstatAr << ts?.deviceNetworkId.toString()
+ }
+ if(pTstat in mTstatAr) { return true }
+ }
+ return result
+}
+
+def checkModeDuplication(modeOne, modeTwo) {
+ def 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 }
+}
+
+def checkFanSpeedSupport(dev) {
+ def req = ["lowSpeed", "medSpeed", "highSpeed"]
+ def devCnt = 0
+ def devData = getDeviceSupportedCommands(dev)
+ devData.each { cmd ->
+ if(cmd.name in req) { devCnt = devCnt+1 }
+ }
+ def speed = dev?.currentValue("currentState") ?: null
+ //log.debug "checkFanSpeedSupport (speed: $speed | devCnt: $devCnt)"
+ return (speed && devCnt == 3) ? true : false
+}
+
+def getTstatCapabilities(tstat, autoType, dyn = false) {
+ try {
+ def canCool = true
+ def canHeat = true
+ def hasFan = true
+ if(tstat?.currentCanCool) { canCool = tstat?.currentCanCool.toBoolean() }
+ if(tstat?.currentCanHeat) { canHeat = tstat?.currentCanHeat.toBoolean() }
+ if(tstat?.currentHasFan) { hasFan = tstat?.currentHasFan.toBoolean() }
+
+ atomicState?."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : ""}TstatCanCool" = canCool
+ atomicState?."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : ""}TstatCanHeat" = canHeat
+ atomicState?."${autoType}${dyn ? "_${tstat?.deviceNetworkId}_" : ""}TstatHasFan" = hasFan
+ } catch (ex) {
+ log.error "getTstatCapabilities Exception:", ex
+ parent?.sendExceptionData("${tstat} - ${autoType} | ${ex.message}", "getTstatCapabilities", true, getAutoType())
+ }
+}
+
+def getSafetyTemps(tstat) {
+ def minTemp = tstat?.currentState("safetyTempMin")?.doubleValue
+ def maxTemp = tstat?.currentState("safetyTempMax")?.doubleValue
+ if(minTemp == 0) { minTemp = null }
+ if(maxTemp == 0) { maxTemp = null }
+ if(minTemp || maxTemp) {
+ return ["min":minTemp, "max":maxTemp]
+ }
+ return null
+}
+
+def getComfortHumidity(tstat) {
+ def maxHum = tstat?.currentValue("comfortHumidityMax") ?: 0
+ if(maxHum) {
+ //return ["min":minHumidity, "max":maxHumidity]
+ return maxHum
+ }
+ return null
+}
+
+def getComfortDewpoint(tstat) {
+ def maxDew = tstat?.currentState("comfortDewpointMax")?.doubleValue ?: 0.0
+ if(maxDew) {
+ //return ["min":minDew, "max":maxDew]
+ return maxDew.toDouble()
+ }
+ return null
+}
+
+def getSafetyTempsOk(tstat) {
+ def sTemps = getSafetyTemps(tstat)
+ //log.debug "sTempsOk: $sTemps"
+ if(sTemps) {
+ def curTemp = tstat?.currentTemperature?.toDouble()
+ //log.debug "curTemp: ${curTemp}"
+ if( ((sTemps?.min.toDouble() != 0) && (curTemp < sTemps?.min.toDouble())) || ((sTemps?.max?.toDouble() != 0) && (curTemp > sTemps?.max?.toDouble())) ) {
+ return false
+ }
+ } // else { log.debug "getSafetyTempsOk: no safety Temps" }
+ return true
+}
+
+def getGlobalDesiredHeatTemp() {
+ return parent?.settings?.locDesiredHeatTemp?.toDouble() ?: null
+}
+
+def getGlobalDesiredCoolTemp() {
+ return parent?.settings?.locDesiredCoolTemp?.toDouble() ?: 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
+}
+
+def isContactOpen(con) {
+ def res = false
+ if(con) {
+ if(con?.currentSwitch == "on") { res = true }
+ }
+ return res
+}
+
+def isSwitchOn(dev) {
+ def res = false
+ if(dev) {
+ dev?.each { d ->
+ if(d?.currentSwitch == "on") { res = true }
+ }
+ }
+ return res
+}
+
+def isPresenceHome(presSensor) {
+ def res = false
+ if(presSensor) {
+ presSensor?.each { d ->
+ if(d?.currentPresence == "present") { res = true }
+ }
+ }
+ return res
+}
+
+def getTstatPresence(tstat) {
+ def pres = "not present"
+ if(tstat) { pres = tstat?.currentPresence }
+ return pres
+}
+
+def setTstatMode(tstat, mode) {
+ def result = false
+ try {
+ if(mode) {
+ 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 }
+
+ if(result) { LogAction("setTstatMode: '${tstat?.label}' Mode has been set to (${mode.toString().toUpperCase()})", "info", false) }
+ else { LogAction("setTstatMode() | Invalid or Missing Mode received: ${mode}", "error", true) }
+ } else {
+ LogAction("setTstatMode() | Invalid or Missing Mode received: ${mode}", "warn", true)
+ }
+ }
+ catch (ex) {
+ log.error "setTstatMode() Exception:", ex
+ parent?.sendExceptionData(ex, "setTstatMode", true, getAutoType())
+ }
+ return result
+}
+
+def setMultipleTstatMode(tstats, mode) {
+ def result = false
+ try {
+ if(tstats && md) {
+ tstats?.each { ts ->
+ if(setTstatMode(ts, mode)) {
+ LogAction("Setting ${ts} Mode to (${mode})", "info", true)
+ result = true
+ } else {
+ return false
+ }
+ }
+ }
+ } catch (ex) {
+ log.error "setMultipleTstatMode() Exception:", ex
+ parent?.sendExceptionData(ex, "setMultipleTstatMode", true, getAutoType())
+ }
+ return result
+}
+
+def setTstatAutoTemps(tstat, coolSetpoint, heatSetpoint) {
+ LogAction("setTstatAutoTemps: [tstat: ${tstat?.displayName} | coolSetpoint: ${coolSetpoint}°${getTemperatureScale()} | heatSetpoint: ${heatSetpoint}°${getTemperatureScale()}]", "info", true)
+ def retVal = false
+ if(tstat) {
+ def hvacMode = tstat?.currentThermostatMode.toString()
+ def curCoolSetpoint = getTstatSetpoint(tstat, "cool")
+ def curHeatSetpoint = getTstatSetpoint(tstat, "heat")
+ def diff = getTemperatureScale() == "C" ? 2.0 : 3.0
+
+ def reqCool = coolSetpoint?.toDouble() ?: null
+ def reqHeat = heatSetpoint?.toDouble() ?: null
+
+ 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))) {
+ def heatFirst
+ if(reqHeat <= curHeatSetpoint) { heatFirst = true }
+ else if(reqCool >= curCoolSetpoint) { heatFirst = false }
+ else if(reqHeat > curHeatSetpoint) { heatFirst = false }
+ else { heatFirst = true }
+ if(heatFirst) {
+ LogAction("setTstatAutoTemps() | Setting tstat [${tstat?.displayName} | mode: (${hvacMode}) | heatSetpoint: (${reqHeat}°${getTemperatureScale()}) | coolSetpoint: (${reqCool}°${getTemperatureScale()})]", "info", true)
+ if(reqHeat != curHeatSetpoint) { tstat?.setHeatingSetpoint(reqHeat); retVal = true }
+ if(reqCool != curCoolSetpoint) { tstat?.setCoolingSetpoint(reqCool); retVal = true }
+ } else {
+ LogAction("setTstatAutoTemps() | Setting tstat: [${tstat?.displayName} | mode: (${hvacMode}) | coolSetpoint: (${reqCool}°${getTemperatureScale()}) | heatSetpoint: (${reqHeat}°${getTemperatureScale()})]", "info", true)
+ if(reqCool != curCoolSetpoint) { tstat?.setCoolingSetpoint(reqCool); retVal = true }
+ if(reqHeat != curHeatSetpoint) { tstat?.setHeatingSetpoint(reqHeat); retVal = true }
+ }
+ } else { LogAction("setTstatAutoTemps() | Setting tstat: [${tstat?.displayName} | mode: (${hvacMode}) is missing cool or heat set points (${reqCool}/${reqHeat}) or is not separated by ${diff}]", "info", true) }
+
+ } else if(hvacMode in ["cool"] && reqCool) {
+ LogAction("setTstatAutoTemps() | Setting tstat [${tstat?.displayName} | mode: (${hvacMode}) | coolSetpoint: (${reqCool}°${getTemperatureScale()})]", "info", true)
+ if(reqCool != curCoolSetpoint) { tstat?.setCoolingSetpoint(reqCool); retVal = true }
+
+ } else if(hvacMode in ["heat"] && reqHeat) {
+ LogAction("setTstatAutoTemps() | Setting tstat [${tstat?.displayName} | mode: (${hvacMode}) | heatSetpoint: (${reqHeat}°${getTemperatureScale()})]", "info", true)
+ if(reqHeat != curHeatSetpoint) { tstat?.setHeatingSetpoint(reqHeat); retVal = true }
+
+ } else { LogAction("setTstatAutoTemps() | Thermostat '${tstat?.displayName}' HVAC Mode is not equal to [AUTO, COOL, or HEAT]", "info", 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 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 smallTempEnum() {
+ def tempUnit = getTemperatureScale()
+ def vals = [
+ 1:"1°${tempUnit}", 2:"2°${tempUnit}", 3:"3°${tempUnit}", 4:"4°${tempUnit}", 5:"5°${tempUnit}", 6:"6°${tempUnit}", 7:"7°${tempUnit}",
+ 8:"8°${tempUnit}", 9:"9°${tempUnit}", 10:"10°${tempUnit}"
+ ]
+ return vals
+}
+
+def switchRunEnum() {
+ def pName = schMotPrefix()
+ def hasFan = atomicState?."${pName}TstatHasFan" ? true : false
+ def vals = [
+ 1:"Heating/Cooling", 2:"With Fan Only"
+ ]
+ if(!hasFan) {
+ vals = [1:"Heating/Cooling"]
+ }
+ return vals
+}
+
+def fanModeTrigEnum() {
+ def pName = schMotPrefix()
+ def canCool = atomicState?."${pName}TstatCanCool" ? true : false
+ def canHeat = atomicState?."${pName}TstatCanHeat" ? true : false
+ def hasFan = atomicState?."${pName}TstatHasFan" ? true : false
+ def vals = ["auto":"Auto", "cool":"Cool", "heat":"Heat", "any":"Any Mode"]
+ if(!canHeat) {
+ vals = ["cool":"Cool", "any":"Any Mode"]
+ }
+ if(!canCool) {
+ vals = ["heat":"Heat", "any":"Any Mode"]
+ }
+ return vals
+}
+
+def tModeHvacEnum(canHeat, canCool) {
+ def vals = ["auto":"Auto", "cool":"Cool", "heat":"Heat"]
+ if(!canHeat) {
+ vals = ["cool":"Cool"]
+ }
+ if(!canCool) {
+ vals = ["heat":"Heat"]
+ }
+ return vals
+}
+
+def alarmActionsEnum() {
+ def vals = ["siren":"Siren", "strobe":"Strobe", "both":"Both (Siren/Strobe)"]
+ return vals
+}
+
+def getEnumValue(enumName, inputName) {
+ def result = "unknown"
+ if(enumName) {
+ enumName?.each { item ->
+ if(item?.key.toString() == inputName?.toString()) {
+ result = item?.value
+ }
+ }
+ }
+ 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)
+ atomicState.sunsetTm = sunsetTm
+ atomicState.sunriseTm = sunriseTm
+}
+
+def parseDt(format, dt) {
+ def result
+ def newDt = Date.parse("$format", dt)
+ result = formatDt(newDt)
+ //log.debug "result: $result"
+ return result
+}
+
+///////////////////////////////////////////////////////////////////////////////
+/******************************************************************************
+| Application Help and License Info Variables |
+*******************************************************************************/
+///////////////////////////////////////////////////////////////////////////////
+def appName() { return "${parent ? "Nest Automations" : "Nest Manager"}${appDevName()}" }
+def appAuthor() { return "Anthony S." }
+def appNamespace() { return "tonesto7" }
+def gitBranch() { return "master" }
+def betaMarker() { return false }
+def appDevType() { return false }
+def appDevName() { return appDevType() ? " (Dev)" : "" }
+def appInfoDesc() {
+ def cur = atomicState?.appData?.updater?.versions?.app?.ver.toString()
+ def beta = betaMarker() ? "" : ""
+ def str = ""
+ str += "${textAppName()}"
+ str += isAppUpdateAvail() ? "\n• ${textVersion()} (Lastest: v${cur})${beta}" : "\n• ${textVersion()}${beta}"
+ str += "\n• ${textModified()}"
+ return str
+}
+def textAppName() { return "${appName()}" }
+def textVersion() { return "Version: ${appVersion()}" }
+def textModified() { return "Updated: ${appVerDate()}" }
+def textAuthor() { return "${appAuthor()}" }
+def textNamespace() { return "${appNamespace()}" }
+def textVerInfo() { return "${appVerInfo()}" }
+def textDonateLink(){ return "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=2CJEVN439EAWS" }
+def stIdeLink() { return "https://graph.api.smartthings.com" }
+def textCopyright() { return "Copyright© 2016 - Anthony S." }
+def textDesc() { return "This SmartApp is used to integrate you're Nest devices with SmartThings as well as allow you to create child automations triggered by user selected actions..." }
+def textHelp() { return "" }
+def textLicense() {
+ return "Licensed under the Apache License, Version 2.0 (the 'License'); "+
+ "you may not use this file except in compliance with the License. "+
+ "You may obtain a copy of the License at"+
+ "\n\n"+
+ " http://www.apache.org/licenses/LICENSE-2.0"+
+ "\n\n"+
+ "Unless required by applicable law or agreed to in writing, software "+
+ "distributed under the License is distributed on an 'AS IS' BASIS, "+
+ "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. "+
+ "See the License for the specific language governing permissions and "+
+ "limitations under the License."
+}
+
+def askAlexaImgUrl() { return "https://raw.githubusercontent.com/MichaelStruck/SmartThingsPublic/master/smartapps/michaelstruck/ask-alexa.src/AskAlexa512.png" }
\ No newline at end of file