/**
* NST Manager
* Copyright (C) 2017-2020 Anthony Santilli
* Author: Anthony Santilli (@tonesto7) Eric Schott (@nh.schottfam)
* November 6,2020
*/
import groovy.json.*
import java.text.SimpleDateFormat
import java.security.MessageDigest
import groovy.transform.Field
definition(
name: "NST Manager",
namespace: "tonesto7",
author: "Anthony S.",
description: "Integrate your Nest products into your Hubitat Elevation Enviroment",
category: "Convenience",
iconUrl: "",
iconX2Url: "",
iconX3Url: "",
importUrl: "https://raw.githubusercontent.com/tonesto7/nst-manager-he/master/apps/nstManager.groovy",
singleInstance: true,
oauth: true
)
static String appVer() { "2.0.8" }
static String namespace() { "tonesto7" }
static Integer devCltNum() { 1 }
static Boolean restEnabled(){ true } // Enables the Rest Stream Device
static Integer DevPoll() { 60 } // 1 minute poll time (when rest is not active)
static Integer StrPoll() { 120 } // 2 minute poll time (when rest is not active)
@SuppressWarnings('unused')
static Integer MetaPoll() { 14400 } // 4 hrs poll time (when rest is not active)
static Integer refreshWait() { 10 } // Restricts Manual Refreshes to every every 10 seconds
static Integer tempChgWaitVal() { 3 } // This is the wait time after manually changing temp before sending the command. It allows successive changes and avoids exceeding nest command limits
preferences {
page(name: "authPage")
page(name: "mainPage")
page(name: "deviceSelectPage")
page(name: "reviewSetupPage")
page(name: "debugPrefPage")
page(name: "devNamePage")
// page(name: "devNameResetPage")
page(name: "nestLoginPrefPage")
page(name: "nestTokenResetPage")
page(name: "automationsPage")
page(name: "automationSchedulePage")
page(name: "notifPrefPage")
}
mappings {
//used during Oauth Authentication
path("/initialize") {action: [GET: "oauthInitUrl"]}
path("/callback") {action: [GET: "callback"]}
}
@Field static final String sNULL = (String)null
@Field static final String sBLK = ''
@Field static final String sSPACE = ' '
@Field static final String sLINEBR = '
'
@Field static final String sBOOL = 'bool'
@Field static final String sENUM = 'enum'
@Field static final String sTRU = 'true'
@Field static final String sFALSE = 'false'
@Field static final String sWARN = 'warn'
@Field static final String sINFO = 'info'
@Field static final String sTRC = 'trace'
@Field static final String sERR = 'error'
@Field static final String sDBG = 'debug'
@Field static final String sCOMPLT = 'complete'
@Field static final String sCLRRED = 'red'
@Field static final String sCLRGRY = 'gray'
@Field static final String sCLRORG = 'orange'
/******************************************************************************
| Application Pages |
*******************************************************************************/
def appInfoSect() {
section() {
String str = """
- ${app.name}
- Version: ${appVer()}
""".toString()
paragraph str
}
}
def authPage() {
//LogTrace("authPage()")
String description
if(getNestAuthToken()!=sNULL) {
description = "You are connected."
return mainPage()
} else {
description = "Tap to enter Nest Login Credentials"
if(!(String)state.access_token) {
getAccessToken()
if(!(String)state.access_token) { enableOauth(); getAccessToken() }
}
Boolean ok4Main = ((String)state.access_token && nestDevAccountCheckOk())
return dynamicPage(name: "authPage", title: sBLK, nextPage: ok4Main ? "mainPage" : sBLK, install: false, uninstall: false) {
if(!ok4Main) {
section () {
String title = sBLK
String desc
if(!(String)state.access_token) {
title = "OAuth Error "
desc = "OAuth is not Enabled for ${app.name} application. Please click remove and review the installation directions again".toString()
} else if(!nestDevAccountCheckOk()) {
title = "Nest Developer Data Missing "
desc = "Client ID and Secret\nAre both missing!\n\nThe built-in Client ID and Secret can no longer be provided.\n\nPlease visit the Wiki at the link below to resolve the issue."
} else {
desc = "Application Status has not received any messages to display"
}
LogAction('Status Message: '+title+desc, sWARN, true)
paragraph desc, required: true, state: sNULL
}
}
section () {
input(name: "useMyClientId", type: sBOOL, title: imgTitle(getAppImg("i_lg"), inputTitleStr("Enter your own ClientId?")), required: false, defaultValue: false, submitOnChange: true)
if(useMyClientId) {
input("clientId", "text", title: imgTitle(getAppImg("i_lg"), inputTitleStr("Nest ClientId")), defaultValue: sBLK, required: true, submitOnChange: true, image: getAppImg("i_lg"))
input("clientSecret", "text", title: imgTitle(getAppImg("i_lg"), inputTitleStr("Nest Client Secret")), defaultValue: sBLK, required: true, submitOnChange: true, image: getAppImg("i_lg"))
} else {
// settingUpdate("clientId", sBLK)
// settingUpdate("clientSecret", sBLK)
}
}
if(ok4Main && getNestAuthToken()==sNULL) {
String redirectUrl = getOauthInitUrl()
LogTrace("AuthToken not found: Directing to Login Page")
section(sectionTitleStr("Nest Authorization Page")) {
String txt = ''
txt += "- Tap Login to Nest below to authorize Hubitat to access your Nest Account.
"
txt += "- You will be taken to the Works with Nest login page.
"
txt += "- Read the permission descriptions and if you Agree press the Accept button.
"
txt += "- You will be redirected back to this page to select your Nest location.
"
txt += "
"
paragraph txt
href url: redirectUrl, style:"external", required: true, title: inputTitleStr("Login to Nest"), description: description
paragraph 'NOTICE: Please use the parent Nest account, Nest Family member accounts will not work correctly
', state: sCOMPLT
}
}
}
}
}
def mainPage() {
//LogTrace("mainPage")
Boolean isInstalled = (Boolean)state.isInstalled == true
Boolean setupComplete = isInstalled
return dynamicPage(name: "mainPage", title: sBLK, nextPage: (!setupComplete ? "reviewSetupPage" : sNULL), install: true, uninstall: isInstalled) {
appInfoSect()
String ttm_str = descriptions('d_ttm')
String ttc_str = descriptions('d_ttc')
if(isInstalled) {
if((String)settings.structures && !(String)state.structures) { state.structures = (String)settings.structures }
section(sectionTitleStr("Nest Location Mode:")) {
String pres = getLocationPresence()
String color = (pres == "away") ? "orange" : (pres == "home" ? "#00c9ff" : sNULL)
paragraph imgTitle(getAppImg("home_icon.png"), strCapitalize(pres ?: "Not Available Yet!"), color), state: sCOMPLT
}
section(sectionTitleStr("Devices & Location:")) {
String t1 = getDevicesDesc(false)
String devDesc = t1 ? t1+'\n\n'+ttm_str : ttc_str
href "deviceSelectPage", title: inputTitleStr("Manage/View Devices"), description: devDesc, state: sCOMPLT
}
}
if(!isInstalled) { devicesPage() }
if(isInstalled) {
if((String)state.structures && ((Map)state.thermostats || (Map)state.protects || (Map)state.cameras)) {
String t1 = getInstAutoTypesDesc()
String autoDesc = t1 ? t1+'\n\n'+ttm_str : ttc_str
section("Manage Automations:") {
href "automationsPage", title: imgTitle(getAppImg("nst_automations_5.png"), inputTitleStr("Automations")), description: autoDesc, state: (t1 ? sCOMPLT : sNULL)
}
}
section("Notifications Options:") {
String t1 = getAppNotifConfDesc()
href "notifPrefPage", title: imgTitle(getAppImg("notification_icon2.png"), inputTitleStr("Notifications")), description: (t1 ? t1+'\n\n'+ttm_str : ttc_str), state: (t1 ? sCOMPLT : sNULL)
}
section(sectionTitleStr("Nest Authentication:")) {
String t1 = getNestAuthToken()!=sNULL ? "Nest Authorized\nLast API Connection:\n• ${getTimestampVal("lastDevDataUpd")}" : sBLK
String authDesc = t1 ? t1+'\n\n'+ttm_str : ttc_str
href "nestLoginPrefPage", title: imgTitle(getAppImg("i_lg"), inputTitleStr("Manage Login")), description: authDesc, state: (t1 ? sCOMPLT : sNULL)
}
section(sectionTitleStr("App Logging:")) {
String t1 = getAppDebugDesc()
href "debugPrefPage", title: imgTitle(getAppImg("log.png"), inputTitleStr("Configure Logging")), description: (t1 ? t1+'\n\n'+ttm_str : ttc_str), state: t1 ? sCOMPLT : sNULL
}
}
}
}
@SuppressWarnings('unused')
def deviceSelectPage() {
Boolean isInstalled = (Boolean)state.isInstalled
return dynamicPage(name: "deviceSelectPage", title: pageTitleStr("Device Selection"), nextPage: (!isInstalled ? "mainPage" : sNULL), install: true, uninstall: false) {
devicesPage()
}
}
def devicesPage() {
Map structs = getNestStructures()
Boolean isInstalled = (Boolean)state.isInstalled
Integer strucSz = (Integer)structs.size()
String structDesc = strucSz==0 ? "No Locations Found" : strucSz.toString()+' Found'
if((Map)state.thermostats || (Map)state.protects || (Map)state.cameras || (Boolean)state.presDevice ) { // if devices are configured, you cannot change the structure until they are removed
section(sectionTitleStr("Nest Location: "+'( '+structDesc+' )')) {
paragraph imgTitle(getAppImg("i_ns"), inputTitleStr("Name:")+' '+(String)structs[(String)state.structures]+"${(structs.size() > 1) ? "\n(Remove All Devices to Change!)" : sBLK}")
}
} else {
section(sectionTitleStr("Select Location:")) {
input(name: "structures", title: imgTitle(getAppImg("i_ns"), inputTitleStr("Available Locations")), type: sENUM, required: true, multiple: false, submitOnChange: true, options: structs)
}
}
if((String)settings.structures) {
state.structures = (String)settings.structures
String newStrucName = structs && structs."${(String)state.structures}" ? (String)structs[(String)state.structures] : sNULL
state.structureName = newStrucName ?: (String)state.structureName
Map stats = getNestThermostats()
Map coSmokes = getNestProtects()
Map cams = getNestCameras()
section(sectionTitleStr("Select Devices:")) {
if(!stats?.size() && !coSmokes.size() && !cams?.size()) { paragraph "No Devices were found
" }
if(stats?.size() > 0) {
input(name: "thermostats", title: imgTitle(getAppImg("i_th"), """Nest Thermostats (${stats?.size()} found)""".toString()), type: sENUM, required: false, multiple: true, submitOnChange: true, options:stats)
}
state.thermostats = (List)settings.thermostats ? statState((List)settings.thermostats) : null
if(coSmokes.size() > 0) {
input(name: "protects", title: imgTitle(getAppImg("i_p"), """Nest Protects (${coSmokes?.size()} found)""".toString()), type: sENUM, required: false, multiple: true, submitOnChange: true, options: coSmokes)
}
state.protects = (List)settings.protects ? coState((List)settings.protects) : null
if(cams.size() > 0) {
input(name: "cameras", title: imgTitle(getAppImg("i_c"), """Nest Cameras (${cams?.size()} found)""".toString()), type: sENUM, required: false, multiple: true, submitOnChange: true, options: cams)
}
state.cameras = (List)settings.cameras ? camState((List)settings.cameras) : null
input(name: "presDevice", title: imgTitle(getAppImg("i_pr"), inputTitleStr("Add Presence Device?")), type: sBOOL, defaultValue: false, required: false, submitOnChange: true)
state.presDevice = (Boolean)settings.presDevice ?: null
input "weatherDevice", "capability.relativeHumidityMeasurement", title: imgTitle(getAppImg("i_t"), inputTitleStr("External Weather Devices?")), required: false, multiple: false, submitOnChange: true
}
Boolean devSelected = ((String)state.structures && ((Map)state.thermostats || (Map)state.protects || (Map)state.cameras || (Boolean)state.presDevice))
if(isInstalled && devSelected) {
section("Customize Device Names:
") {
String descStr = sNULL
if((Boolean)state.devNameOverride) {
if((Boolean)state.custLabelUsed) {
descStr = "• Custom Labels Are Active"
}
if((Boolean)state.useAltNames) {
descStr = "• Using Location Name as Prefix is Active"
}
}
String devDesc = descStr ? '\n'+descStr+'\n\n'+descriptions('d_ttm') : descriptions('d_ttc')
href "devNamePage", title: imgTitle(getAppImg("device_name_icon.png"), inputTitleStr("Device Names")), description: devDesc, state:(!(Boolean)state.devNameOverride || ((Boolean)state.devNameOverride && ((Boolean)state.custLabelUsed || (Boolean)state.useAltNames))) ? sCOMPLT : sBLK
}
}
}
}
@SuppressWarnings('unused')
def reviewSetupPage() {
return dynamicPage(name: "reviewSetupPage", title: sBLK, install: true, uninstall: (Boolean)state.isInstalled) {
section(sectionTitleStr("Device Setup Summary:")) {
String t0 = getDevicesDesc()
String str = t0 ?: sBLK
paragraph title: (!(Boolean)state.isInstalled ? "Devices Pending Install:" : "Installed Devices:"), str
paragraph 'Tap Done to complete the install and create the devices selected
', state: sCOMPLT
}
}
}
@SuppressWarnings('unused')
def devNamePage() {
String pageLbl = (Boolean)state.isInstalled ? "Device Labels" : "Custom Device Labels"
dynamicPage(name: "devNamePage", title: pageLbl, nextPage: sBLK, install: false) {
if((Boolean)settings.devNameOverride == null || (Boolean)state.devNameOverride == null) {
state.devNameOverride = true
settingUpdate("devNameOverride",sTRU,sBOOL)
}
Boolean overrideName = (state.devNameOverride == true)
Boolean altName = ((Boolean)state.useAltNames == true)
Boolean custName = ((Boolean)state.custLabelUsed == true)
section(sectionTitleStr("Device Name Settings")) {
input (name: "devNameOverride", type: sBOOL, title: inputTitleStr("App Overwrites Device Names?"), required: false, defaultValue: overrideName, submitOnChange: true )
if(devNameOverride && !useCustDevNames) {
input (name: "useAltNames", type: sBOOL, title: inputTitleStr("Use Location Name as Prefix?"), required: false, defaultValue: altName, submitOnChange: true, image: sBLK )
}
if(devNameOverride && !useAltNames) {
input (name: "useCustDevNames", type: sBOOL, title: inputTitleStr("Assign Custom Names?"), required: false, defaultValue: custName, submitOnChange: true, image: sBLK )
}
state.devNameOverride = (Boolean)settings.devNameOverride == true
if((Boolean)state.devNameOverride) {
state.useAltNames = (Boolean)settings.useAltNames == true
state.custLabelUsed = (Boolean)settings.useCustDevNames == true
} else {
state.useAltNames = false
state.custLabelUsed = false
}
/*
if((Boolean)state.custLabelUsed) {
paragraph "Custom Labels Are Active", state: sCOMPLT
}
if((Boolean)state.useAltNames) {
paragraph "Using Location Name as Prefix is Active", state: sCOMPLT
}
*/
//paragraph "Current Device Handler Names", image: sBLK
}
Boolean found = false
if((Map)state.thermostats) {
section (sectionTitleStr("Thermostat Device(s):")) {
((Map)state.thermostats)?.each { t ->
found = true
def d = getChildDevice(getNestTstatDni(t))
deviceNameFunc(d, getNestTstatLabel(t.value, t.key), "tstat_${t?.key}_lbl", "thermostat")
}
((Map)state.vThermostats)?.each { t ->
found = true
def d = getChildDevice(getNestvStatDni(t))
deviceNameFunc(d, getNestVtstatLabel(t.value, t.key), "vtstat_${t?.key}_lbl", "thermostat")
}
}
}
if(state.protects) {
section (sectionTitleStr("Protect Device Names:")) {
((Map)state.protects)?.each { p ->
found = true
def d = getChildDevice(getNestProtDni(p))
deviceNameFunc(d, getNestProtLabel(p.value, p.key), "prot_${p?.key}_lbl", "protect")
}
}
}
if(state.cameras) {
section (sectionTitleStr("Camera Device Names:")) {
((Map)state.cameras)?.each { c ->
found = true
def d = getChildDevice(getNestCamDni(c))
deviceNameFunc(d, getNestCamLabel(c.value, c.key), "cam_${c?.key}_lbl", "camera")
}
}
}
if((Boolean)state.presDevice) {
section (sectionTitleStr("Presence Device Name:")) {
found = true
String pLbl = getNestPresLabel()
String dni = getNestPresId()
def d = getChildDevice(dni)
deviceNameFunc(d, pLbl, "presDev_lbl", "presence")
}
}
if(!found) {
paragraph "No Devices Selected
"
}
state.forceChildUpd = true
}
}
def deviceNameFunc(dev, String label, String inputStr, String devType) {
String dstr = sBLK
if(dev) {
dstr += 'Found: '+(String)dev.displayName
if((String)dev.displayName != label) {
String str1 = "\n\nName is not set to default.\nDefault name is:"
dstr += str1+'\n'+label
}
} else {
dstr += 'New Name:\n'+label
}
String dtyp = (Boolean)state.custLabelUsed ? "blank" : devType
paragraph imgTitle(getAppImg(dtyp+'_icon.png'), dstr), state: sCOMPLT
//paragraph "${dstr}", state: sCOMPLT, image: ((Boolean)state.custLabelUsed) ? " " : getAppImg("${devType}_icon.png")
if((Boolean)state.custLabelUsed) {
input "${inputStr}", "text", title: imgTitle(getAppImg(devType+'_icon.png'), inputTitleStr('Custom name for '+label)), defaultValue: label, submitOnChange: true
}
}
String getAppNotifConfDesc() {
String str = sBLK
if(settings.phone || ((Boolean)settings.pushoverEnabled && settings.pushoverDevices)) {
str += ((Boolean)settings.pushoverEnabled) ? "${str != sBLK ? "\n" : sBLK}Pushover: (Enabled)" : sBLK
// str += (settings.phone) ? "${str != sBLK ? "\n" : sBLK}Sending via: (SMS)" : sBLK
if(str != sBLK) {
String t0 = sBLK
t0 += (Boolean)settings.appApiIssuesMsg != false ? "\n • API CMD Failures" : sBLK
t0 += (Boolean)settings.locPresChangeMsg != false ? "\n • Nest Home/Away Status Changes" : sBLK
t0 += (Boolean)settings.camStreamNotifMsg != false ? "\n • Camera Stream Alerts" : sBLK
t0 += (Boolean)settings.automationNotifMsg != false ? "\n • Automation Notifications" : sBLK
if(t0 != sBLK) {
str += "\n\nAlerts:"
str += "${t0}"
}
}
}
return str != sBLK ? str : sNULL
}
@SuppressWarnings('unused')
def notifPrefPage() {
dynamicPage(name: "notifPrefPage", install: false) {
/* section("Enable Text Messaging:") {
input "phone", "phone", title: imgTitle(getAppImg("notification_icon2.png"), inputTitleStr("Send SMS to Number\n(Optional)")), required: false, submitOnChange: true
}*/
section("Enable Notification Devices:") {
input "pushoverEnabled", sBOOL, title: imgTitle(getAppImg("i_pu"), inputTitleStr("Notification Device")), required: false, submitOnChange: true
if((Boolean)settings.pushoverEnabled) {
input "pushoverDevices", "capability.notification", title: imgTitle(getAppImg("i_pu"), inputTitleStr("Notification Device")), required: true, submitOnChange: true
}else state.pushTested=false
}
if(settings.phone || ((Boolean)settings.pushoverEnabled && settings.pushoverDevices)) {
/*
section("Notification Restrictions:") {
def t1 = getNotifSchedDesc()
href "setNotificationTimePage", title: "Notification Restrictions", description: (t1 ?: "Tap to configure"), state: (t1 ? sCOMPLT : sNULL), image: getAppImg("restriction_icon.png")
}
*/
if( ((Boolean)settings.pushoverEnabled && settings.pushoverDevices) && !(Boolean)state.pushTested) {
if(sendMsg("Info", 'Push Notification Test Successful. Notifications Enabled for '+(String)app.label, 0)) {
state.pushTested = true
}
}
section("Alerts:") {
paragraph "Receive notifications when there are issues with the Nest API", state: sCOMPLT
input "appApiIssuesMsg", sBOOL, title: imgTitle(getAppImg("i_i"), inputTitleStr("Notify on API Issues?")), defaultValue: true, submitOnChange: true
paragraph "Get notified when the Location changes from Home/Away", state: sCOMPLT
input "locPresChangeMsg", sBOOL, title: imgTitle(getAppImg("i_pr"), inputTitleStr("Notify on Nest Home/Away changes?")), defaultValue: true, submitOnChange: true
if(settings.cameras) {
paragraph "Get notified on Camera streaming changes", state: sCOMPLT
input "camStreamNotifMsg", sBOOL, title: imgTitle(getAppImg("i_c"), inputTitleStr("Send Cam Streaming Alerts?")), required: false, defaultValue: true, submitOnChange: true
}
paragraph "Automation Notification Messages", state: sCOMPLT
input "automationNotifMsg", sBOOL, title: imgTitle(getAppImg("i_i"), inputTitleStr("Automation Notifications?")), defaultValue: true, submitOnChange: true
}
}
}
}
@SuppressWarnings('unused')
Boolean sendMsg(String msgType, String msg, Integer lvl, pushoverDev=null, sms=null) {
String newMsg = msgType+': '+msg
LogTrace('sendMsg '+lvl.toString()+' '+newMsg)
Boolean retVal = false
if(lvl == 1 && (Boolean)settings.appApiIssuesMsg == false) { return retVal }
if(lvl == 2 && (Boolean)settings.locPresChangeMsg == false) { return retVal }
if(lvl == 3 && (Boolean)settings.camStreamNotifMsg == false) { return retVal }
if((lvl == 4 || lvl == 5) && (Boolean)settings.automationNotifMsg == false) { return retVal }
def notifDev = pushoverDev ?: settings.pushoverDevices
if(notifDev && (Boolean)settings.pushoverEnabled) {
retVal = true
notifDev*.deviceNotification(newMsg)
}
/* String thephone = sms ? sms.toString() : settings.phone ? settings.phone?.toString() : sBLK
if(thephone) {
retVal = true
String t0 = newMsg.take(140)
sendSms(thephone, t0)
}*/
return retVal
}
@SuppressWarnings('unused')
def debugPrefPage() {
dynamicPage(name: "debugPrefPage", install: false) {
section (sectionTitleStr("Application Logs")) {
input ("dbgAppndName", sBOOL, title: imgTitle(getAppImg("log.png"), inputTitleStr("Show App/Device Name on all Log Entries?")), required: false, defaultValue: false, submitOnChange: true)
input ("appDebug", sBOOL, title: imgTitle(getAppImg("log.png"), inputTitleStr("Show ${app.name} Logs in the IDE?")), required: false, defaultValue: false, submitOnChange: true)
if((Boolean)settings.appDebug) {
input ("advAppDebug", sBOOL, title: imgTitle(getAppImg("list_icon.png"), inputTitleStr("Show Verbose (Trace) Logs?")), required: false, defaultValue: false, submitOnChange: true)
input ("showDataChgdLogs", sBOOL, title: imgTitle(getAppImg("i_sw"), inputTitleStr("Show API Data Changed in Logs?")), required: false, defaultValue: false, submitOnChange: true)
} else {
settingUpdate("advAppDebug", sFALSE, sBOOL)
settingUpdate("showDataChgdLogs", sFALSE, sBOOL)
}
}
// section (sectionTitleStr("Reset Application Data")) {
// input (name: "resetAllData", type: sBOOL, title: imgTitle(getAppImg("i_r"), inputTitleStr("Reset Application Data?")), required: false, defaultValue: false, submitOnChange: true)
// }
if((Boolean)settings.appDebug) {
if(getTimestampVal("debugEnableDt") == sNULL) { updTimestampMap("debugEnableDt", getDtNow()) }
} else { updTimestampMap("debugEnableDt") }
state.needChildUpd = true
section("App Info") {
paragraph imgTitle(getAppImg("progress_bar.png"), "Current State Usage:\n${getStateSizePerc()}% (${getStateSize()} bytes)"), required: true, state: (getStateSizePerc() <= 70 ? sCOMPLT : sNULL)
if((Boolean)state.isInstalled && (String)state.structures && ((Map)state.thermostats || (Map)state.protects || (Map)state.cameras)) {
input "enDiagWebPage", sBOOL, title: imgTitle(getAppImg("i_d"), inputTitleStr("Enable Diagnostic Web Page?")), required: false, defaultValue: false, submitOnChange: true
/*
//device won't be created for a while so cannot do this now
if((Boolean)settings.enDiagWebPage) {
def t0 = getRemDiagApp()
String t1 = t0.getAppEndpointUrl("diagHome")
href url: t1, style:"external", title:"NST Diagnostic Web Page", description:"Tap to view", required: true, state: sCOMPLT, image: getAppImg("web_icon.png")
}
*/
}
}
if(getDevOpt()) {
//settingUpdate("enDiagWebPage",sTRU, sBOOL)
}
if((Boolean)settings.enDiagWebPage) {
section("How's Does Log Collection Work:", hideable: true, hidden: true) {
paragraph title: "How will the log collection work?", "When logs are enabled this App will create a child diagnostic app to store your logs which you can view under the diagnostics web page or share the url with the developer for remote troubleshooting.\n\n Turn off to remove the diag app and all data."
}
section("Log Collection:") {
/*
def formatVal = settings.useMilitaryTime ? "MMM d, yyyy - HH:mm:ss" : "MMM d, yyyy - h:mm:ss a"
SimpleDateFormat tf = new SimpleDateFormat(formatVal)
if(getTimeZone()) { tf.setTimeZone(getTimeZone()) }
*/
paragraph "Logging will automatically turn off in 48 hours and all logs will be purged."
//input ("showDataChgdLogs", sBOOL, title: imgTitle(getAppImg("i_sw"), inputTitleStr("Show API Data Changed in Logs?")), required: false, defaultValue: false, submitOnChange: true)
input ("enRemDiagLogging", sBOOL, title: imgTitle(getAppImg("log.png"), inputTitleStr("Enable Log Collection?")), required: false, defaultValue: ((Boolean)state.enRemDiagLogging ?: false), submitOnChange: true)
if((Boolean)state.enRemDiagLogging) {
String str = "Press Done/Save all the way back to the main app page to allow the Diagnostic App to Install"
paragraph str, required: true, state: sCOMPLT
}
}
}
diagLogProcChange((Boolean)settings.enDiagWebPage)
}
}
def getRemDiagApp() {
if((Boolean)settings.enDiagWebPage) {
def remDiagApp = getChildApps()?.find { it?.getAutomationType() == "remDiag" && it?.name == "NST Diagnostics" }
if(remDiagApp) {
//if(remDiagApp?.label != getRemDiagAppChildLabel()) { remDiagApp?.updateLabel(getRemDiagAppChildLabel()) }
return remDiagApp
}
diagLogProcChange((Boolean)settings.enDiagWebPage)
}
return null
}
private void diagLogProcChange(Boolean setOn) {
log.trace "diagLogProcChange($setOn)"
Boolean doInit = false
String msg = "Remote Diagnostic Logs "
Boolean mysetOn = (setOn && (Boolean)settings.enDiagWebPage && (Boolean)settings.enRemDiagLogging)
//log.trace "state: ${(Boolean)state.enRemDiagLogging} time: ${getTimestampVal("remDiagLogActivatedDt")}"
if(mysetOn) {
if(!(Boolean)state.enRemDiagLogging && getTimestampVal("remDiagLogActivatedDt") == sNULL) {
msg += "activated"
doInit = true
updTimestampMap("remDiagLogActivatedDt", getDtNow())
state.enRemDiagLogging = true
updTimestampMap("remDiagDataSentDt", getDtNow()) // allow us some time for child to start
}
} else {
if(getTimestampVal("remDiagLogActivatedDt") != sNULL || (Boolean)state.enRemDiagLogging) {
msg += "deactivated"
settingUpdate("enRemDiagLogging", sFALSE,sBOOL)
state.enRemDiagLogging = false
updTimestampMap("remDiagLogActivatedDt")
atomicState.remDiagLogDataStore = []
doInit = true
}
}
if( ((Boolean)state.remDiagAppAvailable && !(Boolean)settings.enDiagWebPage) ||
((Boolean)state.remDiagAppAvaiable == false && (Boolean)settings.enDiagWebPage) ) {
initRemDiagApp() // create or delete as needed
}
if(doInit) {
log.trace "diagLogProcChange: doInit"
def kdata = getState()?.findAll { (it?.key in ["remDiagLogDataStore" /* , "remDiagDataSentDt"*/ ]) }
kdata.each { kitem ->
state.remove(kitem.key.toString())
}
LogAction(msg, sINFO, true)
if(!(Boolean)state.enRemDiagLogging) { //when turning off, tell automations; turn on - user does done to this app
def cApps = getChildApps()?.findAll { !(it?.getAutomationType() == "remDiag") }
if(cApps) {
cApps?.sort()?.each { chld ->
chld?.updated()
}
}
devs = app.getChildDevices()
devs?.each { dev ->
dev.stateRemove("enRemDiagLogging")
}
}
state.forceChildUpd = true
updTimestampMap("lastAnalyticUpdDt")
}
}
Integer getRemDiagActSec() { return getTimeSeconds("remDiagLogActivatedDt", 100000, "getRemDiagActSec") }
Integer getLastRemDiagSentSec() { return getTimeSeconds("remDiagDataSentDt", 1000, "getLastRemDiagSentSec") }
static Boolean getDevOpt() {
return true
// appSettings?.devOpt.toString() == sTRU ? true : false
}
/******************************************************************************
| PAGE TEXT DESCRIPTION METHODS |
*******************************************************************************/
String getDevicesDesc(Boolean startNewLine=true) {
Boolean pDev = (List)settings.thermostats || (List)settings.protects || (List)settings.cameras
Boolean vDev = (List)settings.vThermostats || (Boolean)settings.presDevice
String str = sBLK
str += pDev ? "${startNewLine ? "\n" : sBLK}Physical Devices:" : sBLK
str += (List)settings.thermostats ? "\n • [${((List)settings.thermostats)?.size()}] Thermostat${(((List)settings.thermostats)?.size() > 1) ? "s" : sBLK}" : sBLK
str += (List)settings.protects ? "\n • [${((List)settings.protects)?.size()}] Protect${(((List)settings.protects)?.size() > 1) ? "s" : sBLK}" : sBLK
str += (List)settings.cameras ? "\n • [${((List)settings.cameras)?.size()}] Camera${(((List)settings.cameras)?.size() > 1) ? "s" : sBLK}" : sBLK
str += vDev ? "${pDev ? "\n" : sBLK}\nVirtual Devices:" : sBLK
str += state.vThermostats ? "\n • [${state.vThermostats?.size()}] Virtual Thermostat${(state.vThermostats?.size() > 1) ? "s" : sBLK}" : sBLK
str += (Boolean)settings.presDevice ? "\n • Presence Device" : sBLK
str += settings.weatherDevice ? "\n • Weather Device Configured" : sBLK
str += (!(List)settings.thermostats && !(List)settings.protects && !(List)settings.cameras && !(Boolean)settings.presDevice) ? "\n • No Devices Selected" : sBLK
return (str != sBLK) ? str : sNULL
}
String getAppDebugDesc() {
String str = sBLK
str += isAppDebug() ? "App Debug: (${debugStatus()})${(Boolean)settings.advAppDebug ? "(Trace)" : sBLK}" : sBLK
str += (Boolean)settings.showDataChgdLogs ? "${str ? "\n" : sBLK}Log API Changes: (${(Boolean)settings.showDataChgdLogs ? "True" : "False"})" : sBLK
str += getRemDiagDesc() ? "${str ? "\n" : sBLK}${getRemDiagDesc()}" : sBLK
return (str != sBLK) ? str : sNULL
}
String getRemDiagDesc() {
String str = sBLK
str += (Boolean)settings.enDiagWebPage ? "Web Page: (${(Boolean)settings.enDiagWebPage})" : sBLK
if((Boolean)settings.enRemDiagLogging) {
str += "\nLog Collection: (${(Boolean)settings.enRemDiagLogging})"
String diagTime = (getTimestampVal("remDiagLogActivatedDt") != sNULL) ? "\n• Will Disable in:\n └ ${getDiagLogTimeRemaining()}" : "\n no time remaining found"
str += diagTime
}
return (str != sBLK) ? str : sNULL
}
/******************************************************************************
* NEST LOGIN PAGES *
*******************************************************************************/
@SuppressWarnings('unused')
def nestLoginPrefPage () {
if(getNestAuthToken()==sNULL) {
return authPage()
} else {
return dynamicPage(name: "nestLoginPrefPage", title: "Nest Authorization Page
", nextPage: getNestAuthToken() ? sBLK : "authPage", install: false) {
String t0=getTimestampVal("authTokenCreatedDt")
updTimestampMap("authTokenCreatedDt", (t0 ?: getDtNow()))
section() {
paragraph "Date Authorized:\n• ${getTimestampVal("authTokenCreatedDt")}".toString(), state: sCOMPLT
if(getTimestampVal("lastDevDataUpd")) {
paragraph "Last API Connection:\n• ${getTimestampVal("lastDevDataUpd")}".toString()
}
}
section(sectionTitleStr("Revoke Authorization Reset:")) {
href "nestTokenResetPage", title: imgTitle(getAppImg("i_r"), inputTitleStr("Log Out and Reset Nest Token")), description: "Tap to Reset Nest Token", required: true, state: sNULL
}
}
}
}
@SuppressWarnings('unused')
def nestTokenResetPage() {
return dynamicPage(name: "nestTokenResetPage", install: false) {
section (sectionTitleStr("Resetting Nest Token")) {
//revokeNestToken()
paragraph "Token Reset Complete...", state: sCOMPLT
paragraph "Press Done/Save to return to Login page"
}
}
}
static String autoAppName() { return "NST Automations" }
@SuppressWarnings('unused')
def automationsPage() {
return dynamicPage(name: "automationsPage", title: "Installed Automations", nextPage: !parent ? sBLK : "automationsPage", install: false) {
def autoApp = getChildApps()?.find { it?.name == autoAppName() || it?.name == "NST Graphs" || it?.name == "NST Diagnostics"}
Boolean autoAppInst = isAutoAppInst()
if(autoApp) { /*Nothing to add here yet*/ }
else {
section(sBLK) {
paragraph "You haven't created any Automations yet!\nTap Create New Automation to get Started"
}
}
section(sBLK) {
app(name: "autoApp", appName: autoAppName(), namespace: "tonesto7", multiple: true, title: imgTitle(getAppImg("nst_automations_5.png"), inputTitleStr("Create New Automation (NST)")))
app(name: "autoApp", appName: "NST Graphs", namespace: "tonesto7", multiple: false, title: imgTitle(getAppImg("i_g"), inputTitleStr("Create Charts Automation")))
app(name: "autoApp", appName: "NST Diagnostics", namespace: "tonesto7", multiple: false, title: imgTitle(getAppImg("i_d"), inputTitleStr("Diagnostics Automation")))
}
if(autoAppInst) {
section("Automation Details:") {
def schEn = getChildApps()?.findAll { (!(it.getAutomationType() in ["nMode", "watchDog", "chart", "remDiag" ]) && it?.getActiveScheduleState()) }
if(schEn?.size()) {
href "automationSchedulePage", title: imgTitle(getAppImg("i_s"), inputTitleStr("View Automation Schedule(s)")), description: sBLK
}
}
section("Advanced Options: (Tap + to Show) ", hideable: true, hidden: true) {
/*
def descStr = sBLK
descStr += (settings.locDesiredCoolTemp || settings.locDesiredHeatTemp) ? "Comfort Settings:" : sBLK
descStr += settings.locDesiredHeatTemp ? "\n • Desired Heat Temp: (${settings.locDesiredHeatTemp}${tUnitStr()})" : sBLK
descStr += settings.locDesiredCoolTemp ? "\n • Desired Cool Temp: (${settings.locDesiredCoolTemp}${tUnitStr()})" : sBLK
descStr += (settings.locDesiredComfortDewpointMax) ? "${(settings.locDesiredCoolTemp || settings.locDesiredHeatTemp) ? "\n\n" : sBLK}Dew Point:" : sBLK
descStr += settings.locDesiredComfortDewpointMax ? "\n • Max Dew Point: (${settings.locDesiredComfortDewpointMax}${tUnitStr()})" : sBLK
descStr += "${(settings.locDesiredCoolTemp || settings.locDesiredHeatTemp) ? "\n\n" : sBLK}${getSafetyValuesDesc()}" ?: sBLK
def prefDesc = (descStr != sBLK) ? descStr+'\n\n'+descriptions('d_ttm') : descriptions('d_ttc')
href "automationGlobalPrefsPage", title: "Global Automation Preferences", description: prefDesc, state: (descStr != sBLK ? sCOMPLT : sNULL), image: getAppImg("global_prefs_icon.png")
*/
input "disableAllAutomations", sBOOL, title: "Disable All Automations?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("disable_icon2.png")
if(state.disableAllAutomations == false && settings.disableAllAutomations) {
toggleAllAutomations(true)
} else if(state.disableAllAutomations && !settings.disableAllAutomations) {
toggleAllAutomations(true)
}
state.disableAllAutomations = settings.disableAllAutomations == true
}
}
state.ok2InstallAutoFlag = true
}
}
def automationSchedulePage() {
dynamicPage(name: "automationSchedulePage", title: "View Schedule Data..", uninstall: false) {
section() {
String str = sBLK
def tz = TimeZone.getTimeZone(location.timeZone.ID)
def sunTimes = app.getSunriseAndSunset()
def sunsetT = Date.parse("E MMM dd HH:mm:ss z yyyy", sunTimes.sunset.toString()).format('h:mm a', tz)
def sunriseT = Date.parse("E MMM dd HH:mm:ss z yyyy", sunTimes.sunrise.toString()).format('h:mm a', tz)
str += "Mode: (${location?.mode})"
str += "\nSunrise: (${sunriseT})"
str += "\nSunset: (${sunsetT})"
paragraph paraTitleStr("Hub Location Info:"), state: sCOMPLT
paragraph sectionTitleStr(str), state: sCOMPLT
}
Integer schSize = 0
getChildApps()?.each { capp ->
Map schInfo = capp.getScheduleDesc()
if(schInfo?.size()) {
Integer curSch = capp.getCurrentSchedule()
schSize = schSize+1
schInfo?.each { schItem ->
section("${capp.label}") {
def schNum = schItem?.key
String schDesc = schItem?.value
Boolean schInUse = (curSch?.toInteger() == schNum?.toInteger())
if(schNum && schDesc) {
paragraph schDesc, state: schInUse ? sCOMPLT : sBLK
}
}
}
}
}
if(schSize < 1) {
section(sBLK) {
paragraph "There is No Schedule Data to Display"
}
}
}
}
private void toggleAllAutomations(Boolean upd = false) {
Boolean t0 = settings.disableAllAutomations == true
state.disableAllAutomations = t0
String disStr = !t0 ? "Returning control to" : "Disabling"
def cApps = getChildApps()
cApps.each { ca ->
LogAction("toggleAllAutomations: ${disStr} automation ${ca.label}", sINFO, true)
ca?.setAutomationStatus(upd)
}
}
Boolean isAutoAppInst() {
Integer chldCnt = 0
childApps?.each { cApp ->
chldCnt = chldCnt + 1
}
return (chldCnt > 0)
}
String getInstAutoTypesDesc() {
Map dat = ["nestMode":0, "watchDog":0, "chart": 0, "remDiag":0, "disabled":0, "schMot":["tSched":0, "remSen":0, "fanCtrl":0, "fanCirc":0, "conWat":0, "extTmp":0, "leakWat":0, "humCtrl":0 ]]
List disItems = []
Map nItems = [:]
List schMotItems = []
childApps?.each { a ->
String type
// String ver
def dis
try {
type = (String)a?.getAutomationType()
dis = (Boolean)a?.getIsAutomationDisabled()
// ver = (String)a?.appVersion()
}
catch(ignored) {
dis = null
// ver = sNULL
type = "old"
}
if(dis) {
disItems.push((String)a.label)
dat["disabled"] = dat["disabled"] ? dat["disabled"]+1 : 1
} else {
String tt1 = sBLK
Boolean clean = false
switch(type) {
case "nMode":
tt1 = "nestMode"
clean = true
break
case "schMot":
List ai
try {
ai = a?.getAutomationsInstalled()
schMotItems += (List)a?.getSchMotConfigDesc(true)
}
catch (Exception ignored) {
log.error "BAD Automation file ${a?.label?.toString()}, please RE-INSTALL automation file"
}
if(ai) {
ai?.each { aut ->
aut?.each { it2 ->
if((String)it2.key == "schMot") {
it2?.value?.each { String it ->
nItems[it] = nItems[it] ? nItems[it]+1 : 1
}
}
}
}
}
dat."schMot" = nItems
break
case "watchDog":
tt1 = "watchDog"
clean = true
break
case "remDiag":
tt1 = "remDiag"
clean = true
break
case "chart":
tt1 = "chart"
clean = true
break
default:
LogAction("Deleting Unknown Automation (${a?.id})", sWARN, true)
deleteChildApp(a.id)
updTimestampMap("lastAnalyticUpdDt")
break
}
if(clean) {
dat."${tt1}" = dat."${tt1}" ? dat."${tt1}"+1 : 1
if(dat."${tt1}" > 1) {
dat."${tt1}" = dat."${tt1}" - 1
LogAction("Deleting Extra ${tt1} (${a?.id})", sWARN, true)
deleteChildApp(a.id)
updTimestampMap("lastAnalyticUpdDt")
}
}
}
}
state.installedAutomations = dat
String str = sBLK
str += (dat.watchDog > 0 || dat?.chart > 0 || dat?.nestMode > 0 || dat?.schMot || dat?.remDiag || dat?.disabled > 0) ? "Installed Automations:" : sBLK
str += (dat.watchDog > 0) ? "\n• Watchdog (Active)" : sBLK
str += (dat.chart > 0) ? "\n• Chart (Active)" : sBLK
str += (dat.remDiag > 0) ? "\n• Remote Diags (Active)" : sBLK
str += (dat.nestMode > 0) ? ((dat?.nestMode > 1) ? "\n• Nest Home/Away (${dat?.nestMode})" : "\n• Nest Home/Away (Active)") : sBLK
def sch = dat.schMot.findAll { it?.value > 0}
str += (sch?.size()) ? "\n• Thermostat (${sch?.size()})" : sBLK
Integer scii = 1
def newList = schMotItems?.unique()
newList?.sort()?.each { sci ->
str += "${scii == newList?.size() ? "\n └" : "\n ├"} $sci"
scii = scii+1
}
str += (disItems?.size() > 0) ? "\n• Disabled: (${disItems?.size()})" : sBLK
return (str != sBLK) ? str : sNULL
}
/******************************************************************************
*######################### NATIVE APP METHODS ############################*
******************************************************************************/
void installed() {
LogAction("Installed with settings: ${settings}", sDBG, true)
initialize()
}
void updated() {
LogAction("${app.label} Updated...with settings: ${settings}", sDBG, true)
if((Boolean)state.needToFinalize) { LogAction("Skipping updated() as auth change in-progress", sWARN, true); return }
initialize()
state.lastUpdatedDt = getDtNow()
}
void uninstalled() {
LogTrace("uninstalled")
try {
state.access_token = null
//Revokes Nest Auth Token
//revokeNestToken()
addRemoveDevices(true)
} catch (ex) {
log.error "uninstalled Exception: ${ex?.message}"
}
}
void initialize() {
LogAction("initialize", sDBG, true)
restStreamHandler(true, "initialize()", false)
unschedule()
unsubscribe()
state.pollingOn = false
state.restStreamingOn = false
state.streamPolling = false
atomicState.diagRunInOn = false
atomicState.workQrunInActive = false
state.pollBlocked = false
state.remove("pollBlockedReason")
stateCleanup()
if(getTimestampVal("debugEnableDt") == sNULL) {
updTimestampMap("debugEnableDt", getDtNow())
settingUpdate("appDebug", sTRU,sBOOL)
runIn(600, logsOff)
} else {
if((Boolean)settings.appDebug || (Boolean)settings.advAppDebug || (Boolean)settings.showDataChgdLogs) { runIn(1800, logsOff) }
}
// force child update on next poll
updTimestampMap("lastChildUpdDt")
updTimestampMap("lastChildForceUpdDt")
updTimestampMap("lastForcePoll")
if((String)settings.structures && (String)state.structures && !(String)state.structureName) {
Map structs = getNestStructures()
if(structs && structs["${(String)state.structures}"]) {
state.structureName = (String)structs[(String)state.structures]
}
}
reInitBuiltins() // get watchDog to release devices
runIn(4, "initialize_Part1", [overwrite: true]) // give time for child apps to run
}
@SuppressWarnings('unused')
void initialize_Part1() {
LogTrace("initialize_Part1")
if(!addRemoveDevices()) {
atomicState.cmdQlist = []
}
state.isInstalled = (List)settings.thermostats || (List)settings.protects || (List)settings.cameras || (Boolean)settings.presDevice
subscriber()
runIn(10, "finishUp", [overwrite: true]) // give time for devices to initialize
}
void finishUp() {
LogTrace("finishUp")
if((Boolean)state.isInstalled) { createSavedNest() }
getChildApps()?.sort()?.each { chld ->
chld?.updated()
}
setPollingState()
}
void logsOff() {
log.warn "${app.label} debug logging disabled...${getDebugLogsOnSec()}"
settingUpdate("appDebug", sFALSE,sBOOL)
settingUpdate("advAppDebug", sFALSE, sBOOL)
settingUpdate("showDataChgdLogs", sFALSE, sBOOL)
updTimestampMap("debugEnableDt")
}
Integer getDebugLogsOnSec() { return getTimeSeconds("debugEnableDt", 0, "getDebugLogsOnSec") }
void reInitBuiltins() {
initWatchdogApp()
initNestModeApp() // this just removes extras
initBuiltin("initChart") // this just removes extras and lets it release device subscriptions
initRemDiagApp()
}
void initNestModeApp() {
initBuiltin("initNestModeApp")
}
void initWatchdogApp() {
initBuiltin("initWatchdogApp")
}
void initRemDiagApp() {
log.trace "initRemDiagApp"
initBuiltin("initRemDiagApp")
}
void initBuiltin(String btype) {
LogTrace("initBuiltin(${btype})")
Boolean keepApp = false
Boolean createApp = false
String autoStr = sBLK
switch (btype) {
case "initNestModeApp":
if(automationNestModeEnabled()) {
keepApp = true
autoStr = "nMode"
}
break
case "initWatchdogApp":
Integer t0 = settings.thermostats?.size()
Integer t1 = settings.cameras?.size()
if((Boolean)state.isInstalled && (t0>0 || t1>0)) {
keepApp = true
createApp = true
}
autoStr = "watchDog"
break
case "initRemDiagApp":
if((Boolean)settings.enDiagWebPage) {
keepApp = true
createApp = true
remDiagAppAvail(true)
} else {
settingUpdate("enRemDiagLogging", sFALSE,sBOOL)
remDiagAppAvail(false)
state.enRemDiagLogging = false
updTimestampMap("remDiagLogActivatedDt")
}
autoStr = "remDiag"
break
case "initChart":
autoStr = "chart"
keepApp = true
break
default:
LogAction("initBuiltin BAD btype ${btype}", sWARN, true)
break
}
//if(isAppLiteMode()) { keepApp = false }
if(autoStr) {
def mynestApp = getChildApps()?.findAll { it?.getAutomationType() == autoStr }
if(createApp && mynestApp?.size() < 1 /* && btype != "initNestModeApp" && btype != "chart" */) {
LogAction("Installing ${autoStr}", sINFO, true)
updTimestampMap("lastAnalyticUpdDt")
try {
if(btype == "initRemDiagApp") {
addChildApp("tonesto7", "NST Diagnostics", getRemDiagAppChildLabel(), null) //[settings:[remDiagFlag:["type":sBOOL, "value":true]]])
}
if(btype == "initWatchdogApp") {
addChildApp("tonesto7", autoAppName(), getWatDogAppChildLabel(), null) //[state:["watchDogFlag":[type:sBOOL, value:true]], state:["autoTyp":[type:"string", value:"watchDog"]]])
}
} catch (ignored) {
Logger("WatchDog create failure", sERR)
//appUpdateNotify(true, "automation")
}
} else if(mynestApp?.size() >= 1) {
Integer cnt = 1
mynestApp?.each { chld ->
if(keepApp && cnt == 1) {
LogTrace("initBuiltin: Running Update Command on ${autoStr}")
chld.updated()
} else if(!keepApp || cnt > 1) {
String slbl = keepApp ? sWARN : sINFO
LogAction("initBuiltin: Deleting ${keepApp ? "Extra " : sBLK}${autoStr} (${chld?.id})", slbl, true)
deleteChildApp(chld.id)
updTimestampMap("lastAnalyticUpdDt")
}
cnt = cnt+1
}
}
}
}
private String getWatDogAppChildLabel() { return (String)location.name+' Watchdog' }
private String getRemDiagAppChildLabel() { return 'NST Location '+(String)location.name+' Diagnostics' }
def subscriber() { }
@SuppressWarnings('unused')
private adj_temp(tempF) {
if(getObjType(tempF) in ["List", "ArrayList"]) {
LogTrace("adj_temp: error temp ${tempF} is list")
}
if(getTemperatureScale() == "C") {
return (tempF - 32) * (5 / 9) as Double
} else {
return tempF
}
}
void setPollingState() {
if(!(Map)state.thermostats && !(Map)state.protects && !(Map)state.cameras && !(Boolean)state.presDevice) {
LogAction("No Devices are Installed | Polling is DISABLED", sINFO, true)
unschedule("poll")
state.pollingOn = false
state.streamPolling = false
} else {
if(getNestAuthToken()==sNULL) {
state.pollingOn = false
}
if(!state.pollingOn && getNestAuthToken()!=sNULL) {
//LogAction("Polling is ACTIVE", sINFO, true)
state.pollingOn = true
Integer pollTime = DevPoll() as Integer
Integer pollStrTime = StrPoll() as Integer
Integer theMax = 60
if(restEnabled() && (Boolean)state.restStreamingOn) {
theMax = 300 // 5 minute poll checks
state.streamPolling = true
}
pollTime = Math.max(pollTime, theMax)
pollStrTime = Math.max(pollStrTime, theMax)
Integer timgcd = gcd([pollTime, pollStrTime])
Random random = new Random()
Integer random_int = random.nextInt(60)
timgcd = (timgcd / 60) < 1 ? 1 : timgcd / 60
Integer random_dint = random.nextInt(timgcd)
LogTrace("Next POLL scheduled (${random_int} ${random_dint}/${timgcd} * * * ?)")
// this runs every timgcd minutes
schedule("${random_int} ${random_dint}/${timgcd} * * * ?", poll)
Integer timChk = state.streamPolling ? 1200 : 240
if(!getTimestampVal("lastDevDataUpd") || getLastDevPollSec() > timChk) {
poll(true)
} else {
runIn(30, "pollFollow", [overwrite: true])
}
}
}
}
@SuppressWarnings('unused')
void startStopStream() {
// log.trace "startStopStream"
if((!restEnabled()) && !(Boolean)state.restStreamingOn) {
return
}
if(restEnabled() && (Boolean)state.restStreamingOn) {
runIn(30, "restStreamCheck", [overwrite: true])
return
}
if(restEnabled() && !(Boolean)state.restStreamingOn) {
restStreamHandler(false, "startStopStream(start stream)")
runIn(30, "restStreamCheck", [overwrite: true])
}
}
def getStreamDevice() {
return getChildDevice(getEventDeviceDni())
}
void restStreamHandler(Boolean close = false, String src, Boolean resetPoll=true) {
LogAction("restStreamHandler(close: ${close}, src: ${src}), resetPoll: ${resetPoll}", sTRC, true)
def dev = getStreamDevice()
if(!dev) {
state.restStreamingOn = false
//return
} else {
if(close) {
state.restStreamingOn = false
dev?.streamStop()
if(state.streamPolling && resetPoll) {
resetPolling()
}
} else {
if(getNestAuthToken()==sNULL) {
LogTrace("restStreamHandler: No authToken")
state.restStreamingOn = false
return
}
dev?.blockStreaming(!restEnabled())
dev?.streamStart()
}
}
}
/*
def setRestActive(val) {
// LogAction("setRestActive($val)", sTRC, true)
if(val == false) {
state.restStreamingOn = false
resetPolling()
}
}
*/
@SuppressWarnings('unused')
void restStreamCheck() {
LogTrace("restStreamCheck")
def streamDev = getStreamDevice()
if(!streamDev) {
state.restStreamingOn = false
resetPolling()
return
}
if(getNestAuthToken()==sNULL) {
//LogAction("restStreamCheck: NestAuthToken Not Found!", sWARN, false)
//return
}
}
private static Integer gcd(Integer a, Integer b) {
while (b > 0) {
Integer temp = b
b = a % b
a = temp
}
return a
}
private static Integer gcd(Listinput = []) {
Integer result = input[0]
for (Integer i = 1; i < input.size; i++) {
result = gcd(result, (Integer)input[i])
}
return result
}
void refresh(child = null) {
String devId = !child?.device?.deviceNetworkId ? child?.toString() : child.device.deviceNetworkId.toString()
//LogAction("Refresh Called by Device: (${child?.device?.displayName}", sDBG, false)
Boolean a=sendNestCmd((String)state.structures, "poll", "poll", 0, devId)
}
/************************************************************************************************
| API/Device Polling Methods |
*************************************************************************************************/
@SuppressWarnings('unused')
void pollFollow() { poll() }
void poll(Boolean force = false, String type = sNULL) {
if(isPollAllowed()) {
if(force) {
forcedPoll(type)
finishPoll()
return
}
Integer pollTime = DevPoll()
if(restEnabled() && (Boolean)state.restStreamingOn) {
pollTime = 300
}
Integer pollTimeout = pollTime*4 + 85
Integer lastCheckin = getLastHeardFromNestSec()
if(lastCheckin > pollTimeout) {
if(restEnabled() && (Boolean)state.restStreamingOn) {
if(lastCheckin < 10000) {
LogAction("We have not heard from Nest Stream in (${lastCheckin}sec.) | Stopping and Restarting Stream", sWARN, true)
}
restStreamHandler(true, "poll", false) // close the stream if we have not heard from it in a while
//state.restStreamingOn = false
}
}
if(state.streamPolling && (!restEnabled() || !(Boolean)state.restStreamingOn)) { // return to normal polling
resetPolling()
return
}
if(restEnabled() && (Boolean)state.restStreamingOn) {
LogTrace("Polling Skipped because Rest Streaming is ON")
if(!state.streamPolling) { // set to stream polling
resetPolling()
return
}
finishPoll()
return
}
runIn(5,"startStopStream", [overwrite: true])
Boolean okStruct = ok2PollStruct()
Boolean okDevice = ok2PollDevice()
Boolean okMeta = ok2PollMetaData()
Boolean meta
Boolean dev = false
Boolean str = false
if(!okDevice && !okStruct && !(getLastHeardFromNestSec() > pollTimeout*3)) {
LogAction("Skipping Poll - Devices Data Updated: (${getLastDevPollSec()}sec) ago | Structure Data Updated: (${getLastStrPollSec()}sec) ago", sINFO, true)
}
else {
String sstr = sBLK
if(okStruct) {
sstr += "Structure Data (Last Updated: ${getLastStrPollSec()} seconds ago)"
str = getApiData("str")
}
if(okDevice) {
sstr += sstr != sBLK ? " | " : sBLK
sstr += "Device Data (Last Updated: ${getLastDevPollSec()} seconds ago)"
dev = getApiData("dev")
}
if(okMeta) {
sstr += sstr != sBLK ? " | " : sBLK
sstr += "Meta Data (Last Updated: ${getLastMetaPollSec()} seconds ago)"
meta = getApiData("meta")
}
if(sstr != sBLK) { LogAction("Gathering Latest Nest ${sstr}", sINFO, true) }
}
finishPoll(str, dev)
}
}
void finishPoll(Boolean str=false, Boolean dev=false) {
//LogTrace("finishPoll($str, $dev) received")
if((Boolean)state.pollBlocked) {
LogAction("Polling BLOCKED | Reason: (${(String)state.pollBlockedReason})", sTRC, true)
if(getLastAnyCmdSentSeconds() > 75) { // if poll is blocked and we have not sent a command recently, try to kick the queues
schedNextWorkQ()
}
return
}
if(getLastChildForceUpdSec() > (15*60)-2) { // if nest goes silent (no changes coming back); force all devices to get an update so they can check health
state.forceChildUpd = true
}
if(dev || str || (Boolean)state.forceChildUpd || (Boolean)state.needChildUpd) { updateChildData() }
apiIssueNotify()
if((Boolean)state.enRemDiagLogging && (Boolean)settings.enRemDiagLogging) {
Boolean a=saveLogtoRemDiagStore(sBLK, sBLK, sBLK, true) // force flush of remote logs
}
}
void resetPolling() {
state.pollingOn = false
state.streamPolling = false
unschedule("poll")
unschedule("finishPoll")
unschedule("postCmd")
unschedule("pollFollow")
setPollingState() // will call poll
}
void schedFinishPoll(Boolean devChg) {
finishPoll(false, devChg)
}
void forcedPoll(String type = sNULL) {
LogTrace("forcedPoll($type) received")
Integer lastFrcdPoll = getLastForcedPollSec()
Integer pollWaitVal = refreshWait()
pollWaitVal = Math.max(pollWaitVal, 10)
if(lastFrcdPoll > pollWaitVal) { // This limits manual forces to 10 seconds or more
updTimestampMap("lastForcePoll", getDtNow())
atomicState.workQrunInActive = false
state.pollBlocked = false
state.remove("pollBlockedReason")
cmdProcState(false)
LogAction("Last Forced Poll was (${lastFrcdPoll} seconds) ago.", sINFO, true)
String str = "Gathering Latest Nest "
if(type == "dev" || !type) {
LogAction("${str}Device Data (forcedPoll)", sINFO, true)
getApiData("dev")
}
if(type == "str" || !type) {
LogAction("${str}Structure Data (forcedPoll)", sINFO, true)
getApiData("str")
}
if(type == "meta" || !type) {
LogAction("${str}Meta Data (forcedPoll)", sINFO, true)
getApiData("meta")
}
updTimestampMap("lastWebUpdDt")
schedNextWorkQ()
} else {
LogAction("Too Soon for Gathering New Data | Elapsed Wait (${lastFrcdPoll}sec.) seconds | Minimum Wait (${refreshWait()}sec.)", sDBG, true)
state.needStrPoll = true
state.needDevPoll = true
}
state.forceChildUpd = true
updateChildData()
}
@SuppressWarnings('unused')
void postCmd() {
//LogTrace("postCmd()")
poll()
}
void remDiagAppAvail(Boolean available) {
state.remDiagAppAvailable = (available == true)
}
void createSavedNest() {
String str = "createSavedNest"
LogTrace("${str}")
if((Boolean)state.isInstalled) {
Map bbb = [:]
Boolean bad = false
if((String)settings.structures && (String)state.structures) {
Map structs = getNestStructures()
String newStrucName = structs && structs."${(String)state.structures}" ? (String)structs[(String)state.structures] : sNULL
if(newStrucName) {
bbb.a_structures_setting = (String)settings.structures
bbb.a_structures_as = (String)state.structures
bbb.a_structure_name_as = (String)state.structureName
Map dData = deviceDataFLD
def t0
t0 = dData?.thermostats?.findAll { (String)it.key in settings.thermostats }
LogAction("${str} | Thermostats(${t0?.size()}): ${settings.thermostats}", sINFO, true)
Map t1 = [:]
t0?.each { devItem ->
LogAction("${str}: Found (${devItem?.value?.name})", sINFO, false)
if(devItem?.key && devItem?.value?.name) {
t1?."${devItem.key.toString()}" = devItem?.value?.name
}
}
Integer t3 = settings.thermostats?.size() ?: 0
if(t1?.size() != t3) { LogAction("Thermostat Counts Wrong! | Current: (${t1?.size()}) | Expected: (${t3})", sERR, true); bad = true }
bbb?.b_thermostats_as = (List)settings.thermostats && dData && (Map)state.thermostats ? t1 : [:]
bbb?.b_thermostats_setting = (List)settings.thermostats ?: []
dData = deviceDataFLD
t0 = null
t0 = dData?.smoke_co_alarms?.findAll { (String)it.key in (List)settings.protects }
LogAction("${str} | Protects(${t0?.size()}): ${(List)settings.protects}", sINFO, true)
t1 = [:]
t0?.each { devItem ->
LogAction("${str}: Found (${devItem?.value?.name})", sINFO, false)
if(devItem?.key && devItem?.value?.name) {
t1."${devItem.key}" = devItem?.value?.name
}
}
t3 = ((List)settings.protects)?.size() ?: 0
if(t1?.size() != t3) { LogAction("Protect Counts Wrong! | Current: (${t1?.size()}) | Expected: (${t3})", sERR, true); bad = true }
bbb.c_protects_as = (List)settings.protects && dData && state.protects ? t1 : [:]
bbb.c_protects_settings = (List)settings.protects ?: []
dData = deviceDataFLD
t0 = null
t0 = dData?.cameras?.findAll { it?.key?.toString() in (List)settings.cameras }
LogAction("${str} | Cameras(${t0?.size()}): ${settings.cameras}", sINFO, true)
t1 = [:]
t0?.each { devItem ->
LogAction("${str}: Found (${devItem?.value?.name})", sINFO, false)
if(devItem?.key && devItem?.value?.name) {
t1."${devItem?.key}" = devItem?.value?.name
}
}
t3 = ((List)settings.cameras)?.size() ?: 0
if(t1?.size() != t3) { LogAction("Camera Counts Wrong! | Current: (${t1?.size()}) | Expected: (${t3})", sERR, true); bad = true }
bbb.d_cameras_as = (List)settings.cameras && dData && state.cameras ? t1 : [:]
bbb.d_cameras_setting = (List)settings.cameras ?: []
} else { LogAction("${str}: No Structures Found!!!", sWARN, true) }
def t0 = state.savedNestSettings ?: null
String t1 = t0 ? new groovy.json.JsonOutput().toJson(t0) : sNULL
String t2 = bbb != [:] ? new groovy.json.JsonOutput().toJson(bbb) : sNULL
if(bad) {
state.savedNestSettingsprev = state.savedNestSettings
state.savedNestSettingslastbuild = bbb
state.remove("savedNestSettings")
}
if(!bad && t2 && (!t0 || t1 != t2)) {
state.savedNestSettings = bbb
state.remove("savedNestSettingsprev")
state.remove("savedNestSettingslastbuild")
//return //true
}
} else { LogAction("${str}: No Structure Settings", sWARN, true) }
} else { LogAction("${str}: NOT Installed!!!", sWARN, true) }
//return //false
}
void mySettingUpdate(String name, value, String type=sNULL) {
if(getDevOpt()) {
LogAction("Setting $name set to type:($type) $value", sWARN, true)
if(!(Boolean)state.ReallyChanged) { return }
}
if((Boolean)state.ReallyChanged) {
settingUpdate(name, value, type)
}
}
void checkRemapping() {
String str = "checkRemapping"
LogTrace(str)
String astr = sBLK
state.ReallyChanged = false
Boolean myRC = (Boolean)state.ReallyChanged
if((Boolean)state.isInstalled && (String)settings.structures) {
Boolean aastr = getApiData("str")
Boolean aadev = getApiData("dev")
//def aameta = getApiData("meta")
Map sData = (Map)state.structData
Map dData = deviceDataFLD
//def mData = state.metaData
def savedNest = state.savedNestSettings
if(sData && dData /* && mData */ && savedNest) {
Map structs = getNestStructures()
if(structs && !getDevOpt() ) {
LogAction("${str}: nothing to do ${structs}", sINFO, true)
//return
} else {
astr += "${str}: found the mess..cleaning up ${structs}"
state.pollBlocked = true
state.pollBlockedReason = "Remapping"
String newStructures_settings = sBLK
List newThermostats_settings = []
Map newvThermostats = [:]
List newProtects_settings = []
List newCameras_settings = []
String oldPresId = getNestPresId()
sData?.each { strucId ->
//def t0 = strucId.key
def t1 = strucId.value
Logger("checkRempapping: t1.name: ${t1?.name?.toString()} a_structure_name_as: ${savedNest?.a_structure_name_as?.toString()}", sINFO)
if(t1?.name && t1?.name?.toString() == savedNest?.a_structure_name_as?.toString()) {
newStructures_settings = [t1?.structure_id]?.join('.') as String
}
}
Logger("checkRempapping: newStructures_settings: ${newStructures_settings.toString()}", sINFO)
if((String)settings.structures && newStructures_settings) {
if((String)settings.structures != newStructures_settings) {
state.ReallyChanged = true
myRC = (Boolean)state.ReallyChanged
astr += ", STRUCTURE CHANGED"
} else {
astr += ", NOTHING REALLY CHANGED (DEVELOPER MODE)"
}
} else { astr += ", no new structure found" }
LogAction(astr, sWARN, true)
astr = sBLK
if(myRC || (newStructures_setting && getDevOpt())) {
mySettingUpdate("structures", newStructures_settings, sENUM)
if(myRC) { state.structures = newStructures_settings }
String newStrucName = newStructures_settings ? (String)((Map)state.structData)[newStructures_settings]?.name : sNULL
astr = "${str}: newStructures ${newStructures_settings} | name: ${newStrucName} | to settings & as structures: ${(String)settings.structures}"
// astr += ",\n as.thermostats: ${state.thermostats} | saveNest: ${savedNest?.b_thermostats_as}\n"
LogAction(astr, sINFO, true)
savedNest?.b_thermostats_as.each { dni ->
String t0 = dni?.key
def dev = getChildDevice(t0)
if(dev) {
// LogAction("${str}: myRC : ${myRC} found dev oldId: ${t0}", sINFO, true)
Boolean gotIt = false
dData?.thermostats?.each { devItem ->
//String t21 = devItem.key
def t22 = devItem.value
String newDevStructId = [t22?.structure_id].join('.')
if(!gotIt && t22 && newDevStructId && newDevStructId == newStructures_settings && dni.value == t22?.name) {
def t6 = [t22?.device_id].join('.')
def t7 = [ ("${t6}".toString()) : dni.value ]
String newDevId
t7.collect { ba ->
newDevId = getNestTstatDni(ba)
}
String rstr = sBLK
if(newDevId) {
newThermostats_settings << newDevId
gotIt = true
rstr = "found newDevId ${newDevId} to replace oldId: ${t0} ${t22?.name} |"
/*
if(settings."${t0}_safety_temp_min") {
mySettingUpdate("${newDevId}_safety_temp_min", settings."${t0}_safety_temp_min", "decimal")
mySettingUpdate("${t0}_safety_temp_min", sBLK)
rstr += ", safety min"
}
if(settings."${t0}_safety_temp_max") {
mySettingUpdate("${newDevId}_safety_temp_max", settings."${t0}_safety_temp_max", "decimal")
mySettingUpdate("${t0}_safety_temp_max", sBLK)
rstr += ", safety max"
}
if(settings."${t0}_comfort_dewpoint_max") {
mySettingUpdate("${newDevId}_comfort_dewpoint_max", settings."${t0}_comfort_dewpoint_max", "decimal")
mySettingUpdate("${t0}_comfort_dewpoint_max", sBLK)
rstr += ", comfort dew"
}
if(settings."${t0}_comfort_humidity_max") {
mySettingUpdate("${newDevId}_comfort_humidity_max", settings."${t0}_comfort_humidity_max", "number")
mySettingUpdate("${t0}_comfort_humidity_max", sBLK)
rstr += ", comfort hum"
}
*/
if(settings."tstat_${t0}_lbl") {
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
mySettingUpdate("tstat_${newDevId}_lbl", settings."tstat_${t0}_lbl", "text")
}
mySettingUpdate("tstat_${t0}_lbl", sBLK)
rstr += ", custom Label"
}
if(state.vThermostats && state."vThermostatv${t0}") {
String physDevId = (String)state."vThermostatMirrorIdv${t0}"
def t1 = state.vThermostats
String t5 = 'v'+newDevId
if(t0 && t0 == physDevId && t1?."v${physDevId}") {
def vdev = getChildDevice("v${t0}")
if(vdev) {
rstr += ", there are virtual devices that match"
if(settings."vtstat_v${t0}_lbl") {
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
mySettingUpdate("vtstat_${t5}_lbl", settings."tstat_v${t0}_lbl", "text")
}
mySettingUpdate("vtstat_v${t0}_lbl", sBLK)
rstr += ", custom vstat Label"
}
newvThermostats."${t5}" = t1."v${t0}"
if(myRC) {
state."vThermostat${t5}" = state."vThermostatv${t0}"
state."vThermostatMirrorId${t5}" = newDevId
state."vThermostatChildAppId${t5}" = state."vThermostatChildAppIdv${t0}"
}
def automationChildApp = getChildApps().find{ it.id == state."vThermostatChildAppIdv${t0}" }
if(automationChildApp != null) {
if(myRC) { automationChildApp.setRemoteSenTstat(newDevId) }
rstr += ", fixed state.remSenTstat"
} else { rstr += ", DID NOT FIND AUTOMATION APP" }
// fix locks
String t3
if(state."remSenLock${t0}") {
rstr += ", fixed locks"
if(myRC) {
state."remSenLock${newDevId}" = state."remSenLock${t0}"
t3 = "remSenLock${t0}"; state.remove(t3.toString())
}
} else { rstr += ", DID NOT FIND LOCK" }
// find the virtual device and reset its dni
rstr += ", reset vDNI"
if(myRC) {
vdev.deviceNetworkId = t5
t3 = "vThermostatv${t0}"; state.remove(t3.toString())
t3 = "vThermostatMirrorIdv${t0}"; state.remove(t3.toString())
t3 = "vThermostatChildAppIdv${t0}"; state.remove(t3.toString())
}
} else { rstr += ", DID NOT FIND VIRTUAL DEVICE" }
def t11 = "oldvstatDatav${t0}"
state.remove(t11.toString())
} else { rstr += ", vstat formality check failed" }
} else { rstr += ", no vstat" }
if(myRC) { dev.deviceNetworkId = newDevId }
}
if(rstr != sBLK) { LogAction("${str}: resultStr: ${rstr}", sINFO, true) }
}
}
if(!gotIt) { LogAction("${str}: NOT matched dev oldId: ${t0}", sWARN, true) }
} else { LogAction("${str}: NOT found dev oldId: ${t0}", sERR, true) }
def t10 = "oldTstatData${t0}"
state.remove(t10.toString())
}
astr = sBLK
if((List)settings.thermostats) {
Integer t0 = settings.thermostats?.size()
Integer t1 = savedNest?.b_thermostats_as?.size()
Integer t2 = newThermostats_settings.size()
if(t0 == t1 && t1 == t2) {
mySettingUpdate("thermostats", newThermostats_settings, sENUM)
astr += "${str}: myRC: ${myRC} newThermostats_settings: ${newThermostats_settings} settings.thermostats: ${settings.thermostats}"
//LogAction("as.thermostats: ${state.thermostats}", sWARN, true)
state.thermostats = null
def t4 = newvThermostats ? newvThermostats?.size() : 0
def t5 = state.vThermostats ? state.vThermostats.size() : 0
if(t4 || t5) {
if(t4 == t5) {
astr += ", AS vThermostats ${newvThermostats}"
if(myRC) { state.vThermostats = newvThermostats }
} else { LogAction("vthermostat sizes don't match ${t4} ${t5}", sWARN, true) }
}
LogAction(astr, sINFO, true)
} else { LogAction("thermostat sizes don't match ${t0} ${t1} ${t2}", sWARN, true) }
}
astr = sBLK
savedNest?.c_protects_as.each { dni ->
String t0 = (String)dni.key
def dev = getChildDevice(t0)
if(dev) {
Boolean gotIt = false
dData?.smoke_co_alarms?.each { devItem ->
astr = sBLK
//String t21 = devItem.key
def t22 = devItem.value
String newDevStructId = [t22?.structure_id].join('.')
if(!gotIt && t22 && newDevStructId && newDevStructId == newStructures_settings && dni.value == t22?.name) {
//def newDevId = [t22?.device_id].join('.')
String t6 = [t22?.device_id].join('.')
def t7 = [ ("${t6}".toString()):dni.value ]
String newDevId
t7.collect { ba ->
newDevId = getNestProtDni(ba)
}
if(newDevId) {
newProtects_settings << newDevId
gotIt = true
astr += "${str}: myRC: ${myRC} found newDevId ${newDevId} to replace oldId: ${t0} ${t22?.name} "
LogAction(astr, sINFO, true)
if(settings."prot_${t0}_lbl") {
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
mySettingUpdate("prot_${newDevId}_lbl", settings."prot_${t0}_lbl", "text")
}
mySettingUpdate("prot_${t0}_lbl", sBLK)
}
if(myRC) { dev.deviceNetworkId = newDevId }
}
}
}
if(!gotIt) { LogAction("${str}: NOT matched dev oldId: ${t0}", sWARN, true) }
} else { LogAction("${str}: NOT found dev oldId: ${t0}", sERR, true) }
def t10 = "oldProtData${t0}"
state.remove(t10.toString())
}
astr = sBLK
if((List)settings.protects) {
Integer t0 = settings.protects?.size()
Integer t1 = savedNest?.c_protects_as?.size()
Integer t2 = newProtects_settings.size()
if(t0 == t1 && t1 == t2) {
mySettingUpdate("protects", newProtects_settings, sENUM)
astr += "newProtects: ${newProtects_settings} settings.protects: ${settings.protects} "
//LogAction("as.protects: ${state.protects}", sWARN, true)
state.protects = null
} else { LogAction("protect sizes don't match ${t0} ${t1} ${t2}", sWARN, true) }
LogAction(astr, sINFO, true)
}
astr = sBLK
savedNest?.d_cameras_as.each { dni ->
String t0 = (String)dni.key
def dev = getChildDevice(t0)
if(dev) {
Boolean gotIt = false
dData?.cameras?.each { devItem ->
astr = sBLK
i//String t21 = devItem.key
def t22 = devItem.value
String newDevStructId = [t22?.structure_id].join('.')
if(!gotIt && t22 && newDevStructId && newDevStructId == newStructures_settings && dni.value == t22?.name) {
//def newDevId = [t22?.device_id].join('.')
String t6 = [t22?.device_id].join('.')
def t7 = [ ("${t6}".toString()):dni.value ]
String newDevId
t7.collect { ba ->
newDevId = getNestCamDni(ba)
}
if(newDevId) {
newCameras_settings << newDevId
gotIt = true
astr += "${str}: myRC: ${myRC} found newDevId ${newDevId} to replace oldId: ${t0} ${t22?.name} "
LogAction(astr, sINFO, true)
if(settings."cam_${t0}_lbl") {
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
mySettingUpdate("cam_${newDevId}_lbl", settings."cam_${t0}_lbl", "text")
}
mySettingUpdate("cam_${t0}_lbl", sBLK)
}
if(myRC) { dev.deviceNetworkId = newDevId }
}
}
}
if(!gotIt) { LogAction("${str}: NOT matched dev oldId: ${t0}", sWARN, true) }
} else { LogAction("${str}: NOT found dev oldId: ${t0}", sERR, true) }
String t10 = "oldCamData${t0}"
state.remove(t10)
}
astr = sBLK
if((List)settings.cameras) {
Integer t0 = settings.cameras?.size()
Integer t1 = savedNest?.d_cameras_as?.size()
Integer t2 = newCameras_settings.size()
if(t0 == t1 && t1 == t2) {
mySettingUpdate("cameras", newCameras_settings, sENUM)
astr += "${str}: newCameras_settings: ${newCameras_settings} settings.cameras: ${settings.cameras}"
//LogAction("as.cameras: ${state.cameras}", sWARN, true)
state.cameras = null
} else { LogAction("camera sizes don't match ${t0} ${t1} ${t2}", sWARN, true) }
LogAction(astr, sINFO, true)
}
/*
The Settings changes made above "do not take effect until a state re-load happens - so you cannot call these here, need to wait a runIn
if(myRC) {
fixDevAS()
}
*/
astr = "oldPresId $oldPresId "
// fix presence
if((Boolean)settings.presDevice) {
if(oldPresId) {
def dev = getChildDevice(oldPresId)
String newId = getNestPresId()
def ndev = getChildDevice(newId)
astr += "| DEV ${dev?.deviceNetworkId} | NEWID $newId | NDEV: ${ndev?.deviceNetworkId} "
String t10 = "oldPresData${dev?.deviceNetworkId}".toString()
state.remove(t10)
if(dev && newId && ndev) { astr += " all good presence" }
else if(!dev) { astr += "where is the pres device?" }
else if(dev && newId && !ndev) {
astr += "will fix presence "
if(myRC) { dev.deviceNetworkId = newId }
} else { LogAction("${dev?.label} $newId ${ndev?.label}", sERR, true) }
} else { LogAction("no oldPresId", sERR, true) }
LogAction(astr, sINFO, true)
}
/*
// fix weather
astr += "oldWeatId $oldWeatId "
if(settings.weatherDevice) {
if(oldWeatId) {
def dev = getChildDevice(oldWeatId)
def newId = getNestWeatherId()
def ndev = getChildDevice(newId)
astr += "| DEV ${dev?.deviceNetworkId} | NEWID $newId | NDEV: ${ndev?.deviceNetworkId} "
def t10 = "oldWeatherData${dev?.deviceNetworkId}"
state.remove(t10.toString())
if(dev && newId && ndev) { astr += " all good weather " }
else if(!dev) { LogAction("where is the weather device?", sWARN, true) }
else if(dev && newId && !ndev) {
astr += "will fix weather"
if(myRC) { dev.deviceNetworkId = newId }
} else { LogAction("${dev?.label} $newId ${ndev?.label}", sERR, true) }
} else { LogAction("no oldWeatId", sERR, true) }
}
LogAction(astr, sINFO, true)
*/
} else { LogAction("no changes or no data a:${(String)settings.structures} b: ${newStructures_settings}", sINFO, true) }
state.pollBlocked = false
state.pollBlockedReason = sBLK
//return
}
} else { LogAction("don't have our data", sWARN, true) }
} else { LogAction("not installed, no structure", sWARN, true) }
}
void fixDevAS() {
LogTrace("fixDevAS")
if((List)settings.thermostats && !(Map)state.thermostats) { state.thermostats = (List)settings.thermostats ? statState((List)settings.thermostats) : null }
if((List)settings.protects && !(Map)state.protects) { state.protects = (List)settings.protects ? coState((List)settings.protects) : null }
if((List)settings.cameras && !(Map)state.cameras) { state.cameras = (List)settings.cameras ? camState((List)settings.cameras) : null }
state.presDevice = (Boolean)settings.presDevice ?: null
//state.weatherDevice = settings.weatherDevice ?: null
}
private Boolean getApiData(String type = sNULL) {
//LogTrace("getApiData($type)")
Boolean result = false
if(!type || getNestAuthToken()==sNULL) { return result }
switch(type) {
case "str":
case "dev":
case "meta":
break
default:
return result
}
String tPath = (type == "str") ? "/structures" : ((type == "dev") ? "/devices" : "/")
Map params = [
uri: getNestApiUrl(),
path: "$tPath",
contentType: "application/json",
headers: ["Authorization": "Bearer ${getNestAuthToken()}"],
timeout: 20
]
try {
httpGet(params) { resp ->
if(resp?.status == 200) {
updTimestampMap("lastHeardFromNestDt", getDtNow())
apiIssueEvent(false)
//state.apiRateLimited = false
//state.apiCmdFailData = null
if(type == "str") {
Map t0 = resp?.data
//LogTrace("API Structure Resp.Data: ${t0}")
if((Map)state.structData == null) { state.structData = t0 }
Boolean chg = didChange((Map)state.structData, t0, "str", "poll")
if(chg) {
result = true
String newStrucName = ((Map)state.structData)?.size() && (String)state.structures ? (String)((Map)state.structData)[(String)state.structures]?.name : sNULL
state.structureName = newStrucName ?: (String)state.structureName
}
}
else if(type == "dev") {
Map t0 = resp?.data
//LogTrace("API Device Resp.Data: ${t0}")
Boolean chg = didChange(deviceDataFLD, t0, "dev", "poll")
if(chg) { result = true }
}
else if(type == "meta") {
//LogTrace("API Metadata Resp.Data: ${resp?.data}")
Map nresp = resp?.data?.metadata
Boolean chg = didChange((Map)state.metaData, nresp, "meta", "poll")
if(chg) { result = true }
}
} else {
LogAction("getApiData - ${type} Received: Resp (${resp?.status})", sERR, true)
apiRespHandler(resp?.status, resp?.data, 'getApiData('+type+')', type+' Poll')
apiIssueEvent(true)
state.forceChildUpd = true
}
}
} catch (ex) {
//state.apiRateLimited = false
state.forceChildUpd = true
if(ex instanceof groovyx.net.http.HttpResponseException && ex?.response) {
apiRespHandler(ex?.response?.status, ex?.response?.data, 'getApiData(ex catch)', type+' Poll')
} else {
if(type == "str") { state.needStrPoll = true }
else if(type == "dev") { state.needDevPoll = true }
else if(type == "meta") { state.needMetaPoll = true }
}
apiIssueEvent(true)
log.error "getApiData (type: $type) Exception: ${ex?.message}"
}
return result
}
def streamDeviceInstalled(val) { state.streamDevice = val }
//void eventStreamActive(Boolean val) { state.eventStreamActive = val }
@Field static Map deviceDataFLD
void receiveEventData(evtData) {
// Map status = [:]
// try {
// LogAction("evtData: $evtData", sTRC, true)
Boolean devChgd = false
Boolean gotSomething = false
if(evtData && evtData.data && restEnabled()) {
if(!(Boolean)state.restStreamingOn) {
state.restStreamingOn = true
// apiIssueEvent(false)
}
if((Map)evtData.data.devices) {
//LogTrace("API Device Resp.Data: ${evtData?.data?.devices}")
gotSomething = true
Boolean chg = didChange(deviceDataFLD, (Map)evtData.data.devices, "dev", "stream")
if(chg) {
devChgd = true
} //else { LogTrace("got deviceData") }
}
if((Map)evtData.data.structures) {
//LogTrace("API Structure Resp.Data: ${evtData?.data?.structures}")
gotSomething = true
Boolean chg = didChange((Map)state.structData, (Map)evtData.data.structures, "str", "stream")
if(chg) {
String newStrucName = state.structData && (String)state.structures ? (String)state.structData[(String)state.structures]?.name : sNULL
state.structureName = newStrucName ?: (String)state.structureName
} //else { LogTrace("got structData") }
}
if(evtData.data.metadata) {
//LogTrace("API Metadata Resp.Data: ${evtData?.data?.metadata}")
gotSomething = true
Boolean chg = didChange((Map)state.metaData, (Map)evtData.data.metadata, "meta", "stream")
//if(!chg) { LogTrace("got metaData") }
}
} else {
LogAction("Did not receive any data in stream response - likely stream shutdown", sWARN, true)
updTimestampMap("lastHeardFromNestDt")
//apiIssueEvent(true)
// if((Boolean)state.restStreamingOn) {
// restStreamHandler(true, "receiveEventData(no data)")
// }
// state.restStreamingOn = false
runIn(6, "pollFollow", [overwrite: true])
}
if(gotSomething) {
updTimestampMap("lastHeardFromNestDt", getDtNow())
//apiIssueEvent(false)
//state.apiRateLimited = false
//state.apiCmdFailData = null
}
if((Boolean)state.forceChildUpd || (Boolean)state.needChildUpd || devChgd) {
schedFinishPoll(devChgd)
}
//status = ["data":"status received...ok", "code":200]
// } catch (ex) {
// log.error "receiveEventData Exception: ${ex?.message}"
// status = ["data":"${ex?.message}", "code":500]
// }
}
Boolean didChange(Map old, Map newer, String type, String src) {
//LogTrace("didChange: type: $type src: $src")
Boolean result = false
String srcStr = src.toUpperCase()
if(newer != null) {
if(type == "str") {
updTimestampMap("lastStrDataUpd", getDtNow())
state.needStrPoll = false
newer.each { // reduce stored state size
if(it?.value) {
String myId = (String)it?.value?.structure_id
if(myId) {
newer[myId].wheres = [:]
}
}
}
}
if(type == "dev") {
updTimestampMap("lastDevDataUpd", getDtNow())
state.needDevPoll = false
newer.each { t -> // This reduces stored state size
String dtyp = t.key
t.value.each {
if(it?.value) {
String myId = (String)it?.value?.device_id
if(myId) {
newer."${dtyp}"[myId].where_id = sBLK
if(newer."${dtyp}"[myId]?.app_url) {
newer."${dtyp}"[myId].app_url = sBLK
}
if(newer."${dtyp}"[myId]?.last_event?.app_url) {
newer."${dtyp}"[myId].last_event.app_url = sBLK
}
if(newer."${dtyp}"[myId]?.last_event?.image_url) {
newer."${dtyp}"[myId].last_event.image_url = sBLK
}
}
}
}
}
}
if(type == "meta") {
updTimestampMap("lastMetaDataUpd", getDtNow())
state.needMetaPoll = false
}
if(type == "str") {
Map tt0 = (Integer)((Map)state.structData)?.size() ? state.structData : null
// Null safe does not work on array references that miss
String myStruc=(String)state.structures
Map t0 = tt0 && myStruc && tt0."${myStruc}" ? (Map)tt0[myStruc] : [:]
Map t1 = newer && myStruc && newer."${myStruc}" ? (Map)newer[myStruc] : [:]
if(t1) {
List chgs = getChanges(t0, t1, "/structures", "structure")
if(chgs) {
result = true
state.forceChildUpd = true
if((Boolean)settings.showDataChgdLogs && !(Boolean)state.enRemDiagLogging) {
if(chgs) { LogAction("STRUCTURE Data Changed ($srcStr): ${chgs}", sINFO, false) }
} else {
LogAction("Nest Structure Data HAS Changed ($srcStr)", sINFO, false)
}
state.structData = newer
}
}
}
else if(type == "dev") {
Boolean devChg = false
def tstats = state.thermostats.collect { dni ->
Boolean a=processChg((String)dni.key, old, newer, "thermostats", "thermostat", srcStr)
devChg = (devChg || a)
result = (a || result)
/* String t1 = (String)dni.key
if(t1 && old && old.thermostats && newer.thermostats && old.thermostats[t1] && newer.thermostats[t1]) {
st1 = new groovy.json.JsonOutput().toJson(old.thermostats[t1])
st2 = new groovy.json.JsonOutput().toJson(newer.thermostats[t1])
if (st1 == st2){
//Nothing to Do
} else {
result = true
state.needChildUpd = true
if((Boolean) settings.showDataChgdLogs && !(Boolean)state.enRemDiagLogging) {
List chgs = getChanges(old.thermostats[t1], newer.thermostats[t1], "/devices/thermostats/${t1}".toString(), "thermostat")
if(chgs) { LogAction("THERMOSTAT Device Changed ($srcStr) | ${getChildDeviceLabel(t1)}: ${chgs}", sINFO, false) }
} else { devChg = true }
}
} */
}
def nProtects = state.protects.collect { dni ->
Boolean a=processChg((String)dni.key, old, newer, "smoke_co_alarms", "protect", srcStr)
devChg = (devChg || a)
result = (a || result)
/* String t1 = (String)dni.key
if(t1 && old && old.smoke_co_alarms && newer.smoke_co_alarms && old.smoke_co_alarms[t1] && newer.smoke_co_alarms[t1]) {
st1 = new groovy.json.JsonOutput().toJson(old.smoke_co_alarms[t1])
st2 = new groovy.json.JsonOutput().toJson(newer.smoke_co_alarms[t1])
if (st1 == st2){
//Nothing to Do
} else {
result = true
state.needChildUpd = true
if((Boolean)settings.showDataChgdLogs && !(Boolean)state.enRemDiagLogging) {
List chgs = getChanges(old.smoke_co_alarms[t1], newer.smoke_co_alarms[t1], "/devices/smoke_co_alarms/${t1}".toString(), "protect")
if(chgs) { LogAction("PROTECT Device Changed ($srcStr) | ${getChildDeviceLabel(t1)}: ${chgs}", sINFO, false) }
} else { devChg = true }
}
} */
}
def nCameras = ((Map)state.cameras).collect { dni ->
Boolean a=processChg((String)dni.key, old, newer, "cameras", "camera", srcStr)
devChg = (devChg || a)
result = (a || result)
/* String t1 = (String)dni.key
if(t1 && old && old.cameras && newer.cameras && old.cameras[t1] && newer.cameras[t1]) {
st1 = new groovy.json.JsonOutput().toJson(old.cameras[t1])
st2 = new groovy.json.JsonOutput().toJson(newer.cameras[t1])
if (st1 == st2){
//Nothing to Do
} else {
result = true
state.needChildUpd = true
if((Boolean)settings.showDataChgdLogs && !(Boolean)state.enRemDiagLogging) {
List chgs = getChanges(old.cameras[t1], newer.cameras[t1], "/devices/cameras/${t1}".toString(), "camera")
if(chgs) { LogAction("CAMERA Device Changed ($srcStr) | ${getChildDeviceLabel(t1)}: ${chgs}", sINFO, false) }
} else { devChg = true }
}
} */
}
if(devChg && !(Boolean)settings.showDataChgdLogs) { LogAction("Nest Device Data HAS Changed ($srcStr)", sINFO, false) }
deviceDataFLD = null
deviceDataFLD = newer
}
else if(type == "meta") {
result = true
state.needChildUpd = true
state.metaData = newer
/*
if((Boolean)settings.showDataChgdLogs != true) {
LogAction("Nest MetaData HAS Changed ($srcStr)", sINFO, false)
} else {
List chgs = getChanges(old, newer, "/metadata", "metadata")
if(chgs) {
LogAction("METADATA Changed ($srcStr): ${chgs}", sINFO, false)
}
}
*/
}
}
//LogAction("didChange: type: $type src: $src result: $result", sINFO, true)
return result
}
Boolean processChg(String dniK, Map old, Map newer, String obj, String objType, String srcStr) {
Boolean result = true
String t1 = dniK
if(t1 && old && old."${obj}" && newer && newer."${obj}") {
Map myMap1 = (Map)old."${obj}"[t1]
Map myMap2 = (Map)newer."${obj}"[t1]
if(myMap1 && myMap2) {
List chgs = getChanges(myMap1, myMap2, "/devices/${obj}/${t1}".toString(), objType)
if (!chgs){
result = false
} else {
if((Boolean)settings.showDataChgdLogs && !(Boolean)state.enRemDiagLogging) {
LogAction("${objType.toUpperCase()} Device Changed ($srcStr) | ${getChildDeviceLabel(t1)}: ${chgs}", sINFO, false)
}
}
}
}
if(result) state.needChildUpd = true
return result
}
List getChanges(mapA, mapB, String headstr, String objType=sNULL) {
def t0 = mapA
def t1 = mapB
def left = t0
def right = t1
List itemsChgd = []
if(left instanceof Map) {
String[] leftKeys = left.keySet()
//String[] rightKeys = right.keySet()
leftKeys.each {
if( left[it] instanceof Map ) {
String tstr=headstr+'/'+it
List chgs = getChanges( left[it], right[it], tstr, objType )
if(chgs && objType!=sNULL) {
itemsChgd += chgs
}
} else {
if(left[it].toString() != right[it].toString()) {
if(objType!=sNULL) {
// LogTrace("getChanges ${headstr} IT: ${it} LEFT: ${left[it]} RIGHT:${right[it]}")
itemsChgd.push(it.toString())
}
}
}
}
if((Integer)itemsChgd.size()) { return itemsChgd }
}
return null
}
private String generateMD5_A(String s){
MessageDigest.getInstance("MD5").digest(s.bytes).encodeHex().toString()
}
void updateChildData(Boolean force = false) {
LogTrace("updateChildData(force: $force) | forceChildUpd: ${(Boolean)state.forceChildUpd} | needChildUpd: ${(Boolean)state.needChildUpd} | pollBlocked: ${(Boolean)state.pollBlocked}".toString())
if((Boolean)state.pollBlocked) { return }
Boolean nforce = (Boolean)state.forceChildUpd
//state.forceChildUpd = true
try {
updTimestampMap("lastChildUpdDt", getDtNow())
if(force || nforce) {
updTimestampMap("lastChildForceUpdDt", getDtNow())
}
String nestTz = getNestTimeZone()?.toString()
String api = apiIssueDesc()
String locPresence = getLocationPresence()
String locSecurityState = getSecurityState()
String locEtaBegin = getEtaBegin()
String locPeakStart = getPeakStart()
String locPeakEnd = getPeakEnd()
// Boolean restStreamingEn = restEnabled() != false
if((Boolean)settings.devNameOverride == null /* || (Boolean)state.useAltNames == null || (Boolean)state.custLabelUsed == null */ ) { // Install / Upgrade force to on
state.devNameOverride = true
settingUpdate("devNameOverride", sTRU, sBOOL)
state.useAltNames = true
settingUpdate("useAltNames", sTRU, sBOOL)
state.custLabelUsed = false
settingUpdate("useCustDevNames", sFALSE, sBOOL)
} else {
state.devNameOverride = (Boolean)settings.devNameOverride == true
if((Boolean)state.useAltNames == null || (Boolean)state.custLabelUsed == null) {
if((Boolean)state.devNameOverride) {
state.useAltNames = (Boolean)settings.useAltNames == true
state.custLabelUsed = (Boolean)settings.useCustDevNames == true
} else {
state.useAltNames = false
state.custLabelUsed = false
}
}
}
Boolean overRideNames = (Boolean)state.devNameOverride == true
def devices = getChildDevices()
devices?.each {
if((Boolean)state.pollBlocked) { return true }
String devId = it?.deviceNetworkId
if(devId && (List)settings.thermostats && deviceDataFLD?.thermostats && deviceDataFLD?.thermostats[devId]) {
Map tData = [data: deviceDataFLD.thermostats[devId], tz: nestTz, apiIssues: api, pres: locPresence, childWaitVal: getChildWaitVal().toInteger(), etaBegin: locEtaBegin]
String oldTstatData = (String)state."oldTstatData${devId}"
String tDataChecksum = generateMD5_A(tData.toString())
state."oldTstatData${devId}" = tDataChecksum
tDataChecksum = (String)state."oldTstatData${devId}"
if(tData && (force || nforce || oldTstatData != tDataChecksum)) {
physDevLblHandler("thermostat", devId, (String)it?.label, "thermostats", (String)tData.data?.name, "tstat", overRideNames)
it.generateEvent(tData)
} else { /* LogTrace("tstat ${devId} did not change") */ }
return true
}
else if(devId && (List)settings.protects && deviceDataFLD?.smoke_co_alarms && deviceDataFLD?.smoke_co_alarms[devId]) {
Map pData = [data: deviceDataFLD.smoke_co_alarms[devId], showProtActEvts: (!showProtActEvts ? false : true), tz: nestTz, apiIssues: api ]
String oldProtData = (String)state."oldProtData${devId}"
String pDataChecksum = generateMD5_A(pData.toString())
state."oldProtData${devId}" = pDataChecksum
pDataChecksum = (String)state."oldProtData${devId}"
if(pData && (force || nforce || oldProtData != pDataChecksum)) {
physDevLblHandler("protect", devId, (String)it?.label, "protects", (String)pData.data?.name, "prot", overRideNames)
it.generateEvent(pData)
} else { /* LogTrace("prot ${devId} did not change") */ }
return true
}
else if(devId && (List)settings.cameras && deviceDataFLD?.cameras && deviceDataFLD?.cameras[devId]) {
Map camData = [data: deviceDataFLD.cameras[devId], tz: nestTz, apiIssues: api, motionSndChgWaitVal: motionSndChgWaitVal, secState: locSecurityState ]
String oldCamData = (String)state."oldCamData${devId}"
String cDataChecksum = generateMD5_A(camData.toString())
state."oldCamData${devId}" = cDataChecksum
cDataChecksum = (String)state."oldCamData${devId}"
if(camData && (force || nforce || oldCamData != cDataChecksum)) {
physDevLblHandler("camera", devId, (String)it?.label, "cameras", (String)camData.data?.name, "cam", overRideNames)
it.generateEvent(camData)
} else { /* LogTrace("cam ${devId} did not change") */ }
return true
}
else if(devId && (Boolean)settings.presDevice && devId == getNestPresId()) {
Map pData = [tz:nestTz, pres: locPresence, apiIssues: api, etaBegin: locEtaBegin, secState: locSecurityState, peakStart: locPeakStart, peakEnd: locPeakEnd ]
String oldPresData = (String)state."oldPresData${devId}"
String pDataChecksum = generateMD5_A(pData.toString())
state."oldPresData${devId}" = pDataChecksum
pDataChecksum = (String)state."oldPresData${devId}"
pData = [tz:nestTz, pres: locPresence, apiIssues: api, lastStrDataUpd: getTimestampVal("lastStrDataUpd"), etaBegin: locEtaBegin, secState: locSecurityState, peakStart: locPeakStart, peakEnd: locPeakEnd ]
if(pData && (force || nforce || oldPresData != pDataChecksum)) {
virtDevLblHandler(devId, (String)it?.label, "pres", "pres", overRideNames)
it.generateEvent(pData)
} else { /* LogTrace("pres ${devId} did not change") */ }
return true
}
else if(devId && state.vThermostats && state."vThermostat${devId}") {
String physdevId = (String)state."vThermostatMirrorId${devId}"
if(physdevId && settings.thermostats && deviceDataFLD?.thermostats && deviceDataFLD?.thermostats[physdevId]) {
Map tmp_data = (Map)deviceDataFLD.thermostats[physdevId]
Map data = tmp_data
def automationChildApp = getChildApps().find{ it.id == state."vThermostatChildAppId${devId}" }
if(automationChildApp != null && !(Boolean)automationChildApp.getIsAutomationDisabled()) {
//data = new JsonSlurper().parseText(JsonOutput.toJson(tmp_data)) // This is a deep clone as object is same reference
data = [:] + tmp_data // This is a deep clone as object is same reference
def tempC
def tempF
if(getTemperatureScale() == "C") {
tempC = automationChildApp.getRemoteSenTemp()
tempF = (tempC * (9 / 5) + 32.0)
} else {
tempF = automationChildApp.getRemoteSenTemp()
tempC = (tempF - 32.0) * (5 / 9) as Double
}
data.ambient_temperature_c = tempC
data.ambient_temperature_f = tempF
def ctempC
def ctempF
if(getTemperatureScale() == "C") {
ctempC = automationChildApp.getRemSenCoolSetTemp()
ctempF = ctempC != null ? (ctempC * (9 / 5) + 32.0) as Integer : null
} else {
ctempF = automationChildApp.getRemSenCoolSetTemp()
ctempC = ctempF != null ? (ctempF - 32.0) * (5 / 9) as Double : null
}
def htempC
def htempF
if(getTemperatureScale() == "C") {
htempC = automationChildApp.getRemSenHeatSetTemp()
htempF = htempC != null ? (htempC * (9 / 5) + 32.0) as Integer : null
} else {
htempF = automationChildApp.getRemSenHeatSetTemp()
htempC = htempF != null ? (htempF - 32.0) * (5 / 9) as Double : null
}
if((String)data?.hvac_mode == "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((String)data?.hvac_mode == "cool") {
data.target_temperature_f = ctempF
data.target_temperature_c = ctempC
} else if((String)data?.hvac_mode == "heat") {
data.target_temperature_f = htempF
data.target_temperature_c = htempC
}
}
Map tData = [data: data, tz: nestTz, apiIssues: api, pres: locPresence, childWaitVal: getChildWaitVal().toInteger(), etaBegin: locEtaBegin, virt: true]
String oldTstatData = (String)state."oldvstatData${devId}"
String tDataChecksum = generateMD5_A(tData.toString())
state."oldvstatData${devId}" = tDataChecksum
tDataChecksum = (String)state."oldvstatData${devId}"
if(tData && (force || nforce || oldTstatData != tDataChecksum)) {
physDevLblHandler("vthermostat", devId, (String)it?.label, "vThermostats", (String)tData.data?.name, "vtstat", overRideNames)
it.generateEvent(tData)
} else { /* LogTrace("tstat ${devId} did not change") */ }
return true
}
}
else if(devId && devId == getNestPresId()) {
return true
}
else if(devId && devId == getEventDeviceDni()) {
return true
}
else {
LogAction("updateChildData() | Unclaimed Device Found (or device with no data available from Nest): (${it?.displayName}) $devId", sWARN, true)
return true
}
}
}
catch (ex) {
log.error "updateChildData Exception: ${ex}"
updTimestampMap("lastChildUpdDt")
return
}
if((Boolean)state.pollBlocked) { return }
if((Boolean)state.forceChildUpd) state.forceChildUpd = false
if((Boolean)state.needChildUpd) state.needChildUpd = false
}
String tUnitStr() {
return "\u00b0${getTemperatureScale()}".toString()
}
private void setDeviceLabel(String devId, String labelStr) {
if(labelStr) {
def dev = getChildDevice(devId)
dev.label = labelStr
}
}
private void physDevLblHandler(String devType, String devId, String devLbl, String devStateName, String apiName, String abrevStr, Boolean ovrRideNames) {
Boolean nameIsDefault = false
String deflbl
String deflblval
state."${devStateName}"?.each { t ->
if(t.key == devId) {
deflblval = (String)t.value
deflbl = getDefaultLabel(devType, deflblval)
}
}
String curlbl = devLbl
if(deflbl && deflbl == curlbl) { nameIsDefault = true }
String newlbl = "getNest${abrevStr.capitalize()}Label"(apiName, devId)
//LogTrace("physDevLblHandler | deflbl: ${deflbl} | curlbl: ${curlbl} | newlbl: ${newlbl} | deflblval: ${deflblval} || devId: ${devId}")
if(ovrRideNames || (nameIsDefault && curlbl != newlbl)) { // label change from nest
if(curlbl != newlbl) {
LogAction('Changing Name of Device from '+curlbl+' to '+newlbl, sINFO, true)
setDeviceLabel(devId, newlbl)
curlbl = newlbl
}
def t0 = state."${devStateName}"
t0[devId] = apiName
state."${devStateName}" = t0
}
String tstr="${abrevStr}_${devId}_lbl".toString()
if((Boolean)state.custLabelUsed && settings."${tstr}" != curlbl) {
settingUpdate(tstr, curlbl)
}
if(!(Boolean)state.custLabelUsed && settings."${tstr}") { settingUpdate(tstr, sBLK) }
tstr="${abrevStr}_${deflblval}_lbl".toString()
if(settings."${tstr}") { settingUpdate(tstr, sBLK) } // clean up old stuff
}
private void virtDevLblHandler(devId, String devLbl, String devMethAbrev, String abrevStr, Boolean ovrRideNames) {
String curlbl = devLbl
String newlbl = "getNest${devMethAbrev.capitalize()}Label"()
//LogTrace("virtDevLblHandler | curlbl: ${curlbl} | newlbl: ${newlbl} || devId: ${devId}")
if(ovrRideNames && curlbl != newlbl) {
LogAction("Changing Name of Device from ${curlbl} to ${newlbl}", sINFO, true)
setDeviceLabel(devId, newlbl?.toString())
curlbl = newlbl?.toString()
}
if((Boolean)state.custLabelUsed && settings."${abrevStr}Dev_lbl" != curlbl) {
settingUpdate("${abrevStr}Dev_lbl", curlbl?.toString())
}
if(!(Boolean)state.custLabelUsed && settings."${abrevStr}Dev_lbl") { settingUpdate("${abrevStr}Dev_lbl", sBLK) }
}
def apiIssues() {
List t0 = (List)state.apiIssuesList ?: [false, false, false, false, false, false, false]
state.apiIssuesList = t0
def result = t0[5..-1].every { it == true } // last 2
String dt = getTimestampVal("apiIssueDt")
if(result) {
String str = dt ? "may still be occurring. Status will clear when last updates are good (Last Updates: ${t0}) | Issues began at ($dt) " : "Detected (${getDtNow()})"
LogAction("Nest API Issues ${str}", sWARN, true)
}
return result
}
String apiIssueDesc() {
String res = "Good"
//this looks at the last 3 items added and determines whether issue is sporadic or outage
List t0 = (List)state.apiIssuesList ?: [false, false, false, false, false, false, false]
state.apiIssuesList = t0
def items = t0[3..-1].findAll { it == true }
//LogTrace("apiIssueDesc: items: $items t0: $t0")
if(items?.size() >= 1 && items?.size() <= 2) { res = "Sporadic Issues" }
else if(items?.size() >= 3) { res = "Full Outage" }
//log.debug "apiIssueDesc: $res"
return res
}
static Integer issueListSize() { return 7 }
Integer getApiIssueSec() { return getTimeSeconds("apiIssueDt", 100000, "getApiIssueSec") }
Integer getLastApiIssueMsgSec() { return getTimeSeconds("lastApiIssueMsgDt", 100000, "getLastApiIssueMsgSec") }
private void apiIssueNotify() {
if( (getApiIssueSec() > 600) && (getLastAnyCmdSentSeconds() > 600)) {
updTimestampMap("apiIssueDt")
state.apiIssuesList = []
if((Boolean)state.apiRateLimited) {
state.apiRateLimited = false
LogAction("Clearing rate Limit", sINFO, true)
}
}
if( !(getLastApiIssueMsgSec() > 900)) { return }
Boolean rateLimit = (Boolean)state.apiRateLimited == true
Boolean apiIssue = apiIssues() ? true : false // any recent API issues
if(apiIssue || rateLimit) {
String msg = sBLK
msg += apiIssue ? "\nThe Nest API appears to be having issues. This will effect the updating of device and location data.\nThe issues started at (${getTimestampVal("apiIssueDt")})" : sBLK
msg += rateLimit ? "${apiIssue ? "\n\n" : "\n"}Your API connection is currently being Rate-limited for excessive commands." : sBLK
if(sendMsg("${app.label} API Issue Warning", msg, 1)) {
updTimestampMap("lastApiIssueMsgDt", getDtNow())
}
}
}
Integer getLastFailedCmdMsgSec() { return getTimeSeconds("lastFailedCmdMsgDt", 100000, "getLastFailedCmdMsgSec") }
private void failedCmdNotify(Map failData, String tstr) {
if(!(getLastFailedCmdMsgSec() > 300)) { return }
Boolean cmdFail = ((String)failData.msg != sNULL)
String cmdstr = tstr ?: (String)state.lastCmdSent
String msg = "\nThe (${cmdstr}) CMD sent to the API has failed.\nStatus Code: ${failData.code}\nErrorMsg: ${failData.msg}\nDT: ${failData.dt}"
if(cmdFail) {
if(sendMsg((String)app.label+' API CMD Failed', msg, 1)) {
updTimestampMap("lastFailedCmdMsgDt", getDtNow())
}
}
LogAction(msg, (cmdFail ? sERR : sWARN), true)
}
private void apiIssueEvent(Boolean issue) {
List list = (List)state.apiIssuesList ?: [false, false, false, false, false, false, false]
Integer listSize = issueListSize()
if(list.size() < listSize) {
list.push(issue)
}
else if(list.size() > listSize) {
Integer nSz = (list.size()-listSize) + 1
List nList = list?.drop(nSz)
nList?.push(issue)
list = nList
}
else if(list.size() == listSize) {
def nList = list.drop(1)
nList?.push(issue)
list = nList
}
state.apiIssuesList = list
if(issue) {
if(!getTimestampVal("apiIssueDt")) {
updTimestampMap("apiIssueDt", getDtNow())
}
} else {
def result = list[3..-1].every { it == false }
Boolean rateLimit = (Boolean)state.apiRateLimited == true
if(rateLimit) {
Integer t0 = state.apiCmdFailData?.dt ? GetTimeDiffSeconds((String)state.apiCmdFailData?.dt, sNULL, "apiIssueEvent").toInteger() : 200
if((t0 > 120 && result) || t0 > 500) {
state.apiRateLimited = false
LogAction("Clearing rate Limit", sINFO, true)
}
}
}
}
private Boolean ok2PollMetaData() {
return pollOk("Meta")
}
private Boolean ok2PollDevice() {
return pollOk("Dev")
}
private Boolean ok2PollStruct() {
return (pollOk("Str") || !state.structData)
}
private Boolean pollOk(String typ) {
if(getNestAuthToken()==sNULL) { return false }
if((Boolean)state.pollBlocked) { return false }
if((Boolean)state."need${typ}Poll") { return true }
Integer pollTime = "${typ}Poll"() as Integer
Integer val = pollTime / 3
val = Math.max(Math.min(val.toInteger(), 50),25)
return (("getLast${typ}PollSec"() + val) > pollTime)
}
private Boolean isPollAllowed() {
return (state.pollingOn && getNestAuthToken()!=sNULL && ((List)settings.thermostats || (List)settings.protects || (List)settings.cameras || (Boolean)settings.presDevice))
}
Integer getLastMetaPollSec() { return getTimeSeconds("lastMetaDataUpd", 100000, "getLastMetaPollSec") }
Integer getLastDevPollSec() { return getTimeSeconds("lastDevDataUpd", 840, "getLastDevPollSec") }
Integer getLastStrPollSec() { return getTimeSeconds("lastStrDataUpd", 1000, "getLastStrPollSec") }
Integer getLastForcedPollSec() { return getTimeSeconds("lastForcePoll", 1000, "getLastForcedPollSec") }
Integer getLastChildUpdSec() { return getTimeSeconds("lastChildUpdDt", 100000, "getLastChildUpdSec") }
Integer getLastChildForceUpdSec() { return getTimeSeconds("lastChildForceUpdDt", 100000, "getLastChildForceUpdSec") }
Integer getLastHeardFromNestSec() { return getTimeSeconds("lastHeardFromNestDt", 100000, "getLastHeardFromNestSec") }
/************************************************************************************************
| Nest API Commands |
*************************************************************************************************/
private void cmdProcState(Boolean value) { atomicState.cmdIsProc = value }
private Boolean cmdIsProc() { return ((Boolean)atomicState.cmdIsProc == true) }
@SuppressWarnings('unused')
private Integer getLastProcSeconds() { return getTimeSeconds("cmdLastProcDt", 0, "getLastProcSeconds") }
static Map apiVar() {
Map 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", fanDuration:"fan_timer_duration", hvacMode:"hvac_mode",
away:"away", streaming:"is_streaming", setTscale:"temperature_scale", eta:"eta"
]
]
return api
}
// There are 3 different return values
def getPdevId(Boolean virt, String devId) {
def pChild = null
if(virt && state.vThermostats && devId) {
if(state."vThermostat${devId}") {
String pdevId = (String)state."vThermostatMirrorId${devId}"
if(pdevId) { pChild = getChildDevice(pdevId) }
if(pChild) { return pChild }
else { return "00000" }
}
}
return pChild
}
/*
void setEtaState(child, etaData, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? child?.toString() : child.device.deviceNetworkId.toString()
String str1 = "setEtaState | "
String strAction = "BAD data"
String strArgs = " ${virtual ? "Virtual " : sBLK}Thermostat (${child?.device?.displayName} - ${devId}) | Trip_Id: ${etaData?.trip_id} | Begin: ${etaData?.estimated_arrival_window_begin} | End: ${etaData?.estimated_arrival_window_end}"
if(etaData?.trip_id && etaData?.estimated_arrival_window_begin && etaData?.estimated_arrival_window_end) {
def etaObj = [ "trip_id":"${etaData.trip_id}", "estimated_arrival_window_begin":"${etaData.estimated_arrival_window_begin}", "estimated_arrival_window_end":"${etaData.estimated_arrival_window_end}" ]
// "trip_id":"sample-trip-id","estimated_arrival_window_begin":"2014-10-31T22:42:00.000Z","estimated_arrival_window_end":"2014-10-31T23:59:59.000Z"
// new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC"))
strAction = "Setting Eta"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
Booelan a=sendNestCmd((String)state.structures, apiVar().rootTypes.struct, apiVar().cmdObjs.eta, etaObj, devId)
return
} else {
if(pChild != "00000") {
LogAction(str1+strAction+strArgs, sDBG, true)
pChild.setNestEta(etaData?.trip_id, etaData?.estimated_arrival_window_begin, etaData.estimated_arrival_window_end) {
}
return
} else {
strAction = "CANNOT Set Eta"
}
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
}
void cancelEtaState(child, trip_id, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? child?.toString() : child.device.deviceNetworkId.toString()
String str1 = "cancelEtaState | "
String strAction = "BAD data"
String strArgs = " ${virtual ? "Virtual " : sBLK}Thermostat (${child?.device?.displayName} - ${devId}) | Trip_Id: ${trip_id}"
if(trip_id) {
def etaObj = [ "trip_id":"${trip_id}", "estimated_arrival_window_begin":0, "estimated_arrival_window_end":0 ]
// "trip_id":"sample-trip-id","estimated_arrival_window_begin":"2014-10-31T22:42:00.000Z","estimated_arrival_window_end":"2014-10-31T23:59:59.000Z"
// new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC"))
strAction = "Cancel Eta"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
Boolean a=sendNestCmd((String)state.structures, apiVar().rootTypes.struct, apiVar().cmdObjs.eta, etaObj, devId)
return
} else {
if(pChild != "00000") {
LogAction(str1+strAction+strArgs, sDBG, true)
pChild.cancelNestEta(trip_id) {
}
return
} else {
strAction = "CANNOT Cancel Eta"
}
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
}
*/
void setCamStreaming(child, streamOn) {
String devId = !child?.device?.deviceNetworkId ? child?.toString() : child.device.deviceNetworkId.toString()
Boolean val = streamOn.toBoolean() ? true : false
LogAction("setCamStreaming | Setting Camera (${child?.device?.displayName} - ${devId}) Streaming to (${val ? "On" : "Off"})", sDBG, true)
Boolean a=sendNestCmd(devId, apiVar().rootTypes.cam, apiVar().cmdObjs.streaming, val, devId)
}
Boolean setStructureAway(child, value, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? sNULL : (String)child.device.deviceNetworkId
Boolean val = value?.toBoolean()
String str1 = "setStructureAway | "
String strAction = "Setting Nest Location:"
String strArgs = " (${child?.device?.displayName} ${!devId ? "" : "- ${devId}"} to (${val ? "Away" : "Home"})"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
if(val) {
Boolean ret = sendNestCmd((String)state.structures, apiVar().rootTypes.struct, apiVar().cmdObjs.away, "away", devId)
// Below is to ensure automations read updated value even if queued
if(ret && state.structData && (String)state.structures && state.structData[(String)state.structures]?.away) {
def t0 = state.structData
t0[(String)state.structures].away = "away"
state.structData = t0
}
return ret
}
else {
Boolean ret = sendNestCmd((String)state.structures, apiVar().rootTypes.struct, apiVar().cmdObjs.away, "home", devId)
if(ret && state.structData && (String)state.structures && state.structData[(String)state.structures]?.away) {
Map t0 = (Map)state.structData
t0[(String)state.structures].away = "home"
state.structData = t0
}
return ret
}
} else {
if(pChild != "00000") {
LogAction(str1+strAction+strArgs, sDBG, true)
if(val) {
pChild.away()
} else {
pChild.present()
}
return true
} else {
strAction = "CANNOT Set Location"
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
return false
}
Boolean setFanMode(child, fanOn, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? sNULL : (String)child.device.deviceNetworkId
Boolean val = fanOn.toBoolean()
String str1 = "setFanMode | "
String strAction = "Setting"
String strArgs = " ${virtual ? "Virtual " : sBLK}Thermostat (${child?.device?.displayName} - ${devId}) Fan Mode to (${val ? "On" : "Auto"})"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.fanActive, val, devId)
} else {
if(pChild != "00000") {
LogAction(str1+strAction+strArgs, sDBG, true)
if(val) {
pChild.fanOn()
} else {
pChild.fanAuto()
}
return true
} else {
strAction = "CANNOT Set"
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
return false
}
Boolean setHvacMode(child, String mode, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? sNULL : (String)child.device.deviceNetworkId
String str1 = "setHvacMode | "
String strAction = "Setting"
String strArgs = " ${virtual ? "Virtual " : sBLK}Thermostat (${child?.device?.displayName} - ${devId}) HVAC Mode to (${mode})"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.hvacMode, mode.toString(), devId)
} else {
if(pChild != "00000") {
LogAction(str1+strAction+strArgs, sDBG, true)
switch (mode) {
case "heat-cool":
pChild.auto()
break
case "heat":
pChild.heat()
break
case "cool":
pChild.cool()
break
case "eco":
pChild.eco()
break
case "off":
pChild.off()
break
case "emergency heat":
pChild.emergencyHeat()
break
default:
LogAction("setHvacMode: Invalid Request: ${mode}", sWARN, true)
break
}
return true
} else {
strAction = "CANNOT Set "
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
return false
}
Boolean setTargetTemp(child, String unit, temp, String mode, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? sNULL : (String)child.device.deviceNetworkId
String str1 = "setTargetTemp | "
String strAction = "Setting"
String strArgs = " ${virtual ? "Virtual " : sBLK}Thermostat (${child?.device?.displayName} - ${devId}) Target Temp to (${temp}${tUnitStr()})"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
if(unit == "C") {
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetC, temp, devId)
}
else {
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetF, temp, devId)
}
} else {
LogAction(str1+strAction+strArgs, sDBG, true)
String appId = (String)state."vThermostatChildAppId${devId}"
def automationChildApp
if(appId) { automationChildApp = getChildApps().find{ it?.id == appId } }
if(automationChildApp) {
Boolean res = automationChildApp.remSenTempUpdate(temp,mode)
if(res) { return res }
}
if(pChild != "00000") {
if(mode == 'cool') {
pChild.setCoolingSetpoint(temp)
} else if(mode == 'heat') {
pChild.setHeatingSetpoint(temp)
} else { LogAction("setTargetTemp - UNKNOWN MODE (${mode}) child ${pChild}", sWARN, true); return false }
return true
} else {
strAction = "CANNOT Set"
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
return false
}
Boolean setTargetTempLow(child, unit, temp, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? sNULL : (String)child.device.deviceNetworkId
String str1 = "setTargetTempLow | "
String strAction
strAction = "Setting"
String strArgs = " ${virtual ? "Virtual " : sBLK}Thermostat (${child?.device?.displayName} - ${devId}) Target Temp Low to (${temp}${tUnitStr()})"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
if(unit == "C") {
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetLowC, temp, devId)
}
else {
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetLowF, temp, devId)
}
} else {
LogAction(str1+strAction+strArgs, sDBG, true)
String appId = (String)state."vThermostatChildAppId${devId}"
def automationChildApp
if(appId) { automationChildApp = getChildApps().find{ it?.id == appId } }
if(automationChildApp) {
Boolean res = automationChildApp.remSenTempUpdate(temp,"heat")
if(res) { return res }
}
if(pChild != "00000") {
pChild.setHeatingSetpoint(temp)
return true
} else {
strAction = "CANNOT Set"
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
return false
}
Boolean setTargetTempHigh(child, unit, temp, Boolean virtual=false) {
String devId = !child?.device?.deviceNetworkId ? sNULL : (String)child.device.deviceNetworkId
String str1 = "setTargetTempHigh | "
String strAction = "Setting"
String strArgs = " ${virtual ? "Virtual " : sBLK}Thermostat (${child?.device?.displayName} - ${devId}) Target Temp High to (${temp}${tUnitStr()})"
def pChild = getPdevId(virtual, devId)
if(pChild == null) {
LogAction(str1+strAction+strArgs, sDBG, true)
if(unit == "C") {
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetHighC, temp, devId)
}
else {
return sendNestCmd(devId, apiVar().rootTypes.tstat, apiVar().cmdObjs.targetHighF, temp, devId)
}
} else {
LogAction(str1+strAction+strArgs, sDBG, true)
String appId = (String)state."vThermostatChildAppId${devId}"
def automationChildApp
if(appId) { automationChildApp = getChildApps().find{ it?.id == appId } }
if(automationChildApp) {
Boolean res = automationChildApp.remSenTempUpdate(temp,"cool")
if(res) { return res }
}
if(pChild != "00000") {
pChild.setCoolingSetpoint(temp)
return true
} else {
strAction = "CANNOT Set"
}
}
LogAction(str1+strAction+strArgs, sWARN, true)
return false
}
Boolean sendNestCmd(String cmdTypeId, String cmdType, String cmdObj, cmdObjVal, String childId) {
// LogAction("sendNestCmd $cmdTypeId, $cmdType, $cmdObj, $cmdObjVal, $childId", sINFO, true)
if(getNestAuthToken()==sNULL) {
LogAction("sendNestCmd Error | Nest Auth Token Not Found", sWARN, true)
return false
}
try {
if(cmdTypeId) {
Integer qnum = getQueueNumber(cmdTypeId)
if(qnum == -1 ) { return false }
state.pollBlocked = true
state.pollBlockedReason = "Sending Cmd"
List cmdData = [cmdTypeId, cmdType, cmdObj, cmdObjVal, now()]
List tempQueue = []
List newCmd = []
Boolean replaced = false
Boolean skipped = false
List cmdQueue = (List)atomicState."cmdQ${qnum}"
if(cmdQueue == null) { cmdQueue = [] }
cmdQueue.each { cmd ->
if(newCmd != []) {
tempQueue << newCmd
}
newCmd = [cmd[0], cmd[1], cmd[2], cmd[3], cmd[4]]
}
if(newCmd != []) { // newCmd is last command in queue
if((String)newCmd[1] == cmdType && (String)newCmd[2] == cmdObj && newCmd[3] == cmdObjVal) { // Exact same command; leave it and skip
skipped = true
tempQueue << newCmd
} else if((String)newCmd[1] == cmdType && (String)newCmd[2] == cmdObj &&
(String)newCmd[2] != (String)apiVar().cmdObjs.away &&
(String)newCmd[2] != (String)apiVar().cmdObjs.fanActive &&
(String)newCmd[2] != (String)apiVar().cmdObjs.fanTimer && (String)newCmd[2] != (String)apiVar().cmdObjs.eta) {
// if we are changing the same setting again use latest - this is Temp settings, hvac
replaced = true
tempQueue << cmdData
} else {
tempQueue << newCmd
tempQueue << cmdData
}
} else {
tempQueue << cmdData
}
atomicState."cmdQ${qnum}" = tempQueue
String str = "Adding"
if(replaced) { str = "Replacing" }
if(skipped) { str = "Skipping" }
if(replaced || skipped) {
LogAction("Command Matches the Last item in Queue ${qnum} - ${str}", sWARN, true)
}
LogAction("${str} Cmd to Queue [${qnum}] (Queued Items: ${tempQueue?.size()}): $cmdTypeId, $cmdType, $cmdObj, $cmdObjVal, $childId", sINFO, true)
state.lastQcmd = cmdData
schedNextWorkQ()
return true
} else {
LogAction("sendNestCmd null cmdTypeId $cmdTypeId, $cmdType, $cmdObj, $cmdObjVal, $childId", sWARN, true)
return false
}
}
catch (ex) {
log.error "sendNestCmd Exception: ${ex?.message}"
return false
}
}
/*
* Each nest device has its own queue (as does the nest structure itself)
* Queues are "assigned" dynamically as they are needed
* Each queue has it own "free" command counts, then commands are limited to 1 per minute.
*/
private Integer getQueueNumber(String cmdTypeId) {
List t0=(List)atomicState.cmdQlist
List cmdQueueList = t0 ?: []
if(t0==null) atomicState.cmdQlist = cmdQueueList
Integer qnum = cmdQueueList.indexOf(cmdTypeId)
if(qnum == -1) {
// need semaphore
cmdQueueList = (List)atomicState.cmdQlist
cmdQueueList << cmdTypeId
atomicState.cmdQlist = cmdQueueList
qnum = cmdQueueList.indexOf(cmdTypeId)
atomicState."cmdQ${qnum}" = null
setLastCmdSentSeconds(qnum, sNULL)
}
qnum = cmdQueueList.indexOf(cmdTypeId)
if(qnum == -1 || qnum == (Integer)null) { LogAction("getQueueNumber: NOT FOUND", sWARN, true ) }
else {
if(getLastCmdSentSeconds(qnum) > 3600) { setRecentSendCmd(qnum, cmdMaxVal()) } // if nothing sent in last hour, reset command limit
}
return qnum
}
/*
* Queues are processed in the order in which commands were sent (across all queues)
* This maintains proper state ordering for changes, as commands can have dependencies in order
*/
Integer getQueueToWork() {
Integer qnum
Long savedtim
List t0=(List)atomicState.cmdQlist
List cmdQueueList = t0 ?: []
if(t0==null) atomicState.cmdQlist = cmdQueueList
cmdQueueList.eachWithIndex { val, idx ->
List cmdQueue = (List)atomicState."cmdQ${idx}"
if(cmdQueue?.size() > 0) {
List cmdData = cmdQueue[0]
Long timVal = (Long)cmdData[4]
if(savedtim == null || timVal < savedtim) {
savedtim = timVal
qnum = idx
}
}
}
// LogTrace("getQueueToWork queue: ${qnum}")
if(qnum != -1 && qnum != null) {
if(getLastCmdSentSeconds(qnum) > 3600) { setRecentSendCmd(qnum, cmdMaxVal()) } // if nothing sent in last hour, reset command limit
}
return qnum
}
private static Integer cmdMaxVal() { return 2 }
void schedNextWorkQ(Boolean useShort=false) {
Integer cmdDelay = getChildWaitVal()
if(useShort) { cmdDelay = 0 }
//
// 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.
// https://nestdevelopers.io/t/user-receiving-exceeded-rate-limit-on-requests-please-try-again-later/354
//
Integer qnum = getQueueToWork()
Integer timeVal = cmdDelay
String str
Integer queueItemsAvail
Integer lastCommandSent
if(qnum != null) {
queueItemsAvail = getRecentSendCmd(qnum)
lastCommandSent = getLastCmdSentSeconds(qnum)
if( (queueItemsAvail == 0 && lastCommandSent > 60) ) { queueItemsAvail = 1 }
if( queueItemsAvail <= 0 || (Boolean)state.apiRateLimited) {
timeVal = 60 + cmdDelay
} else if(lastCommandSent < 60) {
timeVal = (60 - lastCommandSent + cmdDelay)
if(queueItemsAvail > 0) { timeVal = 0 }
}
str = timeVal > cmdDelay || (Boolean)state.apiRateLimited ? "*RATE LIMITING ON* " : sBLK
//LogAction("schedNextWorkQ │ ${str}queue: ${qnum} │ schedTime: ${timeVal} │ recentSendCmd: ${queueItemsAvail} │ last seconds: ${lastCommandSent} │ cmdDelay: ${cmdDelay} | runInActive: ${atomicState.workQrunInActive} | Api Limited: ${(Boolean)state.apiRateLimited}", sINFO, true)
} else {
return //timeVal = 0
}
String actStr = "ALREADY PENDING "
if(cmdIsProc()) { actStr = "COMMAND RUNNING " }
if(!(Boolean)atomicState.workQrunInActive && !cmdIsProc() ) {
atomicState.workQrunInActive = true
if(timeVal != 0) {
actStr = "RUNIN "
runIn(timeVal.toInteger(), "workQueue", [overwrite: true])
} else {
actStr = "DIRECT CALL "
workQueue()
}
}
LogAction("schedNextWorkQ ${actStr} │ ${str}queue: ${qnum} │ schedTime: ${timeVal} │ recentSendCmd: ${queueItemsAvail} │ last seconds: ${lastCommandSent} │ cmdDelay: ${cmdDelay} | runInActive: ${atomicState.workQrunInActive} | command proc: ${cmdIsProc()} | Api Limited: ${(Boolean)state.apiRateLimited}", sINFO, true)
}
private Integer getRecentSendCmd(Integer qnum) {
return atomicState."recentSendCmd${qnum}"
}
private void setRecentSendCmd(Integer qnum, Integer val) {
atomicState?."recentSendCmd${qnum}" = val
}
def sendEcoActionDescToDevice(dev, desc) {
if(dev && desc) {
dev?.ecoDesc(desc)
}
}
private Integer getLastAnyCmdSentSeconds() { return getTimeSeconds("lastCmdSentDt", 3601, "getLastAnyCmdSentSeconds") }
private Integer getLastCmdSentSeconds(Integer qnum) { return getTimeSeconds("lastCmdSentDt${qnum}", 3601, "getLastCmdSentSeconds") }
private void setLastCmdSentSeconds(Integer qnum, String val) {
updTimestampMap("lastCmdSentDt${qnum}", val)
updTimestampMap("lastCmdSentDt", val)
}
/*
void storeLastCmdData(cmd, qnum) {
if(cmd) {
def newVal = ["qnum":qnum, "obj":cmd[2], "value":cmd[3], "date":getDtNow()]
def list = state.cmdDetailHistory ?: []
Integer listSize = 30
if(list?.size() < listSize) {
list.push(newVal)
}
else if(list?.size() > listSize) {
Integer nSz = (list?.size()-listSize) + 1
def nList = list?.drop(nSz)
nList?.push(newVal)
list = nList
}
else if(list?.size() == listSize) {
def nList = list?.drop(1)
nList?.push(newVal)
list = nList
}
if(list) { state.cmdDetailHistory = list }
}
}
*/
void workQueue() {
LogTrace("workQueue")
atomicState.workQrunInActive = false
//def cmdDelay = getChildWaitVal()
List tt0=(List)atomicState.cmdQlist
List cmdQueueList = tt0 ?: []
if(tt0==null) atomicState.cmdQlist = cmdQueueList
Integer qnum = getQueueToWork()
if(qnum == null) { qnum = 0 }
Listt0=(List)atomicState."cmdQ${qnum}"
List cmdQueue = t0 ?: []
if(t0==null) atomicState."cmdQ${qnum}" = cmdQueue
try {
if(cmdQueue?.size() > 0) {
LogTrace("workQueue │ Run Queue: ${qnum}")
runIn(90, "workQueue", [overwrite: true]) //lost schedule catchall
if(!cmdIsProc()) {
cmdProcState(true)
state.pollBlocked = true
state.pollBlockedReason = "Processing Queue"
cmdQueue = (List)atomicState."cmdQ${qnum}"
// log.trace "cmdQueue(workqueue): $cmdQueue"
List cmd = cmdQueue?.remove(0)
// log.trace "cmdQueue(workqueue-after): $cmdQueue"
// log.debug "cmd: $cmd"
atomicState."cmdQ${qnum}" = cmdQueue
Boolean cmdres
if(getLastCmdSentSeconds(qnum) > 3600) { setRecentSendCmd(qnum, cmdMaxVal()) } // if nothing sent in last hour, reset command limit
// storeLastCmdData(cmd, qnum)
if((String)cmd[1] == "poll") {
state.needStrPoll = true
state.needDevPoll = true
state.forceChildUpd = true
cmdres = true
} else {
//cmdres = procNestCmd(getNestApiUrl(), cmd[0], cmd[1], cmd[2], cmd[3], qnum)
cmdres = queueProcNestCmd(getNestApiUrl(), (String)cmd[0], (String)cmd[1], (String)cmd[2], cmd[3], qnum, cmd)
return
}
finishWorkQ(cmd, cmdres)
} else { LogAction("workQueue: busy processing command", sWARN, true) }
} else { state.pollBlocked = false; state.remove("pollBlockedReason"); cmdProcState(false) }
}
catch (ex) {
log.error "workQueue Exception Error: ${ex?.message}"
finishERR()
}
}
void finishERR(){
cmdProcState(false)
state.needDevPoll = true
state.needStrPoll = true
state.forceChildUpd = true
state.pollBlocked = false
state.remove("pollBlockedReason")
atomicState.workQrunInActive = true
runIn(60, "workQueue", [overwrite: true])
runIn(64, "postCmd", [overwrite: true])
}
void finishWorkQ(List cmd, Boolean result) {
LogTrace("finishWorkQ cmd: $cmd | result: $result")
Integer cmdDelay = getChildWaitVal()
if( !result ) {
state.forceChildUpd = true
state.pollBlocked = false
state.remove("pollBlockedReason")
runIn((cmdDelay * 3).toInteger(), "postCmd", [overwrite: true])
}
state.needDevPoll = true
if(cmd && (String)cmd[1] == (String)apiVar().rootTypes.struct) {
state.needStrPoll = true
state.forceChildUpd = true
}
updTimestampMap("cmdLastProcDt", getDtNow())
cmdProcState(false)
Integer qnum = getQueueToWork()
if(qnum == null) { qnum = 0 }
if(!atomicState?."cmdQ${qnum}") { atomicState?."cmdQ${qnum}" = [] }
List cmdQueue = (List)atomicState."cmdQ${qnum}"
if(cmdQueue?.size() == 0) {
state.pollBlocked = false
state.remove("pollBlockedReason")
state.needChildUpd = true
runIn(cmdDelay, "postCmd", [overwrite: true])
}
else { schedNextWorkQ(true) }
if(cmdQueue?.size() > 10) {
sendMsg("Warning", "There is now ${cmdQueue?.size()} events in the Command Queue. Something must be wrong", 1)
LogAction("${cmdQueue?.size()} events in the Command Queue", sWARN, true)
}
//return
}
Boolean queueProcNestCmd(String uri, String typeId, String type, String obj, objVal, Integer qnum, List cmd, Boolean redir = false) {
String myStr = "queueProcNestCmd"
LogTrace("${myStr}: typeId: ${typeId}, type: ${type}, obj: ${obj}, objVal: ${objVal}, qnum: ${qnum}, isRedirUri: ${redir}")
Boolean result = false
String tok=getNestAuthToken()
if(tok==sNULL) { return result }
try {
if(getLastAnyCmdSentSeconds() > 120) {
state.nestRedirectUrl = sNULL
state.remove("nestRedirectUrl") // don't cache the redirect URL too long
}
String url = (!redir && (String)state.nestRedirectUrl) ? (String)state.nestRedirectUrl : uri
//String url = uri
String urlPath = "/${type}/${typeId}".toString()
def data = new JsonBuilder("${obj}":objVal)
Map params = [
uri: url,
path: urlPath,
requestContentType: "application/json",
headers: [
"Content-Type": "application/json",
"Authorization": "Bearer ${tok}".toString()
],
body: data.toString(),
timeout: 20
]
/* //def urlPath
if((uri || (String)state.nestRedirectUrl) && !redir) {
//urlPath = "/${type}/${typeId}"
params.path = "/${type}/${typeId}".toString()
}*/
LogTrace("${myStr} Url: $url | params: ${params}")
LogAction("Processing Queued Cmd: [ObjId: ${typeId} | ObjType: ${type} | ObjKey: ${obj} | ObjVal: ${objVal} | QueueNum: ${qnum} | Redirect: ${redir}]", sTRC, true)
state.lastCmdSent = "$type: (${objKey}: ${objVal})".toString()
adjThrottle(qnum, redir)
def t0 = objVal
// if(t0 instanceof Map) { t0 = [:] + objVal }
Map asyncargs = [
typeId: typeId,
type: type,
obj: obj,
objVal: t0,
qnum: qnum,
cmd: cmd ]
asynchttpPut('nestCmdResponse', params, asyncargs)
} catch(ex) {
log.error "${myStr} (command: $cmd) Exception: ${ex?.message}"//, ex
}
}
@SuppressWarnings('unused')
void nestCmdResponse(resp, data) {
LogAction("nestCmdResponse(${data?.cmd})", sINFO, false)
String typeId = (String)data?.typeId
String type = (String)data?.type
String obj = (String)data?.obj
def objVal = data?.objVal
if(objVal instanceof Map) { objVal = [:] + (Map)data?.objVal }
Integer qnum = (Integer)data?.qnum
List command = (List)data?.cmd
Boolean result = false
String msg="nestCmdResponse | Processed Queue: ${qnum} | Obj: ($type{$obj:$objVal})".toString()
try {
if(!command) { cmdProcState(false); return }
if(resp?.status == 307) {
String redirUrl = resp?.headers?.Location
URI newUri = new URI(redirUrl)
String newUrl = "${newUri?.getScheme()}://${newUri?.getHost()}:${newUri?.getPort()}".toString()
LogTrace(msg+' REDIRECTED! to '+newUrl)
if((newUrl != sNULL && newUrl.startsWith("https://")) && (!(String)state.nestRedirectUrl || (String)state.nestRedirectUrl != newUrl)) {
state.nestRedirectUrl = newUrl
Boolean a=queueProcNestCmd(newUrl, typeId, type, obj, objVal, qnum, command, true)
return
} else { LogAction("did not REDIRECT", sERR, true) }
/*
//LogTrace("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(msg+' SUCCESSFULLY!', sINFO, true)
apiIssueEvent(false)
state.lastCmdSentStatus = "ok"
//atomicState.apiRateLimited = false
//atomicState.apiCmdFailData = null
result = true
}
/*
if(resp?.status == 429) {
// requeue command
def newCmd = [command[0], command[1], command[2], command[3], command[4]]
def tempQueue = []
tempQueue << newCmd
if(!(List)atomicState."cmdQ${qnum}" ) { atomicState."cmdQ${qnum}" = [] }
List cmdQueue = (List)atomicState."cmdQ${qnum}"
cmdQueue.each { cmd ->
newCmd = [cmd[0], cmd[1], cmd[2], cmd[3], cmd[4]]
tempQueue << newCmd
}
atomicState."cmdQ${qnum}" = tempQueue
}
*/
if(resp?.status != 200) {
state.lastCmdSentStatus = "failed"
state.remove("nestRedirectUrl")
if(resp?.hasError()) {
apiRespHandler((resp?.getStatus() ?: null), (resp?.getErrorJson() ?: null), "nestCmdResponse", msg, true)
//apiRespHandler(resp?.status, resp?.data, "procNestCmd", "procNestCmd ${qnum} ($type{$objKey:$objVal})", true)
} else {
LogAction(msg+' could not process error', sERR, true)
}
apiIssueEvent(true)
/*
atomicState.lastCmdSentStatus = "failed"
if(resp?.hasError()) {
apiRespHandler((resp?.getStatus() ?: null), (resp?.getErrorJson() ?: null), "nestCmdResponse", msg, true)
}
apiIssueEvent(true)
*/
}
/*
if(resp?.status == 429) {
result = true // we requeued the command
}
*/
finishWorkQ(command, result)
} catch (ex) {
state.lastCmdSentStatus = "failed"
state.remove("nestRedirectUrl")
finishERR()
if(resp?.hasError()) {
apiRespHandler((resp?.getStatus() ?: null), (/*resp?.getErrorJson() ?:*/ null), "nestCmdResponse Exception", msg, true)
}
apiIssueEvent(true)
log.error msg+" (command: $command) Exception: ${ex?.message}"//, ex
}
}
/*
def procNestCmd(uri, typeId, type, objKey, objVal, qnum, redir = false) {
def result = false
if(!getNestAuthToken()) { return result }
try {
if(getLastAnyCmdSentSeconds() > 120) {
state.nestRedirectUrl = sNULL
state.remove("nestRedirectUrl") // don't cache the redirect URL too long
}
def url = (!redir && (String)state.nestRedirectUrl) ? (String)state.nestRedirectUrl : uri
def data = new JsonBuilder([(objKey):objVal])
def params = [
uri: url,
contentType: "application/json",
headers: [
"Authorization": "Bearer ${getNestAuthToken()}"
],
body: data?.toString()
]
if((uri || (String)state.nestRedirectUrl) && !redir) {
params["path"] = "/${type}/${typeId}"
}
state.lastCmdSent = "$type: (${objKey}: ${objVal})"
adjThrottle(qnum, redir)
// LogTrace("procNestCmd time update recentSendCmd: ${getRecentSendCmd(qnum)} last seconds:${getLastCmdSentSeconds(qnum)} queue: ${qnum}")
httpPut(params) { resp ->
if(resp?.status == 307) {
def redirUrl = resp?.headers?.location
def newUri = new URI(redirUrl?.toString())
def newUrl = "${newUri?.getScheme()}://${newUri?.getHost()}:${newUri?.getPort()}"
if((newUrl != null && newUrl.startsWith("https://")) && (!(String)state.nestRedirectUrl || (String)state.nestRedirectUrl != newUrl)) {
state.nestRedirectUrl = newUrl
if( procNestCmd(redirUrl, typeId, type, objKey, objVal, qnum, true) ) {
return true
}
}
}
else if(resp?.status == 200) {
LogAction("procNestCmd Processed Queue(${qnum}) Item: ($type{$objKey:$objVal}) SUCCESSFULLY!", sINFO, true)
apiIssueEvent(false)
state.lastCmdSentStatus = "ok"
//state.apiRateLimited = false
//state.apiCmdFailData = null
result = true
}
else {
state.lastCmdSentStatus = "failed"
state.remove("nestRedirectUrl")
apiRespHandler(resp?.status, resp?.data, "procNestCmd", "procNestCmd ${qnum} ($type{$objKey:$objVal})", true)
apiIssueEvent(true)
}
}
} catch (ex) {
state.lastCmdSentStatus = "failed"
state.remove("nestRedirectUrl")
cmdProcState(false)
if(ex instanceof groovyx.net.http.HttpResponseException && ex?.response) {
apiRespHandler(ex?.response?.status, ex?.response?.data, "procNestCmd", "procNestCmd ${qnum} ($type{$objKey:$objVal})", true)
} else {
log.error "procNestCmd Exception: ($type | $objKey:$objVal) | Message: ${ex?.message}"
}
apiIssueEvent(true)
}
return result
}
*/
void adjThrottle(Integer qnum, Boolean redir) {
if(!redir) {
Integer t0 = getRecentSendCmd(qnum)
Integer val = t0
if(t0 > 0 /* && (getLastCmdSentSeconds(qnum) < 60) */ ) {
val -= 1
}
Integer t1 = getLastCmdSentSeconds(qnum)
if(t1 > 120 && t1 < 60*45 && val < (cmdMaxVal()-1) ) {
val += 1
}
if(t1 > 60*30 && t1 < 60*45 && val < cmdMaxVal() ) {
val += 1
}
LogTrace("adjThrottle orig recentSendCmd: ${t0} | new: ${val} | last seconds: ${t1} queue: ${qnum}")
setRecentSendCmd(qnum, val)
}
setLastCmdSentSeconds(qnum, getDtNow())
}
void apiRespHandler(code, errJson, String methodName, String tstr=sNULL, Boolean isCmd=false) {
// LogAction("[$methodName] | Status: (${code}) | Error Message: ${errJson}", sWARN, true)
if(!(code?.toInteger() in [200, 307])) {
String result
Boolean notif = true
String errMsg = errJson?.message != sNULL ? (String)errJson?.message : sNULL
switch(code) {
case 400:
result = !errMsg ? "A Bad Request was made to the API..." : errMsg
break
case 401:
result = !errMsg ? "Authentication ERROR, Please try refreshing your login under Authentication settings..." : errMsg
//revokeNestToken()
break
case 403:
result = !errMsg ? "Forbidden: Your Login Credentials are Invalid..." : errMsg
//revokeNestToken()
break
case 429:
result = !errMsg ? "Requests are currently being blocked because of API Rate Limiting..." : errMsg
state.apiRateLimited = true
break
case 500:
result = !errMsg ? "Internal Nest Error:" : errMsg
notif = false
break
case 503:
result = !errMsg ? "There is currently a Nest Service Issue..." : errMsg
notif = false
break
default:
result = !errMsg ? "Received Response..." : errMsg
notif = false
break
}
def failData = ["code":code, "msg":result, "method":methodName, "dt":getDtNow(), "isCmd": isCmd]
state.apiCmdFailData = failData
if(notif || isCmd) {
failedCmdNotify(failData, tstr)
}
LogAction("$methodName error - (Status: $code - $result) - [ErrorLink: ${errJson?.type}] ${errJson?.error} ${errJson?.details}", sERR, true)
}
}
@SuppressWarnings('unused')
String getNestZipCode() {
String tt = getStrucVal("postal_code")
return tt ?: sBLK
}
String getNestTimeZone() {
return getStrucVal("time_zone")
}
String getEtaBegin() {
return getStrucVal("eta_begin")
}
String getPeakStart() {
return getStrucVal("peak_period_start_time")
}
String getPeakEnd() {
return getStrucVal("peak_period_end_time")
}
String getSecurityState() {
return getStrucVal("wwn_security_state")
}
String getLocationPresence() {
return getStrucVal("away")
}
String getStrucVal(String svariable) {
def sData = state.structData
String sKey = (String)state.structures
def asStruc = sData && sKey && sData[sKey] ? sData[sKey] : null
String retVal = asStruc ? asStruc[svariable] ?: sNULL : sNULL
return (retVal != sNULL) ? retVal.toString() : sNULL
}
@SuppressWarnings('unused')
String getStZipCode() { return location?.zipCode?.toString() }
static Integer getChildWaitVal() { return tempChgWaitVal() }
/************************************************************************************
| This Section Discovers all structures and devices on your Nest Account. |
| It also Adds Removes Devices from Hubitat |
************************************************************************************/
private Map getNestStructures() {
//LogTrace("Getting Nest Structures")
Map mstruct = [:]
Map thisstruct = [:]
try {
if(ok2PollStruct()) { getApiData("str") }
if(state.structData) {
def structs = state.structData
structs?.eachWithIndex { struc, index ->
//String strucId = struc?.key
def strucData = struc?.value
String dni = [strucData.structure_id].join('.')
mstruct[dni] = (String)strucData?.name
if((String)strucData?.structure_id == (String)settings.structures) {
thisstruct[dni] = (String)strucData?.name
} else {
if((String)state.structures) {
if((String)strucData?.structure_id == (String)state.structures) {
thisstruct[dni] = (String)strucData?.name
}
} else {
if(!(String)settings.structures) {
thisstruct[dni] = (String)strucData?.name
}
}
}
}
/* if((Map)state.thermostats || (Map)state.protects || (Map)state.cameras || (Boolean)state.presDevice || (Map)state.vThermostats) { // if devices are configured, you cannot change the structure until they are removed
mstruct = thisstruct
}*/
if(ok2PollDevice()) { getApiData("dev") }
} else { LogAction("Missing: structData ${state.structData}", sWARN, true) }
} catch (ex) {
log.error "getNestStructures Exception: ${ex?.message}"
}
return mstruct
}
private Map getNestThermostats() {
//LogTrace("Getting Thermostat list")
Map stats = [:]
Map tstats = (Map)deviceDataFLD?.thermostats
//LogTrace("Found ${tstats?.size()} Thermostats")
tstats.each { stat ->
// String statId = stat?.key
def statData = stat?.value
if(statData?.structure_id == (String)settings.structures) {
String adni = [statData?.device_id].join('.')
stats[adni] = getThermostatDisplayName(statData)
}
}
return stats
}
private Map getNestProtects() {
//LogTrace("Getting Nest Protect List")
Map protects = [:]
Map nProtects = (Map)deviceDataFLD?.smoke_co_alarms
//LogTrace("Found ${nProtects?.size()} Nest Protects")
nProtects.each { dev ->
// String devId = dev?.key
def devData = dev?.value
if(devData?.structure_id == (String)settings.structures) {
String bdni = [devData?.device_id].join('.')
protects[bdni] = getProtectDisplayName(devData)
}
}
return protects
}
private Map getNestCameras() {
//LogTrace("Getting Nest Camera List")
Map cameras = [:]
Map nCameras = (Map)deviceDataFLD?.cameras
//LogTrace("Found ${nCameras?.size()} Nest Cameras")
nCameras.each { dev ->
// String devId = dev?.key
def devData = dev?.value
if(devData?.structure_id == (String)settings.structures) {
String bdni = [devData?.device_id].join('.')
cameras[bdni] = getCameraDisplayName(devData)
}
}
return cameras
}
private Map statState(List val) {
Map stats = [:]
Map tstats = getNestThermostats()
tstats.each { stat ->
String statId = stat?.key
val.each { st ->
if(statId == st) {
String adni = [statId].join('.')
stats[adni] = stat.value
}
}
}
return stats
}
private Map coState(List val) {
Map protects = [:]
Map nProtects = getNestProtects()
nProtects.each { dev ->
val.each { pt ->
String devId = dev.key
if(devId == pt) {
String bdni = [devId].join('.')
protects[bdni] = dev.value
}
}
}
return protects
}
private Map camState(List val) {
Map cams = [:]
Map nCameras = getNestCameras()
nCameras.each { dev ->
val.each { cm ->
String devId = dev.key
if(devId == cm) {
String bdni = [devId].join('.')
cams[bdni] = dev.value
}
}
}
return cams
}
static String getThermostatDisplayName(stat) {
if((String)stat.name) { return (String)stat.name }
else if((String)stat.name_long) { return (String)stat.name_long }
else { return "Thermostatnamenotfound" }
}
static String getProtectDisplayName(prot) {
if((String)prot.name) { return (String)prot.name }
else if((String)prot.name_long) { return (String)prot.name_long }
else { return "Protectnamenotfound" }
}
static String getCameraDisplayName(cam) {
if((String)cam.name) { return (String)cam.name }
else if((String)cam.name_long) { return (String)cam.name_long }
else { return "Cameranamenotfound" }
}
String getNestDeviceDni(dni, String type) {
//LogTrace("getNestDeviceDni: $dni | $type")
String t1=(String)dni.key
String retVal = t1
def d1 = getChildDevice(t1)
if(!d1) {
String t0 = "Nest${type}-${dni.value.toString()} | ${t1}"
d1 = getChildDevice(t0)
if(d1) { retVal = t0 }
}
return retVal
}
String getNestTstatDni(dni) { return getNestDeviceDni(dni, "Thermostat") }
String getNestvStatDni(dni) { return getNestDeviceDni(dni, "vThermostat") }
String getNestProtDni(dni) { return getNestDeviceDni(dni, "Protect") }
String getNestCamDni(dni) { return getNestDeviceDni(dni, "Cam") }
String getNestPresId() {
String dni = "Nest Presence Device" // old name 1
def d3 = getChildDevice(dni)
if(d3) { return dni }
else {
String stStruc=(String)state.structures
if(stStruc) {
dni = 'NestPres'+stStruc // old name 2
d3 = getChildDevice(dni)
if(d3) { return dni }
}
String retVal = sBLK
if(stStruc) { retVal = 'NestPres | '+stStruc }
else if((String)settings.structures) { retVal = 'NestPres | '+(String)settings.structures }
else {
LogAction('getNestPresID No structures '+stStruc, sWARN, true)
}
return retVal
}
}
String getDefaultLabel(String ttype, String name) {
//LogTrace("getDefaultLabel: ${ttype} ${name}")
String defName=sBLK
if(name == sNULL || name == sBLK) {
LogAction("BAD CALL getDefaultLabel: ${ttype}, ${name}", sERR, true)
}else {
switch (ttype) {
case "thermostat":
defName = "Nest Thermostat - ${name}"
if((Boolean)state.devNameOverride && (Boolean)state.useAltNames) { defName = "${location.name} - ${name}" }
break
case "protect":
defName = "Nest Protect - ${name}"
if((Boolean)state.devNameOverride && (Boolean)state.useAltNames) { defName = "${location.name} - ${name}" }
break
case "camera":
defName = "Nest Camera - ${name}"
if((Boolean)state.devNameOverride && (Boolean)state.useAltNames) { defName = "${location.name} - ${name}" }
break
case "vthermostat":
defName = "Nest vThermostat - ${name}"
if((Boolean)state.devNameOverride && (Boolean)state.useAltNames) { defName = "${location.name} - Virtual ${name}" }
break
case "presence":
defName = "Nest Presence Device"
if((Boolean)state.devNameOverride && (Boolean)state.useAltNames) { defName = "${location.name} - Nest Presence Device" }
break
default:
LogAction("BAD CALL getDefaultLabel: ${ttype}, ${name}", sERR, true)
}
}
return defName
}
String getNestTstatLabel(String name, String key) {
//LogTrace("getNestTstatLabel: ${name}")
String defName = getDefaultLabel("thermostat", name)
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
return settings."tstat_${key}_lbl" ?: defName
}
return defName
}
String getNestProtLabel(String name, String key) {
String defName = getDefaultLabel("protect", name)
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
return settings."prot_${key}_lbl" ?: defName
}
return defName
}
String getNestCamLabel(String name, String key) {
String defName = getDefaultLabel("camera", name)
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
return settings."cam_${key}_lbl" ?: defName
}
return defName
}
String getNestVtstatLabel(String name, String key) {
String defName = getDefaultLabel("vthermostat", name)
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
return settings."vtstat_${key}_lbl" ?: defName
}
return defName
}
String getNestPresLabel() {
String defName = getDefaultLabel("presence", "name")
if((Boolean)state.devNameOverride && (Boolean)state.custLabelUsed) {
return settings.presDev_lbl ? settings.presDev_lbl.toString() : defName
}
return defName
}
String getChildDeviceLabel(String dni) {
if(!dni) { return sNULL }
def t0 = getChildDevice(dni)
return t0?.getLabel() ?: sNULL
}
Boolean addRemoveDevices(Boolean uninst=false) {
LogTrace("addRemoveDevices")
Boolean retVal
try {
def devsInUse = []
def tstats
def nProtects
def nCameras
def nVstats
Integer devsCrt = 0
Integer presCnt = 0
Integer streamCnt = 0
Boolean noCreates = true
Boolean noDeletes = true
if(!uninst) {
if((Map)state.thermostats) {
tstats = state.thermostats?.collect { dni ->
def d1 = getChildDevice(getNestTstatDni(dni))
if(!d1) {
String d1Label = getNestTstatLabel("${dni?.value}", (String)dni.key)
d1 = addChildDevice(namespace(), getThermostatChildName(), (String)dni.key, [label: d1Label])
devsCrt = devsCrt + 1
LogAction("Created: ${d1.displayName} with (Id: ${(String)dni.key})", sDBG, true)
} else {
LogAction("Found Existing Device: ${d1.displayName} with (Id: ${(String)dni.key})", sDBG, true)
}
devsInUse += (String)dni.key
}
}
if((Map)state.protects) {
nProtects = state.protects?.collect { dni ->
def d2 = getChildDevice(getNestProtDni(dni))
if(!d2) {
String d2Label = getNestProtLabel("${dni.value}", (String)dni.key)
d2 = addChildDevice(namespace(), getProtectChildName(), (String)dni.key, [label: d2Label])
devsCrt = devsCrt + 1
LogAction("Created: ${d2.displayName} with (Id: ${(String)dni.key})", sDBG, true)
} else {
LogAction("Found Existing Device: ${d2.displayName} with (Id: ${(String)dni.key})", sDBG, true)
}
devsInUse += (String)dni.key
}
}
if((Boolean)state.presDevice) {
try {
String dni = getNestPresId()
def d3 = getChildDevice(dni)
if(!d3) {
String d3Label = getNestPresLabel()
d3 = addChildDevice(namespace(), getPresenceChildName(), dni, [label: d3Label])
devsCrt = devsCrt + 1
LogAction("Created: ${d3.displayName} with (Id: ${dni})", sDBG, true)
} else {
LogAction("Found Existing Device: ${d3.displayName} with (Id: ${dni})", sDBG, true)
}
devsInUse += dni
} catch (ignored) {
LogAction("Nest Presence Device Handler may not be installed/published", sWARN, true)
noCreates = false
}
presCnt = 1
}
if((Map)state.cameras) {
nCameras = state.cameras?.collect { dni ->
def d4 = getChildDevice(getNestCamDni(dni))
if(!d4) {
String d4Label = getNestCamLabel("${dni.value}", (String)dni.key)
d4 = addChildDevice(namespace(), getCameraChildName(), (String)dni.key, [label: d4Label])
devsCrt = devsCrt + 1
LogAction("Created: ${d4.displayName} with (Id: ${(String)dni.key})", sDBG, true)
} else {
LogAction("Found Existing Device: ${d4.displayName} with (Id: ${(String)dni.key})", sDBG, true)
}
devsInUse += (String)dni.key
}
}
if((Map)state.vThermostats) {
nVstats = state.vThermostats.collect { dni ->
//LogAction("state.vThermostats: ${state.vThermostats} dni: ${dni} dni.key: ${(String)dni.key} dni.value: ${dni.value.toString()}", sDBG, true)
def d6 = getChildDevice(getNestvStatDni(dni))
if(!d6) {
String d6Label = getNestVtstatLabel("${dni.value}", (String)dni.key)
//LogAction("CREATED: ${d6Label} with (Id: ${dni.key})", sDBG, true)
d6 = addChildDevice(namespace(), getThermostatChildName(), (String)dni.key, [label: d6Label, "data":["isVirtual":sTRU]])
devsCrt = devsCrt + 1
LogAction("Created: ${d6.displayName} with (Id: ${(String)dni.key})", sDBG, true)
} else {
LogAction("Found: ${d6.displayName} with (Id: ${(String)dni.key}) exists", sDBG, true)
}
devsInUse += (String)dni.key
return d6
}
}
if(restEnabled()) {
def d5 = getChildDevice(getEventDeviceDni())
if(!d5) {
d5 = addChildDevice(namespace(), getEventDeviceName(), getEventDeviceDni(), [label: getEventDeviceName()])
devsCrt = devsCrt + 1
streamCnt = streamCnt + 1
LogAction("Created Device: ${getEventDeviceName()} with (Id: ${getEventDeviceDni()})", sDBG, true)
} else {
LogAction("Found Existing Device: ${getEventDeviceName()} with (Id: ${getEventDeviceDni()})", sDBG, true)
}
devsInUse += getEventDeviceDni()
}
if(devsCrt > 0) {
noCreates = false
LogAction("Created ${devsCrt} Devices | (${tstats?.size()}) Thermostat(s), (${nVstats?.size() ?: 0}) Virtual Thermostat(s),(${nProtects?.size() ?: 0}) Protect(s), (${nCameras?.size() ?: 0}) Cameras(s), (${presCnt}) Presence Device, and (${streamCnt}) Event Stream Device", sDBG, true)
updTimestampMap("lastAnalyticUpdDt")
}
}
if(uninst) {
state.thermostats = [:]
state.vThermostats = [:]
state.protects = [:]
state.cameras = [:]
state.presDevice = false
state.streamDevice = false
}
Boolean noDeleteErr = true
def toDelete
LogTrace("addRemoveDevices devicesInUse: ${devsInUse}")
toDelete = getChildDevices().findAll { !devsInUse?.toString()?.contains(it?.deviceNetworkId) }
if(toDelete?.size() > 0) {
// log.debug "delete: $delete"
// log.debug "devsInUse: $devsInUse"
noDeletes = false
noDeleteErr = false
updTimestampMap("lastAnalyticUpdDt")
LogAction("Removing ${toDelete.size()} devices: ${toDelete}", sDBG, true)
toDelete.each { deleteChildDevice(it.deviceNetworkId) }
noDeleteErr = true
}
retVal = ((unist && noDeleteErr) || (!uninst && (noCreates && noDeletes))) // it worked = no delete errors on uninstall; or no creates or deletes done
} catch (ex) {
if(ex instanceof hubitat.exception.ConflictException) {
def msg = "Error: Can't Remove Device. One or more of them are still in use by other Apps. Please remove them and try again!"
sendPush(msg)
LogAction("addRemoveDevices Exception | $msg", sWARN, true)
} else {
log.error "addRemoveDevices Exception: ${ex?.message}"
}
retVal = false
}
return retVal
}
Boolean addRemoveVthermostat(String tstatdni, tval, String myID) {
String odevId = tstatdni
LogAction("addRemoveVthermostat() tstat: ${tstatdni} devid: ${odevId} tval: ${tval} myID: ${myID} vThermostats: ${state.vThermostats} ", sTRC, true)
if(parent || !myID || tval == null) {
LogAction("got called BADLY ${parent} ${myID} ${tval}", sWARN, true)
return false
}
String tstat = tstatdni
Boolean tStatPhys
def d1 = getChildDevice(odevId)
if(!d1) {
LogAction("addRemoveVthermostat: Cannot find thermostat device child", sERR, true)
if(tval) { return false } // if deleting (false), let it try to proceed
} else {
tstat = d1
tStatPhys = (tstat?.currentNestType == "physical")
if(!tStatPhys && tval) { LogAction("addRemoveVthermostat: Cannot create a virtual thermostat on a virtual thermostat device child", sERR, true) }
}
String devId = "v${odevId}"
// def migrate = migrationInProgress()
// if(!migrate && state."vThermostat${devId}" && myID != state."vThermostatChildAppId${devId}") {
if(state."vThermostat${devId}" && myID != state."vThermostatChildAppId${devId}") {
LogAction("addRemoveVthermostat() not ours ${myID} ${state."vThermostat${devId}"} ${state."vThermostatChildAppId${devId}"}", sTRC, true)
//state."vThermostat${devId}" = false
//state."vThermostatChildAppId${devId}" = null
//state."vThermostatMirrorId${devId}" = null
//state.vThermostats = null
return false
} else if(tval && state."vThermostat${devId}" && myID == state."vThermostatChildAppId${devId}") {
LogAction("addRemoveVthermostat() already created ${myID} ${state."vThermostat${devId}"} ${state."vThermostatChildAppId${devId}"}", sTRC, true)
return true
} else if(!tval && !state."vThermostat${devId}") {
LogAction("addRemoveVthermostat() already removed ${myID} ${state."vThermostat${devId}"} ${state."vThermostatChildAppId${devId}"}", sTRC, true)
return true
} else {
state."vThermostat${devId}" = tval
if(tval && !(String)state."vThermostatChildAppId${devId}") {
LogAction("addRemoveVthermostat() marking for create virtual thermostat tracking ${tstat}", sTRC, true)
state."vThermostatChildAppId${devId}" = myID
state."vThermostatMirrorId${devId}" = odevId
Map vt = (Map)state.vThermostats ?: [:]
vt[devId] = (String)tstat.label
state.vThermostats = vt
if(!settings.resetAllData) { runIn(120, "updated", [overwrite: true]) } // create what is needed
} else if(!tval && (String)state."vThermostatChildAppId${devId}") {
LogAction("addRemoveVthermostat() marking for remove virtual thermostat tracking ${tstat}", sTRC, true)
state."vThermostatChildAppId${devId}" = sNULL
state."vThermostatMirrorId${devId}" = sNULL
state.remove("vThermostat${devId}" as String)
state.remove("vThermostatChildAppId${devId}" as String)
state.remove("vThermostatMirrorId${devId}" as String)
state.remove("oldvstatData${devId}" as String)
Map vt = state.vThermostats
Map newmap = [:]
def vtstat
vtstat = vt.collect { dni ->
//LogAction("vThermostats: ${state.vThermostats} dni: ${dni} dni.key: ${(String)dni.key} dni.value: ${dni.value.toString()} devId: ${devId}", sDBG, true)
String ttkey = (String)dni.key
if(ttkey == devId) { /*log.trace "skipping $dni"*/ }
else { newmap[ttkey] = dni.value }
return true
}
vt = newmap
state.vThermostats = vt
if(!settings.resetAllData) { runIn(120, "updated", [overwrite: true]) } // create what is needed
} else {
LogAction("addRemoveVthermostat() unexpected operation state ${myID} ${state."vThermostat${devId}"} ${state."vThermostatChildAppId${devId}"}", sWARN, true)
return false
}
return true
}
}
Boolean getAccessToken() {
if(!(String)state.access_token) {
try {
state.access_token = createAccessToken()
}
catch (ex) {
String msg = "Error: OAuth is not Enabled for ${app.name}!. Please click remove and Enable Oauth under the App Settings in the IDE".toString()
sendPush(msg)
log.error "getAccessToken Exception ${ex?.message}"
LogAction("getAccessToken Exception | $msg", sWARN, true)
return false
}
}
return true
}
void enableOauth() {
def params = [
uri: "http://localhost:8080/app/edit/update?_action_update=Update&oauthEnabled=true&id=${app.appTypeId}",
headers: ['Content-Type':'text/html;charset=utf-8']
]
try {
httpPost(params) { resp ->
//LogTrace("response data: ${resp.data}")
}
} catch (e) {
log.debug "enableOauth something went wrong: ${e}"
}
}
/*
void resetAppAccessToken() {
LogAction("Resetting getAppDebugDesc Access Token....", sINFO, true)
//restStreamHandler(true, "resetAppAccessToken()")
//state.restStreamingOn = false
revokeAccessToken()
state.access_token = sNULL
//resetPolling()
if(getAccessToken()) {
LogAction("Reset App Access Token... Successful", sINFO, true)
settingUpdate("resetAppAccessToken", sFALSE, sBOOL)
}
//startStopStream()
}
*/
/************************************************************************************************
| Below This line handle Hubitat >> Nest Token Authentication |
*************************************************************************************************/
Boolean nestDevAccountCheckOk() {
if(getNestAuthToken()==sNULL && (clientId() == sNULL || clientSecret() == sNULL) ) { return false }
else { return true }
}
static Map devClientData() {
Integer clt = devCltNum() ?: 1
Map m = [
0: [id: "OWQxMzJlODMtMTFmYy00NWJlLTlhOGQtOTViN2E3Y2IwN2Ew", secret: "TERhSmU4dEFNdmRQR3lGUHQwSkpQMTY1eA=="],
1: [id: "MzFhZWE0NmMtNDA0OC00YzJiLWI2YmUtY2FjN2ZlMzA1ZDRj", secret: "Rm1PNDY5R1hmZFNWam43UGhLbmpHV1psbQ=="],
2: [id: "NjNlOWJlZmEtZGM2Mi00YjczLWFhZjQtZGNmMzgyNmRkNzA0", secret: "OGlxVDhYNDZ3YTJVWm5MMG9lM1RieU9hMA=="]
]
def id = m[clt]?.id?.decodeBase64()
def secret = m[clt]?.secret?.decodeBase64()
return [id: new String(id), secret: new String(secret)]
}
//These are the Nest OAUTH Methods to aquire the auth code and then Access Token.
String clientId() {
if(settings.useMyClientId && settings.clientId) { return settings.clientId }
String id = devClientData()?.id
return id ?: sNULL//Developer ID
}
String clientSecret() {
if(settings.useMyClientId && settings.clientSecret) { return settings.clientSecret }
String sec=devClientData()?.secret
return sec ?: sNULL//Developer Secret
}
String getNestAuthToken() { return (state.authData && state.authData?.token) ? (String)state.authData.token : sNULL }
String getOauthInitUrl() {
Map oauthParams = [
response_type: "code",
client_id: clientId(),
state: getOauthState(),
redirect_uri: getCallbackUrl()
]
//Logger("getOauthInitUrl: https://home.nest.com/login/oauth2?${toQueryString(oauthParams)}", sERR)
return 'https://home.nest.com/login/oauth2?'+toQueryString(oauthParams)
}
@SuppressWarnings('unused')
def callback() {
LogTrace("callback()")
try {
// LogTrace("callback()>> params: $params, params.code ${params.code}")
def code = params.code
// log.trace "Callback Code: $code"
def oauthState = params.state
// log.trace "Callback State: $oauthState"
if(oauthState == getOauthState()) {
Map tokenParams = [
code: code.toString(),
client_id: clientId(),
client_secret: clientSecret(),
grant_type: "authorization_code",
]
String tokenUrl = 'https://api.home.nest.com/oauth2/access_token?'+toQueryString(tokenParams)
//Logger("callback: https://api.home.nest.com/oauth2/access_token?${toQueryString(tokenParams)}", sERR)
httpPost(uri: tokenUrl) { resp ->
Map authData = [:]
authData.token = resp.data.access_token
if(authData.token) {
updTimestampMap("authTokenCreatedDt", getDtNow())
authData.tokenExpires = resp.data.expires_in
state.authData = authData
}
}
if(state.authData?.token) {
LogAction("Nest AuthToken Generated SUCCESSFULLY", sINFO, true)
if((Boolean)state.isInstalled) {
state.needStrPoll = true
state.needDevPoll = true
state.needMetaPoll = true
state.needToFinalize = true
checkRemapping() // settings updates do not take immiediate effect, so we have to wait before using them
state.pollBlocked = true
state.pollBlockedReason = "Awaiting fixDevAS"
runIn(4, "finishRemap", [overwrite: true])
}
success()
} else {
LogAction("Failure Generating Nest AuthToken", sERR, true)
fail()
}
} else { LogAction("callback() params.state != oauthInitState", sERR, true) }
} catch (ex) {
log.error "Oauth Callback Exception: ${ex?.message}"
revokeCleanState()
}
}
@SuppressWarnings('unused')
void finishRemap() {
LogTrace("finishRemap (${(Boolean)state.pollBlocked}) (${state.pollBlockedReason})")
fixDevAS()
state.pollBlocked = false
state.pollBlockedReason = sBLK
state.needToFinalize = false
initialize()
}
static String getNestApiUrl() { return "https://developer-api.nest.com" }
String getStructure() { return (String)state.structures ?: sNULL }
static String getCallbackUrl(){ return "https://cloud.hubitat.com/api/nest" }
String getOauthState() { return "${getHubUID()}/apps/${app.id}/callback?access_token=${(String)state.access_token}".toString() }
String getAppEndpointUrl(subPath) { return "${getApiServerUrl()}/${getHubUID()}/apps/${app.id}${subPath ? "/${subPath}" : sBLK}?access_token=${(String)state.access_token}".toString() }
String getLocalEndpointUrl(subPath){ return "${getLocalApiServerUrl()}/apps/${app.id}${subPath ? "/${subPath}" : sBLK}?access_token=${(String)state.access_token}".toString() }
private static String sectionTitleStr(String title) { return ''+title+'
' }
private static String inputTitleStr(String title) { return ''+title+'' }
private static String pageTitleStr(String title) { return ''+title+'
' }
private static String paraTitleStr(String title) { return ''+title+'' }
static String imgTitle(String imgSrc, String titleStr, String color=sNULL, Integer imgWidth=30, Integer imgHeight=0) {
String imgStyle = sBLK
imgStyle += imgWidth>0 ? 'width: '+(String)(imgWidth.toString())+'px !important;':''
imgStyle += imgHeight>0 ? imgWidth!=0 ? ' ':''+'height: '+(String)(imgHeight.toString())+'px !important;':''
if(color!=sNULL){ return """ ${titleStr}
""".toString() }
else { return """ ${titleStr}""".toString() }
}
// string table for titles
static String titles(String name, Object... args){
Map page_titles=[
// "page_main": "${lname} setup and management",
// "page_add_new_cid_confirm": "Add new CID switch : %s",
// "input_selected_devices": "Select device(s) (%s found)",
"t_dtse": "Delay to set ECO (in Minutes)",
"t_dr": "Delay Restore (in Minutes)",
"t_ca": "Configured Alerts",
"t_cr": "Configured Restrictions",
"t_nt": "Notifications:",
"t_nlw": "Nest Location Watchdog"
]
if(args)
return String.format(page_titles[name], args)
else
return page_titles[name]
}
// string table for descriptions
static String descriptions(String name, Object... args){
Map element_descriptions=[
"d_ttc": "Tap to configure",
"d_ttm": "Tap to modify"
]
if(args)
return String.format((String)element_descriptions[name],args)
else
return (String)element_descriptions[name]
}
@Field static final Map icon_namesFLD=[
"i_lg": "login",
"i_sw": "switch_on",
"i_t": "temperature",
"i_ns": "nest_structure",
"i_th": "thermostat",
"i_p": "protect",
"i_c": "camera",
"i_pr": "presence",
"i_pu": "pushover",
"i_s": "schedule",
"i_d": "diagnostic",
"i_g": "graph",
"i_i": "issue",
"i_r": "reset",
]
static String icons(String name, String napp="App"){
String t0=icon_namesFLD."${name}"
//LogAction("t0 ${t0}", sWARN, true)
if(t0) return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${t0}_icon.png".toString()
else return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${name}".toString()
}
static String getAppImg(String imgName, Boolean on=true){
//return (!disAppIcons || on) ? "https://raw.githubusercontent.com/${gitPath()}/Images/App/$imgName" : sBLK
return on ? icons(imgName) : sBLK
}
static String getDevImg(String imgName, Boolean on=true){
//return (!disAppIcons || on) ? "https://raw.githubusercontent.com/${gitPath()}/Images/Devices/$imgName" : sBLK
return on ? icons(imgName, "Devices") : sBLK
}
//static String getAppImg(imgName, Boolean on = true) { return on ? "https://raw.githubusercontent.com/tonesto7/nest-manager/master/Images/App/$imgName" : sBLK }
//static String getDevImg(imgName, Boolean on = true) { return on ? "https://raw.githubusercontent.com/tonesto7/nest-manager/master/Images/Devices/$imgName" : sBLK }
@SuppressWarnings('unused')
void revokeNestToken() {
if(getNestAuthToken()!=sNULL) {
LogAction("revokeNestToken()", sINFO, true)
restStreamHandler(true, "revokeNestToken()", false)
state.restStreamingOn = false
Map params = [
uri: "https://api.home.nest.com",
path: "/oauth2/access_tokens/${getNestAuthToken()}",
contentType: 'application/json'
]
try {
httpDelete(params) { resp ->
if(resp?.status == 204) {
LogAction("Nest Token revoked", sWARN, true)
revokeCleanState()
//return //true
}
}
}
catch (ex) {
if(ex?.message?.toString() == "Not Found") {
revokeCleanState()
//return //true
} else {
log.error "revokeNestToken Exception: ${ex?.message}"
revokeCleanState()
//return //false
}
}
} else { revokeCleanState() }
}
void revokeCleanState() {
LogTrace("revokeCleanState")
unschedule()
atomicState.diagRunInOn = false
state.access_token = sNULL
// state.accessToken = null
state.authData = null
updTimestampMap("authTokenCreatedDt")
//state.nestAuthTokenExpires = getDtNow()
state.structData = null
deviceDataFLD = null
state.metaData = null
updTimestampMap("lastStrDataUpd")
updTimestampMap("lastDevDataUpd")
updTimestampMap("lastMetaDataUpd")
resetPolling()
state.pollingOn = false
state.streamPolling = false
state.pollBlocked = true
state.pollBlockedReason = "No Auth Token"
atomicState.workQrunInActive = false
}
//HTML Connections Pages
def success() {
String message = """
Your Hubitat Elevation is now connected to Nest!
You will be redirected back to the Hubitat App to finish the rest of the setup in a couple seconds.
"""
connectionStatus(message, true)
}
def fail() {
String message = """
The connection could not be established!
You will be redirected back to the Hubitat App to try the connection again.
"""
connectionStatus(message, true)
}
def connectionStatus(String message, Boolean close = false) {
String redirectHtml = close ? """""" : sBLK
String html = """
Hubitat & Nest connection
${redirectHtml}
""".toString()
/* """ */
render contentType: 'text/html', data: html
}
String toJson(Map m) {
return new org.json.JSONObject(m).toString()
}
String toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}".toString() }.sort().join("&")
}
/************************************************************************************************
| LOGGING AND Diagnostic |
*************************************************************************************************/
void LogTrace(GString msg, String logSrc=sNULL) {
String value=(msg instanceof GString)? "$msg".toString():msg //get rid of GStrings
LogTrace(value,logSrc)
}
void LogTrace(String msg, String logSrc=sNULL) {
Boolean trOn = ((Boolean) settings.appDebug && (Boolean) settings.advAppDebug && !(Boolean) settings.enRemDiagLogging)
if(trOn) {
Boolean logOn = ((Boolean) settings.enRemDiagLogging && (Boolean) state.enRemDiagLogging)
//def theLogSrc = (logSrc == null) ? (parent ? "Automation" : "Manager") : logSrc
Logger(msg, sTRC, logSrc, logOn)
}
}
void LogAction(GString msg, String type=sDBG, Boolean showAlways=false, String logSrc=sNULL) {
String value=(msg instanceof GString)? "$msg".toString():msg //get rid of GStrings
LogAction(value,type,showAlways,logSrc)
}
void LogAction(String msg, String type=sDBG, Boolean showAlways=false, String logSrc=sNULL) {
Boolean isDbg = ((Boolean) settings.appDebug /* && !(Boolean)enRemDiagLogging */)
//def theLogSrc = (logSrc == null) ? (parent ? "Automation" : "Manager") : logSrc
if(showAlways || (isDbg && !showAlways)) { Logger(msg, type, logSrc) }
// if(showAlways || isDbg) { Logger(msg, type) }
}
static String tokenStrScrubber(String str) {
String regex1 = /(Bearer c.{1}\w+)/
String regex2 = /(auth=c.{1}\w+)/
String newStr = str.replaceAll(regex1, "Bearer 'token code redacted'")
newStr = newStr.replaceAll(regex2, "auth='token code redacted'")
//log.debug "newStr: $newStr"
return newStr
}
@SuppressWarnings('unused')
void Logger(String msg, String type, String logSrc=sNULL, Boolean noSTlogger=false) {
String labelstr = sBLK
Boolean logOut = true
if((Boolean)settings.dbgAppndName) { labelstr = app.label+' | ' }
if(msg && type) {
String themsg = tokenStrScrubber(labelstr+msg)
if((Boolean)state.enRemDiagLogging && (Boolean)settings.enRemDiagLogging && (Boolean)state.remDiagAppAvailable) {
String theLogSrc = (logSrc == sNULL) ? (parent ? "Automation" : "Manager") : logSrc
if(saveLogtoRemDiagStore(themsg, type, theLogSrc)) {
logOut = false
}
}
if(logOut) {
switch(type){
case sINFO:
log.info sSPACE + logPrefix(themsg, "#0299b1")
break
case sTRC:
log.trace logPrefix(themsg, sCLRGRY)
break
case sERR:
log.error logPrefix(themsg, sCLRRED)
break
case sWARN:
log.warn sSPACE + logPrefix(themsg, sCLRORG)
break
case sDBG:
default:
log.debug logPrefix(themsg, "purple")
}
}
}
else { log.error "${labelstr}Logger Error - type: ${type} | msg: ${msg}" }
}
static String logPrefix(String msg, String color = sNULL) {
return span("Manager (v" + appVer() + ") | ", sCLRGRY) + span(msg, color)
}
static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLK}" : sBLK }
String getDiagLogTimeRemaining() {
return sec2PrettyTime((3600*48) - Math.abs((getRemDiagActSec() ?: 0)))
}
String sec2PrettyTime(Integer timeSec) {
Integer years = Math.floor(timeSec / 31536000); timeSec -= years * 31536000
Integer months = Math.floor(timeSec / 31536000); timeSec -= months * 2592000
Integer days = Math.floor(timeSec / 86400); timeSec -= days * 86400
Integer hours = Math.floor(timeSec / 3600); timeSec -= hours * 3600
Integer minutes = Math.floor(timeSec / 60); timeSec -= minutes * 60
Integer seconds = Integer.parseInt((timeSec % 60) as String, 10)
Map dt = [y: years, mn: months, d: days, h: hours, m: minutes, s: seconds]
String dtStr = sBLK
// dtStr += dt?.y ? "${dt?.y}yr${dt?.y>1?"s":""}, " : sBLK
// dtStr += dt?.mn ? "${dt?.mn}mon${dt?.mn>1?"s":""}, " : sBLK
// dtStr += dt?.d ? "${dt?.d}day${dt?.d>1?"s":""}, " : sBLK
// dtStr += dt?.h ? "${dt?.h}hr${dt?.h>1?"s":""} " : sBLK
// dtStr += dt?.m ? "${dt?.m}min${dt?.m>1?"s":""} " : sBLK
// dtStr += dt?.s ? "${dt?.s}sec" : sBLK
dtStr += dt?.d ? "${dt?.d}d " : sBLK
dtStr += dt?.h ? "${dt?.h}h " : sBLK
dtStr += dt?.m ? "${dt?.m}m " : sBLK
dtStr += dt?.s ? "${dt?.s}s" : sBLK
return dtStr
}
Boolean saveLogtoRemDiagStore(String msg, String type, String logSrcType=sNULL, Boolean frc=false) {
Boolean retVal = false
// log.trace "saveLogtoRemDiagStore($msg, $type, $logSrcType)"
if((Boolean)state.enRemDiagLogging && (Boolean)settings.enRemDiagLogging) {
/*
def turnOff = false
def reasonStr = sBLK
if(frc == false) {
if(getRemDiagActSec() > (3600 * 48)) {
turnOff = true
reasonStr += "was active for last 48 hours "
}
}
if(turnOff) {
saveLogtoRemDiagStore("Diagnostics disabled due to ${reasonStr}", sINFO, "Manager", true)
diagLogProcChange(false)
log.info "Remote Diagnostics disabled ${reasonStr}"
} else {
*/
if(getStateSizePerc() >= 68) {
log.warn "saveLogtoRemDiagStore: remoteDiag log storage suspended state size is ${getStateSizePerc()}%"
} else {
if(msg) {
Long dt = new Date().getTime()
Map item = ["dt":dt, "type":type, "src":(logSrcType ?: "Not Set"), "msg":msg]
List t0 = (List)atomicState.remDiagLogDataStore
List data = t0 ?: []
data << item
atomicState.remDiagLogDataStore = data
retVal = true
}
}
if(frc) {
List t0 = (List)atomicState.remDiagLogDataStore
List data = t0 ?: []
Integer t1 = (Integer)data.size()
if(t1) {
diagLogChecks(frc)
}
} else {
if(!(Boolean)atomicState.diagRunInOn) {
atomicState.diagRunInOn = true
runIn(10, "diagLogChecks", [overwrite: true])
}
}
/*
List data = (List)atomicState.remDiagLogDataStore ?: []
def t0 = data?.size()
if(t0 && (t0 > 30 || frc || getLastRemDiagSentSec() > 120 || getStateSizePerc() >= 65)) {
def remDiagApp = getRemDiagApp()
if(remDiagApp) {
remDiagApp.savetoRemDiagChild(data)
updTimestampMap("remDiagDataSentDt", getDtNow())
} else {
log.warn "Remote Diagnostics Child app not found"
if(getRemDiagActSec() > 20) { // avoid race that child did not start yet
diagLogProcChange(false)
}
retVal = false
}
atomicState.remDiagLogDataStore = []
}
}
*/
}
return retVal
}
void diagLogChecks(Boolean frc=false) {
atomicState.diagRunInOn = false
if((Boolean)state.enRemDiagLogging && (Boolean)settings.enRemDiagLogging) {
String reasonStr = sBLK
if(!frc && getRemDiagActSec() > (3600 * 48)) {
reasonStr += "was active for last 48 hours "
Boolean a=saveLogtoRemDiagStore("Diagnostics disabled due to ${reasonStr}", sINFO, "Manager", true)
diagLogProcChange(false)
log.info "Remote Diagnostics disabled ${reasonStr}"
return
}
List t1 = (List)atomicState.remDiagLogDataStore
List data = t1 ?: []
Integer t0 = (Integer)data.size()
if(t0 && (t0 > 30 || frc || getLastRemDiagSentSec() > 120 || getStateSizePerc() >= 65)) {
def remDiagApp = getRemDiagApp()
if(remDiagApp) {
remDiagApp?.savetoRemDiagChild(data)
atomicState.remDiagLogDataStore = []
updTimestampMap("remDiagDataSentDt", getDtNow())
} else {
log.warn "Remote Diagnostics Child app not found"
if(getRemDiagActSec() > 20) { // avoid race that child did not start yet
diagLogProcChange(false)
}
}
}
}
}
void settingUpdate(String name, String value, String type=sNULL) {
//LogAction("settingUpdate($name, $value, $type)...", sTRC, false)
if(name){
if(type!=sNULL) app.updateSetting(name, [type: type, value: value])
else app.updateSetting(name, value)
}
}
void settingRemove(String name) {
//LogAction("settingRemove($name)...", sTRC, false)
if(name) app.clearSetting(name)
}
/*
void stateUpdate(String key, value) {
if(key) state."${key}" = value
else LogAction("stateUpdate: null key $key $value", sERR, true)
}
*/
//Things that need to clear up on updates go here
void stateCleanup() {
// LogAction("stateCleanup", sTRC, true)
List data = [ "deviceData", "cmdIsProc", "apiIssuesList", "cmdQlist", "nestRedirectUrl" /*, "timestampDtMap" , "accessToken" */ ]
["lastCmdSent", "recentSendCmd", "cmdQ", "remSenLock", "oldTstat", "oldvstat", "oldCamData", "oldProt", "oldPres" ]?.each { String oi->
state.each { if( (Boolean)((String)it.key).startsWith(oi)) { data.push((String)it.key) } }
}
data.each { String item ->
state.remove(item)
}
updTimestampMap("lastApiIssueMsgDt")
atomicState.workQrunInActive = false
state.forceChildUpd = true
List sdata = ["updChildOnNewOnly", 'debugAppendAppName' ]
sdata.each { String item ->
if(settings."${item}" != null) {
settingUpdate(item, sBLK) // clear settings
}
}
}
/******************************************************************************
* STATIC METHODS *
*******************************************************************************/
static String getThermostatChildName() { return "Nest Thermostat" }
static String getProtectChildName() { return "Nest Protect" }
static String getPresenceChildName() { return "Nest Presence" }
static String getCameraChildName() { return "Nest Camera" }
static String getEventDeviceName() { return "Nest Eventstream" }
static String getEventDeviceDni() { return "nest-eventstream01" }
/*
private Integer convertHexToInt(hex) { Integer.parseInt(hex,16) }
private String convertHexToIP(hex) { [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") }
*/
//Returns app State Info
Integer getStateSize() {
String resultJson = new groovy.json.JsonOutput().toJson(state)
return resultJson.length()
}
Integer getStateSizePerc() { return (Integer) ((stateSize / 100000)*100).toDouble().round(0) } //
String debugStatus() { return !(Boolean)settings.appDebug ? "Off" : "On" }
Boolean isAppDebug() { return ((Boolean)settings.appDebug == true) }
static String getObjType(obj) {
if(obj instanceof String) {return "String"}
else if(obj instanceof GString) {return "GString"}
else if(obj instanceof Map) {return "Map"}
else if(obj instanceof List) {return "List"}
else if(obj instanceof ArrayList) {return "ArrayList"}
else if(obj instanceof Integer) {return "Integer"}
else if(obj instanceof BigInteger) {return "BigInteger"}
else if(obj instanceof Long) {return "Long"}
else if(obj instanceof Boolean) {return "Boolean"}
else if(obj instanceof BigDecimal) {return "BigDecimal"}
else if(obj instanceof Float) {return "Float"}
else if(obj instanceof Byte) {return "Byte"}
else { return "unknown"}
}
def getTimeZone() {
def tz
if(location?.timeZone) { tz = location?.timeZone }
else { tz = getNestTimeZone() ? TimeZone.getTimeZone(getNestTimeZone()) : null }
if(!tz) { LogAction("getTimeZone: Hub or Nest TimeZone not found", sWARN, true) }
return tz
}
String formatDt(Date dt) {
SimpleDateFormat tf = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy")
if(getTimeZone()) { tf.setTimeZone(getTimeZone()) }
else {
LogAction("Hubitat TimeZone is not set; Please verify your Zip code is set under Hub Settings", sWARN, true)
}
return tf.format(dt)
}
private Integer getTimeSeconds(String timeKey, Integer defVal, String meth) {
String t0 = getTimestampVal(timeKey)
return !t0 ? defVal : GetTimeDiffSeconds(t0, sNULL, meth).toInteger()
}
String getTimestampVal(String keyName) {
Map tsData = state.timestampDtMap
if(keyName && tsData && tsData[keyName]) { return (String)tsData[keyName] }
return sNULL
}
void updTimestampMap(String keyName, String dt=sNULL) {
Map data = state.timestampDtMap ?: [:]
if(keyName) { data[keyName] = dt }
state.timestampDtMap = data
}
@SuppressWarnings('unused')
Long GetTimeDiffSeconds(String strtDate, String stpDate=sNULL, String methName=sNULL) {
//LogTrace("[GetTimeDiffSeconds] StartDate: $strtDate | StopDate: ${stpDate ?: "Not Sent"} | MethodName: ${methName ?: "Not Sent"})")
if((strtDate && !stpDate) || (strtDate && stpDate)) {
String stopVal = stpDate!=sNULL ? stpDate : getDtNow()
Long start = Date.parse("E MMM dd HH:mm:ss z yyyy", strtDate).getTime()
Long stop = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal).getTime()
Long diff = (stop - start) / 1000L //
//LogTrace("[GetTimeDiffSeconds] Results for '$methName': ($diff seconds)")
return diff
} else { return null }
}
String getDtNow() {
Date now = new Date()
return formatDt(now)
}
static String strCapitalize(String str) {
return str ? str.capitalize() : sNULL
}
def getSettingVal(String svar) {
if(svar == sNULL) { return settings }
return settings[svar] ?: null
}
def getStateVal(String svar) {
return state[svar] ?: null
}
// Calls by Automation children
// parent only method
Boolean remSenLock(val, String myId) {
Boolean res = false
String k = "remSenLock${val}".toString()
if(val && myId) {
String lval = (String)state."${k}"
if(!lval) {
state."${k}" = myId
res = true
} else if(lval == myId) { res = true }
}
return res
}
Boolean remSenUnlock(val, String myId) {
Boolean res = false
if(val && myId) {
String k = "remSenLock${val}".toString()
String lval = (String)state."${k}"
if(lval) {
if(lval == myId) {
state."${k}" = null
state.remove("${k}" as String)
res = true
}
} else { res = true }
}
return res
}
Boolean automationNestModeEnabled(Boolean val=null) {
LogTrace("NestModeEnabled: $val")
return getSetVal("automationNestModeEnabled", val)
/*
if(val == null) {
return state.automationNestModeEnabled ?: false
} else {
state.automationNestModeEnabled = val.toBoolean()
}
return state.automationNestModeEnabled ?: false
*/
}
Boolean setNModeActive(Boolean val=null) {
LogTrace("setNModeActive: $val")
String myKey = "automationNestModeEcoActive"
Boolean retVal
if(!automationNestModeEnabled(null)) {
retVal = getSetVal(myKey, false)
/*
if(automationNestModeEnabled(null)) {
return getSetVal(myKey, val)
if(val == null) {
return state.automationNestModeEcoActive ?: false
} else {
state.automationNestModeEcoActive = val.toBoolean()
}
} else { getSetVal(myKey, false) }
//return state.automationNestModeEcoActive ?: false
*/
} else { retVal = getSetVal(myKey, val) }
return retVal
}
Boolean getSetVal(String k, Boolean val=null) {
if(val == null) {
return state."${k}" ?: false
} else {
state."${k}" = val.toBoolean()
}
return state."${k}" ?: false
}
def getDevice(String dni) {
def d = getChildDevice(dni)
if(d) { return d }
return null
}
def getDevices() {
def d = getChildDevices()
if(d) { return d }
return null
}
def getTheChildren() {
def d = getChildApps()
if(d) { return d }
return null
}
static String appLabel() { return "NST Manager" }
static String gitRepo() { return "tonesto7/nest-manager"}
static String gitBranch() { return "master" }
static String gitPath() { return gitRepo()+'/'+gitBranch() }