/* * webCoRE - Community's own Rule Engine - Web Edition for HE * * Copyright 2016 Adrian Caramaliu * * webCoRE Fuel Stream & graphs * * * Significant parts of graphs modified from Hubigraph by tchoward * * Copyright 2020, but let's be honest, you'll copy it * * 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. * * Last update May 30, 2024 for Hubitat */ //file:noinspection GroovySillyAssignment //file:noinspection GrDeprecatedAPIUsage //file:noinspection GroovyDoubleNegation //file:noinspection GroovyUnusedAssignment //file:noinspection unused //file:noinspection SpellCheckingInspection //file:noinspection GroovyFallthrough //file:noinspection GrMethodMayBeStatic //file:noinspection UnnecessaryQualifiedReference @Field static final String sVER='v0.3.114.20220203' @Field static final String sHVER='v0.3.114.20240101_HE' static String version(){ return sVER } static String HEversion(){ return sHVER } /** webCoRE DEFINITION **/ static String handle(){ return 'webCoRE' } import groovy.json.* import groovy.time.TimeCategory import java.text.DecimalFormat import groovy.transform.Field import groovy.transform.CompileStatic import java.util.concurrent.Semaphore @CompileStatic static Boolean eric(){ return false } @CompileStatic static Boolean eric1(){ return false } @CompileStatic private Boolean isEric(){ eric1() && isDbg() } static Boolean useRemote(){ return eric1() } private Boolean isSystemType(){ if (!eric()) return isSystemTypeOrHubDeveloper() return false } definition( namespace:"ady624", (sNM):"${handle()} Fuel Stream", description: "Local container for fuel streams, graphs", author:"jp0550", category:"My Apps", iconUrl:gimg('app-CoRE.png'), iconX2Url:gimg('app-CoRE@2x.png'), iconX3Url:gimg('app-CoRE@3x.png'), importUrl:'https://raw.githubusercontent.com/imnotbob/webCoRE/hubitat-patches/smartapps/ady624/webcore-fuel-stream.src/webcore-fuel-stream.groovy', parent: "ady624:${handle()}" ) @Field static final String sBOOL='bool' @Field static final String sTEXT='text' @Field static final String sATTR='attribute' @Field static final String sDISPNM='displayName' @Field static final String sRID='rid' @Field static final String sDT='date' @Field static final String sGRAPHT='graphType' @Field static final String sLONGTS='longtermstorage' @Field static final String sBACKGRND='background' @Field static final String sTRANSPRNT='transparent' @Field static final String sDEFLT='default' @Field static final String sUNITS='units' @Field static final String sUNIT='unit' @Field static final String sVAR='var' @Field static final String sWHT='#FFFFFF' @Field static final String sBLACK='#000000' @Field static final String sSILVER='#C0C0C0' @Field static final String sLGHTGRN='#18BC9C' @Field static final String sDRKBLUE='#2C3E50' @Field static final String sTRUE='true' @Field static final String sFALSE='false' @Field static final String s100='100' @Field static final String s400='400' @Field static final String sRIGHT='right' @Field static final String sLEFT='left' @Field static final String sBLCOL='baseline_column' @Field static final String sBLROW='baseline_row' @Field static final String sALIGNMENT='alignment' @Field static final String sIMPERIAL='imperial' @Field static final String sMETRIC='metric' @Field static final String sMETERSPS='meters_per_second' @Field static final String sMILESPH='miles_per_hour' @Field static final String sKILOSPH='kilometers_per_hour' @Field static final String sCENTER='center' @Field static final String sJUSTIFICATION='justification' @Field static final String sMIN='min' @Field static final String sMAX='max' @Field static final String sSUBONCHG='submit_on_change' @Field static final String sDECIMALS='decimals' @Field static final String sMULTP='multiple' @Field static final String sSUBOC='submitOnChange' @Field static final String sDEFV='defaultValue' @Field static final String sSENSOR='sensor' @Field static final String sCSENSOR='Sensor' @Field static final String sFUELSTRM='fuelstream' @Field static final String sCFUELSTRM='Fuel Stream' @Field static final String sPOLL='poll' @Field static final String sGRPHSTATICSZ='graph_static_size' @Field static final String sGRPHUPDRATE='graph_update_rate' @Field static final String s100PCT='100%' @Field static final String s80PCT='80%' @Field static final Long lMSDAY=86400000L preferences{ page(name: "mainPage", install: true, uninstall: true) page(name: "deviceSelectionPage") page(name: "attributeConfigurationPage", nextPage: "mainPage") page(name: "graphSetupPage", nextPage: "mainPage") page(name: "enableAPIPage") page(name: "disableAPIPage") } mappings{ path("/graph/"){ action: [ GET: "getGraph" ] } path("/getData/"){ action: [ GET: "getData" ] } path("/getOptions/"){ action: [ GET: "getOptions" ] } path("/getSubscriptions/"){ action: [ GET: "getSubscriptions" ] } path("/updateSettings/"){ action: [ POST: "updateSettings" ] } path("/tile/"){ action: [ GET: "getTile" ] } path("/getFile1/"){ action: [ GET: "getFile1" ] } path("/getFile2/"){ action: [ GET: "getFile2" ] } path("/getFile3/"){ action: [ GET: "getFile3" ] } path("/getFile4/"){ action: [ GET: "getFile4" ] } } def installed(){ log.debug "Installed with settings: ${settings}" state[sDBGLVL]=iZ state[sLOGNG]=iZ if(gtSetB('duplicateFlag') && !gtStB('dupPendingSetup')){ Boolean maybeDup= ((String)app?.getLabel())?.contains(' (Dup)') state.dupPendingSetup= true runIn(i2, "processDuplication") if(maybeDup) info "installed found maybe a dup... ${gtSetB('duplicateFlag')}",null }else if(!gtStB('dupPendingSetup')){ if(gtSetStr('app_name')) app.updateLabel(gtSetStr('app_name')) } } private void processDuplication(){ String al= (String)app?.getLabel() String newLbl= "${al}${al?.contains(' (Dup)') ? sBLK : ' (Dup)'}" app?.updateLabel(newLbl) state.dupPendingSetup= true String dupSrcId= settings.duplicateSrcId ? gtSetStr('duplicateSrcId') : sNL Map dupData= parent?.getChildDupeData("graphs", dupSrcId) if(eric()) log.debug "dupData: ${dupData}" if(dupData){ Map dd dd= (Map)dupData?.state if(dd?.size()){ dd.each{ String k,v-> state[k]= v } } Map dd1= (Map)dupData?.settings if(dd1?.size()){ dd1.each{ String k, Map v-> if(sMs(v,sTYPE) in [sENUM, 'mode']){ wremoveSetting(k) settingUpdate(k, (v[sVAL] != null ? v[sVAL] : null), sMs(v,sTYPE)) } } } } (void)parent.childAppDuplicationFinished("graphs", dupSrcId) info "Duplicated Graph has been created... Please open the new graph and configure to complete setup...",null } def uninstalled(){ if(state.endpoint){ try{ log.debug "Revoking API access token" revokeAccessToken() }catch(e){ warn "Unable to revoke API access token: ",null,iN2,e } } removeChildDevices(getChildDevices()) Map foo=(Map)state.fuelStream if(foo){ (void)parent.resetFuelStreamList() fuelFLD=null readTmpFLD= [:] readTmpBFLD= [:] writeTmpFLD= [:] } } private removeChildDevices(delete){ delete.each{ deleteChildDevice(it.deviceNetworkId) } } @Field static final String dupMSGFLD= "This graph is duplicated and has not had configuration completed... Please open graph and configure to complete setup..." def updated(){ log.debug "updated() with settings: ${settings}" Boolean maybeDup= ((String)app?.getLabel())?.contains(' (Dup)') if(maybeDup) info "updated found maybe a dup... ${gtSetB('duplicateFlag')}",null if(gtSetB('duplicateFlag')){ if(gtStB('dupOpenedByUser')){ state.dupPendingSetup= false } if(gtStB('dupPendingSetup')){ info dupMSGFLD,null return } info "removing duplicate status",null wremoveSetting('duplicateFlag'); wremoveSetting('duplicateSrcId') state.remove('dupOpenedByUser'); state.remove('dupPendingSetup'); state.remove('badMode') } wremoveSetting('debug') wremoveSetting('dummy') wremoveSetting('graph_refresh_rate') Map fs=state.fuelStream String typ typ= fs ? sFUELSTRM : gtSetStr(sGRAPHT) if(typ && typ!=sFUELSTRM && (!gtSetStr('app_name') || typ==sLONGTS)){ app.updateSetting('app_name', 'webCoRE '+tDesc()) // cannot rename LTS } if(typ && (typ in [sFUELSTRM,sLONGTS])){ readTmpFLD= [:] // clear memory file cache readTmpBFLD= [:] writeTmpFLD= [:] fuelFLD=null // clear list of fuel streams cache } if(gtSetStr('app_name')) app.updateLabel(gtSetStr('app_name')) state[sDBGLVL]=iZ String tt1=gtSetStr(sLOGNG) Integer tt2=iMs((Map)state,sLOGNG) String tt3=tt2.toString() if(tt1==sNL)setLoggingLevel(tt2 ? tt3:s0) else if(tt1!=tt3)setLoggingLevel(tt1) state.remove('saveC') state.remove('devInstruct') state.remove('graphUsesHistory') if(gtSetB('install_device')){ hubiTool_create_tile() } if(fs){ // is a fuel stream if(app.id){ // if someone changed storage settings List a=getFuelStreamData(null, false) if(a) storeFuelUpdate(a,fs) } } if(typ==sLONGTS){ if(isDbg()) myDetail null,"updated",i1 unschedule() clearSch() clearSema() if(sensors){ for(sensor in (List)sensors){ String sid=gtSensorId(sensor) if(sid==sBLK){ error "updated null sid ${sensor}",null,iN2 continue } List att=(List)settings["${sid}_attributes"] if(att){ for(String attribute in att){ Map data=[(sID): sid, (sATTR): attribute] updateData_LTS(data) setupCron(sensor, attribute) } } } runNextSched() schedule("17 9/30 * ? * * *", checkSched, [overwrite: false]) // watchDog for lts } if(isDbg()) myDetail null,"updated" } } void setLoggingLevel(String level){ Integer mlogging mlogging=level.isInteger()? level.toInteger():iZ mlogging=Math.min(Math.max(iZ,mlogging),i3) app.updateSetting(sLOGNG,[(sTYPE):sENUM,(sVAL):mlogging.toString()]) state[sLOGNG]=mlogging // if(mlogging==iZ)state[sLOGS]=[] } /** * This defines the UI and external web methods for each graph type * A running instance of this code is one of the below 'types' */ @Field static final Map> jumpFLD=[ "gauge":[ main: "mainGauge", deviceSelection: "deviceGauge", attributeConfiguration: "attributeGauge", graphSetup: "graphGauge", getGraph: "getGraph_gauge", getData: "getData_gauge", getOptions: "getOptions_gauge", getSubscriptions: "getSubscriptions_gauge", desc: "Gauge" ], "bar":[ main: "mainBar", deviceSelection: "deviceBar", attributeConfiguration: "attributeBar", graphSetup: "graphBar", getGraph: "getGraph_bar", getData: "getData_bar", getOptions: "getOptions_bar", getSubscriptions: "getSubscriptions_bar", desc: "Bar Graph" ], "timeline":[ main: "mainTimeline", deviceSelection: "deviceTimeline", attributeConfiguration: "attributeTimeline", graphSetup: "graphTimeline", getGraph: "getGraph_timeline", getData: "getData_timeline", getOptions: "getOptions_timeline", getSubscriptions: "getSubscriptions_timeline", desc: "Time Line Chart" ], "timegraph":[ main: "mainTimegraph", deviceSelection: "deviceTimegraph", attributeConfiguration: "attributeTimegraph", graphSetup: "graphTimegraph", getGraph: "getGraph_timegraph", getData: "getData_timegraph", getOptions: "getOptions_timegraph", getSubscriptions: "getSubscriptions_timegraph", desc: "Time Graph" ], "heatmap":[ main: "mainHeatmap", deviceSelection: "deviceHeatmap", attributeConfiguration: "attributeHeatmap", graphSetup: "graphHeatmap", getGraph: "getGraph_heatmap", getData: "getData_heatmap", getOptions: "getOptions_heatmap", getSubscriptions: "getSubscriptions_heatmap", desc: "Heat Map" ], "linegraph":[ main: "mainLinegraph", deviceSelection: "deviceLinegraph", attributeConfiguration: "attributeLinegraph", graphSetup: "graphLinegraph", getGraph: "getGraph_linegraph", getData: "getData_linegraph", getOptions: "getOptions_linegraph", getSubscriptions: "getSubscriptions_linegraph", desc: "Line Graph" ], "rangebar":[ main: "mainRangebar", deviceSelection: "deviceRangebar", attributeConfiguration: "attributeRangebar", graphSetup: "graphRangebar", getGraph: "getGraph_rangebar", getData: "getData_rangebar", getOptions: "getOptions_rangebar", getSubscriptions: "getSubscriptions_rangebar", desc: "Range Bar" ], "radar":[ main: "mainRadar", deviceSelection: "none", attributeConfiguration: "none", graphSetup: "tileRadar", getGraph: "getGraph_radar", getData: "none", getOptions: "none", getSubscriptions: "none", desc: "Radar Tile" ], "weather2":[ main: "mainWeather2", deviceSelection: "deviceWeather2", attributeConfiguration: "none", graphSetup: "tileWeather2", getGraph: "getGraph_weather2", getData: "getData_weather2", getOptions: "getOptions_weather2", getSubscriptions: "none", updateSettings: "updateSettings_weather2", getTile: "getTile_weather2", desc: "Weather Tile 2.0" ], "forecast":[ main: "mainForecast", deviceSelection: "none", attributeConfiguration: "none", graphSetup: "tileForecast", getGraph: "getGraph_forecast", getData: "getData_forecast", getOptions: "getOptions_forecast", getSubscriptions: "none", desc: "Weather Forecast Tile" ], // next two are not graphs "longtermstorage":[ main: "mainLongtermstorage", deviceSelection: "deviceLongtermstorage", attributeConfiguration: "optionsLongtermstorage", graphSetup: "graphLongtermstorage", getGraph: "none", getData: "none", getOptions: "none", getSubscriptions: "none", desc: "Long Term Storage" ], "fuelstream":[ main: "mainFuelstream", desc: "Fuel Stream" ], ] String tDesc(){ String typ=gtSetStr(sGRAPHT) if(typ) return sMs(jumpFLD[typ],'desc') return sNL } def checkDup(){ Boolean dup= (gtSetB('duplicateFlag') && gtStB('dupPendingSetup')) if(dup){ state.dupOpenedByUser= true section(){ paragraph "This Graph was created from an existing graph.

Please review the settings and save to activate...
${state.badMode ?: sBLK}" } } } def mainPage(){ Map fs=(Map)state?.fuelStream String typ // fuel stream does not have graphType set typ= fs ? sFUELSTRM : gtSetStr(sGRAPHT) if(typ){ String s= sMs(jumpFLD[typ],'main') if(isEric())myDetail null,"${s}",i1 def a="${s}"() if(isEric())myDetail null,"${s}" a } else{ Map stuff stuff=[:] for(Map.Entry>par in jumpFLD){ if(par.key in [sFUELSTRM]) continue // don't create fuels this way if(par.key in [sLONGTS]){ Boolean ltsExists=(Boolean)parent.ltsExists() if(ltsExists) continue // can only be 1 LTS } stuff += [(par.key): par.value.desc] } dynamicPage((sNM): "mainPage"){ section(){ input sGRAPHT,sENUM,(sTIT):'Graph Type',options:stuff,(sREQ):true,(sSUBOC):true } } } } def doJump(String meth){ String typ=gtSetStr(sGRAPHT) String s= sMs(jumpFLD[typ],meth) if(isEric())myDetail null,s,i1 def a="${s}"() if(isEric())myDetail null,s a } def deviceSelectionPage(){ doJump('deviceSelection') } def attributeConfigurationPage(){ doJump('attributeConfiguration') } def graphSetupPage(){ doJump('graphSetup') } //oauth endpoints def getGraph(){ String typ=gtSetStr(sGRAPHT) if(isEric())myDetail null,"getGraph_${typ}",i1 String s= (String)"${sMs(jumpFLD[typ],'getGraph')}"() if(isEric()){ String ss= sBLK // s.replaceAll('<', '<').replaceAll('>','>') myDetail null,"getGraph_${typ}: $ss" } return wrender(contentType: "text/html", data: s) } def getData(){ String typ=gtSetStr(sGRAPHT) if(isEric())myDetail null,"getData_${typ}",i1 String s= (String)"${sMs(jumpFLD[typ],'getData')}"() if(isEric())myDetail null,"getData_${typ}: $s" return wrender(contentType: "text/json", data: s) } def getOptions(){ String typ=gtSetStr(sGRAPHT) if(isEric())myDetail null,"getOptions_${typ}",i1 String s= JsonOutput.toJson( (Map)"${sMs(jumpFLD[typ],'getOptions')}"() ) if(isEric())myDetail null,"getOptions_${typ}: $s" return wrender(contentType: "text/json", data: s) } def getSubscriptions(){ String typ=gtSetStr(sGRAPHT) if(isEric())myDetail null,"getSubscriptions_${typ}",i1 String s= JsonOutput.toJson( (Map)"${sMs(jumpFLD[typ],'getSubscriptions')}"() ) if(isEric())myDetail null,"getSubscriptions_${typ}: $s" return wrender(contentType: "text/json", data: s) } def updateSettings(){ doJump('updateSettings') } def getTile(){ doJump('getTile') } void revokeAccessToken(){ state.remove('accessToken') state.remove('endpoint') state.remove('localEndpoint') state.remove('endpointSecret') state.remove('localEndpointURL') state.remove('remoteEndpointURL') } /* shared method */ void initializeAppEndpoint(Boolean disableRetry=false){ String accessToken; accessToken=(String)state.accessToken if(!state.endpoint || !accessToken){ try{ if(!accessToken) accessToken=createAccessToken() // this fills in state.accessToken } catch(e){ debug "Error: ",null,iN2,e } if(accessToken){ state.endpoint=getApiServerUrl() state.localEndpoint=getLocalApiServerUrl() state.localEndpointURL=fullLocalApiServerUrl(sBLK) state.remoteEndpointURL=fullApiServerUrl(sBLK) state.endpointSecret=accessToken }else if(!disableRetry){ state.accessToken=null enableOauth() initializeAppEndpoint(true) }else { error "Could not get access token",null revokeAccessToken() } } } private void enableOauth(){ Map params=[ uri: "http://localhost:8080/app/edit/update?_action_update=Update&oauthEnabled=true&id=${app.appTypeId}".toString(), headers: ['Content-Type':'text/html;charset=utf-8'] ] try{ httpPost(params){ resp -> //LogTrace("response (sDATA): ${resp.data}") } }catch(e){ error "enableOauth something went wrong: ",null,iN2,e } } def enableAPIPage(){ dynamicPage((sNM): "enableAPIPage",(sTIT): sBLK){ section(){ if(!state.localEndpoint) initializeAppEndpoint() if(!state.endpoint){ paragraph "Endpoint creation failed" }else{ paragraph "It has been done. Your token has been CREATED. Tap Done to continue." } } } } def disableAPIPage(){ dynamicPage((sNM): "disableAPIPage"){ section(){ if(state.endpoint){ try{ revokeAccessToken() }catch(ignored){} } paragraph "It has been done. Your token has been REVOKED. Tap Done to continue." } } } /** m.string */ @CompileStatic private static String sMs(Map m,String v){ (String)m[v] } /** m.string */ @CompileStatic private static Integer iMs(Map m,String v){ (Integer)m[v] } @CompileStatic private static Boolean bIs(Map m,String v){ (Boolean)m.get(v) } @Field static final String sFuelDelim='-' /** * Encode a fuel stream identifier Map to settings String * @param stream [i:app.id, c: 'LTS', n:sid+'_'+attribute,w:1,t: getFormattedDate(new Date())] * @return id-canister||name */ @CompileStatic static String encodeStreamN(Map stream){ String streamName="${(stream[sC] ?: sBLK)}||${stream[sN]}" String id="${stream[sI]}" // encoded stream name String name=id+sFuelDelim+streamName return name } /** * Decode a settings string to a search map for the fuel stream * @param stream id-canister||name * @return [i:id, c: canister, n:name] */ @CompileStatic static Map decodeStreamN(String stream){ // parse out i, c, n //String streamName="${(stream.c ?: sBLK)}||${stream.n}" String[] tname=stream.split(sFuelDelim) //id+'-'+streamName Integer i=tname[iZ].toInteger() //"${stream.i}" String[] tname1=tname[i1].split("\\|\\|") //streamName String c=tname1[iZ] String n=tname1[i1] // if(isEric())myDetail null,"decodeStreamN stream: $stream tname: $tname id: $i tname1: $tname1",iN2 return [(sI):i, (sC):c, (sN):n] } // cache of fuelstreams @Field static ListfuelFLD List gtFuelList(){ fuelFLD= !fuelFLD ? (List)parent.listFuelStreams(false) : fuelFLD return fuelFLD } /** * Return stream identifier for settings-encoded fuel stream name * @param name - settings encoded stream name * @return Map [i:, c: , n: ,w:1, t: getFormattedDate(new Date())] */ @CompileStatic Map findStream(String name){ String s= "findStream $name" // if(isEric())myDetail null,s,i1 Map stream; stream=null Listfstreams= gtFuelList() if(name){ Integer i String c,n i=null; c=null; n=null // parse out i, c, n Map r=decodeStreamN(name) i=iMs(r,sI) c=sMs(r,sC) n=sMs(r,sN) String si=sI String sc=sC String sn=sN stream= fstreams.find{ Map it -> iMs(it,si)==i && sMs(it,sc)==c && sMs(it,sn)==n } // if(isEric())myDetail null,s+" found $stream c: $c i:$i n:$n" } return stream } /** * Clear fuel stream settings (for cases only a single data can be used) * @param multiple */ void clearFvarn(String fvarn, Boolean multiple){ //String fvarn=multiple ? 'fstreams' : 'fstream_' def fl= gtSetting(fvarn) if(fl){ List ifstreams=multiple ? (List)fl : [(String)fl] Map stream for(String stream_nm in ifstreams){ stream= findStream(stream_nm) // @return Map [i:, c: , n: ,w:1, t: getFormattedDate(new Date())] //Map ent=[(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: 'stream'] if(stream){ // stream exists //String name=encodeStreamN(stream) String sid=sF+sMs(stream,sI) String attr='stream' quantRmInput(sid, attr) rmAllowLastInput(sid,attr) } } wremoveSetting(fvarn) } } /** * Clear sensor settings (for cases only a single data can be used) * @param multiple */ void clearVarn(Boolean multiple){ String varn=multiple ? 'sensors' : 'sensor_' String attrn attrn= multiple ? sNL : 'attribute_' def sl= gtSetting(varn) if(sl){ List items= multiple ? (List)sl : [sl] for(sensor in items){ String sid=sD+gtSensorId(sensor) if(sid==sD){ error "clearVarn null sid ${sensor}",null,iN2 continue } attrn=multiple ? "attributes_${sid}".toString() : "attribute_" def al= gtSetting(attrn) if(al){ List attrs= multiple ? (List)al : [(String)al] for(String attr in attrs){ Map ent=makeSensorDataEntry(sensor,sid,attr) quantRmInput(sid,attr) if(ent)rmAllowLastInput(ent) else rmAllowLastInput(sid,attr) } wremoveSetting(attrn) attrn= sNL } } wremoveSetting(varn) } if(attrn && gtSetting(attrn)){ wremoveSetting(attrn) } } void clearQuants(){ Integer i String sb for(i=iZ; i=iZ && i<60000) // remove invalid wremoveSetting(sGRPHUPDRATE) } /** * create a sensor dataSource entry * @param sensor * @param sid * @param attr * @return Map [(sT): sSENSOR, id: sid, rid: sensor.id, displayName: sensor.displayName, a: attr] */ Map makeSensorDataEntry(sensor,String sid,String attr){ //String s= "makeSensorDataEntry $sensor $sid $attr " if(!sid || !attr) error("no sid or attr sid: $sid attr: $attr",null,iN2) Map ent ent=[(sT):sSENSOR, (sID):sid, (sRID):sensor.id, (sDISPNM):sensor.displayName, (sA):attr] Map lu= checkLastUpd(ent) if(lu){ ent += lu stToPoll() // javascript code does not handle lastupdate/dynamic updates correctly for the attribute, it uses the sensor } // sensors (devices) are in a setting so they show in use // TODO will need to report to parent 'I'm using these fuel streams' i// if(isEric())myDetail null,s+"ent: $ent",iN2 return ent } private String gtSensorId(sensor){ if(sensor) return sensor.id.toString() else warn "gtSensorID no sensor",null return sBLK } Map makeQuantDataEntry(String typ,String sid,String attrn){ //String s= "makeQuantDataEntry $typ $sid $attrn " String attribute; attribute=sBLK Map ent; ent=null if(typ in [sCFUELSTRM]){ String stream= gtSetting(sid) ent=makeFuelDataEntry(stream) //ent=[(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: attr] attribute= attrn }else if(typ in [sCSENSOR]){ def sensor= gtSetting(sid) String msid=sD+gtSensorId(sensor) if(msid!=sD){ attribute= gtSetting(attrn) ent=makeSensorDataEntry(sensor,msid,attribute) //ent=[(sT): sSENSOR, id: sid, rid: sensor.id, displayName: sensor.displayName, a: attr] }else error "makeQuantDataEntry null sid ${sid} ${sensor}",null,iN2 }else{ warn "no clear typ $typ",null // if(isEric())myDetail null,s+"ent: [:]",iN2 return [:] } if(!ent) return [:] Map nent nent=[(sT): 'quant', (sID): sid, ent: ent, (sRID): ent[sRID], (sDISPNM): sMs(ent,sDISPNM)+' quant', (sA): attribute] // if to return data quantized, add to ent Map params= quantParams(nent[sID],sMs(nent,sA)) if(params){ nent+=[(sQ): params] stToPoll() } /* Map lu= checkLastUpd(nent) if(lu){ nent += lu stToPoll() // javascript code does not handle lastupdate/dynamic updates correctly for the attribute, it uses the sensor } */ // sensors (devices) are in a setting so they show in use // TODO will need to report to parent 'I'm using these fuel streams' // if(isEric())myDetail null,s+"ent: $nent",iN2 return nent } /** * get state.datasources * @return list of data sources *

* Map ent=[(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: 'stream'] *

* Map ent=[(sT): sSENSOR, id: 'd'+rid, rid: sensor.id, displayName: sensor.displayName, a: attr] *

* Map ent=[(sT): 'quant', id: 'q'+[0-10]+'attr', rid: id, displayName: , a: 'quant' or actual attr, qp: [params], ent: [sensor or fuel]] */ List gtDataSources(){ return state.dataSources } /** * create state.datasources from settings * @param multiple - are allowed * @return list of data sources and update to state.dataSources *

* Map ent=[(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: 'stream'] *

* Map ent=[(sT): sSENSOR, id: 'd'+rid, rid: sensor.id, displayName: sensor.displayName, a: attr] *

* Map ent=[(sT): 'quant', id: 'q'+[0-10]+'attr', rid: id, displayName: , a: 'quant' or actual attr, qp: [params], ent: [sensor or fuel]] */ List createDataSources(Boolean multiple){ String s= "createDataSources $multiple " if(isEric())myDetail null,s,i1 String fvarn=multiple ? 'fstreams' : 'fstream_' String varn=multiple ? 'sensors' : 'sensor_' String attrn List dataSources dataSources=[] assignSt('hasFuel',false) Boolean hq= hasQuants() def sl= gtSetting(varn) def fl= gtSetting(fvarn) if(fl || sl || hq){ if(fl){ if(isEric())myDetail null,s+"processing fuel streams ${fl}",iN2 List ifstreams=multiple ? (List)fl : [(String)fl] for(String stream in ifstreams){ Map ent=makeFuelDataEntry(stream) if(ent)dataSources << ent } } if(sl){ if(isEric())myDetail null,s+"processing sensors ${sl}",iN2 List items= multiple ? (List)sl : [sl] for(sensor in items){ String sid=sD+gtSensorId(sensor) if(sid==sD){ error "createDataSources null sid ${sensor}",null,iN2 continue } attrn=multiple ? "attributes_${sid}".toString() : "attribute_" def al= gtSetting(attrn) if(al){ List attrs=multiple ? (List)al : [(String)al] for(String attr in attrs){ Map ent=makeSensorDataEntry(sensor,sid,attr) if(ent) dataSources << ent } }else{ // we don't have complete settings for this sensor... } } } if(hq){ Integer i String sb for(i=iZ; i final_streams List container container=[] Listfstreams= gtFuelList() Integer sz sz=fstreams.size() if(sz){ final_streams=[] String deflt deflt=sBLK if(isEric())myDetail null,"gatherFuelSource fuelstreams $sz $fstreams",iN2 for(Map stream in fstreams){ // Map [i:, c: , n: ,w:1, t: getFormattedDate(new Date())] String name=encodeStreamN(stream) Listfdata= (List)parent.readFuelStream(stream) sz=fdata.size() if(sz){ //if(!deflt) deflt=name final_streams << [(name) : "Fuel Stream $name :: [${fdata[sz-i1][sVAL]}]"] } } final_streams=final_streams.unique(false) if(final_streams == []){ container << hubiForm_text("No data found in stream
Please select a different Fuel Stream") hubiForm_container(container, i1) wremoveSetting(fvarn) }else{ // container << hubiForm_sub_section('Select Fuel Stream') // hubiForm_container(container, i1) input( (sTYPE): sENUM, (sNM): fvarn,(sTIT): ftit, (sREQ): false, (sMULTP): multiple, options: final_streams, (sDEFV): deflt, (sSUBOC): true ) } }else{ // no fuel streams container << hubiForm_text("No fuel streams found
") hubiForm_container(container, i1) } def fl= gtSetting(fvarn) List ifstreams=multiple ? (List)fl : [(String)fl] for(String stream in ifstreams){ Map ent=makeFuelDataEntry(stream) //ent=[(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: attr] if(ent){ if(allowLastActivity) addAllowLastActivity(ent) // lastupdate else rmAllowLastInput(ent) } } if(isEric())myDetail null,"gatherFuelSource $fvarn $multiple" } /** * * @param varn - creates a setting using this variable name * @param attrStr - creates a setting using this variable name * @return */ def gatherSensorSource(String varn, String attrStr, String tit, String cap, String atit, Boolean multiple, Boolean allowLastActivity){ if(isEric())myDetail null,"gatherSensorSource $varn $multiple",i1 List final_attrs String attrn List container input (type: cap, (sNM): varn,(sTIT): tit, (sMULTP): multiple, (sSUBOC): true) if(settings[varn]){ List items= multiple ? (List)settings[varn] : [settings[varn]] for(sensor in items){ container=[] String sid=sD+gtSensorId(sensor) if(sid==sD){ error "gatherSensorSource null sid ${sensor}",null,iN2 continue } //List attributes_=sensor.getSupportedAttributes() List attributes_=sensor.getSupportedAttributes().collect{ it.getName() }.unique().sort() final_attrs=[] String deflt; deflt=sBLK for(String attribute in attributes_){ //String name=attribute.getName() def v= sensor.currentState(attribute,true) if(v!=null){ if(!deflt) deflt=attribute final_attrs << [(attribute) : "$attribute ::: [${v.getValue()}]"] } } //if(allowLastActivity) final_attrs << ["lastupdate": "last activity ::: [${sensor.getLastActivity()}]"] final_attrs=final_attrs.unique(false) attrn= attrStr + (multiple ? sid:sBLK) if(final_attrs == []){ container << hubiForm_text("No supported Numerical Attributes
Please select a different Sensor") hubiForm_container(container, i1) wremoveSetting(attrn) }else{ container << hubiForm_sub_section("${sensor.displayName}") hubiForm_container(container, i1) input( (sTYPE): sENUM, (sNM): attrn,(sTIT): atit, (sREQ): false, (sMULTP): multiple, options: final_attrs, (sDEFV): deflt, (sSUBOC): true ) } def al= gtSetting(attrn) if(al){ List attrs=multiple ? (List)al : [(String)al] for(String attr in attrs){ Map ent=makeSensorDataEntry(sensor,sid,attr) if(ent){ if(allowLastActivity) addAllowLastActivity(ent) // lastupdate else rmAllowLastInput(ent) }else rmAllowLastInput(sid,attr) } } } } if(isEric())myDetail null,"gatherSensorSource $varn $multiple" } def gatherQuantSource(Boolean multiple,Boolean allowLastActivity){ if(isEric())myDetail null,"gatherQuantSource $multiple",i1 List container container=[] Integer i String s,saveS saveS=sNL Boolean fndf; fndf=false for(i=iZ; iDid not find free slot quant list
More than 10 quants for chart") hubiForm_container(container, i1) } if(isEric())myDetail null,"gatherQuantSource $multiple" } def displayQuant(String sid, Boolean allowLastActivity, Boolean create=false){ List container container=[] String attribute= sid+'attr' Boolean attrOk attrOk=false List opts opts= ['None',sCFUELSTRM,sCSENSOR] if(!create){ String typ=gtSetStr(sid+'_type') container << hubiForm_sub_section("Quantized source $sid") opts= ['None',typ] // allow to turn off container << hubiForm_enum ((sTIT): "Source type", (sNM): sid+'_type', list: opts, (sDEFLT): typ, (sSUBONCHG): true) }else{ container << hubiForm_sub_section("Add quantized source $sid") container << hubiForm_enum ((sTIT): "Source type", (sNM): sid+'_type', list: opts, (sDEFLT): 'None', (sSUBONCHG): true) } hubiForm_container(container, i1) String typ=gtSetStr(sid+'_type') if(typ==sCFUELSTRM){ gatherFuelSource(sid,'Source Fuel Stream',false,allowLastActivity) attrOk=true }else if(typ==sCSENSOR){ gatherSensorSource(sid,attribute,'Source Sensor','capability.*','Attribute',false,allowLastActivity) if(gtSetting(attribute)) attrOk=true }else{ // remove settings junk wremoveSetting(sid) quantRmInput(sid, attribute) rmAllowLastInput(sid,attribute) } if(gtSetting(sid) && attrOk){ //if(allowLastActivity) addAllowLastActivity(sid, attribute) // lastupdate quantInput(sid,attribute) } } def quantInput(String sid, String attribute){ if(isEric())myDetail null,"quantInput $sid $attribute",iN2 String s="${sid}_${attribute}".toString() List> quantizationEnum=[ ["0": "None"], ["5" : "5 Minutes"], ["10" : "10 Minutes"], ["20" : "20 Minutes"], ["30" : "30 Minutes"], ["60" : "1 Hour"], ["120" : "2 Hours"], ["180" : "3 Hours"], ["240" : "4 Hours"], ["360" : "6 Hours"], ["480" : "8 Hours"], ["1440" : "24 Hours"], ["10080": "7 Days"]] List> quantizationFunctionEnum=[ [(sNONE): "No Quantization"], ["sum": "Sum Values"], ["average" : "Average Values"], ["count" : "Count Events"], ["min" : "Minimum Value"], ["max" : "Maximum Value"]] //paragraph('Return Quantize data when read? (None means no quantization)') Boolean remove; remove=false String sq=s+'_quantization' if(isEric())myDetail null,"quantInput $sid $attribute ${sq} ${gtSetting(sq)}",iN2 input( (sTYPE): sENUM, (sNM): sq,(sTIT): "Data Quantization Timeframe (None means disabled)", (sREQ): false, (sMULTP): false, options: quantizationEnum, (sSUBOC): true, (sDEFV): s0) String sqv= gtSetting(sq) if(sqv && !(sqv in [s0]) ){ String sf= s+'_quantization_function' input( (sTYPE): sENUM, (sNM): sf,(sTIT): "Quantization Function", (sREQ): false, (sMULTP): false, options: quantizationFunctionEnum, (sSUBOC): true, (sDEFV): "average") String sfv= gtSetting(sf) if(sfv && sfv != sNONE){ input( (sTYPE): sBOOL, (sNM): s+"_boundary",(sTIT): "Quantize Data to Hour/Day Boundary (true changes reading time)?", (sREQ): false, (sMULTP): false, (sSUBOC): true, (sDEFV): false) input( (sTYPE): sENUM, (sNM): s+"_quantization_decimals",(sTIT): "Quantization Decimals to Maintain", (sREQ): false, (sMULTP): false, options: decimalsEnum, (sSUBOC): true, (sDEFV): s1) }else{ remove=true } }else{ remove=true } if(remove){ if(isEric())myDetail null,"quantInput removing",iN2 quantRmInput(sid, attribute) } } void quantRmInput(String sid, String attribute){ String s=sid+'_'+attribute wremoveSetting(s+'_quantization_function') wremoveSetting(s+'_boundary') wremoveSetting(s+'_quantization_decimals') wremoveSetting(s+'_quantization') } /** gather data source inputs for a graph * * @param multiple - allow multiple sources * @param ordered - do ordering * @param cap - capability for selection sensor devices * @return screens, and updates settings */ def gatherDataSources(Boolean multiple=true, Boolean ordered=false, Boolean allowLastActivity=false, String cap="capability.*"){ if(isEric())myDetail null,"gatherDataSources $multiple",i1 String fvarn=multiple ? 'fstreams' : 'fstream_' String ftit=multiple ? 'Choose fuel streams' : sCFUELSTRM String varn=multiple ? 'sensors' : 'sensor_' String tit=multiple ? 'Choose sensors' : sCSENSOR String atit=multiple ? 'Attributes to graph' : 'Attribute for Gauge' List a=createDataSources(multiple) hubiForm_section("Data Source Selection", i1, sBLK, sBLK){ Boolean hq= hasQuants() def sl,fl sl= gtSetting(varn) // fuel streams if(!multiple && (sl || hq) ) clearFvarn(fvarn,multiple) if(multiple || (!multiple && !sl && !hq)){ gatherFuelSource(fvarn,ftit,multiple,allowLastActivity) } fl= gtSetting(fvarn) // sensors if(!multiple && (fl || hq) ) clearVarn(multiple) if(multiple || (!multiple && !fl && !hq)){ String attrn=multiple ? "attributes_": "attribute_" gatherSensorSource(varn, attrn, tit, cap, atit, multiple, allowLastActivity) } sl= gtSetting(varn) fl= gtSetting(fvarn) // calculated, virtual, quant if(!multiple && (fl || sl) ) clearQuants() if(multiple || (!multiple && !fl && !sl)){ gatherQuantSource(multiple,allowLastActivity) } } /* wremoveSetting('f1_1 - test||temp_quantization') wremoveSetting('q0_q0_attr_quantization_function') */ state.remove('lastOrder') List dataSources= createDataSources(multiple) Integer sz=dataSources.size() if(ordered && multiple && sz>i1){ if(isEric())debug "check order",null List all=(1..sz).collect{ Integer it -> sBLK + it.toString() } hubiTools_validate_order(all) } if(isEric())myDetail null,"gatherDataSources $multiple" } /* shared pages */ //def mainGauge(){ //def mainBar(){ //def mainTimeline(){ //def mainHeatmap(){ //def mainLinegraph(){ //def mainRangebar(){ //def mainTimegraph(){ def mainShare1(String instruct, String okSet,Boolean multiple=true,Boolean usesHistory=true){ if(isEric())myDetail null,"mainShare1: $okSet $multiple",iN2 List a=createDataSources(multiple) if(!state.dataSources) wremoveSetting(okSet) if(instruct) state.devInstruct=instruct state.graphUsesHistory=usesHistory dynamicPage((sNM): "mainPage"){ checkDup() if(!state.endpoint){ hubiForm_section("Please set up OAuth API", i1, "report", sBLK){ href( (sNM): "enableAPIPageLink",(sTIT): "Enable API", description: sBLK, page: "enableAPIPage") } }else{ hubiForm_section(tDesc()+" Graph Options", i1, "tune", sBLK){ List container container=[] container << hubiForm_page_button("Select Data Source(s)", "deviceSelectionPage", s100PCT, "vibration") container << hubiForm_page_button("Configure Graph", "graphSetupPage", s100PCT, sPOLL) hubiForm_container(container, i1) } if(settings[okSet]!=null){ local_graph_url() preview_tile() } put_settings() } } } //def deviceGauge(){ //def deviceBar(){ //def deviceTimeline(){ //def deviceTimegraph(){ //def deviceHeatmap(){ //def deviceLinegraph(){ //def deviceRangebar(){ def deviceShare1(Boolean multiple=true, Boolean ordered=false,Boolean allowLastActivity=false){ if(isEric())myDetail null,"deviceShare1: $ordered",iN2 dynamicPage((sNM): "deviceSelectionPage", nextPage:"attributeConfigurationPage"){ String di= (String)gtSt('devInstruct') Boolean gh= gtStB('graphUsesHistory') if(di || gh){ hubiForm_section("Directions", i1, "directions", sBLK){ List container container=[] if(di) container << hubiForm_text(di) if(gh) container << hubiForm_text("Note LTS will be used if enabled for a sensor:attribute when you select a sensor") hubiForm_container(container, i1) } } gatherDataSources(multiple, ordered, allowLastActivity) } } //def attributeTimegraph(){ //def attributeHeatmap(){ //def attributeLinegraph(){ def attributeShare1(Boolean ordered=false, String var_color=sBACKGRND){ if(isEric())myDetail null,"attributeShare1: $ordered $var_color",iN2 List dataSources= createDataSources(true) dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ if(ordered){ hubiForm_section("Graph Order", i1, "directions", sBLK){ hubiForm_list_reorder('graph_order', var_color) } } List container for(Map ent in dataSources){ String sid=sMs(ent,sID) String rid=ent[sRID].toString() String attribute=sMs(ent,sA) String dn= sMs(ent,sDISPNM) String typ=sMs(ent,sT).capitalize() String hint= typ=='Fuel' ? " (Canister ${ent.c} Name ${ent.n})" : sBLK String sa="${sid}_${attribute}".toString() container=[] hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "directions", sid+attribute){ if(typ==sCSENSOR){ if(isLtsAvailable(rid, attribute)){ container << hubiForm_sub_section("Long Term Storage in use") }else{ String tvar="var_"+sa+"_lts" app.updateSetting(tvar, false) settings[tvar]= false } } container << hubiForm_sub_section("Override ${typ} Name on Graph") container << hubiForm_text_input("Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE", "graph_name_override_"+sa, "%deviceName%: %attributeName%", false) hubiForm_container(container, i1) } } } } def local_graph_url(){ List container container=[] hubiForm_section("Local Graph URL", i1, "link", sBLK){ if(useRemote()){ container << hubiForm_switch([(sTIT): "Use cloud endpoints?", (sNM): 'use_cloudEP', (sDEFLT): false, (sSUBONCHG): true]) }else{ app.updateSetting('use_cloudEP', [(sTYPE):sBOOL, (sVAL):sFALSE]) } container << hubiForm_switch([(sTIT): "Use https for any local hub access?", (sNM): 'use_https', (sDEFLT): true, (sSUBONCHG): true]) String s= makeCallBackURL('graph/') container << hubiForm_text(s, s) hubiForm_container(container, i1) } } def preview_tile(){ List container String typ=gtSetStr(sGRAPHT) hubiForm_section("Preview of graph type: ${typ}", i10, "show_chart", sBLK){ container=[] container << hubiForm_graph_preview() hubiForm_container(container, i1) } install_tile() } def install_tile(){ List container String s=gtSetStr('app_name') ?: tDesc() hubiForm_section(s+" Tile Installation", i2, "apps", sBLK){ container=[] container << hubiForm_switch([(sTIT): "Install ${s} Tile Device?", (sNM): 'install_device', (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('install_device')){ container << hubiForm_text_input("Name for ${s} Tile Device", 'device_name', "${s} Tile", true) } hubiForm_container(container, i1) } } def put_settings(Boolean needOauth=true){ if(!needOauth || state.endpoint){ String typ=tDesc() List container container=[] hubiForm_section("webCoRE ${typ} Application Settings", i1, "settings", sBLK){ if(gtSetStr(sGRAPHT)!=sLONGTS){ container << hubiForm_sub_section("Application Name") container << hubiForm_text_input("Rename the Application?", 'app_name', "webCoRE ${typ}", true) }else app.updateSetting('app_name', "webCoRE ${typ}") // cannot rename LTS container << hubiForm_sub_section("Debugging") container << hubiForm_enum((sTIT): "Logging Level", (sNM): sLOGNG, list: [s0,s1,s2,"3"], (sDEFLT): state[sLOGNG] ? state[sLOGNG].toString():s0 ) if(needOauth && state.endpoint){ container << hubiForm_sub_section("Disable Oauth Authorization") container << hubiForm_page_button("Disable API", "disableAPIPage", s100PCT, "cancel") } hubiForm_container(container, i1) } } } /** * get data source entry for sid/attribute pair */ Map findDataSourceEntry(String sid, String attribute){ Map ent ent= null List dataSources=gtDataSources() if(dataSources){ String sa=sA String sd=sID ent= dataSources.find{ Map it -> sMs(it,sd)==sid && sMs(it,sa)==attribute } } if(isEric())myDetail null,"findDataSourceEntry: $sid $attribute $ent",iN2 return ent } /** * get Last data item from a data source entry as internal map * @return [date: (Date)date, (sVAL): v, t: (Long)t] */ Map gtLastData(Map ent, Boolean multiple=true){ Map lst; lst=null List fdata= gtDataSourceData(ent,multiple) Integer sz= fdata.size() if(sz) lst= fdata[sz-i1] return lst } /** * get last value as float from a data Source entry as a special map * @return - [current: (float)val, date: (Date)d] */ Map gtFloatMap(Map ent, Boolean multiple=true){ Map res res=[:] Map lst= gtLastData(ent, multiple) // [date: date, (sVAL): v, t: t] if(lst){ Float val= "${lst[sVAL]}".toFloat() Date dt= dtMdt(lst) res= [(sCUR): val, (sDT): dt] } if(isEric())myDetail null,"gtFloatMap $ent $multiple $res ",iN2 return res } /** * return a map of last item in a data source entry * @return - [(sVAL): (String)x, date: (Date)d] */ Map gtLatestMap(Map ent, Boolean multiple=true){ Map res res=[:] Map lst= gtLastData(ent, multiple) if(lst){ // [date: date, (sVAL): v, t: t] String val= "${lst[sVAL]}" Date dt= dtMdt(lst) res= [(sVAL): val, (sDT): dt] } if(isEric())myDetail null,"gtLatestMap $ent $multiple $res ",iN2 return res } /** * get latest String value of data source entry */ String getLatestVal(Map ent, Boolean multiple=true){ String val val='0.0' Map lst= gtLastData(ent, multiple) if(lst){ val= "${lst[sVAL]}" } if(isEric())myDetail null,"getLatestVal $ent $multiple $val ",iN2 return val } /** * get latest double value of data source entry with override from settings * @return doubleVal */ private Double getValue(String id, String attr, val){ Double ret String s def v= settings["attribute_${id}_${attr}_${val}"] if(v!=null){ s= "${v}".toString() }else{ s= "${val}".toString() } ret= extractNumber(s) return ret } static Double extractNumber(String input){ Listval=input.findAll( /-?\d+\.\d*|-?\d*\.\d+|-?\d+/ )*.toDouble() val[iZ] } /** * get data source data from entry later than time * Shared - used by graphs to returns data later than time * @return internal format [[date: (Date)date, (sVAL): v, t: (Long)t]...] */ List CgetData(Map ent, Date time, Boolean multiple=true){ List return_data return_data= gtDataSourceData(ent,multiple) Long end=time.getTime() List data2 data2=return_data.findAll{ Map it -> lMt(it) > end } if(!data2) data2= return_data ? [return_data[-i1]] : data2 if(isEric())myDetail null,"CgetData: $ent $time ${data2.size()}",iN2 return data2 } /** * get data source data from entry * Shared - get all data for a datasource entry; if quant enabled, data will be quanted * @param sensorV - settings variable name for sensor type (override) * @return internal format [[date: (Date)date, (sVAL): v, t: (Long)t]...] */ List gtDataSourceData(Map ent, Boolean multiple=true, String sensorV=sNL){ if(isEric())myDetail null,"gtDataSourceData $ent $multiple",i1 String attribute=sMs(ent,sA) String typ= sMs(ent,sT).capitalize() Listres res=[] if(typ=='Fuel'){ Map stream= findStream(sMs(ent,'sn')) if(stream) res= (List)parent.readFuelStream(stream) else warn 'gtDataSourceData: stream not found',null } if(typ==sCSENSOR){ String varn= sensorV ?: (multiple ? 'sensors' : 'sensor_') // have to get devices from settings def a=gtSetting(varn) List devs= multiple ? (List)a : [a] String rid=ent[sRID].toString() if(isEric())myDetail null,"varn: $varn devs ${devs} a: ${a} rid: ${myObj(ent[sRID])}",iN2 if(devs.size()){ def sensor=devs.find{ // myDetail null,"${it.id} ${myObj(it.id)} ${myObj(rid)}",iN2 it.id == rid } // myDetail null,"sz is ${devs.size()} $attribute $sensor",iN2 if(sensor && attribute){ // myDetail null,"have sensor and attribute",iN2 res= getAllData(sensor,attribute,1461,true,false) } }else warn 'gtDataSourceData: no devices found',null } if(typ=='Quant'){ res=gtDataSourceData((Map)ent.ent,false, sMs(ent,sID)) // if to return data quantized Map params= quantParams(ent.id,attribute) if(res && params) res= quantizeData(res, params.qm , params.qf, params.qd, params.qb, false) } if(isEric())myDetail null,"gtDataSourceData ${ent} ${multiple} ${res.size()}" return res } def doFile(String file,String typ='text/javascript'){ String filename_= isSystemType() ? 'webcore/': '' + file String ts1= " for ($filename_}" Boolean ok= lowReadFile(filename_,ts1) if(ok){ String sc= readTmpFLD[filename_] return wrender(contentType: typ, data: sc, status:200) } return wrender(contentType: "application/json", data: """{"status":"failure"}""", status: 400) } def getFile1(){ doFile('a930f16d-d5f4-4f37-b874-6b0dcfd47ace-HubiGraph.js') } def getFile2(){ doFile('f06ea400-fe7a-49ef-8c50-6418f0a78dc6-WeatherTile2.css','text/css') } def getFile3(){ doFile('ba8d5ae0-1fbd-430a-bae0-bb5c0bd17ebd-WeatherTile2.js') } def getFile4(){ doFile('a7af9806-4b0e-4032-a78e-a41e27e4d685-WeatherTile.js') } String locationFile(String file, Boolean isSystemType){ if(!useRemote() || !gtSetB('use_cloudEP')){ return "http://${location.hub.localIP}/local/${isSystemType ? 'webcore/' : ''}${file}" }else{ switch(file){ case 'a930f16d-d5f4-4f37-b874-6b0dcfd47ace-HubiGraph.js': return makeCallBackURL('getFile1/') case 'f06ea400-fe7a-49ef-8c50-6418f0a78dc6-WeatherTile2.css': return makeCallBackURL('getFile2/') case 'ba8d5ae0-1fbd-430a-bae0-bb5c0bd17ebd-WeatherTile2.js': return makeCallBackURL('getFile3/') case 'a7af9806-4b0e-4032-a78e-a41e27e4d685-WeatherTile.js': return makeCallBackURL('getFile4/') default: error "Error: unknown file $file",null,iN2 } } } static String scriptIncludes(){ String html= """ """ return html } String scriptIncludes1(Boolean isSystemType){ String html=""" ${scriptIncludes()} """ return html } /* * TODO: Gauge methods */ def mainGauge(){ mainShare1("Choose Numeric Attributes only",'gauge_title',false,false) } def deviceGauge(){ deviceShare1(false) } def attributeGauge(){ List dataSources= createDataSources(false) dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ List container def val String dn dn='unknown' String typ typ=dn if(dataSources){ for(Map ent in dataSources){ Listfdata=gtDataSourceData(ent,false) Integer sz sz=fdata.size() if(sz){ val="${fdata[sz-i1][sVAL]}" } typ=sMs(ent,sT).capitalize() dn= sMs(ent,sDISPNM) String hint= typ=='Fuel' ? " (Canister ${ent.c} Name ${ent.n})" : sBLK String sid=sMs(ent,sID) String rid=ent[sRID].toString() String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "directions", sid+attribute){ if(typ==sCSENSOR){ if(isLtsAvailable(rid, attribute)){ container=[] container << hubiForm_sub_section("Long Term Storage in use") hubiForm_container(container, i1) }else{ String tvar="var_"+sa+"_lts" app.updateSetting(tvar, false) settings[tvar]= false } } } } if(val != null){ hubiForm_section("Min Max Value", i1, sBLK, sBLK){ container=[] container<< hubiForm_text("Current ${typ} Value=$val") container << hubiForm_text_input("Minimum Value for Gauge", "minValue_", s0, false) container << hubiForm_text_input("Maximum Value for Gauge", "maxValue_", s100, false) hubiForm_container(container, i1) } }else{ hubiForm_section("No data", i1, sBLK, sBLK){ container=[] container<< hubiForm_text("No recent valid ${typ} data for ${dn}
Please select a different data Source") hubiForm_container(container, i1) } } } } } def graphGauge(){ Integer num_ String nh= 'num_highlights' dynamicPage((sNM): "graphSetupPage"){ List container hubiForm_section("General Options", i1, sBLK, sBLK){ container=[] if(gtStB('hasFuel')) inputGraphUpdateRate() else app.updateSetting(sGRPHUPDRATE, s0) container << hubiForm_text_input("Gauge Title", "gauge_title", "Gauge Title", false) container << hubiForm_text_input("Gauge Units", "gauge_units", "Units", false) container << hubiForm_text_input("Gauge Number Formatting
Example", "gauge_number_format", "##.#", false) container << hubiForm_slider ((sTIT): "Select Number of Highlight Areas on Gauge", (sNM): nh, (sDEFLT): i3, (sMIN): iZ, (sMAX): i3, (sUNITS): " highlights", (sSUBONCHG): true) hubiForm_container(container, i1) } if(gtSetting(nh) == null){ settings[nh]=i3 app.updateSetting(nh, i3) num_=i3 }else{ num_=gtSetI(nh) } if(num_ > iZ){ hubiForm_section("HighLight Regions", i1, sBLK, sBLK){ container=[] String color_ color_=sNL Integer i for(i=iZ; i dataSources=gtDataSources() if(dataSources){ Map ent= dataSources[iZ] val= getLatestVal(ent,false) } Double d= Math.round(extractNumber(val) * d10) / d10 return JsonOutput.toJson( [(sVAL): d] ) } Map getOptions_gauge(){ List tic_labels tic_labels=[] if(gtSetB('default_major_ticks')){ Integer tic for(tic=iZ; tic ${scriptIncludes()}
""" return html } //oauth endpoints Map getSubscriptions_gauge(){ Map subscriptions subscriptions=[:] List dataSources=gtDataSources() if(dataSources){ Map ent= dataSources[iZ] String typ=sMs(ent,sT).capitalize() if(typ==sCSENSOR){ subscriptions=[ (sID): ent[sRID], (sATTR): sMs(ent,sA) ] }else{ subscriptions=[ (sID): sPOLL, (sATTR): sNONE ] } } return subscriptions } // Shared input method def inputGraphUpdateRate(String d=s0){ String defl defl=d List opt opt=rateEnum if(gtStB('hasFuel')){ stToPoll() defl="600000" opt=rateEnumF } input( (sTYPE): sENUM, (sNM): sGRPHUPDRATE,(sTIT): "Select graph update rate
(For panel viewing; the refresh rate of the graph)", (sMULTP): false, (sREQ): false, options: opt, (sDEFV): defl) } /** refresh rate for graphs with fuel streams */ @Field static List> rateEnumF=[ ["-1":"Never"], // ["0":"Real Time"], // ["10":"10 Milliseconds"], ["1000":"1 Second"], ["5000":"5 Seconds"], ["60000":"1 Minute"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1800000":"Half Hour"], ["3600000":"1 Hour"] ] /** refresh rate for graphs with only sensors */ @Field static List> rateEnum=[ ["-1":"Never"], ["0":"Real Time"], ["10":"10 Milliseconds"], ["1000":"1 Second"], ["5000":"5 Seconds"], ["60000":"1 Minute"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1800000":"Half Hour"], ["3600000":"1 Hour"] ] /** * shared by bar, timeline, heatmap, rangebar to show current sensor and attribute values if curStates==true */ Map gtSensorFmt(Boolean curStates=false,Boolean multiple=true){ if(isEric())myDetail null,"gtSensorFmt curStates: $curStates, (${sMULTP}): $multiple",i1 Map sensors_fmt sensors_fmt=[:] //TODO List dataSources=gtDataSources() Map> res = [:] if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String s=sid Map tres res[s]= res[s] ?: [] if(curStates){ tres= gtLatestMap(ent,multiple) res[s] << [(sNM): attribute, (sVAL): tres] } } for(Map ent in dataSources){ String sid=sMs(ent,sID) String s=sid String dn= sMs(ent,sDISPNM) sensors_fmt[sid]=[ (sID): sid, (sDISPNM): dn] + (curStates ? ["currentStates": res[s] ] : [:]) } } if(isEric())myDetail null,"gtSensorFmt $curStates $sensors_fmt" return sensors_fmt } static String sLblTyp(String typ){ if(typ=='fuel') return sBLK else return 'Sensor ' } def gatherGraphSize(){ List container hubiForm_section("Graph Size", i1, sBLK, sBLK){ container=[] input( (sTYPE): sBOOL, (sNM): sGRPHSTATICSZ,(sTIT): "Set size of Graph?
(False=Fill Window)", (sDEFV): false, (sSUBOC): true) if(gtSetB(sGRPHSTATICSZ)){ container << hubiForm_slider ((sTIT): "Horizontal dimension of the graph", (sNM): "graph_h_size", (sDEFLT): i800, (sMIN): i100, (sMAX): i3000, (sUNITS): " pixels", (sSUBONCHG): false) container << hubiForm_slider ((sTIT): "Vertical dimension of the graph", (sNM): "graph_v_size", (sDEFLT): i600, (sMIN): i100, (sMAX): i3000, (sUNITS): " pixels", (sSUBONCHG): false) } hubiForm_container(container, i1) } } /* * TODO: Bar methods */ def mainBar(){ mainShare1('Choose Numeric Attributes only',sGRPHUPDRATE,true,false) } def deviceBar(){ deviceShare1() } @Field static List decimalsEnum= [ [0:"None (123)"], [1: "One (123.1)"], [2: "Two (123.12)"], [3: "Three (123.123)"], [4: "Four (123.1234)"] ] def attributeBar(){ List dataSources= createDataSources(true) dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ List container hubiForm_section("Graph Order", i1, "directions", sBLK){ hubiForm_list_reorder('graph_order', sBACKGRND) } //Integer count=0 // TODO if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String rid=ent[sRID].toString() String dn=sMs(ent,sDISPNM) String typ=sMs(ent,sT).capitalize() String hint= typ=='Fuel' ? " (Canister ${ent.c} Name ${ent.n})" : sBLK String sa="${sid}_${attribute}".toString() container=[] hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "directions", sid+attribute){ if(typ==sCSENSOR){ if(isLtsAvailable(rid, attribute)){ container << hubiForm_sub_section("Long Term Storage in use") hubiForm_container(container, i1) container=[] }else{ String tvar="var_"+sa+"_lts" app.updateSetting(tvar, false) settings[tvar]= false } } input( (sTYPE): sENUM, (sNM): "attribute_"+sa+"_decimals", (sTIT): "Number of Decimal Places to Display", (sMULTP): false, (sREQ): false, options: decimalsEnum, (sDEFV): 1) container << hubiForm_text_input("Scale Factor for Values
Example: To scale down by 10X, input 0.1
Leave as 1 for unchanged
", "attribute_"+sa+"_scale", s1, false) container << hubiForm_text_input("Override ${typ} Name on Graph
Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE
", "graph_name_override_"+sa, "%deviceName%: %attributeName%", false) container << hubiForm_color ("Bar Background", "attribute_"+sa+"_background", "#3e4475", false, true) container << hubiForm_color ("Bar Border", "attribute_"+sa+"_current_border", sWHT, false) container << hubiForm_slider ((sTIT): "Bar Opacity", (sNM): "attribute_"+sa+"_opacity", (sDEFLT): i100, (sMIN): i1, (sMAX): i100, (sUNITS): "%") container << hubiForm_line_size ((sTIT): "Bar Border", (sNM): "attribute_"+sa+"_current_border", (sDEFLT): i2, (sMIN): i1, (sMAX): i10) container << hubiForm_switch ([(sTIT): "Show Current Value on Bar", (sNM): "attribute_"+sa+"_show_value", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB("attribute_"+sa+"_show_value")){ container<< hubiForm_text_input("Units", "attribute_"+sa+"_annotation_units", sBLK, false) } hubiForm_container(container, i1) } } } } } def graphBar(){ dynamicPage((sNM): "graphSetupPage"){ List container hubiForm_section("General Options", i1, sBLK, sBLK){ container=[] input( (sTYPE): sENUM, (sNM): 'graph_type',(sTIT): "Select graph type", (sMULTP): false, (sREQ): false, options: [[(s1): "Bar Chart"],[(s2): "Column Chart"]], (sDEFV): s1) inputGraphUpdateRate() container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) container << hubiForm_slider ((sTIT): "Graph Bar Width (1%-100%)", (sNM): "graph_bar_percent", (sDEFLT): i90, (sMIN): i1, (sMAX): i100, (sUNITS): "%") container << hubiForm_text_input("Graph Max", "graph_max", sBLK, false) container << hubiForm_text_input("Graph Min", "graph_min", sBLK, false) hubiForm_container(container, i1) } hubiForm_section("Axes", i1, sBLK, sBLK){ container=[] container << hubiForm_color ("Axis", "haxis", sBLACK, false) container << hubiForm_font_size ((sTIT): "Axis", (sNM): "haxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_slider ((sTIT): "Number of Pixels for Axis", (sNM): "graph_h_buffer", (sDEFLT): i40, (sMIN): i10, (sMAX): i500, (sUNITS): " pixels") hubiForm_container(container, i1) } hubiForm_section("Device Names", i1, sBLK, sBLK){ container=[] container << hubiForm_font_size ((sTIT): "Device Name", (sNM): "graph_axis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Device Name","graph_axis", sBLACK, false) container << hubiForm_slider ((sTIT): "Number of Pixels for Device Name Area", (sNM): "graph_v_buffer", (sDEFLT): i100, (sMIN): i10, (sMAX): i500, (sUNITS): " pixels") hubiForm_container(container, i1) } gatherGraphSize() hubiForm_section("Annotations", i1, sBLK, sBLK){ container=[] container << hubiForm_font_size ((sTIT): "Annotation", (sNM): "annotation", (sDEFLT): i16, (sMIN): i2, (sMAX): i40) container << hubiForm_switch ([(sTIT): "Show Annotation Outside (true) or Inside (false) of Bars", (sNM): "annotation_inside", (sDEFLT):false]) container << hubiForm_color ("Annotation", "annotation", sWHT, false) container << hubiForm_color ("Annotation Aura", "annotation_aura", sBLACK, false) container << hubiForm_switch ((sTIT): "Bold Annotation", (sNM): "annotation_bold", (sDEFLT): false) container << hubiForm_switch ((sTIT): "Italic Annotation", (sNM): "annotation_italic", (sDEFLT): false) hubiForm_container(container, i1) } } } String getData_bar(){ Map resp=[:] // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) resp[sid]= resp[sid] ?: [:] Map a= gtFloatMap(ent) if(a) resp[sid][attribute]=a else resp[sid][attribute]=[(sCUR): 1.0, (sDT): new Date()] } } return JsonOutput.toJson(resp) } Map getOptions_bar(){ List colors=[] // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() String attrib_string="attribute_"+sa+"_color" String transparent_attrib_string= attrib_string+"_transparent" colors << (gtSetB(transparent_attrib_string) ? sTRANSPRNT : settings[attrib_string]) } } String axis1,axis2 Boolean gt1= (gtSetStr('graph_type')==s1) axis1= gt1 ? "hAxis" : "vAxis" axis2= gt1 ? "vAxis" : "hAxis" Map options=[ "graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), (sGRAPHT): Integer.parseInt(gtSetStr('graph_type')), "graphOptions": [ "bar" : [ "groupWidth" : "${graph_bar_percent}%", ], "width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, "height": gtSetB(sGRPHSTATICSZ) ? graph_v_size: "90%", "timeline": [ "rowLabelStyle": ["fontSize": graph_axis_font, "color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color], "barLabelStyle": ["fontSize": graph_axis_font] ], "backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), "isStacked": false, "chartArea": [ (sLEFT): gt1 ? graph_v_buffer : graph_h_buffer, (sRIGHT): i10, "top": i10, "bottom": gt1 ? graph_h_buffer : graph_v_buffer ], "legend" : [ "position" : sNONE ], (axis1): [ "viewWindow" : ["max" : graph_max, "min" : graph_min], "minValue" : graph_min, "maxValue" : graph_max, "textStyle" : ["color": gtSetB('haxis_color_transparent') ? sTRANSPRNT : haxis_color, "fontSize": haxis_font] ], (axis2): [ "textStyle" : ["color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color, "fontSize": graph_axis_font] ], "annotations" : [ "alwaysOutside": annotation_inside, "textStyle": [ "fontSize": annotation_font, "bold": annotation_bold, "italic": annotation_italic, "color": gtSetB('annotation_color_transparent') ? sTRANSPRNT : annotation_color, "auraColor":gtSetB('annotation_aura_color_transparent') ? sTRANSPRNT : annotation_aura_color, ], "stem": [ "color": sTRANSPRNT ], "highContrast": sFALSE ], ], "graphLow": graph_min, "graphHigh": graph_max, ] return options } String getGraph_bar(){ String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" String html=""" ${scriptIncludes()}
""" return html } //oauth endpoints Map getSubscriptions_bar(){ List _ids=[] Map _attributes=[:] Map labels=[:] Map colors=[:] Boolean isPoll isPoll=gtStB('hasFuel') // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() _ids << sid _attributes[sid]=[] labels[sid]=[:] colors[sid]=[:] _attributes[sid] << attribute labels[sid][attribute]=settings["graph_name_override_"+sa] String au = gtSetB("attribute_"+sa+"_show_value") ? gtSetStr("attribute_"+sa+"_annotation_units") : sBLK colors[sid][attribute]=[ "backgroundColor": settings["attribute_"+sa+"_background_color"], "currentValueBorderColor": settings["attribute_"+sa+"_current_border_color"], "currentValueBorderLineSize": settings["attribute_"+sa+"_current_border_line_size"], "showAnnotation": settings["attribute_"+sa+"_show_value"], // not used by js "annotation_units": au, // These 4 do not exist as inputs, nor are used in js "currentValueColor": settings["attribute_"+sa+"_current_color"], "annotation_font": settings["attribute_"+sa+"_annotation_font"], "annotation_font_size": settings["attribute_"+sa+"_annotation_font_size"], "annotation_color": settings["attribute_"+sa+"_annotation_color"], "opacity": settings["attribute_"+sa+"_opacity"]/100.0, "scale": settings["attribute_"+sa+"_scale"], "decimals": settings["attribute_"+sa+"_decimals"] ] } } Map sensors_fmt=gtSensorFmt() List order=gtSetStr('graph_order') ? parseJson(gtSetStr('graph_order')) : [] Map subscriptions=[ (sID): isPoll ? sPOLL : sSENSOR, 'sensors': sensors_fmt, "ids": _ids, 'attributes': _attributes, "labels": labels, "colors": colors, "order": order, "graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), ] return subscriptions } @Field static Map> supportedTypes static void fill_supportedTypes(){ supportedTypes = [ "alarm" : [(sSTART): sON, (sEND): sOFF], "contact" : [(sSTART): "open", (sEND): "closed"], (sSWITCH) : [(sSTART): sON, (sEND): sOFF], "motion" : [(sSTART): "active", (sEND): "inactive"], "mute" : [(sSTART): "muted", (sEND): "unmuted"], "presence" : [(sSTART): "present", (sEND): "not present"], "holdableButton": [(sSTART): sTRUE, (sEND): sFALSE], "carbonMonoxide": [(sSTART): "detected", (sEND): "clear"], "playing" : [(sSTART): "playing", (sEND): "stopped"], "door" : [(sSTART): "open", (sEND): "closed"], "speed" : [(sSTART): sON, (sEND): sOFF], "lock" : [(sSTART): "unlocked", (sEND): "locked"], "shock" : [(sSTART): "detected", (sEND): "clear"], "sleepSensor" : [(sSTART): "sleeping", (sEND): "not sleeping"], "smoke" : [(sSTART): "detected", (sEND): "clear"], "sound" : [(sSTART): "detected", (sEND): "not detected"], "tamper" : [(sSTART): "detected", (sEND): "clear"], "valve" : [(sSTART): "open", (sEND): "closed"], "camera" : [(sSTART): sON, (sEND): sOFF], "water" : [(sSTART): "wet", (sEND): "dry"], "windowShade" : [(sSTART): "open", (sEND): "closed"], "acceleration" : [(sSTART): "inactive", (sEND): "active"] ] } @Field static List startTypes=['on', 'open', 'active', 'muted', 'present', 'true', 'detected', 'playing', 'unlocked', 'sleeping', 'wet'] @Field static List endTypes= ['off', 'closed', 'inactive', 'unmuted', 'not present', 'false', 'clear', 'stopped', 'locked', 'not sleeping', 'not detected', 'dry'] Map gtStartEndTypes(Map ent, String attribute){ String defltS, defltE defltS=sBLK; defltE=sBLK if(!supportedTypes) fill_supportedTypes() if(supportedTypes.containsKey(attribute)){ defltS=supportedTypes[attribute][sSTART] defltE=supportedTypes[attribute][sEND] return [(sSTART): defltS, (sEND): defltE] }else{ //figure out from data if there are choices List fdata= gtDataSourceData(ent) Integer sz sz = fdata.size() if(sz>i1){ // [date: date, (sVAL): v, t: t] def val Integer i i=i2 while(i>iZ){ val= fdata[sz-i][sVAL] if(val && val instanceof String){ String s= val if(!defltS && s in startTypes) defltS= s else if(!defltE && s in endTypes) defltE = s if(defltE && defltS) return [(sSTART): defltS, (sEND): defltE] } i-=i1 if(i==iZ){ defltS=sBLK; defltE=sBLK } } } } return null } /** Timespans as MS */ @Field static List> timespanEnum=[ ["60000":"1 Minute"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], ["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"] ] /* * TODO: Timeline methods */ def mainTimeline(){ mainShare1( """Choose Numeric Attributes or common sensor attributes (like on/off, open/close, present/not present, detected/clear, active/inactive, wet/dry)""" ,sGRPHUPDRATE) } def deviceTimeline(){ deviceShare1() } def attributeTimeline(){ List dataSources= createDataSources(true) //state.count_=0 dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ List container hubiForm_section("Directions", i1, "directions", sBLK){ container=[] container << hubiForm_text("""Configure what counts as a 'start' or 'end' event for each attribute on the timeline. For example, Switches start when they are 'on' and end when they are 'off'.\n\nSome attributes will automatically populate. You can change them if you have a different configuration (chances are you won't). Additionally, for devices with numeric values, you can define a range of values that count as 'start' or 'end'. For example, to select all the times a temperature is above 70.5 degrees fahrenheit, you would set the start to '> 70.5', and the end to '< 70.5'. Supported comparators are: '<', '>', '<=', '>=', '==', '!='.\n\nBecause we are dealing with HTML, '<' is abbreviated to &lt; after you save. That is completely normal. It will still work.""" ) container << hubiForm_text("Note LTS will be used if enabled for a sensor:attribute when you select a sensor") hubiForm_container(container, i1) } hubiForm_section("Graph Order", i1, "directions", sBLK){ hubiForm_list_reorder('graph_order', "line") } // TODO Integer cnt cnt=iZ if(dataSources){ for(Map ent in dataSources){ //state.count_++ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String rid=ent[sRID].toString() String dn=sMs(ent,sDISPNM) String typ=sMs(ent,sT).capitalize() String hint= typ=='Fuel' ? " (Canister ${ent.c} Name ${ent.n})" : sBLK String sa="${sid}_${attribute}".toString() hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "directions", sid+attribute){ container=[] if(typ==sCSENSOR){ if(isLtsAvailable(rid, attribute)){ container << hubiForm_sub_section("Long Term Storage in use") }else{ String tvar="var_"+sa+"_lts" app.updateSetting(tvar, false) settings[tvar]= false } } container << hubiForm_text_input("Override ${typ} Name on Graph
Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE
", "graph_name_override_${sa}", "%deviceName%: %attributeName%", false) String defltS, defltE defltS=sBLK; defltE=sBLK Map a=gtStartEndTypes(ent,attribute) if(a){ defltS=sMs(a,sSTART) ?: sBLK defltE=sMs(a,sEND) ?: sBLK } container << hubiForm_color("Line", "attribute_"+sa+"_line", hubiTools_rotating_colors(cnt), false, false) container << hubiForm_text_input("Start event value", "attribute_"+sa+"_start", defltS, false) container << hubiForm_text_input("End event value", "attribute_"+sa+"_end", defltE, false) hubiForm_container(container, i1) } cnt += i1 } } } } def graphTimeline(){ List> lOpts= [["0":"Never"], ["10000":"10 Seconds"], ["30000":"30 seconds"], ["60000":"1 Minute"], ["120000":"2 Minutes"], ["180000":"3 Minutes"], ["240000":"4 Minutes"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1200000":"20 Minutes"], ["1800000":"30 Minutes"], ["3600000":"1 Hour"], ["6400000":"2 Hours"], ["9600000":"3 Hours"], ["13200000":"4 Hours"], ["16800000":"5 Hours"], ["20400000":"6 Hours"]] dynamicPage((sNM): "graphSetupPage"){ List container hubiForm_section("General Options", i1, "directions", sBLK){ inputGraphUpdateRate() input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "Select Time span to Graph", (sMULTP): false, (sREQ): false, options: timespanEnum, (sDEFV): "43200000") input( (sTYPE): sENUM, (sNM): "graph_combine_rate",(sTIT): "Combine events with events less than ? apart", (sMULTP): false, (sREQ): false, options: lOpts, (sDEFV): s0) container=[] container << hubiForm_color ("Background", "graph_background", sWHT, false) } gatherGraphSize() hubiForm_section("Device Name Display", i1, sBLK, sBLK){ container=[] container << hubiForm_color ("Device Text", "graph_axis", sWHT, false) container << hubiForm_font_size ((sTIT): "Device", (sNM): "graph_axis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) hubiForm_container(container, i1) } } } String getData_timeline(){ Map resp=[:] // Date now=new Date() Date then then=new Date() Long graph_time use (TimeCategory){ Double val=Double.parseDouble(gtSetStr('graph_timespan'))/1000.0 then -= (val.toInteger()).seconds graph_time=then.getTime() } // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) //String typ=sMs(ent,sT).capitalize() resp[sid]= resp[sid] ?: [:] List data=CgetData(ent, then) // log.warn "got sensor: $sensor attribute: $attribute data1: $data" Listdata1=data.collect{ Map it-> [(sDT): it.t, (sVAL): "${it[sVAL]}".toString()]} resp[sid][attribute]=data1.findAll{ Map it-> lMs(it,sDT) > graph_time} List temp=([]+data1) as List //temp=temp.sort{ (Long)it.date } resp[sid][attribute]=temp // log.warn "FINAL got sensor: $sensor attribute: $attribute data1: $temp" } } return JsonOutput.toJson(resp) } Map getOptions_timeline(){ if(isEric())myDetail null,"getChartOptions_timeline",i1 List colors=[] List order=hubiTools_get_order(gtSetStr('graph_order')) for(Map device in order){ //String sa="${sid}_${attribute}".toString() String attrib_string="attribute_${device[sID]}_${device[sATTR]}_line_color" String transparent_attrib_string= attrib_string+"_transparent" colors << (gtSetB(transparent_attrib_string) ? sTRANSPRNT : settings[attrib_string]) } Map options=[ "graphTimespan": Integer.parseInt(gtSetStr('graph_timespan')), "graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), "graphCombine_msecs": Integer.parseInt(gtSetStr('graph_combine_rate')), "graphOptions": [ "width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, "height": gtSetB(sGRPHSTATICSZ) ? graph_v_size: s100PCT, "timeline": [ "rowLabelStyle": ["fontSize": graph_axis_font, "color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color], "barLabelStyle": ["fontSize": graph_axis_font], ], "haxis" : [ "text": ["fontSize": "24px"]], "backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), "colors" : colors ], ] if(isEric())myDetail null,"getChartOptions_timeline $options" return options } String getGraph_timeline(){ String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" String html=""" ${scriptIncludes1(isSystemType())}
""" return html } //oauth endpoints Map getSubscriptions_timeline(){ List _ids=[] Map definitions=[:] Map labels=[:] Boolean isPoll isPoll=gtStB('hasFuel') // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() _ids << sid definitions[sid]=[:] labels[sid]=[:] definitions[sid][attribute]=[(sSTART): settings["attribute_${sa}_start"] ?: sSPC, (sEND): settings["attribute_${sa}_end"] ?: sSPC ] labels[sid][attribute]=settings["graph_name_override_${sa}"] } } Map sensors_fmt=gtSensorFmt(true) List order=gtSetStr('graph_order') ? parseJson(gtSetStr('graph_order')) : [] Map subscriptions=[ (sID): isPoll ? sPOLL : sSENSOR, "ids": _ids, 'sensors': sensors_fmt, "definitions": definitions, "labels": labels, "order": order ] return subscriptions } /* * TODO: Timegraph methods */ def mainTimegraph(){ mainShare1('Choose Numeric Attributes only','graph_timespan') } def deviceTimegraph(){ // wremoveSetting('graph_timespan') deviceShare1() } def attributeTimegraph(){ attributeShare1() } def graphTimegraph(){ List> timespanEnum2=[ ["10":"10 Milliseconds"], ["1000":"1 Second"], ["5000":"5 Seconds"], ["30000":"30 Seconds"], ["60000":"1 Minute"], ["120000":"2 Minutes"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1800000":"30 minutes"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], ["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"], ["1209600000":"2 Weeks"], ["2629800000":"1 Month"]] dynamicPage((sNM): "graphSetupPage"){ List container container=[] hubiForm_section("General Options", i1, sBLK, sBLK){ //input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "Select Time span to Graph", (sMULTP): false, (sREQ): true, options: timespanEnum, (sDEFV): "43200000") input( (sTYPE): sENUM, (sNM): 'graph_point_span',(sTIT): "Integration Time
(The amount of time each data point covers)", (sMULTP): false, (sREQ): true, options: timespanEnum2, (sDEFV): "300000", (sSUBOC): true) inputGraphUpdateRate("300000") container=[] container << hubiForm_sub_section("Graph Time Span
Amount of time the graph covers") if(graph_timespan_weeks == null){ app.updateSetting("graph_timespan_weeks", iZ) app.updateSetting("graph_timespan_days", i1) app.updateSetting("graph_timespan_hours", iZ) app.updateSetting("graph_timespan_minutes", iZ) settings["graph_timespan_weeks"]=iZ settings["graph_timespan_days"]=i1 settings["graph_timespan_hours"]=iZ settings["graph_timespan_minutes"]=iZ } container << hubiForm_slider ((sTIT): "Weeks", (sNM): "graph_timespan_weeks", (sDEFLT): iZ, (sMIN): iZ, (sMAX): 104, (sUNITS): " weeks", (sSUBONCHG): true) container << hubiForm_slider ((sTIT): "Days", (sNM): "graph_timespan_days", (sDEFLT): iZ, (sMIN): iZ, (sMAX): 30, (sUNITS): " days", (sSUBONCHG): true) container << hubiForm_slider ((sTIT): "Hours", (sNM): "graph_timespan_hours", (sDEFLT): iZ, (sMIN): iZ, (sMAX): 24, (sUNITS): " hours", (sSUBONCHG): true) container << hubiForm_slider ((sTIT): "Minutes", (sNM): "graph_timespan_minutes", (sDEFLT): iZ, (sMIN): iZ, (sMAX): 60, (sUNITS): " minutes", (sSUBONCHG): true) Long msecs if(graph_timespan_weeks==null){ msecs=86400000L }else{ msecs= Math.round((Double)(graph_timespan_weeks)*604800000+ (Double)(graph_timespan_days)*86400000+ (Double)(graph_timespan_hours)*3600000+ (Double)(graph_timespan_minutes)*60000) } app.updateSetting("graph_timespan", [(sTYPE): "number", (sVAL): msecs]) settings["graph_timespan"]=msecs Integer points=gtSetStr('graph_point_span') ? (msecs/Double.parseDouble(gtSetStr('graph_point_span'))).toInteger() : 280 if(points > 2000){ container << hubiForm_text ("""WARNING: ${(points)} Points will be generated per Attribute per Graph
Too many points will cause webCoRE graphs to hang or take a long time to generate""") }else{ container << hubiForm_text ("NOTE: ${(points)} Points will be generated per Attribute per Graph") } container << hubiForm_sub_section("Other Options") container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) container << hubiForm_switch([(sTIT): "Smooth Graph Points
(Enable Google Graph Smoothing)", (sNM): "graph_smoothing", (sDEFLT): false]) container << hubiForm_switch([(sTIT): "Flip Graph to Vertical?
(Rotate 90 degrees)", (sNM): "graph_y_orientation", (sDEFLT): false]) container << hubiForm_switch([(sTIT): "Reverse Data Order?
(Flip data left to Right)", (sNM): "graph_z_orientation", (sDEFLT): false]) hubiForm_container(container, i1) } hubiForm_section("Graph Title", i1, sBLK, sBLK){ container=[] container << hubiForm_switch([(sTIT): "Show Title on Graph", (sNM): 'graph_show_title', (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('graph_show_title')){ container << hubiForm_text_input("Graph Title", "graph_title", "Graph Title", false) container << hubiForm_font_size((sTIT): "Title", (sNM): "graph_title", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color("Title", "graph_title", sBLACK, false) container << hubiForm_switch ([(sTIT): "Graph Title Inside Graph?", (sNM): 'graph_title_inside', (sDEFLT): false]) } hubiForm_container(container, i1) } hubiForm_section("Graph Fill", i1, sBLK, sBLK){ container=[] container << hubiForm_switch ([(sTIT): "Set Fill % of Graph?
(False=Default (80%) Fill)", (sNM): "graph_percent_fill", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('graph_percent_fill')){ container << hubiForm_slider ((sTIT): "Horizontal fill % of the graph", (sNM): "graph_h_fill", (sDEFLT): 80, (sMIN): i1, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) container << hubiForm_slider ((sTIT): "Vertical fill % of the graph", (sNM): "graph_v_fill", (sDEFLT): 80, (sMIN): i1, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) } hubiForm_container(container, i1) } gatherGraphSize() hubiForm_section("Horizontal Axis", i1, sBLK, sBLK){ //Axis container=[] container << hubiForm_font_size((sTIT): "Horizontal Axis", (sNM): "graph_haxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color("Horizontal Header", "graph_hh", sSILVER, false) container << hubiForm_color("Horizontal Axis", "graph_ha", sSILVER, false) container << hubiForm_text_input("Num Horizontal Gridlines (Blank for auto)", "graph_h_num_grid", sBLK, false) container+= hubiForm_help() hubiForm_container(container, i1) } //Vertical Axis hubiForm_section("Vertical Axis", i1, sBLK, sBLK){ container=[] container << hubiForm_font_size ((sTIT): "Vertical Axis", (sNM): "graph_vaxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Vertical Header", "graph_vh", sBLACK, false) container << hubiForm_color ("Vertical Axis", "graph_va", sSILVER, false) hubiForm_container(container, i1) } //Left Axis List> formatEnum=[["": "No Formatting ::: 12345"], ["decimal":"Decimal ::: 12,345"], ["short": "Short ::: 12K"], ["scientific": "Scientific ::: 1e5"], ["percent": "Percent ::: 1234500%"], ["long": "Long ::: 12 Thousand"]] hubiForm_section("Left Axis", i1, "arrow_back", sBLK){ input( (sTYPE): sENUM, (sNM): "graph_vaxis_1_format",(sTIT): "Number Format", (sMULTP): false, (sREQ): true, options: formatEnum, (sDEFV): sBLK) container=[] container << hubiForm_text_input("Minimum for left axis (Blank for auto)", "graph_vaxis_1_min", sBLK, false) container << hubiForm_text_input("Maximum for left axis (Blank for auto)", "graph_vaxis_1_max", sBLK, false) container << hubiForm_text_input("Num Vertical Gridlines (Blank for auto)", "graph_vaxis_1_num_lines", sBLK, false) container << hubiForm_switch ([(sTIT): "Show Left Axis Label on Graph", (sNM): "graph_show_left_label", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('graph_show_left_label')){ container << hubiForm_text_input("Input Left Axis Label", "graph_left_label", "Left Axis Label", false) container << hubiForm_font_size((sTIT): "Left Axis", (sNM): "graph_left", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color("Left Axis", "graph_left", sWHT, false) } hubiForm_container(container, i1) } //Right Axis hubiForm_section("Right Axis", i1, "arrow_forward", sBLK){ input( (sTYPE): sENUM, (sNM): "graph_vaxis_2_format",(sTIT): "Number Format", (sMULTP): false, (sREQ): true, options: formatEnum, (sDEFV): sBLK) container=[] container << hubiForm_text_input("Minimum for right axis (Blank for auto)", "graph_vaxis_2_min", sBLK, false) container << hubiForm_text_input("Maximum for right axis (Blank for auto)", "graph_vaxis_2_max", sBLK, false) container << hubiForm_text_input("Num Vertical Gridlines (Blank for auto)", "graph_vaxis_2_num_lines", sBLK, false) container << hubiForm_switch ([(sTIT): "Show Right Axis Label on Graph", (sNM): "graph_show_right_label", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('graph_show_right_label')){ container << hubiForm_text_input("Input right Axis Label", "graph_right_label", "Right Axis Label", false) container << hubiForm_font_size ((sTIT): "Right Axis", (sNM): "graph_right", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Right Axis", "graph_right", sWHT, false) } hubiForm_container(container, i1) } //Legend hubiForm_section("Legend", i1, sBLK, sBLK){ container=[] List legendPosition=[["top": "Top"], ["bottom":"Bottom"], ["in": "Inside Top"]] List insidePosition=[[(sSTART): "Left"], ["center": "Center"], [(sEND): "Right"]] container << hubiForm_switch([(sTIT): "Show Legend on Graph", (sNM): "graph_show_legend", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('graph_show_legend')){ container << hubiForm_font_size ((sTIT): "Legend", (sNM): "graph_legend", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Legend", "graph_legend", sBLACK, false) hubiForm_container(container, i1) input( (sTYPE): sENUM, (sNM): "graph_legend_position",(sTIT): "Legend Position", (sDEFV): "bottom", options: legendPosition) input( (sTYPE): sENUM, (sNM): "graph_legend_inside_position",(sTIT): "Legend Justification", (sDEFV): sCENTER, options: insidePosition) }else{ hubiForm_container(container, i1) } } hubiForm_section("Current Value Overlay", i1, sBLK, sBLK){ container=[] container << hubiForm_switch ([(sTIT): "Show Current Values on Graph?", (sNM): 'show_overlay', (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('show_overlay')){ container << hubiForm_color ("Background", "overlay_background", sBLACK, false) container << hubiForm_slider ((sTIT): "Background Opacity", (sNM): "overlay_background_opacity", (sDEFLT): i90, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) container << hubiForm_font_size ((sTIT): "Device", (sNM): 'overlay', (sDEFLT): i12, (sMIN): i2, (sMAX): i40) container << hubiForm_color ("Device Text", 'overlay_text', sWHT, false) List horizontalAlignmentEnum=["Left", "Middle", "Right"] container << hubiForm_enum ((sTIT): "Horizontal Placement", (sNM): "overlay_horizontal_placement", list: horizontalAlignmentEnum, (sDEFLT): "Right") List verticalAlignmentEnum=["Top", "Middle", "Bottom"] container << hubiForm_enum ((sTIT): "Vertical Placement", (sNM): "overlay_vertical_placement", list: verticalAlignmentEnum, (sDEFLT): "Top") }else{ if(gtSetStr('overlay_background_color')!='#000000' || settings['overlay_background_opacity']!=i90){ app.updateSetting('overlay_background_color', [(sTYPE):'color', (sVAL):sBLACK]) app.updateSetting('overlay_background_color_transparent', [(sTYPE):sBOOL, (sVAL):sFALSE]) app.updateSetting("overlay_background_opacity", [(sTYPE):'number',(sVAL):i90]) app.updateSetting("overlay_background_font", [(sTYPE):'number',(sVAL): 12]) app.updateSetting("overlay_text_color", [(sTYPE):'color', (sVAL):sWHT]) app.updateSetting("overlay_text_color_transparent", [(sTYPE):sBOOL, (sVAL):sFALSE]) app.updateSetting("overlay_horizontal_placement", [(sTYPE):sENUM,(sVAL):"Right"]) app.updateSetting("overlay_vertical_placement", [(sTYPE):sENUM,(sVAL):"Top"]) wremoveSetting('overlay_order') } } container << hubiForm_sub_section("Display Order") hubiForm_container(container, i1) container=[] hubiForm_list_reorder('overlay_order', sBACKGRND) //hubiForm_container(container, i1) } /* state.num_devices=iZ List availableAxis=[["0" : "Left Axis"], ["1": "Right Axis"]] if(state.num_devices == i1){ availableAxis=[["0" : "Left Axis"], ["1": "Right Axis"], ["2": "Both Axes"]] }*/ //Line Integer cnt; cnt=iZ //Boolean bar_size_shown=false //Deal with Global-Specific Settings (ie bar spacing and plot-point size) Boolean show_title,show_bar show_title=false show_bar=false //Boolean show_scatter=false // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) switch (gtSetStr("graph_type_${sid}_${attribute}")){ //list:["Line", "Area", "Scatter", "Bar", "Stepped"], case "Bar" : show_title=true; show_bar=true; break } } } if(show_title && show_bar){ hubiForm_section("Overall Settings for Graph Types", i1, sBLK, sBLK){ container=[] container << hubiForm_slider ((sTIT): "Bar Graphs:: Relative Width for Bars", (sNM): 'graph_bar_width', (sDEFLT): i90, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) hubiForm_container(container, i1) } }else app.updateSetting('graph_bar_width', i90) // TODO if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String rid=ent[sRID].toString() String attribute=sMs(ent,sA) String dn=sMs(ent,sDISPNM) String typ=sMs(ent,sT).capitalize() String hint= typ=='Fuel' ? " (Canister ${ent.c} Name ${ent.n})" : sBLK String sa= "${sid}_${attribute}".toString() String asasn= "attribute_${sa}_states" // Name of setting that holds a list of the names of all states (enum and custom). String asacsn= "attribute_${sa}_custom_state_names" // Name of setting that holds a list of the names of custom states. hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "direction",sid+attribute){ container=[] container << hubiForm_sub_section("Plot Options") container << hubiForm_enum ((sTIT): "Plot Type", (sNM): "graph_type_${sa}".toString(), list: ["Line", "Area", "Scatter", "Bar", "Stepped"], (sDEFLT): "Line", (sSUBONCHG): true) // TODO submit_on_change was commented out.... container << hubiForm_enum ((sTIT): "Time Integration Function", (sNM): "var_${sa}_function".toString(), list: ["Average", "Min", "Max", "Mid", "Sum", "Median", "First", "Last"], (sDEFLT): "Average") container << hubiForm_enum ((sTIT): "Axis Side", (sNM): "graph_axis_number_${sa}".toString(), list: ["Left", "Right"], (sDEFLT): "Left") String colorText,fillText colorText=sBLK fillText="Fill" String graphType=gtSetStr("graph_type_${sa}") switch (graphType){ case "Line": colorText="Line" fillText=sBLK break case "Area": colorText="Area Line" break case "Bar": colorText="Bar Border" break case "Scatter": colorText="Border" break case "Stepped": colorText="Line" break default: fillText=sBLK } container << hubiForm_sub_section(colorText+" Options") container << hubiForm_color(colorText, "var_${sa}_stroke", hubiTools_rotating_colors(cnt), false) container << hubiForm_slider ((sTIT): colorText+" Opacity", (sNM): "var_${sa}_stroke_opacity", (sDEFLT): i90, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) container << hubiForm_line_size ((sTIT): colorText, (sNM): "var_${sa}_stroke", (sDEFLT): i2, (sMIN): i1, (sMAX): i20) if(graphType in ["Bar","Area","Stepped"]){ container << hubiForm_sub_section(graphType+sSPC+fillText+" Options") container << hubiForm_color(fillText, "var_${sa}_fill", hubiTools_rotating_colors(cnt), false) container << hubiForm_slider ((sTIT): fillText+" Opacity", (sNM): "var_${sa}_fill_opacity", (sDEFLT): i90, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) } if(graphType in ["Scatter","Line","Area"]){ container << hubiForm_sub_section("Data Points") if(graphType in ["Line","Area"]){ container << hubiForm_switch([(sTIT): "Display Data Points on Line?", (sNM): "var_${sa}_line_plot_points", (sDEFLT): false, (sSUBONCHG): true]) } if(settings["var_${sa}_line_plot_points"] || graphType == "Scatter"){ container << hubiForm_enum ( (sTIT): "Point Type", (sNM): "var_${sa}_point_type".toString(), list: [ "Circle", "Triangle", "Square", "Diamond", "Star", "Polygon"], (sDEFLT): "Circle") container << hubiForm_slider ((sTIT): "Point Size", (sNM): "var_${sa}_point_size".toString(), (sDEFLT): i5, (sMIN): iZ, (sMAX): 60, (sUNITS): " points", (sSUBONCHG): false) if(graphType == "Area"){ container << hubiForm_text ("*Note, Area Plots use the same fill setting for Points and Area (Above)") }else{ container << hubiForm_color("Point Fill", "var_${sa}_fill", hubiTools_rotating_colors(cnt), false) container << hubiForm_slider ((sTIT): "Point Fill Opacity", (sNM): "var_${sa}_fill_opacity", (sDEFLT): i90, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) } }else{ app.updateSetting ("var_${sa}_point_size", iZ) settings["var_${sa}_point_size"]=iZ } } def currentAttribute, sensor Boolean enumType; enumType=false List possible_values; possible_values= [] // List of the names of all states (enum and custom). List possible_custom_values= [] // List of the names of custom states. Integer numStates; numStates= iZ //TODO need to check if dataset is quanted, and based on quant type decide if values can be determined // check if data is regular start: String defltS, defltE Map b=gtStartEndTypes(ent,attribute) if(b){ defltS=sMs(b,sSTART) defltE=sMs(b,sEND) enumType=true possible_values = [defltS,defltE] }else{ if(typ==sCSENSOR){ Boolean multiple=true String varn=multiple ? 'sensors' : 'sensor_' // have to get devices from settings def a=gtSetting(varn) List devs = multiple ? (List)a : [a] if(devs.size()){ sensor=devs.find{ it.id == rid } List sas= (List)sensor.getSupportedAttributes() for(attrib in sas){ if((String)attrib[sNM] == attribute){ currentAttribute=attrib if(attrib.dataType == "ENUM"){ possible_values=currentAttribute.getValues() enumType=true } } } }else warn 'graphTimegraph: no devices found',null } } if(enumType){ container << hubiForm_sub_section("""Numerical values for "$attribute" states""") for(String value in possible_values){ container << hubiForm_text_input("Value for $value", "attribute_${sa}_${value}", s100, false) } } if(1 || !enumType){ // Allow enum types to have custom states also. String csn= "attribute_${sa}_custom_states" Boolean cs cs= settings[csn] container << hubiForm_sub_section("""Custom State Values for "$attribute" """ ) if(cs == null) app.updateSetting(csn, [(sTYPE): sBOOL, (sVAL): sFALSE]) container << hubiForm_switch([(sTIT): "Set Custom State Values?
(For custom drivers w/ non-numeric values)", (sNM): csn, (sDEFLT): false, (sSUBONCHG): true]) cs= settings[csn] if(cs){ //if(!settings["attribute_${sa}_num_custom_states"]){} container << hubiForm_text_input("Number of Custom States", "attribute_${sa}_num_custom_states", s2, true) numStates=Integer.parseInt(settings["attribute_${sa}_num_custom_states"].toString()) String csin Integer i for(i=iZ; iState #"+(i.toString())+"
", csin, sBLK, true) if(settings[csin]){ subcontainer << hubiForm_text_input('Value for "'+settings[csin]+'"', csin+"_value", s0, true) } container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.5, 0.5]]) } } // Remove previous custom state value settings List asacs= (List)settings[asacsn] if(asacs){ List old_custom_values=asacs for(String val in old_custom_values){ wremoveSetting("attribute_${sa}_${val}") } } // Update custom state settings if(cs){ String csin Integer i for(i=iZ; iDisplay Missing Data as a Drop Line?", (sNM): "attribute_${sa}_drop_line", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB("attribute_${sa}_drop_line")){ container << hubiForm_text_input("Value of Missing Data", "attribute_${sa}_drop_value", s0, false) } container << hubiForm_switch([(sTIT): "Extend Left Value?
When values are unavailable at start of timespan,, extend first value to left", (sNM): "attribute_${sa}_extend_left", (sDEFLT): false, (sSUBONCHG): false]) container << hubiForm_switch([(sTIT): "Extend Right Value?
When values are unavailable at end of timespan, extend last value to right", (sNM): "attribute_${sa}_extend_right", (sDEFLT): false, (sSUBONCHG): false]) container << hubiForm_switch([(sTIT): "Interpolate Left Value?
When values are unavailable at start of timespan, interpolate from first value to left edge", (sNM): "attribute_${sa}_interp_left", (sDEFLT): false, (sSUBONCHG): false]) }else{ wremoveSetting("attribute_${sa}_drop_line") wremoveSetting("attribute_${sa}_extend_left") wremoveSetting("attribute_${sa}_extend_right") wremoveSetting("attribute_${sa}_interp_left") } container << hubiForm_sub_section("Restrict Displayed Values") container << hubiForm_switch([(sTIT): "Restrict Displaying Bad Values?", (sNM): "attribute_${sa}_bad_value", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB("attribute_${sa}_bad_value")){ container << hubiForm_text_input("Min Value to Include
If the recorded sensor value is below this value it will be dropped", "attribute_${sa}_min_value", s0, false) container << hubiForm_text_input("Max Value to Include
If the recorded sensor value is above this value it will be dropped", "attribute_${sa}_max_value", s100, false) } container << hubiForm_text_input("Units for Pretty Display", "units_${sa}", sBLK, false) hubiForm_container(container, i1) cnt += i1 } } } } } String getData_timegraph(){ Map resp=[:] // Get extra data before the start of the timespan to cover the width of the first integration bucket. Long timespan = Long.parseLong(gtSetStr('graph_timespan')) Long pointspan = Long.parseLong(gtSetStr('graph_point_span')) Long graph_time = Math.round(wnow() - timespan - (pointspan / 2.0D)) // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa= "${sid}_${attribute}".toString() resp[sid]= resp[sid] ?: [:] List data data = gtDataSourceData(ent) // Get all sensor data regardless of time. // Get the index of the first data item that is in the timespan. Integer first_index = data.findIndexOf{ Map it -> lMt(it) > graph_time} if (first_index >= iZ){ // Some data is in the timespan: if (first_index > iZ){ // Also some data is before the timespan: data = data.subList(first_index - i1, data.size()) // Take the last item before the timespan, and everything later. } else { // No data is before the timespan: // Take all the data that we have. } } else { // No data is in the timespan: if (data.size() >= i1){ // But some data is before the timespan: data = [ data[data.size()-i1] ] // Take the latest data item that we have. } else { // No data at all: // No data at all. MAY NOT BE ABLE TO HIT THIS CASE! } } // Map state names to values. resp[sid][attribute]=data.collect{ Map it -> [(sDT): lMt(it), (sVAL): getValue(sid, attribute, it[sVAL])]} // Eliminate "bad" values if(gtSetB("attribute_${sa}_bad_value")){ Float min=Float.valueOf(settings["attribute_${sa}_min_value"].toString()) Float max=Float.valueOf(settings["attribute_${sa}_max_value"].toString()) resp[sid][attribute]= ((List)resp[sid][attribute]).findAll{ Map it -> (Double)it[sVAL] >= min && (Double)it[sVAL] <= max} } } } return JsonOutput.toJson(resp) } Map getOptions_timegraph(){ /*Setup Series*/ //Map series=["series" : [:]] Map options=[ "graphReduction": gtSetI('graph_max_points'), "graphTimespan": Long.parseLong(gtSetStr('graph_timespan')), "graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), "graphPointSpan": Long.parseLong(gtSetStr('graph_point_span')), // "graphRefreshRate" : Integer.parseInt(gtSetStr('graph_refresh_rate')), "overlays": [ "display_overlays" : show_overlay, "horizontal_alignment" : overlay_horizontal_placement, "vertical_alignment" : overlay_vertical_placement, "order" : gtSetStr('overlay_order') ], "graphOptions": [ "tooltip" : ["format" : "short"], "width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, "height": gtSetB(sGRPHSTATICSZ) ? graph_v_size : s100PCT, "chartArea": [ "width": graph_percent_fill ? "${graph_h_fill}%" : s80PCT, "height": graph_percent_fill ? "${graph_v_fill}%" : s80PCT], "explorer": [ "actions": ["dragToZoom", "rightClickToReset"], "axis": "horizontal", "keepInBounds": true, "maxZoomIn": 40.0 ], "hAxis": [ "textStyle": ["fontSize": graph_haxis_font, "color": gtSetB('graph_hh_color_transparent') ? sTRANSPRNT : graph_hh_color ], "gridlines": ["color": gtSetB('graph_ha_color_transparent') ? sTRANSPRNT : graph_ha_color, "count": graph_h_num_grid != sBLK ? graph_h_num_grid : null ], "format": gtSetStr('graph_h_format')==sBLK?sBLK:gtSetStr('graph_h_format') ], "vAxis": ["textStyle": ["fontSize": graph_vaxis_font, "color": gtSetB('graph_vh_color_transparent') ? sTRANSPRNT : graph_vh_color, ], "gridlines": ["color": gtSetB('graph_va_color_transparent') ? sTRANSPRNT : graph_va_color], ], "vAxes": [ 0: ["title" : graph_show_left_label ? graph_left_label: null, "titleTextStyle": ["color": gtSetB('graph_left_color_transparent') ? sTRANSPRNT : graph_left_color, "fontSize": graph_left_font], "viewWindow": ["min": graph_vaxis_1_min != sBLK ? graph_vaxis_1_min : null, "max": graph_vaxis_1_max != sBLK ? graph_vaxis_1_max : null], "gridlines": ["count" : graph_vaxis_1_num_lines != sBLK ? graph_vaxis_1_num_lines : null ], "minorGridlines": ["count" : 0], "format": graph_vaxis_1_format, ], 1: ["title": graph_show_right_label ? graph_right_label : null, "titleTextStyle": ["color": gtSetB('graph_right_color_transparent') ? sTRANSPRNT : graph_right_color, "fontSize": graph_right_font], "viewWindow": ["min": graph_vaxis_2_min != sBLK ? graph_vaxis_2_min : null, "max": graph_vaxis_2_max != sBLK ? graph_vaxis_2_max : null], "gridlines": ["count" : graph_vaxis_2_num_lines != sBLK ? graph_vaxis_2_num_lines : null ], "minorGridlines": ["count" : 0], "format": graph_vaxis_2_format, ] ], "bar": [ "groupWidth" : graph_bar_width+"%", "fill-opacity" : 0.5], "pointSize": graph_scatter_size, "legend": !gtSetB('graph_show_legend') ? ["position": sNONE] : ["position": graph_legend_position, "alignment": graph_legend_inside_position, "textStyle": ["fontSize": graph_legend_font, "color": gtSetB('graph_legend_color_transparent') ? sTRANSPRNT : gtSetStr('graph_legend_color')]], "backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), "curveType": !graph_smoothing ? sBLK : "function", "title": !gtSetB('graph_show_title') ? sBLK : gtSetStr('graph_title'), "titleTextStyle": !gtSetB('graph_show_title') ? sBLK : ["fontSize": graph_title_font, "color": gtSetB('graph_title_color_transparent') ? sTRANSPRNT : gtSetStr('graph_title_color')], "titlePosition" : gtSetB('graph_title_inside') ? "in" : "out", "interpolateNulls": true, //for null vals on our chart "orientation" : gtSetB('graph_y_orientation')? "vertical" : "horizontal", "reverseCategories" : graph_z_orientation, "series": [:], ] ] Integer count_ count_=iZ // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa= "${sid}_${attribute}".toString() String type_ type_= settings["graph_type_${sa}"] != null ? gtSetStr("graph_type_${sa}").toLowerCase() : 'line' if(type_ == "stepped") type_="steppedArea" Integer axes_=settings["graph_axis_number_${sa}"] == "Left" ? iZ : i1 String stroke_color=gtSetStr("var_${sa}_stroke_color") String stroke_opacity=gtSetStr("var_${sa}_stroke_opacity") //def stroke_line_size=settings["var_${sa}_stroke_line_size"] //String fill_color=settings["var_${sa}_fill_color"] //String fill_opacity=settings["var_${sa}_fill_opacity"] def point_size=settings["var_${sa}_point_size"] String point_type=settings["var_${sa}_point_type"] != null ? gtSetStr("var_${sa}_point_type").toLowerCase() : sBLK type_=type_=='bar' ? 'bars' : type_ options.graphOptions.series << [(count_.toString()) : [ "type" : type_, "targetAxisIndex" : axes_, "pointSize" : point_size, "pointShape" : point_type, "color" : stroke_color, "opacity" : stroke_opacity, ] ] count_++ } } // TODO if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa= "${sid}_${attribute}".toString() //add colors and thicknesses Integer axis=settings["graph_axis_number_${sa}"] == "Left" ? iZ : i1 String tc= "graph_line_${sa}_color" String text_color=gtSetStr(tc) Boolean text_color_transparent=gtSetB(tc+"_transparent") Map annotations=[ "targetAxisIndex": axis, "color": text_color_transparent ? sTRANSPRNT : text_color ] options.graphOptions.series << annotations } } return options } static String getDrawType_timegraph(){ return "google.visualization.LineChart" } static String getRGBA(String hex, opacity){ String c c=hex-"#" c=c.toUpperCase() Integer i=Integer.parseInt(c, 16) Integer r=(i & 0xFF0000) >> 16 Integer g=(i & 0xFF00) >> 8 Integer b=(i & 0xFF) Float o=opacity/100.0 String s=sprintf("rgba( %d, %d, %d, %.2f)", r, g, b, o) return s } String getGraph_timegraph(){ String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" String html html=""" ${scriptIncludes()}
""" if(gtSetB('show_overlay')) html+= getOverlay_timegraph() html+= """ """ return html } String getOverlay_timegraph(){ String html html="""
""" List val=new JsonSlurper().parseText(gtSetStr('overlay_order')) as List for(String str in val){ String[] splitStr=str.split('_') String sid=splitStr[i1] String attribute=splitStr[i2] String sa= "${sid}_${attribute}".toString() Map ent=findDataSourceEntry(sid,attribute) Double v=getValue(sid,attribute,getLatestVal(ent)) String units= gtSetStr("units_${sa}") ?: sBLK String name; name= gtSetStr("graph_name_override_${sa}") name=name.replaceAll("%deviceName%", sMs(ent,sDISPNM)).replaceAll("%attributeName%", attribute) String s=sprintf("%.1f%s",v,units) html += """""" } html += """""" return html } //oauth endpoints Map getSubscriptions_timegraph(){ List ids=[] Map sensors_=[:] Map attributes=[:] Map labels=[:] Map drop_=[:] Map extend_=[:] Map var_=[:] Map graph_type_=[:] Map states_=[:] Boolean isPoll isPoll=gtStB('hasFuel') // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa= "${sid}_${attribute}".toString() String dn=sMs(ent,sDISPNM) //String typ=sMs(ent,sT).capitalize() if(!ids.contains(sid)) ids << sid //TODO //only take what we need //Map sensors_fmt=gtSensorFmt() sensors_[sid]=[ (sID): sid /*, idAsLong: sensor.idAsLong */, (sDISPNM): dn ] attributes[sid]= attributes[sid] ?: [] attributes[sid] << attribute String attr=attribute labels[sid]= labels[sid] ?: [:] labels[sid][attr]=gtSetStr("graph_name_override_${sa}") states_[sid]= states_[sid] ?: [:] String varn= "attribute_${sa}_states".toString() if((List)settings[varn] /* && gtSetB("attribute_${sa}_custom_states") */){ states_[sid][attr]=[:] for(String st in (List)settings[varn]){ states_[sid][attr][st]=settings["attribute_${sa}_${st}"] } } drop_[sid]= drop_[sid] ?: [:] Boolean drop_valid; drop_valid=false if(gtSetB("attribute_${sa}_drop_line")) drop_valid=true drop_[sid][attr]=[ valid: drop_valid ? sTRUE : sFALSE, (sVAL): drop_valid ? settings["attribute_${sa}_drop_value"] : "null", restrict_bad: settings["attribute_${sa}_bad_value"], min: settings["attribute_${sa}_min_value"], max: settings["attribute_${sa}_max_value"] ] extend_[sid]= extend_[sid] ?: [:] extend_[sid][attr]=[ right: settings["attribute_${sa}_extend_right"], left: settings["attribute_${sa}_extend_left"], interp: settings["attribute_${sa}_interp_left"] ] graph_type_[sid]= graph_type_[sid] ?: [:] graph_type_[sid][attr]=settings["graph_type_${sa}"] def stroke_color=settings["var_${sa}_stroke_color"] def stroke_opacity=settings["var_${sa}_stroke_opacity"] def stroke_line_size=settings["var_${sa}_stroke_line_size"] def fill_color=settings["var_${sa}_fill_color"] def fill_opacity=settings["var_${sa}_fill_opacity"] def function=settings["var_${sa}_function"] var_[sid]= var_[sid] ?: [:] var_[sid][attr]=[ stroke_color : stroke_color, stroke_opacity : stroke_opacity, stroke_width: stroke_line_size, fill_color: fill_color, fill_opacity: fill_opacity, function: function, (sUNITS): gtSetStr("units_${sa}") ?: sBLK, ] } } Integer logging_ = iMs((Map)state,sLOGNG) Map subscriptions=[ (sID): isPoll ? sPOLL : sSENSOR, logging: logging_, ids: ids, //.sort(), 'sensors': sensors_, 'attributes': attributes, labels : labels, drop : drop_, extend: extend_, graph_type: graph_type_, var : var_, states: states_ ] return subscriptions } /* * TODO: Heatmap methods */ def mainHeatmap(){ mainShare1("""Choose Numeric Attributes or common sensor attributes (like on/off, open/close, present/not present, detected/clear, active/inactive, wet/dry, last Activity)""", sGRPHUPDRATE,true,false) } def deviceHeatmap(){ deviceShare1(true,false,true) } def attributeHeatmap(){ attributeShare1(true) } static String dd(Double num){ if(num> decayEnum=[["1000":"1 Second"], ["30000":"30 Seconds"], ["60000":"1 Minute"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1800000":"Half Hour"], ["3600000":"1 Hour"], ["7200000":"2 Hours"], ["21600000":"6 Hours"], ["43200000":"12 Hours"], ["86400000":"1 Day"], ["172800000":"2 Days"], ["259200000":"3 Days"], ["345600000":"4 Days"], ["432000000":"5 Days"], ["518400000":"6 Days"], ["604800000":"7 Days"]] List> typeEnum=[[(sVAL): "Value"], [(sTIME) : "Trigger (Time Since Last Update)"]] // TODO Integer count_ count_=iZ List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ //Get Device Count count_++ } } app.updateSetting ("attribute_count", count_) dynamicPage((sNM): "graphSetupPage"){ List container hubiForm_section("General Options", i1, sBLK, sBLK){ container=[] input( (sTYPE): sENUM, (sNM): 'graph_type',(sTIT): "Select Graph Type", (sMULTP): false, (sREQ): false, options: typeEnum, (sDEFV): sVAL, (sSUBOC): true) inputGraphUpdateRate() if(!gtSetStr('graph_type')) graph_type=sVAL if(gtSetStr('graph_type') == sTIME){ input( (sTYPE): sENUM, (sNM): "graph_decay",(sTIT): "Decay Rate", (sMULTP): false, (sREQ): false, options: decayEnum, (sDEFV): "300000", (sSUBOC): true) } container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) container << hubiForm_color ("Graph Line", "graph_line", sBLACK, false) container << hubiForm_line_size ((sTIT): "Graph Line", (sNM): "graph", (sDEFLT): i2, (sMIN): i1, (sMAX): count_, ) hubiForm_container(container, i1) } Integer num_ if(graph_num_gradients == null){ settings["graph_num_gradients"]=s2 app.updateSetting ("graph_num_gradients", s2) num_=i2 }else{ num_=graph_num_gradients.toInteger() } hubiForm_section("Level Gradient", i1, sBLK, sBLK){ container=[] container << hubiForm_text_input("Number of Gradient Levels", "graph_num_gradients", s2, true) List subcontainer if(gtSetStr('graph_type') == sVAL){ Integer gradient for(gradient=iZ; gradient < num_; gradient++){ subcontainer=[] String titleString if(gradient == iZ) titleString="Start" else if(gradient == num_-i1) titleString="End" else titleString="Mid" subcontainer << hubiForm_text_input(titleString+" Value", "graph_gradient_${gradient}_value", (gradient*i10).toString(), false) subcontainer << hubiForm_color ("Gradient #"+gradient, "graph_gradient_${gradient}", hubiTools_rotating_colors(gradient), false) container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.25, 0.75]]) } }else{ Long add_time=(graph_decay.toInteger()/(graph_num_gradients.toInteger()-i1)) Long curr_time curr_time=0L Integer gradient for(gradient=iZ; gradient < num_; gradient++){ subcontainer=[] subcontainer << hubiForm_text_format( [text: convertToString(curr_time), horizontal_align: sRIGHT, vertical_align: "20px", sz: 24] ) app.updateSetting ("graph_gradient_${gradient}_value", curr_time) subcontainer << hubiForm_color ("Gradient #"+gradient, "graph_gradient_${gradient}", hubiTools_rotating_colors(gradient), false) container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.25, 0.75]]) curr_time += add_time } } hubiForm_container(container, i1) } hubiForm_section("Graph Columns", i1, sBLK, sBLK){ container=[] Integer default_=Math.ceil(Math.sqrt(count_)).intValue() Integer cols=graph_num_columns ? "${graph_num_columns}".toInteger() : default_ Integer rows=Math.ceil(count_/cols).intValue() container << hubiForm_slider ((sTIT): "Number of Columns
"+count_+" Devices/Attributes -- "+cols+" X "+rows+"", (sNM): "graph_num_columns", (sDEFLT): default_, (sMIN): i1, (sMAX): count_, (sUNITS): " columns", (sSUBONCHG): true) hubiForm_container(container, i1) } gatherGraphSize() hubiForm_section("Annotations", i1, sBLK, sBLK){ container=[] container << hubiForm_switch([(sTIT): "Show values inside Heat Map?", (sNM): "show_annotations", (sDEFLT): false, (sSUBONCHG): true]) if(gtSetB('show_annotations')){ container << hubiForm_font_size ((sTIT): "Annotation", (sNM): "annotation", (sDEFLT): i16, (sMIN): i2, (sMAX): i40) container << hubiForm_color ("Annotation", "annotation", sWHT, false) container << hubiForm_color ("Annotation Aura", "annotation_aura", sBLACK, false) container << hubiForm_slider ((sTIT): "Number Decimal Places", (sNM): "graph_decimals", (sDEFLT): i1, (sMIN): iZ, (sMAX): i4, (sUNITS): " decimal places") container << hubiForm_switch ([(sTIT): "Bold Annotation", (sNM): "annotation_bold", (sDEFLT):false]) container << hubiForm_switch ([(sTIT): "Italic Annotation", (sNM): "annotation_italic", (sDEFLT):false]) } hubiForm_container(container, i1) } } } String getData_heatmap(){ Map resp=[:] Date now=new Date() //def then=new Date(0) // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) // String rid=ent.rid.toString() String attribute=sMs(ent,sA) resp[sid]= resp[sid] ?: [:] Map lst= gtLastData(ent) // [date: date, (sVAL): v, t: t] if(lst && sMs(ent,'aa') == 'lastupdate'){ //Date lastEvent=(Date)lst.date //sensor.getLastActivity() Long latest= lMt(lst) //lastEvent ? lastEvent.getTime() : 0L resp[sid]['lastupdate']=[(sCUR): (now.getTime()-latest), (sDT): latest] }else{ def latest=lst ? lst[sVAL] : iZ // sensor.latestState(attribute) resp[sid][attribute]=[(sCUR): latest, (sDT): lst[sDT] ?: now] } } } return JsonOutput.toJson(resp) } Map getOptions_heatmap(){ List colors=[] // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String attrib_string="attribute_${sid}_${attribute}_color" String transparent_attrib_string= attrib_string+"_transparent" colors << (gtSetB(transparent_attrib_string) ? sTRANSPRNT : settings[attrib_string]) } } /* String axis1,axis2 if(graph_type == "1"){ axis1="hAxis" axis2="vAxis" }else{ axis1="vAxis" axis2="hAxis" } */ Map options=[ "graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), (sGRAPHT): gtSetStr('graph_type'), "graphOptions": [ "bar" : [ "groupWidth" : s100PCT ], "width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, "height": gtSetB(sGRPHSTATICSZ) ? graph_v_size: s100PCT, "timeline": [ "rowLabelStyle": ["fontSize": graph_axis_font, "color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color], "barLabelStyle": ["fontSize": graph_axis_font] ], "backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), "isStacked": true, "chartArea": [ (sLEFT): i10, (sRIGHT) : i10, "top": i10, "bottom": i10 ], "legend" : [ "position" : sNONE ], "hAxis": [ "textPosition": sNONE, "gridlines" : [ "count" : s0 ] ], "vAxis": [ "textPosition": sNONE, "gridlines" : [ "count" : s0 ] ], "annotations" : [ "alwaysOutside": sFALSE, "textStyle": [ "fontSize": annotation_font, "bold": annotation_bold, "italic": annotation_italic, "color": gtSetB('annotation_color_transparent') ? sTRANSPRNT : annotation_color, "auraColor":gtSetB('annotation_aura_color_transparent') ? sTRANSPRNT : annotation_aura_color, ], "stem": [ "color": sTRANSPRNT, "highContrast": sFALSE ], ], ] ] return options } String getGraph_heatmap(){ String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" String html=""" ${scriptIncludes1(isSystemType())}
""" return html } //oauth endpoints Map getSubscriptions_heatmap(){ Integer count_ count_=iZ List _ids=[] Map _attributes=[:] Map labels=[:] Map gradients=[:] Boolean isPoll isPoll=gtStB('hasFuel') // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String nattribute nattribute=attribute if(sMs(ent,'aa') == "lastupdate") nattribute=ent.aa _ids << sid _attributes[sid] = _attributes[sid] ?: [] _attributes[sid] << nattribute count_++ labels[sid]= labels[sid] ?: [:] labels[sid][nattribute]="${sid} ${nattribute}" labels[sid][nattribute]=settings["graph_name_override_${sid}_${attribute}"] } } Map sensors_fmt=gtSensorFmt() Integer i Integer e=graph_num_gradients.toInteger() for(i=iZ; i> timespanEnum2=[ ["60000":"1 Minute"], ["120000":"2 Minutes"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1800000":"30 minutes"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], ["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"] ] dynamicPage((sNM): "graphSetupPage"){ Boolean non_numeric non_numeric=false List container // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String attribute=sMs(ent,sA) Map a=gtStartEndTypes(ent,attribute) if(a) non_numeric= true } } if(non_numeric) app.updateSetting ('graph_max_points', 0) hubiForm_section("General Options", i1, sBLK, sBLK){ input( (sTYPE): sENUM, (sNM): 'graph_type',(sTIT): "Graph Type", (sDEFV): "Line Graph", options: ["Line Graph", "Area Graph", "Scatter Plot"], (sSUBOC): true) inputGraphUpdateRate() input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "Select Time span to Graph", (sMULTP): false, (sREQ): true, options: timespanEnum, (sDEFV): "43200000") container=[] container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) container << hubiForm_switch((sTIT): "Smooth Graph Points", (sNM): "graph_smoothing", (sDEFLT): false) container << hubiForm_switch((sTIT): "Flip Graph to Vertical?
(Rotate 90 degrees)", (sNM): "graph_y_orientation", (sDEFLT): false) container << hubiForm_switch((sTIT): "Reverse Data Order?
(Flip data left to Right)", (sNM): "graph_z_orientation", (sDEFLT): false) if(!non_numeric) container << hubiForm_slider ((sTIT): "Maximum number of Data Points?
(Zero for ALL)", (sNM): 'graph_max_points', (sDEFLT): iZ, (sMIN): iZ, (sMAX): 1000, (sUNITS): " data points", (sSUBONCHG): false) hubiForm_container(container, i1) } hubiForm_section("Graph Title", i1, sBLK, sBLK){ container=[] container << hubiForm_switch((sTIT): "Show Title on Graph", (sNM): 'graph_show_title', (sDEFLT): false, (sSUBONCHG): true) if(gtSetB('graph_show_title')){ container << hubiForm_text_input("Graph Title", "graph_title", "Graph Title", false) container << hubiForm_font_size((sTIT): "Title", (sNM): "graph_title", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color("Title", "graph_title", sBLACK, false) container << hubiForm_switch((sTIT): "Graph Title Inside Graph?", (sNM): 'graph_title_inside', (sDEFLT): false) } hubiForm_container(container, i1) } gatherGraphSize() hubiForm_section("Horizontal Axis", i1, sBLK, sBLK){ //Axis container=[] container << hubiForm_font_size ((sTIT): "Horizontal Axis", (sNM): "graph_haxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Horizontal Header", "graph_hh", sSILVER, false) container << hubiForm_color ("Horizontal Axis", "graph_ha", sSILVER, false) container << hubiForm_text_input ("Num Horizontal Gridlines
(Blank for auto)", "graph_h_num_grid", sBLK, false) container+= hubiForm_help() hubiForm_container(container, i1) } //Vertical Axis hubiForm_section("Vertical Axis", i1, sBLK, sBLK){ container=[] container << hubiForm_font_size ((sTIT): "Vertical Axis", (sNM): "graph_vaxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Vertical Header", "graph_vh", sBLACK, false) container << hubiForm_color ("Vertical Axis", "graph_va", sSILVER, false) hubiForm_container(container, i1) } //Left Axis hubiForm_section("Left Axis", i1, "arrow_back", sBLK){ container=[] container << hubiForm_text_input("Minimum for left axis(Blank for auto)", "graph_vaxis_1_min", sBLK, false) container << hubiForm_text_input("Maximum for left axis(Blank for auto)", "graph_vaxis_1_max", sBLK, false) container << hubiForm_text_input("Num Vertical Gridlines
(Blank for auto)", "graph_vaxis_1_num_lines", sBLK, false) container << hubiForm_switch ((sTIT): "Show Left Axis Label on Graph", (sNM): "graph_show_left_label", (sDEFLT): false, (sSUBONCHG): true) if(gtSetB('graph_show_left_label')){ container << hubiForm_text_input ("Input Left Axis Label", "graph_left_label", "Left Axis Label", false) container << hubiForm_font_size ((sTIT): "Left Axis", (sNM): "graph_left", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Left Axis", "graph_left", sWHT, false) } hubiForm_container(container, i1) } //Right Axis hubiForm_section("Right Axis", i1, "arrow_forward", sBLK){ container=[] container << hubiForm_text_input("Minimum for right axis(Blank for auto)", "graph_vaxis_2_min", sBLK, false) container << hubiForm_text_input("Maximum for right axis(Blank for auto)", "graph_vaxis_2_max", sBLK, false) container << hubiForm_text_input("Num Vertical Gridlines
(Blank for auto)", "graph_vaxis_2_num_lines", sBLK, false) container << hubiForm_switch ((sTIT): "Show Right Axis Label on Graph", (sNM): "graph_show_right_label", (sDEFLT): false, (sSUBONCHG): true) if(gtSetB('graph_show_right_label')){ container << hubiForm_text_input ("Input right Axis Label", "graph_right_label", "Right Axis Label", false) container << hubiForm_font_size ((sTIT): "Right Axis", (sNM): "graph_right", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Right Axis", "graph_right", sWHT, false) } hubiForm_container(container, i1) } //Legend hubiForm_section("Legend", i1, sBLK, sBLK){ container=[] List legendPosition=[["top": "Top"], ["bottom":"Bottom"], ["in": "Inside Top"]] List insidePosition=[[(sSTART): "Left"], ["center": "Center"], [(sEND): "Right"]] container << hubiForm_switch((sTIT): "Show Legend on Graph", (sNM): "graph_show_legend", (sDEFLT): false, (sSUBONCHG): true) if(gtSetB('graph_show_legend')){ container << hubiForm_font_size ((sTIT): "Legend", (sNM): "graph_legend", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Legend", "graph_legend", sBLACK, false) hubiForm_container(container, i1) input( (sTYPE): sENUM, (sNM): "graph_legend_position",(sTIT): "Legend Position", (sDEFV): "bottom", options: legendPosition) input( (sTYPE): sENUM, (sNM): "graph_legend_inside_position",(sTIT): "Legend Justification", (sDEFV): sCENTER, options: insidePosition) }else{ hubiForm_container(container, i1) } } state.num_devices=iZ if(dataSources){ Integer i; i=iZ for(Map ent in dataSources){ i++ } state.num_devices=i } List availableAxis availableAxis=[[(s0): "Left Axis"], [(s1): "Right Axis"]] if(state.num_devices == i1){ availableAxis=[[(s0): "Left Axis"], [(s1): "Right Axis"], [(s2): "Both Axes"]] } //Line Integer cnt cnt=iZ if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String dn=sMs(ent,sDISPNM) hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}", i1, sBLK,sid+attribute){ container=[] input( (sTYPE): sENUM, (sNM): "graph_axis_number_${sid}_${attribute}",(sTIT): "Graph Axis Side", (sDEFV): s0, options: availableAxis) container << hubiForm_color("Line", "graph_line_${sid}_${attribute}", hubiTools_rotating_colors(cnt), false) container << hubiForm_line_size((sTIT): "Line Thickness", (sNM): "attribute_${sid}_${attribute}", (sDEFLT): i2, (sMIN): i1, (sMAX): i20) //TODO figure out from data if there are choices String startVal, endVal startVal=sBLK endVal=sBLK Map a=gtStartEndTypes(ent,attribute) if(a){ startVal=a[sSTART] endVal=a[sEND] } // String startVal=supportedTypes[attribute] ? supportedTypes[attribute].start : sBLK // String endVal=supportedTypes[attribute] ? supportedTypes[attribute].end : sBLK if(gtSetStr('graph_type') == "Area Graph"){ container << hubiForm_slider ((sTIT): "Opacity of the area below the line", (sNM): "attribute_${sid}_${attribute}_opacity", (sDEFLT): 30, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) } String nnvars= "attribute_${sid}_${attribute}_non_number".toString() String svars= "attribute_${sid}_${attribute}_startString".toString() String evars= "attribute_${sid}_${attribute}_endString".toString() if(startVal != sBLK){ app.updateSetting (nnvars, true) app.updateSetting (svars, startVal) app.updateSetting (evars, endVal) container << hubiForm_text("This Attribute ($attribute) is non-numerical, please choose values for the states below") container << hubiForm_text_input("Value for $startVal", "attribute_${sid}_${attribute}_${startVal}", s100, false) container << hubiForm_text_input("Value for $endVal", "attribute_${sid}_${attribute}_${endVal}", s0, false) hubiForm_container(container, i1) }else{ wremoveSetting(nnvars) wremoveSetting(svars) wremoveSetting(evars) container << hubiForm_switch((sTIT): "Display as a Drop Line", (sNM): "attribute_${sid}_${attribute}_drop_line", (sDEFLT): false, (sSUBONCHG): true) if(gtSetB("attribute_${sid}_${attribute}_drop_line")){ container << hubiForm_text_input("Value to drop the Line", "attribute_${sid}_${attribute}_drop_value", s0, false) hubiForm_container(container, i1) input( (sTYPE): sENUM, (sNM): "attribute_${sid}_${attribute}_drop_time",(sTIT): "Drop Line Time", (sDEFV): "300000", options: timespanEnum2 ) }else{ hubiForm_container(container, i1) } } cnt += i1 } } } } } String getData_linegraph(){ Map resp=[:] Date then then=new Date() Long graph_time use (TimeCategory){ then -= Integer.parseInt(gtSetStr('graph_timespan')).milliseconds graph_time=then.getTime() } // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() resp[sid]= resp[sid] ?: [:] List tEvents List respEvents List data=CgetData(ent, then) //return [date: d, (sVAL): sum.round(decimals), t: d.getTime()] // log.warn "got dn: $dn attribute: $attribute data1: $data" Listdata1 data1=data.collect{ Map it -> [(sDT): it[sT], (sVAL): getValue(sid, attribute, it[sVAL])]} List data2 data2=data1.findAll{ Map it -> lMs(it,sDT) > graph_time } List temp temp=([]+data2)// as List //temp=temp.sort{ (Long)it[sDT] } respEvents=temp data1=null data2=null // log.warn "FINAL got sensor: $sensor attribute: $attribute data1: $temp" temp=null /* respEvents << sensor.statesSince(attribute, then, [(sMAX): 50000]).collect{[ date: it.date.getTime(), (sVAL): getValue(sid, attribute, it.value) ]} respEvents=respEvents.flatten() respEvents=respEvents.reverse() */ //Add drop lines for non-numerical devices if(settings["attribute_${sa}_non_number"] && respEvents.size()>i1){ String start=gtSetStr("attribute_${sa}_startString") String end=gtSetStr("attribute_${sa}_endString") Float startVal=Float.parseFloat( gtSetStr("attribute_${sa}_${start}")) Float endVal=Float.parseFloat( gtSetStr("attribute_${sa}_${end}")) tEvents=[] //Add Start Event Long currDate currDate=then.getTime() if(respEvents[iZ][sVAL] == startVal){ tEvents.push([(sDT): currDate, (sVAL): endVal]) }else{ tEvents.push([(sDT): currDate, (sVAL): startVal]) } Integer i for(i=iZ; i iZ){ Integer reduction=Math.ceil(respEvents.size() / gtSetI('graph_max_points').toDouble()).toInteger() respEvents=respEvents.collate(reduction).collect{ List group -> group.inject([ (sDT): iZ, (sVAL): iZ ]){ col, it -> col[sDT] += it[sDT] / group.size() col[sVAL] += it[sVAL] / group.size() return col } } } //add drop line data tEvents=[] if(gtSetB("attribute_${sa}_drop_line") && respEvents.size()>i1){ def curr, prev Long currDate, prevDate String drop_time= gtSetStr("attribute_${sa}_drop_time") String drop_value= gtSetStr("attribute_${sa}_drop_value") tEvents.push(respEvents[iZ]) Integer i for(i=iZ; i Integer.parseInt(drop_time)){ //add first zero tEvents.push([(sDT): prevDate-1000L, (sVAL): Float.parseFloat(drop_value)]) tEvents.push([(sDT): currDate+1000L, (sVAL): Float.parseFloat(drop_value)]) } tEvents.push(curr) } respEvents=tEvents } resp[sid][attribute]=respEvents } } return JsonOutput.toJson(resp) } Map getOptions_linegraph(){ Boolean grpsz= gtSetB(sGRPHSTATICSZ) Map options=[ "graphReduction": gtSetI('graph_max_points'), "graphTimespan": Integer.parseInt(gtSetStr('graph_timespan')), "graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), "graphOptions": [ "width": grpsz ? graph_h_size : s100PCT, "height": grpsz ? graph_v_size: s100PCT, "chartArea": [ "width": grpsz ? graph_h_size : s80PCT, "height": grpsz ? graph_v_size: s80PCT ], "hAxis": ["textStyle": ["fontSize": graph_haxis_font, "color": gtSetB('graph_hh_color_transparent') ? sTRANSPRNT : graph_hh_color ], "gridlines": ["color": gtSetB('graph_ha_color_transparent') ? sTRANSPRNT : graph_ha_color, "count": graph_h_num_grid != sBLK ? graph_h_num_grid : null ], "format": gtSetStr('graph_h_format')==sBLK?sBLK:gtSetStr('graph_h_format') ], "vAxis": ["textStyle": ["fontSize": graph_vaxis_font, "color": gtSetB('graph_vh_color_transparent') ? sTRANSPRNT : graph_vh_color], "gridlines": ["color": gtSetB('graph_va_color_transparent') ? sTRANSPRNT : graph_va_color], ], "vAxes": [ 0: ["title" : graph_show_left_label ? graph_left_label: null, "titleTextStyle": ["color": gtSetB('graph_left_color_transparent') ? sTRANSPRNT : graph_left_color, "fontSize": graph_left_font], "viewWindow": ["min": graph_vaxis_1_min != sBLK ? graph_vaxis_1_min : null, "max": graph_vaxis_1_max != sBLK ? graph_vaxis_1_max : null], "gridlines": ["count" : graph_vaxis_1_num_lines != sBLK ? graph_vaxis_1_num_lines : null ], "minorGridlines": ["count" : 0] ], 1: ["title": graph_show_right_label ? graph_right_label : null, "titleTextStyle": ["color": gtSetB('graph_right_color_transparent') ? sTRANSPRNT : graph_right_color, "fontSize": graph_right_font], "viewWindow": ["min": graph_vaxis_2_min != sBLK ? graph_vaxis_2_min : null, "max": graph_vaxis_2_max != sBLK ? graph_vaxis_2_max : null], "gridlines": ["count" : graph_vaxis_2_num_lines != sBLK ? graph_vaxis_2_num_lines : null ], "minorGridlines": ["count" : 0] ] ], "legend": !gtSetB('graph_show_legend') ? ["position": sNONE] : ["position": graph_legend_position, "alignment": graph_legend_inside_position, "textStyle": ["fontSize": graph_legend_font, "color": gtSetB('graph_legend_color_transparent') ? sTRANSPRNT : graph_legend_color]], "backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), "curveType": !graph_smoothing ? sBLK : "function", "title": !gtSetB('graph_show_title') ? sBLK : gtSetStr('graph_title'), "titleTextStyle": !gtSetB('graph_show_title') ? sBLK : ["fontSize": graph_title_font, "color": gtSetB('graph_title_color_transparent') ? sTRANSPRNT : gtSetStr('graph_title_color')], "titlePosition" : gtSetB('graph_title_inside') ? "in" : "out", "interpolateNulls": true, //for null vals on our chart "orientation" : gtSetB('graph_y_orientation')? "vertical" : "horizontal", "reverseCategories" : graph_z_orientation, "series": [], ] ] // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() //add colors and thicknesses Integer axis=Integer.parseInt(settings["graph_axis_number_${sa}"].toString()) String tc= "graph_line_${sa}_color" String text_color=gtSetStr(tc) Boolean text_color_transparent=gtSetB(tc+"_transparent") Integer line_thickness=gtSetI("attribute_${sa}_line_size") Float opacity opacity=0.0 if(settings["attribute_${sa}_opacity"]){ opacity=settings["attribute_${sa}_opacity"]/100.0 } Map annotations=[ "targetAxisIndex": axis, "color": text_color_transparent ? sTRANSPRNT : text_color, "stroke": text_color_transparent ? sTRANSPRNT : "red", "lineWidth": line_thickness, "areaOpacity" : opacity ] options.graphOptions.series << annotations } } return options } String getDrawType_linegraph(){ switch (gtSetStr('graph_type')){ case "Line Graph": return "google.visualization.LineChart" case "Area Graph": return "google.visualization.AreaChart" case "Scatter Plot": return "google.visualization.ScatterChart" } return 'bad' } String getGraph_linegraph(){ String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" String html=""" ${scriptIncludes()}
""" return html } //oauth endpoints Map getSubscriptions_linegraph(){ List ids=[] Map sensors_=[:] Map attributes=[:] Map labels=[:] Map drop_=[:] Map non_num_=[:] Boolean isPoll isPoll=gtStB('hasFuel') // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String dn=sMs(ent,sDISPNM) //String typ=sMs(ent,sT).capitalize() String sa="${sid}_${attribute}".toString() String attr=attribute if(!ids.contains(sid)) ids << sid // sensor.idAsLong //only take what we need //Map sensors_fmt=gtSensorFmt() sensors_[sid]=[ (sID): sid /* , idAsLong: sensor.idAsLong */, displayName: dn ] attributes[sid]= attributes[sid] ?: [] attributes[sid] << attribute labels[sid]= labels[sid] ?: [:] labels[sid][attr]= gtSetStr("graph_name_override_${sa}") labels[sid][attr]= gtSetStr("graph_name_override_${sa}") drop_[sid]= drop_[sid] ?: [:] non_num_[sid]= non_num_[sid] ?: [:] if(gtSetB("attribute_${sa}_non_number")){ String startString=gtSetStr("attribute_${sa}_startString") String endString=gtSetStr("attribute_${sa}_endString") non_num_[sid][attr]=[ valid: true, (sSTART): startString, startVal: settings["attribute_${sa}_${startString}"], (sEND): endString, endVal: settings["attribute_${sa}_${endString}"] ] }else{ non_num_[sid][attr]=[ valid: false, (sSTART): sBLK, (sEND): sBLK] } drop_[sid][attr]=[valid: settings["attribute_${sa}_drop_line"], time: settings["attribute_${sa}_drop_time"], (sVAL): settings["attribute_${sa}_drop_value"]] } } Map subscriptions=[ (sID): isPoll ? sPOLL : sSENSOR, ids: ids, 'sensors': sensors_, 'attributes': attributes, labels : labels, drop : drop_, non_num: non_num_ ] return subscriptions } /* * TODO: Rangebar methods */ def mainRangebar(){ mainShare1("Choose Numeric Attribute Only",'graph_timespan') } def deviceRangebar(){ deviceShare1(true,true,false) } def attributeRangebar(){ List dataSources= createDataSources(true) //state.count_=0 dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ List container hubiForm_section("Graph Order", i1, "directions", sBLK){ hubiForm_list_reorder('graph_order', sBACKGRND, "#3e4475") } // TODO if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String rid=ent[sRID].toString() String attribute=sMs(ent,sA) String dn=sMs(ent,sDISPNM) String typ=sMs(ent,sT).capitalize() String hint= typ=='Fuel' ? " (Canister ${ent.c} Name ${ent.n})" : sBLK String sa="${sid}_${attribute}".toString() // Integer cnt=1 //state.count_++ hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "directions",sid+attribute){ container=[] if(typ==sCSENSOR){ if(isLtsAvailable(rid, attribute)){ container << hubiForm_sub_section("Long Term Storage in use") }else{ String tvar="var_"+sa+"_lts" app.updateSetting(tvar, false) settings[tvar]= false } } container << hubiForm_text_input("Override ${typ} Name
Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE
", "graph_name_override_${sa}", "%deviceName%: %attributeName%", false) container << hubiForm_color ("Bar Background", "attribute_${sa}_background","#5b626e", false, true) container << hubiForm_color ("Min/Max", "attribute_${sa}_minmax", "#607c91", false) container << hubiForm_color ("Current Value", "attribute_${sa}_current", "#8eb6d4", false) container << hubiForm_color ("Current Value Border", "attribute_${sa}_current_border", sWHT, false) container << hubiForm_switch ((sTIT): "Show Current Value on Bar?", (sNM): "attribute_${sa}_show_value", (sDEFLT): false, (sSUBONCHG): true) if(gtSetB("attribute_${sa}_show_value")){ container << hubiForm_text_input("Units", "attribute_${sa}_annotation_units", sBLK, false) } hubiForm_container(container, i1) } //cnt += i1 } } } } def graphRangebar(){ List timespanEnum1=[[0:"Live"], [1:"Hourly"], [2:"Daily"], [3:"Every Three Days"], [4:"Weekly"]] dynamicPage((sNM): "graphSetupPage"){ List container hubiForm_section("General Options", i1, sBLK, sBLK){ container=[] input( (sTYPE): sENUM, (sNM): 'graph_type',(sTIT): "Select graph type", (sMULTP): false, (sREQ): false, options: [[(s1): "Bar Chart"],[(s2): "Column Chart"]], (sDEFV): s1) inputGraphUpdateRate() input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "Select Time span to Graph (i.e How Often to Reset Range)", (sMULTP): false, (sREQ): false, options: timespanEnum1, (sDEFV): s2, (sSUBOC): true) container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) container << hubiForm_slider ((sTIT): "Graph Bar Width (1%-100%)", (sNM): "graph_bar_percent", (sDEFLT): i90, (sMIN): i1, (sMAX): i100, (sUNITS): "%") container << hubiForm_text_input("Graph Max", "graph_max", s100, false) container << hubiForm_text_input("Graph Min", "graph_min", s0, false) hubiForm_container(container, i1) } hubiForm_section("Axes", i1, sBLK, sBLK){ container=[] container << hubiForm_color ("Axis", "haxis", sBLACK, false) container << hubiForm_font_size ((sTIT): "Axis", (sNM): "haxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_slider ((sTIT): "Number of Pixels for Axis", (sNM): "graph_h_buffer", (sDEFLT): i40, (sMIN): i10, (sMAX): i500, (sUNITS): " pixels") hubiForm_container(container, i1) } hubiForm_section("Device Names", i1, sBLK, sBLK){ container=[] container << hubiForm_font_size ((sTIT): "Device Name", (sNM): "graph_axis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) container << hubiForm_color ("Device Name","graph_axis", sBLACK, false) container << hubiForm_slider ((sTIT): "Number of Pixels for Device Name Area", (sNM): "graph_v_buffer", (sDEFLT): i100, (sMIN): i10, (sMAX): i500, (sUNITS): " pixels") hubiForm_container(container, i1) } gatherGraphSize() hubiForm_section("Annotations", i1, sBLK, sBLK){ container=[] container << hubiForm_font_size ((sTIT): "Annotation", (sNM): "annotation", (sDEFLT): i16, (sMIN): i2, (sMAX): i40) container << hubiForm_switch ((sTIT): "Show Annotation Outside (true) or Inside (false) of Bars", (sNM): "annotation_inside", (sDEFLT): false) container << hubiForm_color ("Annotation", "annotation", sBLACK, false) container << hubiForm_color ("Annotation Aura", "annotation_aura", sWHT, false) container << hubiForm_switch ((sTIT): "Bold Annotation", (sNM): "annotation_bold", (sDEFLT): false) container << hubiForm_switch ((sTIT): "Italic Annotation", (sNM): "annotation_italic", (sDEFLT): false) hubiForm_container(container, i1) } } } String getData_rangebar(){ Map resp=[:] Date then then=new Date() switch (gtSetStr('graph_timespan')){ case s0: //"Live": break case s1: //"Hourly": use (TimeCategory){ then -= 1.hours } break case s2: //"Daily": then.setHours(0) then.setMinutes(0) then.setSeconds(0) break case "3": //"Every Three Days": use (TimeCategory){ then -= 2.days } then.setHours(0) then.setMinutes(0) then.setSeconds(0) break case "4": //"Weekly": use (TimeCategory){ then -= 6.days } then.setHours(0) then.setMinutes(0) then.setSeconds(0) break } //Long graph_time=then.getTime() // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) resp[sid]= resp[sid] ?: [:] List data=CgetData(ent, then) // log.warn "got sensor: $sensor attribute: $attribute data1: $data" //List data1=data.findAll{ (Long)it.date > graph_time} List temp=data.collect{ Map it -> getValue(sid,attribute,it[sVAL]) } //List temp=sensor.statesSince(attribute, then, [(sMAX): 1000]).collect{ it.getFloatValue() } Integer sz=data.size() //Float v= sensor.currentState(attribute).getFloatValue() Float v= "${data[sz-i1][sVAL]}".toFloat() if(temp.size() == iZ){ resp[sid][attribute]=[(sCUR): v, (sMIN): v, (sMAX): v] }else{ resp[sid][attribute]=[(sCUR): v, (sMIN): temp.min(), (sMAX): temp.max()] } } } return JsonOutput.toJson(resp) } Map getOptions_rangebar(){ List colors=[] // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String attrib_string="attribute_${sid}_${attribute}_color" String transparent_attrib_string= attrib_string+"_transparent" colors << (gtSetB(transparent_attrib_string) ? sTRANSPRNT : gtSetStr(attrib_string)) } } String axis1 String axis2 if(gtSetStr('graph_type') == s1){ axis1="hAxis" axis2="vAxis" }else{ axis1="vAxis" axis2="hAxis" } Map options=[ "graphTimespan": Integer.parseInt(gtSetStr('graph_timespan')), "graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), (sGRAPHT): Integer.parseInt(gtSetStr('graph_type')), "graphOptions": [ "bar" : [ "groupWidth" : "${settings.graph_bar_percent}%", ], "width": gtSetB(sGRPHSTATICSZ) ? settings.graph_h_size : s100PCT, "height": gtSetB(sGRPHSTATICSZ) ? settings.graph_v_size: "90%", "timeline": [ "rowLabelStyle": ["fontSize": settings.graph_axis_font, "color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : settings.graph_axis_color], "barLabelStyle": ["fontSize": settings.graph_axis_font] ], "backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), "isStacked": true, "chartArea": [ (sLEFT): gtSetStr('graph_type') == s1 ? settings.graph_v_buffer : settings.graph_h_buffer, (sRIGHT) : i10, "top": i10, "bottom": gtSetStr('graph_type') == s1 ? settings.graph_h_buffer : settings.graph_v_buffer ], "legend" : [ "position" : sNONE ], (axis1): [ "viewWindow" : ["max" : graph_max, "min" : graph_min], "minValue" : graph_min, "maxValue" : graph_max, "textStyle" : ["color": gtSetB('haxis_color_transparent') ? sTRANSPRNT : haxis_color, "fontSize": haxis_font] ], (axis2): [ "textStyle" : ["color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color, "fontSize": graph_axis_font] ], "annotations" : [ "alwaysOutside": true, "textStyle": [ "fontSize": annotation_font, "bold": annotation_bold, "italic": annotation_italic, "color": gtSetB('annotation_color_transparent') ? sTRANSPRNT : annotation_color, "auraColor":gtSetB('annotation_aura_color_transparent') ? sTRANSPRNT : annotation_aura_color, ], "stem": [ "color": sTRANSPRNT ], "highContrast": sFALSE ], ], "graphLow": graph_min, "graphHigh": graph_max, ] return options } String getGraph_rangebar(){ String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" String html=""" ${scriptIncludes()}
""" return html } //oauth endpoints Map getSubscriptions_rangebar(){ List _ids=[] Map _attributes=[:] Map labels=[:] Map colors=[:] Boolean isPoll isPoll=gtStB('hasFuel') // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() _ids << sid // sensor.id _attributes[sid]=[] labels[sid]=[:] colors[sid]=[:] _attributes[sid] << attribute labels[sid][attribute]= gtSetStr("graph_name_override_${sa}") colors[sid][attribute]=["backgroundColor": gtSetB("attribute_${sa}_background_color_transparent") ? sTRANSPRNT : gtSetStr("attribute_${sa}_background_color"), "minMaxColor": gtSetB("attribute_${sa}_minmax_color_transparent") ? sTRANSPRNT : settings["attribute_${sa}_minmax_color"], "currentValueColor": gtSetB("attribute_${sa}_current_color_transparent") ? sTRANSPRNT : settings["attribute_${sa}_current_color"], "currentValueBorderColor": gtSetB("attribute_${sa}_current_border_color_transparent") ? sTRANSPRNT : settings["attribute_${sa}_current_border_color"], "showAnnotation": settings["attribute_${sa}_show_value"], "annotation_font": settings["attribute_${sa}_annotation_font"], "annotation_units": settings["attribute_${sa}_annotation_units"], ] } } Map sensors_fmt=gtSensorFmt() List order=gtSetStr('graph_order') ? parseJson(gtSetStr('graph_order')) : [] Map subscriptions=[ (sID): isPoll ? sPOLL : sSENSOR, 'sensors': sensors_fmt, "ids": _ids, 'attributes': _attributes, "labels": labels, "colors": colors, "order": order ] return subscriptions } /* * TODO: Radar methods */ def tileRadar(){ List zoomEnum = [[3:"3"], [4: "4"], [5: "5"], [6: "6"], [7: "7"], [8: "8"], [9: "9"], [10: "10"]] List refreshEnum=[[60000:"1 minute"], [300000: "5 minutes"], [600000: "10 minutes"], [1200000: "20 minutes"], [1800000: "30 minutes"], [3600000: "1 hour"]] List> weatherMapEnum=[["radar" : "Current Radar"], ["temp" : "Temperature"], ["wind" : "Wind"], ["rain" : "Rain and Thunder"], ["rainAccu" : "Rain Accumulation"], ["snowAccu" : "Snow Accumulation"], ["snowcover": "Snow Ground Cover"]] List> forecastModelEnum =[["ecmwf": "European Centre for Medium-Range Weather Forecasts"], ["gfs": "Global Forecast System"]] List> hoursModelEnum=[["now" : "Current"], ["12" : "12 Hours"], ["24" : "24 Hours"]] List> measureEnum=[["in": "inches"], ["mm": "millimeters"]] /* List> windEnum=[["knot" : "Knots (k)"], [(sMETERSPS) : "Meters / Second (m/s)"], [(sKILOSPH) : "Kilometers / Hour (km/h)"], [(sMILESPH) : "Miles per Hour (mph)"]] */ List> tempEnum = [[(sFAHR): "Fahrenheit (°F)"], [(sCELS) : "Celsius (°C)"]] dynamicPage((sNM): "graphSetupPage"){ List container hubiForm_section("Tile Setup", i1, sBLK, sBLK){ container=[] container << hubiForm_text_input ("Latitude (Default=Hub location)", "latitude", location.latitude.toString(), false) container << hubiForm_text_input ("Longitude (Default=Hub location)", "longitude", location.longitude.toString(), false) hubiForm_container(container, i1) //if(!overlay) overlay="radar"; /* if(!settings.overlay){ app.updateSetting("overlay", [(sTYPE): sENUM, (sVAL): "radar"]) settings['overlay']='radar' app.updateSetting("refresh", [(sTYPE): sENUM, (sVAL): 600000]) settings['refresh']=600000 app.updateSetting("zoom", [(sTYPE): sENUM, (sVAL): 3]) settings['zoom']=i3 } */ input( (sTYPE): sENUM, (sNM): "zoom",(sTIT): "Zoom Amount", (sREQ): false, (sMULTP): false, options: zoomEnum, (sDEFV): 3, (sSUBOC): false) input( (sTYPE): sENUM, (sNM): "refresh",(sTIT): "Refresh Time", (sREQ): false, (sMULTP): false, options: refreshEnum, (sDEFV): 600000, (sSUBOC): false) input( (sTYPE): sENUM, (sNM): 'overlay',(sTIT): "Map Type", (sREQ): false, (sMULTP): false, options: weatherMapEnum, (sDEFV): "radar", (sSUBOC): true) if(gtSetStr('overlay') != "radar"){ container=[] container << hubiForm_text("""You have chosen a forecast map. Please note:
1. Forecast maps are update on the hour
2. "Current" is the current condition (within the last hour)
3. Refreshing these maps "more often" won't change anything""") hubiForm_container(container, i1) if(gtSetStr('product') == "radar") app.updateSetting('product', [(sTYPE): sENUM, (sVAL): "gfs"]) input( (sTYPE): sENUM, (sNM): 'product',(sTIT): "Forecast Model", (sREQ): false, (sMULTP): false, options: forecastModelEnum, (sDEFV): "gfs", (sSUBOC): false) input( (sTYPE): sENUM, (sNM): "calendar",(sTIT): "Display Time", (sREQ): false, (sMULTP): false, options: hoursModelEnum, (sDEFV): "now", (sSUBOC): false) }else{ app.updateSetting ('product', [(sTYPE): sENUM, (sVAL): "gfs"]) app.updateSetting ("calendar", [(sTYPE): sENUM, (sVAL): "now"]) } /* if(!gtSetStr('wind_units')){ app.updateSetting('wind_units', [(sTYPE): sENUM, (sVAL): sMILESPH]) settings['wind_units']=sMILESPH app.updateSetting('temp_units', [(sTYPE): sENUM, (sVAL): sFAHR]) settings['temp_units']=sFAHR app.updateSetting(sBACKGRND, [(sTYPE): sENUM, (sVAL): sBLACK]) settings['background']='#000000' app.updateSetting("background_opacity", [(sTYPE): sENUM, (sVAL): i90]) settings['background_opacity']=i90 } */ input( (sTYPE): sENUM, (sNM): 'wind_units',(sTIT): "Wind Speed Units", (sREQ): false, (sMULTP): false, options: unitWind /*windEnum*/, (sDEFV): sMILESPH, (sSUBOC): false) input( (sTYPE): sENUM, (sNM): 'temp_units',(sTIT): "Temperature Units", (sREQ): false, (sMULTP): false, options: tempEnum, (sDEFV): sFAHR, (sSUBOC): false) container=[] container << hubiForm_switch((sTIT): "Show Marker on Graph?", (sNM): "marker", (sDEFLT): false, (sSUBONCHG): false) container << hubiForm_color("Background", sBACKGRND, sBLACK, false) container << hubiForm_slider ((sTIT): "Background Opacity", (sNM): "background_opacity", (sDEFLT): i90, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) hubiForm_container(container, i1) } } } def mainRadar(){ dynamicPage((sNM): "mainPage"){ checkDup() List container if(!state.endpoint){ hubiForm_section("Please set up OAuth API", i1, "report", sBLK){ href( (sNM): "enableAPIPageLink",(sTIT): "Enable API", description: sBLK, page: "enableAPIPage") } }else{ hubiForm_section(tDesc()+" Graph Options", i1, "tune", sBLK){ container=[] container << hubiForm_page_button("Setup Tile", "graphSetupPage", s100PCT, "vibration") hubiForm_container(container, i1) } if(gtSetStr('wind_units')){ local_graph_url() preview_tile() } put_settings() } } } String getGraph_radar(){ //String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" String wind wind="kt" switch (gtSetStr('wind_units')){ case "knot" : wind="kt"; break case sMETERSPS : wind="m%2Fs"; break case sKILOSPH : wind="km%2Fh"; break case sMILESPH : wind="mph"; break } String temp temp="%C2%B0F" switch (gtSetStr('temp_units')){ case sFAHR: temp="%C2%B0F"; break case sCELS : temp="%C2%B0C" } String html="""
""" return html } //oauth endpoints /* * TODO: Weather2 methods */ def tileWeather2(){ dynamicPage((sNM): "graphSetupPage"){ hubiForm_section("General Options", i1, sBLK, sBLK){ input( (sTYPE): sENUM, (sNM): "openweather_refresh_rate",(sTIT): "Select OpenWeather Update Rate", (sMULTP): false, (sREQ): true, options: updateEnum, (sDEFV): "300000") List container=[] container << hubiForm_color("Background", sBACKGRND, sBLACK, false) container << hubiForm_slider ((sTIT): "Background Opacity", (sNM): "background_opacity", (sDEFLT): i90, (sMIN): iZ, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) container << hubiForm_switch ((sTIT): "Color Icons?", (sNM): "color_icons", (sDEFLT): false) hubiForm_container(container, i1) // List daysEnum=[[0: "Today"], [1: "Tomorrow"], [2: "2 Days from Now"], [3: "3 Days from Now"], [4: "4 Days from Now"], [5: "Five Days from Now"]] // input( (sTYPE): sENUM, (sNM): "day_num",(sTIT): "Day to Display", (sMULTP): false, (sREQ): false, options: daysEnum, (sDEFV): "1") } ((Map)state.unit_type).each{String key, Map measurement-> if(measurement.out != sNONE ){ hubiForm_section(sMs(measurement,sNM), i1, sBLK, sBLK){ //TODO bad?? List container=[] hubiForm_container(container, i1) input( (sTYPE): sENUM, (sNM): key+"_units",(sTIT): "Displayed Units", (sREQ): false, (sMULTP): false, options: measurement.enum, (sDEFV): measurement.out, (sSUBOC): false) } } } } } def deviceWeather2(){ List final_attrs dynamicPage((sNM): "deviceSelectionPage"){ List container hubiForm_section("Device Selection", i1, sBLK, sBLK){ container=[] container << hubiForm_switch((sTIT): "Make Hubitat Devices Available?", (sNM): "override_openweather", (sDEFLT): false, (sSUBONCHG): true) hubiForm_container(container, i1) } Map> measurement_list=[:] if(gtSetB('override_openweather')){ hubiForm_section("Sensor Selection", i1, sBLK, sBLK){ container=[] if(container)hubiForm_container(container, i1) input ('sensors', "capability.*",(sTIT): "Select Sensors", (sMULTP): true, (sREQ): false, (sSUBOC): true) } if(sensors){ final_attrs=[] Map> sensor_list=[:] for(sensor in (List)sensors){ String sid=gtSensorId(sensor) if(sid==sBLK){ error "deviceWeather2 null sid ${sensor}",null,iN2 continue } List attributes_=(List)sensor.getSupportedAttributes() sensor_list."${sid}"=[:] for(attribute_ in attributes_){ String name=attribute_.getName() def cv= sensor.currentState(name,true) if(cv){ String units=cv.getUnit() def value=cv.getValue() String dn=sensor.displayName sensor_list."${sid}"."${name}"=[ sensor_name: "${dn}", (sVAL): value, (sUNIT): units, supported_unit: getUnits(units, value)] final_attrs << [("${sid}.${name}".toString()) : "${dn} (${name}) ::: [${value} ${units ?: sBLK} ]"] } } } final_attrs=final_attrs.unique(false) ((Map)state.unit_type).each{String key, Map type-> measurement_list."${key}"=[:] if(type.out != sNONE){ hubiForm_section(sMs(type,sNM), i1, sBLK, sBLK){ container=[] input( (sTYPE): sENUM, (sNM): "${key}_devices",(sTIT): sMs(type,sNM), (sREQ): false, (sMULTP): true, options: final_attrs, (sDEFV): sBLK, (sSUBOC): true) if(settings["${key}_devices"]){ settings["${key}_devices"].each{ String iattr-> String attr attr=iattr String sensor_id="${attr}".tokenize('.')[iZ] if(!measurement_list."${key}"."${sensor_id}") measurement_list."${key}"."${sensor_id}"=[:] attr=attr.tokenize('.')[i1] String sensor_name=sensor_list."${sensor_id}"."${attr}".sensor_name if(((Map)state.unit_type)."${key}".enum == sNONE){ container << hubiForm_text(""+sensor_name+" :: "+attr+"") measurement_list."${key}"."${sensor_id}"."${attr}"=[sensor_name: sensor_list."${sensor_id}"."${attr}".sensor_name, in_units: sNONE ] }else if(sensor_list."${sensor_id}"."${attr}".supported_unit.var == key){ String units=sensor_list."${sensor_id}"."${attr}".supported_unit.name container << hubiForm_text(""+sensor_name+" :: "+attr+"
"+' '+" Units="+units) measurement_list."${key}"."${sensor_id}"."${attr}"=[sensor_name: sensor_list."${sensor_id}"."${attr}".sensor_name, in_units: sensor_list."${sensor_id}"."${attr}".supported_unit.units ] }else{ if(container)hubiForm_container(container, i1) String unit=sensor_list."${sensor_id}"."${attr}".unit List list=((Map)state.unit_type)."${key}".enum if(list[iZ].none != "None") input( (sTYPE): sENUM, (sNM): "${key}.${sensor_id}.${attr}", (sTIT): ""+sensor_name+" :: "+attr+"
Valid units not detected ("+unit+'); Expected "'+key+'" type
Please select measurement units below', (sREQ): false, (sMULTP): false, options: list, (sDEFV): sBLK, (sSUBOC): false) measurement_list."${key}"."${sensor_id}"."${attr}"=[sensor_name: sensor_list."${sensor_id}"."${attr}".sensor_name, in_units: settings["${key}.${sensor_id}.${attr}"] ] container=[] } } } if(container)hubiForm_container(container, i1) } } } } state.device_list=measurement_list }else{ //TODO clear out unused settings, sensors wremoveSetting('sensors') } } } @Field static final String sFAHR='fahrenheit' @Field static final String sCELS='celsius' @Field static final String sNONE='none' @Field static final String sYES='yes' @Field static final String sNO='no' @Field static final String sTEMP='temperature' @Field static final String sCUR='current' @Field List unitTemp = [[(sFAHR): "Fahrenheit (°F)"], [(sCELS) : "Celsius (°C)"], ["kelvin" : "Kelvin (K)"]] @Field List> unitWind = [[(sMETERSPS): "Meters per Second (m/s)"], [(sMILESPH): "Miles per Hour (mph)"], ["knots": "Knots (kn)"], [(sKILOSPH): "Kilometers per Hour (km/h)"]] @Field List unitDepth = [["millimeters": "Millimeters (mm)"], ["inches": """Inches (") """]] @Field List unitPressure= [["millibars": "Millibars (mbar)"], ["millimeters_mercury": "Millimeters of Mercury (mmHg)"], ["inches_mercury": "Inches of Mercury (inHg)"], ["hectopascal" : "Hectopascal (hPa)"]] @Field List unitDirection= [["degrees": "Degrees (°)"], ["radians" : "Radians (°)"], ["cardinal": "Cardinal (N, NE, E, SE, etc)"]] @Field List unitTrend = [["trend_numeric": "Numeric (° < 0, °=0, ° > 0)"], ["trend_text": "Text (° rising, ° steady, ° falling)"]] @Field List unitPercent = [["percent_numeric": "Numeric (0 to 100)"], ["percent_decimal": "Decimal (0.0 to 1.0)"]] @Field List unitTime = [["time_seconds" : "Seconds since 1970"], ["time_milliseconds" : "Milliseconds since 1970"], ["time_twelve" : "12 Hour (2:30 PM)"], ["time_two_four" : "24 Hour (14:30)"]] @Field List unitUVI= [["uvi" : "UV Index"]] @Field List unitDistance= [["miles": "Miles"]] @Field List unitBlank= [[(sNONE): "None"]] @Field List unitDayofWeek= [["short": "Short (Thu)"], ["long": "Long (Thursday)"]] @Field List unitText= [["plain": "Unformatted"], ["title": "Title Format"], ["lowercase": "Lowercase"], ["uppercase" : "Uppercase"]] @Field List unitIcon= [[(sICON): "Default Icon"]] @Field List> tileSetFLD= [ [ (sTIT): 'Forecast Weather Icon', (sVAR): "weather_icon", (sTYPE): "weather_icon", period:sCUR, (sVAL): sBLK, (sICON): "alert-circle", icon_loc: sCENTER, icon_space: sBLK, h: i6, w: i12, (sBLROW): i1, (sBLCOL): i13, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): sNONE, decimal: sNO, unit_space: sBLK, font: i40, font_weight: s100, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Current Weather', (sVAR): "description", (sTYPE): "weather_description", period:sCUR, (sVAL): iZ, (sICON): sNONE, icon_loc: sNONE, icon_space: sBLK, h: i4, w: i12, (sBLROW): i7, (sBLCOL): i13, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): iZ, lpad: iZ, rpad: iZ, (sUNIT): sNONE, decimal: sNO, unit_space: sBLK, font: i20, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Current Temperature', (sVAR): "current_temperature", (sTYPE): sTEMP, period:sCUR, (sICON): sNONE, icon_loc: sLEFT, icon_space: sBLK, h: i4, w: i12, (sBLROW): i1, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, font: i20, font_weight: "900", font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Feels Like', (sVAR): "feels_like", (sTYPE): "feels_like", period:sCUR, (sICON): "home-thermometer-outline", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i12, (sBLROW): i5, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: "Feels Like: ", (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, font: i7, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Forecast High', (sVAR): "forecast_high", (sTYPE): "temperature_max", period:"daily.0", (sICON): "arrow-up-thick", icon_loc: sLEFT, icon_space: sBLK, h: i4, w: i6, (sBLROW): i7, (sBLCOL): i7, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, font: i7, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Forecast Low', (sVAR): "forecast_low", (sTYPE): "temperature_min", period:"daily.0", (sICON): "arrow-down-thick", icon_loc: sLEFT, icon_space: sBLK, h: i4, w: i6, (sBLROW): i7, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, font: i6, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Precipitation Title', (sVAR): "precipitation_title", (sTYPE): "blank", period:sNONE, (sICON): "umbrella-outline", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): 11, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: "Precipitation", lpad: iZ, rpad: iZ, (sDECIMALS): i1, (sUNIT): unitDepth, decimal: sNO, unit_space: sBLK, font: i6, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Forecast Precipitation', (sVAR): "forecast_precipitation", (sTYPE): "rain", period:"daily.0", (sICON): "ruler", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): 15, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: sBLK, lpad: iZ, rpad: iZ, (sDECIMALS): i1, (sUNIT): unitDepth, decimal: sYES, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Forecast Percent Precipitation', (sVAR): "forecast_percent_precipitation", (sTYPE): "chance_precipitation", period:"daily.0", (sICON): "cloud-question", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): i13, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitPercent, decimal: sYES, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Current Precipitation', (sVAR): "current_precipitation", (sTYPE): "rain_past_hour", period:sCUR, (sICON): "calendar-today", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): 17, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitDepth, decimal: sYES, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Wind Title', (sVAR): "wind_title", (sTYPE): "blank", period:sNONE, (sICON): "weather-windy-variant", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): 11, (sBLCOL): i9, (sALIGNMENT): sCENTER, text: "Wind", (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): sNONE, decimal: sNO, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Wind Speed', (sVAR): "wind_speed", (sTYPE): "wind_speed", period:sCUR, (sICON): "tailwind", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): i13, (sBLCOL): i9, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitWind, decimal: sYES, unit_space: sSPC, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Wind Gust', (sVAR): "wind_gust", (sTYPE): "wind_gust", period:sCUR, (sICON): "weather-windy", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): 15, (sBLCOL): i9, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitWind, decimal: sYES, unit_space: sSPC, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Wind Direction', (sVAR): "wind_direction", (sTYPE): "wind_direction", period:sCUR, (sICON): "compass-outline", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): 17, (sBLCOL): i9, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitDirection, decimal: sNO, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Pressure Title', (sVAR): "pressure_title", (sTYPE): "blank", period:sCUR, (sICON): "gauge", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): 11, (sBLCOL): 17, (sALIGNMENT): sCENTER, text: "Pressure", (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): sNONE, decimal: sYES, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Current Pressure', (sVAR): "current_pressure", (sTYPE): "pressure", period:sCUR, (sICON): "thermostat", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i8, (sBLROW): i13, (sBLCOL): 17, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitPressure, decimal: sYES, unit_space: sSPC, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Humidity', (sVAR): "current_humidity", (sTYPE): "humidity", period:sCUR, (sICON): "water-percent", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i4, (sBLROW): i20, (sBLCOL): i1, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitPercent, decimal: sYES, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Current Dewpoint', (sVAR): "current_dewpoint", (sTYPE): "dew_point", period:sCUR, (sICON): "wave", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i4, (sBLROW): i20, (sBLCOL): 11, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, font: i4, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Sunrise', (sVAR): "sunrise", (sTYPE): "sunrise", period:sCUR, (sICON): "weather-sunset-up", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i5, (sBLROW): i20, (sBLCOL): 15, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitTime, decimal: sNO, unit_space: sBLK, font: i3, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], [ (sTIT): 'Sunset', (sVAR): "sunset", (sTYPE): "sunset", period:sCUR, (sICON): "weather-sunset-down", icon_loc: sLEFT, icon_space: sSPC, h: i2, w: i5, (sBLROW): i20, (sBLCOL): i20, (sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, lpad: iZ, rpad: iZ, (sUNIT): unitTime, decimal: sNO, unit_space: sBLK, font: i3, font_weight: s400, font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, ], ] @Field Map spanFLD= [ current: [(sTIT): "Current Measurements", num_time: iZ, time_units: sBLK], daily: [(sTIT): "Daily Forecast", num_time: i7, time_units: "day"], hourly: [(sTIT): "Hourly Forecast", num_time: 48, time_units: "hour"], blank: [(sTIT): "Blank Tile", num_time: iZ, time_units: sBLK], sensor: [(sTIT): "Device Measurement", num_time: iZ, time_units: sBLK], ] private static Map fill_temp_type(String name, String type, String ow, String in_units, String current, String hourly, String daily, String sensor){ return [(sNM): name, (sTYPE): type, ow: ow, in_units: in_units,(sCUR): current, hourly: hourly, daily: daily, sensor: sensor] } //@Field static Map span_typeFLD def mainWeather2(){ // state.tile_dimensions=[rows: 14, columns: 26] state.remove('span_type') state.remove('tile_dimensions') // one time initialization if(!state.tile_settings){ /* Map tmap spanFLD.each{ String key, Map item -> if(!tmap) tmap=[:] tmap += [(key): [:]+item] } span_typeFLD= tmap */ /* state.span_type=[ current: [(sTIT): "Current Measurements", num_time: iZ, time_units: sBLK], daily: [(sTIT): "Daily Forecast", num_time: i7, time_units: "day"], hourly: [(sTIT): "Hourly Forecast", num_time: 48, time_units: "hour"], blank: [(sTIT): "Blank Tile", num_time: iZ, time_units: sBLK], sensor: [(sTIT): "Device Measurement", num_time: iZ, time_units: sBLK], ] */ List list=[] tileSetFLD.each{Map item-> Map tmap1 tmap1=[:]+item /* item.each{ String key, item1 -> tmap1 += [(key): item1] } */ list << tmap1 } // This is the internal DB of current values and settings adjustments state.tile_settings=list } //else{ // this remaps internal variables to the source types - can change based on settings/overrides Map temp_type=[ weather_icon: fill_temp_type("Weather Icon",sICON,"weather.0.description",sNONE,sYES,sYES,sYES,sNO), weather_description: fill_temp_type("Weather Description", sTEXT,"weather.0.description", sNONE,sYES,sYES,sYES,sNO), feels_like: fill_temp_type("Feels Like", sTEMP,"feels_like",sFAHR, sYES,sYES,sNO,sNO), feels_like_morning: fill_temp_type("Morning Feels Like", sTEMP, "feels_like.morn", sFAHR, sNO, sNO, sYES, sNO), feels_like_day: fill_temp_type("Day Feels Like", sTEMP, "feels_like.day", sFAHR, sNO, sNO, sYES, sNO), feels_like_evening: fill_temp_type("Evening Feels Like", sTEMP, "feels_like.eve", sFAHR, sNO, sNO, sYES, sNO), feels_like_night: fill_temp_type("Night Feels Like", sTEMP, "feels_like.night", sFAHR, sNO, sNO, sYES, sNO), temperature: fill_temp_type("Temperature", sTEMP, "temp", sFAHR, sYES, sYES, sNO, sNO), temperature_max: fill_temp_type("Maximum Temperature", sTEMP, "temp.max", sFAHR, sNO, sNO, sYES, sNO), temperature_min: fill_temp_type("Minimum Temperature", sTEMP, "temp.min", sFAHR, sNO, sNO, sYES, sNO), temperature_morning: fill_temp_type("Morning Temperature", sTEMP, "temp.morn", sFAHR, sNO, sNO, sYES, sNO), temperature_day: fill_temp_type("Day Temperature", sTEMP, "temp.day", sFAHR, sNO, sNO, sYES, sNO), temperature_evening: fill_temp_type("Evening Temperature", sTEMP, "temp.eve", sFAHR, sNO, sNO, sYES, sNO), temperature_night: fill_temp_type("Night Temperature", sTEMP, "temp.night", sFAHR, sNO, sNO, sYES, sNO), humidity: fill_temp_type("Humidity", "percent", "humidity", "percent_numeric", sYES, sYES, sYES, sNO), dew_point: fill_temp_type("Dew Point",sTEMP, "dew_point", sFAHR, sYES, sYES, sYES, sNO), pressure: fill_temp_type("Pressure","pressure", "pressure", "millibars", sYES, sYES, sYES, sNO), uv_index: fill_temp_type("UV Index", "uvi", "uvi", "uvi", sYES, sNO, sYES, sNO), cloud_coverage: fill_temp_type("Cloud Coverage", "percent", "clouds", "percent_numeric", sYES, sNO, sYES, sNO), visibility: fill_temp_type("Visibility", "distance", "visibility", "miles", sYES, sNO, sYES, sNO), wind_speed: fill_temp_type("Wind Speed", "velocity", "wind_speed", sMILESPH,sYES, sYES, sYES, sNO), wind_gust: fill_temp_type("Wind Gust", "velocity", "wind_gust", sMILESPH, sYES, sYES, sYES, sNO), wind_direction: fill_temp_type("Wind Direction", "direction", "wind_deg", "degrees", sYES, sYES, sYES, sNO), rain_past_hour: fill_temp_type("Rain past Hour", "depth", "rain.1h", "millimeters", sYES, sYES, sNO, sNO), snow_past_hour: fill_temp_type("Snow past Hour", "depth", "snow.1h", "millimeters", sYES, sYES, sNO, sNO), rain: fill_temp_type("Rain", "depth", "rain", "millimeters", sNO, sNO, sYES, sNO), snow: fill_temp_type("Snow", "depth", "snow", "millimeters", sNO, sNO, sYES, sNO), precipitation: fill_temp_type("Precipitation", "depth", "precipitation", "millimeters", sNO, sNO, sYES, sNO), chance_precipitation: fill_temp_type("Chance of Precipitation", "percent", "pop", "percent_decimal", sYES, sYES, sYES, sNO), sunrise: fill_temp_type("Sunrise", sTIME, "sunrise", "time_seconds", sYES, sYES, sYES, sNO), sunset: fill_temp_type("Sunset", sTIME, "sunset", "time_seconds", sYES, sYES, sYES, sNO), hour: fill_temp_type("Hour", sTIME, "dt", "time_seconds", sNO, sYES, sNO, sNO), day: fill_temp_type("Day", "day", "dt", "time_seconds", sNO, sYES, sYES, sNO), blank: fill_temp_type("Blank Tile", "blank", sNONE, sNONE, sNO, sNO, sNO, sNO), time_stamp: fill_temp_type("Data Time Stamp", sTIME, "dt", "time_seconds", sYES, sNO, sNO, sNO) ] //atomicState.tile_type=temp_type Map temp_unit=[ temperature: [(sNM): "Temperature", (sENUM): unitTemp, out: sFAHR, parse_func: "formatNumericData"], percent: [(sNM): "Percentage", (sENUM): unitPercent, out: "percent_numeric", parse_func: "formatNumericData"], (sICON): [(sNM): "Weather Icons", (sENUM): unitIcon, out: sNONE, parse_func: "translateCondition"], pressure: [(sNM): "Pressure", (sENUM): unitPressure, out: "inches_mercury", parse_func: "formatNumericData"], velocity: [(sNM): "Velocity", (sENUM): unitWind, out: sMILESPH, parse_func: "formatNumericData"], time: [(sNM): "Time", (sENUM): unitTime, out: "time_twelve", parse_func: "formatNumericData"], depth: [(sNM): "Depth", (sENUM): unitDepth, out: "inches", parse_func: "formatNumericData"], direction: [(sNM): "Direction", (sENUM): unitDirection, out: "cardinal", parse_func: "formatNumericData"], uvi: [(sNM): "UV Index", (sENUM): unitUVI, out: "uvi", parse_func: "formatNumericData"], visibility: [(sNM): "Visibility", (sENUM): unitDistance, out: "visibility", parse_func: "formatNumericData"], blank: [(sNM): "Blank Tile", (sENUM): unitBlank, out: sNONE, parse_func: sNONE], day: [(sNM): "Day of Week", (sENUM): unitDayofWeek, out: "short", parse_func: "formatDayData"], text: [(sNM): "Text Description", (sENUM): unitText, out: "plain", parse_func: "formatTextData"], ] //atomicState.unit_type=temp_unit //Update the Output Types Map unitT= [:]+temp_unit // atomicState.unit_type temp_unit.each{String key, Map item-> if(settings["${key}_units"]){ unitT."${key}".out=settings["${key}_units"] } } state.unit_type=unitT //reset to OpenWeather Data Map temp=[:] + temp_type //atomicState.tile_type temp.wind_speed.in_units=sMILESPH temp.wind_gust.in_units=sMILESPH //atomicState.tile_type.each{key, item-> temp_type.each{String key, Map item-> if(sMs(item,sSENSOR) == sNO){ temp << [(key): item] } } state.tile_type=temp // } // Map temp=(Map)state.tile_type count=iZ ((Map>>)state.device_list).each{ String type, Map>var1-> if(var1 != [:]){ var1.each{String device, Map var2-> var2.each{String attr, Map var3-> temp."device_${device}_${attr}_${type}"=[(sNM): "${var3.sensor_name} :: ${attr} (${type})", (sTYPE): "${type}", ow: "device.${device}.${attr}", in_units: var3.in_units, (sCUR): sNO, hourly: sNO, daily: sNO, sensor: sYES] } } } } state.tile_type=temp TreeMap typeList= new TreeMap([:]) typeList.main_list=new TreeMap([:]) spanFLD.each{String span_key, Map span-> ((TreeMap)typeList.main_list).put(span_key, [(sNM): span_key.capitalize()]) typeList[span_key]= new TreeMap([:]) ((TreeMap)typeList[span_key]).measurement_list=new TreeMap([:]) ((Map)state.tile_type).each{String key, Map item-> if(item[span_key] == sYES) ((TreeMap)((TreeMap)typeList[span_key]).measurement_list) << [(key): [(sNM): sMs(item,sNM)]] } Integer cnt= iMs(span,'num_time') if(cnt > iZ){ ((TreeMap)typeList[span_key]).time_list=new TreeMap([:]) String time_units=sMs(span,'time_units') ((TreeMap)typeList[span_key]).title= time_units.capitalize()+"s to Display" Map a Integer i for(i=iZ; i container if(!state.endpoint){ hubiForm_section("Please set up OAuth API", i1, "report", sBLK){ href( (sNM): "enableAPIPageLink",(sTIT): "Enable API", description: sBLK, page: "enableAPIPage") } }else{ hubiForm_section("Tile Options", i1, "tune", sBLK){ container=[] container << hubiForm_page_button("Select Device/Data", "deviceSelectionPage", s100PCT, "vibration") container << hubiForm_page_button("Configure Tile", "graphSetupPage", s100PCT, sPOLL) hubiForm_container(container, i1) } if(day_num){ local_graph_url() hubiForm_section("Configure Tile - Desktop Only", i10, "settings", sBLK){ container=[] container << getPreviewWindow("tile_settings_HTML", "mainPage") hubiForm_container(container, i1) } install_tile() } put_settings() } } } def verifyDeviceCallback(response, data){ } String getPreviewWindow(String var, String page){ Map params=[ uri: getEndpointURL(), path: "graph/?access_token=${getEndpointSecret()}", requestContentType: "application/json", ] asynchttpGet(verifyDeviceCallback, params) if(!settings["$var"]){ wremoveSetting(var.toString()) } String html html=""" """ // // //
html+="""
""" return cleanHtml(html) } private static String getAbbrev(String unit){ switch (unit){ case sNONE: return sBLK case sFAHR: return "°" case sCELS: return "°" case "kelvin": return "K" case sMETERSPS: return "m/s" case sMILESPH: return "mph" case "knots": return "kn" case "millimeters": return "mm" case "inches": return '"' case "degrees": return "°" case "radians": return "rad" case "cardinal": return sBLK case "trend_numeric": return sBLK case "trend_text": return sBLK case "percent_numeric": return "%" case "millibars": return "mbar" case "millimeters_mercury": return "mmHg" case "inches_mercury": return "inHg" case "hectopascal": return "hPa" case sKILOSPH : return "km/h" } return sBLK } private Map getUnits(String unit, ival){ if(unit == null) return [(sNM): "unknown", (sVAR): "tbd", (sUNITS): sNONE] try{ switch (unit.toLowerCase()){ case "f": case "°f": return [(sNM): "Fahrenheit (°F)", (sVAR): sTEMP, (sUNITS): sFAHR] case "c": case "°c": return [(sNM): "Celsius (°C)", (sVAR): sTEMP, (sUNITS): sCELS] case "mph": return [(sNM): "Miles per Hour (mph)", (sVAR): "velocity", (sUNITS): sMILESPH] case "m/s": return [(sNM): "Meters per Second (m/s)", (sVAR): "velocity", (sUNITS): sMETERSPS] case "in": case '"': return [(sNM): 'Inches (")', (sVAR): "depth", (sUNITS): "inches"] case "mm": // case '"': return [(sNM): 'Millimeters (mm)', (sVAR): "depth", (sUNITS): "millimeters"] case "°": case "deg": return [(sNM): "Degrees (°)", (sVAR): "direction", (sUNITS): "degrees"] case "rad": return [(sNM): "Radians (°)", (sVAR): "direction", (sUNITS): "radians"] case "inhg": return [(sNM): "Inches of Mercury (inHg)", (sVAR): "pressure", (sUNITS): "inches_mercury"] case "mmhg": return [(sNM): "Millimeters of Mercury mmHg)", (sVAR): "pressure", (sUNITS): "millimeters_mercury"] case "mbar": return [(sNM): "Millibars (mbar)", (sVAR): "pressure", (sUNITS): "millibars"] case "km/h": return [(sNM): "Kilometers per hour (km/h)", (sVAR): "velocity", (sUNITS): sKILOSPH] case "hPa": return [(sNM): "Hectopascal (hPa)", (sVAR):"pressure", (sUNITS): "hectopascal"] case "%": Double value=Double.parseDouble("${ival}") if(value > 1.0 && value < 100.0){ return [(sNM): "Percent (0 to 100)", (sVAR):"percent", (sUNITS): "percent_numeric"] }else if(value >=0.0 && value < 1.0){ return [(sNM): "Percent (0.1 to 1.0)", (sVAR): "percent", (sUNITS): "percent_decimal"] } default: break } }catch(ex){ error("Unable to find (sUNITS): $unit",null,iN2,ex) } return [(sNM): "unknown", (sVAR): "tbd", (sUNITS): sNONE] } static List getIconList(){ return [ [(sNM): "None", (sICON): "alpha-x-circle-outline"], [(sNM): "Cloudy", (sICON): "weather-cloudy"], [(sNM): "Cloudy Alert", (sICON): "weather-cloudy-alert"], [(sNM): "Cloudy Right Arrow", (sICON): "weather-cloudy-arrow-right"], [(sNM): "Fog", (sICON): "weather-fog"], [(sNM): "Hail", (sICON): "weather-hail"], [(sNM): "Hazy", (sICON): "weather-hazy"], [(sNM): "Hurricane", (sICON): "weather-hurricane"], [(sNM): "Lightning", (sICON): "weather-lightning"], [(sNM): "Lightning Raining", (sICON): "weather-lightning-rainy"], [(sNM): "Night", (sICON): "weather-night"], [(sNM): "Night Partly Cloudy", (sICON): "weather-night-partly-cloudy"], [(sNM): "Partly Cloudy", (sICON): "weather-partly-cloudy"], [(sNM): "Partly Lightning", (sICON): "weather-partly-lightning"], [(sNM): "Partly Raining", (sICON): "weather-partly-rainy"], [(sNM): "Partly Snowing", (sICON): "weather-partly-snowy"], [(sNM): "Partly Snowing Raining",icon: "weather-partly-snowy-rainy"], [(sNM): "Pouring", (sICON): "weather-pouring"], [(sNM): "Raining", (sICON): "weather-rainy"], [(sNM): "Snowing", (sICON): "weather-snowy"], [(sNM): "Heavy Snow", (sICON): "weather-snowy-heavy"], [(sNM): "Snowing Raining", (sICON): "weather-snowy-rainy"], [(sNM): "Sunny", (sICON): "weather-sunny"], [(sNM): "Sunny Alert", (sICON): "weather-sunny-alert"], [(sNM): "Sunny Off", (sICON): "weather-sunny-off"], [(sNM): "Sunset", (sICON): "weather-sunset"], [(sNM): "Sunset Down", (sICON): "weather-sunset-down"], [(sNM): "Sunset Up", (sICON): "weather-sunset-up"], [(sNM): "Tornado", (sICON): "weather-tornado"], [(sNM): "Windy", (sICON): "weather-windy"], [(sNM): "Windy 2", (sICON): "weather-windy-variant"], [(sNM): "Home Thermometer", (sICON): "home-thermometer-outline"], [(sNM): "Arrow Up", (sICON): "arrow-up-thick"], [(sNM): "Arrow Down", (sICON): "arrow-down-thick"], [(sNM): "Umbrella", (sICON): "umbrella-outline"], [(sNM): "Ruler", (sICON): "ruler"], [(sNM): "Cloud Question", (sICON): "cloud-question"], [(sNM): "Calendar", (sICON): "calendar-today"], [(sNM): "Tail Wind", (sICON): "tailwind"], [(sNM): "Compass", (sICON): "compass-outline"], [(sNM): "Gauge", (sICON): "gauge"], [(sNM): "Thermostat", (sICON): "thermostat"], [(sNM): "Water Percent", (sICON): "water-percent"], [(sNM): "Wave", (sICON): "wave"], [(sNM): "Snow", (sICON): "snowflake"], [(sNM): "Water", (sICON): "water"],] } Map getOptions_weather2(){ Map options=[ "tile_units": state.unit_type, "openweather_refresh_rate": openweather_refresh_rate ? openweather_refresh_rate : "300000", "tiles" : (List)state.tile_settings, "tile_type" : (Map)state.tile_type, "new_tile_dialog" : state.newTileDialog, "api_code" : getEndpointSecret(), "url" : getEndpointURL(), ] options.out_units=[:] ((Map)state.unit_type).each{String key, Map measurement-> options.out_units << [ (key) : settings["${key}_units"]] } return options } def getMapData(Map map, String loc){ List splt=loc.tokenize('.') def cur cur=map splt.each{String str-> try{ if(str.isNumber()){ Integer num=str.toInteger() cur=cur!=null ? cur[num] : null }else{ cur=cur!=null ? cur[str] : null } }catch(e){ log.debug(loc+": Cannot find data: "+e) return null } } return cur } static String applyDecimals(Map tile, val){ String value value=val.toString() if(value.isNumber()){ def num_decimals=tile[sDECIMALS] value=sprintf("%.${num_decimals}f", value.toFloat()) return value } else return value } static String getWindDirection(idirection){ Double direction direction=Double.parseDouble(idirection.toString()) List bearings = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] Integer bearing = Math.floor( ( (direction + 360.0D + 11.25D).toInteger() % 360 ).toDouble() / 22.5D).toInteger() return bearings[bearing] } def applyConversion(Map tile, ival){ def val; val=ival Map tile_type String out_units, in_units out_units=sBLK in_units=sBLK String sUNS='UNSUPPORTED' try{ tile_type=((Map)state.tile_type)."${tile.type}" out_units=((Map)state.unit_type)."${tile_type.type}".out in_units=tile_type.in_units }catch(ignored){ log.debug("Unable to find units for ${tile.title}:: Input units="+in_units+" Output units="+out_units) return sUNS } if(in_units != out_units && out_units != sNONE) switch (in_units){ //Temperature case sCELS: switch (out_units){ case sFAHR: val=(val * 9 / 5) + 32; break case "kelvin": val=val + 273.15; break default: val=sUNS } break case sFAHR: switch (out_units){ case sCELS: val=(val - 32.0) * (5 / 9); break case "kelvin": val=((val - 32) * (5 / 9)) + 273.15; break default: val=sUNS } break case "kelvin": switch (out_units){ case sFAHR: val=((val - 273.15) * (9 / 5)) + 32; break case sCELS: val=(val - 273.15); break default: val=sUNS } break //Precipitation case "millimeters": if(out_units == "inches"){ val=(val / 25.4) }else val=sUNS break case "inches": if(out_units == "millimeters"){ val=(val * 25.4) }else val=sUNS break //Velocity case sMETERSPS: switch (out_units){ case sMILESPH: val=(val * 2.237); break case "knots": val=(val * 1.944); break case sKILOSPH: val=(val * 3.6); break default: val=sUNS } break case sMILESPH: switch (out_units){ case sMETERSPS: val=(val / 2.237); break case "knots": val=(val / 1.151); break case sKILOSPH: val=(val * 1.609); break default: val=sUNS } break case "knots": switch (out_units){ case sMILESPH: val=(val * 1.151); break case sMETERSPS: val=(val / 1.944); break case sKILOSPH: val=(val * 1.852); break default: val=sUNS } break case sKILOSPH: switch (out_units){ case sMILESPH: val=(val / 1.609); break case sMETERSPS: val=(val / 3.6); break case "knots": val=(val / 1.852); break default: val=sUNS } break //Pressure case "hectopascal": case "millibars": switch (out_units){ case "inches_mercury": val=(val / 33.864); break case "millimeters_mercury": val=(val / 1.333); break case "hectopascal": break default: val=sUNS } break case "inches_mercury": switch (out_units){ case "hectopascal": case "millibars": val=(val * 33.864); break case "inches_mercury": val=(val / 25.4); break default: val=sUNS } break case "millimeters_mercury": switch (out_units){ case "hectopascal": case "millibars": val=(val * 1.333); break case "millimeters_mercury": val=(val * 25.4); break default: val=sUNS } break case "degrees": switch (out_units){ case "cardinal": val=getWindDirection(val) break case "radians": val=(val / 180.0) * 3.1415926535; break default: val=sUNS } break case "radians": switch (out_units){ case "cardinal": val=getWindDirection(( (val * 180) / 3.1415926535) ) break case "degrees": val=((val * 180) / 3.1415926535); break default: val=sUNS } break case "cardinal": switch (val){ case "N": val=0; break case "NNE": val=22.5; break case "NE": val=45; break case "ENE": val=67.5; break case "E": val=90; break case "ESE": val=112.5; break case "SE": val=135; break case "SSE": val=157.5; break case "S": val=180; break case "SSW": val=202.5; break case "SW": val=225; break case "WSW": val=247.5; break case "W":val=270; break case "WNW": val=292.5; break case "NW": val=315; break case "NNW": val=337.5; break default: val=sUNS } if(val != sUNS){ switch (out_units){ case "radians": val=((val / 180 ) * 3.1415926535) ; break case "degrees": val=val; break default: val=sUNS } } break //TEXT CONVERSIONS case "time_seconds": Long v=val*1000L Date d=new Date(v) switch (out_units){ case "time_twelve": SimpleDateFormat simpDate simpDate=new SimpleDateFormat("h:mm") val=simpDate.format(d) break case "time_two_four": SimpleDateFormat simpDate simpDate=new SimpleDateFormat("HH:mm") val=simpDate.format(d) break default: val=sUNS } break case "time_milliseconds": Date d=new Date(val as Long) switch (out_units){ case "time_twelve": val=d.getTimeString() break case "time_two_four": val=d.getTimeString() break default: val=sUNS } break case "percent_numeric": if(out_units == "percent_decimal") val=val / 100.0 else val=sUNS break case "percent_decimal": if(out_units == "percent_numeric") val=val * 100.0 else val=sUNS break } return val } @Field final List>pairingsFLD=[ [(sNM): "thunderstorm with light rain", (sICON): "weather-lightning-rainy"], [(sNM): "thunderstorm with rain", (sICON): "weather-lightning-rainy"], [(sNM): "thunderstorm with heavy rain", (sICON): "weather-lightning-rainy"], [(sNM): "light thunderstorm", (sICON): "weather-lightning"], [(sNM): "thunderstorm", (sICON): "weather-lightning"], [(sNM): "heavy thunderstorm", (sICON): "weather-lightning"], [(sNM): "ragged thunderstorm", (sICON): "weather-lightning"], [(sNM): "thunderstorm with light drizzle", (sICON): "weather-lightning-rainy"], [(sNM): "thunderstorm with drizzle", (sICON): "weather-lightning-rainy"], [(sNM): "thunderstorm with heavy drizzle", (sICON): "weather-lightning-rainy"], [(sNM): "light intensity drizzle", (sICON): "weather-partly-rainy"], [(sNM): "drizzle", (sICON): "weather-partly-rainy"], [(sNM): "heavy intensity drizzle", (sICON): "weather-partly-rainy"], [(sNM): "light intensity drizzle rain", (sICON): "weather-partly-rainy"], [(sNM): "drizzle rain", (sICON): "weather-partly-rainy"], [(sNM): "heavy intensity drizzle rain", (sICON): "weather-rainy"], [(sNM): "shower rain and drizzle", (sICON): "weather-rainy"], [(sNM): "heavy shower rain and drizzle", (sICON): "weather-pouring"], [(sNM): "shower drizzle", (sICON): "weather-rainy"], [(sNM): "light rain", (sICON): "weather-rainy"], [(sNM): "moderate rain", (sICON): "weather-pouring"], [(sNM): "heavy intensity rain", (sICON): "weather-pouring"], [(sNM): "very heavy rain", (sICON): "weather-pouring"], [(sNM): "extreme rain", (sICON): "weather-pouring"], [(sNM): "freezing rain", (sICON): "weather-snowy-rainy"], [(sNM): "light intensity shower rain", (sICON): "weather-rainy"], [(sNM): "shower rain", (sICON): "weather-rainy"], [(sNM): "heavy intensity shower rain", (sICON): "weather-pouring"], [(sNM): "ragged shower rain", (sICON): "weather-partly-rainy"], [(sNM): "light snow", (sICON): "weather-snowy"], [(sNM): "snow", (sICON): "weather-snowy"], [(sNM): "heavy snow", (sICON): "weather-snowy-heavy"], [(sNM): "sleet", (sICON): "weather-hail"], [(sNM): "light shower sleet", (sICON): "weather-hail"], [(sNM): "shower sleet", (sICON): "weather-hail"], [(sNM): "light rain and snow", (sICON): "weather-snowy-rainy"], [(sNM): "rain and snow", (sICON): "weather-snowy-rainy"], [(sNM): "light shower snow", (sICON): "weather-partly-snowy"], [(sNM): "shower snow", (sICON): "weather-partly-snowy"], [(sNM): "heavy shower snow", (sICON): "weather-partly-snowy"], [(sNM): "mist", (sICON): "weather-fog"], [(sNM): "smoke", (sICON): "weather-fog"], [(sNM): "haze", (sICON): "weather-hazy"], [(sNM): "sand dust whirls", (sICON): "weather-tornado"], [(sNM): "fog", (sICON): "weather-fog"], [(sNM): "sand", (sICON): "weather-fog"], [(sNM): "dust", (sICON): "weather-fog"], [(sNM): "volcanic ash", (sICON): "weather-fog"], [(sNM): "squalls", (sICON): "weather-tornado"], [(sNM): "tornado", (sICON): "weather-tornado"], [(sNM): "clear sky night", (sICON): "weather-night"], [(sNM): "clear sky", (sICON): "weather-sunny"], [(sNM): "few clouds night", (sICON): "weather-night-partly-cloudy"], [(sNM): "few clouds", (sICON): "weather-partly-cloudy"], [(sNM): "scattered clouds night", (sICON): "weather-night-partly-cloudy"], [(sNM): "scattered clouds", (sICON): "weather-partly-cloudy"], [(sNM): "broken clouds", (sICON): "weather-cloudy"], [(sNM): "overcast clouds", (sICON): "weather-cloudy"] ] List translateCondition(Map tile, String condition){ //String icon="mdi-weather-sunny-off" List return_val try{ Date now now=new Date() String period=sMs(tile,'period') List timeframe=period.split("\\.") Boolean round_hour round_hour=false if(timeframe[iZ] == "hourly"){ round_hour=true Integer num_hours=timeframe[i1].toInteger() use( TimeCategory ){ now=now + num_hours.hours } } String check_condition check_condition=condition if(isNight(now, round_hour)){ check_condition+=" night" } return [sICON, pairingsFLD.find{Map el-> sMs(el,sNM) == check_condition}[sICON]] }catch(ignored){} try{ return [sICON, pairingsFLD.find{Map el-> sMs(el,sNM) == condition}[sICON]] }catch(ignored){} return_val=[sICON, "alert-circle"] return return_val } List formatNumericData(Map tile, ival){ def val val=ival if(val == null) val=0 return [(sVAL), applyDecimals(tile, applyConversion(tile, val))] } static Float getMinHour(Date date){ return (date.getHours())+(date.getMinutes()/60.0) as Float } Boolean isNight(Date date, Boolean round_hour){ Float sunrise=getMinHour((Date)location.sunrise) Float sunset=getMinHour((Date)location.sunset) Float now=round_hour ? date.getHours().toFloat() : getMinHour(date) //Calendar cal=Calendar.getInstance() return now < sunrise || now > sunset } /* List formatHourData(Map tile, val){ Long val_micro=val*1000L Date date=new Date(val_micro) switch (settings["time_units"]){ case "time_seconds" : return [sVAL, val] case "time_milliseconds" : return [sVAL, val_micro] case "time_twelve" : return [sVAL, date.format('h:mm a', mTZ())] case "time_two_four" : return [sVAL, date.format('HH:mm', mTZ())] } return [sVAL, "XXXX"] } */ List formatDayData(Map tile, val){ Long val_micro=val*1000L Date date=new Date (val_micro) String day if(settings["day_units"] == "short") day=date.format('E', mTZ()) else day=date.format('EEEE', mTZ()) return [sVAL, day] } List formatTextData(Map tile, String val){ switch (settings["text_units"]){ case "plain": return [sVAL, val] case "lowercase": return [sVAL, val.toLowerCase()] case "uppercase": return [sVAL, val.toUpperCase()] case "title": return [sVAL, val.split(sSPC).collect{ String it ->it.capitalize()}.join(sSPC)] } return [sVAL, val] } /* List formatConditionText(Map tile, String val){ return ["value", val.split(sSPC).collect{it.capitalize()}.join(sSPC)] } List formatTitle(Map tile, String val){ return["value", sBLK] } List formatPressure(Map tile, val){ return ["value", "Pressure Trend"] } List formatDewPoint(Map tile, val){ // TODO does not deal with C def dewPoint=val String text text=sBLK if(dewPoint < 50) text="DRY" else if(dewPoint < 55) text= "NORMAL" else if(dewPoint < 60) text= "OPTIMAL" else if(dewPoint < 65) text= "STICKY" else if(dewPoint < 70) text= "MOIST" else if(dewPoint < 75) text= "WET" else text "MISERABLE" return ["value", text] } */ def getSensorData1(String measurement){ Long device_id=(measurement.tokenize('.')[i1]).toLong() String attribute=measurement.tokenize('.')[i2] def sensor=sensors.find{ it.id == device_id } return sensor.currentState(attribute,true).getValue() } void buildWeatherData(){ if(isEric())debug "buildWeatherData",null //def selections=settings["tile_settings"] Map data= (Map)parent.getWData() //log.debug "buildWeatherData got ${data.size()}" List temp=(List)state.tile_settings temp.eachWithIndex{Map tile, index-> def val, rain_val, snow_val val=null rain_val=null snow_val=null String period, measurement period=sBLK measurement=sBLK try{ period=tile.period measurement=state.tile_type."${tile.type}".ow if(period == sSENSOR){ val=getSensorData1(measurement) }else if(measurement == "precipitation"){ rain_val=getMapData(data, period+".rain") snow_val=getMapData(data, period+".snow") //Special Case if(rain_val == null) rain_val=0 if(snow_val == null) snow_val=0 if(snow_val > rain_val){ tile.icon="snowflake" }else{ tile.icon="water" } val=rain_val + snow_val }else if(period != sNONE && measurement != sNONE){ val=getMapData(data, period+"."+measurement) //log.debug "getMapData ${period}.${measurement} val $val" } }catch(ignored){ log.debug(sMs(tile,sNM)+": Unable to get data: "+period+", "+measurement) } String unit_type=state.tile_type."${tile.type}".type String parse_func=((Map)state.unit_type)."${unit_type}".parse_func if(parse_func!=sNONE){ try{ List returnVal="${parse_func}"(tile, val) tile."${returnVal[iZ]}"=returnVal[i1] //log.debug "parse_func: ${parse_func} tile $returnVal" }catch(ex){ log.debug(val+sSPC+unit_type+sSPC+parse_func+"::: Issue executing parse function: $parse_func " + ex) } }else{ tile[sVAL]=sBLK } } state.tile_settings=temp } String getTileHTML(Map item, Boolean locked){ String var=sMs(item,sVAR) BigDecimal fontScale=4.6 BigDecimal lineScale=0.85 BigDecimal iconScale=3.5 //def header=0.1 Integer height=iMs(item,'h') String html; html=sBLK String tile_locked=locked ? sFALSE : sTRUE String background=getRGBA(sMs(item,'background_color'), (Float.parseFloat(item.background_opacity.toString()))) String font=getRGBA(sMs(item,'font_color'), Float.parseFloat(item.font_opacity.toString())) if(bIs(item,'display')){ html += """
${item.title}
""" //Compute Icon and other spacing //Left Icon if(item.icon_loc != sRIGHT){ item.icon_space=item.icon_space ?: sBLK html+="""${item.icon_space}""" } //Text if(item.text == "null" || item.text == null) item.text=sBLK html+="""${item.text}""" //Main Content html += """${item[sVAL]}""" String tile_type String out_units String units //Units try{ tile_type=state.tile_type."${item.type}".type out_units=state.unit_type."${tile_type}".out units=getAbbrev(out_units) }catch(ignored){ units=sBLK } if(units == "unknown") units=sBLK //Unit Spacing html += """${sMs(item,'unit_space')}""" html += """${units}""" //Right Icon if(item.icon_loc == sRIGHT){ html+="""${item.icon_space}""" html+="""""" } html += """
""" } return html } /* def getDrawType(){ return "google.visualization.LineChart" } static String removeLastChar(String str){ str.subSequence(0, str.length() - 1) str } */ // weather2 String defineHTML_Header(){ String html=""" """ return html } static String addColorPicker(Map map){ String var=sMs(map,sVAR) String title=sMs(map,sTIT) String html="""
100%
""" return html } static String addButtonMenu(Map map){ String button_var=sMs(map,'var_name') def default_val=map.default_value String default_icon=sMs(map,'default_icon') Listitem_list=(List)map.list String tooltip=sMs(map,'tooltip') ?: sBLK String side=sMs(map,'side') ?: sLEFT String html html="""
${tooltip}
    """ item_list.each{Map item-> Integer weight=item.font_weight ? iMs(item,'font_weight') : 400 String nm=sMs(item,sNM).toLowerCase() html += """
  • ${item.text ? item.text : sMs(item,sNM)}
  • """ } html += """
""" html += """ """ return html } /* static String addMenu(Map map){ String button_var=map.var_name def default_val=map.default_value String default_icon=map.default_icon List item_list=map.list String tooltip=map.tooltip ? map.tooltip : sBLK String title=map[sTIT] String html="""
${tooltip}
    """ item_list.each{item-> Integer weight=item.font_weight ? item.font_weight : 400 html += """
  • ${item.text ? item.text : item.name}
  • """ } html += """
""" html += """ """ return html } */ static String addIconMenu(Map map){ String button_var=sMs(map,'var_name') def default_val=map.default_value String default_icon=sMs(map,'default_icon') List item_list=(List)map.list String tooltip tooltip=sMs(map,'tooltip') ?: sBLK Boolean description=map.description ? map.description : false Integer width= iMs(map,'width') if(sMs(map,'tooltip') == "Use Icon Name") tooltip="No Icon Selected" String html html="""
""" if(description) html += """ Icon (None) """ html += """
${tooltip}
    """ Integer count count=iZ item_list.each{Map item-> if(count % width == iZ){ html+="""
    """ } Integer weight=item.font_weight ? iMs(item,'font_weight') : 400 String icon_var=sMs(item,sICON).replaceAll("-","_") html += """
  • ${sMs(item,sNM)}
  • """ if(count % width == width-i1){ html+= """
    """ } count++ } html += """
""" html += """ """ return html } static String addSlider(Map map){ String var=sMs(map,sVAR) String title=sMs(map,sTIT) Integer min=iMs(map,sMIN) Integer max=iMs(map,sMAX) Integer value=iMs(map,sVAL) String html="""
0%
""" return html } static String defineHTML_CSS(){ String html=""" """ return html } static String defineSelectBox(Map map){ String title=map[sTIT] String var=sMs(map,sVAR) Map list=(Map)map.list String visible=map.visible == false ? """style="display: none;" """ : sBLK String function=map.function String html html="""
""" return html } String defineNewTileDialog(){ TreeMap typeList=(TreeMap)state.newTileDialog String html html=sBLK html += """
New Tile Options
""" html += """ """ html += """ """ html+= """
""" html += """
""" return html } static String defineTileDialog(){ /* List list=[] for(Map item in (List)state.tile_settings){ list << [(sNM): item[sTIT], (sICON): item[sICON], (sVAR): item.var] } */ String html html="""
Options
Delete this tile
Place New Tile
Save/Close
Save Colors and Opacity to All Tiles
Exit/Don't Save
Refresh View
This may take some time, depending on the number of tiles
""" //ALIGNMENT html+= """
""" html += addIconMenu(var_name: "selected_icon",(sTIT): "Select Tile Type", default_icon: "alpha-x-circle-outline", default_value: sCENTER, tooltip: "Use Icon Name", list: getIconList(), width: 4) html += """
""" html+= addButtonMenu(var_name: "horizontal_alignment", default_icon: "format-align-center", tooltip: "Horizontal Alignment", default_value: sCENTER, side: sLEFT, list:[[(sNM): "Left", (sICON): "format-align-left"], [(sNM): "Center", (sICON): "format-align-center"], [(sNM): "Right", (sICON): "format-align-right"]]) html+= """
""" html+= addButtonMenu(var_name: "icon_spacing", default_icon: "keyboard-space", tooltip: "Icon Spacing", default_value: "Single Space", side: sLEFT, list:[[(sNM): "No Space", (sICON): "arrow-collapse-horizontal"], [(sNM): "Single Space", (sICON): "keyboard-space"], [(sNM): "Double Space", (sICON): "arrow-expand-horizontal"]]) html+= """
""" html+= addButtonMenu(var_name: "decimal_places", default_icon: "decimal", tooltip: "Decimal Places", default_value: "One Decimal", side: sLEFT, list:[[(sNM): "No Decimal", (sICON): "hexadecimal"], [(sNM): "One Decimal", (sICON): "surround-sound-2-0"], [(sNM): "Two Decimals", (sICON): "decimal"]]) html += """
""" html+= addButtonMenu(var_name: 'font_weight', default_icon: "numeric-4-circle", default_value: sCENTER, tooltip: "Font Weight", side: sRIGHT, list:[[(sNM): "Thin", (sICON): "numeric-1-circle"], [(sNM): "Normal", (sICON): "numeric-4-circle"], [(sNM): "Bold", (sICON): "numeric-7-circle"], [(sNM): "Thick", (sICON): "numeric-9-circle"]]) html += """
""" html+= addButtonMenu(var_name: "units_spacing", default_icon: "keyboard-space", tooltip: "Units Spacing", default_value: "Single Space", side: sRIGHT, list:[[(sNM): "No Space", (sICON): "arrow-collapse-horizontal"], [(sNM): "Single Space", (sICON): "keyboard-space"], [(sNM): "Double Space", (sICON): "arrow-expand-horizontal"]]) html += """
""" //TEXT COLOR html+= addColorPicker((sVAR): "text",(sTIT): "Text") //BACKGROUND COLOR html+= addColorPicker((sVAR): sBACKGRND,(sTIT): "Background") //Font Adjustment html += """
""" html+= addSlider((sVAR): "font_adjustment",(sTIT): "Relative Size", (sMIN): -100, (sVAL): iZ, (sMAX):100) html+="""
""" return html } /* String getTileListItem(Map map){ String function=map.function String var= sMs(map,sNM) Map menu=map.list List selections=map.selections.clone() selections << var return "" */ /* String onclick if(!menu.list) onclick="""onclick="${map.function}('${selections}')" """ String html="""""" if(menu.icon){ html += """ """ } if(menu.text){ html += """${text} """ } if(menu.tooltip){ html += """
${tooltip}
""" } html += """
""" if(menu.list){ html += """
    """ menu.list.each{item-> html+= getTileListItem(name: var+"_"+item.name, list: item.list, selections: selections, function: function) } html += """
""" } else{ / var=item.name.replaceAll(" ","") var=parent_+var List select=selections.clone() select << [item.name] func="""onclick="${function_name}('${select}')" """; if(item.list){ html += getTileListItem(name: item.name, parent: var, function: function, list: item.list, selections: select); } else{ html += """
  • ${item.name}
  • """ } } html += """""" return html */ //} String defineHTML_Tile(Boolean locked){ /* String temp_units='°' String rain_units='"' String m_time_units=' am' String e_time_units=' pm' String wind_units=' mph' String pressure_units='inHg' if(tile_units == "metric"){ rain_units='mm' m_time_units='' e_time_units='' wind_units=' m/sec' pressure_units='mmHg' } */ String background; background='black' String bc= 'background_color' if(gtSetStr(bc) != null){ Float transparent=gtSetB(bc+'_transparent') ? 0.0 : background_opacity background=getRGBA(gtSetStr(bc), transparent) } String html_ html_=""" """ html_ += """
    """ //Main Tile Building Code ((List)state.tile_settings).eachWithIndex{Map item, index-> html_ += getTileHTML(item, locked) } html_ += """
    """ html_ += """ """ return html_ } /* String defineHTML_globalVariables(){ String html=""" var sunrise; var sunset; let options=[]; let pws_data=[]; let currentTemperature; """ } */ /* //tile_settings_HTML String defineUpdateDataHTML(String var){ //TODO if(!settings["$var"]){ //if(!settings["$var"]){ wremoveSetting(var.toString()) } app.updateSetting("${var}", [(sVAL): sBLK, (sTYPE): "string"]) } String html="""
    """ return html } */ static String defineScript(){ String html=""" """ return html } String getWeatherTile_weather2(Boolean config){ String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" buildWeatherData() String html html=defineHTML_Header() html += """ """ html += defineHTML_Tile_forecast() html+="" return html } //oauth endpoints String getData_forecast(){ Map data= (Map)parent.getWData() // getPWSData() //String tdata=parent.getOpenWeatherData() // TODO parent.getWData() //if(isEric())myDetail null,"getData_forecast: $data",iN2 return JsonOutput.toJson(data) } /* * TODO: Longtermstorage methods */ def mainLongtermstorage(){ dynamicPage((sNM): "mainPage"){ List container hubiForm_section(tDesc()+" Options", i1, "tune", sBLK){ container=[] container << hubiForm_page_button("Select Device/Data", "deviceSelectionPage", s100PCT, "vibration") container << hubiForm_page_button("Configure/Report Data Storage", "graphSetupPage", s100PCT, sPOLL) hubiForm_container(container, i1) } put_settings(false) } } @Field static String minFwVersion = "2.3.4.132" def deviceLongtermstorage(){ dynamicPage((sNM): "deviceSelectionPage", nextPage:"attributeConfigurationPage"){ String s='hpmSecurity' Boolean fwOk= ((String)location.hub.firmwareVersionString >= minFwVersion) List container if(!fwOk){ if(password && username){ log.debug("Username and Password set") } hubiForm_section("Login Information", i1, sBLK, sBLK){ if(settings[s]==null){ settings[s]=true app.updateSetting(s, true) } container=[] container << hubiForm_switch ((sTIT): "Use Hubitat Security?", (sNM): s, (sDEFLT): true, (sSUBONCHG): true) hubiForm_container(container, i1) if(gtSetB(s)){ input "username", "string",(sTIT): "Hub Security username", (sREQ): false, (sSUBOC): true input "password", "password",(sTIT): "Hub Security password", (sREQ): false, (sSUBOC): true } } } if(!fwOk && gtSetB(s) && !login()){ hubiForm_section("Login Error", i1, sBLK, sBLK){ container=[] container << hubiForm_text("""CANNOT LOGIN
    If you have Hub Security Enabled, please put in correct login credentials
    If not, please deselect Use Hubitat Security""") hubiForm_container(container, i1) } }else{ hubiForm_section("Sensor and Attribute Selection", i1, sBLK, sBLK){ input 'sensors', "capability.*",(sTIT): "Sensor Selection for Long Term Storage", (sMULTP): true, (sSUBOC): true if(sensors){ List final_attrs; final_attrs=[] for(sensor in (List)sensors){ String sid=gtSensorId(sensor) if(sid==sBLK){ error "deviceLongtermstorage null sid ${sensor}",null,iN2 continue } try{ final_attrs=[] //List attributes_=sensor.getSupportedAttributes() List attributes_=sensor.getSupportedAttributes().collect{ it.getName() }.unique().sort() attributes_.each{ String attribute_-> //String name=attribute_.getName() def v= sensor.currentState(attribute_,true) if(v != null){ final_attrs << [(attribute_) : "${attribute_} ::: [${v.getValue()}]"] } } final_attrs=final_attrs.unique(false) }catch(e){ final_attrs=[[(s1): "ERROR"]] error "Error: ",null,iN2,e } String sensor_name=gtLbl(sensor) input( (sTYPE): sENUM, (sNM): "${gtSensorId(sensor)}_attributes",(sTIT): "${sensor_name} attribute(s) to Store", (sREQ): true, (sMULTP): true, options: final_attrs, (sSUBOC): false) } } } } } } def optionsLongtermstorage(){ def hoursEnum=1..24 // def df=new DecimalFormat("#0.0") dynamicPage((sNM): "attributeConfigurationPage"){ for(sensor in (List)sensors){ String sid=gtSensorId(sensor) if(sid==sBLK){ error "optionsLongtermstorage null sid ${sensor}",null,iN2 continue } List att=(List)settings["${sid}_attributes"] if(att){ att.each{ String attribute-> String attr=attribute.replaceAll(sSPC, "_") String sensor_name=gtLbl(sensor) hubiForm_section("${sensor_name} (${attribute})", i1, sBLK, sBLK){ storageLimitInput(sid, attr) String s="${sid}_${attr}".toString() input( (sTYPE): sENUM, (sNM): s+"_time_every",(sTIT): "Attempt to Store Data Every X Hours", (sREQ): true, (sMULTP): false, options: hoursEnum, (sSUBOC): false, (sDEFV): 1) input( (sTYPE): sTIME, (sNM): s+"_time",(sTIT): "Time to Start Storing Data", (sREQ): false, (sMULTP): false, (sSUBOC): false, (sDEFV): "00:00") //quantInput(sid,attr) List container container=[] List events=getAllDataLimit(sensor, attribute, 60) Integer num_events=events?.size() Date now=new Date() if(num_events > i2){ // TODO def span=( dtMdt(events[num_events-i1]).getTime()- dtMdt(events[iZ]).getTime())/(1000*60*60*24) def since=(now.getTime() - dtMdt(events[iZ]).getTime())/(1000*60*60) List quantData= doQuant(events, sid, attr, true) Long frequency=averageFrequency(events) container << hubiForm_sub_section("Estimated Storage Consumption") container << hubiForm_text("Total Events: ${quantData.size()} quantized (${num_events} raw data)") container << hubiForm_text("First Event: ${events[iZ][sDT]} (${round(since)} hours ago)") container << hubiForm_text("Frequency of raw data: 1 event every ${round(frequency/(1000*60))} minutes") List subcontainer subcontainer=[] subcontainer << hubiForm_text(sBLK) subcontainer << hubiForm_text("Daily Storage") subcontainer << hubiForm_text("Weekly Storage") subcontainer << hubiForm_text("Monthly Storage") subcontainer << hubiForm_text("Yearly Storage") container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]]) Integer averageSize= 34 // 50 //Map storage=getCurrentDailyStorage(sensor, attribute) //subcontainer << hubiForm_text(storage.num_events.toString()) //subcontainer << hubiForm_text(convertStorageSize((Integer)storage.size)) subcontainer=[] Integer daily daily=((num_events/span)*averageSize).toInteger() subcontainer << hubiForm_text("Raw Data") subcontainer << hubiForm_text(convertStorageSize(daily)) subcontainer << hubiForm_text(convertStorageSize(daily*7)) subcontainer << hubiForm_text(convertStorageSize(daily*30)) subcontainer << hubiForm_text(convertStorageSize(daily*365)) container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]]) subcontainer=[] daily=((quantData.size()/span)*averageSize).toInteger() subcontainer << hubiForm_text("Quantized Data") subcontainer << hubiForm_text(convertStorageSize(daily)) subcontainer << hubiForm_text(convertStorageSize(daily*7)) subcontainer << hubiForm_text(convertStorageSize(daily*30)) subcontainer << hubiForm_text(convertStorageSize(daily*365)) container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]]) } hubiForm_container(container, i1) } } } } } } def graphLongtermstorage(){ dynamicPage((sNM): "graphSetupPage"){ if(sensors){ List container List subcontainer hubiForm_section("Current Attribute Storage", i1, sBLK, sBLK){ container=[] subcontainer=[] subcontainer << hubiForm_text("Sensor") subcontainer << hubiForm_text("Attribute") subcontainer << hubiForm_text("Number of Events") subcontainer << hubiForm_text("First Event Time") subcontainer << hubiForm_text("Last Event Time") subcontainer << hubiForm_text("File Size") container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]]) Double totalS totalS=0.0D for(sensor in (List)sensors){ String sid=gtSensorId(sensor) if(sid==sBLK){ error "graphLongtermstorage null sid ${sensor}",null,iN2 continue } List att=(List)settings["${sid}_attributes"] if(att){ att.each{ String attribute-> String sensor_name=gtLbl(sensor) subcontainer=[] //appendFile_LTS(sensor, attribute) Map storage=getCurrentDailyStorage(sensor, attribute) String filename_=getFileName(sensor, attribute) String uri_="http://${location.hub.localIP}:8080/local/${filename_}" subcontainer << hubiForm_text(sensor_name, uri_) subcontainer << hubiForm_text(attribute, uri_) subcontainer << hubiForm_text(storage.num_events.toString()) subcontainer << hubiForm_text(formatTime(dtMs(storage,'first'))) subcontainer << hubiForm_text(formatTime(dtMs(storage,'last'))) Integer s= iMs(storage,'size') subcontainer << hubiForm_text(convertStorageSize(s)) totalS += s container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]]) } } } container << hubiForm_text("Total Storage: ${convertStorageSize(totalS.toInteger())}") hubiForm_container(container, i1) } } } } /** * called by graph apps to know if LTS for id, attribute is available * it ends up calling isStorage in LTS app */ Boolean isLtsAvailable(id, String attribute){ return (Boolean)parent.ltsAvailable(id, attribute) } /** LTS only called by parent is LTS stream enabled? */ Boolean isStorage(id, String attribute){ def sensor=sensors?.find{it.id == id} if(sensor != null){ List att=(List)settings["${id}_attributes"] return att.find{ it == attribute } != null } return false } /** LTS only called by parent is LTS stream with quant enabled?*/ Boolean isQuant(id, String attribute){ if(isStorage(id,attribute)){ def sensor=sensors?.find{it.id == id} if(sensor){ String s= "${gtSensorId(sensor)}_${attribute}_quantization" String ts1= s+"_function" return !(settings[ts1]==sNONE || settings[s]==null || settings[s]==s0) } } return false } /** LTS only, called by schedule to add data to file from device events in DB */ void updateData_LTS(Map data){ if(isEric())myDetail null,"updateData $data",i1 Map theEvent theEvent=[:]+data Map qres=queueSemaphore(data) // log.warn "qres:$qres" String msgt msgt="queued" if(!bIs(qres,'exitOut')){ String pNm=sAppId() while(true){ def sensor=sensors?.find{ it.id == theEvent.id } if(sensor) appendFile_LTS(sensor, sMs(theEvent,sATTR)) else warn "Sensor not found ${theEvent}",null theEvent=null getTheLock(pNm,'update Data') List evtQ evtQ=theQueuesVFLD[pNm] if(!evtQ){ if(theSemaphoresVFLD[pNm]<= lMs(qres,'semaphore')){ msgt='Released Lock and exiting' theSemaphoresVFLD[pNm]=lZ theSemaphoresVFLD=theSemaphoresVFLD } releaseTheLock(pNm) break }else{ evtQ=theQueuesVFLD[pNm] ListevtList=evtQ //.sort{ Map it -> lMt(it) } theEvent=evtList.remove(0) Integer qsize=evtList.size() theQueuesVFLD[pNm]=evtList theQueuesVFLD=theQueuesVFLD releaseTheLock(pNm) if(qsize>i20)warn "large queue size ${qsize}".toString(),null } } } if(isEric())myDetail null,"update Data ${msgt}" } // Scheduling functions void setupCron(sensor, String attribute){ if(isEric())myDetail null,"setupCron $sensor $attribute",i1 String dateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ" String sid=gtSensorId(sensor) if(sid){ String attr=attribute.replaceAll(sSPC, "_") Date date=wtimeToday( gtSetStr("${sid}_${attr}_time"), mTZ()) //Date date=Date.parse(dateFormat, gtSetStr("${sid}_${attr}_time")) //error "object: ${describeObject(settings["${sid}_${attr}_time_every"])}",null //log.warn myObj(settings["${sid}_${attr}_time_every"]) Integer repeat= gtSetStr("${sid}_${attr}_time_every").toInteger() //log.warn "$date $repeat ${sensor.id}" addToSched(hrs: date.getHours(), mins: date.getMinutes(), repeatHrs: repeat, sid: sid, (sATTR): attribute) //schedule("0 ${date.getMinutes()} ${date.getHours()}/${repeat} ? * * *", updateData, [overwrite: false, data: [id: sid, attribute: attribute]]) }else error "setupCron null sid ${sensor} ${attribute}",null,iN2 if(isEric())myDetail null,"setupCron $sensor $attribute" } private void clearSch(){ String pNm=sAppId() getTheLock(pNm,'clearSch') atomicState.sched=[] releaseTheLock(pNm) } private addToSched(Map data){ if(isEric())myDetail null,"addToSched",i1 Integer hrs= iMs(data,'hrs') Integer mins= iMs(data,'mins') Integer repeatHrs= iMs(data,'repeatHrs') String sid= sMs(data,'sid') String attribute=sMs(data,sATTR) Long nextRun=pushAhead(hrs,mins,repeatHrs) String pNm=sAppId() getTheLock(pNm,'addToSched') List sched sched=atomicState.sched sched=sched != null ? sched : [] if(sched) unschedule() sched << [hrs: hrs, mins: mins, repeatHrs: repeatHrs, sid: sid, (sATTR): attribute, nextRun: nextRun] atomicState.sched=sched releaseTheLock(pNm) if(isEric())myDetail null,"addToSched" } @CompileStatic Long pushAhead(Integer hrs, Integer mins, Integer repeatHrs){ Long firstOffset= hrs*3600000 + mins*60000 Long baset= getMidnightTime() + firstOffset Long endt=getNextMidnightTime() Long repeatT=repeatHrs*3600000 Long res res=baset Long n=wnow() while (res endt) res= endt + firstOffset return res } private Long getMidnightTime(){ return wtimeToday('00:00',mTZ()).getTime() } private Long getNextMidnightTime(){ return wtimeTodayAfter('23:59','00:00',mTZ()).getTime() } private Date wtimeToday(String str,TimeZone tz){ return (Date)timeToday(str,tz) } private Date wtimeTodayAfter(String astr,String tstr,TimeZone tz){ return (Date)timeTodayAfter(astr,tstr,tz) } private void wrunInMillis(Long t,String m,Map d){ runInMillis(t,m,d) } private runNextSched(Map a=[:]){ String msg msg="runNextSched" if(isEric())myDetail null,msg,i1 String pNm=sAppId() Boolean didSomething, didSched didSomething=false didSched=false getTheLock(pNm,msg) List sched sched=atomicState.sched sched=(sched!=null) ? []+sched : [] Long nextSched nextSched=0L Integer i for(i=iZ; i< sched.size(); i++){ Map s=sched[i] Long nextRun nextRun= lMs(s,'nextRun') Integer hrs= iMs(s,'hrs') Integer mins= iMs(s,'mins') Integer repeatHrs= iMs(s,'repeatHrs') String sid= sMs(s,'sid') String attribute=s[sATTR] if(nextRun < wnow()){ didSomething=true nextRun=pushAhead(hrs,mins,repeatHrs) s.nextRun=nextRun if(didSomething) atomicState.sched=sched releaseTheLock(pNm) updateData_LTS([(sID): sid, (sATTR): attribute]) getTheLock(pNm,msg+' L') } if(!nextSched) nextSched= nextRun if(nextRun< nextSched) nextSched=nextRun } Long n=wnow() if(nextSched>n){ Long t=nextSched-wnow() didSched=true wrunInMillis(t,"runNextSched", [:]) state.nextSched=nextSched if(isEric())myDetail null,msg+" schedule in $t msecs",iN2 }else{ if(isEric())myDetail null,msg+" no nextsched $nextSched or bad choice $n",iN2 } releaseTheLock(pNm) if(!didSomething && !didSched) msg += " did nothing" if(isEric())myDetail null,msg } void checkSched(){ Long next next=state.nextSched next= next ?: 0L if(wnow() > next+900000L){ // 15 mins late String msg='checkSched' if(isEric())myDetail null,msg,i1 runNextSched() if(isEric())myDetail null,msg } } // TODO quant functions /** returns internal format entry */ @CompileStatic Map sum(List events, Integer decimals, Boolean round, Integer granularity){ Float sum sum=new Float(0) for(Map event in events){ sum += Float.valueOf(event[sVAL].toString()) } Map tdate=[(sDT): events[events.size()-i1][sDT], boundary: round, granularity: granularity] Date d=roundDate(tdate) return [(sDT): d, (sVAL): sum.round(decimals), (sT): d.getTime()] } /** returns internal format entry */ @CompileStatic Map average(Listevents, Integer decimals, Boolean round, Integer granularity){ Float sum sum=new Float(0) Integer sz=events.size() for(Map event in events){ sum += Float.valueOf(event[sVAL].toString()) } sum /= sz Map tdate=[(sDT): events[sz-i1][sDT], boundary: round, granularity: granularity] Date d=roundDate(tdate) return [(sDT): d, (sVAL): sum.round(decimals), (sT): d.getTime(), (sQ):i1] } /** returns internal format entry */ @CompileStatic Map min(Listevents, Integer decimals, Boolean round, Integer granularity){ Float min min=Float.valueOf(events[iZ][sVAL].toString()) for(Map event in events){ Float v=Float.valueOf(event[sVAL].toString()) min=v < min ? v : min } Map tdate=[(sDT): events[events.size()-i1][sDT], boundary: round, granularity: granularity] Date d=roundDate(tdate) return [(sDT): d, (sVAL): min.round(decimals), (sT): d.getTime()] } /** returns internal format entry */ @CompileStatic Map max(Listevents, Integer decimals, Boolean round, Integer granularity){ Float max max=Float.valueOf(events[iZ][sVAL].toString()) for(Map event in events){ Float v=Float.valueOf(event[sVAL].toString()) max=v > max ? v : max } Map tdate=[(sDT): events[events.size()-i1][sDT], boundary: round, granularity: granularity] Date d=roundDate(tdate) return [(sDT): d, (sVAL): max.round(decimals), (sT): d.getTime()] } /** returns internal format entry */ @CompileStatic Map count(Listevents, Integer decimals, Boolean round, Integer granularity){ Integer sz=events.size() Map tdate=[(sDT): events[sz-i1][sDT], boundary: round, granularity: granularity] Date d=roundDate(tdate) return [(sDT): d, (sVAL): sz, (sT): d.getTime(), (sQ):i1] } /* static Long getTime(String text){ String dateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ" //String dateFormat="yyyy-MM-dd'T'HH:mm:ssX" return Date.parse(dateFormat, text).getTime() } */ /** round a date based on quant settings */ Date roundDate(Map map){ Date date= dtMdt(map) Boolean boundary= !!bIs(map,'boundary') Integer granularity=map.granularity as Integer if(!boundary) return date Date nearest nearest=date if(granularity > 60 && granularity < 1440) nearest=org.apache.commons.lang3.time.DateUtils.truncate(date, Calendar.HOUR_OF_DAY) else if(granularity == 1440) nearest=org.apache.commons.lang3.time.DateUtils.truncate(date, Calendar.DAY_OF_MONTH) return nearest } /** * * @return - internal format */ @CompileStatic List quantizeData(List events, String mins, String funct, Integer dec, Boolean boundary, Boolean toStore){ Integer minutes=mins as Integer Integer sz String s s='quantizeData ' Boolean isE= isEric() sz=events.size() if(isE)myDetail null,s+"mins: $mins func: $funct decimals: $dec boundary: $boundary size: $sz",i1 if(minutes==iZ || funct==sNONE){ if(isE)myDetail null,s+"no change" return events } Integer decimals=dec as Integer Long milliSeconds=minutes*1000*60 List newEvents newEvents=[] try{ Long stop stop=roundDate([(sDT): dtMdt(events[iZ]), granularity: minutes, boundary: boundary]).getTime() + milliSeconds List tempEvents tempEvents=[] Integer idx idx=iZ Map newEntry Long currTime while (idx < events.size()){ currTime=roundDate([(sDT): events[idx][sDT], granularity: minutes, boundary: boundary]).getTime() if(currTime > stop){ sz=tempEvents.size() newEntry=tempEvents[iZ] if(sz == i1 && newEntry[sQ] == i1){ // deals with count cannot be re-processed if(isE) trace "DID NOT REPROCESS "+s+"$funct $sz",null newEvents.add(newEntry) }else if(sz > 0 ){ if(isE) trace s+"$funct $sz",null newEntry=callFunc(funct,tempEvents, decimals, boundary, minutes) newEvents.add(newEntry) } stop += milliSeconds tempEvents=[] } tempEvents.add(events[idx]) idx++ } // TODO remove this // The last events are not quant'd // (sum, average, min, max, count) sz=tempEvents.size() if( (sz == i1 && tempEvents[iZ][sQ]==i1) || (sz>0 && toStore && funct in ['average','count']) ){ // don't screw up average, count -> leave last unprocessed if(isE) trace s+"$funct adding $sz $tempEvents ",null newEvents=newEvents + tempEvents }else if(sz != iZ){ if(isE) trace s+"LAST $funct $sz",null newEntry=callFunc(funct,tempEvents, decimals, boundary, minutes) newEvents.add(newEntry) } }catch(e){ error s,null,iN2,e } sz=newEvents.size() if(isE)myDetail null,s+"Final size $sz" return newEvents } Map callFunc(String funct, ListtempEvents, Integer decimals, Boolean boundary, Integer minutes){ Map newEntry="${funct}"(tempEvents, decimals, boundary, minutes) return newEntry } // shared def storageLimitInput(String sid, String attribute, String defl="7", String varn=sNL){ List> storageEnum=[ ["1" : "1 Day"], ["2" : "2 Days"], ["3" : "3 Days"], ["4" : "4 Days"], ["5" : "5 Days"], ["6" : "6 Days"], ["7" : "1 Week"], ["14" : "2 Weeks"], ["21" : "3 Weeks"], ["30" : "1 Month"], ["60" : "2 Months"], ["91" : "3 Months"], ["121" : "4 Months"], ["152" : "5 Months"], ["182" : "6 Months"], ["213" : "7 Months"], ["243" : "8 Months"], ["274" : "9 Months"], ["304" : "10 Months"], ["334" : "11 Months"], ["365" : "1 Year"], ["730" : "2 Years"], ["1095" : "3 Years"], ["1461" : "4 Years"]] String s= varn ?: (sid+'_'+attribute+'_storage') input( (sTYPE): sENUM, (sNM): s,(sTIT): "Duration of Storage to Maintain", (sREQ): false, (sMULTP): false, options: storageEnum, (sSUBOC): false, (sDEFV): defl) } static Long averageFrequency(List events){ Long sum sum=0L Integer i for(i=i1; i getEvents(Map map){ if(isEric())myDetail null,"getEvents $map",i1 try{ def sensor=map.sensor String attribute=map[sATTR] Integer days= iMs(map,'days') Date then if(map.start_time){ then= dtMs(map,'start_time') }else{ Date now=new Date() then=now use (TimeCategory){ then -= days.days } } //TODO remove date List respEvents respEvents=(List)sensor.statesSince(attribute, then, [(sMAX): 2000]).collect{ [ (sDT): it.date, (sVAL): it.value, (sT): ((Date)it.date).getTime()] } respEvents=respEvents.flatten() as List respEvents=respEvents.reverse() as List if(isEric())myDetail null,"getEvents $map ${respEvents.size()}" return respEvents as List }catch(e){ error "getEvents",null,iN2,e } if(isEric())myDetail null,"getEvents" return null } Boolean login(){ if(gtSetB('hpmSecurity')){ Boolean result result=false try{ httpPost( [ uri: "http://127.0.0.1:8080", path: "/login", query: [ loginRedirect: "/" ], body: [ username: username, password: password, submit: "Login" ], textParser: true, ignoreSSLIssues: true ] ){ resp -> if(resp.data?.text?.contains("The login information you supplied was incorrect.")) result=false else{ state.cookie=((List) ((String)resp?.headers?.'Set-Cookie')?.split(';'))?.getAt(0) result=true } } }catch(e){ error "Error logging in: ",null,iN2,e result=false } return result } return true } Boolean fileExists(sensor, String attribute, String fname=sNL){ String filename_=fname ?: getFileName(sensor, attribute) String uri="http://${location.hub.localIP}:8080/local/${filename_}" Map params=[ uri: uri, textParser: true, ] Boolean res res=false try{ httpGet(params){ resp -> if(resp.status==200) res=true } }catch(e){ String sensor_name=gtLbl(sensor) if( isFNF(e) ){ debug "File DOES NOT Exist for ${sensor_name} (${attribute})",null,iN2 }else{ error"Find file ${sensor_name} (${attribute}) ($filename_} :: Exception: ",null,iN2,e } } return res } static Boolean isFNF(Exception ex){ if(ex instanceof java.nio.file.NoSuchFileException) return true String file=(String)ex.message return file.contains("Not Found") } @Field volatile static Map readTmpFLD=[:] @Field volatile static Map readTmpBFLD=[:] Boolean lowReadFile(String fname,String ts1){ String pNm=fname readTmpFLD[pNm]=sBLK readTmpFLD= readTmpFLD try{ if((String)location.hub.firmwareVersionString >= minFwVersion){ readTmpBFLD[pNm]=null readTmpBFLD[pNm]= (byte[])downloadHubFile(fname) if(readTmpBFLD[pNm].size()){ readTmpFLD[pNm]=new String(readTmpBFLD[pNm]) readTmpBFLD[pNm]=null readTmpBFLD= readTmpBFLD } }else{ String uri="http://${location.hub.localIP}:8080/local/${fname}" Map params=[ uri: uri, contentType: "text/plain; charset=UTF-8", textParser: true, headers: [ "Cookie": state.cookie, "Accept": 'application/octet-stream' ] ] httpGet(params){ resp -> if(resp.status==200 && resp.data){ Integer i char c i=resp.data.read() while(i!=-1){ c=(char)i readTmpFLD[pNm]+=c i=resp.data.read() } //log.warn "pNm: ${pNm} data: ${data} file: ${readDataFLD[pNm]}" }else{ error "Read Response status $resp.status",null } } } readTmpFLD= readTmpFLD return true }catch(e){ if( isFNF(e) ){ debug "File DOES NOT Exist"+ts1,null,iN2 }else{ error "Read File Data"+ts1+" :: Exception: ",null,iN2,e } readTmpBFLD[pNm]=null readTmpBFLD= readTmpBFLD } return false } /** * returns Map that has internal format in map.data * @param sensor * @param attribute * @param fname * @return Map [ size: x, data: List [[date: date, (sVAL): v, t: t], ....] ] */ Map readFile(sensor, String attribute, String fname=sNL){ String s= "readFile $sensor $attribute $fname" if(isEric())myDetail null,s,i1 String filename_=fname ?: getFileName(sensor, attribute) String pNm=filename_ if(readTmpFLD[pNm]==sNL){ readTmpFLD[pNm]=sBLK; readTmpFLD= readTmpFLD } try{ Integer sz=readTmpFLD[pNm].size() //myDetail null,"pNm: ${pNm} cache sz: $sz",iN2 if(sz> 4){ JsonSlurper jsonSlurper=new JsonSlurper() List parse=convertToInternal((List)jsonSlurper.parseText(readTmpFLD[pNm])) if(isEric())trace "readFile cache hit",null if(isEric())myDetail null,s return ['size': sz, 'data': parse ] } } catch(ignored){} String sensor_name=gtLbl(sensor) String ts1= " for ${sensor_name} (${attribute}) ($filename_}" Boolean ok= lowReadFile(filename_,ts1) if(ok){ Integer sz sz = readTmpFLD[pNm].size() //myDetail null,"after read pNm: ${pNm} cache sz: $sz",iN2 if (sz) { String sc sc = readTmpFLD[pNm] while (sz && sc[sz - i1] != ']') { sc = sc.substring(iZ, sz - i1) sz = sc.size() } readTmpFLD[pNm] = sc } //myDetail null,"after TRIM pNm: ${pNm} cache sz: $sz",iN2 List parse parse = [] if (sz > i1) { JsonSlurper jsonSlurper = new JsonSlurper() parse = convertToInternal((List) jsonSlurper.parseText(readTmpFLD[pNm])) } else sz = iZ if (isEric()) myDetail null, s + " $sz" return ['size': sz, 'data': parse] } readTmpFLD[pNm]=sNL readTmpFLD= readTmpFLD if(isEric())myDetail null,s return ['size': iZ, 'data': [] ] } String getFileName(sensor, String attribute){ String attr=attribute.replaceAll(sSPC, "_") String sid=gtSensorId(sensor) if(sid==sBLK){ error "getFileName null sid ${sensor} ${attribute}",null,iN2 return sid } return "webCoRE_LTS_${sid}_${attr}.json" } /** receives internal format */ List pruneData(List input_data, Integer days){ Boolean isE= isEric() if(isE)myDetail null,"pruneData ${input_data.size()} time: $days",i1 if(days == iZ || !input_data){ if(isE)myDetail null,"pruneData nochange" return input_data } List return_data=[]+input_data if(input_data.size() > 0){ Long startDate; startDate=wnow() startDate -= (days * lMSDAY) Long date date= lMt(input_data[iZ]) while (date && return_data && date < startDate){ if(isE)debug "date: $date startDate: $startDate return_data[0]: ${return_data[iZ]}",null Map a=return_data.remove(0) date=return_data ? lMt(return_data[iZ]) : null } } if(isE)myDetail null,"pruneData ${return_data.size()} time: $days" return return_data } @CompileStatic static List addData(List main, List append){ List return_data=main append.each{Map data-> return_data << data } return return_data // sort it just in case? //return_data=events ? return_data + events : return_data //return_data=return_data.flatten() as List } /** returns internal format */ List getFileData(sensor, String attribute, String fname=sNL){ String s s= "getFileData $sensor $attribute $fname" if(isEric())myDetail null,s,i1 List parse_data parse_data =[] Map json= readFile(sensor,attribute,fname) if(json?.data){ parse_data = (List)json.data s += " ${parse_data.size()}" } if(isEric())myDetail null,s return parse_data } /** shared - old LTS only method */ Map quantParams(sensorId, String attr){ String sid=sensorId.toString() String s="${sid}_${attr}".toString() def v v= settings[s+"_quantization"] String quantization_minutes= v!=null ? (String)v : s0 v= settings[s+"_quantization_function"] String quantization_function= v ? (String)v : "average" v= settings[s+"_quantization_decimals"] Integer quantization_decimals= v!=null ? ((String)v).toInteger() : i1 v= settings[s+"_boundary"] Boolean quantization_boundary= v!=null ? ((String)v).toBoolean() : false if(quantization_minutes!=s0 && quantization_function!=sNONE) return [qm: quantization_minutes, qf: quantization_function, qd: quantization_decimals, qb: quantization_boundary] return null } //shared /** LTS only method - internal data format */ List doQuant(Listdata, sensorId, String attr, Boolean toStore){ Map params= quantParams(sensorId,attr) if(params) return quantizeData(data, params.qm , params.qf, params.qd, params.qb, toStore) else return data } /** * Shared Sensor data only - used by graphs and LTS returns all sensor data, trying to go back at least but no more than maxdays * * @param sensor * @param attribute * @param maxdays * @return Internal data format as List [[date: (Date)date, (sVAL): v, t: (Long)t], ....] & updates LTS file if in use */ ListgetAllDataLimit(sensor,String attribute, Integer maxdays=7){ List data=getAllData(sensor,attribute,maxdays,true,false) Long gt; gt= wnow() gt -= (maxdays * lMSDAY) List all_data all_data= data.findAll{ Map it -> lMt(it) > gt} return all_data } /** * Shared- Sensor data only - used by graphs and LTS returns all sensor data, * trying to go back at least mindays (may be more or less than this) * * @param sensor * @param attribute * @param mindays * @param add * @param updateFile - create lts file if it does not exist * @return Internal data format as List [[date: (Date)date, (sVAL): v, t: (Long)t], ....] */ ListgetAllData(sensor,String attribute, Integer mindays=1461, Boolean add=true, Boolean updateFile=false){ String sid=gtSensorId(sensor) List all_data; all_data=[] if(isEric())myDetail null,"getAllData $sensor $attribute $sid $mindays $add $updateFile",i1 if(sid!=sBLK){ List parse_data parse_data=[] Integer sz sz=iN1 Integer st= mindays Date then then=new Date() use (TimeCategory){ then -= st.days } //warn "then is $then",null Boolean lts lts=false if( (gtSetStr(sGRAPHT)==sLONGTS && isStorage(sid,attribute)) || isLtsAvailable(sid,attribute)){ // if(fileExists(sensor,attribute)){ parse_data=getFileData(sensor, attribute) //Get the most Current Data sz=parse_data.size() if(sz) then= dtMdt(parse_data[sz-i1]) lts=true // } } //warn "then NOW is $then",null all_data=parse_data if(add){ List respEvents=getEvents(sensor: sensor, (sATTR): attribute, start_time: then) if(respEvents){ all_data=addData(parse_data, convertToInternal(respEvents)) } } if(lts && all_data && sz==iZ && updateFile) writeFile(sensor,attribute,all_data) // create file if does not exist if(!all_data && add){ def state_=sensor.currentState(attribute,true) all_data= convertToInternal([[v:state_,t:wnow()]]) } }else error "getAllData ${sensor} (${attribute}) no id",null,iN2 if(isEric())myDetail null,"getAllData ${all_data.size()}" return all_data } /** * LTS only method, sensor data only, requires LTS enabled for sensor/attribute * * @param sensor * @param attribute * @param fname */ void appendFile_LTS(sensor, String attribute, String fname=sNL){ if(isEric())myDetail null,"appendFile_LTS $sensor $attribute",i1 String attr=attribute.replaceAll(sSPC, "_") String sid=gtSensorId(sensor) if(sid!=sBLK){ Integer storage= gtSetStr("${sid}_${attr}_storage").toInteger() List write_data write_data=getAllData(sensor,attribute,storage,true,false) try{ if(write_data.size()){ write_data=pruneData(write_data, storage) //write_data=doQuant(write_data, sid, attr, true) writeFile(sensor, attribute, write_data) }else{ String filename_=fname ?: getFileName(sensor, attribute) String sensor_name=gtLbl(sensor) warn "Append File ${sensor_name} (${attribute}) ($filename_) nothing to write",null } }catch(e){ String filename_=fname ?: getFileName(sensor, attribute) String sensor_name=gtLbl(sensor) error "Append File ${sensor_name} (${attribute}) ($filename_) :: Exception: ",null,iN2,e } }else error "Append File ${sensor} (${attribute}) ($fname) no id",null,iN2 if(isEric())myDetail null,"appendFile_LTS" } /** * Shared Returns internal format from from various file formats * @param json * @return Internal data format as List [[date: (Date)date, (sVAL): v, t: (Long)t], ....] */ @CompileStatic static List convertToInternal(Listjson){ List return_data=[] Long t def v for(Map data in json){ t=null if(data.containsKey(sT)){ t= lMt(data) }else if(data[sDT]){ String dateFormat="yyyy-MM-dd'T'HH:mm:ssX" Date date=Date.parse(dateFormat, sMs(data,sDT)) t= date.getTime() }else if(data[sI]){ t= lMs(data,sI) } Date date=new Date(t) v= data.containsKey(sV) ? data[sV] : data[sVAL] v= data.containsKey(sD) ? data[sD] : v if(data.containsKey(sQ)) return_data << [(sDT): date, (sVAL): v, (sT): t, (sQ): data[sQ]] else return_data << [(sDT): date, (sVAL): v, (sT): t] } return return_data } /** * shared (LTS & fuel) only method - convert different formats to file format * @input internal format [[date: (Date)date, (sVAL): v, t: (Long)t]...] * @returns [[ v: v, t: long]...] */ @CompileStatic static List rtnFileData(List events){ List file_data=[] def v Long t for(Map data in events){ v= data.containsKey(sV) ? data[sV] : data[sVAL] v= data.containsKey(sD) ? data[sD] : v t= data.containsKey(sI) ? lMs(data,sI) : 0L t= data.containsKey(sT) ? lMt(data) : dtMdt(data).getTime() if(data.containsKey(sQ)) file_data << [(sV): v, (sT): t, (sQ):data[sQ]] else file_data << [(sV): v, (sT): t] } return file_data } @Field volatile static Map writeTmpFLD=[:] /** shared (LTS & fuel) only method - save different formats to file format */ Boolean writeFile(sensor, String attribute, List events, String fname=sNL){ String s= "writeFile $sensor $attribute $fname" if(isEric())myDetail null,s,i1 String filename_=fname ?: getFileName(sensor, attribute) String pNm=filename_ List file_data file_data=rtnFileData(events) writeTmpFLD[pNm]=file_data ? JsonOutput.toJson(file_data) : sBLK file_data=null Boolean fwOk= ((String)location.hub.firmwareVersionString >= minFwVersion) if(fwOk || login()){ if(readTmpFLD[pNm]==sNL){ readTmpFLD[pNm]=sBLK; readTmpFLD= readTmpFLD } Integer sz= readTmpFLD[pNm].size() /* Integer sz1= writeTmpFLD[pNm].size() myDetail null,"pNm: ${pNm} cache sz: $sz new data: ${sz1}",iN2 if(sz){ String sc=readTmpFLD[pNm] String st=sc[sz-1] if(st=='\n') myDetail null, 'FOUND NEWLINE',iN2 myDetail null,"last char CACHE DATA is ${sc[sz-1]}",iN2 myDetail null,"last char NEW DATA is ${writeTmpFLD[pNm][sz1-1]}",iN2 } */ if(sz> 4 && sz==writeTmpFLD[pNm].size() && writeTmpFLD[pNm]==readTmpFLD[pNm]){ writeTmpFLD[pNm]=sBLK; writeTmpFLD= writeTmpFLD if(isEric()) trace "writeFile no changes",null if(isEric())myDetail null,s+" TRUE" return true } try{ Boolean res; res=false if(fwOk){ readTmpBFLD[pNm]= writeTmpFLD[pNm].getBytes() uploadHubFile(filename_, readTmpBFLD[pNm]) readTmpBFLD[pNm]=null readTmpFLD[pNm]=writeTmpFLD[pNm] res=true }else{ Date d=new Date() String encodedString="thebearmay$d".bytes.encodeBase64().toString() Map params=[ uri: "http://127.0.0.1:8080", path: "/hub/fileManager/upload", query: [ "folder": "/" ], headers: [ "Cookie": state.cookie, "Content-Type": "multipart/form-data; boundary=$encodedString" ], body: """--${encodedString} Content-Disposition: form-data; name="uploadFile"; filename="${filename_}" Content-Type: "text/plain; charset=UTF-8" ${writeTmpFLD[pNm]} --${encodedString} Content-Disposition: form-data; name="folder" --${encodedString}--""", timeout: 300, ignoreSSLIssues: true ] httpPost(params){ resp -> if(resp.status!=200){ error "Write Response status $resp.status",null readTmpFLD[pNm]=sNL }else{ readTmpFLD[pNm]=writeTmpFLD[pNm] res=true } } } readTmpFLD= readTmpFLD writeTmpFLD[pNm]=sBLK; writeTmpFLD= writeTmpFLD if(res){ if(isEric())myDetail null,s+" TRUE" return true } }catch(e){ String sensor_name=gtLbl(sensor) error "Write File ${sensor_name} (${attribute}) ($filename_} :: Exception: ",null,iN2,e } readTmpBFLD[pNm]=null readTmpFLD[pNm]=sNL; readTmpFLD= readTmpFLD writeTmpFLD[pNm]=sBLK; writeTmpFLD= writeTmpFLD } if(isEric())myDetail null,s+" FALSE" return false } /** LTS only method */ Map getCurrentDailyStorage(sensor, String attribute, String fname=sNL){ Map json=fileExists(sensor,attribute,fname) ? readFile(sensor, attribute,fname) : null if(json?.data){ List data=(List)json.data Integer size= iMs(json,'size') Integer dsz=data.size() Date first Date then if(dsz){ first = new Date( lMt(data[iZ])) then = new Date( lMt(data[dsz-i1])) }else{ first=null then=null } return [num_events: dsz, first: first, last: then, 'size': size] }else{ try{ Integer storage String sid=gtSensorId(sensor) if(sid!=sBLK){ storage= gtSetStr("${sid}_${attribute}_storage").toInteger() storage=storage ?: 30 List respEvents=getEvents(sensor: sensor, (sATTR): attribute, days: storage) writeFile(sensor, attribute, respEvents) Integer sz= respEvents.size() return [num_events: sz, first: dtMdt(respEvents[iZ]), last: dtMdt(respEvents[sz-i1]), 'size': sz*34] }else error "getCurrentDailyStorage null sid ${sensor} (${attribute}) ($fname)",null,iN2 }catch (e){ error "Error: ",null,iN2,e } } return null } /** fuel stream method */ Map getCurrentDailyStorageFS(){ List a=getFuelStreamData(null) List file_data file_data=rtnFileData(a) // we are measure as stored size Integer sz; sz=file_data.toString().size() Map json=['size': sz, 'data': a ] if(json.data){ List data=(List)json.data Integer size= iMs(json,'size') Date first=new Date( lMt(data[iZ])) sz= data.size() Date then=new Date( lMt(data[sz-i1])) return [num_events: sz, first: first, last: then, 'size': size] } return null } /* Map getSensor(String str){ List split=str.tokenize('.') def sensor=sensors?.find{ it.id == split[0]} return [ sensor: sensor, attribute: split[1] ] } */ static String convertStorageSize(Integer num){ DecimalFormat df=new DecimalFormat("#0.0") if(num < 1024){ return df.format(num)+" bytes" }else if(num < 1048576){ return df.format(num/1024.0)+" KB" }else{ return df.format(num/1048576.0)+" MB" } } static String round(num){ DecimalFormat df=new DecimalFormat("#0.0") return df.format(num.toString().toDouble()) } /* * TODO: Fuel Stream */ def mainFuelstream(){ dynamicPage((sNM): "mainPage",(sTIT): "Settings", uninstall: true, install: true){ String uf='useFiles' if( !(gtSetB(uf) && gtStB(uf)) ){ section('Use HE files for data storage'){ input( (sTYPE): sBOOL, (sNM): uf,(sTIT): "Use HE files for fuelstream storage?", (sREQ): false, (sMULTP): false, (sSUBOC): true, (sDEFV): false) } } if(gtSetB(uf) || gtStB(uf)){ String s='hpmSecurity' section('Security'){ if(settings[s]==null){ settings[s]=true app.updateSetting(s, [(sTYPE): sBOOL, (sVAL): sTRUE]) } input( (sTYPE): sBOOL, (sNM): s,(sTIT): "Use Hubitat Security", (sREQ): false, (sMULTP): false, (sSUBOC): true, (sDEFV): true) if(gtSetB(s)){ input "username", "string",(sTIT): "Hub Security username", (sREQ): false, (sSUBOC): true input "password", "password",(sTIT): "Hub Security password", (sREQ): false, (sSUBOC): true } } if(gtSetB(s) && settings.password && !login()){ section('Login Error'){ paragraph("""CANNOT LOGIN
    If you have Hub Security Enabled, please put in correct login credentials
    If not, please deselect Use Hubitat Security""" ) } } } section('Storage Limits'){ input "maxSize", "number",(sTIT): "Max size of this fuelStream data in KB", (sDEFV): 95 // Maxsize or n days (ie both limits hold) //input "storage_days", "number",(sTIT): "Max # of days of data in this fuelStream", (sDEFV): 1461 storageLimitInput(sNL, sNL,"1461",'storage_days') } section('Logging'){ input sLOGNG,sENUM,(sTIT):'Logging Level',options:[(s0):"None",(s1):"Minimal",(s2):"Medium","3":"Full"],description:'Logging level',defaultValue:state[sLOGNG] ? state[sLOGNG].toString():s0 } List a a=getFuelStreamDBData(false) state[uf]= gtSetB(uf) && !(a) section('Storage Configuration'){ if(gtSetB(uf)){ String attribute=fuelNattr() def sensor=app Boolean fexists fexists= fileExists(sensor,attribute,fuelName()) if(a){ paragraph("Found DB Storage in use, with use files selected") input( (sTYPE): sBOOL, (sNM): "convertToFile",(sTIT): "Convert to File storage", (sREQ): false, (sMULTP): false, (sSUBOC): true, (sDEFV): false) if(gtSetB('convertToFile')){ if(!fexists){ if(writeFile(sensor, attribute, a,fuelName())){ state.remove('fuelStreamData') info "Converted to file",null fexists=true state[uf]= gtSetB(uf) }else{ error "conversion to file failed",null } }else{ paragraph("Found file exists with DB storage in use") } app.updateSetting("convertToFile", [(sTYPE): sBOOL, (sVAL): sFALSE]) } } } Map storage=getCurrentDailyStorageFS() if(!gtSetB(uf) || !gtStB(uf)){ paragraph("Using HE DB as storage") } if(gtSetB(uf) && gtStB(uf)){ paragraph("Using HE Files as storage") } Integer max=gtSetI('maxSize') ?: 95 paragraph("Storage Limit: ${max}KB") paragraph("Current storage usage is ${convertStorageSize(storage.size)}") Integer storageSize=state.toString().size() paragraph("Current state usage is ${convertStorageSize(storageSize)}") paragraph("Details: ${storage}") } } } /** * methods called by webCoRE parent to operate on streams */ public void createStream(settings){ fuelFLD=null // fuelstream does not have graphType set state.fuelStream=[(sI): settings.id, (sC): (settings.canister ?: sBLK), (sN): settings.name, w: i1, (sT): getFormattedDate(new Date())] } /** * Called by parent to get list of streams in this app instance * Can be filtered to fuelstreams only, or fuel and LTS. Graphs have no stream * Typical fuelstreams have 1 data set, LTS may have many data sets (each returned as a stream) * @return */ public List getFuelStreams(Boolean includeLTS){ List res res = [] if(includeLTS && gtSetStr(sGRAPHT)==sLONGTS){ if(sensors){ for(sensor in (List)sensors){ String sid=gtSensorId(sensor) if(sid==sBLK){ error "getFuelStreams null sid ${sensor}",null,iN2 continue } List att=(List)settings["${sid}_attributes"] if(att){ for(String attribute in att){ //make up stream descriptions String ltsdesc= sid+'_'+attribute String nm= gtLbl(sensor)+'_'+attribute res << [(sI):ltsdesc, (sC): 'LTS', (sN):nm,w:i1,(sT): getFormattedDate(new Date())] } } } } }else{ Map fs=(Map)state.fuelStream if(fs) res << fs } if(isEric())myDetail null,"getFuelStreams $includeLTS $res",iN2 res } /** * fuel stream or LTS only - called by main webCoRE for webCoRE console to get data in stream -> returns webCoRE IDE format */ public List listFuelStreamData(String streamid){ if(isEric())myDetail null,"listFuelStreamData $streamid",iN2 // [[ d: itemvalue, i: item.t]] List ideData=[] List res // if we are LTS, need to find proper stream based on id if(gtSetStr(sGRAPHT)==sLONGTS){ String[] tname = streamid.split('_') String id =tname[iZ] String attribute= tname[i1] res=null if(sensors && id && attribute){ for(sensor in (List)sensors){ String sid=gtSensorId(sensor) if(sid==sBLK){ error "listFuelStreamData null sid ${sensor}",null,iN2 continue } if(id == sid){ res= getAllData(sensor,attribute,1461,true,false) break } } } }else{ res=getFuelStreamData(null) // //getFuelStreamData().collect{ it + [(sT): getFormattedDate(new Date((Long)it.i))]} } if(res){ for(Map data in res){ def v=data.containsKey(sV) ? data[sV] : data[sVAL] Long t=data.containsKey(sT) ? lMt(data) : dtMdt(data).getTime() //ideData << [ (sD): v, (sI): t, (sT): getFormattedDate(new Date(t))] ideData << [ (sD): v, (sT): t ] } } return ideData } /** fuel stream only - called by pistons to read entire stream, returns internal format */ public List readFuelStream(Map req){ if(!req)return null if(isEric())myDetail null,"readFuelStream $req",iN2 return getFuelStreamData(req) } /** fuel stream only - called by pistons to overwrite entire stream, input is internal format */ public void writeFuelStream(Map req){ // overwrite if(!req)return if(req.d instanceof List){ if(isEric())myDetail null,"writeFuelStream $req",iN2 storeFuelUpdate((List)req.d,req,true) } } /** fuel stream only - called by pistons to clear fuel stream */ public void clearFuelStream(Map req){ if(!req)return if(isEric())myDetail null,"clearFuelStream $req",iN2 storeFuelUpdate([],req,true) } /** fuel stream only - called by pistons to append data to fuel stream, adds current time to data added */ public void updateFuelStream(Map req){ // append // def canister=req.c ?: sBLK // def name=req.n // def instance=req.i // def data=req.d // def source=req.s if(isEric())myDetail null,"updateFuelStream $req",iN2 if(!req)return List stream= getFuelStreamData(req) // [[ date: Date, (sVAL): v, t: long]] // old internal format conversion //Boolean a=stream.add([d: req.d, i: wnow()]) Date n= new Date() Boolean a=stream.add([(sVAL): req[sD], (sDT): n, (sT): n.getTime()]) storeFuelUpdate(stream,req) } // Internal methods /** fuel stream only - return file name for this fuel stream */ String fuelName(){ String s= getFSFileName(sF+app.id.toString(),fuelNattr()) if(isEric())myDetail null,"fuelName $s",iN2 return s } /** return cleaned name */ String getFSFileName(String sensorId, String attribute){ String attr=attribute.replaceAll(sSPC, "_") String s= "WebCoRE_Fuel_${sensorId}_${attr}.json" if(isEric())myDetail null,"getFSFileName $s",iN2 return s } /** fuel stream only - return an attribute string for this fuel stream */ @CompileStatic String fuelNattr(){ Map fs=(Map)gtSt("fuelStream") //state.fuelStream=[i: settings.id, c: (settings.canister ?: sBLK), n: settings.name, w: i1, (sT): getFormattedDate(new Date())] String c=sMs(fs,sC) ?: sBLK String n=sMs(fs,sN) Integer i=iMs(fs,sI) String d='_' String attribute=c+d+n+d+i.toString() if(isEric())myDetail null,"fuelNattr $attribute",iN2 return attribute.replaceAll(sSPC, d) } /** fuel stream only - returns internal format read from fuel stream based on storage settings */ public List getFuelStreamData(Map req,Boolean init=true){ if(isEric())myDetail null,"getFuelStreamData $req $init",iN2 // [[ date: Date, value, v, (sT): long]] if(!gtStB('useFiles')){ return getFuelStreamDBData(init) }else return getFuelStreamFData() } /** * fuel stream only - returns internal format * @param init * @return Internal data format as List [[date: date, (sVAL): v, t: t], ....] */ List getFuelStreamDBData(Boolean init=true){ if(isEric())myDetail null,"getFuelStreamDBData $init",iN2 // [[ date: Date, value, v, t: long]] if(!state.fuelStreamData){ if(init) state.fuelStreamData=[] } return convertToInternal((List)state.fuelStreamData) } /** fuel stream only - returns internal format */ List getFuelStreamFData(){ // [[ date: Date, (sVAL): v, t: long]] if(isEric())myDetail null,"getFuelStreamFData",iN2 if(gtStB('useFiles')){ String attribute=fuelNattr() def sensor=app List stream= getFileData(sensor, attribute, fuelName()) List tstor=(List)state[attribute] ?: [] return stream+tstor }else{ log.warn "file requested for fuelstream and file not enabled" } return null } /** fuel stream only - receives internal format, returns trimmed internal format */ @CompileStatic List cleanFuelStream(List istream){ //ensure max size is obeyed List stream //[date: date, (sVAL): v, t: t] stream=istream if(!stream) return [] String msg msg=sBLK Integer osz=stream.size() //String s="${sid}_${attribute}".toString() s+'_storage' Integer storage=(gtSetting("storage_days") ?: 1461) as Integer msg += "original sz: $osz " List parse_data=pruneData(stream, storage) stream=parse_data Integer nsz nsz=stream.size() msg += "after maxdays $storage sz1: $nsz " List tstream tstream= rtnFileData(stream) // need to work with as stored size Double storageSize= tstream.toString().size() / 1024.0D Integer max=(gtSetting('maxSize') ?: 95) as Integer Boolean a if(storageSize.toInteger() > max){ Integer points=stream.size() Double averageSize=points > 0 ? (storageSize/points).toDouble() : 0.0D Integer pointsToRemove pointsToRemove=averageSize > 0 ? ((storageSize - max) / averageSize).toInteger() : 0 pointsToRemove=pointsToRemove > 0 ? pointsToRemove : 0 msg +="size trim to $max: Size ${storageSize}KB Points ${points} Avg $averageSize Remove $pointsToRemove ".toString() List toBeRemoved=stream.sort{ Map it -> it.t }.take(pointsToRemove) a=stream.removeAll(toBeRemoved) } nsz=stream.size() if(osz!=nsz){ msg += "Trimmed fuel stream, $osz, $nsz" if(msg && isDbg()) debug msg,null } return stream } /** fuel stream only - receives internal format, stores as file format based on fuel stream storage settings */ void storeFuelUpdate(Lististream,Map req,Boolean frc=false){ if(isEric())myDetail null,"storeFuelUpdate ${istream.size()} $req $frc",iN2 Boolean res Liststream stream=cleanFuelStream(istream) //[date: date, (sVAL): v, t: t] stream= rtnFileData(stream) if(!gtStB('useFiles')){ res=storeFuelDBData(stream) }else res=storeFuelFileData(stream,frc) if(!res) warn "storeFuelUpdate failed",null } /** fuel stream only - receives file format, stores in HE DB */ Boolean storeFuelDBData(Liststream){ if(isEric())myDetail null,"storeFuelDBData ${stream.size()}",iN2 if(!gtStB('useFiles')){ state.fuelStreamData=stream return true } return false } /** fuel stream only - receives file format, stores in file */ Boolean storeFuelFileData(Lististream,Boolean frc){ if(isEric())myDetail null,"storeFuelFileData ${istream.size()} $frc",iN2 if(gtStB('useFiles')){ String attribute=fuelNattr() def sensor=app Liststream=istream /* Integer osz=istream.size() Liststream=cleanFuelStream(istream) Integer nsz=stream.size() if(!frc && nsz>0 && osz==nsz){ Long lst=nsz>1 ? (Long)stream[nsz-1].i : 0L Long lst2=nsz> 1 ? (Long)stream[nsz-2].i : 0L if((lst-lst2) < 1800000L){ // 30 mins List tstor=(List)state[attribute] ?: [] if(tstor.toString().size()<2000 && tstor.size()<20){ Map item=stream.pop() Boolean a= tstor.add(item) state[attribute]=tstor return true } } } */ state[attribute]= [] return writeFile(sensor, attribute, stream, fuelName()) } return false } @CompileStatic static String getFormattedDate(Date date=new Date()){ SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") format.setTimeZone(TimeZone.getTimeZone("UTC")) format.format(date) } // TODO: Keep updated0 @CompileStatic static String cleanHtml(String htm){ return htm.replace('\t', sBLK).replace('\n', sBLK).replace(' ', sBLK).replaceAll('> ','>').replaceAll(' >','>') //return htm.replace('\t', sSPC).replace('\n', sSPC).replace(' ', sSPC).replace(' ', sSPC).replace(' ',sSPC).replaceAll('> ','>').replaceAll(' >','>') } // Material-Design-lite // https://getmdl.io def hubiForm_container(List containers, Integer inumPerRow=i1, Boolean save=false){ Integer numPerRow numPerRow=inumPerRow String style if(numPerRow == iZ){ style="""style="margin: 0 !important; padding: 0 !important;""" numPerRow=i1 }else{ style=sBLK } String html_ html_="""
    """ containers.each{String container-> html_ += """
    """ html_ += container html_ += """
    """ } html_ += """
    """ if(save) state.saveC=cleanHtml(html_) paragraph cleanHtml(html_) } static String hubiForm_subcontainer(Map map){ List containers=(List)map.objects List breakdown=(List)map.breakdown String html_ html_ = """
    """ Integer count count=iZ containers.each{String container-> def sz_12=12*breakdown[count] def sz_8=8*breakdown[count] def sz_4=4*breakdown[count] html_ += """
    """ html_ += container html_ += """
    """ count++ } html_ += """
    """ return cleanHtml(html_) } List hubiForm_help(){ List container; container=[] container << hubiForm_text_input ("Horizontal Axis Format", 'graph_h_format', sBLK, true) if(gtSetStr('graph_h_format')){ Date today=new Date() container << hubiForm_text("""Horizontal Axis Sample: ${today.format(gtSetStr('graph_h_format'))}""") } container << hubiForm_switch ((sTIT): "Show String Formatting Help", (sNM): 'dummy', (sDEFLT): false, (sSUBONCHG): true) if(gtSetB('dummy')){ List> rows=[] List header=["Name", "Format", "Result"] rows << ["Year", "Y", "2022"] rows << ["Month Number", "M", "12"] rows << ["Month Name ", "MMM", "Feb"] rows << ["Month Full Name", "MMMM", "February"] rows << ["Day of Month", "d", "February"] rows << ["Day of Week", "EEE", "Mon"] rows << ["Day of Week", "EEEE", "Monday"] rows << ["Period", "a", "AM/PM"] rows << ["Hour (12)", "h", "1..12"] rows << ["Hour (12)", "hh", "01..12"] rows << ["Hour (24)", "H", "0..23"] rows << ["Hour (24)", "HH", "00..23"] rows << ["Minute", "m", "0..59"] rows << ["Minute", "mm", "00..59"] rows << ["Seconds", "s", "0..59"] rows << ["Seconds", "ss", "00..59 "] /* List val=[] val <<"Name"; val << "Format" ; val <<"Result" val <<"Year"; val << "Y"; val << "2022" val <<"Month Number"; val << "M"; val << "12" val <<"Month Name "; val << "MMM"; val << "Feb" val <<"Month Full Name"; val << "MMMM"; val << "February" val <<"Day of Month"; val << "d"; val << "February" val <<"Day of Week"; val << "EEE"; val << "Mon" val <<"Day of Week"; val << "EEEE"; val << "Monday" val <<"Period"; val << "a"; val << "AM/PM" val <<"Hour (12)"; val << "h"; val << "1..12" val <<"Hour (12)"; val << "hh"; val << "01..12" val <<"Hour (24)"; val << "H"; val << "0..23" val <<"Hour (24)"; val << "HH"; val << "00..23" val <<"Minute"; val << "m"; val << "0..59" val <<"Minute"; val << "mm"; val << "00..59" val <<"Seconds"; val << "s"; val << "0..59" val <<"Seconds"; val << "ss"; val << "00..59 " container << hubiForm_cell(val, 3) */ container << hubiForm_table([header: header, rows: rows]) container << hubiForm_text("""Example: "EEEE, MMM d, Y hh:mm:ss a"
    = "Monday, June 6, 2022 08:21:33 AM
    """) } return container } static String hubiForm_table(Map map){ List header=(List)map.header List> rows=(List>)map.rows List footer=map.footer ? (List)map.footer : [] String html_ html_="""
    ${name} ${s}
    """ header.each{ String cell-> html_ += """ """ } html_ += """ """ //Integer count=0 rows.each{ List row-> html_ += """ """ row.each{ String cell-> html_ += """ """ } html_ += """ """ } //rows html_ += """ """ footer.each{ String cell-> html_ += """ """ } html_ += """ """ html_ += """
    ${cell}
    ${cell}
    ${cell}
    """ return cleanHtml(html_) } static String hubiForm_text(String text, String link=null){ String html_ if(link != null){ html_="""${text}""" }else{ html_="""${text}""" } return html_ } static String hubiForm_text_format(Map map){ String text=sMs(map,sTEXT) String halign=map.horizontal_align ? "text-align: ${map.horizontal_align};" : sBLK //String valign=map.vertical_align ? "vertical-align: ${map.vertical_align}; " : sBLK String size=map.sz ? "font-size: ${map.sz}px;" : sBLK String html_="""

    $text

    """ return cleanHtml(html_) } static def hubiForm_page_button(String title, String page, String width, String icon){ String html_ html_=""" """ return cleanHtml(html_) } def hubiForm_section(String title, Integer pos, String icon, String suffix, Closure code){ String id=title.replace(' ', '_').replace('(', sBLK).replace(')',sBLK).replace(':','_') String title_=title.replace("'", "").replace("`", "") String titleHTML="""
    ${title_}
    """ String modContent modContent=""" """ modContent=cleanHtml(modContent) section(modContent, code) } String hubiForm_enum(Map map){ String title=sMs(map,sTIT) String var=sMs(map,sNM) List list=(List)map.list String defaultVal=sMs(map,sDEFLT) Boolean submit_on_change=map.submit_on_change String s; s=gtSetStr(var) if(!s){ app.updateSetting (var, [(sVAL):defaultVal, (sTYPE):sENUM]) settings[var]=defaultVal s=defaultVal } String actualVal=s String submitOnChange=submit_on_change ? "submitOnChange" : sBLK String html_ html_="""
    """ /* html_ += """
    • """ list.each{ String item -> html_ += actualVal==item ? """
    • """ : """
    • """ } html_ += """
    """ */ html_ += """
    """ return cleanHtml(html_) } String hubiForm_switch(Map map){ String title=sMs(map,sTIT) String var=sMs(map,sNM) Boolean defaultVal=map.default Boolean submit_on_change=map.submit_on_change if(settings[var]==null){ app.updateSetting (var, !!defaultVal) settings[var]= !!defaultVal } Boolean actualVal=settings[var] != null ? settings[var] : defaultVal String submitOnChange=submit_on_change ? "submitOnChange" : sBLK String html_="""
    """ return cleanHtml(html_) } String hubiForm_text_input(String title, String ivar, String defaultVal, Boolean submitOnChange){ String var=ivar.toString() String s; s= gtSetStr(var) if(!s){ app.updateSetting(var, defaultVal) settings[var]=defaultVal s=defaultVal } String html_="""
    """ return cleanHtml(html_) } /** * gathers settings input (Integer)varname_font */ String hubiForm_font_size(Map map){ String title=sMs(map,sTIT) String varname=sMs(map,sNM) Integer default_=iMs(map,sDEFLT) Integer min=iMs(map,sMIN) Integer max=iMs(map,sMAX) Boolean submit_on_change=map.submit_on_change String baseId=varname String varFontSize="${varname}_font" Integer varVal; varVal=gtSetI(varFontSize) if(varVal==null || (min!=null && varValmax)){ app.updateSetting(varFontSize, default_) settings[varFontSize]=default_ varVal=default_ } String submitOnChange=submit_on_change ? "submitOnChange" : sBLK String html_ = """
    Font Size: ${varVal}
    """ return cleanHtml(html_) } /** * gathers settings input (Integer)varname_font */ String hubiForm_fontvx_size(Map map){ String title=sMs(map,sTIT) String varname=sMs(map,sNM) Integer default_=iMs(map,sDEFLT) Integer min=iMs(map,sMIN) Integer max=iMs(map,sMAX) Boolean submit_on_change=map.submit_on_change String baseId=varname String weight=map.weight ? "font-weight: ${map.weight} !important;" : sBLK String icon icon=sNL String varFontSize="${varname}_font" Integer varVal; varVal= gtSetI(varFontSize) if(varVal==null || (min!=null && varValmax)){ app.updateSetting(varFontSize, default_) settings[varFontSize]=default_ varVal=default_ } Integer icon_size= i10*varVal String jq if(map.icon){ icon=""" cloud """ jq="""jQuery('.test').css('font-size', 10*val+"px"); """ }else{ jq=""" jQuery('#${baseId}_font_size_val').css("font-size", 0.5*val+"em"); jQuery('#${baseId}_font_size_val').text(text); """ } String submitOnChange=submit_on_change ? "submitOnChange" : sBLK String html_ html_ = """
    """ return cleanHtml(html_) } String hubiForm_line_size(Map map){ String title=sMs(map,sTIT) String varname=sMs(map,sNM) Integer default_=iMs(map,sDEFLT) Integer min=iMs(map,sMIN) Integer max=iMs(map,sMAX) Boolean submit_on_change=map.submit_on_change String baseId=varname String varLineSize="${varname}_line_size" Integer varVal; varVal=gtSetI(varLineSize) if(varVal==null || (min!=null && varValmax)){ app.updateSetting(varLineSize, default_) settings[varLineSize]=default_ varVal=default_ } String submitOnChange=submit_on_change ? "submitOnChange" : sBLK String html_ = """
    Width: ${varVal}
    """ return cleanHtml(html_) } String hubiForm_slider(Map map){ String title=sMs(map,sTIT) String varname=sMs(map,sNM) Integer default_=iMs(map,sDEFLT) Integer min=iMs(map,sMIN) Integer max=iMs(map,sMAX) String units=sMs(map,sUNITS) Boolean submit_on_change=map.submit_on_change //def fontSize String varSize=varname String baseId=varname Integer varVal; varVal=gtSetI(varSize) if(varVal==null || (min!=null && varValmax)){ settings[varSize]=default_ app.updateSetting(varSize, default_) varVal=default_ } String submitOnChange=submit_on_change ? "submitOnChange" : sBLK String html_ = """
    ${varVal}${units}
    """ return cleanHtml(html_) } /** * gathers settings input (String)varname_color, (Boolean)varname_color_transparent */ String hubiForm_color(String title, String varname, String defaultColorValue, Boolean defaultTransparentValue, Boolean submit=false){ String varnameColor="${varname}_color" String varnameTransparent= varnameColor+"_transparent" String colorTitle="${title} Color" String notTransparentTitle="Transparent" String transparentTitle="${title}: Transparent" String curColor; curColor= settings[varnameColor] if(!curColor){ app.updateSetting(varnameColor, defaultColorValue) settings[varnameColor]= defaultColorValue curColor= defaultColorValue } Boolean curTransparent= settings[varnameTransparent]!=null ? gtSetB(varnameTransparent) : defaultTransparentValue if(settings[varnameTransparent]!=curTransparent){ app.updateSetting(varnameTransparent, curTransparent) settings[varnameTransparent]=curTransparent } Boolean isTransparent=curTransparent String html_ = """
    ${!isTransparent ? """""" : """"""}
    ${!isTransparent ? """
    """ : ""}
    """ return cleanHtml(html_) } String hubiForm_graph_preview(){ // if(!state.count_) state.count_=7 String html_ = """
    """ return cleanHtml(html_) } static String hubiForm_sub_section(String myText=sBLK){ String id=myText.replaceAll("[^a-zA-Z0-9]", sBLK) String newText=myText.replaceAll("'", "").replaceAll("`", "") String html_="""

    ${newText}

    """ return cleanHtml(html_) } /* static String hubiForm_cell(List containers, Integer numPerRow){ String html_ html_ = """
    """ containers.each{container-> html_ += """
    """ html_ += container html_ += """
    """ } html_ += """
    """ return cleanHtml(html_) } */ def hubiForm_list_reorder(String var, String var_color, String solid_background=sBLK){ Boolean result_; result_=null // TODO List dataSources=gtDataSources() String v=gtSetStr(var) if(v){ List list_=hubiTools_get_order(v) //Check List result_=hubiTools_check_list(dataSources, list_) } String nres nres=gtSetStr(var) if(!result_ && var){ settings[var]=null nres=sNL wremoveSetting(var) } //build list order //Setup Original Ordering if(nres==sNL){ nres="[" //settings["${var}"]="[" // TODO if(dataSources){ Integer count_; count_=iZ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) //settings["${var}"] += /"attribute_${sid}_${attribute}",/ nres += ((nres.length()>i1 ? ',' : sBLK) + /"attribute_${sid}_${attribute}"/ ) String tvar= "attribute_${sid}_${attribute}_${var_color}_color".toString() if(settings[tvar] == null){ String cl='color' if(solid_background== sBLK){ String c= hubiTools_rotating_colors(count_) settings[tvar]=c settingUpdate(tvar, c, cl) }else{ settings[tvar]=solid_background app.updateSetting(tvar, solid_background, cl) } } count_++ } } //settings["${var}"]=settings["${var}"].substring(0, settings["${var}"].length() - i1) nres== nres.substring(iZ, nres.length() - i1) //settings["${var}"] += "]" nres += "]" settings[var]=nres app.updateSetting(var, nres) } List list_data=[] List order_=hubiTools_get_order(nres) String title_ for(Map device_ in order_){ String deviceName_=hubiTools_get_name_from_id(sMs(device_,sID)) title_="""${deviceName_}

    ${device_[sATTR]}

    """ title_.replace("'", "").replace("`", "") list_data << [(sTIT): title_, (sVAR): "attribute_${device_[sID]}_${device_[sATTR]}"] } String var_val_=nres.replace('"', '"') String html_ html_="""
    """ for(Map data in list_data){ String color_=settings["${data.var}_${var_color}_color"] String id_="${data.var}" html_ += """
    fiber_manual_record """ html_ += sMs(data,sTIT) html_ += """
    """ } html_ += """
    """ paragraph cleanHtml(html_) } /** Tools */ void hubiTool_create_tile(){ if(isInf()) info "Checking webCoRE Child Tile Device",null,iN2 String dname dname=gtSetStr('device_name') if(!dname){ dname= gtSetStr('app_name') ?: tDesc() dname += ' Tile' } def childDevice childDevice=getChildDevice("webCoRE_${app.id}") if(!childDevice){ if(isDbg()) debug "Creating Device $dname",null,iN2 childDevice=addChildDevice("ady624", "webCoRE Graphs Tile Device", "webCoRE_${app.id}", null,[completedSetup: true, label: dname]) if(childDevice) info "Created HTTP Switch [${childDevice}]",null }else{ if(childDevice.label!=dname){ childDevice.label=dname if(isDbg())debug "Device Label Updated to [${dname}]",null,iN2 } } //Send the html String s= "${makeCallBackURL('graph/')}" childDevice.setGraph(s) if(isDbg())debug "Sent setGraph: ${s}",null,iN2 } void hubiTools_validate_order(List all){ if(isEric())myDetail null,"_validate_order $all",i1 List order order=[] List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ // TODO need to include attribute to make unique String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() String varn='displayOrder_'+sa order << settings[varn] } } if(isEric())myDetail null,"_validate_order $order",iN2 //if we are initialized and need to check if(isEric())myDetail null,"_validate_order ${state.lastOrder}",iN2 if(state.lastOrder && ((List)state.lastOrder)[iZ]){ List remains=all.findAll{ String it -> !order.contains(it) } List dupes=[] order.each{ ord -> if(order.count(ord) > i1) dupes << ord } if(dataSources){ Integer idx; idx=iZ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() String varn='displayOrder_'+sa if(((List)state.lastOrder)[idx] == order[idx] && dupes.contains(settings[varn])){ settings[varn]=remains[iZ] app.updateSetting(varn, [(sVAL): remains[iZ], (sTYPE): sENUM]) remains.removeAt(0) } idx++ } } } //reconstruct order order=[] if(dataSources){ for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) String sa="${sid}_${attribute}".toString() String varn='displayOrder_'+sa order << settings[varn] } } if(isEric())myDetail null,"_validate_order $order" state.lastOrder=order } static String hubiTools_rotating_colors(Integer c){ String ret=sWHT Integer color=c % 13 switch (color){ case 0: return hubiTools_get_color_code("RED") case 1: return hubiTools_get_color_code("GREEN") case 2: return hubiTools_get_color_code("BLUE") case 3: return hubiTools_get_color_code("MAROON") case 4: return hubiTools_get_color_code("YELLOW") case 5: return hubiTools_get_color_code("OLIVE") case 6: return hubiTools_get_color_code("AQUA") case 7: return hubiTools_get_color_code("LIME") case 8: return hubiTools_get_color_code("NAVY") case 9: return hubiTools_get_color_code("FUCHSIA") case 10: return hubiTools_get_color_code("PURPLE") case 11: return hubiTools_get_color_code("TEAL") case 12: return hubiTools_get_color_code("ORANGE") } return ret } static String hubiTools_get_color_code(String input_color){ String new_color=input_color.toUpperCase() switch (new_color){ case "WHITE" : return sWHT case "SILVER" : return sSILVER case "GRAY" : return "#808080" case "BLACK" : return sBLACK case "RED" : return "#FF0000" case "GREEN" : return "#008000" case "BLUE" : return "#0000FF" case "MAROON" : return "#800000" case "YELLOW" : return "#FFFF00" case "OLIVE" : return "#808000" case "AQUA" : return "#00FFFF" case "LIME" : return "#00FF00" case "NAVY" : return "#000080" case "FUCHSIA" :return "#FF00FF" case "PURPLE" : return "#800080" case "TEAL" : return "#008080" case "ORANGE" : return "#FFA500" } return 'error_color_code' } String hubiTools_get_name_from_id(String id){ //, sensors){ String return_val return_val="Error" // TODO List dataSources=gtDataSources() if(dataSources){ for(Map ent in dataSources){ if(id == sMs(ent,sID)){ return_val=sMs(ent,sDISPNM) break } } } return return_val } List hubiTools_get_order(String order){ if(isEric())myDetail(null,"_get_order ${order}",i1) List split_=order.replace('"', sBLK).replace('[', sBLK).replace(']', sBLK).replace("attribute_", sBLK).split(',') List list_=[] split_.each{ String device-> List sub_=device.split('_') list_ << [(sID): sub_[iZ], (sATTR):sub_[i1]] } if(isEric())myDetail null,"_get_order $order $list_" return list_ } Boolean hubiTools_check_list(List dataSources, List list_){ if(isEric())myDetail null,"_check_list $dataSources $list_",i1 Boolean result; result=true Integer count_; count_=iZ Integer sz; sz=list_.size() //check for addition/changes if(dataSources){ Boolean inner_result Integer i for(Map ent in dataSources){ String sid=sMs(ent,sID) String attribute=sMs(ent,sA) count_++ inner_result=false for(i=iZ; ii2 } private Boolean isTrc(){ (Integer)state[sLOGNG]>i1 } private Boolean isInf(){ (Integer)state[sLOGNG]>iZ } private void myDetail(Map r9,String msg,Integer shift=iN1){ Map a=log(msg,r9,shift,null,sWARN,true,false) } @Field static final String sTMSTMP='timestamp' @Field static final String sDBGLVL='debugLevel' @Field static final String sLOGNG='logging' @Field static final String sLOGS='logs' @Field static final String sTIMER='timer' @Field static final String sENUM='enum' @Field static final String sA='a' @Field static final String sB='b' @Field static final String sC='c' @Field static final String sD='d' @Field static final String sE='e' @Field static final String sF='f' @Field static final String sI='i' @Field static final String sM='m' @Field static final String sN='n' @Field static final String sO='o' @Field static final String sP='p' @Field static final String sQ='q' @Field static final String sS='s' @Field static final String sT='t' @Field static final String sV='v' @Field static final Double d1=1.0D @Field static final Double d10=10.0D private Map log(message,Map r9,Integer shift=iN2,Exception err=null,String cmd=sNL,Boolean force=false,Boolean svLog=true){ if(cmd==sTIMER){ return [(sM):message.toString(),(sT):wnow(),(sS):shift,(sE):err] } String myMsg Exception merr merr=err Integer mshift mshift=shift if(message instanceof Map){ mshift=iMs(message,sS) merr=(Exception)message[sE] myMsg=sMs(message,sM)+" (${elapseT(lMt(message))}ms)".toString() }else myMsg=message.toString() String mcmd=cmd!=sNL ? cmd:sDBG Integer level level=state[sDBGLVL] ? (Integer)state[sDBGLVL]:iZ //shift is // 0 initialize level,level set to 1 // 1 start of routine,level up // -1 end of routine,level down // anything else: nothing happens // Integer maxLevel=4 String ss='╔' String sb='║' String se='╚' String prefix prefix=sb String prefix2 prefix2=sb // String pad=sBLK //"░" switch(mshift){ case iZ: level=iZ case i1: level+=i1 prefix=se prefix2=ss // pad="═" break case iN1: level-=i1 // pad='═' prefix=ss prefix2=se break } if(level>iZ){ prefix=prefix.padLeft(level+(mshift==iN1 ? i1:iZ),sb) prefix2=prefix2.padLeft(level+(mshift==iN1 ? i1:iZ),sb) } state[sDBGLVL]=level Boolean hasErr=(merr!=null && !!merr) myMsg=myMsg.replaceAll(/(\r\n|\r|\n|\\r\\n|\\r|\\n)+/,"\r") if(myMsg.size()>1024){ myMsg=myMsg[iZ..1023]+'...[TRUNCATED]' } List msgs=!hasErr ? myMsg.tokenize("\r"):[myMsg] if(r9 && r9[sTMSTMP]){ if(svLog && r9[sLOGS] instanceof List){ for(String msg in msgs){ Boolean a=((List)r9[sLOGS]).push([(sO):elapseT(lMs(r9,sTMSTMP)),(sP):prefix2,(sM):msg+(hasErr ? " $merr".toString():sBLK),(sC):mcmd]) } } } String myPad=sSPC if(hasErr) myMsg+="$merr".toString() if((mcmd in [sERROR,sWARN]) || hasErr || force || !svLog || !r9 || bIs(r9,'logsToHE') || isEric())doLog(mcmd, myPad+prefix+sSPC+myMsg) //}else log."$mcmd" myMsg return [:] } void doLog(String mcmd, String msg){ String clr switch(mcmd){ case sINFO: clr= '#0299b1' break case sTRC: clr= sCLRGRY break case sDBG: clr= 'purple' break case sWARN: clr= sCLRORG break case sERROR: default: clr= sCLRRED } String myMsg= msg.replaceAll(sLTH, '<').replaceAll(sGTH, '>') log."$mcmd" span(myMsg,clr) } private void info(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sINFO)} private void trace(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sTRC)} private void debug(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sDBG)} private void warn(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sWARN)} private void error(message,Map r9,Integer shift=iN2,Exception err=null){ String aa aa=sNL String bb bb=sNL try{ if(err){ aa=getExceptionMessageWithLine(err) bb=getStackTrace(err) } Map a=log(message,r9,shift,err,sERROR) }catch(ignored){} if(aa||bb)log.error tDesc()+" exception: "+aa+" \n"+bb } //error "object: ${describeObject(e)}",r9 @CompileStatic private static Date dtMdt(Map m){ (Date)m[sDT] } @CompileStatic private static Date dtMs(Map m,String s){ (Date)m[s] } @CompileStatic private static Long lMs(Map m,String v){ (Long)m[v] } @CompileStatic private static Long lMt(Map m){ (Long)m[sT] } private Map timer(String message,Map r9,Integer shift=iN2,Exception err=null){ log(message,r9,shift,err,sTIMER)} @Field static final String sLTH='<' @Field static final String sGTH='>' @Field static final String sCLR4D9 = '#2784D9' @Field static final String sCLRRED = 'red' @Field static final String sCLRRED2 = '#cc2d3b' @Field static final String sCLRGRY = 'gray' @Field static final String sCLRGRN = 'green' @Field static final String sCLRGRN2 = '#43d843' @Field static final String sCLRORG = 'orange' @Field static final String sLINEBR = '
    ' static String span(String str,String clr=sNL,String sz=sNL,Boolean bld=false,Boolean br=false){ return str ? "${str}${br ? sLINEBR:sBLK}": sBLK } @CompileStatic private Long elapseT(Long t,Long n=wnow()){ return Math.round(d1*n-t) } @Field static final String sSPCSB7=' │' @Field static final String sSPCSB6=' │' @Field static final String sSPCS6 =' ' @Field static final String sSPCS5 =' ' @Field static final String sSPCST='┌─ ' @Field static final String sSPCSM='├─ ' @Field static final String sSPCSE='└─ ' @Field static final String sNWL='\n' @Field static final String sDBNL='\n\n • ' @CompileStatic static String spanStr(Boolean html,String s){ return html? span(s) : s } @CompileStatic static String doLineStrt(Integer level,ListnewLevel){ String lineStrt; lineStrt=sNWL Boolean dB; dB=false Integer i for(i=iZ;i lastLevel,String listLabel,Boolean html=false,Boolean reorder=true){ String str; str=sBLK Integer cnt; cnt=i1 List newLevel=lastLevel List list1=data?.collect{it} Integer sz=list1.size() for(Object par in list1){ String lbl=listLabel+"[${cnt-i1}]".toString() if(par instanceof Map){ Map newmap=[:] newmap[lbl]=(Map)par Boolean t1=cnt==sz newLevel[level]=t1 str+=dumpMapDesc(newmap,level,newLevel,cnt,sz,!t1,html,reorder) }else if(par instanceof List || par instanceof ArrayList){ Map newmap=[:] newmap[lbl]=par Boolean t1=cnt==sz newLevel[level]=t1 str+=dumpMapDesc(newmap,level,newLevel,cnt,sz,!t1,html,reorder) }else{ String lineStrt lineStrt=doLineStrt(level,lastLevel) lineStrt+=cnt==i1 && sz>i1 ? sSPCST:(cnt lastLevel,Integer listCnt=null,Integer listSz=null,Boolean listCall=false,Boolean html=false,Boolean reorder=true){ String str; str=sBLK Integer cnt; cnt=i1 Integer sz=data?.size() Map svMap,svLMap,newMap; svMap=[:]; svLMap=[:]; newMap=[:] for(par in data){ String k=(String)par.key def v=par.value if(reorder && v instanceof Map){ svMap+=[(k): v] }else if(reorder && (v instanceof List || v instanceof ArrayList)){ svLMap+=[(k): v] }else newMap+=[(k):v] } newMap+=svMap+svLMap Integer lvlpls=level+i1 for(par in newMap){ String lineStrt List newLevel=lastLevel Boolean thisIsLast=cnt==sz && !listCall if(level>iZ)newLevel[(level-i1)]=thisIsLast Boolean theLast theLast=thisIsLast if(level==iZ)lineStrt=sDBNL else{ theLast=theLast && thisIsLast lineStrt=doLineStrt(level,newLevel) if(listSz && listCnt && listCall)lineStrt+=listCnt==i1 && listSz>i1 ? sSPCST:(listCnt lastLevel=[true] String str=dumpMapDesc(data,iZ,lastLevel,null,null,false,true,reorder) return str!=sBLK ? str:'No Data was returned' } 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+'' } @Field static final String sGITP='https://cdn.jsdelivr.net/gh/imnotbob/webCoRE@hubitat-patches/resources/icons/' private static String gimg(String imgSrc){ return sGITP+imgSrc } @CompileStatic private static String imgTitle(String imgSrc,String titleStr,String color=sNL,Integer imgWidth=30,Integer imgHeight=iZ){ String imgStyle imgStyle=sBLK String myImgSrc=gimg(imgSrc) imgStyle+=imgWidth>iZ ? 'width: '+imgWidth.toString()+'px !important;':sBLK imgStyle+=imgHeight>iZ ? imgWidth!=iZ ? sSPC:sBLK+'height:'+imgHeight.toString()+'px !important;':sBLK if(color!=sNL) return """
    ${titleStr}
    """.toString() else return """ ${titleStr}""".toString() } static String myObj(obj){ if(obj instanceof String)return 'String' else if(obj instanceof Map)return 'Map' else if(obj instanceof List)return 'List' else if(obj instanceof ArrayList)return 'ArrayList' else if(obj instanceof BigInteger)return 'BigInt' else if(obj instanceof Long)return 'Long' else if(obj instanceof Integer)return 'Int' else if(obj instanceof Boolean)return 'Bool' else if(obj instanceof BigDecimal)return 'BigDec' else if(obj instanceof Double)return 'Double' else if(obj instanceof Float)return 'Float' else if(obj instanceof Byte)return 'Byte' // else if(obj instanceof com.hubitat.app.DeviceWrapper)return 'Device' else return 'unknown' } @Field volatile static Map lockTimesVFLD=[:] @Field volatile static Map lockHolderVFLD=[:] @CompileStatic void getTheLock(String qname,String meth=sNL,Boolean longWait=false){ Boolean a=getTheLockW(qname,meth,longWait) } @Field static final Long lTHOUS=1000L @CompileStatic Boolean getTheLockW(String qname,String meth=sNL,Boolean longWait=false){ Long waitT=longWait? lTHOUS:10L Boolean wait wait=false Integer semaNum=semaNum(qname) String semaSNum=semaNum.toString() Semaphore sema=sema(semaNum) while(!sema.tryAcquire()){ // did not get lock Long t t=lockTimesVFLD[semaSNum] if(t==null){ t=wnow() lockTimesVFLD[semaSNum]=t lockTimesVFLD=lockTimesVFLD } if(isEric())warn "waiting for ${qname} ${semaSNum} lock access, $meth, long: $longWait, holder: ${lockHolderVFLD[semaSNum]}",null wpauseExecution(waitT) wait=true if(elapseT(t)>30000L){ releaseTheLock(qname) if(isEric())warn "overriding lock $meth",null } } lockTimesVFLD[semaSNum]=wnow() lockTimesVFLD=lockTimesVFLD lockHolderVFLD[semaSNum]=sAppId()+sSPC+meth lockHolderVFLD=lockHolderVFLD return wait } @CompileStatic static void releaseTheLock(String qname){ Integer semaNum=semaNum(qname) String semaSNum=semaNum.toString() Semaphore sema=sema(semaNum) lockTimesVFLD[semaSNum]=(Long)null lockTimesVFLD=lockTimesVFLD // lockHolderVFLD[semaSNum]=sNL // lockHolderVFLD=lockHolderVFLD sema.release() } void clearSema(){ String pNm=sAppId() getTheLock(pNm,'updated') theSemaphoresVFLD[pNm]=lZ theSemaphoresVFLD=theSemaphoresVFLD theQueuesVFLD[pNm]=[] theQueuesVFLD=theQueuesVFLD // forces volatile cache flush releaseTheLock(pNm) } @Field static Semaphore theLock0FLD=new Semaphore(1) @Field static final Integer iStripes=1 @CompileStatic static Integer semaNum(String name){ if(name.isNumber())return name.toInteger()%iStripes Integer hash=smear(name.hashCode()) return Math.abs(hash)%iStripes } @CompileStatic static Semaphore sema(Integer snum){ switch(snum){ case 0: return theLock0FLD default: //log.error "bad hash result $snum" return null } } private static Integer smear(Integer hashC){ Integer hashCode hashCode=hashC hashCode ^= (hashCode >>> i20) ^ (hashCode >>> i12) return hashCode ^ (hashCode >>> i7) ^ (hashCode >>> i4) } @Field volatile static Map> theQueuesVFLD=[:] @Field volatile static Map theSemaphoresVFLD=[:] // This can queue event @CompileStatic private Map queueSemaphore(Map event){ Long tt1 tt1=wnow() Long startTime startTime=tt1 Long r_semaphore r_semaphore=lZ Long semaphoreDelay semaphoreDelay=lZ String semaphoreName semaphoreName=sNL Boolean didQ didQ=false Boolean waited String mSmaNm=sAppId() waited=getTheLockW(mSmaNm,'queue') tt1=wnow() Long lastSemaphore Boolean clrC clrC=false Integer qsize qsize=iZ while(true){ Long t0=theSemaphoresVFLD[mSmaNm] Long tt0=t0!=null ? t0:lZ lastSemaphore=tt0 if(lastSemaphore==lZ || tt1-lastSemaphore>100000L){ theSemaphoresVFLD[mSmaNm]=tt1 theSemaphoresVFLD=theSemaphoresVFLD semaphoreName=mSmaNm semaphoreDelay=waited ? tt1-startTime:lZ r_semaphore=tt1 break } if(event!=null){ Map mEvt=event List evtQ evtQ=theQueuesVFLD[mSmaNm] evtQ=evtQ!=null ? evtQ:(List)[] qsize=evtQ.size() if(qsize>i12)clrC=true else{ Boolean a=evtQ.push(mEvt) theQueuesVFLD[mSmaNm]=evtQ theQueuesVFLD=theQueuesVFLD didQ=true } } break } releaseTheLock(mSmaNm) if(clrC){ error "large queue size ${qsize} clearing",null //clear1(true,true,true,true) } return [ semaphore:r_semaphore, semaphoreName:semaphoreName, semaphoreDelay:semaphoreDelay, waited:waited, exitOut:didQ ] } @Field static final String sAE='Accept-encoding' @Field static final String sCE='Content-Encoding' @Field static final String sGZIP='gzip' @Field static final String sDATA='data' @Field static final String sUTF8='UTF-8' private Map wrender(Map options=[:]){ //debug "wrender: options:: ${options} " //debug "request: ${request} " /* Map h=(Map)request?.headers if(h && sMs(h,sAE)?.contains(sGZIP)){ // debug "will accept gzip" String s=sMs(options,sDATA) Integer sz=s?.length() if(sz>256){ try{ String a= string2gzip(s) Integer nsz=a.size() if(eric1())debug "options.data is $sz after compression $nsz saving ${Math.round((d1-(nsz/sz))*1000.0D)/d10}%",null // options[sDATA]=a // options[sCE]=sGZIP }catch(ignored){} } } */ render(options + [gzipContent: true]) } static String string2gzip(String s){ ByteArrayOutputStream baos= new ByteArrayOutputStream() GZIPOutputStream zipStream= new GZIPOutputStream(baos) zipStream.write(s.getBytes(sUTF8)) zipStream.close() byte[] result= baos.toByteArray() baos.close() return result.encodeBase64() } @Field static final String sDIV='/' def gforward(String path){ String ep; ep = path if(ep.endsWith(sDIV))ep=ep.substring(iZ,ep.length()-i1) if(ep in ['graph', 'tile']) ep='get'+ep.capitalize() if(isDbg()) myDetail null,"forwarding to $ep",iN2 "${ep}"() } @Field volatile static Map endPointFLD=[:] @Field volatile static String parentHashFLD='' private String makeCallBackURL(String path){ if(!useRemote() || !gtSetB('use_cloudEP')){ return "${getEndpointURL()}${path}?access_token=${getEndpointSecret()}".toString() } Map t0; t0= endPointFLD if(!t0){ t0=(Map)parent.getWCendpoints() endPointFLD= t0 } String id; id= parentHashFLD if(!id){ id= (String)parent.hashPID(sAppId()) parentHashFLD= id } //if(isDbg()) myDetail null,"create URL ${t0.cp}/gforward/${id}?access_token=${t0.at}&path=${path}",iN2 return "${t0.cp}/gforward/${id}?access_token=${t0.at}&path=${path}".toString() } private String getEndpointURL(){ // only a local endpoint // state.remoteEndpointURL will give cloud endpoint to this app // but still need to be on local network due to js/css files on hub that are referenced String ep //ep= useRemote() && gtSetB('use_cloudEP') ? "${state.remoteEndpointURL}".toString() : "${state.localEndpointURL}".toString() ep= "${state.localEndpointURL}".toString() if(gtSetB('use_https')){ if(!ep.contains('https') && ep.contains('http:')){ ep= ep.replace('http:', 'https:') } } return ep } private String getEndpointSecret(){ return "${state.endpointSecret}".toString() } private Long wnow(){ return (Long)now() } private Date wtoDateTime(String s){ return (Date)toDateTime(s) } private String sAppId(){ return ((Long)app.id).toString() } private void wpauseExecution(Long t){ pauseExecution(t) } private void wremoveSetting(String s){ app.removeSetting(s) } void settingUpdate(String name, value, String type=sNL){ if(name && type){ app?.updateSetting(name, [(sTYPE): type, (sVAL): value]) } else if(name && type == sNL){ app?.updateSetting(name, value) } } private gtSetting(String nm){ return settings."${nm}" } private String gtSetStr(String nm){ return (String)settings[nm] } private Boolean gtSetB(String nm){ return (Boolean)settings[nm] } private Integer gtSetI(String nm){ return (Integer)settings[nm] } private Boolean gtStB(String nm){ return (Boolean)state[nm] } private gtSt(String nm){ return state.get(nm) } private gtAS(String nm){ return atomicState.get(nm) } /** assign to state */ private void assignSt(String nm,v){ state."${nm}"=v } /** assign to atomicState */ private void assignAS(String nm,v){ atomicState."${nm}"=v } private Map gtState(){ return state } private gtLocation(){ return location } //******************************************************************* // CLONE CHILD LOGIC //******************************************************************* public Map getSettingsAndStateMap(){ Map setObjs = [:] def vv String sk,typ ((Map)settings).keySet().each{ String theKey-> sk= theKey typ=getSettingType(sk) vv= settings[sk] if(setObjs[sk]!=null) warn "overwriting ${setObjs[sk]} with ${typ}",null if(typ==sTIME) vv= dateTimeFmt(wtoDateTime((String)vv), "HH:mm") if(typ.startsWith('capability')){ typ= 'capability' vv= vv instanceof List ? ((List)vv)?.collect{ it?.id?.toString() } : vv?.id?.toString() } if(typ=='device') vv= vv instanceof List ? ((List)vv)?.collect{ it?.id?.toString() } : vv?.id?.toString() setObjs[sk]= [(sTYPE): typ, (sVAL): vv] } Map data= [:] String newlbl= app?.getLabel()?.toString() //?.replace(" (A ${sPAUSESymFLD})", sBLK) data.label= newlbl?.replace(" (A)", sBLK) List setSkip=['install_device','device_name'] data.settings= setObjs.findAll{ !(it.key in setSkip) } List stateSkip= [ /* "isInstalled", "isParent", */ "accessToken", "debugLevel", "endpoint", "localEndpoint", "endpointSecret", "localEndpointURL", "remoteEndpointURL", "dupPendingSetup", "dupOpenedByUser" ] data.state= ((Map)state)?.findAll{ !((String)it?.key in stateSkip) } return data } private String gtLbl(d){ return "${d?.label ?: d?.name}".toString() }