/** * * Modified heavily by EricS * Based on ideas from Joshua Spain * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * March 30, 2024 */ //file:noinspection unused //file:noinspection GroovyUnusedAssignment //file:noinspection SpellCheckingInspection import groovy.json.* import java.text.SimpleDateFormat import groovy.time.* import groovy.transform.Field definition( name: "Tank Utility (Connect)", namespace: "imnotbob", author: "Eric S", description: "Virtual device handler for Tank Utility Propane tank monitor", category: "My Apps", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", singleInstance: true, oauth:true ) static String appVersion() { "0.0.5" } preferences { page(name: "settings", title: "Settings", content: "settingsPage", install:true) } mappings { path("/deviceTiles") {action: [GET: "renderDeviceTiles"]} path("/getTile/:dni") {action: [GET: "getTile"]} } private static String TankUtilAPIEndPoint(){ return "https://data.tankutility.com" } private static String TankUtilityDataEndPoint(){ return TankUtilAPIEndPoint() } private static getChildName(){ return "Tank Utility" } void installed(){ log.info "Installed with settings: ${settings}" initialize() } void updated(){ log.info "Updated with settings: ${settings}" initialize() } void uninstalled(){ def todelete = getAllChildDevices() todelete.each { deleteChildDevice(it.deviceNetworkId) } } void initialize(){ LogTrace("initialize") settingUpdate("showDebug", "true", "bool") Boolean traceWasOn; traceWasOn = false if(settings.advAppDebug){ traceWasOn = true } settingUpdate("advAppDebug", "true", "bool") state.deviceData = [:] if(!state.autoTyp){ state.autoTyp = "chart" } unsubscribe() unschedule() //stateRemove("evalSched") //stateRemove("detailEventHistory") setAutomationStatus() List devs = getDevices() Map devicestatus = RefreshDeviceStatus() Boolean quickOut; quickOut = false devs.each { String dev -> String ChildName = getChildName() String TUDeviceID = dev String dni = getDeviceDNI(TUDeviceID) Map devinfo = devicestatus[TUDeviceID] def d; d = getChildDevice(dni) if(!d){ d = addChildDevice("imnotbob", ChildName, dni, null, ["label": devinfo.name ?: ChildName]) LogAction("created ${d.displayName} with dni: ${dni}", "info", true) runIn(5, "updated", [overwrite: true]) quickOut = true return } if(d){ LogAction("device for ${d.displayName} with dni ${dni} found", "info", true) subscribe(d, "energy", automationGenericEvt) subscribe(d, "temperature", automationGenericEvt) } return d } if(quickOut){ return } // we'll be back with runIn after devices settle subscribe(location, "sunrise", automationGenericEvt) runEvery1Hour("pollChildren") scheduleAutomationEval(30) if(!traceWasOn){ settingUpdate("advAppDebug", "false", "bool") } runIn(1800, logsOff, [overwrite: true]) pollChildren(false) } private settingsPage(){ if(!state.access_token){ getAccessToken() } if(!state.access_token){ enableOauth(); getAccessToken() } return dynamicPage(name: "settings", title: "Settings", nextPage: "", uninstall:true, install:true){ Boolean message = getToken() if(!message){ section("Authentication"){ paragraph "${state.lastErr} \n\nEnter your TankUtility Username and Password." input "UserName", "string", title: "Tank Utility Username", required: true input "Password", "string", title: "Tank Utility Password", required: true, submitOnChange: true } }else{ section("Authentication"){ paragraph "Authentication Succeeded!" } section(){ List devs; devs = (List)state.devices if(!devs){ devs = getDevices() Map devicestatus = RefreshDeviceStatus() } String t1 t1 = devs?.size() ? "Status\n • Tanks (${devs.size()})" : "" if(devs?.size() > 1){ String myUrl = getAppEndpointUrl("deviceTiles") String myLUrl = getLocalEndpointUrl("deviceTiles") String myStr = """ All Tanks (local)""" paragraph imgTitle(getAppImg("graph_icon.png"), paraTitleStr(myStr)) } devs.each { dev -> String deviceid = dev String dni = getDeviceDNI(deviceid) def d1 = getChildDevice(dni) if(!d1){ return } // Likely should let someone select which tanks are created here. /* Map deviceData = state.deviceData Map devData = deviceData[deviceid] Map LastReading = (Map)devData.lastReading def temperature = LastReading.temperature.toInteger() def level = (LastReading.tank).toFloat().round(2) String lastReadTime = LastReading.time_iso def capacity = devData.capacity def events = [ ['temperature': temperature], ['level': level], ['energy': level], ['capacity': capacity], ['lastreading': lastReadTime], ] */ String myUrl = getAppEndpointUrl("getTile/"+dni) String myLUrl = getLocalEndpointUrl("getTile/"+dni) //Logger("mainAuto myUrl: ${myUrl} myLUrl: ${myLUrl}") String myStr = """ ${d1.label ?: d1.name} (local)""" paragraph imgTitle(getAppImg("graph_icon.png"), paraTitleStr(myStr)) } } } section(sectionTitleStr("Automation Options:")){ input "autoDisabledreq", "bool", title: imgTitle(getAppImg("disable_icon2.png"), inputTitleStr("Disable this Automation?")), required: false, defaultValue: false /* state.autoDisabled */, submitOnChange: true setAutomationStatus() input("showDebug", "bool", title: imgTitle(getAppImg("debug_icon.png"), inputTitleStr("Debug Option")), description: "Show ${app?.name} Logs in the IDE?", required: false, defaultValue: false, submitOnChange: true) if(showDebug){ input("advAppDebug", "bool", title: imgTitle(getAppImg("list_icon.png"), inputTitleStr("Show Verbose Logs?")), required: false, defaultValue: false, submitOnChange: true) }else{ settingUpdate("advAppDebug", "false", "bool") } } section(sectionTitleStr("Application Security")){ paragraph title:"What does resetting do?", "If you share a url with someone and want to remove their access you can reset your token and this will invalidate any URL you shared and create a new one for you. This will require any use in dashboards to be updated to the new URL." input (name: "resetAppAccessToken", type: "bool", title: "Reset Access Token?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("reset_icon.png")) resetAppAccessToken(settings.resetAppAccessToken == true) } /* section(sectionTitleStr("Automation Name:")){ def newName = getAutoTypeLabel() if(!app?.label){ app?.updateLabel("${newName}") } label title: imgTitle(getAppImg("name_tag_icon.png"), inputTitleStr("Label this Automation: Suggested Name: ${newName}")), defaultValue: "${newName}", required: true //, wordWrap: true if(!state.isInstalled){ paragraph "Make sure to name it something that you can easily recognize." } } */ } } List getDevices(){ LogTrace("getDevices") List devices; devices = [] if(!getToken()){ LogAction("getDevice: no token available", "info", true) return devices } Map Params = [ uri: TankUtilityDataEndPoint(), path: "/api/devices", query: [ token: state.APIToken ], timeout: 20 ] try { httpGet(Params){ resp -> if(resp.status == 200){ String a= ((StringReader)resp.data).getText() Map data= (Map)(new JsonSlurper().parseText(a)) ((List)data.devices).each { dev -> String dni = [app.id, dev].join('.') LogAction("Found device ID: ${dni}", "debug", false) devices += dev } }else{ LogAction("Error from Tank Utility in getDevices - Return Code: ${resp.status} | Response: ${resp.data}", "error", true) state.APIToken = null state.APITokenExpirationTime = 0L } } state.devices = devices } catch (e){ log.error "Error in getDevices: $e" state.APIToken = null state.APITokenExpirationTime = 0L } return devices } private Map RefreshDeviceStatus(Boolean sync=true){ LogTrace("RefreshDeviceStatus()") Map deviceData = (Map)state.deviceData ?: [:] if(!getToken()){ LogAction("RefreshDeviceStatus: no token available", "info", true) state.deviceData = [:] return [:] } List devices = state.devices if(!devices){ LogAction("RefreshDeviceState: no devices available", "warn", true) state.deviceData = [:] return [:] } devices.each {String dev -> //String dni = getDeviceDNI(dev) Map Params = [ uri: TankUtilityDataEndPoint(), path: "/api/devices/${dev}", query: [ token: state.APIToken ], timeout: 20 ] if(sync){ try { httpGet(Params){ resp -> if(resp.status == 200){ String a= ((StringReader)resp.data).getText() Map data= (Map)(new JsonSlurper().parseText(a)) deviceData[dev] = (Map)data.device LogTrace("RefreshDeviceStatus: received device data for ${dev} = ${deviceData[dev]}") }else{ LogAction("RefreshDeviceStatus: Error while receiving events ${resp.status}", "error", true) state.APIToken = null state.APITokenExpirationTime = 0L } } } catch (e){ log.error "Error while processing events for RefreshDeviceStatus ${e}" state.APIToken = null state.APITokenExpirationTime = 0L } } else { asynchttpGet('ahandler', Params, [a:dev]) return [:] } } state.deviceData = deviceData return deviceData } void ahandler(resp, Map edata) { LogTrace("ahandler()") Map deviceData = (Map)state.deviceData ?: [:] Integer responseCode = resp.status if (responseCode == 200 && resp.data) { String dev = edata.a //log.error "resp.data: ${resp.data}" Map rData = (Map) new JsonSlurper().parseText((String) resp.data) deviceData[dev] = (Map)rData.device LogTrace("RefreshDeviceStatus: received device data for ${dev} = ${deviceData[dev]}") state.deviceData = deviceData runIn(8, "pollChildrenF") }else{ String dev = edata.a LogAction("RefreshDeviceStatus: Error while receiving events ${resp.status} for ${dev}", "error", true) state.APIToken = null state.APITokenExpirationTime = 0L } } private Boolean getToken(){ LogTrace("getToken()") Boolean message; message = true if(!settings.UserName || !settings.Password){ LogAction("getToken no password", "warn", false) return false } if (isTokenExpired() || !state.APIToken){ LogTrace("API token expired at ${state.APITokenExpirationTime}. Refreshing API Token") message = getAPIToken() if(!message){ log.warn "getToken $message Was not able to refresh API token expired at ${state.APITokenExpirationTime}." } } return message } Boolean isTokenExpired(){ Long currentDate = wnow() if (!state.APITokenExpirationTime){ return true }else{ Long ExpirationDate = state.APITokenExpirationTime return currentDate >= ExpirationDate } } private String getBase64AuthString(){ String authorize = "${settings.UserName}:${settings.Password}" String authorize_encoded = authorize.bytes.encodeBase64() return authorize_encoded } private Long wnow() { return (Long)now() } private Boolean getAPIToken(){ log.trace "getAPIToken()Requesting an API Token!" Map Params = [ uri: TankUtilAPIEndPoint(), path: "/api/getToken", headers: ['Authorization': "Basic ${getBase64AuthString()}"], timeout: 20 ] try { httpGet(Params){ resp -> String a= ((StringReader)resp.data).getText() LogAction("getToken Return Code: ${resp.status} Response: ${a}", "debug", false) if(resp.status == 200){ if (a){ Map data= (Map)(new JsonSlurper().parseText(a)) state.APIToken = data?.token state.APITokenExpirationTime = wnow() + (24L * 60 * 60 * 1000) LogAction("Token refresh Success. Token expires at ${state.APITokenExpirationTime}", "info", false) state.lastErr="" return true } } state.lastErr="Was not able to refresh API token ${resp.data.error}" } } catch (e){ state.lastErr="Error in the getAPIToken method: $e" } if(state.lastErr){ log.error "returning false getAPIToken ${state.lastErr}" state.APIToken = null state.APITokenExpirationTime = 0L return false } return true } void pollChildrenF(){ pollChildren(false) } void pollChildren(Boolean updateData=true){ Long execTime = wnow() LogTrace("pollChildren $updateData") if(!getToken()){ LogAction("pollChilcren: Was not able to refresh API token expired at ${state.APITokenExpirationTime}.", "warn", true) return } List devices = state.devices if(!devices){ LogAction("pollChilcren: no devices available", "warn", true) return } Map deviceData if(updateData){ deviceData = RefreshDeviceStatus(false) return }else{ deviceData = state.deviceData } devices.each {dev -> try { String deviceid = dev String dni = getDeviceDNI(deviceid) def d = getChildDevice(dni) if(!d){ LogAction("pollChilcren: no device found $dni", "warn", true) return false } Map devData = deviceData[deviceid] if(!devData){ LogAction("pollChilcren: no device data available $d.label", "warn", true) return false } Map LastReading = (Map)devData.lastReading def temperature = LastReading.temperature.toInteger() def level = (LastReading.tank).toFloat().round(2) String lastReadTime = LastReading.time_iso def capacity = devData.capacity def battery = devData.battery_level def est_fill = devData.estimated_fill_date Double consumption = ((Double)devData.average_consumption)?.round(2) def gal = (capacity * level/100).toFloat().round(2) List t0 = (List)state."TEnergyTbl${d.id}" //def t1 = t0?.size() > 1 ? t0[-2] : null List t1 t1 = t0?.size() > 2 && (t0[-2])[1].toFloat().round(2) == (t0[-1])[1].toFloat().round(2) ? t0[-3] : null //Logger("t1: $t1 t0: ${t0} 2nd ${(t0[-2])[1]} last ${(t0[-1])[1]} 3rd ${t0[-3]}") t1 = (!t1 && t0?.size() > 1) ? t0[-2] : t1 //Logger("again t1: $t1 t0: ${t0}") def ylevel = t1 ? t1[1] : 0 def ygal = (capacity * ylevel/100).toFloat().round(2) //def used = gal <= ygal ? (ygal-gal).toFloat().round(2) : "refilled" def used used = (ygal-gal).toFloat().round(2) used = (used < -2) ? "${used} (refilled)" : used List events = [ ['temperature': temperature], ['level': level], ['energy': level], ['capacity': capacity], ['lastreading': lastReadTime], ['gallons': gal], ['used': used], ['battery': battery!='good'? 5 : 100], ['estimatedfill': est_fill], ['avgconsumption': consumption], ] LogAction("pollChidren: Sending events: ${events}", "info", false) events.each { event -> d.generateEvent(event) } LogTrace("pollChildren: sent device data for ${deviceid} = ${devData}") } catch (e){ log.error "pollChildren: Error while sending events for pollChildren: ${e}" } } storeExecutionHistory((wnow()-execTime), "pollChildren") } String getDeviceDNI(String DeviceID){ return [app.id, DeviceID].join('.') } static String strCapitalize(str){ return str ? str?.toString()?.capitalize() : (String)null } void automationGenericEvt(evt){ Long startTime = wnow() Long eventDelay = startTime - evt.date.getTime() LogAction("${evt.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", "info", false) doTheEvent(evt) } void doTheEvent(evt){ if(!getIsAutomationDisabled()){ scheduleAutomationEval() storeLastEventData(evt) } } void storeLastEventData(evt){ if(evt){ def newVal = ["name":evt.name, "displayName":evt.displayName, "value":evt.value, "date":formatDt((Date)evt.date), "unit":evt.unit] state.lastEventData = newVal //log.debug "LastEvent: ${state.lastEventData}" List list; list = state.detailEventHistory ?: [] Integer listSize = 10 if(list?.size() < listSize){ list.push(newVal) } else if(list?.size() > listSize){ Integer nSz = (list?.size()-listSize) + 1 List nList = list?.drop(nSz) nList?.push(newVal) list = nList } else if(list?.size() == listSize){ List nList = list?.drop(1) nList?.push(newVal) list = nList } if(list){ state.detailEventHistory = list } } } void storeExecutionHistory(val, String method = null){ //log.debug "storeExecutionHistory($val, $method)" //try { if(method){ LogTrace("${method} Execution Time: (${val} milliseconds)") } if(method in ["watchDogCheck", "checkNestMode", "schMotCheck"]){ state.autoExecMS = val ?: null List list; list = (List)state.evalExecutionHistory ?: [] Integer listSize = 20 list = addToList(val, list, listSize) if(list){ state.evalExecutionHistory = list } } //if(!(method in ["watchDogCheck", "checkNestMode"])){ List list; list = (List)state.detailExecutionHistory ?: [] Integer listSize = 15 list = addToList([val, method, getDtNow()], list, listSize) if(list){ state.detailExecutionHistory = list } //} /* } catch (ex){ log.error "storeExecutionHistory Exception:", ex //parent?.sendExceptionData(ex, "storeExecutionHistory", true, getAutoType()) } */ } static List addToList(val, List ilist, Integer listSize){ List list; list=[]+ilist if(list?.size() < listSize){ list.push(val) } else if(list?.size() > listSize){ Integer nSz = (list?.size()-listSize) + 1 List nList = list?.drop(nSz) nList?.push(val) list = nList } else if(list?.size() == listSize){ List nList = list?.drop(1) nList?.push(val) list = nList } return list } static Integer defaultAutomationTime(){ return 5 } void scheduleAutomationEval(Integer schedtime = defaultAutomationTime()){ Integer theTime; theTime = schedtime if(theTime < defaultAutomationTime()){ theTime = defaultAutomationTime() } String autoType = getAutoType() def random = new Random() Integer random_int = random.nextInt(6) // this randomizes a bunch of automations firing at same time off same event Boolean waitOverride; waitOverride = false switch(autoType){ case "chart": if(theTime == defaultAutomationTime()){ theTime += random_int } Integer schWaitVal; schWaitVal = settings.schMotWaitVal?.toInteger() ?: 60 if(schWaitVal > 120){ schWaitVal = 120 } Integer t0 = getAutoRunSec() if((schWaitVal - t0) >= theTime ){ theTime = (schWaitVal - t0) waitOverride = true } //theTime = Math.min( Math.max(theTime,defaultAutomationTime()), 120) break } if(!state.evalSched){ runIn(theTime, "runAutomationEval", [overwrite: true]) state.autoRunInSchedDt = getDtNow() state.evalSched = true state.evalSchedLastTime = theTime }else{ String str = "scheduleAutomationEval: " Integer t0; t0 = (Integer)state.evalSchedLastTime if(t0 == null){ t0 = 0 } Integer timeLeftPrev; timeLeftPrev = t0 - getAutoRunInSec() if(timeLeftPrev < 0){ timeLeftPrev = 100 } String str1 = " Schedule change: from (${timeLeftPrev}sec) to (${theTime}sec)" if(timeLeftPrev > (theTime + 5) || waitOverride){ if(Math.abs(timeLeftPrev - theTime) > 3){ runIn(theTime, "runAutomationEval", [overwrite: true]) state.autoRunInSchedDt = getDtNow() state.evalSched = true state.evalSchedLastTime = theTime LogTrace("${str}Performing${str1}") } }else{ LogTrace("${str}Skipping${str1}") } } } Integer getAutoRunSec(){ return !state.autoRunDt ? 100000 : GetTimeDiffSeconds((String)state.autoRunDt, null, "getAutoRunSec").toInteger() } Integer getAutoRunInSec(){ return !state.autoRunInSchedDt ? 100000 : GetTimeDiffSeconds((String)state.autoRunInSchedDt, null, "getAutoRunInSec").toInteger() } void runAutomationEval(){ LogTrace("runAutomationEval") Long execTime = wnow() String autoType = getAutoType() state.evalSched = false state.evalSchedLastTime = null switch(autoType){ case "chart": List devs = state.devices devs.each { dev -> String deviceid = dev Map deviceData = state.deviceData Map devData = deviceData[deviceid] Map LastReading = (Map)devData.lastReading String dni = getDeviceDNI(deviceid) def d1 = getChildDevice(dni) Integer temperature = LastReading.temperature.toInteger() def level = (LastReading.tank).toFloat().round(2) // String lastReadTime = LastReading.time_iso // def capacity = devData.capacity /* def events = [ ['temperature': temperature], ['level': level], ['energy': level], ['capacity': capacity], ['lastreading': lastReadTime], ] */ getSomeData(d1, temperature, level) } /* def weather = parent.getSettingVal("weatherDevice") if(weather){ getSomeWData(weather) } def tstats = parent.getSettingVal("thermostats") def foundTstats if(tstats){ foundTstats = tstats?.collect { dni -> def d1 = parent.getDevice(dni) if(d1){ //LogAction("Found: ${d1?.displayName} with (Id: ${dni})", "debug", false) getSomeData(d1) } return d1 } } def vtstats = parent.getStateVal("vThermostats") def foundvTstats if(tvstats){ foundvTstats = vtstats?.collect { dni -> def mydni = parent.getNestvStatDni(dni).toString() def d1 = parent.getDevice(mydni) if(d1){ //LogAction("Found: ${d1?.displayName} with (Id: ${mydni})", "debug", false) getSomeData(d1) } return d1 } } */ break default: LogAction("runAutomationEval: Invalid Option Received ${autoType}", "warn", true) break } storeExecutionHistory((wnow()-execTime), "runAutomationEval") } void getSomeData(dev, temperature, level){ LogAction("getSomeData: ${temperature} ${level}", "info", false) if (state."TtempTbl${dev.id}" == null){ state."TtempTbl${dev.id}" = [] state."TEnergyTbl${dev.id}" = [] } List tempTbl = (List)state."TtempTbl${dev.id}" List energyTbl = (List)state."TEnergyTbl${dev.id}" Date newDate = new Date() if(newDate == null){ Logger("got null for new Date()") } Integer dayNum = newDate.format("D", (TimeZone)location.timeZone) as Integer // def hr = newDate.format("H", location.timeZone) as Integer // def mins = newDate.format("m", location.timeZone) as Integer state."TtempTbl${dev.id}" = addValue(tempTbl, dayNum, temperature) state."TEnergyTbl${dev.id}" = addValue(energyTbl, dayNum, level) } static List addValue(List table, Integer dayNum, val){ List newTable newTable = table if(table?.size()){ Integer lastDay = table.last()[0] /* def last = table.last()[1] def secondtolast if(table?.size() > 1){ secondtolast = table[-2][1] } */ if(lastDay == dayNum /* || (val == last && val == secondtolast)*/ ){ newTable = table.take(table.size() - 1) } } newTable.add([dayNum, val]) while(newTable.size() > 365){ newTable.removeAt(0) } return newTable } def getTile(){ LogTrace ("getTile()") String responseMsg String dni = "${params?.dni}" if (dni){ def device = getChildDevice(dni) // def device = parent.getDevice(dni) if (device){ return renderDeviceTiles(null, device) }else{ responseMsg = "Device '${dni}' Not Found" } }else{ responseMsg = "Invalid Parameters" } render contentType: "text/html", data: "${responseMsg}" } def renderDeviceTiles(type=null, theDev=null){ Long execTime = wnow() // try { String devHtml, navHtml, scrStr devHtml = "" navHtml = "" scrStr = "" List allDevices allDevices = [] if(theDev){ allDevices << theDev }else{ allDevices = app.getChildDevices(true) } List devices = allDevices Integer devNum; devNum = 1 String myType = type ?: "All Devices" devices?.sort {it?.getLabel()}?.each { dev -> Map navMap Boolean hasHtml = true // (dev?.hasHtml() == true) //Logger("renderDeviceTiles: ${dev.id} ${dev.name} ${theDev?.name} ${dev.typeName}") if((dev?.typeName in ["Tank Utility"]) && ((hasHtml && !type) || (hasHtml && type && dev?.name == type)) ){ LogTrace("renderDeviceTiles: ${dev.id} ${dev.name} ${theDev?.name} ${dev.typeName}") navMap = ["key":dev?.getLabel(), "items":[]] def navItems = navHtmlBuilder(navMap, devNum) String myTile = getEDeviceTile(devNum, dev) //dev.name == "Nest Thermostat" ? getTDeviceTile(devNum, dev) : getWDeviceTile(devNum, dev) if(navItems?.html){ navHtml += navItems?.html } if(navItems?.js){ scrStr += navItems?.js } devHtml += """

${dev?.getLabel()}

${myTile}
""" devNum = devNum+1 } } String myTitle; myTitle = "All Devices" myTitle = type ? "${type}s" : myTitle myTitle = theDev ? "Tank Chart" : myTitle String html = """ ${getWebHeaderHtml(myType, true, true, true, true)}
${devHtml}
""" /* """ */ storeExecutionHistory((wnow()-execTime), "renderDeviceTiles") render contentType: "text/html", data: html // } catch (ex){ log.error "renderDeviceData Exception:", ex } } static Map navHtmlBuilder(Map navMap, Integer idNum){ Map res = [:] String htmlStr, jsStr htmlStr = "" jsStr = "" if(navMap?.key){ htmlStr += """ """ res["html"] = htmlStr res["js"] = jsStr return res } static String navJsBuilder(String btnId, String divId){ String res = """ \$("#${btnId}").click(function(){ \$("html, body").animate({scrollTop: \$("#${divId}").offset().top - hdrHeight - 20},500); closeNavMenu(); toggleMenuBtn(); }); """ return "\n${res}" } String getWebHeaderHtml(String title, Boolean clipboard=true, Boolean vex=false, Boolean swiper=false, Boolean charts=false){ String html html = """ Tank Utility Graphs ('${location?.name}') - ${title} """ html += clipboard ? """""" : "" html += vex ? """""" : "" html += swiper ? """""" : "" html += vex ? """""" : "" html += vex ? """""" : "" html += swiper ? """""" : "" html += charts ? """""" : "" html += vex ? """""" : "" return html } static String hideChartHtml(){ String data = """

Event History


Waiting for more data to be collected...

This may take a few hours

""" return data } String getAutoType(){ return (String)state.autoTyp ?: (String)null } String getAutomationType(){ return (String)state.autoTyp ?: (String)null } String getAppEndpointUrl(String subPath){ return "${getFullApiServerUrl()}${subPath ? "/${subPath}" : ""}?access_token=${state.access_token}" } String getLocalEndpointUrl(String subPath){ return "${getFullLocalApiServerUrl()}${subPath ? "/${subPath}" : ""}?access_token=${state.access_token}" } Boolean getAccessToken(){ try { if(!state.access_token){ state.access_token = createAccessToken() } else { return true } } catch (ex){ String msg = "Error: OAuth is not Enabled for ${app?.name}!." // sendPush(msg) log.error "getAccessToken Exception ${ex?.message}" LogAction("getAccessToken Exception | $msg", "warn", true) return false } } 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(Boolean reset){ if(!reset){ return } LogAction("Resetting Access Token....", "info", true) //revokeAccessToken() state.access_token = null state.accessToken = null if(getAccessToken()){ LogAction("Reset Access Token... Successful", "info", true) settingUpdate("resetAppAccessToken", "false", "bool") } } static String sectionTitleStr(String title) { return '

'+title+'

' } static String inputTitleStr(String title) { return ''+title+'' } //static String pageTitleStr(String title) { return '

'+title+'

' } static String paraTitleStr(String title) { return ''+title+'' } static String imgTitle(String imgSrc, String titleStr, String color=null, Integer imgWidth=30, Integer imgHeight=null){ String imgStyle = "" imgStyle += imgWidth ? "width: ${imgWidth}px !important;" : "" imgStyle += imgHeight ? "${imgWidth ? " " : ""}height: ${imgHeight}px !important;" : "" if(color){ return """
${titleStr}
""" } else { return """ ${titleStr}""" } } static String icons(String name, String napp="App"){ Map icon_names = [ "i_dt": "delay_time", "i_not": "notification", "i_calf": "cal_filter", "i_set": "settings", "i_sw": "switch_on", "i_mod": "mode", "i_hmod": "hvac_mode", "i_inst": "instruct", "i_err": "error", "i_cfg": "configure", "i_t": "temperature" ] //return icon_names[name] String t0 = icon_names."${name}" //LogAction("t0 ${t0}", "warn", true) if(t0) return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${t0}_icon.png".toString() else return "https://raw.githubusercontent.com/${gitPath()}/Images/$napp/${name}".toString() } static String gitRepo() { return "tonesto7/nest-manager"} static String gitBranch() { return "master" } static String gitPath() { return "${gitRepo()}/${gitBranch()}"} String getAppImg(String imgName, Boolean on = null){ return (!disAppIcons || on) ? icons(imgName) : "" } String getDevImg(String imgName, Boolean on = null){ return (!disAppIcons || on) ? icons(imgName, "Devices") : "" } void logsOff(){ Logger("debug logging disabled...") settingUpdate("showDebug", "false", "bool") settingUpdate("advAppDebug", "false", "bool") } void settingUpdate(String name, value, String type=null){ //LogTrace("settingUpdate($name, $value, $type)...") if(name){ if(value == "" || value == null || value == []){ settingRemove(name) return } } if(name && type){ app?.updateSetting(name, [type: "$type", value: value]) } else if (name && type == null){ app?.updateSetting(name, value) } } void settingRemove(String name){ //LogTrace("settingRemove($name)...") if(name){ app?.clearSetting(name.toString()) } } def stateRemove(key){ //if(state.containsKey(key)){ state.remove(key?.toString()) } state.remove(key?.toString()) return true } def setAutomationStatus(Boolean upd=false){ Boolean myDis; myDis = (settings.autoDisabledreq == true) Boolean settingsReset = false // (parent.getSettingVal("disableAllAutomations") == true) Boolean storAutoType = getAutoType() == "storage" if(settingsReset && !storAutoType){ if(!myDis && settingsReset){ LogAction("setAutomationStatus: Nest Integrations forcing disable", "info", true) } myDis = true } else if(storAutoType){ myDis = false } if(!getIsAutomationDisabled() && myDis){ LogAction("Automation Disabled at (${getDtNow()})", "info", true) state.autoDisabledDt = getDtNow() } else if(getIsAutomationDisabled() && !myDis){ LogAction("Automation Enabled at (${getDtNow()})", "info", true) state.autoDisabledDt = null } state.autoDisabled = myDis if(upd){ app.update() } } Boolean getIsAutomationDisabled(){ def dis = state.autoDisabled return (dis != null && dis == true) } // getStartTime("dewTbl", "dewTblYest")) def getStartTime(String tbl1, String tbl2=(String)null){ Integer startTime; startTime = 24 if ( ((List)state."${tbl1}")?.size()){ startTime = (Integer)(((List)state."${tbl1}").min{it[0].toInteger()}[0].toInteger()) } if ( ((List)state."${tbl2}")?.size()){ startTime = Math.min(startTime, (Integer)(((List)state."${tbl2}").min{it[0].toInteger()}[0].toInteger()) ) } return startTime } // getMinTemp("tempTblYest", "tempTbl", "dewTbl", "dewTblYest")) def getMinTemp(tbl1, tbl2=null, tbl3=null, tbl4=null){ List list = [] if (((List)state."${tbl1}")?.size() > 0){ list.add( ((List)state."${tbl1}")?.min { it[1] }[1]) } if (((List)state."${tbl2}")?.size() > 0){ list.add( ((List)state."${tbl2}").min { it[1] }[1]) } if (((List)state."${tbl3}")?.size() > 0){ list.add( ((List)state."${tbl3}").min { it[1] }[1]) } if (((List)state."${tbl4}")?.size() > 0){ list.add( ((List)state."${tbl4}").min { it[1] }[1]) } //LogAction("getMinTemp: ${list.min()} result: ${list}", "trace") return list?.min() } // getMaxTemp("tempTblYest", "tempTbl", "dewTbl", "dewTblYest")) def getMaxTemp(tbl1, tbl2=null, tbl3=null, tbl4=null){ List list = [] if (((List)state."${tbl1}")?.size() > 0){ list.add( ((List)state."${tbl1}").max { it[1] }[1]) } if (((List)state."${tbl2}")?.size() > 0){ list.add( ((List)state."${tbl2}").max { it[1] }[1]) } if (((List)state."${tbl3}")?.size() > 0){ list.add( ((List)state."${tbl3}").max { it[1] }[1]) } if (((List)state."${tbl4}")?.size() > 0){ list.add( ((List)state."${tbl4}").max { it[1] }[1]) } //LogAnction("getMaxTemp: ${list.max()} result: ${list}", "trace") return list?.max() } String getEDeviceTile(Integer devNum=null, dev){ //def obs = getApiXUData(dev) // try { if ( ((List)state."TtempTbl${dev.id}")?.size() <= 0 || ((List)state."TEnergyTbl${dev.id}")?.size() <= 0){ return hideChartHtml() // hideWeatherHtml() } //Logger("W1") //String updateAvail = !state.updateAvailable ? "" : """
Device Update Available!
""" //String clientBl = state.clientBl ? """
Your Manager client has been blacklisted!\nPlease contact the Nest Manager developer to get the issue resolved!!!
""" : "" def temperature; temperature=0 def level String lastReadTime; lastReadTime=(String)null def capacity List devs = state.devices devs.each { mydev -> String deviceid = mydev String dni = getDeviceDNI(deviceid) Map deviceData Map devData if(dev?.deviceNetworkId == dni){ deviceData = (Map)state.deviceData devData = deviceData[deviceid] // def d1 = getChildDevice(dni) Map LastReading = (Map)devData.lastReading //noinspection GrReassignedInClosureLocalVar temperature = LastReading.temperature.toInteger() level = (LastReading.tank).toFloat().round(2) lastReadTime = LastReading.time_iso capacity = devData.capacity } } //Logger("W2") if(capacity==null || level==null) {return (String)null} def regex1 = /Z/ String tt0 = lastReadTime?.replaceAll(regex1,"-0000") Date curConn = tt0 ? Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", tt0) : null //"Not Available" String formatVal = "MMM d, yyyy h:mm a" def tf = new SimpleDateFormat(formatVal) if(getTimeZone()){ tf.setTimeZone(getTimeZone()) } String curConnFmt = curConn!=null ? tf.format(curConn) : "Not Available" def gal = (capacity * level/100).toFloat().round(2) List t0 = (List)state."TEnergyTbl${dev.id}" //def t1 = t0?.size() > 1 ? t0[-2] : null List t1 t1 = t0?.size() > 2 && (t0[-2])[1].toFloat().round(2) == (t0[-1])[1].toFloat().round(2) ? t0[-3] : null //Logger("t1: $t1 t0: ${t0} 2nd ${(t0[-2])[1]} last ${(t0[-1])[1]} 3rd ${t0[-3]}") t1 = (!t1 && t0?.size() > 1) ? t0[-2] : t1 //Logger("again t1: $t1 t0: ${t0}") def ylevel = t1 ? t1[1] : 0 def ygal = (capacity * ylevel/100).toFloat().round(2) //def used = gal <= ygal ? (ygal-gal).toFloat().round(2) : "refilled" def used used = (ygal-gal).toFloat().round(2) used = (used < -2) ? "${used} (refilled)" : used //Logger("used: $used gal: $gal ygal: $ygal ylevel: $ylevel t1: $t1") Integer num; num = 1 def te0 = capacity*0.8 if(gal >= (te0*0.25)){ num = 2 } if(gal >= (te0*0.45)){ num = 3 } if(gal >= (te0*0.65)){ num = 4 } if(gal >= (te0*0.9)){ num = 5 } //def url = "https://app.tankutility.com/images/tank-${num}.png" String url = "https://raw.githubusercontent.com/imnotbob/tankUtility/master/Images/tank-${num}.png".toString() String mainHtml = """

Tank Details

Capacity: ${capacity}
Tank Level: ${level}%
Gallons: ${gal}
Gallons used: ${used}
Tank Temperature: ${temperature}
Last Updated: ${curConnFmt}

${level}

${historyGraphHtml(devNum,dev)}
""" return mainHtml } /* if (state."TtempTbl${dev.id}" == null){ state."TtempTbl${dev.id}" = [] state."TEnergyTbl${dev.id}" = [] } */ String getDataString(Integer seriesIndex, dev){ String dataString; dataString = "" List dataTable dataTable = [] switch (seriesIndex){ case 1: dataTable = (List)state."TtempTbl${dev.id}" break case 2: dataTable = (List)state."TEnergyTbl${dev.id}" break } dataTable.each(){ List dataArray = [it[0],null,null] dataArray[seriesIndex] = it[1] dataString += dataArray?.toString() + "," } return dataString } String historyGraphHtml(Integer devNum=null, dev){ //Logger("HistoryG 1") String html if (state."TtempTbl${dev.id}"?.size() > 0 && state."TEnergyTbl${dev.id}"?.size() > 0){ String tempStr = getTempUnitStr() def minval = getMinTemp("TtempTbl${dev.id}") String minstr// = "minValue: ${minval}," //Logger("HistoryG 1a") def maxval = getMaxTemp("TtempTbl${dev.id}") String maxstr// = "maxValue: ${maxval}," //Logger("HistoryG 1b") //def differ = maxval - minval //LogAction("differ ${differ}", "trace") minstr = "minValue: ${(minval - (wantMetric() ? 2:5))}," maxstr = "maxValue: ${(maxval + (wantMetric() ? 2:5))}," //Logger("HistoryG 2") html = """

History

""" }else{ html = """

Event History



Waiting for more data to be collected

This may take at a couple hours

""" } return html } /* def hideWeatherHtml(){ def data = """



The Required Weather data is not available yet...



Please refresh this page after a couple minutes...





""" // render contentType: "text/html", data: data, status: 200 } */ Boolean wantMetric(){ return (getTemperatureScale() == "C") } String getTempUnitStr(){ String tempStr; tempStr = "\u00b0F" if(wantMetric()){ tempStr = "\u00b0C" } return tempStr } private static TimeZone mTZ(){ return TimeZone.getDefault() } // (TimeZone)location.timeZone static TimeZone getTimeZone(){ return mTZ() /* TimeZone tz; tz = null if(location?.timeZone){ tz = location.timeZone } if(!tz){ LogAction("getTimeZone: Hub or Nest TimeZone not found", "warn", true) } return tz*/ } static String getDtNow(){ Date now = new Date() return formatDt(now) } static String formatDt(Date dt){ SimpleDateFormat tf = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy") if(getTimeZone()){ tf.setTimeZone(getTimeZone()) } /* else { LogAction("HE TimeZone is not set; Please open your location and Press Save", "warn", true) return "" } */ return tf.format(dt) } static Long GetTimeDiffSeconds(String strtDate, String stpDate=null, String methName=null){ //LogTrace("[GetTimeDiffSeconds] StartDate: $strtDate | StopDate: ${stpDate ?: "Not Sent"} | MethodName: ${methName ?: "Not Sent"})") if((strtDate && !stpDate) || (strtDate && stpDate)){ //if(strtDate?.contains("dtNow")){ return 10000 } Date now = new Date() String stopVal = stpDate ? stpDate.toString() : formatDt(now) Long start = Date.parse("E MMM dd HH:mm:ss z yyyy", strtDate).getTime() Long stop = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal).getTime() Long diff = Math.round((stop - start) / 1000L) //LogTrace("[GetTimeDiffSeconds] Results for '$methName': ($diff seconds)") return diff }else{ return null } } /************************************************************************************************ | LOGGING AND Diagnostic | *************************************************************************************************/ void LogTrace(String msg, String logSrc=null){ Boolean trOn = (showDebug && advAppDebug) if(trOn){ Logger(msg, "trace", logSrc) } } void LogAction(String msg, String type=null, Boolean showAlways=false, String logSrc=null){ String myType = type ?: "debug" Boolean isDbg = showDebug ? true : false if(showAlways || isDbg){ Logger(msg, myType, logSrc) } } void Logger(String msg, String type=null, String logSrc=null, Boolean noLog=false){ String myType = type ?: "debug" if(!noLog){ if(msg && myType){ String labelstr labelstr = "" if(state.dbgAppndName == null){ def tval = settings.dbgAppndName state.dbgAppndName = (tval || tval == null) } String t0 = app.label if(state.dbgAppndName){ labelstr = t0+" | " } String themsg = labelstr+msg //log.debug "Logger remDiagTest: $msg | $type | $logSrc" switch(myType){ case "debug": log.debug themsg break case "info": log.info '|| '+themsg break case "trace": log.trace '| '+themsg break case "error": log.error '| '+themsg break case "warn": log.warn '| '+themsg break default: log.debug themsg break } } else { log.error "${labelstr}Logger Error - type: ${type} | msg: ${msg} | logSrc: ${logSrc}" } } }