/* * * Modified by David LaPorte * Based on the Tank Utility app from 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. * */ import groovy.json.* import java.text.SimpleDateFormat import groovy.time.* import groovy.transform.Field definition( name: "Smart Oil Gauge (Connect)", namespace: "dlaporte", author: "David LaPorte", description: "Virtual device handler for Smart Oil Gauge", 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.3" } 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 SmartOilGaugeAPIEndPoint(){ return "https://api.dropletfuel.com" } private static String SmartOilGaugeDataEndPoint(){ return SmartOilGaugeAPIEndPoint() } private static getChildName(){ return "Smart Oil Gauge" } 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 = false if(settings.advAppDebug){ traceWasOn = true } settingUpdate("advAppDebug", "true", "bool") if(!state.autoTyp){ state.autoTyp = "chart" } unsubscribe() unschedule() setAutomationStatus() List devs = getDevices() Map devicestatus = RefreshDeviceStatus() Boolean quickOut = false devs.each { String dev -> String ChildName = getChildName() String SOGDeviceID = dev String dni = getDeviceDNI(SOGDeviceID) LogTrace("ChildName: " + ChildName) Map devinfo = devicestatus[SOGDeviceID] def d = getChildDevice(dni) if(!d){ label = "Tank " + devinfo.tank_num d = addChildDevice("dlaporte", ChildName, dni) LogAction("created ${d.displayName} with dni: ${dni}", "info", true) runIn(5, "updated", [overwrite: true]) quickOut = true return }else{ } if(d){ LogAction("device for ${d.displayName} with dni ${dni} found", "info", true) subscribe(d, "energy", 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} Enter your Smart Oil Gauge Client ID and Secret." input "ClientId", "string", title: "Smart Oil Gauge Client ID", required: true input "ClientSecret", "string", title: "Smart Oil Gauge Client Secret", required: true, submitOnChange: true } }else{ section("Authentication"){ paragraph "Authentication Succeeded!" } section(){ List devs = 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 } String myUrl = getAppEndpointUrl("getTile/"+dni) String myLUrl = getLocalEndpointUrl("getTile/"+dni) 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) } } } List getDevices(){ LogTrace("getDevices()") List devices = [] if(!getToken()){ LogAction("getDevice: no token available", "info", true) return devices } def Params = [ uri: SmartOilGaugeDataEndPoint(), path: "/auto/get_tank_data.php", body: [ 'access_token': state.APIToken, 'start_index': 0, 'max_results': 1000 ], ] try { httpPost(Params){ resp -> if(resp.status == 200) { obs = parseJson(resp.data.toString()) obs["data"].each { dev -> LogAction("Found device ID: ${dev.sensor_id}", "debug", false) devices += dev.sensor_id } }else{ LogAction("Error from Smart Oil Gauge 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(){ LogTrace("RefreshDeviceStatus()") Map deviceData = [:] if(!getToken()){ LogAction("RefreshDeviceStatus: no token available", "info", true) return deviceData } def Params = [ uri: SmartOilGaugeDataEndPoint(), path: "/auto/get_tank_data.php", body: [ 'access_token': state.APIToken, 'start_index': 0, 'max_results': 1000 ] ] try { httpPost(Params){ resp -> if(resp.status == 200) { obs = parseJson(resp.data.toString()) obs["data"].each { dev -> LogTrace("tank " + dev.tank_num) String dni = [app.id, dev.sensor_id].join('.') deviceData[dev.sensor_id] = dev LogTrace("RefreshDeviceStatus: received device data for ${dni} = ${deviceData[dev.sensor_id]}") } } 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 } state.deviceData = deviceData return deviceData } private Boolean getToken(){ LogTrace("getToken()") Boolean message = true if(!settings.ClientId || !settings.ClientSecret){ LogAction("getToken no ClientId", "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 = now() if (!state.APITokenExpirationTime){ return true }else{ Long ExpirationDate = state.APITokenExpirationTime if (currentDate >= ExpirationDate){return true}else{return false} } } private Boolean getAPIToken(){ log.trace "getAPIToken()Requesting an API Token!" def Params = [ uri: SmartOilGaugeAPIEndPoint(), path: "/brand_token.php", //headers: ['Authorization': "Basic ${getBase64AuthString()}"] body: [ 'grant_type': 'client_credentials', 'client_id': settings.ClientId, 'client_secret': settings.ClientSecret ] ] try { httpPost(Params){ resp -> LogAction("getToken Return Code: ${resp.status} Response: ${resp.data}", "debug", false) if(resp.status == 200){ if (resp.data.access_token){ state.APIToken = resp?.data?.access_token state.APITokenExpirationTime = now() + (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 pollChildren(Boolean updateData=true){ Long execTime = now() LogTrace("pollChildren") if(!getToken()){ LogAction("pollChildren: Was not able to refresh API token expired at ${state.APITokenExpirationTime}.", "warn", true) return } List devices = state.devices if(!devices){ LogAction("pollChildren: no devices available", "warn", true) return } Map deviceData if(updateData){ deviceData = RefreshDeviceStatus() }else{ deviceData = state.deviceData } devices.each {dev -> try { String deviceid = dev String dni = getDeviceDNI(deviceid) def d = getChildDevice(dni) if(!d){ LogAction("pollChildren: no device found $dni", "warn", true) return false } def devData = deviceData[deviceid] if(!devData){ LogAction("pollChildren: no device data available $d.label", "warn", true) return false } def gallons = devData.gallons def level = ((devData.gallons.toFloat()/devData.tank_volume.toFloat())*100).round(2) def lastReadTime = devData.last_read def capacity = devData.tank_volume def battery if (devData.battery == "Excellent") { battery = 100 } else if (devData.battery == "Good") { battery = 75 } else if (devData.battery == "Fair") { battery = 50 } else if (devData.battery == "Poor") { battery = 1 } else { battery = 0 } def events = [ ['level': level], ['energy': level], ['humidity': level], ['capacity': capacity], ['gallons': gallons], ['lastreading': lastReadTime], ['battery': battery] ] 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((now()-execTime), "pollChildren") } String getDeviceDNI(String DeviceID){ return [app.id, DeviceID].join('.') } static String strCapitalize(str){ return str ? str?.toString().capitalize() : null } void automationGenericEvt(evt){ Long startTime = now() Long eventDelay = startTime - evt.date.getTime() LogAction("${evt?.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(evt?.value)}) with a delay of ${eventDelay}ms", "info", false) doTheEvent(evt) } void doTheEvent(evt){ if(getIsAutomationDisabled()){ return } else { scheduleAutomationEval() storeLastEventData(evt) } } void storeLastEventData(evt){ if(evt){ def newVal = ["name":evt.name, "displayName":evt.displayName, "value":evt.value, "date":formatDt(evt.date), "unit":evt.unit] state.lastEventData = newVal //log.debug "LastEvent: ${state.lastEventData}" 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 = state.evalExecutionHistory ?: [] Integer listSize = 20 list = addToList(val, list, listSize) if(list){ state.evalExecutionHistory = list } } //if(!(method in ["watchDogCheck", "checkNestMode"])){ 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()) } */ } List addToList(val, list, Integer listSize){ 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 = 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 = false switch(autoType){ case "chart": if(theTime == defaultAutomationTime()){ theTime += random_int } Integer 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 = state.evalSchedLastTime if(t0 == null){ t0 = 0 } Integer 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(state.autoRunDt, null, "getAutoRunSec").toInteger() } Integer getAutoRunInSec(){ return !state.autoRunInSchedDt ? 100000 : GetTimeDiffSeconds(state.autoRunInSchedDt, null, "getAutoRunInSec").toInteger() } void runAutomationEval(){ LogTrace("runAutomationEval") Long execTime = now() String autoType = getAutoType() state.evalSched = false state.evalSchedLastTime = null switch(autoType){ case "chart": List devs = state.devices devs.each { dev -> String deviceid = dev def deviceData = state.deviceData def devData = deviceData[deviceid] def LastReading = devData.lastReading String dni = getDeviceDNI(deviceid) def d1 = getChildDevice(dni) def level = ((devData.gallons.toFloat() / devData.tank_volume.toFloat())*100).round(2) getSomeData(d1, level) } break default: LogAction("runAutomationEval: Invalid Option Received ${autoType}", "warn", true) break } storeExecutionHistory((now()-execTime), "runAutomationEval") } void getSomeData(dev, level){ LogAction("getSomeData: ${level}", "info", false) if (state."TEnergyTbl${dev.id}" == null){ state."TEnergyTbl${dev.id}" = [] } List energyTbl = state."TEnergyTbl${dev.id}" Date newDate = new Date() if(newDate == null){ Logger("got null for new Date()") } Integer dayNum = newDate.format("D", location.timeZone) as Integer state."TEnergyTbl${dev.id}" = addValue(energyTbl, dayNum, level) } List addValue(List table, Integer dayNum, val){ List newTable = table if(table?.size()){ Integer lastDay = table.last()[0] 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 = now() // try { String devHtml = "" String navHtml = "" String scrStr = "" def allDevices = [] if(theDev){ allDevices << theDev }else{ allDevices = app.getChildDevices(true) } def devices = allDevices Integer devNum = 1 String myType = type ?: "All Devices" devices?.sort {it?.getLabel()}.each { dev -> def navMap = [:] Boolean hasHtml = true // (dev?.hasHtml() == true) if((dev?.typeName in ["Smart Oil Gauge"]) && ((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 += """