Skip to content

Instantly share code, notes, and snippets.

@imnotbob
Created October 24, 2023 23:46
Show Gist options
  • Save imnotbob/7f46a6f6efcfe1e32e7365de0614b51c to your computer and use it in GitHub Desktop.
Save imnotbob/7f46a6f6efcfe1e32e7365de0614b51c to your computer and use it in GitHub Desktop.
webcore-fuel-stream.groovy for cloud endpoint in child app
/*
* 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('<', '&lt;').replaceAll('>','&gt;')
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 &amp;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":"