Created
October 24, 2023 23:46
-
-
Save imnotbob/7f46a6f6efcfe1e32e7365de0614b51c to your computer and use it in GitHub Desktop.
webcore-fuel-stream.groovy for cloud endpoint in child app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* webCoRE - Community's own Rule Engine - Web Edition for HE | |
* | |
* Copyright 2016 Adrian Caramaliu <ady624("at" sign goes here)gmail.com> | |
* | |
* 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 October 18, 2023 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.20231018_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 true } | |
@CompileStatic | |
static Boolean eric1(){ return true } | |
@CompileStatic | |
private Boolean isEric(){ eric1() && isDbg() } | |
static Boolean useRemote(){ return true } | |
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 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" ] } | |
} | |
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<String,Object> dd | |
dd= (Map<String,Object>)dupData?.state | |
if(dd?.size()){ | |
dd.each{ String k,v-> state[k]= v } | |
} | |
Map<String,Map> dd1= (Map<String,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)) | |
} | |
} | |
} | |
} | |
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){ | |
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<Map> 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<String> att=(List<String>)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<String,Map<String,String>> 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.<br><br>Please review the settings and save to activate...<br>${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<String,String> stuff | |
stuff=[:] | |
for(Map.Entry<String,Map<String,String>>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 List<Map>fuelFLD | |
List<Map> gtFuelList(){ | |
fuelFLD= !fuelFLD ? 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 | |
List<Map>fstreams= 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<String> ifstreams=multiple ? (List<String>)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<String> attrs= multiple ? (List<String>)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<i10; i++){ | |
sb= i.toString() | |
String sid= sQ+sb | |
String attribute= sid+'attr' | |
quantRmInput(sid, attribute) | |
rmAllowLastInput(sid,attribute) | |
wremoveSetting(sid+'_type') | |
wremoveSetting(sid) | |
} | |
} | |
Boolean hasQuants(){ | |
Integer i | |
String s | |
for(i=iZ; i<i10; i++){ | |
s= i.toString() | |
String sid= sQ+s | |
String typ=gtSetStr(sid+'_type') | |
if(typ in [sCFUELSTRM,sCSENSOR]) return true | |
} | |
return false | |
} | |
/** | |
* create a fuel stream dataSource entry | |
* @param stream | |
* @return Map [(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: 'stream' q: params] | |
*/ | |
Map makeFuelDataEntry(String stream, String attr='stream'){ | |
//String s= "makeFuelDataEntry $stream " | |
if(!stream) return [:] | |
Map r=decodeStreamN(stream) | |
Integer i= iMs(r,sI) | |
String c= sMs(r,sC) | |
String n= sMs(r,sN) | |
//[i:app.id, c: 'LTS', n:sid+'_'+attribute,w:1,t: getFormattedDate(new Date())] | |
//String attribute=c+d+n+d+i.toString() | |
//String fuelNattr(){ | |
Map ent | |
ent=[(sT): 'fuel', (sID): sF+i.toString(), rid: i, sn: stream, (sDISPNM): 'Fuel Stream '+i.toString(), (sN): n, (sC): c, (sA): attr] | |
ent += checkLastUpd(ent) | |
stToPoll() | |
// 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: $ent", iN2 | |
return ent | |
} | |
/** | |
* Set browser polling needs to be done because graph uses fuel stream or synthetic data | |
*/ | |
void stToPoll(){ | |
assignSt('hasFuel',true) | |
String ii= gtSetStr(sGRPHUPDRATE) | |
Integer i= ii!=sNL ? ii.toInteger() : iN1 | |
if(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, rid: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, rid: ent.rid, (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 | |
* <br><br> | |
* Map ent=[(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: 'stream'] | |
* <br><br> | |
* Map ent=[(sT): sSENSOR, id: 'd'+rid, rid: sensor.id, displayName: sensor.displayName, a: attr] | |
* <br><br> | |
* Map ent=[(sT): 'quant', id: 'q'+[0-10]+'attr', rid: id, displayName: <entered>, a: 'quant' or actual attr, qp: [params], ent: [sensor or fuel]] | |
*/ | |
List<Map> gtDataSources(){ | |
return state.dataSources | |
} | |
/** | |
* create state.datasources from settings | |
* @param multiple - are allowed | |
* @return list of data sources and update to state.dataSources | |
* <br><br> | |
* Map ent=[(sT): 'fuel', id: 'f'+i.toString(), rid: i, sn: stream, displayName: 'Fuel Stream '+i.toString(), n: n, c: c, a: 'stream'] | |
* <br><br> | |
* Map ent=[(sT): sSENSOR, id: 'd'+rid, rid: sensor.id, displayName: sensor.displayName, a: attr] | |
* <br><br> | |
* Map ent=[(sT): 'quant', id: 'q'+[0-10]+'attr', rid: id, displayName: <entered>, a: 'quant' or actual attr, qp: [params], ent: [sensor or fuel]] | |
*/ | |
List<Map> 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<Map> 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<String> ifstreams=multiple ? (List<String>)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<String> attrs=multiple ? (List<String>)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<i10; i++){ | |
sb= i.toString() | |
String sid= sQ+sb | |
String attribute= sid+'attr' | |
String typ=gtSetStr(sid+'_type') | |
if(!(typ in [sCFUELSTRM,sCSENSOR])){ | |
quantRmInput(sid, attribute) | |
rmAllowLastInput(sid,attribute) | |
wremoveSetting(sid+'_type') | |
wremoveSetting(sid) | |
}else{ | |
if(isEric()) myDetail null,s+"processing quants ${sid}",iN2 | |
Map ent=makeQuantDataEntry(typ,sid,attribute) | |
if(ent)dataSources << ent | |
} | |
} | |
} | |
} | |
if(isEric())myDetail null,s | |
state.dataSources=dataSources | |
return dataSources | |
} | |
def addAllowLastActivity(Map ent){ | |
String attribute= sMs(ent,sA) | |
String s= "lstUpd_${ent[sID]}_${attribute}".toString() | |
String name= sMs(ent,sDISPNM) | |
input( (sTYPE): sBOOL, (sNM): s,(sTIT): "Use last modified time for stream $name attribute $attribute as value?", | |
(sREQ): false, (sMULTP): false, (sSUBOC): false, (sDEFV): false) | |
} | |
// deal with fuel selection lastupdate - which means last time this stream value was updated | |
Map checkLastUpd(Map ent){ | |
String sn= "lstUpd_${ent[sID]}_${sMs(ent,sA)}".toString() | |
if(gtSetB(sn)) return [('aa'):'lastupdate'] | |
return [:] | |
} | |
void rmAllowLastInput(String sid,String attribute){ | |
String s='lstUpd_'+sid+'_'+attribute | |
wremoveSetting(s) | |
} | |
void rmAllowLastInput(Map ent){ | |
String s= "lstUpd_${ent[sID]}_${sMs(ent,sA)}".toString() | |
wremoveSetting(s) | |
} | |
/** | |
* | |
* @param fvarn - creates a setting using this variable name | |
* @param ftit | |
* @param multiple | |
* @param allowLastActivity | |
* @return | |
*/ | |
def gatherFuelSource(String fvarn,String ftit,Boolean multiple,Boolean allowLastActivity){ | |
if(isEric())myDetail null,"gatherFuelSource $fvarn $multiple",i1 | |
// fuel streams | |
List<Map> final_streams | |
List<String> container | |
container=[] | |
List<Map>fstreams= 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) | |
List<Map>fdata=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("<b>No data found in stream</b><br><small>Please select a different Fuel Stream</small>") | |
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("<b>No fuel streams found</b><br>") | |
hubiForm_container(container, i1) | |
} | |
def fl= gtSetting(fvarn) | |
List<String> ifstreams=multiple ? (List<String>)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<Map> final_attrs | |
String attrn | |
List<String> 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<String> 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("<b>No supported Numerical Attributes</b><br><small>Please select a different Sensor</small>") | |
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<String> attrs=multiple ? (List<String>)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<String> container | |
container=[] | |
Integer i | |
String s,saveS | |
saveS=sNL | |
Boolean fndf; fndf=false | |
for(i=iZ; i<i10; i++){ | |
s= i.toString() | |
String sid= sQ+s | |
//String attribute= sid+'attr' | |
String typ=gtSetStr(sid+'_type') | |
if(typ in [sCFUELSTRM,sCSENSOR]){ | |
displayQuant(sid,allowLastActivity,false) | |
}else if(saveS==sNL){ saveS=s; fndf=true} | |
} | |
if(fndf){ | |
displayQuant(sQ+saveS,allowLastActivity,true) | |
}else{ | |
container << hubiForm_text("<b>Did not find free slot quant list</b><br><small>More than 10 quants for chart</small>") | |
hubiForm_container(container, i1) | |
} | |
if(isEric())myDetail null,"gatherQuantSource $multiple" | |
} | |
def displayQuant(String sid, Boolean allowLastActivity, Boolean create=false){ | |
List<String> container | |
container=[] | |
String attribute= sid+'attr' | |
Boolean attrOk | |
attrOk=false | |
List<String> 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<Map<String,String>> 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<Map<String,String>> 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<Map> 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<Map> dataSources= createDataSources(multiple) | |
Integer sz=dataSources.size() | |
if(ordered && multiple && sz>i1){ | |
if(isEric())debug "check order",null | |
List<String> 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<String> 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<String> 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<Map> 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<String> container | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String rid=ent.rid.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("<small></i>Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE</i></small>", | |
"graph_name_override_"+sa, | |
"%deviceName%: %attributeName%", false) | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
} | |
def local_graph_url(){ | |
List<String> container | |
container=[] | |
hubiForm_section("Local Graph URL", i1, "link", sBLK){ | |
String s= makeCallBackURL('graph/') | |
container << hubiForm_text(s, s) | |
hubiForm_container(container, i1) | |
} | |
} | |
def preview_tile(){ | |
List<String> 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<String> 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<String> 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<Map> 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<Map> 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){ | |
List<Double>val=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<Map> CgetData(Map ent, Date time, Boolean multiple=true){ | |
List<Map> return_data | |
return_data= gtDataSourceData(ent,multiple) | |
Long end=time.getTime() | |
List<Map> 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<Map> 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() | |
List<Map>res | |
res=[] | |
if(typ=='Fuel'){ | |
Map stream= findStream(sMs(ent,'sn')) | |
if(stream) | |
res=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.rid.toString() | |
if(isEric())myDetail null,"varn: $varn devs ${devs} a: ${a} rid: ${myObj(ent.rid)}",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 | |
} | |
static String scriptIncludes(){ | |
String html= """ | |
<script src="https://code.jquery.com/jquery-3.5.0.min.js" integrity="sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=" crossorigin="anonymous"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.min.js" integrity="sha256-awnFgdmMV/qmPoT6Fya+g8h/E4m0Z+UFwEHZck/HRcc=" crossorigin="anonymous"></script> | |
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> | |
""" | |
return html | |
} | |
String scriptIncludes1(Boolean isSystemType){ | |
String html=""" | |
${scriptIncludes()} | |
<script src="http://${location.hub.localIP}/local/${isSystemType ? 'webcore/' : ''}a930f16d-d5f4-4f37-b874-6b0dcfd47ace-HubiGraph.js"></script> | |
""" | |
return html | |
} | |
/* | |
* TODO: Gauge methods | |
*/ | |
def mainGauge(){ | |
mainShare1("Choose Numeric Attributes only",'gauge_title',false,false) | |
} | |
def deviceGauge(){ | |
deviceShare1(false) | |
} | |
def attributeGauge(){ | |
List<Map> dataSources= createDataSources(false) | |
dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ | |
List<String> container | |
def val | |
String dn | |
dn='unknown' | |
String typ | |
typ=dn | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
List<Map>fdata=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.rid.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("<b>Current ${typ} Value=</b>$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("<b>No recent valid ${typ} data for ${dn}</b><br><small>Please select a different data Source</small>") | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
} | |
} | |
def graphGauge(){ | |
Integer num_ | |
String nh= 'num_highlights' | |
dynamicPage((sNM): "graphSetupPage"){ | |
List<String> 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<br><small>Example</small>", "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<num_; i+=i1){ | |
switch (i){ | |
case iZ : color_="#00FF00"; break | |
case i1 : color_="#a9a67e"; break | |
case i2 : color_="#FF0000"; break | |
} | |
container << hubiForm_color("Highlight $i", "highlight${i}", color_, false) | |
container << hubiForm_text_input("Select Highlight Start Region Value ${i}", "highlight${i}_start", sBLK, false) | |
} | |
container << hubiForm_text_input("Select Highlight End Region Value ${i-i1}", "highlight_end", sBLK, false) | |
hubiForm_container(container, i1) | |
} | |
} | |
hubiForm_section("Major and Minor Tics", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_slider ((sTIT): "Number Minor Tics", (sNM): "gauge_minor_tics", (sDEFLT): i3, (sMIN): iZ, (sUNITS): " tics") | |
container << hubiForm_switch ([(sTIT): "Use Custom Tics/Labels", (sNM): "default_major_ticks", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('default_major_ticks')){ | |
String vn='gauge_major_tics' | |
if(settings[vn] == null){ | |
settings[vn]=i3 | |
app.updateSetting(vn, i3) | |
} | |
container << hubiForm_slider ((sTIT): "Number Major Tics", (sNM): vn, (sDEFLT): i3, (sMIN): iZ, (sMAX): i20, (sUNITS): " tics") | |
Integer tic | |
for(tic=iZ; tic<gtSetI(vn); tic++){ | |
container << hubiForm_text_input("Input the Label for Tick ${tic+i1}", "tic_title${tic}", "Label", false) | |
} | |
} | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
String getData_gauge(){ | |
String val | |
val='0.0' | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
Map ent= dataSources[iZ] | |
val= getLatestVal(ent,false) | |
} | |
return JsonOutput.toJson( [(sVAL): extractNumber(val)] ) | |
} | |
Map getOptions_gauge(){ | |
List tic_labels | |
tic_labels=[] | |
if(gtSetB('default_major_ticks')){ | |
Integer tic | |
for(tic=iZ; tic<gtSetI('gauge_major_tics'); tic++){ | |
tic_labels += gtSetStr("tic_title${tic}") | |
} | |
} | |
String redColor, redFrom, redTo, yellowColor, yellowFrom, yellowTo, greenColor, greenFrom, greenTo | |
redColor=sBLK | |
redFrom=sBLK | |
redTo=sBLK | |
yellowColor=sBLK | |
yellowFrom=sBLK | |
yellowTo=sBLK | |
greenColor=sBLK | |
greenFrom=sBLK | |
greenTo=sBLK | |
switch (gtSetI('num_highlights')){ | |
case i3: | |
redColor=gtSetB('highlight2_color_transparent') ? sTRANSPRNT : gtSetStr('highlight2_color') | |
redFrom=gtSetStr('highlight2_start') | |
case i2: | |
yellowColor=gtSetB('highlight1_color_transparent') ? sTRANSPRNT : gtSetStr('highlight1_color') | |
yellowFrom=gtSetStr('highlight1_start') | |
case i1: | |
greenColor=gtSetB('highlight0_color_transparent') ? sTRANSPRNT : gtSetStr('highlight0_color') | |
greenFrom=gtSetStr('highlight0_start') | |
} | |
switch (gtSetI('num_highlights')){ | |
case i3: | |
redTo=gtSetStr('highlight_end') | |
yellowTo=gtSetStr('highlight2_start') | |
greenTo = gtSetStr('highlight1_start') | |
break | |
case i2: | |
yellowTo=gtSetStr('highlight_end') | |
greenTo = gtSetStr('highlight1_start') | |
break | |
case i1: | |
greenTo = gtSetStr('highlight_end') | |
break | |
} | |
Map options=[ | |
"graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), | |
"graphOptions": [ | |
"width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, | |
"height": gtSetB(sGRPHSTATICSZ) ? graph_v_size: s100PCT, | |
"min": minValue_, | |
"max": maxValue_, | |
"greenFrom": greenFrom, | |
"greenTo": greenTo, | |
"greenColor": greenColor, | |
"yellowFrom": yellowFrom, | |
"yellowTo": yellowTo, | |
"yellowColor": yellowColor, | |
"redFrom": redFrom, | |
"redTo": redTo, | |
"redColor": redColor, | |
"backgroundColor": gtSetB('graph_background_color_transparency') ? sTRANSPRNT: gtSetStr('graph_background_color'), | |
"majorTicks" : gtSetB('default_major_ticks') ? tic_labels : sBLK, | |
"minorTicks" : gauge_minor_tics | |
] | |
] | |
return options | |
} | |
String getGraph_gauge(){ | |
String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String html=""" | |
<!DOCTYPE html> | |
<html style="${fullSizeStyle}"> | |
<head> | |
${scriptIncludes()} | |
<script type="text/javascript"> | |
google.charts.load('current',{'packages':['gauge']}); | |
let options=[]; | |
let subscriptions={}; | |
let graphData={}; | |
let websocket; | |
let chart; | |
let callbackEvent=null; | |
class Loader{ | |
constructor(){ | |
this.elem=jQuery(jQuery(document.body).prepend(` | |
<div class="loaderContainer"> | |
<div class="dotsContainer"> | |
<div class="dot"></div> | |
<div class="dot"></div> | |
<div class="dot"></div> | |
</div> | |
<div class="text"></div> | |
</div> | |
`).children()[0]); | |
} | |
setText(text){ | |
this.elem.find('.text').text(text); | |
} | |
remove(){ | |
this.elem.remove(); | |
} | |
} | |
function getOptions(){ | |
return jQuery.get("${makeCallBackURL('getOptions/')}", (data) =>{ | |
options=data; | |
console.log("Got Options"); | |
console.log(options); | |
}); | |
} | |
function getSubscriptions(){ | |
return jQuery.get("${makeCallBackURL('getSubscriptions/')}", (data) =>{ | |
console.log("Got Subscriptions"); | |
console.log(data); | |
subscriptions=data; | |
}); | |
} | |
function getGraphData(){ | |
return jQuery.get("${makeCallBackURL('getData/')}", (data) =>{ | |
console.log("Got Graph Data"); | |
console.log(data); | |
graphData=data; | |
}); | |
} | |
function parseEvent(event){ | |
let deviceId=event.deviceId; | |
//only accept relevent events | |
if(subscriptions.id == deviceId && subscriptions.attribute.includes(event.name)){ | |
let value=event.value; | |
graphData.value=parseFloat(value.match(/[0-9.]+/g)[0]); | |
update(); | |
} | |
} | |
function update(callback){ | |
drawChart(callback); | |
} | |
async function aupdate(){ | |
let old=graphData.value; | |
await getGraphData(); | |
if(old != graphData.value) drawChart(); | |
} | |
async function onLoad(){ | |
//let loader=new Loader(); | |
//first load | |
//loader.setText('Getting options (1/4)'); | |
await getOptions(); | |
//loader.setText('Getting device data (2/4)'); | |
await getSubscriptions(); | |
//loader.setText('Getting events (3/4)'); | |
await getGraphData(); | |
//loader.setText('Drawing chart (4/4)'); | |
chart=new google.visualization.Gauge(document.getElementById("timeline")); | |
update(() =>{ | |
//destroy loader when we are done with it | |
//loader.remove(); | |
}); | |
if(subscriptions.id=='poll'){ | |
if(options.graphUpdateRate > 0){ | |
setInterval(() =>{ | |
aupdate(); | |
}, options.graphUpdateRate); | |
} | |
} else{ | |
//start our update cycle | |
//start websocket | |
websocket=new WebSocket("ws://" + location.hostname + "/eventsocket"); | |
websocket.onopen=() =>{ | |
console.log("WebSocket Opened!"); | |
} | |
websocket.onmessage=(event) =>{ | |
parseEvent(JSON.parse(event.data)); | |
} | |
} | |
//attach resize listener | |
window.addEventListener("resize", () =>{ | |
drawChart(); | |
}); | |
} | |
function onBeforeUnload(){ | |
if(websocket) websocket.close(); | |
} | |
function drawChart(callback){ | |
let dataTable=new google.visualization.DataTable(); | |
dataTable.addColumn('string', 'Label'); | |
dataTable.addColumn('number', 'Value'); | |
dataTable.addRow(['${gauge_title}', graphData.value]); | |
var formatter=new google.visualization.NumberFormat( | |
{suffix: "${gauge_units}", pattern: "${gauge_number_format}"} | |
); | |
formatter.format(dataTable, 1); | |
if(callbackEvent){ | |
google.visualization.events.removeListener(callbackEvent); | |
callbackEvent=null; | |
} | |
//if we have a callback | |
if(callback){ | |
callbackEvent=google.visualization.events.addListener(chart, 'ready', callback); | |
} | |
chart.draw(dataTable, options.graphOptions); | |
} | |
google.charts.setOnLoadCallback(onLoad); | |
window.onBeforeUnload=onBeforeUnload; | |
</script> | |
</head> | |
<body style="${fullSizeStyle}"> | |
<div id="timeline" style="${fullSizeStyle}" align="center"></div> | |
</body> | |
</html> | |
""" | |
return html | |
} | |
//oauth endpoints | |
Map getSubscriptions_gauge(){ | |
Map subscriptions | |
subscriptions=[:] | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
Map ent= dataSources[iZ] | |
String typ=sMs(ent,sT).capitalize() | |
if(typ==sCSENSOR){ | |
subscriptions=[ | |
(sID): ent.rid, | |
(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): "<b>Select graph update rate</b><br><small>(For panel viewing; the refresh rate of the graph)</small>", (sMULTP): false, (sREQ): false, options: opt, (sDEFV): defl) | |
} | |
/** refresh rate for graphs with fuel streams */ | |
@Field static List<Map<String,String>> 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<Map<String,String>> 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<Map> dataSources=gtDataSources() | |
Map<String,List<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<String> container | |
hubiForm_section("Graph Size", i1, sBLK, sBLK){ | |
container=[] | |
input( (sTYPE): sBOOL, (sNM): sGRPHSTATICSZ,(sTIT): "<b>Set size of Graph?</b><br><small>(False=Fill Window)</small>", (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<Map> decimalsEnum= [ [0:"None (123)"], [1: "One (123.1)"], [2: "Two (123.12)"], [3: "Three (123.123)"], [4: "Four (123.1234)"] ] | |
def attributeBar(){ | |
List<Map> dataSources= createDataSources(true) | |
dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ | |
List<String> 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.rid.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): "<b>Number of Decimal Places to Display</b>", | |
(sMULTP): false, (sREQ): false, options: decimalsEnum, (sDEFV): 1) | |
container << hubiForm_text_input("<b>Scale Factor for Values</b><br><small>Example: To scale down by 10X, input 0.1<br>Leave as <b>1</b> for unchanged</small>", | |
"attribute_"+sa+"_scale", | |
s1, false) | |
container << hubiForm_text_input("<b>Override ${typ} Name on Graph</b><small></i><br>Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE</i></small>", | |
"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<String> container | |
hubiForm_section("General Options", i1, sBLK, sBLK){ | |
container=[] | |
input( (sTYPE): sENUM, (sNM): 'graph_type',(sTIT): "<b>Select graph type</b>", (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<Map> 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<Map> 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=""" | |
<!DOCTYPE html> | |
<html style="${fullSizeStyle}"> | |
<head> | |
${scriptIncludes()} | |
<script type="text/javascript"> | |
google.charts.load('current',{'packages':['corechart']}); | |
let options=[]; | |
let subscriptions={}; | |
let graphData={}; | |
//stack for accumulating points to average | |
let stack={}; | |
let websocket; | |
let chart; | |
let callbackEvent=null; | |
class Loader{ | |
constructor(){ | |
this.elem=jQuery(jQuery(document.body).prepend(` | |
<div class="loaderContainer"> | |
<div class="dotsContainer"> | |
<div class="dot"></div> | |
<div class="dot"></div> | |
<div class="dot"></div> | |
</div> | |
<div class="text"></div> | |
</div> | |
`).children()[0]); | |
} | |
setText(text){ | |
this.elem.find('.text').text(text); | |
} | |
remove(){ | |
this.elem.remove(); | |
} | |
} | |
function getOptions(){ | |
return jQuery.get("${makeCallBackURL('getOptions/')}", (data) =>{ | |
options=data; | |
console.log("Got Options"); | |
console.log(options); | |
}); | |
} | |
function getSubscriptions(){ | |
return jQuery.get("${makeCallBackURL('getSubscriptions/')}", (data) =>{ | |
console.log("Got Subscriptions"); | |
console.log(data); | |
subscriptions=data; | |
}); | |
} | |
function getGraphData(){ | |
return jQuery.get("${makeCallBackURL('getData/')}", (data) =>{ | |
console.log("Got Graph Data"); | |
console.log(data); | |
graphData=data; | |
}); | |
} | |
function parseEvent(event){ | |
let odeviceId=event.deviceId; | |
let deviceId="d"+odeviceId; | |
//only accept relevent events | |
if(subscriptions.ids.includes(deviceId) && subscriptions.attributes[deviceId].includes(event.name)){ | |
let value=event.value; | |
let attribute=event.name; | |
console.log("Got Name: ", attribute, "Value: ", value); | |
graphData[sdeviceId][attribute].current=value; | |
graphData[sdeviceId][attribute].date=new Date(); | |
//update if we are realtime | |
if(options.graphUpdateRate === 0) update(); | |
} | |
} | |
async function aupdate(){ | |
await getGraphData(); | |
drawChart(); | |
} | |
function update(callback){ | |
drawChart(callback); | |
} | |
async function onLoad(){ | |
//append our css | |
jQuery(document.head).append(` | |
<style> | |
.loaderContainer{ | |
position: fixed; | |
z-index: 100; | |
width: 100%; | |
height: 100%; | |
background-color: white; | |
display: flex; | |
flex-flow: column nowrap; | |
justify-content: center; | |
align-items: middle; | |
} | |
.dotsContainer{ | |
height: 60px; | |
padding-bottom: 10px; | |
display: flex; | |
flex-flow: row nowrap; | |
justify-content: center; | |
align-items: flex-end; | |
} | |
@keyframes bounce{ | |
0%{ | |
transform: translateY(0); | |
} | |
50%{ | |
transform: translateY(-50px); | |
} | |
100%{ | |
transform: translateY(0); | |
} | |
} | |
.dot{ | |
box-sizing: border-box; | |
margin: 0 25px; | |
width: 10px; | |
height: 10px; | |
border: solid 5px black; | |
border-radius: 5px; | |
animation-name: bounce; | |
animation-duration: 1s; | |
animation-iteration-count: infinite; | |
} | |
.dot:nth-child(1){ | |
animation-delay: 0ms; | |
} | |
.dot:nth-child(2){ | |
animation-delay: 333ms; | |
} | |
.dot:nth-child(3){ | |
animation-delay: 666ms; | |
} | |
.text{ | |
font-family: Arial; | |
font-weight: 200; | |
font-size: 2rem; | |
text-align: center; | |
} | |
</style> | |
`); | |
let loader=new Loader(); | |
//first load | |
loader.setText('Getting options (1/4)'); | |
await getOptions(); | |
loader.setText('Getting device data (2/4)'); | |
await getSubscriptions(); | |
loader.setText('Getting events (3/4)'); | |
await getGraphData(); | |
loader.setText('Drawing chart (4/4)'); | |
if(options.graphType == 1){ | |
chart=new google.visualization.BarChart(document.getElementById("timeline")); | |
} else{ | |
chart=new google.visualization.ColumnChart(document.getElementById("timeline")); | |
} | |
update(() =>{ | |
//destroy loader when we are done with it | |
loader.remove(); | |
}); | |
if(subscriptions.id=='poll'){ | |
if(options.graphUpdateRate > 0){ | |
setInterval(() =>{ | |
aupdate(); | |
}, options.graphUpdateRate); | |
} | |
} else{ | |
//start our update cycle | |
if(options.graphUpdateRate !== -1){ | |
//start websocket | |
websocket=new WebSocket("ws://" + location.hostname + "/eventsocket"); | |
websocket.onopen=() =>{ | |
console.log("WebSocket Opened!"); | |
} | |
websocket.onmessage=(event) =>{ | |
parseEvent(JSON.parse(event.data)); | |
} | |
if(options.graphUpdateRate !== 0){ | |
setInterval(() =>{ | |
update(); | |
}, options.graphUpdateRate); | |
} | |
} | |
} | |
//attach resize listener | |
window.addEventListener("resize", () =>{ | |
drawChart(); | |
}); | |
} | |
function onBeforeUnload(){ | |
if(websocket) websocket.close(); | |
} | |
function formatValue(val, opts){ | |
val=val * parseFloat(opts.scale); | |
return val.toFixed(opts.decimals); | |
} | |
function drawChart(callback){ | |
let now=new Date().getTime(); | |
let min=now - options.graphTimespan; | |
const date_options={ | |
weekday: "long", | |
year: "numeric", | |
month:"long", | |
day:"numeric" | |
}; | |
const time_options ={ | |
hour12 : true, | |
hour: "2-digit", | |
minute: "2-digit", | |
second: "2-digit" | |
}; | |
const dataTable=new google.visualization.arrayToDataTable([[{ type: 'string', label: 'Device' },{ type: 'number', label: 'Value'},{ role: "style" },{ role: "tooltip" },{ role: "annotation" },]]); | |
subscriptions.order.forEach(orderStr =>{ | |
const splitStr=orderStr.split('_'); | |
const deviceId=splitStr[1]; | |
const attr=splitStr[2]; | |
const event=graphData[deviceId][attr]; | |
const cur_=parseFloat(event.current); | |
var cur_String=''; | |
var units_=``; | |
var t_date=new Date(event.date); | |
var date_String=t_date.toLocaleDateString("en-US",date_options); | |
var time_String=t_date.toLocaleTimeString("en-US",time_options); | |
const name=subscriptions.labels[deviceId][attr].replace('%deviceName%', subscriptions.sensors[deviceId].displayName).replace('%attributeName%', attr); | |
const colors=subscriptions.colors[deviceId][attr]; | |
if(colors.showAnnotation == true){ | |
cur_String=`\${formatValue(cur_, colors)}\${colors.annotation_units} `; | |
units_=`\${colors.annotation_units}`; | |
} | |
var stats_=`\${name}\nCurrent: \${event.current}\${units_}\nDate: \${date_String} \${time_String}` | |
dataTable.addRow([name, cur_, `{color: \${colors.backgroundColor}; | |
stroke-color: \${colors.currentValueBorderColor}; | |
fill-opacity: \${colors.opacity}; | |
stroke-width: \${colors.currentValueBorderLineSize};}`, | |
`\${stats_}`, | |
`\${cur_String} `]); | |
}); | |
if(callbackEvent){ | |
google.visualization.events.removeListener(callbackEvent); | |
callbackEvent=null; | |
} | |
//if we have a callback | |
if(callback){ | |
callbackEvent=google.visualization.events.addListener(chart, 'ready', callback); | |
} | |
chart.draw(dataTable, options.graphOptions); | |
} | |
google.charts.setOnLoadCallback(onLoad); | |
window.onBeforeUnload=onBeforeUnload; | |
</script> | |
</head> | |
<body style="${fullSizeStyle}"> | |
<div id="timeline" style="${fullSizeStyle}" align="center"></div> | |
</body> | |
</html> | |
""" | |
return html | |
} | |
//oauth endpoints | |
Map getSubscriptions_bar(){ | |
List _ids=[] | |
Map _attributes=[:] | |
Map labels=[:] | |
Map colors=[:] | |
Boolean isPoll | |
isPoll=gtStB('hasFuel') | |
// TODO | |
List<Map> 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<String,Map<String,String>> 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<String> startTypes=['on', 'open', 'active', 'muted', 'present', 'true', 'detected', 'playing', 'unlocked', 'sleeping', 'wet'] | |
@Field static List<String> 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<Map> 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<Map<String,String>> 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<Map> dataSources= createDataSources(true) | |
//state.count_=0 | |
dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ | |
List<String> 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.rid.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<small></i><br>Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE</i></small>", | |
"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<Map<String,String>> 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":" |