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":"6 Hours"]] | |
dynamicPage((sNM): "graphSetupPage"){ | |
List<String> container | |
hubiForm_section("General Options", i1, "directions", sBLK){ | |
inputGraphUpdateRate() | |
input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "<b>Select Time span to Graph</b>", (sMULTP): false, (sREQ): false, options: timespanEnum, (sDEFV): "43200000") | |
input( (sTYPE): sENUM, (sNM): "graph_combine_rate",(sTIT): "<b>Combine events with events less than ? apart</b>", (sMULTP): false, (sREQ): false, options: lOpts, (sDEFV): s0) | |
container=[] | |
container << hubiForm_color ("Background", "graph_background", sWHT, false) | |
} | |
gatherGraphSize() | |
hubiForm_section("Device Name Display", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_color ("Device Text", "graph_axis", sWHT, false) | |
container << hubiForm_font_size ((sTIT): "Device", (sNM): "graph_axis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
String getData_timeline(){ | |
Map resp=[:] | |
// Date now=new Date() | |
Date then | |
then=new Date() | |
Long graph_time | |
use (TimeCategory){ | |
Double val=Double.parseDouble(gtSetStr('graph_timespan'))/1000.0 | |
then -= (val.toInteger()).seconds | |
graph_time=then.getTime() | |
} | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
//String typ=sMs(ent,sT).capitalize() | |
resp[sid]= resp[sid] ?: [:] | |
List<Map> data=CgetData(ent, then) | |
// log.warn "got sensor: $sensor attribute: $attribute data1: $data" | |
List<Map>data1=data.collect{ Map it-> [(sDT): it.t, (sVAL): "${it[sVAL]}".toString()]} | |
resp[sid][attribute]=data1.findAll{ Map it-> lMs(it,sDT) > graph_time} | |
List<Map> temp=([]+data1) as List<Map> | |
//temp=temp.sort{ (Long)it.date } | |
resp[sid][attribute]=temp | |
// log.warn "FINAL got sensor: $sensor attribute: $attribute data1: $temp" | |
} | |
} | |
return JsonOutput.toJson(resp) | |
} | |
Map getOptions_timeline(){ | |
if(isEric())myDetail null,"getChartOptions_timeline",i1 | |
List colors=[] | |
List<Map> order=hubiTools_get_order(gtSetStr('graph_order')) | |
for(Map device in order){ | |
//String sa="${sid}_${attribute}".toString() | |
String attrib_string="attribute_${device[sID]}_${device[sATTR]}_line_color" | |
String transparent_attrib_string= attrib_string+"_transparent" | |
colors << (gtSetB(transparent_attrib_string) ? sTRANSPRNT : settings[attrib_string]) | |
} | |
Map options=[ | |
"graphTimespan": Integer.parseInt(gtSetStr('graph_timespan')), | |
"graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), | |
"graphCombine_msecs": Integer.parseInt(gtSetStr('graph_combine_rate')), | |
"graphOptions": [ | |
"width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, | |
"height": gtSetB(sGRPHSTATICSZ) ? graph_v_size: s100PCT, | |
"timeline": [ | |
"rowLabelStyle": ["fontSize": graph_axis_font, "color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color], | |
"barLabelStyle": ["fontSize": graph_axis_font], | |
], | |
"haxis" : [ "text": ["fontSize": "24px"]], | |
"backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), | |
"colors" : colors | |
], | |
] | |
if(isEric())myDetail null,"getChartOptions_timeline $options" | |
return options | |
} | |
String getGraph_timeline(){ | |
String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String html=""" | |
<!DOCTYPE html> | |
<html style="${fullSizeStyle}"> | |
<head> | |
${scriptIncludes1(isSystemType())} | |
<script type="text/javascript"> | |
//google.load("visualization", "1.1",{packages:["timeline"]}); | |
google.charts.load('current',{'packages':['timeline']}); | |
google.charts.setOnLoadCallback(onLoad); | |
let options=[]; | |
let subscriptions={}; | |
let graphData={}; | |
let unparsedData={}; | |
let websocket; | |
let chart; | |
let callbackEvent=null; | |
let tooltipEvent=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) =>{ | |
console.log("Got Options"); | |
console.log(data); | |
options=data; | |
}); | |
} | |
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); | |
unparsedData=data; | |
let now=new Date().getTime(); | |
let min=now; | |
min -= options.graphTimespan; | |
//parse data | |
Object.entries(unparsedData).forEach(([id, allEvents]) =>{ | |
graphData[id]={}; | |
Object.entries(allEvents).forEach(([attribute, events]) =>{ | |
console.log("graphData reset"); | |
console.log(id); | |
console.log(attribute); | |
graphData[id][attribute]=[]; | |
const start_event=subscriptions.definitions[id][attribute].start; | |
console.log(start_event); | |
const end_event=subscriptions.definitions[id][attribute].end; | |
console.log(end_event); | |
const thisOut=graphData[id][attribute]; | |
var date; | |
var seconds=options.graphCombine_msecs; | |
var skip_trigger; | |
if(events.length > 0){ | |
//if our first event is an end event, start at 1 | |
thisOut.push(evalTest(start_event, events[0].value) ?{ start: events[0].date } :{ end: events[0].date }); | |
for(let i=1; i < events.length; i++){ | |
const is_start=evalTest(start_event, events[i].value); | |
const is_end=evalTest(end_event, events[i].value); | |
//always add the first event | |
if(is_end && !thisOut[thisOut.length - 1].end){ | |
thisOut[thisOut.length - 1].end=events[i].date; | |
} else if(is_start && thisOut[thisOut.length - 1].end){ | |
/*TCH - Look for more than 5 minutes between events*/ | |
if(events[i].date - thisOut[thisOut.length - 1].end > seconds){ | |
thisOut.push({ start: events[i].date }); | |
} else{ | |
skip_trigger=true; | |
} | |
} else if (is_end && skip_trigger){ | |
thisOut[thisOut.length - 1].end=events[i].date; | |
skip_trigger=false; | |
} | |
} | |
} | |
//if it's already on, add an event | |
else if(evalTest(start_event, subscriptions.sensors[id].currentStates.find((it) => it.name == attribute).value)){ | |
thisOut.push({ start: min }); | |
} | |
}); | |
}); | |
console.log("Parsed Data"); | |
console.log(Object.assign({}, graphData)); | |
}); | |
} | |
function parseEvent(event){ | |
const now=new Date().getTime(); | |
let odeviceId=event.deviceId; | |
let deviceId="d"+odeviceId; | |
let attribute=event.name; | |
//only accept relevent events | |
if(Object.keys(subscriptions.sensors).includes("" + deviceId) && Object.keys(subscriptions.definitions[deviceId]).includes(attribute)){ | |
const pastEvents=graphData[deviceId][attribute]; | |
if(pastEvents.length > 0){ | |
const start_event=subscriptions.definitions[deviceId][attribute].start; | |
const end_event=subscriptions.definitions[deviceId][attribute].end; | |
const is_start=evalTest(start_event, event.value); | |
const is_end=evalTest(end_event, event.value); | |
if(is_end && !pastEvents[pastEvents.length - 1].end) pastEvents[pastEvents.length - 1].end=now; | |
else if(is_start && pastEvents[pastEvents.length - 1].end) pastEvents.push({ start: now }); | |
} else{ | |
pastEvents.push({ start: now }); | |
} | |
//update if we are realtime | |
if(options.graphUpdateRate === 0) update(); | |
} | |
} | |
function evalTest(evalStrPre, value){ | |
const evalStr=he.decode(evalStrPre); | |
const operatorMatch=evalStr.replace(' ', '').match(/(<=)|(>=)|<|>|(==)|(!=)/g); | |
if(operatorMatch){ | |
const operator=operatorMatch[0]; | |
const rest=parseFloat(evalStr.replace(operator, '')); | |
const floatValue=parseFloat(value); | |
switch (operator){ | |
case '<': | |
return floatValue < rest; | |
case '>': | |
return floatValue > rest; | |
case '==': | |
return floatValue == rest; | |
case '!=': | |
return floatValue != rest; | |
case '<=': | |
return floatValue <= rest; | |
case '>=': | |
return floatValue >= rest; | |
default: | |
} | |
} else{ | |
return value == evalStr; | |
} | |
} | |
async function aupdate(){ | |
await getGraphData(); | |
//drawChart(); | |
update(); | |
} | |
async function update(callback){ | |
let now=new Date().getTime(); | |
let min=now; | |
min -= options.graphTimespan; | |
//parse data | |
//boot old data | |
Object.entries(graphData).forEach(([id, allEvents]) =>{ | |
Object.entries(allEvents).forEach(([attribute, events]) =>{ | |
//shift left points and mark for deletion if applicable | |
let newArr=events.map(it =>{ | |
let ret={ ...it } | |
if(it.end && it.end < min){ | |
ret={}; | |
} | |
else if(it.start && it.start < min) ret.start=min; | |
return ret; | |
}); | |
//delete non-existant nodes | |
newArr=newArr.filter(it => it.start || it.end); | |
//merge events | |
let mergedArr=[]; | |
newArr.forEach((event, index) =>{ | |
if(index === 0) mergedArr.push(event); | |
else{ | |
if(event.start - mergedArr[mergedArr.length - 1].end <= options.graphCombine_msecs){ | |
mergedArr[mergedArr.length - 1].end=event.end; | |
} else mergedArr.push(event); | |
} | |
}); | |
graphData[id][attribute]=mergedArr; | |
}); | |
}); | |
drawChart(now, min, 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)'); | |
chart=new google.visualization.Timeline(document.getElementById("timeline")); | |
//update data | |
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", () =>{ | |
let now=new Date().getTime(); | |
let min=now; | |
min -= options.graphTimespan; | |
drawChart(now, min); | |
}); | |
} | |
function getToolTip(name, start, end){ | |
var html = "<div class='mdl-layout__header' style='display: block; background:#033673; width: 100%; padding-top:10px; padding-bottom:5px; overflow: hidden;'>"; | |
html += "<div class='mdl-layout__header-row'"; | |
html += "<span class='mdl-layout__title' style='font-size: 14px; color:#FFFFFF !important; width: auto; font-family:Roboto, Helvetica, Arial, sans-serif !important;'>"; | |
html += name; | |
html += "</span>"; | |
html += "</div>"; | |
html += "</div>"; | |
html += "<div class='mdl-grid' style='padding: 5px; background:#FFFFFF; font-family:Roboto, Helvetica, Arial, sans-serif !important;'>" | |
html += "<div class='mdl-cell mdl-cell--12-col-desktop mdl-cell--8-col-tablet mdl-cell--4-col-phone' style='margin-bottom: 5px; padding: 5px;' >"; | |
html=html+ start.toDateString()+" at "+start.toLocaleTimeString('en-US'); | |
html += "</div>"; | |
html += "<div class='mdl-cell mdl-cell--12-col-desktop mdl-cell--8-col-tablet mdl-cell--4-col-phone' style='margin-bottom: 5px; padding: 5px;'>"; | |
html=html+ end.toDateString()+" at "+end.toLocaleTimeString('en-US'); | |
html += "</div>"; | |
//var html="<p style='font-family:courier,arial,helvetica; font-size: 14px;'><b>"+name+"</b><br><hr><br>"; | |
//html += "Start: "+start.toDateString()+" at "+start.toLocaleTimeString('en-US')+"<br>"; | |
//html += "End: "+end.toDateString()+" at "+start.toLocaleTimeString('en-US')+"<br>"; | |
return html; | |
} | |
function drawChart(now, min, callback){ | |
let dataTable=new google.visualization.DataTable(); | |
dataTable.addColumn({ type: 'string', id: 'Device' }); | |
dataTable.addColumn({ type: 'date', id: 'Start' }); | |
dataTable.addColumn({ type: 'date', id: 'End' }); | |
dataTable.addColumn({ type: 'string', 'role': 'tooltip', 'p':{'html': true}}); | |
subscriptions.order.forEach(orderStr =>{ | |
const splitStr=orderStr.split('_'); | |
const id=splitStr[1]; | |
const attribute=splitStr[2]; | |
const events=graphData[id][attribute]; | |
let newArr=[...events]; | |
//add endpoints for orphans | |
newArr=newArr.map((it) =>{ | |
if(!it.start){ | |
return{...it, start: min } | |
} | |
else if(!it.end) return{...it, end: now} | |
return it; | |
}); | |
//add endpoint buffers | |
if(newArr.length == 0){ | |
newArr.push({ start: min, end: min }); | |
newArr.push({ start: now, end: now }); | |
} else{ | |
if(newArr[0].start != min) newArr.push({ start: min, end: min }); | |
if(newArr[newArr.length - 1].end != now) newArr.push({ start: now, end: now }); | |
} | |
let name=subscriptions.sensors[id].displayName; | |
dataTable.addRows(newArr.map((parsed) => [ | |
subscriptions.labels[id][attribute].replace('%deviceName%', name).replace('%attributeName%', attribute), | |
new Date(parsed.start), | |
new Date(parsed.end), | |
getToolTip( | |
subscriptions.labels[id][attribute].replace('%deviceName%', name).replace('%attributeName%', attribute), | |
new Date(parsed.start), | |
new Date(parsed.end) ) | |
])); | |
}); | |
if(tooltipEvent){ | |
google.visualization.events.removeListener(tooltipEvent); | |
tooltipEvent=null; | |
} | |
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); | |
if(!tooltipEvent){ | |
tooltipEvent=google.visualization.events.addListener(chart, 'onmouseover', tooltipHandler); | |
} | |
function tooltipHandler(e){ | |
if(e.row != null){ | |
jQuery(".google-visualization-tooltip").html(dataTable.getValue(e.row,3)).css({width:"auto",height:"auto"}); | |
} | |
} | |
} | |
</script> | |
</head> | |
<body style="${fullSizeStyle}"> | |
<div id="timeline" style="${fullSizeStyle}" align="center"></div> | |
</body> | |
</html> | |
""" | |
return html | |
} | |
//oauth endpoints | |
Map getSubscriptions_timeline(){ | |
List _ids=[] | |
Map definitions=[:] | |
Map labels=[:] | |
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 | |
definitions[sid]=[:] | |
labels[sid]=[:] | |
definitions[sid][attribute]=[(sSTART): settings["attribute_${sa}_start"] ?: sSPC, | |
(sEND): settings["attribute_${sa}_end"] ?: sSPC ] | |
labels[sid][attribute]=settings["graph_name_override_${sa}"] | |
} | |
} | |
Map sensors_fmt=gtSensorFmt(true) | |
List order=gtSetStr('graph_order') ? parseJson(gtSetStr('graph_order')) : [] | |
Map subscriptions=[ | |
(sID): isPoll ? sPOLL : sSENSOR, | |
"ids": _ids, | |
'sensors': sensors_fmt, | |
"definitions": definitions, | |
"labels": labels, | |
"order": order | |
] | |
return subscriptions | |
} | |
/* | |
* TODO: Timegraph methods | |
*/ | |
def mainTimegraph(){ | |
mainShare1('Choose Numeric Attributes only','graph_timespan') | |
} | |
def deviceTimegraph(){ | |
// wremoveSetting('graph_timespan') | |
deviceShare1() | |
} | |
def attributeTimegraph(){ | |
attributeShare1() | |
} | |
def graphTimegraph(){ | |
List<Map<String,String>> timespanEnum2=[ | |
["10":"10 Milliseconds"], ["1000":"1 Second"], ["5000":"5 Seconds"], ["30000":"30 Seconds"], | |
["60000":"1 Minute"], ["120000":"2 Minutes"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], | |
["1800000":"30 minutes"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], | |
["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"], ["1209600000":"2 Weeks"], | |
["2629800000":"1 Month"]] | |
dynamicPage((sNM): "graphSetupPage"){ | |
List<String> container | |
container=[] | |
hubiForm_section("General Options", i1, sBLK, sBLK){ | |
//input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "<b>Select Time span to Graph</b>", (sMULTP): false, (sREQ): true, options: timespanEnum, (sDEFV): "43200000") | |
input( (sTYPE): sENUM, (sNM): 'graph_point_span',(sTIT): "<b>Integration Time</b><br><small>(The amount of time each data point covers)</small>", | |
(sMULTP): false, (sREQ): true, options: timespanEnum2, (sDEFV): "300000", (sSUBOC): true) | |
inputGraphUpdateRate("300000") | |
container=[] | |
container << hubiForm_sub_section("Graph Time Span<br><small>Amount of time the graph covers</small>") | |
if(graph_timespan_weeks == null){ | |
app.updateSetting("graph_timespan_weeks", iZ) | |
app.updateSetting("graph_timespan_days", i1) | |
app.updateSetting("graph_timespan_hours", iZ) | |
app.updateSetting("graph_timespan_minutes", iZ) | |
settings["graph_timespan_weeks"]=iZ | |
settings["graph_timespan_days"]=i1 | |
settings["graph_timespan_hours"]=iZ | |
settings["graph_timespan_minutes"]=iZ | |
} | |
container << hubiForm_slider ((sTIT): "<b>Weeks</b>", (sNM): "graph_timespan_weeks", | |
(sDEFLT): iZ, (sMIN): iZ, (sMAX): 104, (sUNITS): " weeks", (sSUBONCHG): true) | |
container << hubiForm_slider ((sTIT): "<b>Days</b>", (sNM): "graph_timespan_days", | |
(sDEFLT): iZ, (sMIN): iZ, (sMAX): 30, (sUNITS): " days", (sSUBONCHG): true) | |
container << hubiForm_slider ((sTIT): "<b>Hours</b>", (sNM): "graph_timespan_hours", | |
(sDEFLT): iZ, (sMIN): iZ, (sMAX): 24, (sUNITS): " hours", (sSUBONCHG): true) | |
container << hubiForm_slider ((sTIT): "<b>Minutes</b>", (sNM): "graph_timespan_minutes", | |
(sDEFLT): iZ, (sMIN): iZ, (sMAX): 60, (sUNITS): " minutes", (sSUBONCHG): true) | |
Long msecs | |
if(graph_timespan_weeks==null){ | |
msecs=86400000L | |
}else{ | |
msecs= Math.round((Double)(graph_timespan_weeks)*604800000+ | |
(Double)(graph_timespan_days)*86400000+ | |
(Double)(graph_timespan_hours)*3600000+ | |
(Double)(graph_timespan_minutes)*60000) | |
} | |
app.updateSetting("graph_timespan", [(sTYPE): "number", (sVAL): msecs]) | |
settings["graph_timespan"]=msecs | |
Integer points=gtSetStr('graph_point_span') ? (msecs/Double.parseDouble(gtSetStr('graph_point_span'))).toInteger() : 280 | |
if(points > 2000){ | |
container << hubiForm_text ("""<span style="color: red; font-weight: bold;">WARNING:</span> <b>${(points)} Points </b>will be generated per Attribute per Graph<br><small>Too many points will cause webCoRE graphs to hang or take a long time to generate</small>""") | |
}else{ | |
container << hubiForm_text ("NOTE: <b>${(points)} Points </b>will be generated per Attribute per Graph") | |
} | |
container << hubiForm_sub_section("Other Options") | |
container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) | |
container << hubiForm_switch([(sTIT): "<b>Smooth Graph Points</b><br><small>(Enable Google Graph Smoothing)</small>", (sNM): "graph_smoothing", (sDEFLT): false]) | |
container << hubiForm_switch([(sTIT): "<b>Flip Graph to Vertical?</b><br><small>(Rotate 90 degrees)</small>", (sNM): "graph_y_orientation", (sDEFLT): false]) | |
container << hubiForm_switch([(sTIT): "<b>Reverse Data Order?</b><br><small> (Flip data left to Right)</small>", (sNM): "graph_z_orientation", (sDEFLT): false]) | |
hubiForm_container(container, i1) | |
} | |
hubiForm_section("Graph Title", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_switch([(sTIT): "<b>Show Title on Graph</b>", (sNM): 'graph_show_title', (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('graph_show_title')){ | |
container << hubiForm_text_input("<b>Graph Title</b>", "graph_title", "Graph Title", false) | |
container << hubiForm_font_size((sTIT): "Title", (sNM): "graph_title", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color("Title", "graph_title", sBLACK, false) | |
container << hubiForm_switch ([(sTIT): "Graph Title Inside Graph?", (sNM): 'graph_title_inside', (sDEFLT): false]) | |
} | |
hubiForm_container(container, i1) | |
} | |
hubiForm_section("Graph Fill", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_switch ([(sTIT): "<b>Set Fill % of Graph?</b><br><small>(False=Default (80%) Fill)</small>", | |
(sNM): "graph_percent_fill", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('graph_percent_fill')){ | |
container << hubiForm_slider ((sTIT): "Horizontal fill % of the graph", (sNM): "graph_h_fill", | |
(sDEFLT): 80, (sMIN): i1, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) | |
container << hubiForm_slider ((sTIT): "Vertical fill % of the graph", (sNM): "graph_v_fill", | |
(sDEFLT): 80, (sMIN): i1, (sMAX): i100, (sUNITS): "%", (sSUBONCHG): false) | |
} | |
hubiForm_container(container, i1) | |
} | |
gatherGraphSize() | |
hubiForm_section("Horizontal Axis", i1, sBLK, sBLK){ | |
//Axis | |
container=[] | |
container << hubiForm_font_size((sTIT): "Horizontal Axis", (sNM): "graph_haxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color("Horizontal Header", "graph_hh", sSILVER, false) | |
container << hubiForm_color("Horizontal Axis", "graph_ha", sSILVER, false) | |
container << hubiForm_text_input("<b>Num Horizontal Gridlines</b><small> (Blank for auto)</small>", "graph_h_num_grid", sBLK, false) | |
container+= hubiForm_help() | |
hubiForm_container(container, i1) | |
} | |
//Vertical Axis | |
hubiForm_section("Vertical Axis", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_font_size ((sTIT): "Vertical Axis", (sNM): "graph_vaxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Vertical Header", "graph_vh", sBLACK, false) | |
container << hubiForm_color ("Vertical Axis", "graph_va", sSILVER, false) | |
hubiForm_container(container, i1) | |
} | |
//Left Axis | |
List<Map<String,String>> formatEnum=[["": "No Formatting ::: 12345"], ["decimal":"Decimal ::: 12,345"], ["short": "Short ::: 12K"], ["scientific": "Scientific ::: 1e5"], ["percent": "Percent ::: 1234500%"], ["long": "Long ::: 12 Thousand"]] | |
hubiForm_section("Left Axis", i1, "arrow_back", sBLK){ | |
input( (sTYPE): sENUM, (sNM): "graph_vaxis_1_format",(sTIT): "<b>Number Format</b>", (sMULTP): false, (sREQ): true, options: formatEnum, (sDEFV): sBLK) | |
container=[] | |
container << hubiForm_text_input("<b>Minimum for left axis</b><small> (Blank for auto)</small>", "graph_vaxis_1_min", sBLK, false) | |
container << hubiForm_text_input("<b>Maximum for left axis</b><small> (Blank for auto)</small>", "graph_vaxis_1_max", sBLK, false) | |
container << hubiForm_text_input("<b>Num Vertical Gridlines</b><small> (Blank for auto)</small>", "graph_vaxis_1_num_lines", sBLK, false) | |
container << hubiForm_switch ([(sTIT): "<b>Show Left Axis Label on Graph</b>", (sNM): "graph_show_left_label", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('graph_show_left_label')){ | |
container << hubiForm_text_input("<b>Input Left Axis Label</b>", "graph_left_label", "Left Axis Label", false) | |
container << hubiForm_font_size((sTIT): "Left Axis", (sNM): "graph_left", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color("Left Axis", "graph_left", sWHT, false) | |
} | |
hubiForm_container(container, i1) | |
} | |
//Right Axis | |
hubiForm_section("Right Axis", i1, "arrow_forward", sBLK){ | |
input( (sTYPE): sENUM, (sNM): "graph_vaxis_2_format",(sTIT): "<b>Number Format</b>", (sMULTP): false, (sREQ): true, options: formatEnum, (sDEFV): sBLK) | |
container=[] | |
container << hubiForm_text_input("<b>Minimum for right axis</b><small> (Blank for auto)</small>", "graph_vaxis_2_min", sBLK, false) | |
container << hubiForm_text_input("<b>Maximum for right axis</b><small> (Blank for auto)</small>", "graph_vaxis_2_max", sBLK, false) | |
container << hubiForm_text_input("<b>Num Vertical Gridlines</b><small> (Blank for auto)</small>", "graph_vaxis_2_num_lines", sBLK, false) | |
container << hubiForm_switch ([(sTIT): "<b>Show Right Axis Label on Graph</b>", (sNM): "graph_show_right_label", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('graph_show_right_label')){ | |
container << hubiForm_text_input("<b>Input right Axis Label</b>", "graph_right_label", "Right Axis Label", false) | |
container << hubiForm_font_size ((sTIT): "Right Axis", (sNM): "graph_right", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Right Axis", "graph_right", sWHT, false) | |
} | |
hubiForm_container(container, i1) | |
} | |
//Legend | |
hubiForm_section("Legend", i1, sBLK, sBLK){ | |
container=[] | |
List<Map> legendPosition=[["top": "Top"], ["bottom":"Bottom"], ["in": "Inside Top"]] | |
List<Map> insidePosition=[[(sSTART): "Left"], ["center": "Center"], [(sEND): "Right"]] | |
container << hubiForm_switch([(sTIT): "<b>Show Legend on Graph</b>", (sNM): "graph_show_legend", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('graph_show_legend')){ | |
container << hubiForm_font_size ((sTIT): "Legend", (sNM): "graph_legend", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Legend", "graph_legend", sBLACK, false) | |
hubiForm_container(container, i1) | |
input( (sTYPE): sENUM, (sNM): "graph_legend_position",(sTIT): "<b>Legend Position</b>", (sDEFV): "bottom", options: legendPosition) | |
input( (sTYPE): sENUM, (sNM): "graph_legend_inside_position",(sTIT): "<b>Legend Justification</b>", (sDEFV): sCENTER, options: insidePosition) | |
}else{ | |
hubiForm_container(container, i1) | |
} | |
} | |
hubiForm_section("Current Value Overlay", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_switch ([(sTIT): "<b>Show Current Values on Graph?</b>", (sNM): 'show_overlay', (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('show_overlay')){ | |
container << hubiForm_color ("Background", "overlay_background", sBLACK, false) | |
container << hubiForm_slider ((sTIT): "Background Opacity", | |
(sNM): "overlay_background_opacity", | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
container << hubiForm_font_size ((sTIT): "Device", (sNM): 'overlay', (sDEFLT): i12, (sMIN): i2, (sMAX): i40) | |
container << hubiForm_color ("Device Text", 'overlay_text', sWHT, false) | |
List<String> horizontalAlignmentEnum=["Left", "Middle", "Right"] | |
container << hubiForm_enum ((sTIT): "Horizontal Placement", | |
(sNM): "overlay_horizontal_placement", | |
list: horizontalAlignmentEnum, | |
(sDEFLT): "Right") | |
List<String> verticalAlignmentEnum=["Top", "Middle", "Bottom"] | |
container << hubiForm_enum ((sTIT): "Vertical Placement", | |
(sNM): "overlay_vertical_placement", | |
list: verticalAlignmentEnum, | |
(sDEFLT): "Top") | |
}else{ | |
if(gtSetStr('overlay_background_color')!='#000000' || settings['overlay_background_opacity']!=i90){ | |
app.updateSetting('overlay_background_color', [(sTYPE):'color', (sVAL):sBLACK]) | |
app.updateSetting('overlay_background_color_transparent', [(sTYPE):sBOOL, (sVAL):sFALSE]) | |
app.updateSetting("overlay_background_opacity", [(sTYPE):'number',(sVAL):i90]) | |
app.updateSetting("overlay_background_font", [(sTYPE):'number',(sVAL): 12]) | |
app.updateSetting("overlay_text_color", [(sTYPE):'color', (sVAL):sWHT]) | |
app.updateSetting("overlay_text_color_transparent", [(sTYPE):sBOOL, (sVAL):sFALSE]) | |
app.updateSetting("overlay_horizontal_placement", [(sTYPE):sENUM,(sVAL):"Right"]) | |
app.updateSetting("overlay_vertical_placement", [(sTYPE):sENUM,(sVAL):"Top"]) | |
wremoveSetting('overlay_order') | |
} | |
} | |
container << hubiForm_sub_section("Display Order") | |
hubiForm_container(container, i1) | |
container=[] | |
hubiForm_list_reorder('overlay_order', sBACKGRND) | |
//hubiForm_container(container, i1) | |
} | |
/* state.num_devices=iZ | |
List availableAxis=[["0" : "Left Axis"], ["1": "Right Axis"]] | |
if(state.num_devices == i1){ | |
availableAxis=[["0" : "Left Axis"], ["1": "Right Axis"], ["2": "Both Axes"]] | |
}*/ | |
//Line | |
Integer cnt; cnt=iZ | |
//Boolean bar_size_shown=false | |
//Deal with Global-Specific Settings (ie bar spacing and plot-point size) | |
Boolean show_title,show_bar | |
show_title=false | |
show_bar=false | |
//Boolean show_scatter=false | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
switch (gtSetStr("graph_type_${sid}_${attribute}")){ | |
//list:["Line", "Area", "Scatter", "Bar", "Stepped"], | |
case "Bar" : show_title=true; show_bar=true; break | |
} | |
} | |
} | |
if(show_title && show_bar){ | |
hubiForm_section("Overall Settings for Graph Types", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_slider ((sTIT): "Bar Graphs:: Relative Width for Bars", | |
(sNM): 'graph_bar_width', | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
hubiForm_container(container, i1) | |
} | |
}else | |
app.updateSetting('graph_bar_width', i90) | |
// TODO | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String rid=ent.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() | |
String asasn= "attribute_${sa}_states" // Name of setting that holds a list of the names of all states (enum and custom). | |
String asacsn= "attribute_${sa}_custom_state_names" // Name of setting that holds a list of the names of custom states. | |
hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "direction",sid+attribute){ | |
container=[] | |
container << hubiForm_sub_section("Plot Options") | |
container << hubiForm_enum ((sTIT): "Plot Type", | |
(sNM): "graph_type_${sa}".toString(), | |
list: ["Line", "Area", "Scatter", "Bar", "Stepped"], | |
(sDEFLT): "Line", | |
(sSUBONCHG): true) | |
// TODO submit_on_change was commented out.... | |
container << hubiForm_enum ((sTIT): "Time Integration Function", | |
(sNM): "var_${sa}_function".toString(), | |
list: ["Average", "Min", "Max", "Mid", "Sum", "Median", "First", "Last"], | |
(sDEFLT): "Average") | |
container << hubiForm_enum ((sTIT): "Axis Side", | |
(sNM): "graph_axis_number_${sa}".toString(), | |
list: ["Left", "Right"], | |
(sDEFLT): "Left") | |
String colorText,fillText | |
colorText=sBLK | |
fillText="Fill" | |
String graphType=gtSetStr("graph_type_${sa}") | |
switch (graphType){ | |
case "Line": | |
colorText="Line" | |
fillText=sBLK | |
break | |
case "Area": | |
colorText="Area Line" | |
break | |
case "Bar": | |
colorText="Bar Border" | |
break | |
case "Scatter": | |
colorText="Border" | |
break | |
case "Stepped": | |
colorText="Line" | |
break | |
default: | |
fillText=sBLK | |
} | |
container << hubiForm_sub_section(colorText+" Options") | |
container << hubiForm_color(colorText, | |
"var_${sa}_stroke", | |
hubiTools_rotating_colors(cnt), | |
false) | |
container << hubiForm_slider ((sTIT): colorText+" Opacity", | |
(sNM): "var_${sa}_stroke_opacity", | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
container << hubiForm_line_size ((sTIT): colorText, | |
(sNM): "var_${sa}_stroke", | |
(sDEFLT): i2, (sMIN): i1, (sMAX): i20) | |
if(graphType in ["Bar","Area","Stepped"]){ | |
container << hubiForm_sub_section(graphType+sSPC+fillText+" Options") | |
container << hubiForm_color(fillText, | |
"var_${sa}_fill", | |
hubiTools_rotating_colors(cnt), | |
false) | |
container << hubiForm_slider ((sTIT): fillText+" Opacity", | |
(sNM): "var_${sa}_fill_opacity", | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
} | |
if(graphType in ["Scatter","Line","Area"]){ | |
container << hubiForm_sub_section("Data Points") | |
if(graphType in ["Line","Area"]){ | |
container << hubiForm_switch([(sTIT): "<b>Display Data Points on Line?</b>", (sNM): "var_${sa}_line_plot_points", (sDEFLT): false, (sSUBONCHG): true]) | |
} | |
if(settings["var_${sa}_line_plot_points"] || graphType == "Scatter"){ | |
container << hubiForm_enum ( | |
(sTIT): "Point Type", | |
(sNM): "var_${sa}_point_type".toString(), | |
list: [ "Circle", "Triangle", "Square", "Diamond", "Star", "Polygon"], | |
(sDEFLT): "Circle") | |
container << hubiForm_slider ((sTIT): "Point Size", | |
(sNM): "var_${sa}_point_size".toString(), | |
(sDEFLT): i5, | |
(sMIN): iZ, | |
(sMAX): 60, | |
(sUNITS): " points", | |
(sSUBONCHG): false) | |
if(graphType == "Area"){ | |
container << hubiForm_text ("<b>*Note, Area Plots use the same fill setting for Points and Area (Above)") | |
}else{ | |
container << hubiForm_color("Point Fill", | |
"var_${sa}_fill", | |
hubiTools_rotating_colors(cnt), | |
false) | |
container << hubiForm_slider ((sTIT): "Point Fill Opacity", | |
(sNM): "var_${sa}_fill_opacity", | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
} | |
}else{ | |
app.updateSetting ("var_${sa}_point_size", iZ) | |
settings["var_${sa}_point_size"]=iZ | |
} | |
} | |
def currentAttribute, sensor | |
Boolean enumType; enumType=false | |
List<String> possible_values; possible_values= [] // List of the names of all states (enum and custom). | |
List<String> possible_custom_values= [] // List of the names of custom states. | |
Integer numStates; numStates= iZ | |
//TODO need to check if dataset is quanted, and based on quant type decide if values can be determined | |
// check if data is regular start: | |
String defltS, defltE | |
Map b=gtStartEndTypes(ent,attribute) | |
if(b){ | |
defltS=sMs(b,sSTART) | |
defltE=sMs(b,sEND) | |
enumType=true | |
possible_values = [defltS,defltE] | |
}else{ | |
if(typ==sCSENSOR){ | |
Boolean multiple=true | |
String varn=multiple ? 'sensors' : 'sensor_' // have to get devices from settings | |
def a=gtSetting(varn) | |
List devs = multiple ? (List)a : [a] | |
if(devs.size()){ | |
sensor=devs.find{ it.id == rid } | |
List sas= (List)sensor.getSupportedAttributes() | |
for(attrib in sas){ | |
if((String)attrib[sNM] == attribute){ | |
currentAttribute=attrib | |
if(attrib.dataType == "ENUM"){ | |
possible_values=currentAttribute.getValues() | |
enumType=true | |
} | |
} | |
} | |
}else warn 'graphTimegraph: no devices found',null | |
} | |
} | |
if(enumType){ | |
container << hubiForm_sub_section("""Numerical values for "$attribute" states""") | |
for(String value in possible_values){ | |
container << hubiForm_text_input("Value for <mark>$value</mark>", | |
"attribute_${sa}_${value}", | |
s100, | |
false) | |
} | |
} | |
if(1 || !enumType){ // Allow enum types to have custom states also. | |
String csn= "attribute_${sa}_custom_states" | |
Boolean cs | |
cs= settings[csn] | |
container << hubiForm_sub_section("""Custom State Values for "$attribute" """ ) | |
if(cs == null) | |
app.updateSetting(csn, [(sTYPE): sBOOL, (sVAL): sFALSE]) | |
container << hubiForm_switch([(sTIT): "<b>Set Custom State Values?</b><br><small>(For custom drivers w/ non-numeric values)</small>", | |
(sNM): csn, | |
(sDEFLT): false, | |
(sSUBONCHG): true]) | |
cs= settings[csn] | |
if(cs){ | |
//if(!settings["attribute_${sa}_num_custom_states"]){} | |
container << hubiForm_text_input("<b>Number of Custom States</b>", | |
"attribute_${sa}_num_custom_states", | |
s2, true) | |
numStates=Integer.parseInt(settings["attribute_${sa}_num_custom_states"].toString()) | |
String csin | |
Integer i | |
for(i=iZ; i<numStates; i++){ | |
List subcontainer=[] | |
csin= "attribute_${sa}_custom_state_${i}" | |
subcontainer << hubiForm_text_input("<b>State #"+(i.toString())+"</b>", | |
csin, | |
sBLK, | |
true) | |
if(settings[csin]){ | |
subcontainer << hubiForm_text_input('<b>Value for "<mark>'+settings[csin]+'</mark></b>"', | |
csin+"_value", | |
s0, | |
true) | |
} | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.5, 0.5]]) | |
} | |
} | |
// Remove previous custom state value settings | |
List<String> asacs= (List<String>)settings[asacsn] | |
if(asacs){ | |
List<String> old_custom_values=asacs | |
for(String val in old_custom_values){ | |
wremoveSetting("attribute_${sa}_${val}") | |
} | |
} | |
// Update custom state settings | |
if(cs){ | |
String csin | |
Integer i | |
for(i=iZ; i<numStates; i++){ | |
csin= "attribute_${sa}_custom_state_${i}" | |
String csi= settings[csin] | |
String csival= settings[csin+"_value"] | |
if(csi && csival){ | |
String val=csi | |
possible_values << val | |
possible_custom_values << val | |
app.updateSetting("attribute_${sa}_${val}", csival) | |
} | |
} | |
} | |
} | |
// Update or remove the list of custom state names. | |
if(possible_custom_values.size()){ | |
app.updateSetting (asacsn, possible_custom_values) | |
}else{ | |
wremoveSetting(asacsn) | |
} | |
// Update or remove the list of all (enum and custom) state names. | |
if(possible_values.size()){ | |
app.updateSetting (asasn, possible_values) | |
}else{ | |
wremoveSetting(asasn) | |
} | |
//Line and Area Graphs can be "Drop-line" | |
if((graphType in ["Line","Area","Stepped"]) && !enumType && gtSetB("attribute_${sa}_custom_states") == false){ | |
container << hubiForm_sub_section("Handle Missing Values") | |
container << hubiForm_switch([(sTIT): "<b>Display Missing Data as a Drop Line?</b>", (sNM): "attribute_${sa}_drop_line", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB("attribute_${sa}_drop_line")){ | |
container << hubiForm_text_input("<b>Value of Missing Data</b>", | |
"attribute_${sa}_drop_value", | |
s0, false) | |
} | |
container << hubiForm_switch([(sTIT): "<b>Extend Left Value?</b><br><small>When values are unavailable at start of timespan,, extend first value to left</small>", | |
(sNM): "attribute_${sa}_extend_left", (sDEFLT): false, (sSUBONCHG): false]) | |
container << hubiForm_switch([(sTIT): "<b>Extend Right Value?</b><br><small>When values are unavailable at end of timespan, extend last value to right</small>", | |
(sNM): "attribute_${sa}_extend_right", (sDEFLT): false, (sSUBONCHG): false]) | |
container << hubiForm_switch([(sTIT): "<b>Interpolate Left Value?</b><br><small>When values are unavailable at start of timespan, interpolate from first value to left edge</small>", | |
(sNM): "attribute_${sa}_interp_left", (sDEFLT): false, (sSUBONCHG): false]) | |
}else{ | |
wremoveSetting("attribute_${sa}_drop_line") | |
wremoveSetting("attribute_${sa}_extend_left") | |
wremoveSetting("attribute_${sa}_extend_right") | |
wremoveSetting("attribute_${sa}_interp_left") | |
} | |
container << hubiForm_sub_section("Restrict Displayed Values") | |
container << hubiForm_switch([(sTIT): "<b>Restrict Displaying Bad Values?</b>", (sNM): "attribute_${sa}_bad_value", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB("attribute_${sa}_bad_value")){ | |
container << hubiForm_text_input("<b>Min Value to Include</b><br><small>If the recorded sensor value is <b>below</b> this value it will be dropped</small>", | |
"attribute_${sa}_min_value", | |
s0, false) | |
container << hubiForm_text_input("<b>Max Value to Include</b><br><small>If the recorded sensor value is <b>above</b> this value it will be dropped</small>", | |
"attribute_${sa}_max_value", | |
s100, false) | |
} | |
container << hubiForm_text_input("<b>Units for Pretty Display</b>", | |
"units_${sa}", | |
sBLK, | |
false) | |
hubiForm_container(container, i1) | |
cnt += i1 | |
} | |
} | |
} | |
} | |
} | |
String getData_timegraph(){ | |
Map<String,Map> resp=[:] | |
// Get extra data before the start of the timespan to cover the width of the first integration bucket. | |
Long timespan = Long.parseLong(gtSetStr('graph_timespan')) | |
Long pointspan = Long.parseLong(gtSetStr('graph_point_span')) | |
Long graph_time = Math.round(wnow() - timespan - (pointspan / 2.0D)) | |
// TODO | |
List<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() | |
resp[sid]= resp[sid] ?: [:] | |
List<Map> data | |
data = gtDataSourceData(ent) // Get all sensor data regardless of time. | |
// Get the index of the first data item that is in the timespan. | |
Integer first_index = data.findIndexOf{ Map it -> lMt(it) > graph_time} | |
if (first_index >= iZ){ // Some data is in the timespan: | |
if (first_index > iZ){ // Also some data is before the timespan: | |
data = data.subList(first_index - i1, data.size()) // Take the last item before the timespan, and everything later. | |
} else { // No data is before the timespan: | |
// Take all the data that we have. | |
} | |
} else { // No data is in the timespan: | |
if (data.size() >= i1){ // But some data is before the timespan: | |
data = [ data[data.size()-i1] ] // Take the latest data item that we have. | |
} else { // No data at all: | |
// No data at all. MAY NOT BE ABLE TO HIT THIS CASE! | |
} | |
} | |
// Map state names to values. | |
resp[sid][attribute]=data.collect{ Map it -> [(sDT): lMt(it), (sVAL): getValue(sid, attribute, it[sVAL])]} | |
// Eliminate "bad" values | |
if(gtSetB("attribute_${sa}_bad_value")){ | |
Float min=Float.valueOf(settings["attribute_${sa}_min_value"].toString()) | |
Float max=Float.valueOf(settings["attribute_${sa}_max_value"].toString()) | |
resp[sid][attribute]= ((List<Map>)resp[sid][attribute]).findAll{ Map it -> (Double)it[sVAL] >= min && (Double)it[sVAL] <= max} | |
} | |
} | |
} | |
return JsonOutput.toJson(resp) | |
} | |
Map getOptions_timegraph(){ | |
/*Setup Series*/ | |
//Map<String,Map> series=["series" : [:]] | |
Map options=[ | |
"graphReduction": gtSetI('graph_max_points'), | |
"graphTimespan": Long.parseLong(gtSetStr('graph_timespan')), | |
"graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), | |
"graphPointSpan": Long.parseLong(gtSetStr('graph_point_span')), | |
// "graphRefreshRate" : Integer.parseInt(gtSetStr('graph_refresh_rate')), | |
"overlays": [ "display_overlays" : show_overlay, | |
"horizontal_alignment" : overlay_horizontal_placement, | |
"vertical_alignment" : overlay_vertical_placement, | |
"order" : gtSetStr('overlay_order') | |
], | |
"graphOptions": [ | |
"tooltip" : ["format" : "short"], | |
"width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, | |
"height": gtSetB(sGRPHSTATICSZ) ? graph_v_size : s100PCT, | |
"chartArea": [ "width": graph_percent_fill ? "${graph_h_fill}%" : s80PCT, | |
"height": graph_percent_fill ? "${graph_v_fill}%" : s80PCT], | |
"explorer": [ | |
"actions": ["dragToZoom", "rightClickToReset"], | |
"axis": "horizontal", | |
"keepInBounds": true, | |
"maxZoomIn": 40.0 | |
], | |
"hAxis": [ | |
"textStyle": ["fontSize": graph_haxis_font, | |
"color": gtSetB('graph_hh_color_transparent') ? sTRANSPRNT : graph_hh_color | |
], | |
"gridlines": ["color": gtSetB('graph_ha_color_transparent') ? sTRANSPRNT : graph_ha_color, | |
"count": graph_h_num_grid != sBLK ? graph_h_num_grid : null | |
], | |
"format": gtSetStr('graph_h_format')==sBLK?sBLK:gtSetStr('graph_h_format') | |
], | |
"vAxis": ["textStyle": ["fontSize": graph_vaxis_font, | |
"color": gtSetB('graph_vh_color_transparent') ? sTRANSPRNT : graph_vh_color, | |
], | |
"gridlines": ["color": gtSetB('graph_va_color_transparent') ? sTRANSPRNT : graph_va_color], | |
], | |
"vAxes": [ | |
0: ["title" : graph_show_left_label ? graph_left_label: null, | |
"titleTextStyle": ["color": gtSetB('graph_left_color_transparent') ? sTRANSPRNT : graph_left_color, "fontSize": graph_left_font], | |
"viewWindow": ["min": graph_vaxis_1_min != sBLK ? graph_vaxis_1_min : null, | |
"max": graph_vaxis_1_max != sBLK ? graph_vaxis_1_max : null], | |
"gridlines": ["count" : graph_vaxis_1_num_lines != sBLK ? graph_vaxis_1_num_lines : null ], | |
"minorGridlines": ["count" : 0], | |
"format": graph_vaxis_1_format, | |
], | |
1: ["title": graph_show_right_label ? graph_right_label : null, | |
"titleTextStyle": ["color": gtSetB('graph_right_color_transparent') ? sTRANSPRNT : graph_right_color, "fontSize": graph_right_font], | |
"viewWindow": ["min": graph_vaxis_2_min != sBLK ? graph_vaxis_2_min : null, | |
"max": graph_vaxis_2_max != sBLK ? graph_vaxis_2_max : null], | |
"gridlines": ["count" : graph_vaxis_2_num_lines != sBLK ? graph_vaxis_2_num_lines : null ], | |
"minorGridlines": ["count" : 0], | |
"format": graph_vaxis_2_format, | |
] | |
], | |
"bar": [ "groupWidth" : graph_bar_width+"%", "fill-opacity" : 0.5], | |
"pointSize": graph_scatter_size, | |
"legend": !gtSetB('graph_show_legend') ? ["position": sNONE] : ["position": graph_legend_position, | |
"alignment": graph_legend_inside_position, | |
"textStyle": ["fontSize": graph_legend_font, | |
"color": gtSetB('graph_legend_color_transparent') ? sTRANSPRNT : gtSetStr('graph_legend_color')]], | |
"backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), | |
"curveType": !graph_smoothing ? sBLK : "function", | |
"title": !gtSetB('graph_show_title') ? sBLK : gtSetStr('graph_title'), | |
"titleTextStyle": !gtSetB('graph_show_title') ? sBLK : ["fontSize": graph_title_font, "color": gtSetB('graph_title_color_transparent') ? sTRANSPRNT : gtSetStr('graph_title_color')], | |
"titlePosition" : gtSetB('graph_title_inside') ? "in" : "out", | |
"interpolateNulls": true, //for null vals on our chart | |
"orientation" : gtSetB('graph_y_orientation')? "vertical" : "horizontal", | |
"reverseCategories" : graph_z_orientation, | |
"series": [:], | |
] | |
] | |
Integer count_ | |
count_=iZ | |
// TODO | |
List<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 type_ | |
type_= settings["graph_type_${sa}"] != null ? gtSetStr("graph_type_${sa}").toLowerCase() : 'line' | |
if(type_ == "stepped") type_="steppedArea" | |
Integer axes_=settings["graph_axis_number_${sa}"] == "Left" ? iZ : i1 | |
String stroke_color=gtSetStr("var_${sa}_stroke_color") | |
String stroke_opacity=gtSetStr("var_${sa}_stroke_opacity") | |
//def stroke_line_size=settings["var_${sa}_stroke_line_size"] | |
//String fill_color=settings["var_${sa}_fill_color"] | |
//String fill_opacity=settings["var_${sa}_fill_opacity"] | |
def point_size=settings["var_${sa}_point_size"] | |
String point_type=settings["var_${sa}_point_type"] != null ? gtSetStr("var_${sa}_point_type").toLowerCase() : sBLK | |
type_=type_=='bar' ? 'bars' : type_ | |
options.graphOptions.series << [(count_.toString()) : [ | |
"type" : type_, | |
"targetAxisIndex" : axes_, | |
"pointSize" : point_size, | |
"pointShape" : point_type, | |
"color" : stroke_color, | |
"opacity" : stroke_opacity, | |
] | |
] | |
count_++ | |
} | |
} | |
// TODO | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
String sa= "${sid}_${attribute}".toString() | |
//add colors and thicknesses | |
Integer axis=settings["graph_axis_number_${sa}"] == "Left" ? iZ : i1 | |
String tc= "graph_line_${sa}_color" | |
String text_color=gtSetStr(tc) | |
Boolean text_color_transparent=gtSetB(tc+"_transparent") | |
Map annotations=[ | |
"targetAxisIndex": axis, | |
"color": text_color_transparent ? sTRANSPRNT : text_color | |
] | |
options.graphOptions.series << annotations | |
} | |
} | |
return options | |
} | |
static String getDrawType_timegraph(){ | |
return "google.visualization.LineChart" | |
} | |
static String getRGBA(String hex, opacity){ | |
String c | |
c=hex-"#" | |
c=c.toUpperCase() | |
Integer i=Integer.parseInt(c, 16) | |
Integer r=(i & 0xFF0000) >> 16 | |
Integer g=(i & 0xFF00) >> 8 | |
Integer b=(i & 0xFF) | |
Float o=opacity/100.0 | |
String s=sprintf("rgba( %d, %d, %d, %.2f)", r, g, b, o) | |
return s | |
} | |
String getGraph_timegraph(){ | |
String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String html | |
html=""" | |
<!DOCTYPE html> | |
<html style="${fullSizeStyle}"> | |
<link rel='icon' href='https://www.shareicon.net/data/256x256/2015/09/07/97252_barometer_512x512.png' type='image/x-icon'/> | |
<link rel="apple-touch-icon" href="https://www.shareicon.net/data/256x256/2015/09/07/97252_barometer_512x512.png"> | |
<head> | |
${scriptIncludes()} | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/3.0.16/svg.min.js" integrity="sha256-MCvBrhCuX8GNt0gmv06kZ4jGIi1R2QNaSkadjRzinFs=" crossorigin="anonymous"></script> | |
<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; | |
let overlayEvent=null; | |
let overlayDone=0; | |
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) =>{ | |
subscriptions=data; | |
console.log("Got Subscriptions"); | |
console.log(subscriptions); | |
}); | |
} | |
function getGraphData(){ | |
return jQuery.get("${makeCallBackURL('getData/')}", (data) =>{ | |
console.log("Got Graph Data"); | |
graphData=data; | |
}); | |
} | |
function parseEvent(event){ | |
const now=new Date().getTime(); | |
let odeviceId=event.deviceId; | |
let deviceId="d"+odeviceId; | |
let attribute=event.name; | |
let value=event.value; | |
//only accept relevent events | |
if(subscriptions.ids.includes(deviceId) && subscriptions.attributes[deviceId].includes(attribute)){ | |
let state = subscriptions.states?.[deviceId]?.[attribute]?.[value]; | |
if (state != undefined) { | |
value = state; | |
} | |
value = parseFloat(value); | |
if (subscriptions.drop[deviceId][attribute].restrict_bad && | |
(isNaN(value) || | |
(value < subscriptions.drop[deviceId][attribute].min) || | |
(value > subscriptions.drop[deviceId][attribute].max))) { | |
return; | |
} | |
graphData[deviceId][attribute].push({ date: now, value: value }); | |
updateOverlay(deviceId, attribute, value); | |
if(options.graphUpdateRate === 0) update(); | |
} | |
} | |
async function aupdate(){ | |
await getGraphData(); | |
//drawChart(); | |
update(); | |
} | |
function update(callback){ | |
//boot old data | |
let min=new Date().getTime(); | |
min -= options.graphTimespan; | |
// Need to keep extra data before the start of the specified timespan to cover the width of the first integration bucket. | |
min -= options.graphPointSpan / 2; | |
//First Filter Events that are too old | |
Object.entries(graphData).forEach(([deviceId, attributes]) =>{ | |
Object.entries(attributes).forEach(([attribute, events]) =>{ | |
let index = events.findIndex(it => it.date > min); // Get index of the first event in the timespan. | |
if (index > 1) { // If we have more than one event before the timespan: | |
graphData[deviceId][attribute]=events.slice(index-1); // Keep just one event before the timespan (and everything in the timespan). | |
} | |
}); | |
}); | |
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; | |
} | |
.overlay{ | |
box-sizing: border-box; | |
padding: ${overlay_font ? (overlay_font.toInteger()/2): 12}px ${overlay_font}px; | |
position: absolute; | |
background-color: ${gtSetStr('overlay_background_color') ? getRGBA(gtSetStr('overlay_background_color'), overlay_background_opacity) : ""}; | |
top: 50px; | |
left: 100px; | |
text-align: center; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); | |
} | |
.overlay-title{ | |
font-size: ${overlay_font}px; | |
text-align: left; | |
color: ${overlay_text_color}; | |
font-family: Arial, Helvetica, sans-serif; | |
} | |
.overlay-number{ | |
font-size: ${overlay_font}px; | |
font-weight: 900; | |
text-align: right; | |
padding: 0px 0px 0px ${overlay_font}px; | |
color: ${overlay_text_color}; | |
font-family: Arial, Helvetica, sans-serif; | |
} | |
.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)'); | |
chart=new ${drawType_timegraph}(document.getElementById("timeline")); | |
//create stack | |
Object.entries(graphData).forEach(([deviceId, attrs]) =>{ | |
stack[deviceId]={}; | |
Object.keys(attrs).forEach(attr =>{ | |
stack[deviceId][attr]=[]; | |
}); | |
}) | |
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 averageEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
return matches.reduce((sum, it) =>{ | |
if(sum.value == drop_val) sum.value=0; | |
sum.value += it.value / matches.length; | |
return sum; | |
},{ date: minTime+((maxTime - minTime)/2), value: drop_val}); | |
} | |
function sumEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
return matches.reduce((sum, it) =>{ | |
if(sum.value == drop_val) sum.value=parseFloat(0); | |
sum.value += parseFloat(it.value); | |
return sum; | |
},{ date: minTime+((maxTime - minTime)/2), value: drop_val}); | |
} | |
function maxEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
if (matches.length != 0){ | |
return{ date: minTime+((maxTime - minTime)/2), value: Math.max.apply(Math, matches.map(function(o){ return o.value; })) }; | |
} | |
else | |
return{ date: minTime+((maxTime - minTime)/2), value: drop_val }; | |
} | |
function minEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
if(matches.length != 0) | |
return{ date: minTime+((maxTime - minTime)/2), value: Math.min.apply(Math, matches.map(function(o){ return o.value; })) }; | |
else | |
return{ date: minTime+((maxTime - minTime)/2), value: drop_val }; | |
} | |
function midEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
if(matches.length != 0) | |
return{ date: minTime+((maxTime - minTime)/2), value: matches[Math.floor(matches.length/2)].value }; | |
else | |
return{ date: minTime+((maxTime - minTime)/2), value: drop_val }; | |
} | |
function medianEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
if(matches.length != 0) | |
return{ date: minTime+((maxTime - minTime)/2), value: matches.sort((a, b) => a.value - b.value)[Math.floor(matches.length/2)].value }; | |
else | |
return{ date: minTime+((maxTime - minTime)/2), value: drop_val }; | |
} | |
function firstEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
if(matches.length != 0) | |
return{ date: minTime+((maxTime - minTime)/2), value: matches[0].value }; | |
else | |
return{ date: minTime+((maxTime - minTime)/2), value: drop_val }; | |
} | |
function lastEvents(minTime, maxTime, data, drop_val){ | |
const matches=data.filter(it => it.date > minTime && it.date <= maxTime); | |
if(matches.length != 0) | |
return{ date: minTime+((maxTime - minTime)/2), value: matches[matches.length - 1].value }; | |
else | |
return{ date: minTime+((maxTime - minTime)/2), value: drop_val }; | |
} | |
function getStyle(deviceIndex, attribute){ | |
let style=subscriptions.var[deviceIndex][attribute] | |
let stroke_color=style.stroke_color == null ? "" : style.stroke_color; | |
let stroke_opacity=style.stroke_opacity == null ? "" : parseFloat(style.stroke_opacity)/100.0; | |
let stroke_width=style.stroke_width == null ? "" : style.stroke_width; | |
let fill_color=style.fill_color == null ? "" : style.fill_color; | |
let fill_opacity=style.fill_opacity == null ? "" : parseFloat(style.fill_opacity)/100.0; | |
let returnString=`{ stroke-color: \${stroke_color}; stroke-opacity: \${stroke_opacity}; stroke-width: \${stroke_width}; fill-opacity: \${fill_opacity}; fill-color: \${fill_color}; }` | |
if(subscriptions.graph_type[deviceIndex][attribute] == "Stepped") returnString=`{ stroke-opacity: \${stroke_opacity}; stroke-width: \${stroke_width}; fill-opacity: \${fill_opacity}; fill-color: \${fill_color}; }` | |
return returnString; | |
} | |
function drawChart(callback){ | |
let now=new Date().getTime(); | |
let min=now - options.graphTimespan; | |
let dataTable=new google.visualization.DataTable(); | |
dataTable.addColumn({ label: 'Date', type: 'datetime', }); | |
let colNums={}; | |
let i=0; | |
subscriptions.ids.forEach((deviceId) =>{ | |
subscriptions.attributes[deviceId].forEach((attr) =>{ | |
//console.log(deviceId+" "+attr); | |
dataTable.addColumn({ label: subscriptions.labels[deviceId][attr].replace('%deviceName%', subscriptions.sensors[deviceId].displayName).replace('%attributeName%', attr), type: 'number' }); | |
dataTable.addColumn({ role: "style" }); | |
}); | |
}); | |
// BUILD THE STYLES | |
// COLLATE THE CURRENT DATA | |
let accumData={}; | |
let then=now - options.graphTimespan; | |
let spacing=options.graphPointSpan; | |
let overlay=10; | |
var current; | |
var drop_val; | |
var newEntry; | |
var next; | |
// Round up the timespan start point to naturally align the center of the integration intervals on day / hour / minute / second boundaries. | |
if(spacing >= 86400000){ // Align to day | |
let d=new Date(then + 86400000 - 1); | |
d.setHours(0, 0, 0, 0); | |
then=d.getTime(); | |
} else if (spacing >= 3600000) { // Align to hour multiple | |
let d=new Date(then + spacing - 1); | |
let spacing_hours = Math.floor(spacing / 3600000); | |
let hours = Math.floor(d.getHours() / spacing_hours) * spacing_hours; | |
d.setHours(hours, 0, 0, 0); | |
then=d.getTime(); | |
} else if (spacing >= 60000) { // Align to minute multiple | |
let d=new Date(then + spacing - 1); | |
let spacing_minutes = Math.floor(spacing / 60000); | |
let minutes = Math.floor(d.getMinutes() / spacing_minutes) * spacing_minutes; | |
d.setMinutes(minutes, 0, 0); | |
then=d.getTime(); | |
} else if (spacing >= 1000) { // Align to second multiple | |
let d=new Date(then + spacing - 1); | |
let spacing_seconds = Math.floor(spacing / 1000); | |
let seconds = Math.floor(d.getSeconds() / spacing_seconds) * spacing_seconds; | |
d.setSeconds(seconds, 0); | |
then=d.getTime(); | |
} | |
//console.info(subscriptions); | |
//map the graph data | |
Object.entries(graphData).forEach(([deviceIndex, attributes]) =>{ | |
Object.entries(attributes).forEach(([attribute, events]) =>{ | |
let func=subscriptions.var[deviceIndex][attribute].function; | |
let num_events=events.length; | |
let first_valid_index = events.findIndex(it => it.date > then); // Index of the first event that is in the timespan. | |
let last_invalid_index = (first_valid_index >= 0) ? first_valid_index-1 : num_events-1; | |
let extend_left=subscriptions.extend[deviceIndex][attribute].left; | |
let extend_right=subscriptions.extend[deviceIndex][attribute].right; | |
let interp_left=subscriptions.extend[deviceIndex][attribute].interp; | |
let drop_line=subscriptions.drop[deviceIndex][attribute].valid; | |
let drop_val=null; | |
let newEntry=undefined; | |
let adj_events=events; | |
if(drop_line == "true"){ | |
drop_val=parseFloat(subscriptions.drop[deviceIndex][attribute].value); | |
} else if (first_valid_index>=0 && extend_left){ | |
drop_val=events[first_valid_index].value; | |
} else if (interp_left // Left interpolation is enabled | |
&& (first_valid_index > 0) // and we have a data point that is before the timespan | |
&& (events[first_valid_index].date > then + (spacing / 2)) // and the first valid data point is not in the first bucket | |
) { | |
// Replace the last data item that is before the timespan with a dummy data item at the start of the timespan, | |
// having a value interpolated between the last data item that is not in the timespan and the first one that is. | |
let dummy_date = then; | |
let dummy_value = events[last_invalid_index].value + ((events[first_valid_index].value - events[last_invalid_index].value) * | |
(dummy_date - events[last_invalid_index].date) / (events[first_valid_index].date - events[last_invalid_index].date)) | |
let dummyDate = new Date(dummy_date); | |
adj_events = structuredClone(events); | |
adj_events[last_invalid_index].value = dummy_value; | |
adj_events[last_invalid_index].date = dummy_date; | |
} | |
// The start of the timespan has previously been adjusted to be on a naturally aligned boundary. | |
// Start the first integration bucket 1/2 of the spacing earlier to center the bucket on the boundary. | |
current=then - (spacing / 2); | |
// Loop through each time bucket, creating a single data point for the bucket from all of the events that are in the bucket. | |
while (current < now){ | |
if(subscriptions.graph_type[deviceIndex][attribute] == "Stepped"){ | |
drop_val=newEntry?.value ?? events[last_invalid_index]?.value ?? null; | |
} | |
next=current+spacing; | |
switch (func){ | |
case "Average": newEntry=averageEvents(current, next, adj_events, drop_val); break; | |
case "Min": newEntry=minEvents(current, next, adj_events, drop_val); break; | |
case "Max": newEntry=maxEvents(current, next, adj_events, drop_val); break; | |
case "Mid": newEntry=midEvents(current, next, adj_events, drop_val); break; | |
case "Sum": newEntry=sumEvents(current, next, adj_events, drop_val); break; | |
case "Median": newEntry=medianEvents(current, next, adj_events, drop_val); break; | |
case "First": newEntry=firstEvents(current, next, adj_events, drop_val); break; | |
case "Last": newEntry=lastEvents(current, next, adj_events, drop_val); break; | |
} | |
if(drop_line != "true"){ | |
if((first_valid_index >= 0) && (next >= events[first_valid_index].date) && extend_left){ | |
drop_val=null; // The extend left feature has done its job, so disable it now. | |
} | |
if((first_valid_index >= 0) && (events[num_events-1].date <= next) && extend_right){ | |
drop_val=events[num_events-1].value; // Enable the extend right feature from here to the end. | |
} | |
} | |
accumData[newEntry.date]=[ ...(accumData[newEntry.date] ? accumData[newEntry.date] : []), newEntry.value]; | |
accumData[newEntry.date]=[ ...(accumData[newEntry.date] ? accumData[newEntry.date] : []), getStyle(deviceIndex, attribute)]; | |
current += spacing; | |
} | |
}); | |
}); | |
let parsedGraphData=Object.entries(accumData).map(([date, vals]) => [new Date(parseInt(date)), ...vals]); | |
parsedGraphData.forEach(it =>{ | |
dataTable.addRow(it); | |
}); | |
// DRAW THE GRAPH | |
let graphOptions=Object.assign({}, options.graphOptions); | |
graphOptions.hAxis=Object.assign(graphOptions.hAxis,{ viewWindow:{ min: new Date(min), max: new Date(now) } }); | |
if(overlayEvent){ | |
google.visualization.events.removeListener(overlayEvent); | |
overlayEvent=null; | |
overlayDone=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); | |
} | |
if(options.overlays.display_overlays && !overlayDone){ | |
overlayEvent=google.visualization.events.addListener(chart, 'ready', placeMarker.bind(chart, dataTable)); | |
} | |
chart.draw(dataTable, graphOptions); | |
} | |
function updateOverlay(deviceId, attribute, value){ | |
//console.log(deviceId+" "+attribute+" "+value); | |
let searchString="#overlay-"+deviceId+"_"+attribute+"-number"; | |
let val=parseFloat(value).toFixed(1)+" "+subscriptions.var[deviceId][attribute].units; | |
//console.log(searchString); | |
jQuery(searchString).text(val); | |
} | |
function placeMarker(dataTable){ | |
var cli=this.getChartLayoutInterface(); | |
var chartArea=cli.getChartAreaBoundingBox(); | |
let width=jQuery('#graph-overlay').outerWidth(); | |
let height=jQuery('#graph-overlay').outerHeight(); | |
let overlay=options.overlays; | |
//console.debug("Width =", width); | |
//console.debug(chartArea); | |
//console.debug(cli); | |
switch (overlay.vertical_alignment){ | |
case "Top": document.querySelector('.overlay').style.top=Math.floor(chartArea.top) + "px"; + "px"; break; | |
case "Middle": document.querySelector('.overlay').style.top=Math.floor(chartArea.height/2+chartArea.top-height/2) + "px"; + "px"; break; | |
case "Bottom": document.querySelector('.overlay').style.top=Math.floor(chartArea.height+chartArea.top-height) + "px"; + "px"; break; | |
} | |
switch (overlay.horizontal_alignment){ | |
case "Left": document.querySelector('.overlay').style.left=Math.floor(chartArea.left) + "px"; break; | |
case "Middle": document.querySelector('.overlay').style.left=Math.floor(chartArea.width/2-(width/2)+chartArea.left) + "px"; break; | |
case "Right": document.querySelector('.overlay').style.left=Math.floor(chartArea.width+chartArea.left-width) + "px"; break; | |
} | |
//document.querySelector('.overlay').style.width=Math.floor(chartArea.width*0.25) + "px"; | |
//document.querySelector('.overlay').style.height=Math.floor(chartArea.height*0.25) + "px"; | |
}; | |
google.charts.setOnLoadCallback(onLoad); | |
window.onBeforeUnload=onBeforeUnload; | |
</script> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | |
</head> | |
<body style="${fullSizeStyle}"> | |
<div id="timeline" style="${fullSizeStyle}" align="center"></div> | |
""" | |
if(gtSetB('show_overlay')) html+= getOverlay_timegraph() | |
html+= """ | |
</body> | |
</html> | |
""" | |
return html | |
} | |
String getOverlay_timegraph(){ | |
String html | |
html="""<div id="graph-overlay" class="overlay"><table style="width:100%">""" | |
List<String> val=new JsonSlurper().parseText(gtSetStr('overlay_order')) as List<String> | |
for(String str in val){ | |
String[] splitStr=str.split('_') | |
String sid=splitStr[i1] | |
String attribute=splitStr[i2] | |
String sa= "${sid}_${attribute}".toString() | |
Map ent=findDataSourceEntry(sid,attribute) | |
Double v=getValue(sid,attribute,getLatestVal(ent)) | |
String units= gtSetStr("units_${sa}") ?: sBLK | |
String name; name= gtSetStr("graph_name_override_${sa}") | |
name=name.replaceAll("%deviceName%", sMs(ent,sDISPNM)).replaceAll("%attributeName%", attribute) | |
String s=sprintf("%.1f%s",v,units) | |
html += """<tr><td class="overlay-title" id="overlay-${sa}-name">${name}</td> | |
<td class="overlay-number" id="overlay-${sa}-number">${s}</td></tr>""" | |
} | |
html += """</div>""" | |
return html | |
} | |
//oauth endpoints | |
Map getSubscriptions_timegraph(){ | |
List<String> ids=[] | |
Map sensors_=[:] | |
Map attributes=[:] | |
Map labels=[:] | |
Map drop_=[:] | |
Map extend_=[:] | |
Map var_=[:] | |
Map graph_type_=[:] | |
Map states_=[:] | |
Boolean isPoll | |
isPoll=gtStB('hasFuel') | |
// TODO | |
List<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 dn=sMs(ent,sDISPNM) | |
//String typ=sMs(ent,sT).capitalize() | |
if(!ids.contains(sid)) ids << sid | |
//TODO | |
//only take what we need | |
//Map sensors_fmt=gtSensorFmt() | |
sensors_[sid]=[ (sID): sid /*, idAsLong: sensor.idAsLong */, (sDISPNM): dn ] | |
attributes[sid]= attributes[sid] ?: [] | |
attributes[sid] << attribute | |
String attr=attribute | |
labels[sid]= labels[sid] ?: [:] | |
labels[sid][attr]=gtSetStr("graph_name_override_${sa}") | |
states_[sid]= states_[sid] ?: [:] | |
String varn= "attribute_${sa}_states".toString() | |
if((List)settings[varn] /* && gtSetB("attribute_${sa}_custom_states") */){ | |
states_[sid][attr]=[:] | |
for(String st in (List<String>)settings[varn]){ | |
states_[sid][attr][st]=settings["attribute_${sa}_${st}"] | |
} | |
} | |
drop_[sid]= drop_[sid] ?: [:] | |
Boolean drop_valid; drop_valid=false | |
if(gtSetB("attribute_${sa}_drop_line")) | |
drop_valid=true | |
drop_[sid][attr]=[ valid: drop_valid ? sTRUE : sFALSE, | |
(sVAL): drop_valid ? settings["attribute_${sa}_drop_value"] : "null", | |
restrict_bad: settings["attribute_${sa}_bad_value"], | |
min: settings["attribute_${sa}_min_value"], | |
max: settings["attribute_${sa}_max_value"] | |
] | |
extend_[sid]= extend_[sid] ?: [:] | |
extend_[sid][attr]=[ | |
right: settings["attribute_${sa}_extend_right"], | |
left: settings["attribute_${sa}_extend_left"], | |
interp: settings["attribute_${sa}_interp_left"] | |
] | |
graph_type_[sid]= graph_type_[sid] ?: [:] | |
graph_type_[sid][attr]=settings["graph_type_${sa}"] | |
def stroke_color=settings["var_${sa}_stroke_color"] | |
def stroke_opacity=settings["var_${sa}_stroke_opacity"] | |
def stroke_line_size=settings["var_${sa}_stroke_line_size"] | |
def fill_color=settings["var_${sa}_fill_color"] | |
def fill_opacity=settings["var_${sa}_fill_opacity"] | |
def function=settings["var_${sa}_function"] | |
var_[sid]= var_[sid] ?: [:] | |
var_[sid][attr]=[ | |
stroke_color : stroke_color, | |
stroke_opacity : stroke_opacity, | |
stroke_width: stroke_line_size, | |
fill_color: fill_color, | |
fill_opacity: fill_opacity, | |
function: function, | |
(sUNITS): gtSetStr("units_${sa}") ?: sBLK, | |
] | |
} | |
} | |
Integer logging_ = iMs((Map)state,sLOGNG) | |
Map subscriptions=[ | |
(sID): isPoll ? sPOLL : sSENSOR, | |
logging: logging_, | |
ids: ids, //.sort(), | |
'sensors': sensors_, | |
'attributes': attributes, | |
labels : labels, | |
drop : drop_, | |
extend: extend_, | |
graph_type: graph_type_, | |
var : var_, | |
states: states_ | |
] | |
return subscriptions | |
} | |
/* | |
* TODO: Heatmap methods | |
*/ | |
def mainHeatmap(){ | |
mainShare1("""Choose Numeric Attributes or common sensor attributes (like on/off, open/close, present/not present, | |
detected/clear, active/inactive, wet/dry, last Activity)""", | |
sGRPHUPDRATE,true,false) | |
} | |
def deviceHeatmap(){ | |
deviceShare1(true,false,true) | |
} | |
def attributeHeatmap(){ | |
attributeShare1(true) | |
} | |
static String dd(Double num){ | |
if(num<10.0D) return s0+num.toInteger().toString() | |
else return num.toInteger().toString() | |
} | |
static String convertToString(Long msec_){ | |
Long msec=msec_ | |
if(msec == 0L) return "00:00:00" | |
Double hours=Math.floor(msec/3600000.0D) | |
Double mins=Math.floor((msec%3600000)/60000.0D) | |
Double secs=Math.floor((msec%60000)/1000.0D) | |
return dd(hours)+":"+dd(mins)+":"+dd(secs) | |
} | |
def graphHeatmap(){ | |
List<Map<String,String>> decayEnum=[["1000":"1 Second"], ["30000":"30 Seconds"], ["60000":"1 Minute"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], | |
["1800000":"Half Hour"], ["3600000":"1 Hour"], ["7200000":"2 Hours"], ["21600000":"6 Hours"], ["43200000":"12 Hours"], ["86400000":"1 Day"], | |
["172800000":"2 Days"], ["259200000":"3 Days"], ["345600000":"4 Days"], ["432000000":"5 Days"], ["518400000":"6 Days"], ["604800000":"7 Days"]] | |
List<Map<String,String>> typeEnum=[[(sVAL): "Value"], [(sTIME) : "Trigger (Time Since Last Update)"]] | |
// TODO | |
Integer count_ | |
count_=iZ | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
//Get Device Count | |
count_++ | |
} | |
} | |
app.updateSetting ("attribute_count", count_) | |
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: typeEnum, (sDEFV): sVAL, (sSUBOC): true) | |
inputGraphUpdateRate() | |
if(!gtSetStr('graph_type')) graph_type=sVAL | |
if(gtSetStr('graph_type') == sTIME){ | |
input( (sTYPE): sENUM, (sNM): "graph_decay",(sTIT): "<b>Decay Rate</b>", (sMULTP): false, (sREQ): false, options: decayEnum, (sDEFV): "300000", (sSUBOC): true) | |
} | |
container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) | |
container << hubiForm_color ("Graph Line", "graph_line", sBLACK, false) | |
container << hubiForm_line_size ((sTIT): "Graph Line", | |
(sNM): "graph", | |
(sDEFLT): i2, | |
(sMIN): i1, | |
(sMAX): count_, | |
) | |
hubiForm_container(container, i1) | |
} | |
Integer num_ | |
if(graph_num_gradients == null){ | |
settings["graph_num_gradients"]=s2 | |
app.updateSetting ("graph_num_gradients", s2) | |
num_=i2 | |
}else{ | |
num_=graph_num_gradients.toInteger() | |
} | |
hubiForm_section("Level Gradient", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_text_input("Number of Gradient Levels", | |
"graph_num_gradients", | |
s2, | |
true) | |
List subcontainer | |
if(gtSetStr('graph_type') == sVAL){ | |
Integer gradient | |
for(gradient=iZ; gradient < num_; gradient++){ | |
subcontainer=[] | |
String titleString | |
if(gradient == iZ) titleString="Start" | |
else if(gradient == num_-i1) titleString="End" | |
else titleString="Mid" | |
subcontainer << hubiForm_text_input(titleString+" Value", | |
"graph_gradient_${gradient}_value", | |
(gradient*i10).toString(), | |
false) | |
subcontainer << hubiForm_color ("Gradient #"+gradient, | |
"graph_gradient_${gradient}", | |
hubiTools_rotating_colors(gradient), | |
false) | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.25, 0.75]]) | |
} | |
}else{ | |
Long add_time=(graph_decay.toInteger()/(graph_num_gradients.toInteger()-i1)) | |
Long curr_time | |
curr_time=0L | |
Integer gradient | |
for(gradient=iZ; gradient < num_; gradient++){ | |
subcontainer=[] | |
subcontainer << hubiForm_text_format( | |
[text: convertToString(curr_time), | |
horizontal_align: sRIGHT, | |
vertical_align: "20px", | |
sz: 24] ) | |
app.updateSetting ("graph_gradient_${gradient}_value", curr_time) | |
subcontainer << hubiForm_color ("Gradient #"+gradient, | |
"graph_gradient_${gradient}", | |
hubiTools_rotating_colors(gradient), | |
false) | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.25, 0.75]]) | |
curr_time += add_time | |
} | |
} | |
hubiForm_container(container, i1) | |
} | |
hubiForm_section("Graph Columns", i1, sBLK, sBLK){ | |
container=[] | |
Integer default_=Math.ceil(Math.sqrt(count_)).intValue() | |
Integer cols=graph_num_columns ? "${graph_num_columns}".toInteger() : default_ | |
Integer rows=Math.ceil(count_/cols).intValue() | |
container << hubiForm_slider ((sTIT): "Number of Columns<br><small>"+count_+" Devices/Attributes -- "+cols+" X "+rows+"</small>", | |
(sNM): "graph_num_columns", | |
(sDEFLT): default_, | |
(sMIN): i1, | |
(sMAX): count_, | |
(sUNITS): " columns", | |
(sSUBONCHG): true) | |
hubiForm_container(container, i1) | |
} | |
gatherGraphSize() | |
hubiForm_section("Annotations", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_switch([(sTIT): "Show values inside Heat Map?", (sNM): "show_annotations", (sDEFLT): false, (sSUBONCHG): true]) | |
if(gtSetB('show_annotations')){ | |
container << hubiForm_font_size ((sTIT): "Annotation", (sNM): "annotation", (sDEFLT): i16, (sMIN): i2, (sMAX): i40) | |
container << hubiForm_color ("Annotation", "annotation", sWHT, false) | |
container << hubiForm_color ("Annotation Aura", "annotation_aura", sBLACK, false) | |
container << hubiForm_slider ((sTIT): "Number Decimal Places", (sNM): "graph_decimals", (sDEFLT): i1, (sMIN): iZ, (sMAX): i4, (sUNITS): " decimal places") | |
container << hubiForm_switch ([(sTIT): "Bold Annotation", (sNM): "annotation_bold", (sDEFLT):false]) | |
container << hubiForm_switch ([(sTIT): "Italic Annotation", (sNM): "annotation_italic", (sDEFLT):false]) | |
} | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
String getData_heatmap(){ | |
Map<String,Map> resp=[:] | |
Date now=new Date() | |
//def then=new Date(0) | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
// String rid=ent.rid.toString() | |
String attribute=sMs(ent,sA) | |
resp[sid]= resp[sid] ?: [:] | |
Map lst= gtLastData(ent) | |
// [date: date, (sVAL): v, t: t] | |
if(lst && sMs(ent,'aa') == 'lastupdate'){ | |
//Date lastEvent=(Date)lst.date //sensor.getLastActivity() | |
Long latest= lMt(lst) //lastEvent ? lastEvent.getTime() : 0L | |
resp[sid]['lastupdate']=[(sCUR): (now.getTime()-latest), (sDT): latest] | |
}else{ | |
def latest=lst ? lst[sVAL] : iZ // sensor.latestState(attribute) | |
resp[sid][attribute]=[(sCUR): latest, (sDT): lst[sDT] ?: now] | |
} | |
} | |
} | |
return JsonOutput.toJson(resp) | |
} | |
Map getOptions_heatmap(){ | |
List colors=[] | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
String attrib_string="attribute_${sid}_${attribute}_color" | |
String transparent_attrib_string= attrib_string+"_transparent" | |
colors << (gtSetB(transparent_attrib_string) ? sTRANSPRNT : settings[attrib_string]) | |
} | |
} | |
/* | |
String axis1,axis2 | |
if(graph_type == "1"){ | |
axis1="hAxis" | |
axis2="vAxis" | |
}else{ | |
axis1="vAxis" | |
axis2="hAxis" | |
} */ | |
Map options=[ | |
"graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), | |
(sGRAPHT): gtSetStr('graph_type'), | |
"graphOptions": [ | |
"bar" : [ "groupWidth" : s100PCT ], | |
"width": gtSetB(sGRPHSTATICSZ) ? graph_h_size : s100PCT, | |
"height": gtSetB(sGRPHSTATICSZ) ? graph_v_size: s100PCT, | |
"timeline": [ | |
"rowLabelStyle": ["fontSize": graph_axis_font, "color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color], | |
"barLabelStyle": ["fontSize": graph_axis_font] | |
], | |
"backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), | |
"isStacked": true, | |
"chartArea": [ (sLEFT): i10, | |
(sRIGHT) : i10, | |
"top": i10, | |
"bottom": i10 | |
], | |
"legend" : [ "position" : sNONE ], | |
"hAxis": [ "textPosition": sNONE, | |
"gridlines" : [ "count" : s0 ] | |
], | |
"vAxis": [ "textPosition": sNONE, | |
"gridlines" : [ "count" : s0 ] | |
], | |
"annotations" : [ "alwaysOutside": sFALSE, | |
"textStyle": [ | |
"fontSize": annotation_font, | |
"bold": annotation_bold, | |
"italic": annotation_italic, | |
"color": gtSetB('annotation_color_transparent') ? sTRANSPRNT : annotation_color, | |
"auraColor":gtSetB('annotation_aura_color_transparent') ? sTRANSPRNT : annotation_aura_color, | |
], | |
"stem": [ "color": sTRANSPRNT, | |
"highContrast": sFALSE | |
], | |
], | |
] | |
] | |
return options | |
} | |
String getGraph_heatmap(){ | |
String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String html=""" | |
<!DOCTYPE html> | |
<html style="${fullSizeStyle}"> | |
<head> | |
${scriptIncludes1(isSystemType())} | |
<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"); | |
subscriptions=data; | |
}); | |
} | |
function getValue(data, date, attr){ | |
if(options.graphType == "time" || attr == "lastupdate"){ | |
let now=new Date(); | |
let then=new Date(date); | |
return now.getTime()-then.getTime(); | |
} | |
switch (data){ | |
case "active" : return 100; | |
case "inactive" : return 0; | |
case "on" : return 100; | |
case "off" : return 0; | |
case "open" : return 100; | |
case "closed" : return 0; | |
case "detected" : return 100; | |
case "not detected" : return 0; | |
case "clear" : return 0; | |
case "wet" : return 100; | |
case "dry" : return 0; | |
case "unlocked" : return 100; | |
case "locked" : return 0; | |
case "present" : return 100; | |
case "not present" : return 0; | |
case "sleeping" : return 100; | |
case "not sleeping" : return 0; | |
case "muted" : return 100; | |
case "unmuted" : return 0; | |
} | |
return data; | |
} | |
function getGraphData(){ | |
return jQuery.get("${makeCallBackURL('getData/')}", (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)) || | |
(subscriptions.ids.includes(deviceId) && subscriptions.attributes[deviceId].includes("lastupdate"))){ | |
let value=event.value; | |
let attribute=event.name; | |
console.log("Trigger: ", attribute, "Value: ", value); | |
if(subscriptions.attributes[deviceId].includes("lastupdate")){ | |
let now=new Date(); | |
graphData[deviceId]["lastupdate"].current=now.getTime(); | |
graphData[deviceId]["lastupdate"].date=new Date(); | |
} else{ | |
graphData[deviceId][attribute].current=value; | |
graphData[deviceId][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)'); | |
chart=new google.visualization.BarChart(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 dd(num){ | |
if(num<10) return "0"+num.toString(); | |
else return num.toString(); | |
} | |
function convertToString(msec){ | |
if(msec == "0" || msec == 0) return "0 Seconds ago"; | |
let days=parseInt(Math.floor(msec/86400000)); | |
let hours=parseInt(Math.floor((msec%86400000)/3600000)); | |
let mins=parseInt(Math.floor((msec%3600000)/60000)); | |
let secs=parseInt(Math.floor((msec%60000)/1000)); | |
let dayString=days == 0 ? "" : days.toString()+" Days"; | |
dayString=days == 1 ? "1 Day" : dayString | |
let hourString=hours == 0 ? "" : hours.toString()+" Hours "; | |
hourString=hours == 1 ? "1 Hour" : hourString; | |
let minuteString=mins == 0 ? "" : mins.toString()+" Minutes "; | |
minuteString=mins == 1 ? "1 Minute" : minuteString; | |
let secondString=secs == 0 ? "" : secs.toString()+" Seconds "; | |
secondString=secs == 1 ? "1 Second" : secondString; | |
return dayString+" "+hourString+" "+minuteString+" "+secondString; | |
} | |
function getDataList(){ | |
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" | |
}; | |
let data=[]; | |
subscriptions.order.forEach(orderStr =>{ | |
const splitStr=orderStr.split('_'); | |
const deviceId=splitStr[1]; | |
const attr=splitStr[2]; | |
const event=graphData[deviceId][attr]; | |
const cur_=parseFloat(getValue(event.current, event.date, attr)); | |
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); | |
var value_=event.current; | |
var stats_=`\${name}\nCurrent: \${value_}\${units_}\nDate: \${date_String} \${time_String}`; | |
if(attr == "lastupdate"){ | |
value_=convertToString(value_); | |
stats_=`\${name} \nLast Update: \${value_}\${units_}\nDate: \${date_String} \${time_String}`; | |
} | |
data.push({name: name, value: cur_, str: stats_}); | |
}); | |
return data; | |
} | |
function drawChart(callback){ | |
//get number of elements | |
let numElements=subscriptions.count; | |
let colorProfile=[]; | |
for (i=0; i<subscriptions.num_gradients; i++) | |
colorProfile.push(subscriptions.gradients[i]); | |
let dataArray=[]; | |
let tempArray=[]; | |
let dim=getRowColumnsBlank(numElements); | |
let map=new Map(); | |
let cols=subscriptions.num_columns; | |
let rows=Math.ceil(numElements/cols); | |
//Build the header based on the number of elements | |
let header=[]; | |
header.push('Device'); | |
for (i=0; i< cols; i++){ | |
header.push("R"+i); | |
header.push({role:"style"}); | |
header.push({role:"tooltip"}); | |
header.push({role:"annotation"}); | |
} | |
dataArray.push(header); | |
let data=getDataList(); | |
let idx=0; | |
let color=0; | |
let width=subscriptions.line_thickness; | |
let line_color=subscriptions.line_color; | |
let fill_opacity=1.0; | |
for (i=0; i<rows; i++){ | |
tempArray=[]; | |
tempArray.push("Row"+i); | |
for (j=0; j<cols; j++){ | |
if(idx>= numElements){ | |
tempArray.push(0); | |
value=''; | |
str=''; | |
color=options.graphOptions.backgroundColor; | |
line_color=subscriptions.line_color; | |
opacity=0.0; | |
width=0; | |
fill_opacity=0.0; | |
attr=''; | |
} else{ | |
tempArray.push(10); | |
value=data[idx].value; | |
str=data[idx].str; | |
color=getcolor(colorProfile, value); | |
line_color=subscriptions.line_color; | |
opacity=1.0; | |
width=subscriptions.line_thickness; | |
if(subscriptions.show_annotations){ | |
val=parseFloat(value).toFixed(subscriptions.decimals); | |
attr=val; | |
} else{ | |
attr=''; | |
} | |
} | |
tempArray.push('stroke-color: '+line_color+'; stroke-opacity: '+opacity+'; stroke-width: '+width+'; color: '+color+'; fill-opacity: '+fill_opacity ); | |
tempArray.push(str); | |
tempArray.push(attr); | |
idx++; | |
} | |
dataArray.push(tempArray); | |
} | |
var dataTable=google.visualization.arrayToDataTable(dataArray); | |
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_heatmap(){ | |
Integer count_ | |
count_=iZ | |
List _ids=[] | |
Map _attributes=[:] | |
Map labels=[:] | |
Map gradients=[:] | |
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 nattribute | |
nattribute=attribute | |
if(sMs(ent,'aa') == "lastupdate") nattribute=ent.aa | |
_ids << sid | |
_attributes[sid] = _attributes[sid] ?: [] | |
_attributes[sid] << nattribute | |
count_++ | |
labels[sid]= labels[sid] ?: [:] | |
labels[sid][nattribute]="${sid} ${nattribute}" | |
labels[sid][nattribute]=settings["graph_name_override_${sid}_${attribute}"] | |
} | |
} | |
Map sensors_fmt=gtSensorFmt() | |
Integer i | |
Integer e=graph_num_gradients.toInteger() | |
for(i=iZ; i<e; i++){ | |
gradients[i]=["val": settings["graph_gradient_${i}_value"], "color": settings["graph_gradient_${i}_color"]] | |
} | |
List order=gtSetStr('graph_order') ? parseJson(gtSetStr('graph_order')) : [] | |
Map subscriptions=[ | |
(sID): isPoll ? sPOLL : sSENSOR, | |
"decimals" : graph_decimals, | |
"count" : count_, | |
'sensors': sensors_fmt, | |
"ids": _ids, | |
'attributes': _attributes, | |
"labels": labels, | |
"order": order, | |
"show_annotations": show_annotations, | |
"gradients": gradients, | |
"num_gradients" : graph_num_gradients.toInteger(), | |
"num_columns" : graph_num_columns, | |
"line_color" : graph_line_color, | |
"line_thickness" : graph_line_size, | |
] | |
return subscriptions | |
} | |
/* | |
* TODO: Linegraph methods | |
*/ | |
def mainLinegraph(){ | |
mainShare1(sNL,'graph_timespan') | |
} | |
def attributeLinegraph(){ | |
attributeShare1() | |
} | |
def deviceLinegraph(){ | |
deviceShare1() | |
} | |
def graphLinegraph(){ | |
List<Map<String,String>> timespanEnum2=[ | |
["60000":"1 Minute"], ["120000":"2 Minutes"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], | |
["1800000":"30 minutes"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], | |
["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"] | |
] | |
dynamicPage((sNM): "graphSetupPage"){ | |
Boolean non_numeric | |
non_numeric=false | |
List<String> container | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String attribute=sMs(ent,sA) | |
Map a=gtStartEndTypes(ent,attribute) | |
if(a) | |
non_numeric= true | |
} | |
} | |
if(non_numeric) | |
app.updateSetting ('graph_max_points', 0) | |
hubiForm_section("General Options", i1, sBLK, sBLK){ | |
input( (sTYPE): sENUM, (sNM): 'graph_type',(sTIT): "<b>Graph Type</b>", (sDEFV): "Line Graph", options: ["Line Graph", "Area Graph", "Scatter Plot"], (sSUBOC): true) | |
inputGraphUpdateRate() | |
input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "<b>Select Time span to Graph</b>", (sMULTP): false, (sREQ): true, options: timespanEnum, (sDEFV): "43200000") | |
container=[] | |
container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) | |
container << hubiForm_switch((sTIT): "Smooth Graph Points", (sNM): "graph_smoothing", (sDEFLT): false) | |
container << hubiForm_switch((sTIT): "<b>Flip Graph to Vertical?</b><br><small>(Rotate 90 degrees)</small>", (sNM): "graph_y_orientation", (sDEFLT): false) | |
container << hubiForm_switch((sTIT): "<b>Reverse Data Order?</b><br><small> (Flip data left to Right)</small>", (sNM): "graph_z_orientation", (sDEFLT): false) | |
if(!non_numeric) | |
container << hubiForm_slider ((sTIT): "Maximum number of Data Points?</b><br><small>(Zero for ALL)</small>", (sNM): 'graph_max_points', (sDEFLT): iZ, (sMIN): iZ, (sMAX): 1000, (sUNITS): " data points", (sSUBONCHG): false) | |
hubiForm_container(container, i1) | |
} | |
hubiForm_section("Graph Title", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_switch((sTIT): "Show Title on Graph", (sNM): 'graph_show_title', (sDEFLT): false, (sSUBONCHG): true) | |
if(gtSetB('graph_show_title')){ | |
container << hubiForm_text_input("Graph Title", "graph_title", "Graph Title", false) | |
container << hubiForm_font_size((sTIT): "Title", (sNM): "graph_title", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color("Title", "graph_title", sBLACK, false) | |
container << hubiForm_switch((sTIT): "Graph Title Inside Graph?", (sNM): 'graph_title_inside', (sDEFLT): false) | |
} | |
hubiForm_container(container, i1) | |
} | |
gatherGraphSize() | |
hubiForm_section("Horizontal Axis", i1, sBLK, sBLK){ | |
//Axis | |
container=[] | |
container << hubiForm_font_size ((sTIT): "Horizontal Axis", (sNM): "graph_haxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Horizontal Header", "graph_hh", sSILVER, false) | |
container << hubiForm_color ("Horizontal Axis", "graph_ha", sSILVER, false) | |
container << hubiForm_text_input ("<b>Num Horizontal Gridlines</b><br><small>(Blank for auto)</small>", "graph_h_num_grid", sBLK, false) | |
container+= hubiForm_help() | |
hubiForm_container(container, i1) | |
} | |
//Vertical Axis | |
hubiForm_section("Vertical Axis", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_font_size ((sTIT): "Vertical Axis", (sNM): "graph_vaxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Vertical Header", "graph_vh", sBLACK, false) | |
container << hubiForm_color ("Vertical Axis", "graph_va", sSILVER, false) | |
hubiForm_container(container, i1) | |
} | |
//Left Axis | |
hubiForm_section("Left Axis", i1, "arrow_back", sBLK){ | |
container=[] | |
container << hubiForm_text_input("<b>Minimum for left axis</b><small>(Blank for auto)</small>", "graph_vaxis_1_min", sBLK, false) | |
container << hubiForm_text_input("<b>Maximum for left axis</b><small>(Blank for auto)</small>", "graph_vaxis_1_max", sBLK, false) | |
container << hubiForm_text_input("<b>Num Vertical Gridlines</b><br><small>(Blank for auto)</small>", "graph_vaxis_1_num_lines", sBLK, false) | |
container << hubiForm_switch ((sTIT): "<b>Show Left Axis Label on Graph</b>", (sNM): "graph_show_left_label", (sDEFLT): false, (sSUBONCHG): true) | |
if(gtSetB('graph_show_left_label')){ | |
container << hubiForm_text_input ("<b>Input Left Axis Label</b>", "graph_left_label", "Left Axis Label", false) | |
container << hubiForm_font_size ((sTIT): "Left Axis", (sNM): "graph_left", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Left Axis", "graph_left", sWHT, false) | |
} | |
hubiForm_container(container, i1) | |
} | |
//Right Axis | |
hubiForm_section("Right Axis", i1, "arrow_forward", sBLK){ | |
container=[] | |
container << hubiForm_text_input("<b>Minimum for right axis</b><small>(Blank for auto)</small>", "graph_vaxis_2_min", sBLK, false) | |
container << hubiForm_text_input("<b>Maximum for right axis</b><small>(Blank for auto)</small>", "graph_vaxis_2_max", sBLK, false) | |
container << hubiForm_text_input("<b>Num Vertical Gridlines</b><br><small>(Blank for auto)</small>", "graph_vaxis_2_num_lines", sBLK, false) | |
container << hubiForm_switch ((sTIT): "<b>Show Right Axis Label on Graph</b>", (sNM): "graph_show_right_label", (sDEFLT): false, (sSUBONCHG): true) | |
if(gtSetB('graph_show_right_label')){ | |
container << hubiForm_text_input ("<b>Input right Axis Label</b>", "graph_right_label", "Right Axis Label", false) | |
container << hubiForm_font_size ((sTIT): "Right Axis", (sNM): "graph_right", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Right Axis", "graph_right", sWHT, false) | |
} | |
hubiForm_container(container, i1) | |
} | |
//Legend | |
hubiForm_section("Legend", i1, sBLK, sBLK){ | |
container=[] | |
List<Map> legendPosition=[["top": "Top"], ["bottom":"Bottom"], ["in": "Inside Top"]] | |
List<Map> insidePosition=[[(sSTART): "Left"], ["center": "Center"], [(sEND): "Right"]] | |
container << hubiForm_switch((sTIT): "Show Legend on Graph", (sNM): "graph_show_legend", (sDEFLT): false, (sSUBONCHG): true) | |
if(gtSetB('graph_show_legend')){ | |
container << hubiForm_font_size ((sTIT): "Legend", (sNM): "graph_legend", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Legend", "graph_legend", sBLACK, false) | |
hubiForm_container(container, i1) | |
input( (sTYPE): sENUM, (sNM): "graph_legend_position",(sTIT): "<b>Legend Position</b>", (sDEFV): "bottom", options: legendPosition) | |
input( (sTYPE): sENUM, (sNM): "graph_legend_inside_position",(sTIT): "<b>Legend Justification</b>", (sDEFV): sCENTER, options: insidePosition) | |
}else{ | |
hubiForm_container(container, i1) | |
} | |
} | |
state.num_devices=iZ | |
if(dataSources){ | |
Integer i; i=iZ | |
for(Map ent in dataSources){ | |
i++ | |
} | |
state.num_devices=i | |
} | |
List<Map> availableAxis | |
availableAxis=[[(s0): "Left Axis"], [(s1): "Right Axis"]] | |
if(state.num_devices == i1){ | |
availableAxis=[[(s0): "Left Axis"], [(s1): "Right Axis"], [(s2): "Both Axes"]] | |
} | |
//Line | |
Integer cnt | |
cnt=iZ | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
String dn=sMs(ent,sDISPNM) | |
hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}", i1, sBLK,sid+attribute){ | |
container=[] | |
input( (sTYPE): sENUM, (sNM): "graph_axis_number_${sid}_${attribute}",(sTIT): "<b>Graph Axis Side</b>", (sDEFV): s0, options: availableAxis) | |
container << hubiForm_color("Line", | |
"graph_line_${sid}_${attribute}", | |
hubiTools_rotating_colors(cnt), | |
false) | |
container << hubiForm_line_size((sTIT): "Line Thickness", | |
(sNM): "attribute_${sid}_${attribute}", | |
(sDEFLT): i2, (sMIN): i1, (sMAX): i20) | |
//TODO figure out from data if there are choices | |
String startVal, endVal | |
startVal=sBLK | |
endVal=sBLK | |
Map a=gtStartEndTypes(ent,attribute) | |
if(a){ | |
startVal=a[sSTART] | |
endVal=a[sEND] | |
} | |
// String startVal=supportedTypes[attribute] ? supportedTypes[attribute].start : sBLK | |
// String endVal=supportedTypes[attribute] ? supportedTypes[attribute].end : sBLK | |
if(gtSetStr('graph_type') == "Area Graph"){ | |
container << hubiForm_slider ((sTIT): "Opacity of the area below the line", | |
(sNM): "attribute_${sid}_${attribute}_opacity", | |
(sDEFLT): 30, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
} | |
String nnvars= "attribute_${sid}_${attribute}_non_number".toString() | |
String svars= "attribute_${sid}_${attribute}_startString".toString() | |
String evars= "attribute_${sid}_${attribute}_endString".toString() | |
if(startVal != sBLK){ | |
app.updateSetting (nnvars, true) | |
app.updateSetting (svars, startVal) | |
app.updateSetting (evars, endVal) | |
container << hubiForm_text("<b><mark>This Attribute ($attribute) is non-numerical, please choose values for the states below</mark></b>") | |
container << hubiForm_text_input("Value for <mark>$startVal</mark>", | |
"attribute_${sid}_${attribute}_${startVal}", | |
s100, false) | |
container << hubiForm_text_input("Value for <mark>$endVal</mark>", | |
"attribute_${sid}_${attribute}_${endVal}", | |
s0, false) | |
hubiForm_container(container, i1) | |
}else{ | |
wremoveSetting(nnvars) | |
wremoveSetting(svars) | |
wremoveSetting(evars) | |
container << hubiForm_switch((sTIT): "Display as a Drop Line", (sNM): "attribute_${sid}_${attribute}_drop_line", (sDEFLT): false, (sSUBONCHG): true) | |
if(gtSetB("attribute_${sid}_${attribute}_drop_line")){ | |
container << hubiForm_text_input("Value to drop the Line", | |
"attribute_${sid}_${attribute}_drop_value", | |
s0, false) | |
hubiForm_container(container, i1) | |
input( (sTYPE): sENUM, (sNM): "attribute_${sid}_${attribute}_drop_time",(sTIT): "Drop Line Time", (sDEFV): "300000", options: timespanEnum2 ) | |
}else{ | |
hubiForm_container(container, i1) | |
} | |
} | |
cnt += i1 | |
} | |
} | |
} | |
} | |
} | |
String getData_linegraph(){ | |
Map resp=[:] | |
Date then | |
then=new Date() | |
Long graph_time | |
use (TimeCategory){ | |
then -= Integer.parseInt(gtSetStr('graph_timespan')).milliseconds | |
graph_time=then.getTime() | |
} | |
// TODO | |
List<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() | |
resp[sid]= resp[sid] ?: [:] | |
List tEvents | |
List<Map> respEvents | |
List<Map> data=CgetData(ent, then) | |
//return [date: d, (sVAL): sum.round(decimals), t: d.getTime()] | |
// log.warn "got dn: $dn attribute: $attribute data1: $data" | |
List<Map>data1 | |
data1=data.collect{ Map it -> [(sDT): it[sT], (sVAL): getValue(sid, attribute, it[sVAL])]} | |
List<Map> data2 | |
data2=data1.findAll{ Map it -> lMs(it,sDT) > graph_time } | |
List<Map> temp | |
temp=([]+data2)// as List<Map> | |
//temp=temp.sort{ (Long)it[sDT] } | |
respEvents=temp | |
data1=null | |
data2=null | |
// log.warn "FINAL got sensor: $sensor attribute: $attribute data1: $temp" | |
temp=null | |
/* | |
respEvents << sensor.statesSince(attribute, then, [(sMAX): 50000]).collect{[ date: it.date.getTime(), (sVAL): getValue(sid, attribute, it.value) ]} | |
respEvents=respEvents.flatten() | |
respEvents=respEvents.reverse() | |
*/ | |
//Add drop lines for non-numerical devices | |
if(settings["attribute_${sa}_non_number"] && respEvents.size()>i1){ | |
String start=gtSetStr("attribute_${sa}_startString") | |
String end=gtSetStr("attribute_${sa}_endString") | |
Float startVal=Float.parseFloat( gtSetStr("attribute_${sa}_${start}")) | |
Float endVal=Float.parseFloat( gtSetStr("attribute_${sa}_${end}")) | |
tEvents=[] | |
//Add Start Event | |
Long currDate | |
currDate=then.getTime() | |
if(respEvents[iZ][sVAL] == startVal){ | |
tEvents.push([(sDT): currDate, (sVAL): endVal]) | |
}else{ | |
tEvents.push([(sDT): currDate, (sVAL): startVal]) | |
} | |
Integer i | |
for(i=iZ; i<respEvents.size(); i++){ | |
currDate= lMs(respEvents[i],sDT) | |
if(respEvents[i][sVAL] == startVal){ | |
tEvents.push([(sDT): currDate-1000L, (sVAL): endVal]) | |
}else{ | |
tEvents.push([(sDT): currDate-1000L, (sVAL): startVal]) | |
} | |
tEvents.push(respEvents[i]) | |
} | |
respEvents=tEvents | |
} | |
//graph_max_points | |
if(gtSetI('graph_max_points') > iZ){ | |
Integer reduction=Math.ceil(respEvents.size() / gtSetI('graph_max_points').toDouble()).toInteger() | |
respEvents=respEvents.collate(reduction).collect{ List group -> | |
group.inject([ (sDT): iZ, (sVAL): iZ ]){ col, it -> | |
col[sDT] += it[sDT] / group.size() | |
col[sVAL] += it[sVAL] / group.size() | |
return col | |
} | |
} | |
} | |
//add drop line data | |
tEvents=[] | |
if(gtSetB("attribute_${sa}_drop_line") && respEvents.size()>i1){ | |
def curr, prev | |
Long currDate, prevDate | |
String drop_time= gtSetStr("attribute_${sa}_drop_time") | |
String drop_value= gtSetStr("attribute_${sa}_drop_value") | |
tEvents.push(respEvents[iZ]) | |
Integer i | |
for(i=iZ; i<respEvents.size(); i++){ | |
curr=respEvents[i] | |
prev=respEvents[i-i1] | |
currDate=lMs(curr,sDT) | |
prevDate=lMs(prev,sDT) | |
if((currDate - prevDate) > Integer.parseInt(drop_time)){ | |
//add first zero | |
tEvents.push([(sDT): prevDate-1000L, (sVAL): Float.parseFloat(drop_value)]) | |
tEvents.push([(sDT): currDate+1000L, (sVAL): Float.parseFloat(drop_value)]) | |
} | |
tEvents.push(curr) | |
} | |
respEvents=tEvents | |
} | |
resp[sid][attribute]=respEvents | |
} | |
} | |
return JsonOutput.toJson(resp) | |
} | |
Map getOptions_linegraph(){ | |
Boolean grpsz= gtSetB(sGRPHSTATICSZ) | |
Map options=[ | |
"graphReduction": gtSetI('graph_max_points'), | |
"graphTimespan": Integer.parseInt(gtSetStr('graph_timespan')), | |
"graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), | |
"graphOptions": [ | |
"width": grpsz ? graph_h_size : s100PCT, | |
"height": grpsz ? graph_v_size: s100PCT, | |
"chartArea": [ | |
"width": grpsz ? graph_h_size : s80PCT, | |
"height": grpsz ? graph_v_size: s80PCT | |
], | |
"hAxis": ["textStyle": ["fontSize": graph_haxis_font, | |
"color": gtSetB('graph_hh_color_transparent') ? sTRANSPRNT : graph_hh_color ], | |
"gridlines": ["color": gtSetB('graph_ha_color_transparent') ? sTRANSPRNT : graph_ha_color, | |
"count": graph_h_num_grid != sBLK ? graph_h_num_grid : null | |
], | |
"format": gtSetStr('graph_h_format')==sBLK?sBLK:gtSetStr('graph_h_format') | |
], | |
"vAxis": ["textStyle": ["fontSize": graph_vaxis_font, | |
"color": gtSetB('graph_vh_color_transparent') ? sTRANSPRNT : graph_vh_color], | |
"gridlines": ["color": gtSetB('graph_va_color_transparent') ? sTRANSPRNT : graph_va_color], | |
], | |
"vAxes": [ | |
0: ["title" : graph_show_left_label ? graph_left_label: null, | |
"titleTextStyle": ["color": gtSetB('graph_left_color_transparent') ? sTRANSPRNT : graph_left_color, "fontSize": graph_left_font], | |
"viewWindow": ["min": graph_vaxis_1_min != sBLK ? graph_vaxis_1_min : null, | |
"max": graph_vaxis_1_max != sBLK ? graph_vaxis_1_max : null], | |
"gridlines": ["count" : graph_vaxis_1_num_lines != sBLK ? graph_vaxis_1_num_lines : null ], | |
"minorGridlines": ["count" : 0] | |
], | |
1: ["title": graph_show_right_label ? graph_right_label : null, | |
"titleTextStyle": ["color": gtSetB('graph_right_color_transparent') ? sTRANSPRNT : graph_right_color, "fontSize": graph_right_font], | |
"viewWindow": ["min": graph_vaxis_2_min != sBLK ? graph_vaxis_2_min : null, | |
"max": graph_vaxis_2_max != sBLK ? graph_vaxis_2_max : null], | |
"gridlines": ["count" : graph_vaxis_2_num_lines != sBLK ? graph_vaxis_2_num_lines : null ], | |
"minorGridlines": ["count" : 0] | |
] | |
], | |
"legend": !gtSetB('graph_show_legend') ? ["position": sNONE] : ["position": graph_legend_position, | |
"alignment": graph_legend_inside_position, | |
"textStyle": ["fontSize": graph_legend_font, | |
"color": gtSetB('graph_legend_color_transparent') ? sTRANSPRNT : graph_legend_color]], | |
"backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), | |
"curveType": !graph_smoothing ? sBLK : "function", | |
"title": !gtSetB('graph_show_title') ? sBLK : gtSetStr('graph_title'), | |
"titleTextStyle": !gtSetB('graph_show_title') ? sBLK : ["fontSize": graph_title_font, "color": gtSetB('graph_title_color_transparent') ? sTRANSPRNT : gtSetStr('graph_title_color')], | |
"titlePosition" : gtSetB('graph_title_inside') ? "in" : "out", | |
"interpolateNulls": true, //for null vals on our chart | |
"orientation" : gtSetB('graph_y_orientation')? "vertical" : "horizontal", | |
"reverseCategories" : graph_z_orientation, | |
"series": [], | |
] | |
] | |
// TODO | |
List<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() | |
//add colors and thicknesses | |
Integer axis=Integer.parseInt(settings["graph_axis_number_${sa}"].toString()) | |
String tc= "graph_line_${sa}_color" | |
String text_color=gtSetStr(tc) | |
Boolean text_color_transparent=gtSetB(tc+"_transparent") | |
Integer line_thickness=gtSetI("attribute_${sa}_line_size") | |
Float opacity | |
opacity=0.0 | |
if(settings["attribute_${sa}_opacity"]){ | |
opacity=settings["attribute_${sa}_opacity"]/100.0 | |
} | |
Map annotations=[ | |
"targetAxisIndex": axis, | |
"color": text_color_transparent ? sTRANSPRNT : text_color, | |
"stroke": text_color_transparent ? sTRANSPRNT : "red", | |
"lineWidth": line_thickness, | |
"areaOpacity" : opacity | |
] | |
options.graphOptions.series << annotations | |
} | |
} | |
return options | |
} | |
String getDrawType_linegraph(){ | |
switch (gtSetStr('graph_type')){ | |
case "Line Graph": return "google.visualization.LineChart" | |
case "Area Graph": return "google.visualization.AreaChart" | |
case "Scatter Plot": return "google.visualization.ScatterChart" | |
} | |
return 'bad' | |
} | |
String getGraph_linegraph(){ | |
String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String html=""" | |
<!DOCTYPE html> | |
<html style="${fullSizeStyle}"> | |
<link rel='icon' href='https://www.shareicon.net/data/256x256/2015/09/07/97252_barometer_512x512.png' type='image/x-icon'/> | |
<link rel="apple-touch-icon" href="https://www.shareicon.net/data/256x256/2015/09/07/97252_barometer_512x512.png"> | |
<head> | |
${scriptIncludes()} | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/3.0.16/svg.min.js" integrity="sha256-MCvBrhCuX8GNt0gmv06kZ4jGIi1R2QNaSkadjRzinFs=" crossorigin="anonymous"></script> | |
<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){ | |
const now=new Date().getTime(); | |
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; | |
non_num=subscriptions.non_num[deviceId][attribute]; | |
if(non_num.valid){ | |
if(value == non_num.start){ | |
graphData[deviceId][attribute].push({ date: now-1000, value: non_num.endVal}); | |
graphData[deviceId][attribute].push({ date: now, value: non_num.startVal}); | |
} else if (value == non_num.end){ | |
graphData[deviceId][attribute].push({ date: now-1000, value: non_num.startVal}); | |
graphData[deviceId][attribute].push({ date: now, value: non_num.endVal}); | |
} | |
} else{ | |
stack[deviceId][attribute].push({ date: now, value: value }); | |
//check the stack | |
const graphEvents=graphData[deviceId][attribute]; | |
const stackEvents=stack[deviceId][attribute]; | |
const span=graphEvents[1].date - graphEvents[0].date; | |
if(stackEvents[stackEvents.length - 1].date - graphEvents[graphEvents.length - 1].date >= span | |
|| (stackEvents.length > 1 | |
&& stackEvents[stackEvents.length - 1].date - stackEvents[0].date >= span)){ | |
//push the stack | |
graphData[deviceId][attribute].push(stack[deviceId][attribute].reduce((accum, it) => accum={ date: accum.date + it.date / stackEvents.length, value: accum.value + it.value / stackEvents.length },{ date: 0, value: 0.0 })); | |
stack[deviceId][attribute]=[]; | |
//check for drop | |
const thisDrop=subscriptions.drop[deviceId][attribute]; | |
const thisEvents=graphData[deviceId][attribute]; | |
if(thisDrop.valid && thisEvents[thisEvents.length - 2].date - thisEvents[thisEvents.length - 1].date > thisDrop.time){ | |
graphData[deviceId][attribute].splice(thisEvents.length - 2, 0,{ date: thisEvents[thisEvents.length - 2].date + 1000, value: thisDrop.value }); | |
graphData[deviceId][attribute].splice(thisEvents.length - 2, 0,{ date: thisEvents[thisEvents.length - 1].date - 1000, value: thisDrop.value }); | |
} | |
} | |
} | |
//update if we are realtime | |
if(options.graphUpdateRate === 0) update(); | |
} | |
} | |
async function aupdate(){ | |
await getGraphData(); | |
//drawChart(); | |
update(); | |
} | |
function update(callback){ | |
//boot old data | |
let min=new Date().getTime(); | |
min -= options.graphTimespan; | |
Object.entries(graphData).forEach(([deviceId, attributes]) =>{ | |
Object.entries(attributes).forEach(([attribute, events]) =>{ | |
//filter old events | |
graphData[deviceId][attribute]=events.filter(it => it.date > min); | |
}); | |
}); | |
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)'); | |
chart=new ${drawType_linegraph}(document.getElementById("timeline")); | |
//create stack | |
Object.entries(graphData).forEach(([deviceId, attrs]) =>{ | |
stack[deviceId]={}; | |
Object.keys(attrs).forEach(attr =>{ | |
stack[deviceId][attr]=[]; | |
}); | |
}) | |
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 drawChart(callback){ | |
let now=new Date().getTime(); | |
let min=now - options.graphTimespan; | |
let dataTable=new google.visualization.DataTable(); | |
dataTable.addColumn({ label: 'Date', type: 'datetime' }); | |
let colNums={}; | |
let i=0; | |
subscriptions.ids.forEach((deviceId) =>{ | |
colNums[deviceId]={}; | |
subscriptions.attributes[deviceId].forEach((attr) =>{ | |
dataTable.addColumn({ label: subscriptions.labels[deviceId][attr].replace('%deviceName%', subscriptions.sensors[deviceId].displayName).replace('%attributeName%', attr), type: 'number' }); | |
colNums[deviceId][attr]=i++; | |
}); | |
}); | |
const totalCols=i; | |
let parsedGraphData=[]; | |
//map the graph data | |
Object.entries(graphData).forEach(([deviceIndex, attributes]) =>{ | |
Object.entries(attributes).forEach(([attribute, events]) =>{ | |
non_num=subscriptions.non_num[deviceIndex][attribute]; | |
var length=events.length; | |
events.forEach((event) =>{ | |
//Make a new entry | |
let newEntry=Array.apply(null, new Array(totalCols + 1)); | |
newEntry[0]=event.date; | |
newEntry[colNums[deviceIndex][attribute] + 1]=event.value; | |
parsedGraphData.push(newEntry); | |
}); | |
}); | |
}); | |
//map the stack | |
Object.entries(stack).forEach(([deviceIndex, attributes]) =>{ | |
Object.entries(attributes).forEach(([attribute, events]) =>{ | |
if(events.length > 0){ | |
const event=events.reduce((accum, it) => accum={ date: accum.date, value: accum.value + it.value / events.length },{ date: now, value: 0.0 }); | |
let newEntry=Array.apply(null, new Array(totalCols + 1)); | |
newEntry[0]=event.date; | |
newEntry[colNums[deviceIndex][attribute] + 1]=event.value; | |
parsedGraphData.push(newEntry); | |
} | |
}); | |
}); | |
parsedGraphData=parsedGraphData.map((it) => [ new Date(it[0]), ...it.slice(1).map((it) => parseFloat(it)) ]); | |
parsedGraphData.forEach(it =>{ | |
dataTable.addRow(it); | |
}); | |
let graphOptions=Object.assign({}, options.graphOptions); | |
graphOptions.hAxis=Object.assign(graphOptions.hAxis,{ viewWindow:{ min: new Date(min), max: new Date(now) } }); | |
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, 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_linegraph(){ | |
List<String> ids=[] | |
Map sensors_=[:] | |
Map attributes=[:] | |
Map labels=[:] | |
Map drop_=[:] | |
Map non_num_=[:] | |
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 dn=sMs(ent,sDISPNM) | |
//String typ=sMs(ent,sT).capitalize() | |
String sa="${sid}_${attribute}".toString() | |
String attr=attribute | |
if(!ids.contains(sid)) ids << sid // sensor.idAsLong | |
//only take what we need | |
//Map sensors_fmt=gtSensorFmt() | |
sensors_[sid]=[ (sID): sid /* , idAsLong: sensor.idAsLong */, displayName: dn ] | |
attributes[sid]= attributes[sid] ?: [] | |
attributes[sid] << attribute | |
labels[sid]= labels[sid] ?: [:] | |
labels[sid][attr]= gtSetStr("graph_name_override_${sa}") | |
labels[sid][attr]= gtSetStr("graph_name_override_${sa}") | |
drop_[sid]= drop_[sid] ?: [:] | |
non_num_[sid]= non_num_[sid] ?: [:] | |
if(gtSetB("attribute_${sa}_non_number")){ | |
String startString=gtSetStr("attribute_${sa}_startString") | |
String endString=gtSetStr("attribute_${sa}_endString") | |
non_num_[sid][attr]=[ valid: true, | |
(sSTART): startString, | |
startVal: settings["attribute_${sa}_${startString}"], | |
(sEND): endString, | |
endVal: settings["attribute_${sa}_${endString}"] | |
] | |
}else{ | |
non_num_[sid][attr]=[ valid: false, | |
(sSTART): sBLK, | |
(sEND): sBLK] | |
} | |
drop_[sid][attr]=[valid: settings["attribute_${sa}_drop_line"], | |
time: settings["attribute_${sa}_drop_time"], | |
(sVAL): settings["attribute_${sa}_drop_value"]] | |
} | |
} | |
Map subscriptions=[ | |
(sID): isPoll ? sPOLL : sSENSOR, | |
ids: ids, | |
'sensors': sensors_, | |
'attributes': attributes, | |
labels : labels, | |
drop : drop_, | |
non_num: non_num_ | |
] | |
return subscriptions | |
} | |
/* | |
* TODO: Rangebar methods | |
*/ | |
def mainRangebar(){ | |
mainShare1("Choose Numeric Attribute Only",'graph_timespan') | |
} | |
def deviceRangebar(){ | |
deviceShare1(true,true,false) | |
} | |
def attributeRangebar(){ | |
List<Map> dataSources= createDataSources(true) | |
//state.count_=0 | |
dynamicPage((sNM): "attributeConfigurationPage", nextPage:"graphSetupPage"){ | |
List<String> container | |
hubiForm_section("Graph Order", i1, "directions", sBLK){ | |
hubiForm_list_reorder('graph_order', sBACKGRND, "#3e4475") | |
} | |
// TODO | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String rid=ent.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() | |
// Integer cnt=1 | |
//state.count_++ | |
hubiForm_section("${sLblTyp(sMs(ent,sT))}${dn} - ${attribute}${hint}", i1, "directions",sid+attribute){ | |
container=[] | |
if(typ==sCSENSOR){ | |
if(isLtsAvailable(rid, attribute)){ | |
container << hubiForm_sub_section("Long Term Storage in use") | |
}else{ | |
String tvar="var_"+sa+"_lts" | |
app.updateSetting(tvar, false) | |
settings[tvar]= false | |
} | |
} | |
container << hubiForm_text_input("<b>Override ${typ} Name</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","#5b626e", false, true) | |
container << hubiForm_color ("Min/Max", "attribute_${sa}_minmax", "#607c91", false) | |
container << hubiForm_color ("Current Value", "attribute_${sa}_current", "#8eb6d4", false) | |
container << hubiForm_color ("Current Value Border", "attribute_${sa}_current_border", sWHT, false) | |
container << hubiForm_switch ((sTIT): "Show Current Value on Bar?", (sNM): "attribute_${sa}_show_value", (sDEFLT): false, (sSUBONCHG): true) | |
if(gtSetB("attribute_${sa}_show_value")){ | |
container << hubiForm_text_input("Units", "attribute_${sa}_annotation_units", sBLK, false) | |
} | |
hubiForm_container(container, i1) | |
} | |
//cnt += i1 | |
} | |
} | |
} | |
} | |
def graphRangebar(){ | |
List timespanEnum1=[[0:"Live"], [1:"Hourly"], [2:"Daily"], [3:"Every Three Days"], [4:"Weekly"]] | |
dynamicPage((sNM): "graphSetupPage"){ | |
List<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() | |
input( (sTYPE): sENUM, (sNM): "graph_timespan",(sTIT): "<b>Select Time span to Graph (i.e How Often to Reset Range)</b>", (sMULTP): false, (sREQ): false, options: timespanEnum1, (sDEFV): s2, (sSUBOC): true) | |
container << hubiForm_color ("Graph Background", "graph_background", sWHT, false) | |
container << hubiForm_slider ((sTIT): "Graph Bar Width (1%-100%)", (sNM): "graph_bar_percent", (sDEFLT): i90, (sMIN): i1, (sMAX): i100, (sUNITS): "%") | |
container << hubiForm_text_input("Graph Max", "graph_max", s100, false) | |
container << hubiForm_text_input("Graph Min", "graph_min", s0, false) | |
hubiForm_container(container, i1) | |
} | |
hubiForm_section("Axes", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_color ("Axis", "haxis", sBLACK, false) | |
container << hubiForm_font_size ((sTIT): "Axis", (sNM): "haxis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_slider ((sTIT): "Number of Pixels for Axis", (sNM): "graph_h_buffer", (sDEFLT): i40, (sMIN): i10, (sMAX): i500, (sUNITS): " pixels") | |
hubiForm_container(container, i1) | |
} | |
hubiForm_section("Device Names", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_font_size ((sTIT): "Device Name", (sNM): "graph_axis", (sDEFLT): i9, (sMIN): i2, (sMAX): i20) | |
container << hubiForm_color ("Device Name","graph_axis", sBLACK, false) | |
container << hubiForm_slider ((sTIT): "Number of Pixels for Device Name Area", (sNM): "graph_v_buffer", (sDEFLT): i100, (sMIN): i10, (sMAX): i500, (sUNITS): " pixels") | |
hubiForm_container(container, i1) | |
} | |
gatherGraphSize() | |
hubiForm_section("Annotations", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_font_size ((sTIT): "Annotation", (sNM): "annotation", (sDEFLT): i16, (sMIN): i2, (sMAX): i40) | |
container << hubiForm_switch ((sTIT): "Show Annotation Outside (true) or Inside (false) of Bars", (sNM): "annotation_inside", (sDEFLT): false) | |
container << hubiForm_color ("Annotation", "annotation", sBLACK, false) | |
container << hubiForm_color ("Annotation Aura", "annotation_aura", sWHT, false) | |
container << hubiForm_switch ((sTIT): "Bold Annotation", (sNM): "annotation_bold", (sDEFLT): false) | |
container << hubiForm_switch ((sTIT): "Italic Annotation", (sNM): "annotation_italic", (sDEFLT): false) | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
String getData_rangebar(){ | |
Map resp=[:] | |
Date then | |
then=new Date() | |
switch (gtSetStr('graph_timespan')){ | |
case s0: //"Live": | |
break | |
case s1: //"Hourly": | |
use (TimeCategory){ | |
then -= 1.hours | |
} | |
break | |
case s2: //"Daily": | |
then.setHours(0) | |
then.setMinutes(0) | |
then.setSeconds(0) | |
break | |
case "3": //"Every Three Days": | |
use (TimeCategory){ | |
then -= 2.days | |
} | |
then.setHours(0) | |
then.setMinutes(0) | |
then.setSeconds(0) | |
break | |
case "4": //"Weekly": | |
use (TimeCategory){ | |
then -= 6.days | |
} | |
then.setHours(0) | |
then.setMinutes(0) | |
then.setSeconds(0) | |
break | |
} | |
//Long graph_time=then.getTime() | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
resp[sid]= resp[sid] ?: [:] | |
List<Map> data=CgetData(ent, then) | |
// log.warn "got sensor: $sensor attribute: $attribute data1: $data" | |
//List data1=data.findAll{ (Long)it.date > graph_time} | |
List<Double> temp=data.collect{ Map it -> getValue(sid,attribute,it[sVAL]) } | |
//List temp=sensor.statesSince(attribute, then, [(sMAX): 1000]).collect{ it.getFloatValue() } | |
Integer sz=data.size() | |
//Float v= sensor.currentState(attribute).getFloatValue() | |
Float v= "${data[sz-i1][sVAL]}".toFloat() | |
if(temp.size() == iZ){ | |
resp[sid][attribute]=[(sCUR): v, (sMIN): v, (sMAX): v] | |
}else{ | |
resp[sid][attribute]=[(sCUR): v, (sMIN): temp.min(), (sMAX): temp.max()] | |
} | |
} | |
} | |
return JsonOutput.toJson(resp) | |
} | |
Map getOptions_rangebar(){ | |
List colors=[] | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
String attrib_string="attribute_${sid}_${attribute}_color" | |
String transparent_attrib_string= attrib_string+"_transparent" | |
colors << (gtSetB(transparent_attrib_string) ? sTRANSPRNT : gtSetStr(attrib_string)) | |
} | |
} | |
String axis1 | |
String axis2 | |
if(gtSetStr('graph_type') == s1){ | |
axis1="hAxis" | |
axis2="vAxis" | |
}else{ | |
axis1="vAxis" | |
axis2="hAxis" | |
} | |
Map options=[ | |
"graphTimespan": Integer.parseInt(gtSetStr('graph_timespan')), | |
"graphUpdateRate": Integer.parseInt(gtSetStr(sGRPHUPDRATE)), | |
(sGRAPHT): Integer.parseInt(gtSetStr('graph_type')), | |
"graphOptions": [ | |
"bar" : [ "groupWidth" : "${settings.graph_bar_percent}%", | |
], | |
"width": gtSetB(sGRPHSTATICSZ) ? settings.graph_h_size : s100PCT, | |
"height": gtSetB(sGRPHSTATICSZ) ? settings.graph_v_size: "90%", | |
"timeline": [ | |
"rowLabelStyle": ["fontSize": settings.graph_axis_font, "color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : settings.graph_axis_color], | |
"barLabelStyle": ["fontSize": settings.graph_axis_font] | |
], | |
"backgroundColor": gtSetB('graph_background_color_transparent') ? sTRANSPRNT : gtSetStr('graph_background_color'), | |
"isStacked": true, | |
"chartArea": [ (sLEFT): gtSetStr('graph_type') == s1 ? settings.graph_v_buffer : settings.graph_h_buffer, | |
(sRIGHT) : i10, | |
"top": i10, | |
"bottom": gtSetStr('graph_type') == s1 ? settings.graph_h_buffer : settings.graph_v_buffer ], | |
"legend" : [ "position" : sNONE ], | |
(axis1): [ "viewWindow" : ["max" : graph_max, | |
"min" : graph_min], | |
"minValue" : graph_min, | |
"maxValue" : graph_max, | |
"textStyle" : ["color": gtSetB('haxis_color_transparent') ? sTRANSPRNT : haxis_color, | |
"fontSize": haxis_font] | |
], | |
(axis2): [ "textStyle" : ["color": gtSetB('graph_axis_color_transparent') ? sTRANSPRNT : graph_axis_color, | |
"fontSize": graph_axis_font] | |
], | |
"annotations" : [ "alwaysOutside": true, | |
"textStyle": [ | |
"fontSize": annotation_font, | |
"bold": annotation_bold, | |
"italic": annotation_italic, | |
"color": gtSetB('annotation_color_transparent') ? sTRANSPRNT : annotation_color, | |
"auraColor":gtSetB('annotation_aura_color_transparent') ? sTRANSPRNT : annotation_aura_color, | |
], | |
"stem": [ "color": sTRANSPRNT ], | |
"highContrast": sFALSE | |
], | |
], | |
"graphLow": graph_min, | |
"graphHigh": graph_max, | |
] | |
return options | |
} | |
String getGraph_rangebar(){ | |
String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String html=""" | |
<!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[deviceId][attribute].current=value; | |
if(value > graphData[deviceId][attribute].max) graphData[deviceId][attribute].max=value; | |
else if (value < graphData[deviceId][attribute].min) graphData[deviceId][attribute].min=value; | |
//update if we are realtime | |
if(options.graphUpdateRate === 0) update(); | |
} | |
} | |
async function aupdate(){ | |
await getGraphData(); | |
//drawChart(); | |
update(); | |
} | |
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 drawChart(callback){ | |
let now=new Date().getTime(); | |
let min=now - options.graphTimespan; | |
const dataTable=new google.visualization.arrayToDataTable([[{ type: 'string', label: 'Device' },{ type: 'number', label: 'na' }, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'nb' }, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'nc' }, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'nd'}, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'ne'},{ role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'a'}, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'b'}, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'c' }, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'd' }, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
{ type: 'number', label: 'e' }, { role: "style" },{ role: "tooltip" },{ role: "annotation" }, | |
]]); | |
let globalMax=options.graphHigh; | |
let globalMin=options.graphLow; | |
subscriptions.order.forEach(orderStr =>{ | |
const splitStr=orderStr.split('_'); | |
const deviceId=splitStr[1]; | |
const attr=splitStr[2]; | |
const event=graphData[deviceId][attr]; | |
globalMax=globalMax < event.max ? event.max : globalMax; | |
globalMin=globalMin > event.min ? event.min : globalMin; | |
}); | |
globalMax=globalMax < 0 ? 0 : globalMax; | |
globalMin=globalMin > 0 ? 0 : globalMin; | |
console.log (globalMin+" "+globalMax); | |
subscriptions.order.forEach(orderStr =>{ | |
const splitStr=orderStr.split('_'); | |
const deviceId=splitStr[1]; | |
const attr=splitStr[2]; | |
const event=graphData[deviceId][attr]; | |
var max_=event.max; | |
var min_=event.min; | |
var cur_=parseFloat(event.current); | |
var L=parseFloat(globalMin); | |
var H=parseFloat(globalMax); | |
var Mi=min_; | |
var Ma=max_; | |
var C1=cur_ - (0.5*(( options.graphHigh - options.graphLow ) * 0.01)); //the bar is 1% high | |
var C2=cur_ + (0.5*(( options.graphHigh - options.graphLow ) * 0.01)); //the bar is 1% highglobalMa | |
var na, nb, nc, nd, ne; | |
var a, b, c, d, e; | |
//Handle all the positive ranges | |
a=Mi - L; | |
b=C1 - Mi; | |
c=C2 - C1; | |
d=Ma - C2; | |
e=H - Ma; | |
//Handle all the negative ranges | |
na=-e; | |
nb=-d; | |
nc=-c; | |
nd=-b; | |
ne=-a; | |
if(H <= 0){ | |
a=0; b=0; c=0; d=0; e=0; | |
} else if (Ma <= 0){ | |
a=0; b=0; c=0; d=0; | |
e=H; | |
na=Ma; | |
} else if (C2 <=0 ){ | |
a=0; b=0; c=0; | |
d=Ma; | |
nb=C2; | |
na=0; | |
} else if (C1 <= 0){ | |
a=0; b=0; | |
c=C2; | |
nc=C1; | |
na=0; nb=0; | |
} else if (Mi <= 0){ | |
a=0; | |
b=C1; | |
nd=Mi; | |
na=0; nb=0; nc=0; | |
} else if (L <= 0){ | |
a=Mi; | |
ne=L; | |
na=0; nb=0; nc=0; nd=0; | |
} else{ | |
na=0; nb=0; nc=0; nd=0; ne=0; | |
} | |
var cur_String=''; | |
var units_=``; | |
const name=subscriptions.labels[deviceId][attr].replace('%deviceName%', subscriptions.sensors[deviceId].displayName).replace('%attributeName%', attr); | |
const colors=subscriptions.colors[deviceId][attr]; | |
if(colors.annotation_units != null){ | |
units_=`\${colors.annotation_units}` | |
} | |
cur_String=``; | |
ncur_String=``; | |
if(colors.showAnnotation == true){ | |
if(cur_ >= 0) cur_String=`\${cur_.toFixed(1)}\${units_}`; | |
if(cur_ < 0) ncurString=`\${cur_.toFixed(1)}\${units_}`; | |
} | |
var stats_=`\${name}\nMin: \${min_}\${units_}\nMax: \${max_}\${units_}\nCurrent: \${cur_}\${units_}` | |
dataTable.addRow([name, na, `color: \${colors.backgroundColor}`, `\${stats_}`, '', | |
nb, `color: \${colors.minMaxColor}`, `\${stats_}`, '', | |
nc, `{color: \${colors.currentValueColor}; stroke-color: \${colors.currentValueBorderColor}; stroke-opacity: 1.0; stroke-width: 1;}`, `\${stats_}`, ncur_String, | |
nd, `color: \${colors.minMaxColor}`, `\${stats_}`, '', | |
ne, `color: \${colors.backgroundColor}`, `\${stats_}`, '', | |
a, `color: \${colors.backgroundColor}`, `\${stats_}`, '', | |
b, `color: \${colors.minMaxColor}`, `\${stats_}`, '', | |
c, `{color: \${colors.currentValueColor}; stroke-color: \${colors.currentValueBorderColor}; stroke-opacity: 1.0; stroke-width: 1;}`, `\${stats_}`, cur_String, | |
d, `color: \${colors.minMaxColor}`, `\${stats_}`, '', | |
e, `color: \${colors.backgroundColor}`, `\${stats_}`, '' | |
]); | |
}); | |
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_rangebar(){ | |
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 // sensor.id | |
_attributes[sid]=[] | |
labels[sid]=[:] | |
colors[sid]=[:] | |
_attributes[sid] << attribute | |
labels[sid][attribute]= gtSetStr("graph_name_override_${sa}") | |
colors[sid][attribute]=["backgroundColor": gtSetB("attribute_${sa}_background_color_transparent") ? sTRANSPRNT : gtSetStr("attribute_${sa}_background_color"), | |
"minMaxColor": gtSetB("attribute_${sa}_minmax_color_transparent") ? sTRANSPRNT : settings["attribute_${sa}_minmax_color"], | |
"currentValueColor": gtSetB("attribute_${sa}_current_color_transparent") ? sTRANSPRNT : settings["attribute_${sa}_current_color"], | |
"currentValueBorderColor": gtSetB("attribute_${sa}_current_border_color_transparent") ? sTRANSPRNT : settings["attribute_${sa}_current_border_color"], | |
"showAnnotation": settings["attribute_${sa}_show_value"], | |
"annotation_font": settings["attribute_${sa}_annotation_font"], | |
"annotation_units": settings["attribute_${sa}_annotation_units"], | |
] | |
} | |
} | |
Map sensors_fmt=gtSensorFmt() | |
List order=gtSetStr('graph_order') ? parseJson(gtSetStr('graph_order')) : [] | |
Map subscriptions=[ | |
(sID): isPoll ? sPOLL : sSENSOR, | |
'sensors': sensors_fmt, | |
"ids": _ids, | |
'attributes': _attributes, | |
"labels": labels, | |
"colors": colors, | |
"order": order | |
] | |
return subscriptions | |
} | |
/* | |
* TODO: Radar methods | |
*/ | |
def tileRadar(){ | |
List<Map> zoomEnum = [[3:"3"], [4: "4"], [5: "5"], [6: "6"], [7: "7"], [8: "8"], [9: "9"], [10: "10"]] | |
List<Map> refreshEnum=[[60000:"1 minute"], [300000: "5 minutes"], [600000: "10 minutes"], [1200000: "20 minutes"], [1800000: "30 minutes"], [3600000: "1 hour"]] | |
List<Map<String,String>> weatherMapEnum=[["radar" : "Current Radar"], | |
["temp" : "Temperature"], | |
["wind" : "Wind"], | |
["rain" : "Rain and Thunder"], | |
["rainAccu" : "Rain Accumulation"], | |
["snowAccu" : "Snow Accumulation"], | |
["snowcover": "Snow Ground Cover"]] | |
List<Map<String,String>> forecastModelEnum =[["ecmwf": "European Centre for Medium-Range Weather Forecasts"], | |
["gfs": "Global Forecast System"]] | |
List<Map<String,String>> hoursModelEnum=[["now" : "Current"], | |
["12" : "12 Hours"], | |
["24" : "24 Hours"]] | |
List<Map<String,String>> measureEnum=[["in": "inches"], | |
["mm": "millimeters"]] | |
/* List<Map<String,String>> windEnum=[["knot" : "Knots (k)"], | |
[(sMETERSPS) : "Meters / Second (m/s)"], | |
[(sKILOSPH) : "Kilometers / Hour (km/h)"], | |
[(sMILESPH) : "Miles per Hour (mph)"]] */ | |
List<Map<String,String>> tempEnum = [[(sFAHR): "Fahrenheit (°F)"], | |
[(sCELS) : "Celsius (°C)"]] | |
dynamicPage((sNM): "graphSetupPage"){ | |
List<String> container | |
hubiForm_section("Tile Setup", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_text_input ("<b>Latitude (Default=Hub location)</b>", "latitude", location.latitude.toString(), false) | |
container << hubiForm_text_input ("<b>Longitude (Default=Hub location)</b>", "longitude", location.longitude.toString(), false) | |
hubiForm_container(container, i1) | |
//if(!overlay) overlay="radar"; | |
/* if(!settings.overlay){ | |
app.updateSetting("overlay", [(sTYPE): sENUM, (sVAL): "radar"]) | |
settings['overlay']='radar' | |
app.updateSetting("refresh", [(sTYPE): sENUM, (sVAL): 600000]) | |
settings['refresh']=600000 | |
app.updateSetting("zoom", [(sTYPE): sENUM, (sVAL): 3]) | |
settings['zoom']=i3 | |
} */ | |
input( (sTYPE): sENUM, (sNM): "zoom",(sTIT): "<b>Zoom Amount</b>", (sREQ): false, (sMULTP): false, options: zoomEnum, (sDEFV): 3, (sSUBOC): false) | |
input( (sTYPE): sENUM, (sNM): "refresh",(sTIT): "<b>Refresh Time</b>", (sREQ): false, (sMULTP): false, options: refreshEnum, (sDEFV): 600000, (sSUBOC): false) | |
input( (sTYPE): sENUM, (sNM): 'overlay',(sTIT): "<b>Map Type</b>", (sREQ): false, (sMULTP): false, options: weatherMapEnum, (sDEFV): "radar", (sSUBOC): true) | |
if(gtSetStr('overlay') != "radar"){ | |
container=[] | |
container << hubiForm_text("""<b>You have chosen a forecast map.</b> Please note:<br> | |
1. Forecast maps are update on the hour<br> | |
2. "Current" is the current condition (within the last hour)<br> | |
3. Refreshing these maps "more often" won't change anything""") | |
hubiForm_container(container, i1) | |
if(gtSetStr('product') == "radar") app.updateSetting('product', [(sTYPE): sENUM, (sVAL): "gfs"]) | |
input( (sTYPE): sENUM, (sNM): 'product',(sTIT): "<b>Forecast Model</b>", (sREQ): false, (sMULTP): false, options: forecastModelEnum, (sDEFV): "gfs", (sSUBOC): false) | |
input( (sTYPE): sENUM, (sNM): "calendar",(sTIT): "<b>Display Time</b>", (sREQ): false, (sMULTP): false, options: hoursModelEnum, (sDEFV): "now", (sSUBOC): false) | |
}else{ | |
app.updateSetting ('product', [(sTYPE): sENUM, (sVAL): "gfs"]) | |
app.updateSetting ("calendar", [(sTYPE): sENUM, (sVAL): "now"]) | |
} | |
/* if(!gtSetStr('wind_units')){ | |
app.updateSetting('wind_units', [(sTYPE): sENUM, (sVAL): sMILESPH]) | |
settings['wind_units']=sMILESPH | |
app.updateSetting('temp_units', [(sTYPE): sENUM, (sVAL): sFAHR]) | |
settings['temp_units']=sFAHR | |
app.updateSetting(sBACKGRND, [(sTYPE): sENUM, (sVAL): sBLACK]) | |
settings['background']='#000000' | |
app.updateSetting("background_opacity", [(sTYPE): sENUM, (sVAL): i90]) | |
settings['background_opacity']=i90 | |
} */ | |
input( (sTYPE): sENUM, (sNM): 'wind_units',(sTIT): "<b>Wind Speed Units</b>", (sREQ): false, (sMULTP): false, options: unitWind /*windEnum*/, (sDEFV): sMILESPH, (sSUBOC): false) | |
input( (sTYPE): sENUM, (sNM): 'temp_units',(sTIT): "<b>Temperature Units</b>", (sREQ): false, (sMULTP): false, options: tempEnum, (sDEFV): sFAHR, (sSUBOC): false) | |
container=[] | |
container << hubiForm_switch((sTIT): "<b>Show Marker on Graph?</b>", | |
(sNM): "marker", | |
(sDEFLT): false, | |
(sSUBONCHG): false) | |
container << hubiForm_color("Background", | |
sBACKGRND, | |
sBLACK, | |
false) | |
container << hubiForm_slider ((sTIT): "Background Opacity", | |
(sNM): "background_opacity", | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
def mainRadar(){ | |
dynamicPage((sNM): "mainPage"){ | |
checkDup() | |
List<String> container | |
if(!state.endpoint){ | |
hubiForm_section("Please set up OAuth API", i1, "report", sBLK){ | |
href( (sNM): "enableAPIPageLink",(sTIT): "Enable API", description: sBLK, page: "enableAPIPage") | |
} | |
}else{ | |
hubiForm_section(tDesc()+" Graph Options", i1, "tune", sBLK){ | |
container=[] | |
container << hubiForm_page_button("Setup Tile", "graphSetupPage", s100PCT, "vibration") | |
hubiForm_container(container, i1) | |
} | |
if(gtSetStr('wind_units')){ | |
local_graph_url() | |
preview_tile() | |
} | |
put_settings() | |
} | |
} | |
} | |
String getGraph_radar(){ | |
//String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String wind | |
wind="kt" | |
switch (gtSetStr('wind_units')){ | |
case "knot" : wind="kt"; break | |
case sMETERSPS : wind="m%2Fs"; break | |
case sKILOSPH : wind="km%2Fh"; break | |
case sMILESPH : wind="mph"; break | |
} | |
String temp | |
temp="%C2%B0F" | |
switch (gtSetStr('temp_units')){ | |
case sFAHR: temp="%C2%B0F"; break | |
case sCELS : temp="%C2%B0C" | |
} | |
String html=""" | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<style> | |
.wrapper{ | |
display: flex; | |
flex-flow: column; | |
height: 100%; | |
background-color: ${getRGBA(gtSetStr('background_color'), background_opacity)}; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="wrapper" id="radar"> | |
<iframe id="windy2" style="position: absolute !important;z-index: 2;" src="" data-fs="false"> | |
</iframe> | |
<iframe id="windy" style="position: absolute !important; z-index: 3;" src="" onload="(() =>{ | |
const NAME='once'; | |
var frameRefreshInterval; | |
var count=0; | |
if(this.name !== NAME){ | |
console.log('START') | |
this.name=NAME | |
frameRefreshInterval=setInterval(refreshFrame, ${refresh}); | |
} | |
function refreshFrame(){ | |
console.log('Refresh'+count); | |
document.getElementById('windy2').style.visibility='visible'; | |
//this.style.visibilty='hidden'; | |
document.getElementById('windy').style.zIndex=1; | |
document.getElementById('windy').src=document.getElementById('windy').src | |
count++; | |
} | |
setTimeout(() =>{ document.getElementById('windy').style.zIndex=3; }, 1000); | |
setTimeout(() =>{ document.getElementById('windy2').src=document.getElementById('windy2').src }, 2000); | |
})()"></iframe> | |
</div> | |
<script> | |
var url="https://embed.windy.com/embed2.html"; | |
var width=document.getElementById('radar').offsetWidth-5; | |
var height=window.innerHeight-15; | |
var params="?lat=${latitude}&lon=${longitude}&detailLat=${latitude}&detailLon=${longitude}&width="+width+"&height"+height+"&zoom=${zoom}&level=surface&overlay=${settings.overlay}&product=${product}&menu=&message=true&marker=${gtSetB('marker') ? 'true' : ''}&calendar=${calendar}&pressure=&type=map&location=coordinates&detail=&metricWind=${wind}&metricTemp=${temp}&radarRange=-1" | |
var iframe_url=url + params; | |
console.log(iframe_url); | |
document.getElementById("windy").src=iframe_url; | |
document.getElementById("windy2").src=iframe_url; | |
document.getElementById("windy").width=width+"px"; | |
document.getElementById("windy2").width=width+"px"; | |
document.getElementById("windy").height=height+"px"; | |
document.getElementById("windy2").height=height+"px"; | |
</script> | |
</body> | |
</html> | |
""" | |
return html | |
} | |
//oauth endpoints | |
/* | |
* TODO: Weather2 methods | |
*/ | |
def tileWeather2(){ | |
dynamicPage((sNM): "graphSetupPage"){ | |
hubiForm_section("General Options", i1, sBLK, sBLK){ | |
input( (sTYPE): sENUM, (sNM): "openweather_refresh_rate",(sTIT): "<b>Select OpenWeather Update Rate</b>", (sMULTP): false, (sREQ): true, options: updateEnum, (sDEFV): "300000") | |
List<String> container=[] | |
container << hubiForm_color("Background", | |
sBACKGRND, | |
sBLACK, | |
false) | |
container << hubiForm_slider ((sTIT): "Background Opacity", | |
(sNM): "background_opacity", | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
container << hubiForm_switch ((sTIT): "Color Icons?", (sNM): "color_icons", (sDEFLT): false) | |
hubiForm_container(container, i1) | |
// List<Map> daysEnum=[[0: "Today"], [1: "Tomorrow"], [2: "2 Days from Now"], [3: "3 Days from Now"], [4: "4 Days from Now"], [5: "Five Days from Now"]] | |
// input( (sTYPE): sENUM, (sNM): "day_num",(sTIT): "Day to Display", (sMULTP): false, (sREQ): false, options: daysEnum, (sDEFV): "1") | |
} | |
((Map<String,Map>)state.unit_type).each{String key, Map measurement-> | |
if(measurement.out != sNONE ){ | |
hubiForm_section(sMs(measurement,sNM), i1, sBLK, sBLK){ | |
//TODO bad?? | |
List<String> container=[] | |
hubiForm_container(container, i1) | |
input( (sTYPE): sENUM, (sNM): key+"_units",(sTIT): "Displayed Units", (sREQ): false, (sMULTP): false, | |
options: measurement.enum, (sDEFV): measurement.out, (sSUBOC): false) | |
} | |
} | |
} | |
} | |
} | |
def deviceWeather2(){ | |
List<Map> final_attrs | |
dynamicPage((sNM): "deviceSelectionPage"){ | |
List<String> container | |
hubiForm_section("Device Selection", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_switch((sTIT): "Make Hubitat Devices Available?", (sNM): "override_openweather", (sDEFLT): false, (sSUBONCHG): true) | |
hubiForm_container(container, i1) | |
} | |
Map<String,Map<String,Map>> measurement_list=[:] | |
if(gtSetB('override_openweather')){ | |
hubiForm_section("Sensor Selection", i1, sBLK, sBLK){ | |
container=[] | |
if(container)hubiForm_container(container, i1) | |
input ('sensors', "capability.*",(sTIT): "Select Sensors", (sMULTP): true, (sREQ): false, (sSUBOC): true) | |
} | |
if(sensors){ | |
final_attrs=[] | |
Map<String,Map<String,Map>> sensor_list=[:] | |
for(sensor in (List)sensors){ | |
String sid=gtSensorId(sensor) | |
if(sid==sBLK){ | |
error "deviceWeather2 null sid ${sensor}",null,iN2 | |
continue | |
} | |
List attributes_=(List)sensor.getSupportedAttributes() | |
sensor_list."${sid}"=[:] | |
for(attribute_ in attributes_){ | |
String name=attribute_.getName() | |
def cv= sensor.currentState(name,true) | |
if(cv){ | |
String units=cv.getUnit() | |
def value=cv.getValue() | |
String dn=sensor.displayName | |
sensor_list."${sid}"."${name}"=[ sensor_name: "${dn}", (sVAL): value, (sUNIT): units, supported_unit: getUnits(units, value)] | |
final_attrs << [("${sid}.${name}".toString()) : "${dn} (${name}) ::: [${value} ${units ?: sBLK} ]"] | |
} | |
} | |
} | |
final_attrs=final_attrs.unique(false) | |
((Map<String,Map>)state.unit_type).each{String key, Map type-> | |
measurement_list."${key}"=[:] | |
if(type.out != sNONE){ | |
hubiForm_section(sMs(type,sNM), i1, sBLK, sBLK){ | |
container=[] | |
input( (sTYPE): sENUM, (sNM): "${key}_devices",(sTIT): sMs(type,sNM), (sREQ): false, (sMULTP): true, options: final_attrs, (sDEFV): sBLK, (sSUBOC): true) | |
if(settings["${key}_devices"]){ | |
settings["${key}_devices"].each{ String iattr-> | |
String attr | |
attr=iattr | |
String sensor_id="${attr}".tokenize('.')[iZ] | |
if(!measurement_list."${key}"."${sensor_id}") | |
measurement_list."${key}"."${sensor_id}"=[:] | |
attr=attr.tokenize('.')[i1] | |
String sensor_name=sensor_list."${sensor_id}"."${attr}".sensor_name | |
if(((Map<String,Map>)state.unit_type)."${key}".enum == sNONE){ | |
container << hubiForm_text("<b>"+sensor_name+" :: "+attr+"</b>") | |
measurement_list."${key}"."${sensor_id}"."${attr}"=[sensor_name: sensor_list."${sensor_id}"."${attr}".sensor_name, | |
in_units: sNONE | |
] | |
}else if(sensor_list."${sensor_id}"."${attr}".supported_unit.var == key){ | |
String units=sensor_list."${sensor_id}"."${attr}".supported_unit.name | |
container << hubiForm_text("<b>"+sensor_name+" :: "+attr+"</b><br>"+'	'+" Units="+units) | |
measurement_list."${key}"."${sensor_id}"."${attr}"=[sensor_name: sensor_list."${sensor_id}"."${attr}".sensor_name, | |
in_units: sensor_list."${sensor_id}"."${attr}".supported_unit.units | |
] | |
}else{ | |
if(container)hubiForm_container(container, i1) | |
String unit=sensor_list."${sensor_id}"."${attr}".unit | |
List<Map> list=((Map<String,Map>)state.unit_type)."${key}".enum | |
if(list[iZ].none != "None") | |
input( (sTYPE): sENUM, (sNM): "${key}.${sensor_id}.${attr}", | |
(sTIT): "<b>"+sensor_name+" :: "+attr+"</b><br>Valid units not detected ("+unit+'); Expected <b>"'+key+'"</b> type<br><small>Please select measurement units below</small>', | |
(sREQ): false, (sMULTP): false, | |
options: list, | |
(sDEFV): sBLK, (sSUBOC): false) | |
measurement_list."${key}"."${sensor_id}"."${attr}"=[sensor_name: sensor_list."${sensor_id}"."${attr}".sensor_name, | |
in_units: settings["${key}.${sensor_id}.${attr}"] | |
] | |
container=[] | |
} | |
} | |
} | |
if(container)hubiForm_container(container, i1) | |
} | |
} | |
} | |
} | |
state.device_list=measurement_list | |
}else{ | |
//TODO clear out unused settings, sensors | |
wremoveSetting('sensors') | |
} | |
} | |
} | |
@Field static final String sFAHR='fahrenheit' | |
@Field static final String sCELS='celsius' | |
@Field static final String sNONE='none' | |
@Field static final String sYES='yes' | |
@Field static final String sNO='no' | |
@Field static final String sTEMP='temperature' | |
@Field static final String sCUR='current' | |
@Field List<Map> unitTemp = [[(sFAHR): "Fahrenheit (°F)"], [(sCELS) : "Celsius (°C)"], ["kelvin" : "Kelvin (K)"]] | |
@Field List<Map<String,String>> unitWind = [[(sMETERSPS): "Meters per Second (m/s)"], [(sMILESPH): "Miles per Hour (mph)"], ["knots": "Knots (kn)"], [(sKILOSPH): "Kilometers per Hour (km/h)"]] | |
@Field List<Map> unitDepth = [["millimeters": "Millimeters (mm)"], ["inches": """Inches (") """]] | |
@Field List<Map> unitPressure= [["millibars": "Millibars (mbar)"], ["millimeters_mercury": "Millimeters of Mercury (mmHg)"], ["inches_mercury": "Inches of Mercury (inHg)"], ["hectopascal" : "Hectopascal (hPa)"]] | |
@Field List<Map> unitDirection= [["degrees": "Degrees (°)"], ["radians" : "Radians (°)"], ["cardinal": "Cardinal (N, NE, E, SE, etc)"]] | |
@Field List<Map> unitTrend = [["trend_numeric": "Numeric (° < 0, °=0, ° > 0)"], ["trend_text": "Text (° rising, ° steady, ° falling)"]] | |
@Field List<Map> unitPercent = [["percent_numeric": "Numeric (0 to 100)"], ["percent_decimal": "Decimal (0.0 to 1.0)"]] | |
@Field List<Map> unitTime = [["time_seconds" : "Seconds since 1970"], ["time_milliseconds" : "Milliseconds since 1970"], ["time_twelve" : "12 Hour (2:30 PM)"], ["time_two_four" : "24 Hour (14:30)"]] | |
@Field List<Map> unitUVI= [["uvi" : "UV Index"]] | |
@Field List<Map> unitDistance= [["miles": "Miles"]] | |
@Field List<Map> unitBlank= [[(sNONE): "None"]] | |
@Field List<Map> unitDayofWeek= [["short": "Short (Thu)"], ["long": "Long (Thursday)"]] | |
@Field List<Map> unitText= [["plain": "Unformatted"], ["title": "Title Format"], ["lowercase": "Lowercase"], ["uppercase" : "Uppercase"]] | |
@Field List<Map> unitIcon= [[(sICON): "Default Icon"]] | |
@Field List<Map<String,Object>> tileSetFLD= [ | |
[ | |
(sTIT): 'Forecast Weather Icon', (sVAR): "weather_icon", (sTYPE): "weather_icon", period:sCUR, (sVAL): sBLK, | |
(sICON): "alert-circle", icon_loc: sCENTER, icon_space: sBLK, | |
h: i6, w: i12, (sBLROW): i1, (sBLCOL): i13, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): sNONE, decimal: sNO, unit_space: sBLK, | |
font: i40, font_weight: s100, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Current Weather', (sVAR): "description", (sTYPE): "weather_description", period:sCUR, (sVAL): iZ, | |
(sICON): sNONE, icon_loc: sNONE, icon_space: sBLK, | |
h: i4, w: i12, (sBLROW): i7, (sBLCOL): i13, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): iZ, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): sNONE, decimal: sNO, unit_space: sBLK, | |
font: i20, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Current Temperature', (sVAR): "current_temperature", (sTYPE): sTEMP, period:sCUR, | |
(sICON): sNONE, icon_loc: sLEFT, icon_space: sBLK, | |
h: i4, w: i12, (sBLROW): i1, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, | |
font: i20, font_weight: "900", | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Feels Like', (sVAR): "feels_like", (sTYPE): "feels_like", period:sCUR, | |
(sICON): "home-thermometer-outline", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i12, (sBLROW): i5, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: "Feels Like: ", (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, | |
font: i7, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Forecast High', (sVAR): "forecast_high", (sTYPE): "temperature_max", period:"daily.0", | |
(sICON): "arrow-up-thick", icon_loc: sLEFT, icon_space: sBLK, | |
h: i4, w: i6, (sBLROW): i7, (sBLCOL): i7, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, | |
font: i7, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Forecast Low', (sVAR): "forecast_low", (sTYPE): "temperature_min", period:"daily.0", | |
(sICON): "arrow-down-thick", icon_loc: sLEFT, icon_space: sBLK, | |
h: i4, w: i6, (sBLROW): i7, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, | |
font: i6, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Precipitation Title', (sVAR): "precipitation_title", (sTYPE): "blank", period:sNONE, | |
(sICON): "umbrella-outline", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): 11, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: "Precipitation", | |
lpad: iZ, rpad: iZ, (sDECIMALS): i1, | |
(sUNIT): unitDepth, decimal: sNO, unit_space: sBLK, | |
font: i6, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Forecast Precipitation', (sVAR): "forecast_precipitation", (sTYPE): "rain", period:"daily.0", | |
(sICON): "ruler", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): 15, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: sBLK, | |
lpad: iZ, rpad: iZ, (sDECIMALS): i1, | |
(sUNIT): unitDepth, decimal: sYES, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Forecast Percent Precipitation', (sVAR): "forecast_percent_precipitation", (sTYPE): "chance_precipitation", period:"daily.0", | |
(sICON): "cloud-question", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): i13, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitPercent, decimal: sYES, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Current Precipitation', (sVAR): "current_precipitation", (sTYPE): "rain_past_hour", period:sCUR, | |
(sICON): "calendar-today", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): 17, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitDepth, decimal: sYES, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Wind Title', (sVAR): "wind_title", (sTYPE): "blank", period:sNONE, | |
(sICON): "weather-windy-variant", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): 11, (sBLCOL): i9, | |
(sALIGNMENT): sCENTER, text: "Wind", (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): sNONE, decimal: sNO, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Wind Speed', (sVAR): "wind_speed", (sTYPE): "wind_speed", period:sCUR, | |
(sICON): "tailwind", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): i13, (sBLCOL): i9, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitWind, decimal: sYES, unit_space: sSPC, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Wind Gust', (sVAR): "wind_gust", (sTYPE): "wind_gust", period:sCUR, | |
(sICON): "weather-windy", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): 15, (sBLCOL): i9, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitWind, decimal: sYES, unit_space: sSPC, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Wind Direction', (sVAR): "wind_direction", (sTYPE): "wind_direction", period:sCUR, | |
(sICON): "compass-outline", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): 17, (sBLCOL): i9, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitDirection, decimal: sNO, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Pressure Title', (sVAR): "pressure_title", (sTYPE): "blank", period:sCUR, | |
(sICON): "gauge", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): 11, (sBLCOL): 17, | |
(sALIGNMENT): sCENTER, text: "Pressure", (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): sNONE, decimal: sYES, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Current Pressure', (sVAR): "current_pressure", (sTYPE): "pressure", period:sCUR, | |
(sICON): "thermostat", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i8, (sBLROW): i13, (sBLCOL): 17, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitPressure, decimal: sYES, unit_space: sSPC, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Humidity', (sVAR): "current_humidity", (sTYPE): "humidity", period:sCUR, | |
(sICON): "water-percent", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i4, (sBLROW): i20, (sBLCOL): i1, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitPercent, decimal: sYES, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Current Dewpoint', (sVAR): "current_dewpoint", (sTYPE): "dew_point", period:sCUR, | |
(sICON): "wave", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i4, (sBLROW): i20, (sBLCOL): 11, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitTemp, decimal: sYES, unit_space: sBLK, | |
font: i4, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Sunrise', (sVAR): "sunrise", (sTYPE): "sunrise", period:sCUR, | |
(sICON): "weather-sunset-up", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i5, (sBLROW): i20, (sBLCOL): 15, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitTime, decimal: sNO, unit_space: sBLK, | |
font: i3, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
[ | |
(sTIT): 'Sunset', (sVAR): "sunset", (sTYPE): "sunset", period:sCUR, | |
(sICON): "weather-sunset-down", icon_loc: sLEFT, icon_space: sSPC, | |
h: i2, w: i5, (sBLROW): i20, (sBLCOL): i20, | |
(sALIGNMENT): sCENTER, text: sBLK, (sDECIMALS): i1, | |
lpad: iZ, rpad: iZ, | |
(sUNIT): unitTime, decimal: sNO, unit_space: sBLK, | |
font: i3, font_weight: s400, | |
font_color: sDRKBLUE, font_opacity: s100, background_color: sLGHTGRN, background_opacity: s100, | |
font_auto_resize: sTRUE, (sJUSTIFICATION): sCENTER, font_adjustment: iZ, display: true, | |
], | |
] | |
@Field Map<String,Map> spanFLD= [ | |
current: [(sTIT): "Current Measurements", num_time: iZ, time_units: sBLK], | |
daily: [(sTIT): "Daily Forecast", num_time: i7, time_units: "day"], | |
hourly: [(sTIT): "Hourly Forecast", num_time: 48, time_units: "hour"], | |
blank: [(sTIT): "Blank Tile", num_time: iZ, time_units: sBLK], | |
sensor: [(sTIT): "Device Measurement", num_time: iZ, time_units: sBLK], | |
] | |
private static Map fill_temp_type(String name, String type, String ow, String in_units, String current, String hourly, String daily, String sensor){ | |
return [(sNM): name, (sTYPE): type, ow: ow, in_units: in_units,(sCUR): current, hourly: hourly, daily: daily, sensor: sensor] | |
} | |
//@Field static Map<String,Map> span_typeFLD | |
def mainWeather2(){ | |
// state.tile_dimensions=[rows: 14, columns: 26] | |
state.remove('span_type') | |
state.remove('tile_dimensions') | |
// one time initialization | |
if(!state.tile_settings){ | |
/* Map<String,Map> tmap | |
spanFLD.each{ String key, Map item -> | |
if(!tmap) tmap=[:] | |
tmap += [(key): [:]+item] | |
} | |
span_typeFLD= tmap */ | |
/* state.span_type=[ current: [(sTIT): "Current Measurements", num_time: iZ, time_units: sBLK], | |
daily: [(sTIT): "Daily Forecast", num_time: i7, time_units: "day"], | |
hourly: [(sTIT): "Hourly Forecast", num_time: 48, time_units: "hour"], | |
blank: [(sTIT): "Blank Tile", num_time: iZ, time_units: sBLK], | |
sensor: [(sTIT): "Device Measurement", num_time: iZ, time_units: sBLK], | |
] */ | |
List<Map> list=[] | |
tileSetFLD.each{Map<String,Object> item-> | |
Map<String,Object> tmap1 | |
tmap1=[:]+item | |
/* item.each{ String key, item1 -> | |
tmap1 += [(key): item1] | |
} */ | |
list << tmap1 | |
} | |
// This is the internal DB of current values and settings adjustments | |
state.tile_settings=list | |
} //else{ | |
// this remaps internal variables to the source types - can change based on settings/overrides | |
Map<String,Map> temp_type=[ | |
weather_icon: fill_temp_type("Weather Icon",sICON,"weather.0.description",sNONE,sYES,sYES,sYES,sNO), | |
weather_description: fill_temp_type("Weather Description", sTEXT,"weather.0.description", sNONE,sYES,sYES,sYES,sNO), | |
feels_like: fill_temp_type("Feels Like", sTEMP,"feels_like",sFAHR, sYES,sYES,sNO,sNO), | |
feels_like_morning: fill_temp_type("Morning Feels Like", sTEMP, "feels_like.morn", sFAHR, sNO, sNO, sYES, sNO), | |
feels_like_day: fill_temp_type("Day Feels Like", sTEMP, "feels_like.day", sFAHR, sNO, sNO, sYES, sNO), | |
feels_like_evening: fill_temp_type("Evening Feels Like", sTEMP, "feels_like.eve", sFAHR, sNO, sNO, sYES, sNO), | |
feels_like_night: fill_temp_type("Night Feels Like", sTEMP, "feels_like.night", sFAHR, sNO, sNO, sYES, sNO), | |
temperature: fill_temp_type("Temperature", sTEMP, "temp", sFAHR, sYES, sYES, sNO, sNO), | |
temperature_max: fill_temp_type("Maximum Temperature", sTEMP, "temp.max", sFAHR, sNO, sNO, sYES, sNO), | |
temperature_min: fill_temp_type("Minimum Temperature", sTEMP, "temp.min", sFAHR, sNO, sNO, sYES, sNO), | |
temperature_morning: fill_temp_type("Morning Temperature", sTEMP, "temp.morn", sFAHR, sNO, sNO, sYES, sNO), | |
temperature_day: fill_temp_type("Day Temperature", sTEMP, "temp.day", sFAHR, sNO, sNO, sYES, sNO), | |
temperature_evening: fill_temp_type("Evening Temperature", sTEMP, "temp.eve", sFAHR, sNO, sNO, sYES, sNO), | |
temperature_night: fill_temp_type("Night Temperature", sTEMP, "temp.night", sFAHR, sNO, sNO, sYES, sNO), | |
humidity: fill_temp_type("Humidity", "percent", "humidity", "percent_numeric", sYES, sYES, sYES, sNO), | |
dew_point: fill_temp_type("Dew Point",sTEMP, "dew_point", sFAHR, sYES, sYES, sYES, sNO), | |
pressure: fill_temp_type("Pressure","pressure", "pressure", "millibars", sYES, sYES, sYES, sNO), | |
uv_index: fill_temp_type("UV Index", "uvi", "uvi", "uvi", sYES, sNO, sYES, sNO), | |
cloud_coverage: fill_temp_type("Cloud Coverage", "percent", "clouds", "percent_numeric", sYES, sNO, sYES, sNO), | |
visibility: fill_temp_type("Visibility", "distance", "visibility", "miles", sYES, sNO, sYES, sNO), | |
wind_speed: fill_temp_type("Wind Speed", "velocity", "wind_speed", sMILESPH,sYES, sYES, sYES, sNO), | |
wind_gust: fill_temp_type("Wind Gust", "velocity", "wind_gust", sMILESPH, sYES, sYES, sYES, sNO), | |
wind_direction: fill_temp_type("Wind Direction", "direction", "wind_deg", "degrees", sYES, sYES, sYES, sNO), | |
rain_past_hour: fill_temp_type("Rain past Hour", "depth", "rain.1h", "millimeters", sYES, sYES, sNO, sNO), | |
snow_past_hour: fill_temp_type("Snow past Hour", "depth", "snow.1h", "millimeters", sYES, sYES, sNO, sNO), | |
rain: fill_temp_type("Rain", "depth", "rain", "millimeters", sNO, sNO, sYES, sNO), | |
snow: fill_temp_type("Snow", "depth", "snow", "millimeters", sNO, sNO, sYES, sNO), | |
precipitation: fill_temp_type("Precipitation", "depth", "precipitation", "millimeters", sNO, sNO, sYES, sNO), | |
chance_precipitation: fill_temp_type("Chance of Precipitation", "percent", "pop", "percent_decimal", sYES, sYES, sYES, sNO), | |
sunrise: fill_temp_type("Sunrise", sTIME, "sunrise", "time_seconds", sYES, sYES, sYES, sNO), | |
sunset: fill_temp_type("Sunset", sTIME, "sunset", "time_seconds", sYES, sYES, sYES, sNO), | |
hour: fill_temp_type("Hour", sTIME, "dt", "time_seconds", sNO, sYES, sNO, sNO), | |
day: fill_temp_type("Day", "day", "dt", "time_seconds", sNO, sYES, sYES, sNO), | |
blank: fill_temp_type("Blank Tile", "blank", sNONE, sNONE, sNO, sNO, sNO, sNO), | |
time_stamp: fill_temp_type("Data Time Stamp", sTIME, "dt", "time_seconds", sYES, sNO, sNO, sNO) | |
] | |
//atomicState.tile_type=temp_type | |
Map<String,Map> temp_unit=[ | |
temperature: [(sNM): "Temperature", (sENUM): unitTemp, out: sFAHR, parse_func: "formatNumericData"], | |
percent: [(sNM): "Percentage", (sENUM): unitPercent, out: "percent_numeric", parse_func: "formatNumericData"], | |
(sICON): [(sNM): "Weather Icons", (sENUM): unitIcon, out: sNONE, parse_func: "translateCondition"], | |
pressure: [(sNM): "Pressure", (sENUM): unitPressure, out: "inches_mercury", parse_func: "formatNumericData"], | |
velocity: [(sNM): "Velocity", (sENUM): unitWind, out: sMILESPH, parse_func: "formatNumericData"], | |
time: [(sNM): "Time", (sENUM): unitTime, out: "time_twelve", parse_func: "formatNumericData"], | |
depth: [(sNM): "Depth", (sENUM): unitDepth, out: "inches", parse_func: "formatNumericData"], | |
direction: [(sNM): "Direction", (sENUM): unitDirection, out: "cardinal", parse_func: "formatNumericData"], | |
uvi: [(sNM): "UV Index", (sENUM): unitUVI, out: "uvi", parse_func: "formatNumericData"], | |
visibility: [(sNM): "Visibility", (sENUM): unitDistance, out: "visibility", parse_func: "formatNumericData"], | |
blank: [(sNM): "Blank Tile", (sENUM): unitBlank, out: sNONE, parse_func: sNONE], | |
day: [(sNM): "Day of Week", (sENUM): unitDayofWeek, out: "short", parse_func: "formatDayData"], | |
text: [(sNM): "Text Description", (sENUM): unitText, out: "plain", parse_func: "formatTextData"], | |
] | |
//atomicState.unit_type=temp_unit | |
//Update the Output Types | |
Map<String,Map> unitT= [:]+temp_unit // atomicState.unit_type | |
temp_unit.each{String key, Map item-> | |
if(settings["${key}_units"]){ | |
unitT."${key}".out=settings["${key}_units"] | |
} | |
} | |
state.unit_type=unitT | |
//reset to OpenWeather Data | |
Map<String,Map> temp=[:] + temp_type //atomicState.tile_type | |
temp.wind_speed.in_units=sMILESPH | |
temp.wind_gust.in_units=sMILESPH | |
//atomicState.tile_type.each{key, item-> | |
temp_type.each{String key, Map item-> | |
if(sMs(item,sSENSOR) == sNO){ | |
temp << [(key): item] | |
} | |
} | |
state.tile_type=temp | |
// } | |
// Map<String,Map> temp=(Map<String,Map>)state.tile_type | |
count=iZ | |
((Map<String,Map<String,Map<String,Map>>>)state.device_list).each{ String type, Map<String,Map<String,Map>>var1-> | |
if(var1 != [:]){ | |
var1.each{String device, Map<String,Map> var2-> | |
var2.each{String attr, Map var3-> | |
temp."device_${device}_${attr}_${type}"=[(sNM): "${var3.sensor_name} :: ${attr} (${type})", (sTYPE): "${type}", ow: "device.${device}.${attr}", in_units: var3.in_units, (sCUR): sNO, hourly: sNO, daily: sNO, sensor: sYES] | |
} | |
} | |
} | |
} | |
state.tile_type=temp | |
TreeMap<String,TreeMap> typeList= new TreeMap([:]) | |
typeList.main_list=new TreeMap([:]) | |
spanFLD.each{String span_key, Map span-> | |
((TreeMap)typeList.main_list).put(span_key, [(sNM): span_key.capitalize()]) | |
typeList[span_key]= new TreeMap([:]) | |
((TreeMap)typeList[span_key]).measurement_list=new TreeMap([:]) | |
((Map<String,Map>)state.tile_type).each{String key, Map item-> | |
if(item[span_key] == sYES) | |
((TreeMap)((TreeMap)typeList[span_key]).measurement_list) << [(key): [(sNM): sMs(item,sNM)]] | |
} | |
Integer cnt= iMs(span,'num_time') | |
if(cnt > iZ){ | |
((TreeMap)typeList[span_key]).time_list=new TreeMap([:]) | |
String time_units=sMs(span,'time_units') | |
((TreeMap)typeList[span_key]).title= time_units.capitalize()+"s to Display" | |
Map a | |
Integer i | |
for(i=iZ; i<cnt; i++){ | |
String s= i<i10 ? "0$i".toString() : "$i".toString() | |
String m | |
if(time_units == "day" && i==iZ) | |
m=" Today" | |
else if (time_units == "day" && i==i1) | |
m= " Tomorrow" | |
else if (i==i1) | |
m= " $i ${time_units} from now" | |
else | |
m= " $i ${time_units}s from now".toString() | |
a = [(s): [(sNM): m]] | |
((TreeMap)((TreeMap)typeList[span_key]).time_list) << a | |
} | |
} | |
} | |
//atomicState.newTileDialog=sBLK | |
state.newTileDialog=typeList.sort() | |
dynamicPage((sNM): "mainPage"){ | |
checkDup() | |
List<String> container | |
if(!state.endpoint){ | |
hubiForm_section("Please set up OAuth API", i1, "report", sBLK){ | |
href( (sNM): "enableAPIPageLink",(sTIT): "Enable API", description: sBLK, page: "enableAPIPage") | |
} | |
}else{ | |
hubiForm_section("Tile Options", i1, "tune", sBLK){ | |
container=[] | |
container << hubiForm_page_button("Select Device/Data", "deviceSelectionPage", s100PCT, "vibration") | |
container << hubiForm_page_button("Configure Tile", "graphSetupPage", s100PCT, sPOLL) | |
hubiForm_container(container, i1) | |
} | |
if(day_num){ | |
local_graph_url() | |
hubiForm_section("Configure Tile - Desktop Only", i10, "settings", sBLK){ | |
container=[] | |
container << getPreviewWindow("tile_settings_HTML", "mainPage") | |
hubiForm_container(container, i1) | |
} | |
install_tile() | |
} | |
put_settings() | |
} | |
} | |
} | |
def verifyDeviceCallback(response, data){ | |
} | |
String getPreviewWindow(String var, String page){ | |
Map params=[ | |
uri: getEndpointURL(), | |
path: "graph/?access_token=${getEndpointSecret()}", | |
requestContentType: "application/json", | |
] | |
asynchttpGet(verifyDeviceCallback, params) | |
if(!settings["$var"]){ wremoveSetting(var.toString()) } | |
String html | |
html=""" | |
<style> | |
.iframe-container{ | |
overflow: hidden | |
width: 55vmin | |
height: 65vmin | |
position: relative | |
} | |
.iframe-container iframe{ | |
border: 0 | |
left: 0 | |
position: absolute | |
top: 0 | |
} | |
</style> | |
""" | |
//<input type="text" id="settings${var}" name="settings[${var}]" value="${settings[var]}" style="display: none;" > | |
//<div class="form-group" style="display:none;"> | |
// <input type="hidden" name="${var}.type" value="text"> | |
// <input type="hidden" name="${var}.multiple" value="false"> | |
//</div> | |
//<div> | |
html+=""" | |
<div class="iframe-container"> | |
<iframe id="preview_frame" style="width: 100%; height: 100%; position: relative; z-index: 1; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAEq2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIiCiAgIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSIyIgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMiIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMiIKICAgdGlmZjpSZXNvbHV0aW9uVW5pdD0iMiIKICAgdGlmZjpYUmVzb2x1dGlvbj0iNzIuMCIKICAgdGlmZjpZUmVzb2x1dGlvbj0iNzIuMCIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyMC0wNi0wMlQxOTo0NzowNS0wNDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMC0wNi0wMlQxOTo0NzowNS0wNDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjguMyIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMC0wNi0wMlQxOTo0NzowNS0wNDowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+IC4TuwAAAYRpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAACiRdZE7SwNBFEaPiRrxQQQFLSyCRiuVGEG0sUjwBWqRRPDVbDYvIYnLboIEW8E2oCDa+Cr0F2grWAuCoghiZWGtaKOy3k2EBIkzzL2Hb+ZeZr4BWyippoxqD6TSGT0w4XPNLyy6HM/UYqONfroU1dBmguMh/h0fd1RZ+abP6vX/uYqjIRI1VKiqEx5VNT0jPCk8vZbRLN4WblUTSkT4VLhXlwsK31p6uMgvFseL/GWxHgr4wdYs7IqXcbiM1YSeEpaX404ls+rvfayXNEbTc0HJnbI6MAgwgQ8XU4zhZ4gBRiQO0YdXHBoQ7yrXewr1s6xKrSpRI4fOCnESZOgVNSvdo5JjokdlJslZ/v/11YgNeovdG31Q82Sab93g2ILvvGl+Hprm9xHYH+EiXapfPYDhd9HzJc29D84NOLssaeEdON+E9gdN0ZWCZJdli8Xg9QSaFqDlGuqXip797nN8D6F1+aor2N2DHjnvXP4Bhcln9Ef7rWMAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAXSURBVAiZY7hw4cL///8Z////f/HiRQBMEQrfQiLDpgAAAABJRU5ErkJggg=='); background-size: 25px; background-repeat: repeat; image-rendering: pixelated;" src="${makeCallBackURL('graph/')}" data-fullscreen="false" | |
onload="(() =>{ | |
})()""></iframe> | |
</div> | |
""" | |
return cleanHtml(html) | |
} | |
private static String getAbbrev(String unit){ | |
switch (unit){ | |
case sNONE: return sBLK | |
case sFAHR: return "°" | |
case sCELS: return "°" | |
case "kelvin": return "K" | |
case sMETERSPS: return "m/s" | |
case sMILESPH: return "mph" | |
case "knots": return "kn" | |
case "millimeters": return "mm" | |
case "inches": return '"' | |
case "degrees": return "°" | |
case "radians": return "rad" | |
case "cardinal": return sBLK | |
case "trend_numeric": return sBLK | |
case "trend_text": return sBLK | |
case "percent_numeric": return "%" | |
case "millibars": return "mbar" | |
case "millimeters_mercury": return "mmHg" | |
case "inches_mercury": return "inHg" | |
case "hectopascal": return "hPa" | |
case sKILOSPH : return "km/h" | |
} | |
return sBLK | |
} | |
private Map getUnits(String unit, ival){ | |
if(unit == null) return [(sNM): "unknown", (sVAR): "tbd", (sUNITS): sNONE] | |
try{ | |
switch (unit.toLowerCase()){ | |
case "f": | |
case "°f": | |
return [(sNM): "Fahrenheit (°F)", (sVAR): sTEMP, (sUNITS): sFAHR] | |
case "c": | |
case "°c": | |
return [(sNM): "Celsius (°C)", (sVAR): sTEMP, (sUNITS): sCELS] | |
case "mph": | |
return [(sNM): "Miles per Hour (mph)", (sVAR): "velocity", (sUNITS): sMILESPH] | |
case "m/s": | |
return [(sNM): "Meters per Second (m/s)", (sVAR): "velocity", (sUNITS): sMETERSPS] | |
case "in": | |
case '"': | |
return [(sNM): 'Inches (")', (sVAR): "depth", (sUNITS): "inches"] | |
case "mm": | |
// case '"': | |
return [(sNM): 'Millimeters (mm)', (sVAR): "depth", (sUNITS): "millimeters"] | |
case "°": | |
case "deg": | |
return [(sNM): "Degrees (°)", (sVAR): "direction", (sUNITS): "degrees"] | |
case "rad": | |
return [(sNM): "Radians (°)", (sVAR): "direction", (sUNITS): "radians"] | |
case "inhg": | |
return [(sNM): "Inches of Mercury (inHg)", (sVAR): "pressure", (sUNITS): "inches_mercury"] | |
case "mmhg": | |
return [(sNM): "Millimeters of Mercury mmHg)", (sVAR): "pressure", (sUNITS): "millimeters_mercury"] | |
case "mbar": | |
return [(sNM): "Millibars (mbar)", (sVAR): "pressure", (sUNITS): "millibars"] | |
case "km/h": | |
return [(sNM): "Kilometers per hour (km/h)", (sVAR): "velocity", (sUNITS): sKILOSPH] | |
case "hPa": | |
return [(sNM): "Hectopascal (hPa)", (sVAR):"pressure", (sUNITS): "hectopascal"] | |
case "%": | |
Double value=Double.parseDouble("${ival}") | |
if(value > 1.0 && value < 100.0){ | |
return [(sNM): "Percent (0 to 100)", (sVAR):"percent", (sUNITS): "percent_numeric"] | |
}else if(value >=0.0 && value < 1.0){ | |
return [(sNM): "Percent (0.1 to 1.0)", (sVAR): "percent", (sUNITS): "percent_decimal"] | |
} | |
default: | |
break | |
} | |
}catch(ex){ | |
error("Unable to find (sUNITS): $unit",null,iN2,ex) | |
} | |
return [(sNM): "unknown", (sVAR): "tbd", (sUNITS): sNONE] | |
} | |
static List<Map> getIconList(){ | |
return [ | |
[(sNM): "None", (sICON): "alpha-x-circle-outline"], | |
[(sNM): "Cloudy", (sICON): "weather-cloudy"], | |
[(sNM): "Cloudy Alert", (sICON): "weather-cloudy-alert"], | |
[(sNM): "Cloudy Right Arrow", (sICON): "weather-cloudy-arrow-right"], | |
[(sNM): "Fog", (sICON): "weather-fog"], | |
[(sNM): "Hail", (sICON): "weather-hail"], | |
[(sNM): "Hazy", (sICON): "weather-hazy"], | |
[(sNM): "Hurricane", (sICON): "weather-hurricane"], | |
[(sNM): "Lightning", (sICON): "weather-lightning"], | |
[(sNM): "Lightning Raining", (sICON): "weather-lightning-rainy"], | |
[(sNM): "Night", (sICON): "weather-night"], | |
[(sNM): "Night Partly Cloudy", (sICON): "weather-night-partly-cloudy"], | |
[(sNM): "Partly Cloudy", (sICON): "weather-partly-cloudy"], | |
[(sNM): "Partly Lightning", (sICON): "weather-partly-lightning"], | |
[(sNM): "Partly Raining", (sICON): "weather-partly-rainy"], | |
[(sNM): "Partly Snowing", (sICON): "weather-partly-snowy"], | |
[(sNM): "Partly Snowing Raining",icon: "weather-partly-snowy-rainy"], | |
[(sNM): "Pouring", (sICON): "weather-pouring"], | |
[(sNM): "Raining", (sICON): "weather-rainy"], | |
[(sNM): "Snowing", (sICON): "weather-snowy"], | |
[(sNM): "Heavy Snow", (sICON): "weather-snowy-heavy"], | |
[(sNM): "Snowing Raining", (sICON): "weather-snowy-rainy"], | |
[(sNM): "Sunny", (sICON): "weather-sunny"], | |
[(sNM): "Sunny Alert", (sICON): "weather-sunny-alert"], | |
[(sNM): "Sunny Off", (sICON): "weather-sunny-off"], | |
[(sNM): "Sunset", (sICON): "weather-sunset"], | |
[(sNM): "Sunset Down", (sICON): "weather-sunset-down"], | |
[(sNM): "Sunset Up", (sICON): "weather-sunset-up"], | |
[(sNM): "Tornado", (sICON): "weather-tornado"], | |
[(sNM): "Windy", (sICON): "weather-windy"], | |
[(sNM): "Windy 2", (sICON): "weather-windy-variant"], | |
[(sNM): "Home Thermometer", (sICON): "home-thermometer-outline"], | |
[(sNM): "Arrow Up", (sICON): "arrow-up-thick"], | |
[(sNM): "Arrow Down", (sICON): "arrow-down-thick"], | |
[(sNM): "Umbrella", (sICON): "umbrella-outline"], | |
[(sNM): "Ruler", (sICON): "ruler"], | |
[(sNM): "Cloud Question", (sICON): "cloud-question"], | |
[(sNM): "Calendar", (sICON): "calendar-today"], | |
[(sNM): "Tail Wind", (sICON): "tailwind"], | |
[(sNM): "Compass", (sICON): "compass-outline"], | |
[(sNM): "Gauge", (sICON): "gauge"], | |
[(sNM): "Thermostat", (sICON): "thermostat"], | |
[(sNM): "Water Percent", (sICON): "water-percent"], | |
[(sNM): "Wave", (sICON): "wave"], | |
[(sNM): "Snow", (sICON): "snowflake"], | |
[(sNM): "Water", (sICON): "water"],] | |
} | |
Map getOptions_weather2(){ | |
Map options=[ | |
"tile_units": state.unit_type, | |
"openweather_refresh_rate": openweather_refresh_rate ? openweather_refresh_rate : "300000", | |
"tiles" : (List)state.tile_settings, | |
"tile_type" : (Map)state.tile_type, | |
"new_tile_dialog" : state.newTileDialog, | |
"api_code" : getEndpointSecret(), | |
"url" : getEndpointURL(), | |
] | |
options.out_units=[:] | |
((Map<String,Map>)state.unit_type).each{String key, Map measurement-> | |
options.out_units << [ (key) : settings["${key}_units"]] | |
} | |
return options | |
} | |
def getMapData(Map map, String loc){ | |
List<String> splt=loc.tokenize('.') | |
def cur | |
cur=map | |
splt.each{String str-> | |
try{ | |
if(str.isNumber()){ | |
Integer num=str.toInteger() | |
cur=cur!=null ? cur[num] : null | |
}else{ | |
cur=cur!=null ? cur[str] : null | |
} | |
}catch(e){ | |
log.debug(loc+": Cannot find data: "+e) | |
return null | |
} | |
} | |
return cur | |
} | |
static String applyDecimals(Map tile, val){ | |
String value | |
value=val.toString() | |
if(value.isNumber()){ | |
def num_decimals=tile[sDECIMALS] | |
value=sprintf("%.${num_decimals}f", value.toFloat()) | |
return value | |
} | |
else return value | |
} | |
static String getWindDirection(idirection){ | |
Double direction | |
direction=Double.parseDouble(idirection.toString()) | |
List<String> bearings = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] | |
Integer bearing = Math.floor( ( (direction + 360.0D + 11.25D).toInteger() % 360 ).toDouble() / 22.5D).toInteger() | |
return bearings[bearing] | |
} | |
def applyConversion(Map tile, ival){ | |
def val; val=ival | |
Map tile_type | |
String out_units, in_units | |
out_units=sBLK | |
in_units=sBLK | |
String sUNS='UNSUPPORTED' | |
try{ | |
tile_type=((Map<String,Map>)state.tile_type)."${tile.type}" | |
out_units=((Map<String,Map>)state.unit_type)."${tile_type.type}".out | |
in_units=tile_type.in_units | |
}catch(ignored){ | |
log.debug("Unable to find units for ${tile.title}:: Input units="+in_units+" Output units="+out_units) | |
return sUNS | |
} | |
if(in_units != out_units && out_units != sNONE) | |
switch (in_units){ | |
//Temperature | |
case sCELS: | |
switch (out_units){ | |
case sFAHR: val=(val * 9 / 5) + 32; break | |
case "kelvin": val=val + 273.15; break | |
default: val=sUNS | |
} | |
break | |
case sFAHR: | |
switch (out_units){ | |
case sCELS: val=(val - 32.0) * (5 / 9); break | |
case "kelvin": val=((val - 32) * (5 / 9)) + 273.15; break | |
default: val=sUNS | |
} | |
break | |
case "kelvin": | |
switch (out_units){ | |
case sFAHR: val=((val - 273.15) * (9 / 5)) + 32; break | |
case sCELS: val=(val - 273.15); break | |
default: val=sUNS | |
} | |
break | |
//Precipitation | |
case "millimeters": | |
if(out_units == "inches"){ | |
val=(val / 25.4) | |
}else val=sUNS | |
break | |
case "inches": | |
if(out_units == "millimeters"){ | |
val=(val * 25.4) | |
}else val=sUNS | |
break | |
//Velocity | |
case sMETERSPS: | |
switch (out_units){ | |
case sMILESPH: val=(val * 2.237); break | |
case "knots": val=(val * 1.944); break | |
case sKILOSPH: val=(val * 3.6); break | |
default: val=sUNS | |
} | |
break | |
case sMILESPH: | |
switch (out_units){ | |
case sMETERSPS: val=(val / 2.237); break | |
case "knots": val=(val / 1.151); break | |
case sKILOSPH: val=(val * 1.609); break | |
default: val=sUNS | |
} | |
break | |
case "knots": | |
switch (out_units){ | |
case sMILESPH: val=(val * 1.151); break | |
case sMETERSPS: val=(val / 1.944); break | |
case sKILOSPH: val=(val * 1.852); break | |
default: val=sUNS | |
} | |
break | |
case sKILOSPH: | |
switch (out_units){ | |
case sMILESPH: val=(val / 1.609); break | |
case sMETERSPS: val=(val / 3.6); break | |
case "knots": val=(val / 1.852); break | |
default: val=sUNS | |
} | |
break | |
//Pressure | |
case "hectopascal": | |
case "millibars": | |
switch (out_units){ | |
case "inches_mercury": val=(val / 33.864); break | |
case "millimeters_mercury": val=(val / 1.333); break | |
case "hectopascal": break | |
default: val=sUNS | |
} | |
break | |
case "inches_mercury": | |
switch (out_units){ | |
case "hectopascal": | |
case "millibars": val=(val * 33.864); break | |
case "inches_mercury": val=(val / 25.4); break | |
default: val=sUNS | |
} | |
break | |
case "millimeters_mercury": | |
switch (out_units){ | |
case "hectopascal": | |
case "millibars": val=(val * 1.333); break | |
case "millimeters_mercury": val=(val * 25.4); break | |
default: val=sUNS | |
} | |
break | |
case "degrees": | |
switch (out_units){ | |
case "cardinal": | |
val=getWindDirection(val) | |
break | |
case "radians": val=(val / 180.0) * 3.1415926535; break | |
default: val=sUNS | |
} | |
break | |
case "radians": | |
switch (out_units){ | |
case "cardinal": | |
val=getWindDirection(( (val * 180) / 3.1415926535) ) | |
break | |
case "degrees": val=((val * 180) / 3.1415926535); break | |
default: val=sUNS | |
} | |
break | |
case "cardinal": | |
switch (val){ | |
case "N": val=0; break | |
case "NNE": val=22.5; break | |
case "NE": val=45; break | |
case "ENE": val=67.5; break | |
case "E": val=90; break | |
case "ESE": val=112.5; break | |
case "SE": val=135; break | |
case "SSE": val=157.5; break | |
case "S": val=180; break | |
case "SSW": val=202.5; break | |
case "SW": val=225; break | |
case "WSW": val=247.5; break | |
case "W":val=270; break | |
case "WNW": val=292.5; break | |
case "NW": val=315; break | |
case "NNW": val=337.5; break | |
default: val=sUNS | |
} | |
if(val != sUNS){ | |
switch (out_units){ | |
case "radians": val=((val / 180 ) * 3.1415926535) ; break | |
case "degrees": val=val; break | |
default: val=sUNS | |
} | |
} | |
break | |
//TEXT CONVERSIONS | |
case "time_seconds": | |
Long v=val*1000L | |
Date d=new Date(v) | |
switch (out_units){ | |
case "time_twelve": | |
SimpleDateFormat simpDate | |
simpDate=new SimpleDateFormat("h:mm") | |
val=simpDate.format(d) | |
break | |
case "time_two_four": | |
SimpleDateFormat simpDate | |
simpDate=new SimpleDateFormat("HH:mm") | |
val=simpDate.format(d) | |
break | |
default: | |
val=sUNS | |
} | |
break | |
case "time_milliseconds": | |
Date d=new Date(val as Long) | |
switch (out_units){ | |
case "time_twelve": | |
val=d.getTimeString() | |
break | |
case "time_two_four": | |
val=d.getTimeString() | |
break | |
default: | |
val=sUNS | |
} | |
break | |
case "percent_numeric": | |
if(out_units == "percent_decimal") val=val / 100.0 | |
else val=sUNS | |
break | |
case "percent_decimal": | |
if(out_units == "percent_numeric") val=val * 100.0 | |
else val=sUNS | |
break | |
} | |
return val | |
} | |
@Field final List<Map<String,String>>pairingsFLD=[ | |
[(sNM): "thunderstorm with light rain", (sICON): "weather-lightning-rainy"], | |
[(sNM): "thunderstorm with rain", (sICON): "weather-lightning-rainy"], | |
[(sNM): "thunderstorm with heavy rain", (sICON): "weather-lightning-rainy"], | |
[(sNM): "light thunderstorm", (sICON): "weather-lightning"], | |
[(sNM): "thunderstorm", (sICON): "weather-lightning"], | |
[(sNM): "heavy thunderstorm", (sICON): "weather-lightning"], | |
[(sNM): "ragged thunderstorm", (sICON): "weather-lightning"], | |
[(sNM): "thunderstorm with light drizzle", (sICON): "weather-lightning-rainy"], | |
[(sNM): "thunderstorm with drizzle", (sICON): "weather-lightning-rainy"], | |
[(sNM): "thunderstorm with heavy drizzle", (sICON): "weather-lightning-rainy"], | |
[(sNM): "light intensity drizzle", (sICON): "weather-partly-rainy"], | |
[(sNM): "drizzle", (sICON): "weather-partly-rainy"], | |
[(sNM): "heavy intensity drizzle", (sICON): "weather-partly-rainy"], | |
[(sNM): "light intensity drizzle rain", (sICON): "weather-partly-rainy"], | |
[(sNM): "drizzle rain", (sICON): "weather-partly-rainy"], | |
[(sNM): "heavy intensity drizzle rain", (sICON): "weather-rainy"], | |
[(sNM): "shower rain and drizzle", (sICON): "weather-rainy"], | |
[(sNM): "heavy shower rain and drizzle", (sICON): "weather-pouring"], | |
[(sNM): "shower drizzle", (sICON): "weather-rainy"], | |
[(sNM): "light rain", (sICON): "weather-rainy"], | |
[(sNM): "moderate rain", (sICON): "weather-pouring"], | |
[(sNM): "heavy intensity rain", (sICON): "weather-pouring"], | |
[(sNM): "very heavy rain", (sICON): "weather-pouring"], | |
[(sNM): "extreme rain", (sICON): "weather-pouring"], | |
[(sNM): "freezing rain", (sICON): "weather-snowy-rainy"], | |
[(sNM): "light intensity shower rain", (sICON): "weather-rainy"], | |
[(sNM): "shower rain", (sICON): "weather-rainy"], | |
[(sNM): "heavy intensity shower rain", (sICON): "weather-pouring"], | |
[(sNM): "ragged shower rain", (sICON): "weather-partly-rainy"], | |
[(sNM): "light snow", (sICON): "weather-snowy"], | |
[(sNM): "snow", (sICON): "weather-snowy"], | |
[(sNM): "heavy snow", (sICON): "weather-snowy-heavy"], | |
[(sNM): "sleet", (sICON): "weather-hail"], | |
[(sNM): "light shower sleet", (sICON): "weather-hail"], | |
[(sNM): "shower sleet", (sICON): "weather-hail"], | |
[(sNM): "light rain and snow", (sICON): "weather-snowy-rainy"], | |
[(sNM): "rain and snow", (sICON): "weather-snowy-rainy"], | |
[(sNM): "light shower snow", (sICON): "weather-partly-snowy"], | |
[(sNM): "shower snow", (sICON): "weather-partly-snowy"], | |
[(sNM): "heavy shower snow", (sICON): "weather-partly-snowy"], | |
[(sNM): "mist", (sICON): "weather-fog"], | |
[(sNM): "smoke", (sICON): "weather-fog"], | |
[(sNM): "haze", (sICON): "weather-hazy"], | |
[(sNM): "sand dust whirls", (sICON): "weather-tornado"], | |
[(sNM): "fog", (sICON): "weather-fog"], | |
[(sNM): "sand", (sICON): "weather-fog"], | |
[(sNM): "dust", (sICON): "weather-fog"], | |
[(sNM): "volcanic ash", (sICON): "weather-fog"], | |
[(sNM): "squalls", (sICON): "weather-tornado"], | |
[(sNM): "tornado", (sICON): "weather-tornado"], | |
[(sNM): "clear sky night", (sICON): "weather-night"], | |
[(sNM): "clear sky", (sICON): "weather-sunny"], | |
[(sNM): "few clouds night", (sICON): "weather-night-partly-cloudy"], | |
[(sNM): "few clouds", (sICON): "weather-partly-cloudy"], | |
[(sNM): "scattered clouds night", (sICON): "weather-night-partly-cloudy"], | |
[(sNM): "scattered clouds", (sICON): "weather-partly-cloudy"], | |
[(sNM): "broken clouds", (sICON): "weather-cloudy"], | |
[(sNM): "overcast clouds", (sICON): "weather-cloudy"] | |
] | |
List<String> translateCondition(Map tile, String condition){ | |
//String icon="mdi-weather-sunny-off" | |
List return_val | |
try{ | |
Date now | |
now=new Date() | |
String period=sMs(tile,'period') | |
List<String> timeframe=period.split("\\.") | |
Boolean round_hour | |
round_hour=false | |
if(timeframe[iZ] == "hourly"){ | |
round_hour=true | |
Integer num_hours=timeframe[i1].toInteger() | |
use( TimeCategory ){ | |
now=now + num_hours.hours | |
} | |
} | |
String check_condition | |
check_condition=condition | |
if(isNight(now, round_hour)){ | |
check_condition+=" night" | |
} | |
return [sICON, pairingsFLD.find{Map<String,String> el-> sMs(el,sNM) == check_condition}[sICON]] | |
}catch(ignored){} | |
try{ | |
return [sICON, pairingsFLD.find{Map<String,String> el-> sMs(el,sNM) == condition}[sICON]] | |
}catch(ignored){} | |
return_val=[sICON, "alert-circle"] | |
return return_val | |
} | |
List<String> formatNumericData(Map tile, ival){ | |
def val | |
val=ival | |
if(val == null) | |
val=0 | |
return [(sVAL), applyDecimals(tile, applyConversion(tile, val))] | |
} | |
static Float getMinHour(Date date){ | |
return (date.getHours())+(date.getMinutes()/60.0) as Float | |
} | |
Boolean isNight(Date date, Boolean round_hour){ | |
Float sunrise=getMinHour((Date)location.sunrise) | |
Float sunset=getMinHour((Date)location.sunset) | |
Float now=round_hour ? date.getHours().toFloat() : getMinHour(date) | |
//Calendar cal=Calendar.getInstance() | |
return now < sunrise || now > sunset | |
} | |
/* | |
List formatHourData(Map tile, val){ | |
Long val_micro=val*1000L | |
Date date=new Date(val_micro) | |
switch (settings["time_units"]){ | |
case "time_seconds" : return [sVAL, val] | |
case "time_milliseconds" : return [sVAL, val_micro] | |
case "time_twelve" : return [sVAL, date.format('h:mm a', mTZ())] | |
case "time_two_four" : return [sVAL, date.format('HH:mm', mTZ())] | |
} | |
return [sVAL, "XXXX"] | |
} | |
*/ | |
List<String> formatDayData(Map tile, val){ | |
Long val_micro=val*1000L | |
Date date=new Date (val_micro) | |
String day | |
if(settings["day_units"] == "short") day=date.format('E', mTZ()) | |
else day=date.format('EEEE', mTZ()) | |
return [sVAL, day] | |
} | |
List<String> formatTextData(Map tile, String val){ | |
switch (settings["text_units"]){ | |
case "plain": return [sVAL, val] | |
case "lowercase": return [sVAL, val.toLowerCase()] | |
case "uppercase": return [sVAL, val.toUpperCase()] | |
case "title": return [sVAL, val.split(sSPC).collect{ String it ->it.capitalize()}.join(sSPC)] | |
} | |
return [sVAL, val] | |
} | |
/* | |
List<String> formatConditionText(Map tile, String val){ | |
return ["value", val.split(sSPC).collect{it.capitalize()}.join(sSPC)] | |
} | |
List<String> formatTitle(Map tile, String val){ | |
return["value", sBLK] | |
} | |
List formatPressure(Map tile, val){ | |
return ["value", "Pressure Trend"] | |
} | |
List<String> formatDewPoint(Map tile, val){ | |
// TODO does not deal with C | |
def dewPoint=val | |
String text | |
text=sBLK | |
if(dewPoint < 50) text="DRY" | |
else if(dewPoint < 55) text= "NORMAL" | |
else if(dewPoint < 60) text= "OPTIMAL" | |
else if(dewPoint < 65) text= "STICKY" | |
else if(dewPoint < 70) text= "MOIST" | |
else if(dewPoint < 75) text= "WET" | |
else text "MISERABLE" | |
return ["value", text] | |
} | |
*/ | |
def getSensorData1(String measurement){ | |
Long device_id=(measurement.tokenize('.')[i1]).toLong() | |
String attribute=measurement.tokenize('.')[i2] | |
def sensor=sensors.find{ it.id == device_id } | |
return sensor.currentState(attribute,true).getValue() | |
} | |
void buildWeatherData(){ | |
if(isEric())debug "buildWeatherData",null | |
//def selections=settings["tile_settings"] | |
Map data=parent.getWData() | |
//log.debug "buildWeatherData got ${data.size()}" | |
List<Map> temp=(List<Map>)state.tile_settings | |
temp.eachWithIndex{Map tile, index-> | |
def val, rain_val, snow_val | |
val=null | |
rain_val=null | |
snow_val=null | |
String period, measurement | |
period=sBLK | |
measurement=sBLK | |
try{ | |
period=tile.period | |
measurement=state.tile_type."${tile.type}".ow | |
if(period == sSENSOR){ | |
val=getSensorData1(measurement) | |
}else if(measurement == "precipitation"){ | |
rain_val=getMapData(data, period+".rain") | |
snow_val=getMapData(data, period+".snow") | |
//Special Case | |
if(rain_val == null) rain_val=0 | |
if(snow_val == null) snow_val=0 | |
if(snow_val > rain_val){ | |
tile.icon="snowflake" | |
}else{ | |
tile.icon="water" | |
} | |
val=rain_val + snow_val | |
}else if(period != sNONE && measurement != sNONE){ | |
val=getMapData(data, period+"."+measurement) | |
//log.debug "getMapData ${period}.${measurement} val $val" | |
} | |
}catch(ignored){ | |
log.debug(sMs(tile,sNM)+": Unable to get data: "+period+", "+measurement) | |
} | |
String unit_type=state.tile_type."${tile.type}".type | |
String parse_func=((Map<String,Map>)state.unit_type)."${unit_type}".parse_func | |
if(parse_func!=sNONE){ | |
try{ | |
List returnVal="${parse_func}"(tile, val) | |
tile."${returnVal[iZ]}"=returnVal[i1] | |
//log.debug "parse_func: ${parse_func} tile $returnVal" | |
}catch(ex){ | |
log.debug(val+sSPC+unit_type+sSPC+parse_func+"::: Issue executing parse function: $parse_func " + ex) | |
} | |
}else{ | |
tile[sVAL]=sBLK | |
} | |
} | |
state.tile_settings=temp | |
} | |
String getTileHTML(Map item, Boolean locked){ | |
String var=sMs(item,sVAR) | |
BigDecimal fontScale=4.6 | |
BigDecimal lineScale=0.85 | |
BigDecimal iconScale=3.5 | |
//def header=0.1 | |
Integer height=iMs(item,'h') | |
String html; html=sBLK | |
String tile_locked=locked ? sFALSE : sTRUE | |
String background=getRGBA(sMs(item,'background_color'), (Float.parseFloat(item.background_opacity.toString()))) | |
String font=getRGBA(sMs(item,'font_color'), Float.parseFloat(item.font_opacity.toString())) | |
if(bIs(item,'display')){ | |
html += """ <div id="${var}_tile_main" class="grid-stack-item" data-gs-id="${var}" data-gs-x="${item.baseline_column}" | |
data-gs-y="${item.baseline_row}" data-gs-width="${item.w}" data-gs-height="${height}" data-gs-locked="${tile_locked}" | |
ondblclick="setOptions('${var}')"> | |
<div id="${var}_title" style="display: none;">${item.title}</div> | |
<div id="${var}_font_adjustment" style="display: none;">${item.font_adjustment}</div> | |
<div class="mdl-tooltip" for="${var}_tile_main" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">${item.title}</div> | |
<div id="${var}_tile" class="grid-stack-item-content" style="font-size: ${fontScale*height}vh; | |
line-height: ${fontScale*lineScale*height}vh; | |
text-align: ${item.justification}; | |
background-color: ${background}; | |
font-weight: ${item.font_weight};"> | |
""" | |
//Compute Icon and other spacing | |
//Left Icon | |
if(item.icon_loc != sRIGHT){ | |
item.icon_space=item.icon_space ?: sBLK | |
html+="""<span id="${var}_icon" class="mdi mdi-${item.icon}" style="font-size: ${iconScale*height}vh; color: ${font};">${item.icon_space}</span>""" | |
} | |
//Text | |
if(item.text == "null" || item.text == null) item.text=sBLK | |
html+="""<span id="${var}_text" style="color: ${font};">${item.text}</span>""" | |
//Main Content | |
html += """<span id="${var}" style="color: ${font};">${item[sVAL]}</span>""" | |
String tile_type | |
String out_units | |
String units | |
//Units | |
try{ | |
tile_type=state.tile_type."${item.type}".type | |
out_units=state.unit_type."${tile_type}".out | |
units=getAbbrev(out_units) | |
}catch(ignored){ | |
units=sBLK | |
} | |
if(units == "unknown") units=sBLK | |
//Unit Spacing | |
html += """<span id="${var}_unit_space">${sMs(item,'unit_space')}</span>""" | |
html += """<span id="${var}_units" style="font-size: ${iconScale*height}vh; color: ${font};">${units}</span>""" | |
//Right Icon | |
if(item.icon_loc == sRIGHT){ | |
html+="""<span>${item.icon_space}</span>""" | |
html+="""<span id="${var}_icon" class="mdi mdi-${item.icon}" style="color: ${font};"></span>""" | |
} | |
html += """</div></div>""" | |
} | |
return html | |
} | |
/* | |
def getDrawType(){ | |
return "google.visualization.LineChart" | |
} | |
static String removeLastChar(String str){ | |
str.subSequence(0, str.length() - 1) | |
str | |
} | |
*/ | |
// weather2 | |
String defineHTML_Header(){ | |
String html=""" | |
<!DOCTYPE html> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.4.55/css/materialdesignicons.min.css"> | |
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> | |
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css"> | |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"> | |
<link rel="stylesheet" href="http://${location.hub.localIP}/local/${isSystemType() ? 'webcore/' : ''}f06ea400-fe7a-49ef-8c50-6418f0a78dc6-WeatherTile2.css"> | |
<script> | |
const localURL = "${getEndpointURL()}"; | |
const secretEndpoint= "${getEndpointSecret()}"; | |
const latitude = "${latitude}"; | |
const longitude = "${longitude}"; | |
const tile_key = "${tile_key}"; | |
</script> | |
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script> | |
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> | |
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script> | |
<script type="text/javascript" src="https://unpkg.com/@fonticonpicker/fonticonpicker/dist/js/jquery.fonticonpicker.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/gridstack@1.1.2/dist/gridstack.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/gridstack@1.1.2/dist/gridstack.jQueryUI.js"></script> | |
<script type="text/javascript" src="https://www.google.com/jsapi"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js" integrity="sha512-0bEtK0USNd96MnO4XhH8jhv3nyRF0eK87pJke6pkYf3cM0uDIhNJy9ltuzqgypoIFXw3JSuiy04tVk4AjpZdZw==" crossorigin="anonymous"></script> | |
<script defer src="http://${location.hub.localIP}/local/${isSystemType() ? 'webcore/' : ''}ba8d5ae0-1fbd-430a-bae0-bb5c0bd17ebd-WeatherTile2.js"></script> | |
""" | |
return html | |
} | |
static String addColorPicker(Map map){ | |
String var=sMs(map,sVAR) | |
String title=sMs(map,sTIT) | |
String html=""" | |
<div class="border-container"> | |
<div id="text_box" class="flex-container"> | |
<div class="flex-item" style="flex-basis: 25%;"> | |
<span><label for="${var}_color">${title}</label></span> | |
</div> | |
<div class="flex-item" style="flex-basis: 75%;"> | |
<span><label for="${var}_color">Opacity</label></span> | |
</div> | |
</div> | |
<div id="text_color_box" class="flex-container"> | |
<div class="flex-item" style="flex-basis: 25%;"> | |
<span><input type="color" id="${var}_color" name="${var}_color" value=sWHT></span> | |
</div> | |
<div class="flex-item" style="flex-basis: 60%;"> | |
<input id="${var}_slider" class="mdl-slider mdl-js-slider" type="range" min="0" max="100" value="100" tabindex="0" | |
oninput="${var}_showMessage(this.value)" onchange="${var}_showMessage(this.value)"> | |
</div> | |
<div class="flex-item" style="flex-basis: 15%;"> | |
<div class="item" id="${var}_message">100%</div> | |
</div> | |
</div> | |
</div> | |
<!-- JAVASCRIPT --> | |
<script language="javascript"> | |
function ${var}_showMessage(value){ | |
document.getElementById("${var}_message").innerHTML=value + "%"; | |
} | |
</script> | |
""" | |
return html | |
} | |
static String addButtonMenu(Map map){ | |
String button_var=sMs(map,'var_name') | |
def default_val=map.default_value | |
String default_icon=sMs(map,'default_icon') | |
List<Map>item_list=(List<Map>)map.list | |
String tooltip=sMs(map,'tooltip') ?: sBLK | |
String side=sMs(map,'side') ?: sLEFT | |
String html | |
html=""" | |
<div id="${button_var}_value" style="display: none;">${default_val}</div> | |
<div id="${button_var}_icon" style="display: none;">${default_icon}</div> | |
<button id="${button_var}_button" | |
class="mdl-button mdl-js-button mdl-button--icon mdi mdi-${default_icon}"> | |
</button> | |
<div class="mdl-tooltip" for="${button_var}_button">${tooltip}</div> | |
<ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect mdl-menu--bottom-${side}" for="${button_var}_button"> | |
""" | |
item_list.each{Map item-> | |
Integer weight=item.font_weight ? iMs(item,'font_weight') : 400 | |
String nm=sMs(item,sNM).toLowerCase() | |
html += """ | |
<li class="mdl-menu__item" onclick="${button_var}_itemSelected('${item.icon}', '${nm}')"> | |
<div id="${nm}_icon" style="display: none;">${item.icon}</div> | |
<span id="${nm}" class=" mdi mdi-${item.icon}" style="vertical-align: middle; font-weight: ${weight};"></span> | |
<span> ${item.text ? item.text : sMs(item,sNM)}</span> | |
</li> | |
""" | |
} | |
html += """ | |
</ul> | |
""" | |
html += """ | |
<script> | |
function ${button_var}_itemSelected(icon, val){ | |
replaceIcons("${button_var}_button", icon); | |
document.getElementById("${button_var}_value").textContent=val; | |
document.getElementById("${button_var}_icon").textContent=icon; | |
} | |
</script> | |
""" | |
return html | |
} | |
/* | |
static String addMenu(Map map){ | |
String button_var=map.var_name | |
def default_val=map.default_value | |
String default_icon=map.default_icon | |
List<Map> item_list=map.list | |
String tooltip=map.tooltip ? map.tooltip : sBLK | |
String title=map[sTIT] | |
String html=""" | |
<div> | |
<div id="${button_var}_value" style="display: none;">${default_val}</div> | |
<div id="${button_var}_icon" style="display: none;">mdi-${default_icon}</div> | |
<span> | |
<button id="${button_var}_button" class="mdl-button mdl-js-button mdl-js-ripple-effect" tabindex="-1"> | |
<i id="${button_var}_icon_display" class="mdi mdi-${default_icon}"> | |
<label id="${button_var}_text_display"> ${title}</label> | |
</i> | |
</button> | |
<div class="mdl-tooltip" for="${button_var}_button">${tooltip}</div> | |
</span> | |
<ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect" for="${button_var}_button" style="overflow-y: scroll; max-height: 50vh; line-height: 10px;"> """ | |
item_list.each{item-> | |
Integer weight=item.font_weight ? item.font_weight : 400 | |
html += """ | |
<li id="${item.var}_list_main" class="mdl-menu__item" onclick="${button_var}_itemSelected('${item.icon}', '${item.var}')"> | |
<div id="${item.var}_list_item" style="display: none;">${item.icon}</div> | |
<span id="${item.var}_list_title" class=" mdi mdi-${item.icon}" style="vertical-align: middle; font-weight: ${weight};"></span> | |
<span id="${item.var}_list_name">${item.text ? item.text : item.name}</span> | |
</li>""" | |
} | |
html += """ | |
</ul></div> | |
""" | |
html += """ | |
<script> | |
function ${button_var}_itemSelected(icon, val){ | |
let currentIcon=document.getElementById("${button_var}_icon").textContent; | |
let iconDisplay=jQuery("#${button_var}_icon_display"); | |
console.log(iconDisplay.hasClass("mdi")); | |
iconDisplay.removeClass(currentIcon); | |
iconDisplay.addClass(icon); | |
document.getElementById("${button_var}_text_display").textContent=document.getElementById(val+"_list_name").textContent; | |
document.getElementById("${button_var}_value").textContent=val; | |
document.getElementById("${button_var}_icon").textContent=icon; | |
} | |
</script> | |
""" | |
return html | |
} | |
*/ | |
static String addIconMenu(Map map){ | |
String button_var=sMs(map,'var_name') | |
def default_val=map.default_value | |
String default_icon=sMs(map,'default_icon') | |
List<Map> item_list=(List<Map>)map.list | |
String tooltip | |
tooltip=sMs(map,'tooltip') ?: sBLK | |
Boolean description=map.description ? map.description : false | |
Integer width= iMs(map,'width') | |
if(sMs(map,'tooltip') == "Use Icon Name") tooltip="No Icon Selected" | |
String html | |
html=""" | |
<div> | |
<div id="${button_var}_menu" class="flex-item" style="flex-grow:1;" tabindex="-1; "> | |
<div id="${button_var}_value" style="display: none;">${default_val}</div> | |
<div id="${button_var}_icon" style="display: none;">${default_icon}</div> | |
<div> | |
<button id="${button_var}_button" | |
class="mdl-button mdl-js-button mdl-button--icon mdi mdi-${default_icon}"> | |
</button> | |
""" | |
if(description) | |
html += """ <span> <b>Icon</b> </span><span id= "${button_var}_text">(None)</span> | |
""" | |
html += """ </div> | |
<div id="${button_var}_tooltip" class="mdl-tooltip" for="${button_var}_menu">${tooltip}</div> | |
<ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect" for="${button_var}_menu" style="max-height: 40vh; overflow-y: scroll !important;"> | |
""" | |
Integer count | |
count=iZ | |
item_list.each{Map item-> | |
if(count % width == iZ){ | |
html+="""<div class="flex-container"> | |
""" | |
} | |
Integer weight=item.font_weight ? iMs(item,'font_weight') : 400 | |
String icon_var=sMs(item,sICON).replaceAll("-","_") | |
html += """ <div class="flex-item" style="flex-grow:1;"> | |
<li class="mdl-menu__item" onclick="${button_var}_itemSelected('${item.icon}', '${sMs(item,sNM).toLowerCase()}', '${sMs(item,sNM)}')"> | |
<div id="${button_var}_${icon_var}_icon" style="display: none;">${item.icon}</div> | |
<span id="${button_var}_${icon_var}" class=" mdi mdi-${item.icon}" style="vertical-align: middle; font-size: 5vw;"></span> | |
<div id="${button_var}_${icon_var}_text" class="mdl-tooltip" for="${button_var}_${icon_var}">${sMs(item,sNM)}</div> | |
</li> | |
</div> | |
""" | |
if(count % width == width-i1){ | |
html+= """</div> | |
""" | |
} | |
count++ | |
} | |
html += """</ul> | |
</div> | |
</div> | |
""" | |
html += """ | |
<script> | |
function ${button_var}_itemSelected(icon, val, name){ | |
replaceIcons("${button_var}_button", icon); | |
document.getElementById("${button_var}_value").textContent=val; | |
document.getElementById("${button_var}_icon").textContent=icon; | |
""" | |
if(description) | |
html += """ document.getElementById("${button_var}_text").textContent="("+name+")";""" | |
if(map.tooltip == "Use Icon Name") | |
html += """ document.getElementById("${button_var}_tooltip").textContent="Selected Icon: "+name;""" | |
html += """ | |
} | |
</script> | |
""" | |
return html | |
} | |
static String addSlider(Map map){ | |
String var=sMs(map,sVAR) | |
String title=sMs(map,sTIT) | |
Integer min=iMs(map,sMIN) | |
Integer max=iMs(map,sMAX) | |
Integer value=iMs(map,sVAL) | |
String html=""" | |
<div id="${var}_box" class="flex-container"> | |
<div class="flex-item" style="flex-basis: 35%;"> | |
<label for="${var}_slider">${title}</label> | |
</div> | |
<div class="flex-item" style="flex-grow: auto;"> | |
<input id="${var}_slider" class="mdl-slider mdl-js-slider" type="range" min="${min}" max="${max}" value="${value}" | |
tabindex="0" oninput="${var}_showMessage(this.value)" onchange="${var}_showMessage(this.value)"> | |
</div> | |
<div class="flex-item" style="flex-basis: 15%;"> | |
<div id="${var}_message">0%</div> | |
</div> | |
</div> | |
<script language="javascript"> | |
function ${var}_showMessage(value){ | |
document.getElementById("${var}_message").innerHTML=value + "%"; | |
} | |
</script> | |
""" | |
return html | |
} | |
static String defineHTML_CSS(){ | |
String html=""" | |
<style> | |
.grid-stack{ | |
background: #000000; | |
} | |
.grid-stack-item-content{ | |
color: #2c3e50; | |
text-align: center; | |
background-color: #18bc9c; | |
left: 1px !important; | |
right: 1px !important; | |
} | |
.grid-stack-item-content{overflow:hidden !important} | |
/* Optional styles for demos */ | |
.btn-primary{ | |
color: #fff; | |
background-color: #007bff; | |
} | |
.btn{ | |
display: inline-block; | |
padding: .375rem .75rem; | |
line-height: 1.5; | |
border-radius: .25rem; | |
} | |
.font-test{ | |
line-height: 10vw; | |
padding-top: 0px !important; | |
font-size: 10vw; | |
margin: 0 !important; | |
text-align: center; | |
} | |
a{ | |
text-decoration: none; | |
} | |
h1{ | |
font-size: 2.5rem; | |
margin-bottom: .5rem; | |
} | |
.placeholder-content{ | |
left: 0; | |
right: 0; | |
} | |
.flex-container{ | |
display: flex; | |
flex-wrap: nowrap; | |
width: 100%; | |
background-color: rgba(0,0,0,0); | |
} | |
.flex-container > div{ | |
background-color: rgba(0,0,0,0); | |
width: auto; | |
margin: 2px; | |
text-align: center; | |
line-height: 3vh; | |
font-size: 3vh; | |
} | |
.border-container{ | |
border-style: solid none none none; | |
padding-bottom: 1vh; | |
padding-top: 1vh; | |
width: 100%; | |
} | |
.mdl-textfield__label{ | |
margin-bottom:0px !important; | |
margin-top:0px !important; | |
} | |
</style> | |
""" | |
return html | |
} | |
static String defineSelectBox(Map map){ | |
String title=map[sTIT] | |
String var=sMs(map,sVAR) | |
Map<String,Map> list=(Map<String,Map>)map.list | |
String visible=map.visible == false ? """style="display: none;" """ : sBLK | |
String function=map.function | |
String html | |
html=""" | |
<div id=${var}_main class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" ${visible}> | |
<select class="mdl-textfield__input" id="${var}" name="${var}" style="line-height: 5vh !important" onchange="${function}(this.value)"> | |
<option value="blank"></option> | |
""" | |
list.each{key, item-> | |
html+="""<option value="${key}">${sMs(item,sNM)}</option>""" | |
} | |
html+= | |
""" | |
</select> | |
<label class="mdl-textfield__label" for="${var}">${title}</label> | |
</div> | |
""" | |
return html | |
} | |
String defineNewTileDialog(){ | |
TreeMap<String,TreeMap> typeList=(TreeMap<String,TreeMap>)state.newTileDialog | |
String html | |
html=sBLK | |
html += """ | |
<dialog id="addTileDialog" class="mdl-dialog mdl-shadow--12dp" tabindex="-1" style="background-color: rgba(255, 255, 255, 0.90); border-radius: 2vh; height: 95vh; visibility: none;"> | |
<div class="mdl-dialog__content"> | |
<div class="mdl-layout"> | |
<div id="options_title" class="mdl-layout__title" style="color: black; text-align: center;"> | |
New Tile Options | |
</div> | |
<div class="mdl-grid" style="width: 100%"> | |
<div class="border-container"> | |
<div id="menu_items" class="flex-container"> | |
<div class="flex-item" style="max-width:18%; flex-basis: 18%" tabindex="-1"> | |
<button id="save_button" type="button" class="mdl-button mdi mdi-content-save" onclick="addNewTileClose()" style="color: darkgreen; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="save_button" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">Save/Close</div> | |
</div> | |
<div class="flex-item" style="max-width:18%; flex-basis: 18% padding-bottom: 0 !important;" tabindex="-1"> | |
<button id="close_button" type="button" class="mdl-button mdi mdi-close-circle" onclick="closeAddTileWindow()" style="color: darkred; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="close_button" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">Exit/Don't Save</div> | |
</div> | |
</div> | |
</div> | |
<div class="border-container"> | |
<div id="menu_items" class="flex-container"> | |
<div class="flex-item" style="max-width:50%; flex-basis: 50%" tabindex="-1"> | |
""" | |
TreeMap list | |
list=new TreeMap(typeList.main_list) | |
html+= defineSelectBox((sTIT): "Title Span", (sVAR): "new_tile_span", list: list, function: "selectTileSpan") | |
html += """ | |
</div> | |
</div> | |
""" | |
html += """ | |
<div id="menu_items" class="flex-container"> | |
<div class="flex-item" style="max-width:75%; flex-basis: 75%" tabindex="-1"> | |
""" | |
spanFLD.each{String span_key, Map span-> | |
list=new TreeMap( (TreeMap)((TreeMap)typeList[span_key]).measurement_list) | |
//if(list!=[:]) | |
if(list) | |
html+= defineSelectBox((sTIT): span[sTIT], (sVAR): span_key+"_measurement", list: list, visible: false, function: "selectTileType") | |
} | |
html += """ | |
</div> | |
</div> | |
""" | |
html += """ | |
<div id="menu_items" class="flex-container"> | |
<div class="flex-item" style="max-width:75%; flex-basis: 90%" tabindex="-1"> | |
""" | |
spanFLD.each{String span_key, Map span-> | |
TreeMap tl= (TreeMap)typeList[span_key] | |
if(tl[sTIT]){ | |
list=new TreeMap((TreeMap)tl.time_list) | |
html+= defineSelectBox((sTIT): tl[sTIT], (sVAR): span_key+"_time", list: list, visible: false, function: "selectTileTime") | |
} | |
} | |
/* | |
html+= defineSelectBox((sTIT): "Days to Display", (sVAR): "daily_time", list: daily_list, visible: false, function: "selectTileTime"); | |
html+= defineSelectBox((sTIT): "Hours to Display", (sVAR): "hourly_time", list: hourly_list, visible: false, function: "selectTileTime"); | |
*/ | |
html += """ | |
</div> | |
</div> | |
""" | |
html+= """</div> | |
""" | |
html += """</div></div></dialog> | |
""" | |
return html | |
} | |
static String defineTileDialog(){ | |
/* | |
List<Map> list=[] | |
for(Map item in (List<Map>)state.tile_settings){ | |
list << [(sNM): item[sTIT], (sICON): item[sICON], (sVAR): item.var] | |
} */ | |
String html | |
html=""" | |
<dialog id="tileOptions" class="mdl-dialog mdl-shadow--12dp" tabindex="-1" style="background-color: rgba(255, 255, 255, 0.90); border-radius: 2vh; height: 95vh; visibility: none;"> | |
<div class="mdl-dialog__content"> | |
<div class="mdl-layout"> | |
<div id="options_title" class="mdl-layout__title" style="color: black; text-align: center;"> | |
Options | |
</div> | |
<div class="mdl-grid" style="width: 100%"> | |
<div class="border-container"> | |
<div id="text_box" class="flex-container"> | |
<div class="flex-item" style="max-width:15%; flex-basis: 15%;" tabindex="-1"> | |
<button id="trash_button" type="button" class="mdl-button mdi mdi-trash-can-outline" onclick="deleteTile()" style="color: darkred; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="trash_button" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">Delete this tile</div> | |
</div> | |
<div class="flex-item" style="max-width:15%; flex-basis: 15%" tabindex="-1"> | |
<button id="new_tile" type="button" class="mdl-button mdi mdi-shape-rectangle-plus"" onclick="newTile()" style="color: darkgreen; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="new_tile" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">Place New Tile</div> | |
</div> | |
<div class="flex-item" style="max-width:15%; flex-basis: 15%" tabindex="-1"> | |
<button id="save_button" type="button" class="mdl-button mdi mdi-content-save" onclick="saveWindow()" style="color: darkgreen; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="save_button" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">Save/Close</div> | |
</div> | |
<div class="flex-item" style="max-width:15%; flex-basis: 15%" tabindex="-1"> | |
<button id="save_all_button" type="button" class="mdl-button mdi mdi-content-save-all" onclick="saveAllWindow()" style="color: darkgreen; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="save_all_button" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">Save Colors and Opacity to All Tiles</div> | |
</div> | |
<div class="flex-item" style="max-width:15%; flex-basis: 15% padding-bottom: 0 !important;" tabindex="-1"> | |
<button id="close_button" type="button" class="mdl-button mdi mdi-close-circle" onclick="closeWindow()" style="color: darkred; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="close_button" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)">Exit/Don't Save</div> | |
</div> | |
<div class="flex-item" style="max-width:15%; flex-basis: 15% padding-bottom: 0 !important;" tabindex="-1"> | |
<button id="update_button" type="button" class="mdl-button mdi mdi-cloud-refresh" onclick="getWeatherData()" style="color: darkgreen; font-size: 4vh !important;"></button> | |
<div class="mdl-tooltip" for="update_button" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)"><b>Refresh View</b><br>This may take some time, depending on the number of tiles</div> | |
</div> | |
</div> | |
</div> | |
""" | |
//ALIGNMENT | |
html+= """ | |
<!-- ALIGNMENT --> | |
<div class="border-container"> | |
<div id="text_box" class="flex-container"> | |
<div class="flex-item" style="flex-grow:1;" tabindex="-1"> | |
""" | |
html += addIconMenu(var_name: "selected_icon",(sTIT): "Select Tile Type", default_icon: "alpha-x-circle-outline", | |
default_value: sCENTER, tooltip: "Use Icon Name", list: getIconList(), width: 4) | |
html += """ | |
</div> | |
<div class="flex-item" style="flex-grow:1;" tabindex="-1"> | |
""" | |
html+= addButtonMenu(var_name: "horizontal_alignment", default_icon: "format-align-center", tooltip: "Horizontal Alignment", default_value: sCENTER, side: sLEFT, | |
list:[[(sNM): "Left", (sICON): "format-align-left"], | |
[(sNM): "Center", (sICON): "format-align-center"], | |
[(sNM): "Right", (sICON): "format-align-right"]]) | |
html+= """ | |
</div> | |
<div class="flex-item" style="flex-grow:1;" tabindex="-1"> | |
""" | |
html+= addButtonMenu(var_name: "icon_spacing", default_icon: "keyboard-space", tooltip: "Icon Spacing", default_value: "Single Space", side: sLEFT, | |
list:[[(sNM): "No Space", (sICON): "arrow-collapse-horizontal"], | |
[(sNM): "Single Space", (sICON): "keyboard-space"], | |
[(sNM): "Double Space", (sICON): "arrow-expand-horizontal"]]) | |
html+= """ | |
</div> | |
<div class="flex-item" style="flex-grow:1;" tabindex="-1"> | |
""" | |
html+= addButtonMenu(var_name: "decimal_places", default_icon: "decimal", tooltip: "Decimal Places", default_value: "One Decimal", side: sLEFT, | |
list:[[(sNM): "No Decimal", (sICON): "hexadecimal"], | |
[(sNM): "One Decimal", (sICON): "surround-sound-2-0"], | |
[(sNM): "Two Decimals", (sICON): "decimal"]]) | |
html += """ | |
</div> | |
<div class="flex-item" style="flex-grow:1;" tabindex="-1"> | |
""" | |
html+= addButtonMenu(var_name: 'font_weight', default_icon: "numeric-4-circle", default_value: sCENTER, tooltip: "Font Weight", side: sRIGHT, | |
list:[[(sNM): "Thin", (sICON): "numeric-1-circle"], | |
[(sNM): "Normal", (sICON): "numeric-4-circle"], | |
[(sNM): "Bold", (sICON): "numeric-7-circle"], | |
[(sNM): "Thick", (sICON): "numeric-9-circle"]]) | |
html += """ | |
</div> | |
<div class="flex-item" style="flex-grow:1;" tabindex="-1"> | |
""" | |
html+= addButtonMenu(var_name: "units_spacing", default_icon: "keyboard-space", tooltip: "Units Spacing", default_value: "Single Space", side: sRIGHT, | |
list:[[(sNM): "No Space", (sICON): "arrow-collapse-horizontal"], | |
[(sNM): "Single Space", (sICON): "keyboard-space"], | |
[(sNM): "Double Space", (sICON): "arrow-expand-horizontal"]]) | |
html += """ | |
</div> | |
</div> | |
</div> | |
""" | |
//TEXT COLOR | |
html+= addColorPicker((sVAR): "text",(sTIT): "Text") | |
//BACKGROUND COLOR | |
html+= addColorPicker((sVAR): sBACKGRND,(sTIT): "Background") | |
//Font Adjustment | |
html += """ | |
<div class="border-container"> | |
<div id="text_box" class="flex-container"> | |
""" | |
html+= addSlider((sVAR): "font_adjustment",(sTIT): "Relative Size", (sMIN): -100, (sVAL): iZ, (sMAX):100) | |
html+=""" | |
</div> | |
</div> | |
<div class="border-container"> | |
<!-- CUSTOM TEXT --> | |
<div class="flex-item" style="flex-grow:auto;" tabindex="-1"> | |
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> | |
<input class="mdl-textfield__input" type="text" id="tileText"> | |
<label class="mdl-textfield__label" for="tileText">Static Text</label> | |
</div> | |
</div> | |
</div> | |
</div></div></div> | |
</dialog> | |
""" | |
return html | |
} | |
/* | |
String getTileListItem(Map map){ | |
String function=map.function | |
String var= sMs(map,sNM) | |
Map menu=map.list | |
List selections=map.selections.clone() | |
selections << var | |
return "" | |
*/ | |
/* String onclick | |
if(!menu.list) onclick="""onclick="${map.function}('${selections}')" """ | |
String html="""<span id=${var}_menu ${onclick}>""" | |
if(menu.icon){ | |
html += """<button id="${var}_button" class="mdl-button mdl-js-button mdl-js-ripple-effect" tabindex="-1"> | |
<i id="${var}_icon_display" class="mdi mdi-${default_icon}" style="color: darkgreen; font-size: 6vh !important;"></i> | |
</button> | |
""" | |
} | |
if(menu.text){ | |
html += """<span id=${var}_text>${text}</span> """ | |
} | |
if(menu.tooltip){ | |
html += """<div class="mdl-tooltip" for="${button_var}_button">${tooltip}</div>""" | |
} | |
html += """</span>""" | |
if(menu.list){ | |
html += """<ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect" for="${var}_menu" style="overflow-y: scroll; max-height: 50vh; line-height: 10px;"> """ | |
menu.list.each{item-> | |
html+= getTileListItem(name: var+"_"+item.name, list: item.list, selections: selections, function: function) | |
} | |
html += """</ul>""" | |
} else{ | |
/ var=item.name.replaceAll(" ","") | |
var=parent_+var | |
List select=selections.clone() | |
select << [item.name] | |
func="""onclick="${function_name}('${select}')" """; | |
if(item.list){ | |
html += getTileListItem(name: item.name, parent: var, function: function, list: item.list, selections: select); | |
} | |
else{ | |
html += """ <li id="${var}_list_main" class="mdl-menu__item" ${func}> | |
<span id="${var}_list_name">${item.name}</span> | |
</li>""" | |
} | |
} | |
html += """</ul>""" | |
return html | |
*/ | |
//} | |
String defineHTML_Tile(Boolean locked){ | |
/* | |
String temp_units='°' | |
String rain_units='"' | |
String m_time_units=' am' | |
String e_time_units=' pm' | |
String wind_units=' mph' | |
String pressure_units='inHg' | |
if(tile_units == "metric"){ | |
rain_units='mm' | |
m_time_units='' | |
e_time_units='' | |
wind_units=' m/sec' | |
pressure_units='mmHg' | |
} | |
*/ | |
String background; background='black' | |
String bc= 'background_color' | |
if(gtSetStr(bc) != null){ | |
Float transparent=gtSetB(bc+'_transparent') ? 0.0 : background_opacity | |
background=getRGBA(gtSetStr(bc), transparent) | |
} | |
String html_ | |
html_=""" | |
<style type="text/css"> | |
.grid-stack-item-removing{ | |
opacity: 0.8; | |
filter: blur(5px); | |
} | |
""" | |
html_ += """ | |
#trash{ | |
background: rgba(0, 0, 0, 0); | |
} | |
</style> | |
""" | |
html_ += """ | |
<body style="background-color:${background}; overflow: visible;"> | |
<div class="flex-container" style="display: none;"> | |
<div id="trash" class="flex-item" style="flex-grow:1;"> | |
<span id="trash" class="text-center mdi mdi-trash-can-outline" style="color: rgba(255, 50, 50, 0.75); background-color: rgba(0,0,0,100); font-size: 10vh; line-height: 15vh"></span> | |
</div> | |
<div class="mdl-tooltip" for="trash" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)"> | |
<div>Drag a TILE to</div> | |
<div class="mdi mdi-trash-can-outline" style="font-size: 5vh"></div> | |
<div>to REMOVE it</div> | |
</div> | |
<div style="flex-grow: 6;"></div> | |
<div class="mdl-tooltip" for="add_tile" style="background-color: rgba(255,255,255,0.75); color: rgba(0,0,0,100);)"> | |
<div>CLICK to ADD a TILE</div> | |
</div> | |
</div> | |
<div class="grid-stack grid-stack-26" data-gs-animate="yes" data-gs-verticalMargin="1" data-gs-column="26" id="main_grid"> | |
""" | |
//Main Tile Building Code | |
((List<Map>)state.tile_settings).eachWithIndex{Map item, index-> | |
html_ += getTileHTML(item, locked) | |
} | |
html_ += """ | |
</div> | |
</div> | |
</div> | |
""" | |
html_ += """ | |
<style> | |
.mdl-layout__title{ | |
padding-bottom: 20px; | |
background: transparent; | |
} | |
.mdl-grid__hubitat{ | |
padding: 0px !important; | |
margin: 5px !important; | |
} | |
.mdl-dialog__content{ | |
padding: 0px !important; | |
margin: 5px !important; | |
} | |
.mdl-dialog{ | |
width: 75vw !important; | |
} | |
.is-checked{} | |
</style> | |
""" | |
return html_ | |
} | |
/* | |
String defineHTML_globalVariables(){ | |
String html=""" | |
var sunrise; | |
var sunset; | |
let options=[]; | |
let pws_data=[]; | |
let currentTemperature; | |
""" | |
} */ | |
/* | |
//tile_settings_HTML | |
String defineUpdateDataHTML(String var){ | |
//TODO | |
if(!settings["$var"]){ | |
//if(!settings["$var"]){ wremoveSetting(var.toString()) } | |
app.updateSetting("${var}", [(sVAL): sBLK, (sTYPE): "string"]) | |
} | |
String html=""" | |
<input type="text" id="settings${var}" name="settings[${var}]" value="${settings[var]}" style="display: none;" > | |
<div class="form-group"> | |
<input type="hidden" name="${var}.type" value="text" submitOnChange> | |
<input type="hidden" name="${var}.multiple" value="false"> | |
</div> | |
""" | |
return html | |
} | |
*/ | |
static String defineScript(){ | |
String html=""" | |
<script type="text/javascript"> | |
</script> | |
""" | |
return html | |
} | |
String getWeatherTile_weather2(Boolean config){ | |
String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
buildWeatherData() | |
String html | |
html=defineHTML_Header() | |
html += """<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"><style>""" | |
//CSS | |
html += defineHTML_CSS() | |
html += """</head> | |
<body onload="initializeWeather()"> | |
""" | |
html += defineHTML_Tile(config) | |
if(config) html += defineTileDialog() | |
if(config) html += defineNewTileDialog() | |
html += defineScript() | |
html+="</body></html>" | |
return html | |
} | |
//oauth endpoints | |
def getTile_weather2(){ | |
return wrender(contentType: "text/html", data: getWeatherTile_weather2(false)) | |
} | |
String getGraph_weather2(){ | |
return getWeatherTile_weather2(true) | |
//return wrender(contentType: "text/html", data: getWeatherTile_weather2(true)) | |
} | |
String getData_weather2(){ | |
buildWeatherData() | |
return JsonOutput.toJson((List)state.tile_settings) | |
} | |
def updateSettings_weather2(){ | |
state.tile_settings=request.JSON | |
//atomicState.temp_tile_settings=request.JSON | |
return wrender(contentType: "application/json", data: """{"status":"success"}""") | |
} | |
/* | |
* TODO: Forecast methods | |
*/ | |
@Field static List<Map> unitPrecip | |
@Field static List<Map> unitDate | |
@Field static List<Map> selectionsF | |
@Field static Integer rowsF | |
@Field static Integer columnsF | |
void initFields(){ | |
if(!unitPrecip){ | |
unitPrecip=[["millimeters": "Millimeters (mm)"], ["inches": """Inches (") """]] | |
unitDate=[["day_only": "Day Only (Thursday)"], ["date_only": "Date Only (29)"], ["day_date": "Day and Date (Thursday 29)"], ["month_day": "Month and Day (June 29)"]] | |
selectionsF=[ | |
[(sTIT): 'Weather Forecast Icon', (sVAR): "weather_icon", ow: "weather.0.description", iu: sNONE, (sICON): sNONE, icon_loc: sNONE, icon_space: sBLK, h: i4, w: i4, (sBLROW): i2, (sBLCOL): i1, (sALIGNMENT): sCENTER, lpad: iZ, rpad: iZ, (sUNIT): sNONE, decimal: sNO, font: i20, font_weight: s400, (sIMPERIAL): sNONE, (sMETRIC): sNONE], | |
[(sTIT): 'Forecast Description', (sVAR): "description", ow: "weather.0.description", iu: sNONE, (sICON): sNONE, icon_loc: sNONE, icon_space: sBLK, h: i2, w: i4, (sBLROW): i6, (sBLCOL): i1, (sALIGNMENT): sCENTER, lpad: iZ, rpad: iZ, (sUNIT): sNONE, decimal: sNO, font: i10, font_weight: s400, (sIMPERIAL): sNONE, (sMETRIC): sNONE], | |
[(sTIT): 'Forecast Temperature', (sVAR): "temperature", ow: "temp.day", iu: sFAHR, (sICON): sNONE, icon_loc: sNONE, icon_space: sBLK, h: i4, w: i2, (sBLROW): i8, (sBLCOL): i1, (sALIGNMENT): sRIGHT, lpad: iZ, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, font: i20, font_weight: "900", (sIMPERIAL): sFAHR, (sMETRIC): sCELS], | |
[(sTIT): 'Forecast High', (sVAR): "high", ow: "temp.max", iu: sFAHR, (sICON): "mdi-arrow-up-thick", icon_loc: sRIGHT, icon_space: sBLK, h: i2, w: i2, (sBLROW): i8, (sBLCOL): i3, (sALIGNMENT): sLEFT, lpad: i3, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, font: i7, font_weight: "700", (sIMPERIAL): sFAHR, (sMETRIC): sCELS], | |
[(sTIT): 'Forecast Low', (sVAR): "low", ow: "temp.min", iu: sFAHR, (sICON): "mdi-arrow-down-thick", icon_loc: sRIGHT, icon_space: sBLK, h: i2, w: i2, (sBLROW): i10, (sBLCOL): i3, (sALIGNMENT): sLEFT, lpad: i3, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, font: i7, font_weight: "700", (sIMPERIAL): sFAHR, (sMETRIC): sCELS], | |
[(sTIT): 'Precipitation Forecast', (sVAR): "precipitation", ow: "rain", iu: "millimeters", (sICON): "mdi-umbrella-outline", icon_loc: sLEFT, icon_space: sSPC, h: i1, w: i2, (sBLROW): i12, (sBLCOL): i1, (sALIGNMENT): sRIGHT, lpad: iZ, rpad: i3, (sUNIT): unitPrecip, decimal: sYES, font: i4, font_weight: s400, (sIMPERIAL): "inches", (sMETRIC): "millimeters"], | |
[(sTIT): 'Precipitation Forecast Percent', (sVAR): "precipitation_percent", ow: "pop", iu: "percent_decimal", (sICON): sNONE, icon_loc: sNONE, icon_space: sSPC, h: i1, w: i2, (sBLROW): i12, (sBLCOL): i3, (sALIGNMENT): sLEFT, lpad: i3, rpad: iZ, (sUNIT): unitPercent, decimal: sYES, font: i4, font_weight: s400, (sIMPERIAL): "percent_numeric", (sMETRIC): "percent_numeric"], | |
[(sTIT): 'Sunrise', (sVAR): "sunrise", ow: "sunrise", iu: "time_seconds", (sICON): "mdi-weather-sunset-up", icon_loc: sLEFT, icon_space: sSPC, h: i1, w: i2, (sBLROW): i13, (sBLCOL): i1, (sALIGNMENT): sRIGHT, lpad: iZ, rpad: i3, (sUNIT): unitTime, decimal: sNO, font: i4, font_weight: s400, (sIMPERIAL): "time_twelve", (sMETRIC): "time_two_four"], | |
[(sTIT): 'Sunrise Temp', (sVAR): "sunrise_temp", ow: "temp.morn", iu: sFAHR, (sICON): sNONE, icon_loc: sNONE, icon_space: sSPC, h: i1, w: i1, (sBLROW): i13, (sBLCOL): i3, (sALIGNMENT): sLEFT, lpad: i3, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, font: i4, font_weight: s400, (sIMPERIAL): sFAHR, (sMETRIC): sCELS], | |
[(sTIT): 'Sunset', (sVAR): "sunset", ow: "sunset", iu: "time_seconds", (sICON): "mdi-weather-sunset-down", icon_loc: sLEFT, icon_space: sSPC, h: i1, w: i2, (sBLROW): 14, (sBLCOL): i1, (sALIGNMENT): sRIGHT, lpad: iZ, rpad: i3, (sUNIT): unitTime, decimal: sNO, font: i4, font_weight: s400, (sIMPERIAL): "time_twelve", (sMETRIC): "time_two_four"], | |
[(sTIT): 'Sunset Temp', (sVAR): "sunset_temp", ow: "temp.eve", iu: sFAHR, (sICON): sNONE, icon_loc: sNONE, icon_space: sSPC, h: i1, w: i1, (sBLROW): 14, (sBLCOL): i3, (sALIGNMENT): sLEFT, lpad: i3, rpad: iZ, (sUNIT): unitTemp, decimal: sYES, font: i4, font_weight: s400, (sIMPERIAL): sFAHR, (sMETRIC): sCELS], | |
[(sTIT): 'Dewpoint', (sVAR): "dewpoint", ow: "dew_point", iu: sFAHR, (sICON): "mdi-waves", icon_loc: sLEFT, icon_space: sSPC, h: i1, w: i2, (sBLROW): 15, (sBLCOL): i1, (sALIGNMENT): sRIGHT, lpad: iZ, rpad: i3, (sUNIT): unitTemp, decimal: sYES, font: i4, font_weight: s400, (sIMPERIAL): sFAHR, (sMETRIC): sCELS], | |
[(sTIT): 'Dewpoint Description', (sVAR): "dewpoint_desc", ow: "dew_point", iu: sNONE, (sICON): sNONE, icon_loc: sNONE, icon_space: sSPC, h: i1, w: i2, (sBLROW): 15, (sBLCOL): i3, (sALIGNMENT): sLEFT, lpad: i3, rpad: iZ, (sUNIT): unitTemp, decimal: sNO, font: i4, font_weight: s400, (sIMPERIAL): sNONE, (sMETRIC): sNONE], | |
[(sTIT): 'Forecast Wind', (sVAR): "wind", ow: "wind_speed", iu: sMILESPH, (sICON): "mdi-tailwind", icon_loc: sLEFT, icon_space: sSPC, h: i1, w: i2, (sBLROW): i16, (sBLCOL): i1, (sALIGNMENT): sRIGHT, lpad: iZ, rpad: i3, (sUNIT): unitWind, decimal: sYES, font: i4, font_weight: s400, (sIMPERIAL): sMILESPH, (sMETRIC): sMETERSPS], | |
[(sTIT): 'Forecast Clouds', (sVAR): "clouds", ow: "clouds", iu: "percent_numeric", (sICON): "mdi-cloud-outline", icon_loc: sRIGHT, icon_space: sSPC, h: i1, w: i2, (sBLROW): i16, (sBLCOL): i3, (sALIGNMENT): sLEFT, lpad: i3, rpad: iZ, (sUNIT): unitPercent, decimal: sNO, font: i4, font_weight: s400, (sIMPERIAL): "percent_numeric", (sMETRIC): "percent_numeric"], | |
[(sTIT): 'Day and Date', (sVAR): "date", ow: "dt", iu: "time_seconds", (sICON): sNONE, icon_loc: sNONE, icon_space: sSPC, h: i2, w: i4, (sBLROW): 18, (sBLCOL): i1, (sALIGNMENT): sCENTER, lpad: iZ, rpad: iZ, (sUNIT): unitDate, decimal: sNO, font: i8, font_weight: "800", (sIMPERIAL): "day_date", (sMETRIC): "day_date"], | |
] | |
rowsF=19 | |
columnsF=4 | |
} | |
} | |
@Field static List<Map> updateEnum=[["60000":"1 Minute"],["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1200000":"20 Minutes"], ["1800000":"Half Hour"], | |
["3600000":"1 Hour"], ["6400000":"2 Hours"], ["19200000":"6 Hours"], ["43200000":"12 Hours"], ["86400000":"1 Day"]] | |
def tileForecast(){ | |
List<Map> unitEnum = [[(sIMPERIAL):"Imperial (°F, mph, in, inHg, 0:00 am)"], [(sMETRIC):"Metric (°C, m/sec, mm, mmHg, 00:00)"]] | |
initFields() | |
dynamicPage((sNM): "graphSetupPage"){ | |
List<String> container | |
Map map=parent.openWeatherConfig() | |
hubiForm_section("General Options", i1, sBLK, sBLK){ | |
//input( (sTYPE): sENUM, (sNM): "openweather_refresh_rate",(sTIT): "<b>Select OpenWeather Update Rate</b>", (sMULTP): false, (sREQ): true, options: updateEnum, (sDEFV): "300000") | |
/* if(gtSetB('override_openweather')){ | |
input( (sTYPE): sENUM, (sNM): "pws_refresh_rate",(sTIT): "<b>Select PWS Update Rate</b>", (sMULTP): false, (sREQ): true, options: updateEnum, (sDEFV): "300000") | |
} */ | |
container=[] | |
//container << hubiForm_text_input ("<b>Open Weather Map Key</b>", "tile_key", sBLK, true) | |
//container << hubiForm_text_input ("<b>Latitude (Default=Hub location)</b>", "latitude", location.latitude.toString(), false) | |
//container << hubiForm_text_input ("<b>Longitude (Default=Hub location)</b>", "longitude", location.longitude.toString(), false) | |
if(map){ | |
app.updateSetting("latitude", map.latitude) | |
app.updateSetting("longitude", map.longitude) | |
app.updateSetting("tile_key", map.apiKey) | |
//apiVer: gtSetB('apiVer'), | |
//wunits: gtSetStr('wunits')?:'imperial' | |
String val | |
switch(sMs(map,'pollInterval')){ | |
case '1 Minute': | |
val="60000" | |
break | |
case '5 Minutes': | |
val="300000" | |
break | |
case '10 Minutes': | |
val="600000" | |
break | |
case '15 Minutes': | |
val="1200000" | |
break | |
case '30 Minutes': | |
val="1800000" | |
break | |
case '1 Hour': | |
val="3600000" | |
break | |
default: | |
val="10800000" | |
} | |
app.updateSetting("openweather_refresh_rate", val) | |
container << hubiForm_text("""Using $map settings from main app """ ) | |
container << hubiForm_color("Background", | |
sBACKGRND, | |
sBLACK, | |
false) | |
container << hubiForm_slider ((sTIT): "Background Opacity", | |
(sNM): "background_opacity", | |
(sDEFLT): i90, | |
(sMIN): iZ, | |
(sMAX): i100, | |
(sUNITS): "%", | |
(sSUBONCHG): false) | |
container << hubiForm_switch ((sTIT): "Color Icons?", (sNM): "color_icons", (sDEFLT): false) | |
hubiForm_container(container, i1) | |
List daysEnum=[[0: "Today"], [1: "Tomorrow"], [2: "2 Days from Now"], [3: "3 Days from Now"], [4: "4 Days from Now"], [5: "Five Days from Now"]] | |
input( (sTYPE): sENUM, (sNM): "day_num",(sTIT): "Day to Display", (sMULTP): false, (sREQ): false, options: daysEnum, (sDEFV): s1) | |
}else{ | |
container << hubiForm_text("""Main app is not configured for openweather""" ) | |
hubiForm_container(container, i1) | |
} | |
} | |
if(map){ | |
selectionsF.each{Map measurement-> | |
String mvar=sMs(measurement,sVAR) | |
hubiForm_section(sMs(measurement,sTIT), i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_switch ((sTIT): "Display "+sMs(measurement,sTIT)+"?", (sNM): mvar+"_display", (sDEFLT): true, (sSUBONCHG): true) | |
if((settings["${mvar}_display"]==null) || gtSetB("${mvar}_display")){ | |
container << hubiForm_fontvx_size((sTIT): mvar == "weather_icon" ? "Icon Size" : "Font Size", | |
(sNM): mvar, | |
(sDEFLT): measurement.font, | |
(sMIN): i1, | |
(sMAX): measurement.font*i2, | |
weight: sMs(measurement,'font_weight').toInteger(), | |
(sICON): mvar == "weather_icon") | |
container << hubiForm_slider ((sTIT): "Text Weight (400=normal, 700= bold)", | |
(sNM): mvar+"_font_weight", | |
(sDEFLT): sMs(measurement,'font_weight').toInteger(), | |
(sMIN): i100, | |
(sMAX): 900, | |
(sUNITS): sBLK, | |
(sSUBONCHG): false) | |
container << hubiForm_color("Font", mvar, sWHT, false) | |
hubiForm_container(container, i1) | |
if(sMs(measurement,'decimal') == sYES){ | |
container=[] | |
container << hubiForm_switch ((sTIT): "Display Unit Values (mm, mph, mbar, °, etc)", (sNM): mvar+"_display_units", (sDEFLT): true, (sSUBONCHG): false) | |
hubiForm_container(container, i1) | |
input( (sTYPE): sENUM, (sNM): mvar+"_decimal",(sTIT): "Decimal Places", (sREQ): false, (sMULTP): false, options: decimalsEnum, (sDEFV): 1, (sSUBOC): false) | |
} | |
String defs1=sMs(measurement,sIMPERIAL) | |
String ts1= mvar+"_units" | |
if(defs1 != sNONE){ | |
input( (sTYPE): sENUM, (sNM): ts1,(sTIT): "Displayed Units", (sREQ): false, (sMULTP): false, options: measurement.unit, (sDEFV): defs1, (sSUBOC): false) | |
} | |
if(settings[ts1] == defs1) wremoveSetting(ts1) | |
}else{ | |
hubiForm_container(container, i1) | |
for( String ts1 in [ "_font", "_font_weight", "_color", "_color_transparent", "_display_units", "_decimal", "_units" ]){ | |
String s= mvar+ts1 | |
if(settings[s]!=null) wremoveSetting(s) | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
def mainForecast(){ | |
initFields() | |
dynamicPage((sNM): "mainPage"){ | |
checkDup() | |
List<String> container | |
if(!state.endpoint){ | |
hubiForm_section("Please set up OAuth API", i1, "report", sBLK){ | |
href( (sNM): "enableAPIPageLink",(sTIT): "Enable API", description: sBLK, page: "enableAPIPage") | |
} | |
}else{ | |
hubiForm_section("Tile Options", i1, "tune", sBLK){ | |
container=[] | |
container << hubiForm_page_button("Configure Tile", "graphSetupPage", s100PCT, sPOLL) | |
hubiForm_container(container, i1) | |
} | |
if(tile_key){ | |
local_graph_url() | |
preview_tile() | |
} | |
put_settings() | |
} | |
selectionsF.each{Map measurement-> | |
String mvar=sMs(measurement,sVAR) | |
if((settings["${mvar}_display"]==null) || gtSetB("${mvar}_display")){ | |
List defs=[ measurement.font, sMs(measurement,'font_weight').toInteger(), sWHT, false, true, s1, sMs(measurement,sIMPERIAL) ] | |
Integer i; i=iZ | |
for( String ts1 in [ "_font", "_font_weight", "_color", "_color_transparent", "_display_units", "_decimal", "_units" ]){ | |
String s= mvar+ts1 | |
def v=settings[s] | |
//log.warn "checking $s $v == ${defs[i]}" | |
if(v!=null && v == defs[i]){ | |
wremoveSetting(s) | |
//log.warn "removed $s" | |
} | |
i++ | |
} | |
} | |
} | |
} | |
} | |
Map getOptions_forecast(){ | |
Map options=[ | |
"tile_units": tile_units, | |
"display_day": day_num, | |
"color_icons": color_icons, | |
"openweather_refresh_rate": openweather_refresh_rate, | |
"measurements": [], | |
] | |
initFields() | |
selectionsF.each{ Map measurement-> | |
String var=sMs(measurement,sVAR) | |
String outUnits=gtSetStr("${var}_units") ?: (sMs(measurement,sIMPERIAL) ?: sNONE) | |
String decimals=sMs(measurement,'decimal') == sYES ? (settings["${var}_decimal"] != i1 ? gtSetStr("${var}_decimal") :s1): sNONE | |
(List)options.measurements << [ "name": var, | |
"openweather": measurement.ow, | |
"in_unit" : measurement.iu, | |
"out_unit" : outUnits, | |
"decimals" : decimals, | |
] | |
} | |
return options | |
} | |
String defineHTML_Header_forecast(){ | |
String html=""" | |
<!DOCTYPE html> | |
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> | |
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.4.55/css/materialdesignicons.min.css"> | |
<script> | |
const localURL = "${getEndpointURL()}"; | |
const secretEndpoint= "${getEndpointSecret()}"; | |
const latitude = "${latitude}"; | |
const longitude = "${longitude}"; | |
const tile_key = "${tile_key}"; | |
</script> | |
<script src="https://code.getmdl.io/1.3.0/material.min.js"></script> | |
<!--script defer src="http://192.168.1.64:8080/WeatherTile.js"></script> --> | |
<script defer src="http://${location.hub.localIP}/local/${isSystemType() ? 'webcore/' : ''}a7af9806-4b0e-4032-a78e-a41e27e4d685-WeatherTile.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script> | |
<script type="text/javascript" src="https://www.google.com/jsapi"></script> | |
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> | |
""" | |
return html | |
} | |
String defineHTML_CSS_forecast(){ | |
initFields() | |
Integer num_columns=columnsF | |
def column_width=100.0/num_columns | |
Integer num_rows=rowsF | |
def row_height=100.0/num_rows | |
/* String background='black' | |
if(background_color != null){ | |
Float transparent=background_color_transparent ? 0.0 : background_opacity | |
background=getRGBA(background_color, transparent) | |
} */ | |
String html | |
html=""" | |
.grid-container{ | |
display: grid; | |
""" | |
html += " grid-template-columns:" | |
Integer i | |
for(i=iZ; i<num_columns; i++) | |
html+="${column_width}vw " | |
html += ";" | |
html +=" grid-template-rows: " | |
html +="${row_height/2}vh " | |
for(i=iZ; i<num_rows-i1; i++) | |
html+="${row_height}vh " | |
html+="${row_height/2}vh;" | |
html+= """ | |
grid-gap: 0px; | |
align-items: center; | |
background-color: ${getRGBA(gtSetStr('background_color'), background_opacity)}; | |
} | |
.grid-container > div{ | |
text-align: center; | |
} | |
""" | |
//current_row=2 //leave top row blank | |
selectionsF.each{Map item-> | |
String var=sMs(item,sVAR) | |
if(gtSetB("${var}_display")){ | |
String font=settings["${var}_font"] ?: item.font | |
def weight=settings["${var}_font_weight"] ?: item.font_weight | |
String color=settings["${var}_color"] ?: sWHT | |
def row_start=item.baseline_row | |
def row_end=item.baseline_row + item.h | |
def column_start=item.baseline_column | |
def column_end=item.baseline_column + item.w | |
html += """ | |
.${var}{ | |
grid-row-start: ${row_start}; | |
grid-row-end: ${row_end}; | |
grid-column-start: ${column_start}; | |
grid-column-end: ${column_end}; | |
font-size: ${font}vh; | |
padding-top: 0vmin !important; | |
padding-left: ${item.lpad}vw !important; | |
padding-right: ${item.rpad}vw !important; | |
text-align: ${item.alignment} !important; | |
color: ${color} !important; | |
font-weight: ${weight}; | |
} | |
""" | |
} | |
} | |
return html | |
} | |
String defineHTML_Tile_forecast(){ | |
/* | |
def temp_units='°' | |
def rain_units='"' | |
def m_time_units=' am' | |
def e_time_units=' pm' | |
def wind_units=' mph' | |
def pressure_units='inHg' | |
if(tile_units == "metric"){ | |
rain_units='mm' | |
m_time_units='' | |
e_time_units='' | |
wind_units=' m/sec' | |
pressure_units='mmHg' | |
} */ | |
initFields() | |
String html | |
html=""" | |
<div class="grid-container"> | |
""" | |
selectionsF.each{Map item-> | |
String var=sMs(item,sVAR) | |
html += """<div class="${var}">""" | |
//Left Icon | |
if(item.icon != sNONE && item.icon_loc == sLEFT){ | |
//log.debug(item.icon) | |
html+="""<span class="mdi ${item.icon}">${item.icon_space}</span>""" | |
} | |
//Main Content | |
html += """<span id="${var}"></span>""" | |
//Units | |
String un=gtSetStr("${var}_units") ?: sMs(item,sIMPERIAL) | |
String units=getAbbrev(un) | |
Boolean disu=settings["${var}_display_units"]!=null ? gtSetB("${var}_display_units") : true | |
if(disu && sMs(item,sIMPERIAL) != sNONE && units != "unknown") html+="""<span>${units}</span>""" | |
//Right Icon | |
if(item.icon != sNONE && item.icon_loc == sRIGHT){ | |
html+="""<span>${item.icon_space}</span>""" | |
html+="""<span class="mdi ${item.icon}"></span>""" | |
} | |
html += """</div>""" | |
} | |
html += """ | |
</div> | |
""" | |
return html | |
} | |
/* | |
String defineHTML_globalVariables(){ | |
String html=""" | |
var sunrise; | |
var sunset; | |
let options=[]; | |
let pws_data=[]; | |
let currentTemperature; | |
""" | |
} */ | |
String getGraph_forecast(){ | |
// String fullSizeStyle="margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden" | |
String html | |
html=defineHTML_Header_forecast() | |
html += "<head><style>" | |
//CSS | |
html += defineHTML_CSS_forecast() | |
html += """</style></head><body onload="initializeForecast()">""" | |
html += defineHTML_Tile_forecast() | |
html+="</body></html>" | |
return html | |
} | |
//oauth endpoints | |
String getData_forecast(){ | |
Map data= (Map)parent.getWData() // getPWSData() | |
//String tdata=parent.getOpenWeatherData() // TODO parent.getWData() | |
//if(isEric())myDetail null,"getData_forecast: $data",iN2 | |
return JsonOutput.toJson(data) | |
} | |
/* | |
* TODO: Longtermstorage methods | |
*/ | |
def mainLongtermstorage(){ | |
dynamicPage((sNM): "mainPage"){ | |
List<String> container | |
hubiForm_section(tDesc()+" Options", i1, "tune", sBLK){ | |
container=[] | |
container << hubiForm_page_button("Select Device/Data", "deviceSelectionPage", s100PCT, "vibration") | |
container << hubiForm_page_button("Configure/Report Data Storage", "graphSetupPage", s100PCT, sPOLL) | |
hubiForm_container(container, i1) | |
} | |
put_settings(false) | |
} | |
} | |
@Field static String minFwVersion = "2.3.4.132" | |
def deviceLongtermstorage(){ | |
dynamicPage((sNM): "deviceSelectionPage", nextPage:"attributeConfigurationPage"){ | |
String s='hpmSecurity' | |
Boolean fwOk= ((String)location.hub.firmwareVersionString >= minFwVersion) | |
List<String> container | |
if(!fwOk){ | |
if(password && username){ | |
log.debug("Username and Password set") | |
} | |
hubiForm_section("Login Information", i1, sBLK, sBLK){ | |
if(settings[s]==null){ | |
settings[s]=true | |
app.updateSetting(s, true) | |
} | |
container=[] | |
container << hubiForm_switch ((sTIT): "<b>Use Hubitat Security?</b>", | |
(sNM): s, (sDEFLT): true, (sSUBONCHG): true) | |
hubiForm_container(container, i1) | |
if(gtSetB(s)){ | |
input "username", "string",(sTIT): "Hub Security username", (sREQ): false, (sSUBOC): true | |
input "password", "password",(sTIT): "Hub Security password", (sREQ): false, (sSUBOC): true | |
} | |
} | |
} | |
if(!fwOk && gtSetB(s) && !login()){ | |
hubiForm_section("Login Error", i1, sBLK, sBLK){ | |
container=[] | |
container << hubiForm_text("""<b>CANNOT LOGIN</b><br>If you have Hub Security Enabled, please put in correct login credentials<br> | |
If not, please deselect <b>Use Hubitat Security</b>""") | |
hubiForm_container(container, i1) | |
} | |
}else{ | |
hubiForm_section("Sensor and Attribute Selection", i1, sBLK, sBLK){ | |
input 'sensors', "capability.*",(sTIT): "<b>Sensor Selection for Long Term Storage</b>", (sMULTP): true, (sSUBOC): true | |
if(sensors){ | |
List<Map> final_attrs; final_attrs=[] | |
for(sensor in (List)sensors){ | |
String sid=gtSensorId(sensor) | |
if(sid==sBLK){ | |
error "deviceLongtermstorage null sid ${sensor}",null,iN2 | |
continue | |
} | |
try{ | |
final_attrs=[] | |
//List attributes_=sensor.getSupportedAttributes() | |
List<String> attributes_=sensor.getSupportedAttributes().collect{ it.getName() }.unique().sort() | |
attributes_.each{ String attribute_-> | |
//String name=attribute_.getName() | |
def v= sensor.currentState(attribute_,true) | |
if(v != null){ | |
final_attrs << [(attribute_) : "${attribute_} ::: [${v.getValue()}]"] | |
} | |
} | |
final_attrs=final_attrs.unique(false) | |
}catch(e){ | |
final_attrs=[[(s1): "ERROR"]] | |
error "Error: ",null,iN2,e | |
} | |
String sensor_name=gtLbl(sensor) | |
input( (sTYPE): sENUM, (sNM): "${gtSensorId(sensor)}_attributes",(sTIT): "${sensor_name} attribute(s) to Store", | |
(sREQ): true, (sMULTP): true, options: final_attrs, (sSUBOC): false) | |
} | |
} | |
} | |
} | |
} | |
} | |
def optionsLongtermstorage(){ | |
def hoursEnum=1..24 | |
// def df=new DecimalFormat("#0.0") | |
dynamicPage((sNM): "attributeConfigurationPage"){ | |
for(sensor in (List)sensors){ | |
String sid=gtSensorId(sensor) | |
if(sid==sBLK){ | |
error "optionsLongtermstorage null sid ${sensor}",null,iN2 | |
continue | |
} | |
List<String> att=(List<String>)settings["${sid}_attributes"] | |
if(att){ | |
att.each{ String attribute-> | |
String attr=attribute.replaceAll(sSPC, "_") | |
String sensor_name=gtLbl(sensor) | |
hubiForm_section("${sensor_name} (${attribute})", i1, sBLK, sBLK){ | |
storageLimitInput(sid, attr) | |
String s="${sid}_${attr}".toString() | |
input( (sTYPE): sENUM, (sNM): s+"_time_every",(sTIT): "Attempt to Store Data Every X Hours", | |
(sREQ): true, (sMULTP): false, options: hoursEnum, (sSUBOC): false, (sDEFV): 1) | |
input( (sTYPE): sTIME, (sNM): s+"_time",(sTIT): "Time to Start Storing Data", | |
(sREQ): false, (sMULTP): false, (sSUBOC): false, (sDEFV): "00:00") | |
//quantInput(sid,attr) | |
List<String> container | |
container=[] | |
List<Map> events=getAllDataLimit(sensor, attribute, 60) | |
Integer num_events=events?.size() | |
Date now=new Date() | |
if(num_events > i2){ | |
// TODO | |
def span=( dtMdt(events[num_events-i1]).getTime()- dtMdt(events[iZ]).getTime())/(1000*60*60*24) | |
def since=(now.getTime() - dtMdt(events[iZ]).getTime())/(1000*60*60) | |
List quantData= doQuant(events, sid, attr, true) | |
Long frequency=averageFrequency(events) | |
container << hubiForm_sub_section("Estimated Storage Consumption") | |
container << hubiForm_text("<b>Total Events:</b> ${quantData.size()} quantized (${num_events} raw data)") | |
container << hubiForm_text("<b>First Event:</b> ${events[iZ][sDT]} (<b>${round(since)}</b> hours ago)") | |
container << hubiForm_text("<b>Frequency of raw data:</b> 1 event every ${round(frequency/(1000*60))} minutes") | |
List subcontainer | |
subcontainer=[] | |
subcontainer << hubiForm_text(sBLK) | |
subcontainer << hubiForm_text("<b>Daily Storage</b>") | |
subcontainer << hubiForm_text("<b>Weekly Storage</b>") | |
subcontainer << hubiForm_text("<b>Monthly Storage</b>") | |
subcontainer << hubiForm_text("<b>Yearly Storage</b>") | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]]) | |
Integer averageSize= 34 // 50 | |
//Map storage=getCurrentDailyStorage(sensor, attribute) | |
//subcontainer << hubiForm_text(storage.num_events.toString()) | |
//subcontainer << hubiForm_text(convertStorageSize((Integer)storage.size)) | |
subcontainer=[] | |
Integer daily | |
daily=((num_events/span)*averageSize).toInteger() | |
subcontainer << hubiForm_text("Raw Data") | |
subcontainer << hubiForm_text(convertStorageSize(daily)) | |
subcontainer << hubiForm_text(convertStorageSize(daily*7)) | |
subcontainer << hubiForm_text(convertStorageSize(daily*30)) | |
subcontainer << hubiForm_text(convertStorageSize(daily*365)) | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]]) | |
subcontainer=[] | |
daily=((quantData.size()/span)*averageSize).toInteger() | |
subcontainer << hubiForm_text("Quantized Data") | |
subcontainer << hubiForm_text(convertStorageSize(daily)) | |
subcontainer << hubiForm_text(convertStorageSize(daily*7)) | |
subcontainer << hubiForm_text(convertStorageSize(daily*30)) | |
subcontainer << hubiForm_text(convertStorageSize(daily*365)) | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]]) | |
} | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
} | |
} | |
} | |
def graphLongtermstorage(){ | |
dynamicPage((sNM): "graphSetupPage"){ | |
if(sensors){ | |
List<String> container | |
List<String> subcontainer | |
hubiForm_section("Current Attribute Storage", i1, sBLK, sBLK){ | |
container=[] | |
subcontainer=[] | |
subcontainer << hubiForm_text("<b>Sensor</b>") | |
subcontainer << hubiForm_text("<b>Attribute</b>") | |
subcontainer << hubiForm_text("<b>Number of Events</b>") | |
subcontainer << hubiForm_text("<b>First Event Time</b>") | |
subcontainer << hubiForm_text("<b>Last Event Time</b>") | |
subcontainer << hubiForm_text("<b>File Size</b>") | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]]) | |
Double totalS | |
totalS=0.0D | |
for(sensor in (List)sensors){ | |
String sid=gtSensorId(sensor) | |
if(sid==sBLK){ | |
error "graphLongtermstorage null sid ${sensor}",null,iN2 | |
continue | |
} | |
List<String> att=(List<String>)settings["${sid}_attributes"] | |
if(att){ | |
att.each{ String attribute-> | |
String sensor_name=gtLbl(sensor) | |
subcontainer=[] | |
//appendFile_LTS(sensor, attribute) | |
Map storage=getCurrentDailyStorage(sensor, attribute) | |
String filename_=getFileName(sensor, attribute) | |
String uri_="http://${location.hub.localIP}:8080/local/${filename_}" | |
subcontainer << hubiForm_text(sensor_name, uri_) | |
subcontainer << hubiForm_text(attribute, uri_) | |
subcontainer << hubiForm_text(storage.num_events.toString()) | |
subcontainer << hubiForm_text(formatTime(dtMs(storage,'first'))) | |
subcontainer << hubiForm_text(formatTime(dtMs(storage,'last'))) | |
Integer s= iMs(storage,'size') | |
subcontainer << hubiForm_text(convertStorageSize(s)) | |
totalS += s | |
container << hubiForm_subcontainer([objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]]) | |
} | |
} | |
} | |
container << hubiForm_text("<b>Total Storage:</b> ${convertStorageSize(totalS.toInteger())}") | |
hubiForm_container(container, i1) | |
} | |
} | |
} | |
} | |
/** | |
* called by graph apps to know if LTS for id, attribute is available | |
* it ends up calling isStorage in LTS app | |
*/ | |
Boolean isLtsAvailable(id, String attribute){ | |
return (Boolean)parent.ltsAvailable(id, attribute) | |
} | |
/** LTS only called by parent is LTS stream enabled? */ | |
Boolean isStorage(id, String attribute){ | |
def sensor=sensors?.find{it.id == id} | |
if(sensor != null){ | |
List<String> att=(List<String>)settings["${id}_attributes"] | |
return att.find{ it == attribute } != null | |
} | |
return false | |
} | |
/** LTS only called by parent is LTS stream with quant enabled?*/ | |
Boolean isQuant(id, String attribute){ | |
if(isStorage(id,attribute)){ | |
def sensor=sensors?.find{it.id == id} | |
if(sensor){ | |
String s= "${gtSensorId(sensor)}_${attribute}_quantization" | |
String ts1= s+"_function" | |
return !(settings[ts1]==sNONE || settings[s]==null || settings[s]==s0) | |
} | |
} | |
return false | |
} | |
/** LTS only, called by schedule to add data to file from device events in DB */ | |
void updateData_LTS(Map data){ | |
if(isEric())myDetail null,"updateData $data",i1 | |
Map theEvent | |
theEvent=[:]+data | |
Map qres=queueSemaphore(data) | |
// log.warn "qres:$qres" | |
String msgt | |
msgt="queued" | |
if(!bIs(qres,'exitOut')){ | |
String pNm=sAppId() | |
while(true){ | |
def sensor=sensors?.find{ it.id == theEvent.id } | |
if(sensor) appendFile_LTS(sensor, sMs(theEvent,sATTR)) | |
else warn "Sensor not found ${theEvent}",null | |
theEvent=null | |
getTheLock(pNm,'update Data') | |
List<Map> evtQ | |
evtQ=theQueuesVFLD[pNm] | |
if(!evtQ){ | |
if(theSemaphoresVFLD[pNm]<= lMs(qres,'semaphore')){ | |
msgt='Released Lock and exiting' | |
theSemaphoresVFLD[pNm]=lZ | |
theSemaphoresVFLD=theSemaphoresVFLD | |
} | |
releaseTheLock(pNm) | |
break | |
}else{ | |
evtQ=theQueuesVFLD[pNm] | |
List<Map>evtList=evtQ //.sort{ Map it -> lMt(it) } | |
theEvent=evtList.remove(0) | |
Integer qsize=evtList.size() | |
theQueuesVFLD[pNm]=evtList | |
theQueuesVFLD=theQueuesVFLD | |
releaseTheLock(pNm) | |
if(qsize>i20)warn "large queue size ${qsize}".toString(),null | |
} | |
} | |
} | |
if(isEric())myDetail null,"update Data ${msgt}" | |
} | |
// Scheduling functions | |
void setupCron(sensor, String attribute){ | |
if(isEric())myDetail null,"setupCron $sensor $attribute",i1 | |
String dateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ" | |
String sid=gtSensorId(sensor) | |
if(sid){ | |
String attr=attribute.replaceAll(sSPC, "_") | |
Date date=wtimeToday( gtSetStr("${sid}_${attr}_time"), mTZ()) | |
//Date date=Date.parse(dateFormat, gtSetStr("${sid}_${attr}_time")) | |
//error "object: ${describeObject(settings["${sid}_${attr}_time_every"])}",null | |
//log.warn myObj(settings["${sid}_${attr}_time_every"]) | |
Integer repeat= gtSetStr("${sid}_${attr}_time_every").toInteger() | |
//log.warn "$date $repeat ${sensor.id}" | |
addToSched(hrs: date.getHours(), mins: date.getMinutes(), repeatHrs: repeat, sid: sid, (sATTR): attribute) | |
//schedule("0 ${date.getMinutes()} ${date.getHours()}/${repeat} ? * * *", updateData, [overwrite: false, data: [id: sid, attribute: attribute]]) | |
}else | |
error "setupCron null sid ${sensor} ${attribute}",null,iN2 | |
if(isEric())myDetail null,"setupCron $sensor $attribute" | |
} | |
private void clearSch(){ | |
String pNm=sAppId() | |
getTheLock(pNm,'clearSch') | |
atomicState.sched=[] | |
releaseTheLock(pNm) | |
} | |
private addToSched(Map data){ | |
if(isEric())myDetail null,"addToSched",i1 | |
Integer hrs= iMs(data,'hrs') | |
Integer mins= iMs(data,'mins') | |
Integer repeatHrs= iMs(data,'repeatHrs') | |
String sid= sMs(data,'sid') | |
String attribute=sMs(data,sATTR) | |
Long nextRun=pushAhead(hrs,mins,repeatHrs) | |
String pNm=sAppId() | |
getTheLock(pNm,'addToSched') | |
List<Map> sched | |
sched=atomicState.sched | |
sched=sched != null ? sched : [] | |
if(sched) unschedule() | |
sched << [hrs: hrs, mins: mins, repeatHrs: repeatHrs, sid: sid, (sATTR): attribute, nextRun: nextRun] | |
atomicState.sched=sched | |
releaseTheLock(pNm) | |
if(isEric())myDetail null,"addToSched" | |
} | |
@CompileStatic | |
Long pushAhead(Integer hrs, Integer mins, Integer repeatHrs){ | |
Long firstOffset= hrs*3600000 + mins*60000 | |
Long baset= getMidnightTime() + firstOffset | |
Long endt=getNextMidnightTime() | |
Long repeatT=repeatHrs*3600000 | |
Long res | |
res=baset | |
Long n=wnow() | |
while (res<n && res<endt){ | |
res += repeatT | |
} | |
if(res > endt) res= endt + firstOffset | |
return res | |
} | |
private Long getMidnightTime(){ return wtimeToday('00:00',mTZ()).getTime() } | |
private Long getNextMidnightTime(){ return wtimeTodayAfter('23:59','00:00',mTZ()).getTime() } | |
private Date wtimeToday(String str,TimeZone tz){ return (Date)timeToday(str,tz) } | |
private Date wtimeTodayAfter(String astr,String tstr,TimeZone tz){ return (Date)timeTodayAfter(astr,tstr,tz) } | |
private void wrunInMillis(Long t,String m,Map d){ runInMillis(t,m,d) } | |
private runNextSched(Map a=[:]){ | |
String msg | |
msg="runNextSched" | |
if(isEric())myDetail null,msg,i1 | |
String pNm=sAppId() | |
Boolean didSomething, didSched | |
didSomething=false | |
didSched=false | |
getTheLock(pNm,msg) | |
List<Map> sched | |
sched=atomicState.sched | |
sched=(sched!=null) ? []+sched : [] | |
Long nextSched | |
nextSched=0L | |
Integer i | |
for(i=iZ; i< sched.size(); i++){ | |
Map s=sched[i] | |
Long nextRun | |
nextRun= lMs(s,'nextRun') | |
Integer hrs= iMs(s,'hrs') | |
Integer mins= iMs(s,'mins') | |
Integer repeatHrs= iMs(s,'repeatHrs') | |
String sid= sMs(s,'sid') | |
String attribute=s[sATTR] | |
if(nextRun < wnow()){ | |
didSomething=true | |
nextRun=pushAhead(hrs,mins,repeatHrs) | |
s.nextRun=nextRun | |
if(didSomething) atomicState.sched=sched | |
releaseTheLock(pNm) | |
updateData_LTS([(sID): sid, (sATTR): attribute]) | |
getTheLock(pNm,msg+' L') | |
} | |
if(!nextSched) nextSched= nextRun | |
if(nextRun< nextSched) nextSched=nextRun | |
} | |
Long n=wnow() | |
if(nextSched>n){ | |
Long t=nextSched-wnow() | |
didSched=true | |
wrunInMillis(t,"runNextSched", [:]) | |
state.nextSched=nextSched | |
if(isEric())myDetail null,msg+" schedule in $t msecs",iN2 | |
}else{ | |
if(isEric())myDetail null,msg+" no nextsched $nextSched or bad choice $n",iN2 | |
} | |
releaseTheLock(pNm) | |
if(!didSomething && !didSched) msg += " did nothing" | |
if(isEric())myDetail null,msg | |
} | |
void checkSched(){ | |
Long next | |
next=state.nextSched | |
next= next ?: 0L | |
if(wnow() > next+900000L){ // 15 mins late | |
String msg='checkSched' | |
if(isEric())myDetail null,msg,i1 | |
runNextSched() | |
if(isEric())myDetail null,msg | |
} | |
} | |
// TODO quant functions | |
/** returns internal format entry */ | |
@CompileStatic | |
Map sum(List<Map> events, Integer decimals, Boolean round, Integer granularity){ | |
Float sum | |
sum=new Float(0) | |
for(Map event in events){ | |
sum += Float.valueOf(event[sVAL].toString()) | |
} | |
Map tdate=[(sDT): events[events.size()-i1][sDT], boundary: round, granularity: granularity] | |
Date d=roundDate(tdate) | |
return [(sDT): d, (sVAL): sum.round(decimals), (sT): d.getTime()] | |
} | |
/** returns internal format entry */ | |
@CompileStatic | |
Map average(List<Map>events, Integer decimals, Boolean round, Integer granularity){ | |
Float sum | |
sum=new Float(0) | |
Integer sz=events.size() | |
for(Map event in events){ | |
sum += Float.valueOf(event[sVAL].toString()) | |
} | |
sum /= sz | |
Map tdate=[(sDT): events[sz-i1][sDT], boundary: round, granularity: granularity] | |
Date d=roundDate(tdate) | |
return [(sDT): d, (sVAL): sum.round(decimals), (sT): d.getTime(), (sQ):i1] | |
} | |
/** returns internal format entry */ | |
@CompileStatic | |
Map min(List<Map>events, Integer decimals, Boolean round, Integer granularity){ | |
Float min | |
min=Float.valueOf(events[iZ][sVAL].toString()) | |
for(Map event in events){ | |
Float v=Float.valueOf(event[sVAL].toString()) | |
min=v < min ? v : min | |
} | |
Map tdate=[(sDT): events[events.size()-i1][sDT], boundary: round, granularity: granularity] | |
Date d=roundDate(tdate) | |
return [(sDT): d, (sVAL): min.round(decimals), (sT): d.getTime()] | |
} | |
/** returns internal format entry */ | |
@CompileStatic | |
Map max(List<Map>events, Integer decimals, Boolean round, Integer granularity){ | |
Float max | |
max=Float.valueOf(events[iZ][sVAL].toString()) | |
for(Map event in events){ | |
Float v=Float.valueOf(event[sVAL].toString()) | |
max=v > max ? v : max | |
} | |
Map tdate=[(sDT): events[events.size()-i1][sDT], boundary: round, granularity: granularity] | |
Date d=roundDate(tdate) | |
return [(sDT): d, (sVAL): max.round(decimals), (sT): d.getTime()] | |
} | |
/** returns internal format entry */ | |
@CompileStatic | |
Map count(List<Map>events, Integer decimals, Boolean round, Integer granularity){ | |
Integer sz=events.size() | |
Map tdate=[(sDT): events[sz-i1][sDT], boundary: round, granularity: granularity] | |
Date d=roundDate(tdate) | |
return [(sDT): d, (sVAL): sz, (sT): d.getTime(), (sQ):i1] | |
} | |
/* | |
static Long getTime(String text){ | |
String dateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ" | |
//String dateFormat="yyyy-MM-dd'T'HH:mm:ssX" | |
return Date.parse(dateFormat, text).getTime() | |
} */ | |
/** round a date based on quant settings */ | |
Date roundDate(Map map){ | |
Date date= dtMdt(map) | |
Boolean boundary= !!bIs(map,'boundary') | |
Integer granularity=map.granularity as Integer | |
if(!boundary) return date | |
Date nearest | |
nearest=date | |
if(granularity > 60 && granularity < 1440) | |
nearest=org.apache.commons.lang3.time.DateUtils.truncate(date, Calendar.HOUR_OF_DAY) | |
else if(granularity == 1440) | |
nearest=org.apache.commons.lang3.time.DateUtils.truncate(date, Calendar.DAY_OF_MONTH) | |
return nearest | |
} | |
/** | |
* | |
* @return - internal format | |
*/ | |
@CompileStatic | |
List quantizeData(List<Map> events, String mins, String funct, Integer dec, Boolean boundary, Boolean toStore){ | |
Integer minutes=mins as Integer | |
Integer sz | |
String s | |
s='quantizeData ' | |
Boolean isE= isEric() | |
sz=events.size() | |
if(isE)myDetail null,s+"mins: $mins func: $funct decimals: $dec boundary: $boundary size: $sz",i1 | |
if(minutes==iZ || funct==sNONE){ | |
if(isE)myDetail null,s+"no change" | |
return events | |
} | |
Integer decimals=dec as Integer | |
Long milliSeconds=minutes*1000*60 | |
List<Map> newEvents | |
newEvents=[] | |
try{ | |
Long stop | |
stop=roundDate([(sDT): dtMdt(events[iZ]), granularity: minutes, boundary: boundary]).getTime() + milliSeconds | |
List<Map> tempEvents | |
tempEvents=[] | |
Integer idx | |
idx=iZ | |
Map newEntry | |
Long currTime | |
while (idx < events.size()){ | |
currTime=roundDate([(sDT): events[idx][sDT], granularity: minutes, boundary: boundary]).getTime() | |
if(currTime > stop){ | |
sz=tempEvents.size() | |
newEntry=tempEvents[iZ] | |
if(sz == i1 && newEntry[sQ] == i1){ // deals with count cannot be re-processed | |
if(isE) trace "DID NOT REPROCESS "+s+"$funct $sz",null | |
newEvents.add(newEntry) | |
}else if(sz > 0 ){ | |
if(isE) trace s+"$funct $sz",null | |
newEntry=callFunc(funct,tempEvents, decimals, boundary, minutes) | |
newEvents.add(newEntry) | |
} | |
stop += milliSeconds | |
tempEvents=[] | |
} | |
tempEvents.add(events[idx]) | |
idx++ | |
} | |
// TODO remove this | |
// The last events are not quant'd | |
// (sum, average, min, max, count) | |
sz=tempEvents.size() | |
if( (sz == i1 && tempEvents[iZ][sQ]==i1) || | |
(sz>0 && toStore && funct in ['average','count']) ){ // don't screw up average, count -> leave last unprocessed | |
if(isE) trace s+"$funct adding $sz $tempEvents ",null | |
newEvents=newEvents + tempEvents | |
}else if(sz != iZ){ | |
if(isE) trace s+"LAST $funct $sz",null | |
newEntry=callFunc(funct,tempEvents, decimals, boundary, minutes) | |
newEvents.add(newEntry) | |
} | |
}catch(e){ | |
error s,null,iN2,e | |
} | |
sz=newEvents.size() | |
if(isE)myDetail null,s+"Final size $sz" | |
return newEvents | |
} | |
Map callFunc(String funct, List<Map>tempEvents, Integer decimals, Boolean boundary, Integer minutes){ | |
Map newEntry="${funct}"(tempEvents, decimals, boundary, minutes) | |
return newEntry | |
} | |
// shared | |
def storageLimitInput(String sid, String attribute, String defl="7", String varn=sNL){ | |
List<Map<String,String>> storageEnum=[ | |
["1" : "1 Day"], ["2" : "2 Days"], ["3" : "3 Days"], ["4" : "4 Days"], ["5" : "5 Days"], ["6" : "6 Days"], | |
["7" : "1 Week"], ["14" : "2 Weeks"], ["21" : "3 Weeks"], | |
["30" : "1 Month"], ["60" : "2 Months"], ["91" : "3 Months"], ["121" : "4 Months"], ["152" : "5 Months"], ["182" : "6 Months"], | |
["213" : "7 Months"], ["243" : "8 Months"], ["274" : "9 Months"], ["304" : "10 Months"], ["334" : "11 Months"], | |
["365" : "1 Year"], ["730" : "2 Years"], ["1095" : "3 Years"], ["1461" : "4 Years"]] | |
String s= varn ?: (sid+'_'+attribute+'_storage') | |
input( (sTYPE): sENUM, (sNM): s,(sTIT): "Duration of Storage to Maintain", | |
(sREQ): false, (sMULTP): false, options: storageEnum, (sSUBOC): false, (sDEFV): defl) | |
} | |
static Long averageFrequency(List<Map> events){ | |
Long sum | |
sum=0L | |
Integer i | |
for(i=i1; i<events.size(); i++){ | |
// TODO | |
sum += dtMdt(events[i]).getTime() - dtMdt(events[i-i1]).getTime() | |
} | |
return Math.round(sum/events.size()) | |
} | |
/** pull device events from HE DB */ | |
List<Map> getEvents(Map map){ | |
if(isEric())myDetail null,"getEvents $map",i1 | |
try{ | |
def sensor=map.sensor | |
String attribute=map[sATTR] | |
Integer days= iMs(map,'days') | |
Date then | |
if(map.start_time){ | |
then= dtMs(map,'start_time') | |
}else{ | |
Date now=new Date() | |
then=now | |
use (TimeCategory){ | |
then -= days.days | |
} | |
} | |
//TODO remove date | |
List<Map> respEvents | |
respEvents=(List<Map>)sensor.statesSince(attribute, then, [(sMAX): 2000]).collect{ [ (sDT): it.date, (sVAL): it.value, (sT): ((Date)it.date).getTime()] } | |
respEvents=respEvents.flatten() as List<Map> | |
respEvents=respEvents.reverse() as List<Map> | |
if(isEric())myDetail null,"getEvents $map ${respEvents.size()}" | |
return respEvents as List<Map> | |
}catch(e){ | |
error "getEvents",null,iN2,e | |
} | |
if(isEric())myDetail null,"getEvents" | |
return null | |
} | |
Boolean login(){ | |
if(gtSetB('hpmSecurity')){ | |
Boolean result | |
result=false | |
try{ | |
httpPost( | |
[ | |
uri: "http://127.0.0.1:8080", | |
path: "/login", | |
query: [ loginRedirect: "/" ], | |
body: [ | |
username: username, | |
password: password, | |
submit: "Login" | |
], | |
textParser: true, | |
ignoreSSLIssues: true | |
] | |
){ resp -> | |
if(resp.data?.text?.contains("The login information you supplied was incorrect.")) | |
result=false | |
else{ | |
state.cookie=((List) ((String)resp?.headers?.'Set-Cookie')?.split(';'))?.getAt(0) | |
result=true | |
} | |
} | |
}catch(e){ | |
error "Error logging in: ",null,iN2,e | |
result=false | |
} | |
return result | |
} | |
return true | |
} | |
Boolean fileExists(sensor, String attribute, String fname=sNL){ | |
String filename_=fname ?: getFileName(sensor, attribute) | |
String uri="http://${location.hub.localIP}:8080/local/${filename_}" | |
Map params=[ | |
uri: uri, | |
textParser: true, | |
] | |
Boolean res | |
res=false | |
try{ | |
httpGet(params){ resp -> | |
if(resp.status==200) res=true | |
} | |
}catch(e){ | |
String sensor_name=gtLbl(sensor) | |
if( isFNF(e) ){ | |
debug "File DOES NOT Exist for ${sensor_name} (${attribute})",null,iN2 | |
}else{ | |
error"Find file ${sensor_name} (${attribute}) ($filename_} :: Exception: ",null,iN2,e | |
} | |
} | |
return res | |
} | |
static Boolean isFNF(Exception ex){ | |
if(ex instanceof java.nio.file.NoSuchFileException) return true | |
String file=(String)ex.message | |
return file.contains("Not Found") | |
} | |
@Field volatile static Map<String,String> readTmpFLD=[:] | |
@Field volatile static Map<String,byte[]> readTmpBFLD=[:] | |
/** | |
* returns Map that has internal format in map.data | |
* @param sensor | |
* @param attribute | |
* @param fname | |
* @return Map [ size: x, data: List<Map> [[date: date, (sVAL): v, t: t], ....] ] | |
*/ | |
Map readFile(sensor, String attribute, String fname=sNL){ | |
String s= "readFile $sensor $attribute $fname" | |
if(isEric())myDetail null,s,i1 | |
String filename_=fname ?: getFileName(sensor, attribute) | |
String pNm=filename_ | |
if(readTmpFLD[pNm]==sNL){ readTmpFLD[pNm]=sBLK; readTmpFLD= readTmpFLD } | |
try{ | |
Integer sz=readTmpFLD[pNm].size() | |
//myDetail null,"pNm: ${pNm} cache sz: $sz",iN2 | |
if(sz> 4){ | |
JsonSlurper jsonSlurper=new JsonSlurper() | |
List<Map> parse=convertToInternal((List<Map>)jsonSlurper.parseText(readTmpFLD[pNm])) | |
if(isEric())trace "readFile cache hit",null | |
if(isEric())myDetail null,s | |
return ['size': sz, 'data': parse ] | |
} | |
} catch(ignored){} | |
readTmpFLD[pNm]=sBLK | |
readTmpFLD= readTmpFLD | |
try{ | |
if((String)location.hub.firmwareVersionString >= minFwVersion){ | |
readTmpBFLD[pNm]=null | |
readTmpBFLD[pNm]= (byte[])downloadHubFile(filename_) | |
if(readTmpBFLD[pNm].size()){ | |
readTmpFLD[pNm]=new String(readTmpBFLD[pNm]) | |
readTmpBFLD[pNm]=null | |
readTmpBFLD= readTmpBFLD | |
} | |
}else{ | |
String uri="http://${location.hub.localIP}:8080/local/${filename_}" | |
Map params=[ | |
uri: uri, | |
contentType: "text/plain; charset=UTF-8", | |
textParser: true, | |
headers: [ "Cookie": state.cookie, "Accept": 'application/octet-stream' ] | |
] | |
httpGet(params){ resp -> | |
if(resp.status==200 && resp.data){ | |
Integer i | |
char c | |
i=resp.data.read() | |
while(i!=-1){ | |
c=(char)i | |
readTmpFLD[pNm]+=c | |
i=resp.data.read() | |
} | |
//log.warn "pNm: ${pNm} data: ${data} file: ${readDataFLD[pNm]}" | |
}else{ | |
error "Read Response status $resp.status",null | |
} | |
} | |
} | |
readTmpFLD= readTmpFLD | |
Integer sz | |
sz=readTmpFLD[pNm].size() | |
//myDetail null,"after read pNm: ${pNm} cache sz: $sz",iN2 | |
if(sz){ | |
String sc | |
sc = readTmpFLD[pNm] | |
while(sz && sc[sz-i1]!= ']'){ | |
sc = sc.substring(iZ,sz-i1) | |
sz=sc.size() | |
} | |
readTmpFLD[pNm]=sc | |
} | |
//myDetail null,"after TRIM pNm: ${pNm} cache sz: $sz",iN2 | |
List<Map> parse | |
parse=[] | |
if(sz>i1){ | |
JsonSlurper jsonSlurper=new JsonSlurper() | |
parse=convertToInternal((List<Map>)jsonSlurper.parseText(readTmpFLD[pNm])) | |
}else sz=iZ | |
if(isEric())myDetail null,s+" $sz" | |
return ['size': sz, 'data': parse ] | |
}catch(e){ | |
String sensor_name=gtLbl(sensor) | |
String ts1= " for ${sensor_name} (${attribute}) ($filename_}" | |
if( isFNF(e) ){ | |
debug "File DOES NOT Exist"+ts1,null,iN2 | |
}else{ | |
error "Read File Data"+ts1+" :: Exception: ",null,iN2,e | |
} | |
readTmpBFLD[pNm]=null | |
readTmpBFLD= readTmpBFLD | |
} | |
readTmpFLD[pNm]=sNL | |
readTmpFLD= readTmpFLD | |
if(isEric())myDetail null,s | |
return ['size': iZ, 'data': [] ] | |
} | |
String getFileName(sensor, String attribute){ | |
String attr=attribute.replaceAll(sSPC, "_") | |
String sid=gtSensorId(sensor) | |
if(sid==sBLK){ | |
error "getFileName null sid ${sensor} ${attribute}",null,iN2 | |
return sid | |
} | |
return "webCoRE_LTS_${sid}_${attr}.json" | |
} | |
/** receives internal format */ | |
List<Map> pruneData(List<Map> input_data, Integer days){ | |
Boolean isE= isEric() | |
if(isE)myDetail null,"pruneData ${input_data.size()} time: $days",i1 | |
if(days == iZ || !input_data){ | |
if(isE)myDetail null,"pruneData nochange" | |
return input_data | |
} | |
List return_data=[]+input_data | |
if(input_data.size() > 0){ | |
Long startDate; startDate=wnow() | |
startDate -= (days * lMSDAY) | |
Long date | |
date= lMt(input_data[iZ]) | |
while (date && return_data && date < startDate){ | |
if(isE)debug "date: $date startDate: $startDate return_data[0]: ${return_data[iZ]}",null | |
Map a=return_data.remove(0) | |
date=return_data ? lMt(return_data[iZ]) : null | |
} | |
} | |
if(isE)myDetail null,"pruneData ${return_data.size()} time: $days" | |
return return_data | |
} | |
@CompileStatic | |
static List<Map> addData(List<Map> main, List<Map> append){ | |
List<Map> return_data=main | |
append.each{Map data-> | |
return_data << data | |
} | |
return return_data | |
// sort it just in case? | |
//return_data=events ? return_data + events : return_data | |
//return_data=return_data.flatten() as List<Map> | |
} | |
/** returns internal format */ | |
List<Map> getFileData(sensor, String attribute, String fname=sNL){ | |
String s | |
s= "getFileData $sensor $attribute $fname" | |
if(isEric())myDetail null,s,i1 | |
List<Map> parse_data | |
parse_data =[] | |
Map json= readFile(sensor,attribute,fname) | |
if(json?.data){ | |
parse_data = (List<Map>)json.data | |
s += " ${parse_data.size()}" | |
} | |
if(isEric())myDetail null,s | |
return parse_data | |
} | |
/** shared - old LTS only method */ | |
Map quantParams(sensorId, String attr){ | |
String sid=sensorId.toString() | |
String s="${sid}_${attr}".toString() | |
def v | |
v= settings[s+"_quantization"] | |
String quantization_minutes= v!=null ? (String)v : s0 | |
v= settings[s+"_quantization_function"] | |
String quantization_function= v ? (String)v : "average" | |
v= settings[s+"_quantization_decimals"] | |
Integer quantization_decimals= v!=null ? ((String)v).toInteger() : i1 | |
v= settings[s+"_boundary"] | |
Boolean quantization_boundary= v!=null ? ((String)v).toBoolean() : false | |
if(quantization_minutes!=s0 && quantization_function!=sNONE) | |
return [qm: quantization_minutes, qf: quantization_function, qd: quantization_decimals, qb: quantization_boundary] | |
return null | |
} | |
//shared | |
/** LTS only method - internal data format */ | |
List<Map> doQuant(List<Map>data, sensorId, String attr, Boolean toStore){ | |
Map params= quantParams(sensorId,attr) | |
if(params) | |
return quantizeData(data, params.qm , params.qf, params.qd, params.qb, toStore) | |
else | |
return data | |
} | |
/** | |
* Shared Sensor data only - used by graphs and LTS returns all sensor data, trying to go back at least but no more than maxdays | |
* | |
* @param sensor | |
* @param attribute | |
* @param maxdays | |
* @return Internal data format as List<Map> [[date: (Date)date, (sVAL): v, t: (Long)t], ....] & updates LTS file if in use | |
*/ | |
List<Map>getAllDataLimit(sensor,String attribute, Integer maxdays=7){ | |
List<Map> data=getAllData(sensor,attribute,maxdays,true,false) | |
Long gt; gt= wnow() | |
gt -= (maxdays * lMSDAY) | |
List<Map> all_data | |
all_data= data.findAll{ Map it -> lMt(it) > gt} | |
return all_data | |
} | |
/** | |
* Shared- Sensor data only - used by graphs and LTS returns all sensor data, | |
* trying to go back at least mindays (may be more or less than this) | |
* | |
* @param sensor | |
* @param attribute | |
* @param mindays | |
* @param add | |
* @param updateFile - create lts file if it does not exist | |
* @return Internal data format as List<Map> [[date: (Date)date, (sVAL): v, t: (Long)t], ....] | |
*/ | |
List<Map>getAllData(sensor,String attribute, Integer mindays=1461, Boolean add=true, Boolean updateFile=false){ | |
String sid=gtSensorId(sensor) | |
List<Map> all_data; all_data=[] | |
if(isEric())myDetail null,"getAllData $sensor $attribute $sid $mindays $add $updateFile",i1 | |
if(sid!=sBLK){ | |
List<Map> parse_data | |
parse_data=[] | |
Integer sz | |
sz=iN1 | |
Integer st= mindays | |
Date then | |
then=new Date() | |
use (TimeCategory){ | |
then -= st.days | |
} | |
//warn "then is $then",null | |
Boolean lts | |
lts=false | |
if( (gtSetStr(sGRAPHT)==sLONGTS && isStorage(sid,attribute)) || isLtsAvailable(sid,attribute)){ | |
// if(fileExists(sensor,attribute)){ | |
parse_data=getFileData(sensor, attribute) | |
//Get the most Current Data | |
sz=parse_data.size() | |
if(sz) then= dtMdt(parse_data[sz-i1]) | |
lts=true | |
// } | |
} | |
//warn "then NOW is $then",null | |
all_data=parse_data | |
if(add){ | |
List<Map> respEvents=getEvents(sensor: sensor, (sATTR): attribute, start_time: then) | |
if(respEvents){ | |
all_data=addData(parse_data, convertToInternal(respEvents)) | |
} | |
} | |
if(lts && all_data && sz==iZ && updateFile) writeFile(sensor,attribute,all_data) // create file if does not exist | |
if(!all_data && add){ | |
def state_=sensor.currentState(attribute,true) | |
all_data= convertToInternal([[v:state_,t:wnow()]]) | |
} | |
}else | |
error "getAllData ${sensor} (${attribute}) no id",null,iN2 | |
if(isEric())myDetail null,"getAllData ${all_data.size()}" | |
return all_data | |
} | |
/** | |
* LTS only method, sensor data only, requires LTS enabled for sensor/attribute | |
* | |
* @param sensor | |
* @param attribute | |
* @param fname | |
*/ | |
void appendFile_LTS(sensor, String attribute, String fname=sNL){ | |
if(isEric())myDetail null,"appendFile_LTS $sensor $attribute",i1 | |
String attr=attribute.replaceAll(sSPC, "_") | |
String sid=gtSensorId(sensor) | |
if(sid!=sBLK){ | |
Integer storage= gtSetStr("${sid}_${attr}_storage").toInteger() | |
List<Map> write_data | |
write_data=getAllData(sensor,attribute,storage,true,false) | |
try{ | |
if(write_data.size()){ | |
write_data=pruneData(write_data, storage) | |
//write_data=doQuant(write_data, sid, attr, true) | |
writeFile(sensor, attribute, write_data) | |
}else{ | |
String filename_=fname ?: getFileName(sensor, attribute) | |
String sensor_name=gtLbl(sensor) | |
warn "Append File ${sensor_name} (${attribute}) ($filename_) nothing to write",null | |
} | |
}catch(e){ | |
String filename_=fname ?: getFileName(sensor, attribute) | |
String sensor_name=gtLbl(sensor) | |
error "Append File ${sensor_name} (${attribute}) ($filename_) :: Exception: ",null,iN2,e | |
} | |
}else | |
error "Append File ${sensor} (${attribute}) ($fname) no id",null,iN2 | |
if(isEric())myDetail null,"appendFile_LTS" | |
} | |
/** | |
* Shared Returns internal format from from various file formats | |
* @param json | |
* @return Internal data format as List<Map> [[date: (Date)date, (sVAL): v, t: (Long)t], ....] | |
*/ | |
@CompileStatic | |
static List<Map> convertToInternal(List<Map>json){ | |
List<Map> return_data=[] | |
Long t | |
def v | |
for(Map data in json){ | |
t=null | |
if(data.containsKey(sT)){ | |
t= lMt(data) | |
}else if(data[sDT]){ | |
String dateFormat="yyyy-MM-dd'T'HH:mm:ssX" | |
Date date=Date.parse(dateFormat, sMs(data,sDT)) | |
t= date.getTime() | |
}else if(data[sI]){ | |
t= lMs(data,sI) | |
} | |
Date date=new Date(t) | |
v= data.containsKey(sV) ? data[sV] : data[sVAL] | |
v= data.containsKey(sD) ? data[sD] : v | |
if(data.containsKey(sQ)) | |
return_data << [(sDT): date, (sVAL): v, (sT): t, (sQ): data[sQ]] | |
else | |
return_data << [(sDT): date, (sVAL): v, (sT): t] | |
} | |
return return_data | |
} | |
/** | |
* shared (LTS & fuel) only method - convert different formats to file format | |
* @input internal format [[date: (Date)date, (sVAL): v, t: (Long)t]...] | |
* @returns [[ v: v, t: long]...] | |
*/ | |
@CompileStatic | |
static List<Map> rtnFileData(List<Map> events){ | |
List<Map> file_data=[] | |
def v | |
Long t | |
for(Map data in events){ | |
v= data.containsKey(sV) ? data[sV] : data[sVAL] | |
v= data.containsKey(sD) ? data[sD] : v | |
t= data.containsKey(sI) ? lMs(data,sI) : 0L | |
t= data.containsKey(sT) ? lMt(data) : dtMdt(data).getTime() | |
if(data.containsKey(sQ)) | |
file_data << [(sV): v, (sT): t, (sQ):data[sQ]] | |
else | |
file_data << [(sV): v, (sT): t] | |
} | |
return file_data | |
} | |
@Field volatile static Map<String,String> writeTmpFLD=[:] | |
/** shared (LTS & fuel) only method - save different formats to file format */ | |
Boolean writeFile(sensor, String attribute, List<Map> events, String fname=sNL){ | |
String s= "writeFile $sensor $attribute $fname" | |
if(isEric())myDetail null,s,i1 | |
String filename_=fname ?: getFileName(sensor, attribute) | |
String pNm=filename_ | |
List<Map> file_data | |
file_data=rtnFileData(events) | |
writeTmpFLD[pNm]=file_data ? JsonOutput.toJson(file_data) : sBLK | |
file_data=null | |
Boolean fwOk= ((String)location.hub.firmwareVersionString >= minFwVersion) | |
if(fwOk || login()){ | |
if(readTmpFLD[pNm]==sNL){ readTmpFLD[pNm]=sBLK; readTmpFLD= readTmpFLD } | |
Integer sz= readTmpFLD[pNm].size() | |
/* Integer sz1= writeTmpFLD[pNm].size() | |
myDetail null,"pNm: ${pNm} cache sz: $sz new data: ${sz1}",iN2 | |
if(sz){ | |
String sc=readTmpFLD[pNm] | |
String st=sc[sz-1] | |
if(st=='\n') myDetail null, 'FOUND NEWLINE',iN2 | |
myDetail null,"last char CACHE DATA is ${sc[sz-1]}",iN2 | |
myDetail null,"last char NEW DATA is ${writeTmpFLD[pNm][sz1-1]}",iN2 | |
} */ | |
if(sz> 4 && sz==writeTmpFLD[pNm].size() && writeTmpFLD[pNm]==readTmpFLD[pNm]){ | |
writeTmpFLD[pNm]=sBLK; writeTmpFLD= writeTmpFLD | |
if(isEric()) trace "writeFile no changes",null | |
if(isEric())myDetail null,s+" TRUE" | |
return true | |
} | |
try{ | |
Boolean res; res=false | |
if(fwOk){ | |
readTmpBFLD[pNm]= writeTmpFLD[pNm].getBytes() | |
uploadHubFile(filename_, readTmpBFLD[pNm]) | |
readTmpBFLD[pNm]=null | |
readTmpFLD[pNm]=writeTmpFLD[pNm] | |
res=true | |
}else{ | |
Date d=new Date() | |
String encodedString="thebearmay$d".bytes.encodeBase64().toString() | |
Map params=[ | |
uri: "http://127.0.0.1:8080", | |
path: "/hub/fileManager/upload", | |
query: [ "folder": "/" ], | |
headers: [ | |
"Cookie": state.cookie, | |
"Content-Type": "multipart/form-data; boundary=$encodedString" | |
], | |
body: """--${encodedString} | |
Content-Disposition: form-data; name="uploadFile"; filename="${filename_}" | |
Content-Type: "text/plain; charset=UTF-8" | |
${writeTmpFLD[pNm]} | |
--${encodedString} | |
Content-Disposition: form-data; name="folder" | |
--${encodedString}--""", | |
timeout: 300, | |
ignoreSSLIssues: true | |
] | |
httpPost(params){ resp -> | |
if(resp.status!=200){ | |
error "Write Response status $resp.status",null | |
readTmpFLD[pNm]=sNL | |
}else{ | |
readTmpFLD[pNm]=writeTmpFLD[pNm] | |
res=true | |
} | |
} | |
} | |
readTmpFLD= readTmpFLD | |
writeTmpFLD[pNm]=sBLK; writeTmpFLD= writeTmpFLD | |
if(res){ | |
if(isEric())myDetail null,s+" TRUE" | |
return true | |
} | |
}catch(e){ | |
String sensor_name=gtLbl(sensor) | |
error "Write File ${sensor_name} (${attribute}) ($filename_} :: Exception: ",null,iN2,e | |
} | |
readTmpBFLD[pNm]=null | |
readTmpFLD[pNm]=sNL; readTmpFLD= readTmpFLD | |
writeTmpFLD[pNm]=sBLK; writeTmpFLD= writeTmpFLD | |
} | |
if(isEric())myDetail null,s+" FALSE" | |
return false | |
} | |
/** LTS only method */ | |
Map getCurrentDailyStorage(sensor, String attribute, String fname=sNL){ | |
Map json=fileExists(sensor,attribute,fname) ? readFile(sensor, attribute,fname) : null | |
if(json?.data){ | |
List<Map> data=(List<Map>)json.data | |
Integer size= iMs(json,'size') | |
Integer dsz=data.size() | |
Date first | |
Date then | |
if(dsz){ | |
first = new Date( lMt(data[iZ])) | |
then = new Date( lMt(data[dsz-i1])) | |
}else{ | |
first=null | |
then=null | |
} | |
return [num_events: dsz, first: first, last: then, 'size': size] | |
}else{ | |
try{ | |
Integer storage | |
String sid=gtSensorId(sensor) | |
if(sid!=sBLK){ | |
storage= gtSetStr("${sid}_${attribute}_storage").toInteger() | |
storage=storage ?: 30 | |
List<Map> respEvents=getEvents(sensor: sensor, (sATTR): attribute, days: storage) | |
writeFile(sensor, attribute, respEvents) | |
Integer sz= respEvents.size() | |
return [num_events: sz, first: dtMdt(respEvents[iZ]), last: dtMdt(respEvents[sz-i1]), 'size': sz*34] | |
}else | |
error "getCurrentDailyStorage null sid ${sensor} (${attribute}) ($fname)",null,iN2 | |
}catch (e){ | |
error "Error: ",null,iN2,e | |
} | |
} | |
return null | |
} | |
/** fuel stream method */ | |
Map getCurrentDailyStorageFS(){ | |
List<Map> a=getFuelStreamData(null) | |
List<Map> file_data | |
file_data=rtnFileData(a) // we are measure as stored size | |
Integer sz; sz=file_data.toString().size() | |
Map json=['size': sz, 'data': a ] | |
if(json.data){ | |
List<Map> data=(List<Map>)json.data | |
Integer size= iMs(json,'size') | |
Date first=new Date( lMt(data[iZ])) | |
sz= data.size() | |
Date then=new Date( lMt(data[sz-i1])) | |
return [num_events: sz, first: first, last: then, 'size': size] | |
} | |
return null | |
} | |
/* | |
Map getSensor(String str){ | |
List<String> split=str.tokenize('.') | |
def sensor=sensors?.find{ it.id == split[0]} | |
return [ sensor: sensor, attribute: split[1] ] | |
} */ | |
static String convertStorageSize(Integer num){ | |
DecimalFormat df=new DecimalFormat("#0.0") | |
if(num < 1024){ | |
return df.format(num)+" bytes" | |
}else if(num < 1048576){ | |
return df.format(num/1024.0)+" KB" | |
}else{ | |
return df.format(num/1048576.0)+" MB" | |
} | |
} | |
static String round(num){ | |
DecimalFormat df=new DecimalFormat("#0.0") | |
return df.format(num.toString().toDouble()) | |
} | |
/* | |
* TODO: Fuel Stream | |
*/ | |
def mainFuelstream(){ | |
dynamicPage((sNM): "mainPage",(sTIT): "Settings", uninstall: true, install: true){ | |
String uf='useFiles' | |
if( !(gtSetB(uf) && gtStB(uf)) ){ | |
section('Use HE files for data storage'){ | |
input( (sTYPE): sBOOL, (sNM): uf,(sTIT): "Use HE files for fuelstream storage?", | |
(sREQ): false, (sMULTP): false, (sSUBOC): true, (sDEFV): false) | |
} | |
} | |
if(gtSetB(uf) || gtStB(uf)){ | |
String s='hpmSecurity' | |
section('Security'){ | |
if(settings[s]==null){ | |
settings[s]=true | |
app.updateSetting(s, [(sTYPE): sBOOL, (sVAL): sTRUE]) | |
} | |
input( (sTYPE): sBOOL, (sNM): s,(sTIT): "Use Hubitat Security", | |
(sREQ): false, (sMULTP): false, (sSUBOC): true, (sDEFV): true) | |
if(gtSetB(s)){ | |
input "username", "string",(sTIT): "Hub Security username", (sREQ): false, (sSUBOC): true | |
input "password", "password",(sTIT): "Hub Security password", (sREQ): false, (sSUBOC): true | |
} | |
} | |
if(gtSetB(s) && settings.password && !login()){ | |
section('Login Error'){ | |
paragraph("""<b>CANNOT LOGIN</b><br>If you have Hub Security Enabled, please put in correct login credentials<br> If not, please deselect <b>Use Hubitat Security</b>""" ) | |
} | |
} | |
} | |
section('Storage Limits'){ | |
input "maxSize", "number",(sTIT): "Max size of this fuelStream data in KB", (sDEFV): 95 | |
// Maxsize or n days (ie both limits hold) | |
//input "storage_days", "number",(sTIT): "Max # of days of data in this fuelStream", (sDEFV): 1461 | |
storageLimitInput(sNL, sNL,"1461",'storage_days') | |
} | |
section('Logging'){ | |
input sLOGNG,sENUM,(sTIT):'Logging Level',options:[(s0):"None",(s1):"Minimal",(s2):"Medium","3":"Full"],description:'Logging level',defaultValue:state[sLOGNG] ? state[sLOGNG].toString():s0 | |
} | |
List<Map> a | |
a=getFuelStreamDBData(false) | |
state[uf]= gtSetB(uf) && !(a) | |
section('Storage Configuration'){ | |
if(gtSetB(uf)){ | |
String attribute=fuelNattr() | |
def sensor=app | |
Boolean fexists | |
fexists= fileExists(sensor,attribute,fuelName()) | |
if(a){ | |
paragraph("Found DB Storage in use, with use files selected") | |
input( (sTYPE): sBOOL, (sNM): "convertToFile",(sTIT): "Convert to File storage", | |
(sREQ): false, (sMULTP): false, (sSUBOC): true, (sDEFV): false) | |
if(gtSetB('convertToFile')){ | |
if(!fexists){ | |
if(writeFile(sensor, attribute, a,fuelName())){ | |
state.remove('fuelStreamData') | |
info "Converted to file",null | |
fexists=true | |
state[uf]= gtSetB(uf) | |
}else{ | |
error "conversion to file failed",null | |
} | |
}else{ | |
paragraph("Found file exists with DB storage in use") | |
} | |
app.updateSetting("convertToFile", [(sTYPE): sBOOL, (sVAL): sFALSE]) | |
} | |
} | |
} | |
Map storage=getCurrentDailyStorageFS() | |
if(!gtSetB(uf) || !gtStB(uf)){ | |
paragraph("Using HE DB as storage") | |
} | |
if(gtSetB(uf) && gtStB(uf)){ | |
paragraph("Using HE Files as storage") | |
} | |
Integer max=gtSetI('maxSize') ?: 95 | |
paragraph("Storage Limit: ${max}KB") | |
paragraph("Current storage usage is ${convertStorageSize(storage.size)}") | |
Integer storageSize=state.toString().size() | |
paragraph("Current state usage is ${convertStorageSize(storageSize)}") | |
paragraph("Details: ${storage}") | |
} | |
} | |
} | |
/** | |
* methods called by webCoRE parent to operate on streams | |
*/ | |
public void createStream(settings){ | |
fuelFLD=null | |
// fuelstream does not have graphType set | |
state.fuelStream=[(sI): settings.id, (sC): (settings.canister ?: sBLK), (sN): settings.name, w: i1, (sT): getFormattedDate(new Date())] | |
} | |
/** | |
* Called by parent to get list of streams in this app instance | |
* Can be filtered to fuelstreams only, or fuel and LTS. Graphs have no stream | |
* Typical fuelstreams have 1 data set, LTS may have many data sets (each returned as a stream) | |
* @return | |
*/ | |
public List getFuelStreams(Boolean includeLTS){ | |
List<Map> res | |
res = [] | |
if(includeLTS && gtSetStr(sGRAPHT)==sLONGTS){ | |
if(sensors){ | |
for(sensor in (List)sensors){ | |
String sid=gtSensorId(sensor) | |
if(sid==sBLK){ | |
error "getFuelStreams null sid ${sensor}",null,iN2 | |
continue | |
} | |
List<String> att=(List<String>)settings["${sid}_attributes"] | |
if(att){ | |
for(String attribute in att){ | |
//make up stream descriptions | |
String ltsdesc= sid+'_'+attribute | |
String nm= gtLbl(sensor)+'_'+attribute | |
res << [(sI):ltsdesc, (sC): 'LTS', (sN):nm,w:i1,(sT): getFormattedDate(new Date())] | |
} | |
} | |
} | |
} | |
}else{ | |
Map fs=(Map)state.fuelStream | |
if(fs) res << fs | |
} | |
if(isEric())myDetail null,"getFuelStreams $includeLTS $res",iN2 | |
res | |
} | |
/** | |
* fuel stream or LTS only - called by main webCoRE for webCoRE console to get data in stream -> returns webCoRE IDE format | |
*/ | |
public List<Map> listFuelStreamData(String streamid){ | |
if(isEric())myDetail null,"listFuelStreamData $streamid",iN2 | |
// [[ d: itemvalue, i: item.t]] | |
List<Map> ideData=[] | |
List<Map> res | |
// if we are LTS, need to find proper stream based on id | |
if(gtSetStr(sGRAPHT)==sLONGTS){ | |
String[] tname = streamid.split('_') | |
String id =tname[iZ] | |
String attribute= tname[i1] | |
res=null | |
if(sensors && id && attribute){ | |
for(sensor in (List)sensors){ | |
String sid=gtSensorId(sensor) | |
if(sid==sBLK){ | |
error "listFuelStreamData null sid ${sensor}",null,iN2 | |
continue | |
} | |
if(id == sid){ | |
res= getAllData(sensor,attribute,1461,true,false) | |
break | |
} | |
} | |
} | |
}else{ | |
res=getFuelStreamData(null) | |
// //getFuelStreamData().collect{ it + [(sT): getFormattedDate(new Date((Long)it.i))]} | |
} | |
if(res){ | |
for(Map data in res){ | |
def v=data.containsKey(sV) ? data[sV] : data[sVAL] | |
Long t=data.containsKey(sT) ? lMt(data) : dtMdt(data).getTime() | |
//ideData << [ (sD): v, (sI): t, (sT): getFormattedDate(new Date(t))] | |
ideData << [ (sD): v, (sT): t ] | |
} | |
} | |
return ideData | |
} | |
/** fuel stream only - called by pistons to read entire stream, returns internal format */ | |
public List<Map> readFuelStream(Map req){ | |
if(!req)return null | |
if(isEric())myDetail null,"readFuelStream $req",iN2 | |
return getFuelStreamData(req) | |
} | |
/** fuel stream only - called by pistons to overwrite entire stream, input is internal format */ | |
public void writeFuelStream(Map req){ // overwrite | |
if(!req)return | |
if(req.d instanceof List){ | |
if(isEric())myDetail null,"writeFuelStream $req",iN2 | |
storeFuelUpdate((List)req.d,req,true) | |
} | |
} | |
/** fuel stream only - called by pistons to clear fuel stream */ | |
public void clearFuelStream(Map req){ | |
if(!req)return | |
if(isEric())myDetail null,"clearFuelStream $req",iN2 | |
storeFuelUpdate([],req,true) | |
} | |
/** fuel stream only - called by pistons to append data to fuel stream, adds current time to data added */ | |
public void updateFuelStream(Map req){ // append | |
// def canister=req.c ?: sBLK | |
// def name=req.n | |
// def instance=req.i | |
// def data=req.d | |
// def source=req.s | |
if(isEric())myDetail null,"updateFuelStream $req",iN2 | |
if(!req)return | |
List<Map> stream= getFuelStreamData(req) | |
// [[ date: Date, (sVAL): v, t: long]] | |
// old internal format conversion //Boolean a=stream.add([d: req.d, i: wnow()]) | |
Date n= new Date() | |
Boolean a=stream.add([(sVAL): req[sD], (sDT): n, (sT): n.getTime()]) | |
storeFuelUpdate(stream,req) | |
} | |
// Internal methods | |
/** fuel stream only - return file name for this fuel stream */ | |
String fuelName(){ | |
String s= getFSFileName(sF+app.id.toString(),fuelNattr()) | |
if(isEric())myDetail null,"fuelName $s",iN2 | |
return s | |
} | |
/** return cleaned name */ | |
String getFSFileName(String sensorId, String attribute){ | |
String attr=attribute.replaceAll(sSPC, "_") | |
String s= "WebCoRE_Fuel_${sensorId}_${attr}.json" | |
if(isEric())myDetail null,"getFSFileName $s",iN2 | |
return s | |
} | |
/** fuel stream only - return an attribute string for this fuel stream */ | |
@CompileStatic | |
String fuelNattr(){ | |
Map fs=(Map)gtSt("fuelStream") | |
//state.fuelStream=[i: settings.id, c: (settings.canister ?: sBLK), n: settings.name, w: i1, (sT): getFormattedDate(new Date())] | |
String c=sMs(fs,sC) ?: sBLK | |
String n=sMs(fs,sN) | |
Integer i=iMs(fs,sI) | |
String d='_' | |
String attribute=c+d+n+d+i.toString() | |
if(isEric())myDetail null,"fuelNattr $attribute",iN2 | |
return attribute.replaceAll(sSPC, d) | |
} | |
/** fuel stream only - returns internal format read from fuel stream based on storage settings */ | |
public List<Map> getFuelStreamData(Map req,Boolean init=true){ | |
if(isEric())myDetail null,"getFuelStreamData $req $init",iN2 | |
// [[ date: Date, value, v, (sT): long]] | |
if(!gtStB('useFiles')){ | |
return getFuelStreamDBData(init) | |
}else return getFuelStreamFData() | |
} | |
/** | |
* fuel stream only - returns internal format | |
* @param init | |
* @return Internal data format as List<Map> [[date: date, (sVAL): v, t: t], ....] | |
*/ | |
List<Map> getFuelStreamDBData(Boolean init=true){ | |
if(isEric())myDetail null,"getFuelStreamDBData $init",iN2 | |
// [[ date: Date, value, v, t: long]] | |
if(!state.fuelStreamData){ | |
if(init) state.fuelStreamData=[] | |
} | |
return convertToInternal((List)state.fuelStreamData) | |
} | |
/** fuel stream only - returns internal format */ | |
List<Map> getFuelStreamFData(){ | |
// [[ date: Date, (sVAL): v, t: long]] | |
if(isEric())myDetail null,"getFuelStreamFData",iN2 | |
if(gtStB('useFiles')){ | |
String attribute=fuelNattr() | |
def sensor=app | |
List<Map> stream= getFileData(sensor, attribute, fuelName()) | |
List<Map> tstor=(List)state[attribute] ?: [] | |
return stream+tstor | |
}else{ | |
log.warn "file requested for fuelstream and file not enabled" | |
} | |
return null | |
} | |
/** fuel stream only - receives internal format, returns trimmed internal format */ | |
@CompileStatic | |
List<Map> cleanFuelStream(List<Map> istream){ | |
//ensure max size is obeyed | |
List<Map> stream | |
//[date: date, (sVAL): v, t: t] | |
stream=istream | |
if(!stream) return [] | |
String msg | |
msg=sBLK | |
Integer osz=stream.size() | |
//String s="${sid}_${attribute}".toString() s+'_storage' | |
Integer storage=(gtSetting("storage_days") ?: 1461) as Integer | |
msg += "original sz: $osz " | |
List<Map> parse_data=pruneData(stream, storage) | |
stream=parse_data | |
Integer nsz | |
nsz=stream.size() | |
msg += "after maxdays $storage sz1: $nsz " | |
List<Map> tstream | |
tstream= rtnFileData(stream) // need to work with as stored size | |
Double storageSize= tstream.toString().size() / 1024.0D | |
Integer max=(gtSetting('maxSize') ?: 95) as Integer | |
Boolean a | |
if(storageSize.toInteger() > max){ | |
Integer points=stream.size() | |
Double averageSize=points > 0 ? (storageSize/points).toDouble() : 0.0D | |
Integer pointsToRemove | |
pointsToRemove=averageSize > 0 ? ((storageSize - max) / averageSize).toInteger() : 0 | |
pointsToRemove=pointsToRemove > 0 ? pointsToRemove : 0 | |
msg +="size trim to $max: Size ${storageSize}KB Points ${points} Avg $averageSize Remove $pointsToRemove ".toString() | |
List<Map> toBeRemoved=stream.sort{ Map it -> it.t }.take(pointsToRemove) | |
a=stream.removeAll(toBeRemoved) | |
} | |
nsz=stream.size() | |
if(osz!=nsz){ | |
msg += "Trimmed fuel stream, $osz, $nsz" | |
if(msg && isDbg()) debug msg,null | |
} | |
return stream | |
} | |
/** fuel stream only - receives internal format, stores as file format based on fuel stream storage settings */ | |
void storeFuelUpdate(List<Map>istream,Map req,Boolean frc=false){ | |
if(isEric())myDetail null,"storeFuelUpdate ${istream.size()} $req $frc",iN2 | |
Boolean res | |
List<Map>stream | |
stream=cleanFuelStream(istream) | |
//[date: date, (sVAL): v, t: t] | |
stream= rtnFileData(stream) | |
if(!gtStB('useFiles')){ | |
res=storeFuelDBData(stream) | |
}else res=storeFuelFileData(stream,frc) | |
if(!res) warn "storeFuelUpdate failed",null | |
} | |
/** fuel stream only - receives file format, stores in HE DB */ | |
Boolean storeFuelDBData(List<Map>stream){ | |
if(isEric())myDetail null,"storeFuelDBData ${stream.size()}",iN2 | |
if(!gtStB('useFiles')){ | |
state.fuelStreamData=stream | |
return true | |
} | |
return false | |
} | |
/** fuel stream only - receives file format, stores in file */ | |
Boolean storeFuelFileData(List<Map>istream,Boolean frc){ | |
if(isEric())myDetail null,"storeFuelFileData ${istream.size()} $frc",iN2 | |
if(gtStB('useFiles')){ | |
String attribute=fuelNattr() | |
def sensor=app | |
List<Map>stream=istream | |
/* | |
Integer osz=istream.size() | |
List<Map>stream=cleanFuelStream(istream) | |
Integer nsz=stream.size() | |
if(!frc && nsz>0 && osz==nsz){ | |
Long lst=nsz>1 ? (Long)stream[nsz-1].i : 0L | |
Long lst2=nsz> 1 ? (Long)stream[nsz-2].i : 0L | |
if((lst-lst2) < 1800000L){ // 30 mins | |
List<Map> tstor=(List)state[attribute] ?: [] | |
if(tstor.toString().size()<2000 && tstor.size()<20){ | |
Map item=stream.pop() | |
Boolean a= tstor.add(item) | |
state[attribute]=tstor | |
return true | |
} | |
} | |
} */ | |
state[attribute]= [] | |
return writeFile(sensor, attribute, stream, fuelName()) | |
} | |
return false | |
} | |
@CompileStatic | |
static String getFormattedDate(Date date=new Date()){ | |
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") | |
format.setTimeZone(TimeZone.getTimeZone("UTC")) | |
format.format(date) | |
} | |
// TODO: Keep updated0 | |
@CompileStatic | |
static String cleanHtml(String htm){ | |
return htm.replace('\t', sBLK).replace('\n', sBLK).replace(' ', sBLK).replaceAll('> ','>').replaceAll(' >','>') | |
//return htm.replace('\t', sSPC).replace('\n', sSPC).replace(' ', sSPC).replace(' ', sSPC).replace(' ',sSPC).replaceAll('> ','>').replaceAll(' >','>') | |
} | |
// Material-Design-lite | |
// https://getmdl.io | |
def hubiForm_container(List<String> containers, Integer inumPerRow=i1, Boolean save=false){ | |
Integer numPerRow | |
numPerRow=inumPerRow | |
String style | |
if(numPerRow == iZ){ | |
style="""style="margin: 0 !important; padding: 0 !important;""" | |
numPerRow=i1 | |
}else{ | |
style=sBLK | |
} | |
String html_ | |
html_=""" | |
<div class="mdl-grid" style="margin: 0 !important; padding: 0 !important;"> | |
""" | |
containers.each{String container-> | |
html_ += """<div class="mdl-cell mdl-cell--${12/numPerRow}-col-desktop mdl-cell--${8/numPerRow}-col-tablet mdl-cell--${4/numPerRow}-col-phone" ${style}>""" | |
html_ += container | |
html_ += """</div> | |
""" | |
} | |
html_ += """</div> | |
""" | |
if(save) state.saveC=cleanHtml(html_) | |
paragraph cleanHtml(html_) | |
} | |
static String hubiForm_subcontainer(Map map){ | |
List<String> containers=(List<String>)map.objects | |
List<Number> breakdown=(List<Number>)map.breakdown | |
String html_ | |
html_ = | |
""" | |
<div class="mdl-grid" style="margin: 0; padding: 0; "> | |
""" | |
Integer count | |
count=iZ | |
containers.each{String container-> | |
def sz_12=12*breakdown[count] | |
def sz_8=8*breakdown[count] | |
def sz_4=4*breakdown[count] | |
html_ += """ <div class="mdl-cell mdl-cell--${sz_12.intValue()}-col-desktop mdl-cell--${sz_8.intValue()}-col-tablet mdl-cell--${sz_4.intValue()}-col-phone" style= "justify-content: center;" > | |
""" | |
html_ += container | |
html_ += """ | |
</div> | |
""" | |
count++ | |
} | |
html_ += """ | |
</div> | |
""" | |
return cleanHtml(html_) | |
} | |
List hubiForm_help(){ | |
List<String> container; container=[] | |
container << hubiForm_text_input ("Horizontal Axis Format", 'graph_h_format', sBLK, true) | |
if(gtSetStr('graph_h_format')){ | |
Date today=new Date() | |
container << hubiForm_text("""<i><small><b>Horizontal Axis Sample:</b> ${today.format(gtSetStr('graph_h_format'))}</small></i>""") | |
} | |
container << hubiForm_switch ((sTIT): "Show String Formatting Help", (sNM): 'dummy', (sDEFLT): false, (sSUBONCHG): true) | |
if(gtSetB('dummy')){ | |
List<List<String>> rows=[] | |
List<String> header=["<small>Name", "Format", "Result"] | |
rows << ["Year", "Y", "2022"] | |
rows << ["Month Number", "M", "12"] | |
rows << ["Month Name ", "MMM", "Feb"] | |
rows << ["Month Full Name", "MMMM", "February"] | |
rows << ["Day of Month", "d", "February"] | |
rows << ["Day of Week", "EEE", "Mon"] | |
rows << ["Day of Week", "EEEE", "Monday"] | |
rows << ["Period", "a", "AM/PM"] | |
rows << ["Hour (12)", "h", "1..12"] | |
rows << ["Hour (12)", "hh", "01..12"] | |
rows << ["Hour (24)", "H", "0..23"] | |
rows << ["Hour (24)", "HH", "00..23"] | |
rows << ["Minute", "m", "0..59"] | |
rows << ["Minute", "mm", "00..59"] | |
rows << ["Seconds", "s", "0..59"] | |
rows << ["Seconds", "ss", "00..59 </small>"] | |
/* List val=[] | |
val <<"<b>Name"; val << "Format" ; val <<"Result</b>" | |
val <<"<small>Year"; val << "Y"; val << "2022" | |
val <<"Month Number"; val << "M"; val << "12" | |
val <<"Month Name "; val << "MMM"; val << "Feb" | |
val <<"Month Full Name"; val << "MMMM"; val << "February" | |
val <<"Day of Month"; val << "d"; val << "February" | |
val <<"Day of Week"; val << "EEE"; val << "Mon" | |
val <<"Day of Week"; val << "EEEE"; val << "Monday" | |
val <<"Period"; val << "a"; val << "AM/PM" | |
val <<"Hour (12)"; val << "h"; val << "1..12" | |
val <<"Hour (12)"; val << "hh"; val << "01..12" | |
val <<"Hour (24)"; val << "H"; val << "0..23" | |
val <<"Hour (24)"; val << "HH"; val << "00..23" | |
val <<"Minute"; val << "m"; val << "0..59" | |
val <<"Minute"; val << "mm"; val << "00..59" | |
val <<"Seconds"; val << "s"; val << "0..59" | |
val <<"Seconds"; val << "ss"; val << "00..59 </small>" | |
container << hubiForm_cell(val, 3) */ | |
container << hubiForm_table([header: header, rows: rows]) | |
container << hubiForm_text("""<b><small>Example: "EEEE, MMM d, Y hh:mm:ss a" <br>= "Monday, June 6, 2022 08:21:33 AM</small></b>""") | |
} | |
return container | |
} | |
static String hubiForm_table(Map map){ | |
List<String> header=(List<String>)map.header | |
List<List<String>> rows=(List<List<String>>)map.rows | |
List<String> footer=map.footer ? (List<String>)map.footer : [] | |
String html_ | |
html_=""" | |
<table class="mdl-data-table mdl-shadow--2dp dataTable" role="grid" data-upgraded=",MaterialDataTable"> | |
<thead><tr> | |
""" | |
header.each{ String cell-> | |
html_ += """ <th class="mdl-data-table__cell--non-numeric ">${cell}</th>""" | |
} | |
html_ += """ | |
</tr></thead> | |
<tbody> | |
""" | |
//Integer count=0 | |
rows.each{ List<String> row-> | |
html_ += """<tr role="row" class="odd"> | |
""" | |
row.each{ String cell-> | |
html_ += """<td class="mdl-data-table__cell--non-numeric">${cell}</td> | |
""" | |
} | |
html_ += """</tr> | |
""" | |
} //rows | |
html_ += """<tr role="row" class="even"> | |
""" | |
footer.each{ String cell-> | |
html_ += """<td class="mdl-data-table__cell--non-numeric">${cell}</td> | |
""" | |
} | |
html_ += """</tr> | |
""" | |
html_ += """ </tbody></table> | |
""" | |
return cleanHtml(html_) | |
} | |
static String hubiForm_text(String text, String link=null){ | |
String html_ | |
if(link != null){ | |
html_="""<a href="${link}" target="_blank">${text}</a>""" | |
}else{ | |
html_="""${text}""" | |
} | |
return html_ | |
} | |
static String hubiForm_text_format(Map map){ | |
String text=sMs(map,sTEXT) | |
String halign=map.horizontal_align ? "text-align: ${map.horizontal_align};" : sBLK | |
//String valign=map.vertical_align ? "vertical-align: ${map.vertical_align}; " : sBLK | |
String size=map.sz ? "font-size: ${map.sz}px;" : sBLK | |
String html_="""<p style="$halign padding-top:20px; $size">$text</p>""" | |
return cleanHtml(html_) | |
} | |
static def hubiForm_page_button(String title, String page, String width, String icon){ | |
String html_ | |
html_=""" | |
<button type="button" name="_action_href_${page}|${page}|1" class="btn btn-default btn-lg btn-block hrefElem mdl-button--raised mdl-shadow--2dp mdl-button__icon" style="text-align:left;width:${width}; margin: 0;"> | |
<span style="text-align:left;white-space:pre-wrap"> | |
${title} | |
</span> | |
<ul class="nav nav-pills pull-right"> | |
<li><i class="material-icons">${icon}</i></li> | |
</ul> | |
<br> | |
<span class="state-incomplete-text " style="text-align: left; white-space:pre-wrap"></span> | |
</button> | |
""" | |
return cleanHtml(html_) | |
} | |
def hubiForm_section(String title, Integer pos, String icon, String suffix, Closure code){ | |
String id=title.replace(' ', '_').replace('(', sBLK).replace(')',sBLK).replace(':','_') | |
String title_=title.replace("'", "").replace("`", "") | |
String titleHTML=""" | |
<div class="mdl-layout__header" style="display: block; background:#033673; margin: 0 -16px; width: calc(100% + 32px); position: relative; z-index: ${pos}; overflow: visible;"> | |
<div class="mdl-layout__header-row"> | |
<span class="mdl-layout__title" style="margin-left: -32px; font-size: 18px; width: auto;"> | |
${title_} | |
</span> | |
<div class="mdl-layout-spacer"></div> | |
<ul class="nav nav-pills pull-right"> | |
<li> <i class="material-icons">${icon}</i></li> | |
</ul> | |
</div> | |
</div> | |
""" | |
String modContent | |
modContent=""" | |
<div id=${id} style="display: none;"></div> | |
<script> | |
var sectionElem=jQuery('#${id}').parent(); | |
/*hide default header*/ | |
sectionElem.css('display', 'none'); | |
sectionElem.css('z-index', ${pos}); | |
var elem=sectionElem.parent().parent(); | |
elem.addClass('mdl-card mdl-card-wide mdl-shadow--8dp'); | |
elem.css('width', '100%'); | |
elem.css('padding', '0 16px'); | |
elem.css('display', 'block'); | |
elem.css('min-height', 0); | |
elem.css('position', 'relative'); | |
elem.css('z-index', ${pos}); | |
elem.css('overflow', 'visible'); | |
elem.prepend('${titleHTML}'); | |
</script> | |
""" | |
modContent=cleanHtml(modContent) | |
section(modContent, code) | |
} | |
String hubiForm_enum(Map map){ | |
String title=sMs(map,sTIT) | |
String var=sMs(map,sNM) | |
List<String> list=(List<String>)map.list | |
String defaultVal=sMs(map,sDEFLT) | |
Boolean submit_on_change=map.submit_on_change | |
String s; s=gtSetStr(var) | |
if(!s){ | |
app.updateSetting (var, [(sVAL):defaultVal, (sTYPE):sENUM]) | |
settings[var]=defaultVal | |
s=defaultVal | |
} | |
String actualVal=s | |
String submitOnChange=submit_on_change ? "submitOnChange" : sBLK | |
String html_ | |
html_=""" | |
<div class="form-group"> | |
<input type="hidden" name="${var}.type" value=${sENUM}> | |
<input type="hidden" name="${var}.multiple" value="false"> | |
</div> | |
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield" style="" data-upgraded=",MaterialTextfield"> | |
<label for="settings[${var}]" class="control-label"> | |
<b> ${title} </b> | |
</label> | |
<select id="settings[${var}]" name="settings[${var}]" class="selectpicker form-control mdl-switch__input ${submitOnChange} SumoUnder" placeholder="Click to set" data-default="${defaultVal}" tabindex="-1"> | |
<option class="optiondefault" value="" style="display: block;">No selection</option> | |
""" | |
String selectedStringS | |
list.each{ String item -> | |
String selectedString | |
if(actualVal == item){ | |
selectedString = / selected="selected"/ | |
selectedStringS = item | |
}else | |
selectedString=sBLK | |
html_ += """ | |
<option value="${item}"${selectedString}>${item}</option>""" | |
} | |
html_ += """ | |
</select> | |
""" | |
/* | |
html_ += """ | |
<div class="optWrapper"> | |
<ul class="options"> | |
<li class="opt optiondefault"><label>No selection</label></li> | |
""" | |
list.each{ String item -> | |
html_ += actualVal==item ? """<li class="opt selected"><label>${item}</label></li>""" : """<li class="opt"><label>${item}</label></li>""" | |
} | |
html_ += """ | |
</ul> | |
</div> | |
""" | |
*/ | |
html_ += """ | |
</div> | |
""" | |
return cleanHtml(html_) | |
} | |
String hubiForm_switch(Map map){ | |
String title=sMs(map,sTIT) | |
String var=sMs(map,sNM) | |
Boolean defaultVal=map.default | |
Boolean submit_on_change=map.submit_on_change | |
if(settings[var]==null){ | |
app.updateSetting (var, !!defaultVal) | |
settings[var]= !!defaultVal | |
} | |
Boolean actualVal=settings[var] != null ? settings[var] : defaultVal | |
String submitOnChange=submit_on_change ? "submitOnChange" : sBLK | |
String html_=""" | |
<div class="form-group"> | |
<input type="hidden" name="${var}.type" value=${sBOOL}> | |
<input type="hidden" name="${var}.multiple" value="false"> | |
</div> | |
<label for="settings[${var}]" class="mdl-switch mdl-js-switch mdl-js-ripple-effect mdl-js-ripple-effect--ignore-events is-upgraded ${actualVal ? "is-checked" : ""} data-upgraded=",MaterialSwitch,MaterialRipple"> | |
<input name="checkbox[${var}]" id="settings[${var}]" class="mdl-switch__input ${submitOnChange}" type="checkbox" ${actualVal ? "checked" : ""}> | |
<div class="mdl-switch__label" >${title}</div> | |
<div class="mdl-switch__track"></div> | |
<div class="mdl-switch__thumb"> | |
<span class="mdl-switch__focus-helper"> | |
</span> | |
</div> | |
<span class="mdl-switch__ripple-container mdl-js-ripple-effect mdl-ripple--center" data-upgraded=",MaterialRipple"> | |
<span class="mdl-ripple"> | |
</span> | |
</span> | |
</label> | |
<input name="settings[${var}]" type="hidden" value="${actualVal}"> | |
""" | |
return cleanHtml(html_) | |
} | |
String hubiForm_text_input(String title, String ivar, String defaultVal, Boolean submitOnChange){ | |
String var=ivar.toString() | |
String s; s= gtSetStr(var) | |
if(!s){ | |
app.updateSetting(var, defaultVal) | |
settings[var]=defaultVal | |
s=defaultVal | |
} | |
String html_=""" | |
<div class="form-group"> | |
<input type="hidden" name="${var}.type" value="text"> | |
<input type="hidden" name="${var}.multiple" value="false"> | |
</div> | |
<label for="settings[${var}]" class="control-label">${title}</label> | |
<input type="text" name="settings[${var}]" | |
class="mdl-textfield__input ${submitOnChange ? "submitOnChange" : ""} " | |
value="${s}" placeholder="Click to set" id="settings[${var}]"> | |
""" | |
return cleanHtml(html_) | |
} | |
/** | |
* gathers settings input (Integer)varname_font | |
*/ | |
String hubiForm_font_size(Map map){ | |
String title=sMs(map,sTIT) | |
String varname=sMs(map,sNM) | |
Integer default_=iMs(map,sDEFLT) | |
Integer min=iMs(map,sMIN) | |
Integer max=iMs(map,sMAX) | |
Boolean submit_on_change=map.submit_on_change | |
String baseId=varname | |
String varFontSize="${varname}_font" | |
Integer varVal; varVal=gtSetI(varFontSize) | |
if(varVal==null || (min!=null && varVal<min) || (max!=null && varVal>max)){ | |
app.updateSetting(varFontSize, default_) | |
settings[varFontSize]=default_ | |
varVal=default_ | |
} | |
String submitOnChange=submit_on_change ? "submitOnChange" : sBLK | |
String html_ = | |
""" | |
<table style="width:100%"> | |
<tr><td><label for="settings[${varFontSize}]" class="control-label"><b>${title} Font Size</b></td> | |
<td > | |
<span id="${baseId}_font_size_val" style="text-align:right; font-size:${varVal}px">Font Size: ${varVal}</span> | |
</td> | |
</label> | |
</tr> | |
</table> | |
<input type="range" min="$min" max="$max" name="settings[${varFontSize}]" | |
class="mdl-slider $submitOnChange " | |
value="${varVal}" | |
id="settings[${varFontSize}]" | |
onchange="${baseId}_updateFontSize(this.value);"> | |
<div class="form-group"> | |
<input type="hidden" name="${varFontSize}.type" value="number"> | |
<input type="hidden" name="${varFontSize}.multiple" value="false"> | |
</div> | |
<script> | |
function ${baseId}_updateFontSize(val){ | |
var text=""; | |
text += "Font Size: "+val; | |
jQuery('#${baseId}_font_size_val').css("font-size", val+"px"); | |
jQuery('#${baseId}_font_size_val').text(text); | |
} | |
</script> | |
""" | |
return cleanHtml(html_) | |
} | |
/** | |
* gathers settings input (Integer)varname_font | |
*/ | |
String hubiForm_fontvx_size(Map map){ | |
String title=sMs(map,sTIT) | |
String varname=sMs(map,sNM) | |
Integer default_=iMs(map,sDEFLT) | |
Integer min=iMs(map,sMIN) | |
Integer max=iMs(map,sMAX) | |
Boolean submit_on_change=map.submit_on_change | |
String baseId=varname | |
String weight=map.weight ? "font-weight: ${map.weight} !important;" : sBLK | |
String icon | |
icon=sNL | |
String varFontSize="${varname}_font" | |
Integer varVal; varVal= gtSetI(varFontSize) | |
if(varVal==null || (min!=null && varVal<min) || (max!=null && varVal>max)){ | |
app.updateSetting(varFontSize, default_) | |
settings[varFontSize]=default_ | |
varVal=default_ | |
} | |
Integer icon_size= i10*varVal | |
String jq | |
if(map.icon){ | |
icon=""" | |
<style> | |
.material-icons.test{ font-size: ${icon_size}px; } | |
</style> | |
<i id="${baseId}_icon" class="material-icons test">cloud</i> | |
""" | |
jq="""jQuery('.test').css('font-size', 10*val+"px"); | |
""" | |
}else{ | |
jq=""" | |
jQuery('#${baseId}_font_size_val').css("font-size", 0.5*val+"em"); | |
jQuery('#${baseId}_font_size_val').text(text); | |
""" | |
} | |
String submitOnChange=submit_on_change ? "submitOnChange" : sBLK | |
String html_ | |
html_ = | |
""" | |
<label for="settings[${varFontSize}]" class="control-label" style= "vertical-align: bottom;"> | |
<b>${title}</b> | |
<span id="${baseId}_font_size_val" style="float:right; font-size: ${varVal*0.5}em; ${weight}"> | |
${icon == sNL ? varVal : icon} | |
</span> | |
</label> | |
<input type="range" min="$min" max="$max" name="settings[${varFontSize}]" | |
class="mdl-slider $submitOnChange " | |
value="${varVal}" | |
id="settings[${varFontSize}]" | |
onchange="${baseId}_updateFontSize(this.value);"> | |
<div class="form-group"> | |
<input type="hidden" name="${varFontSize}.type" value="number"> | |
<input type="hidden" name="${varFontSize}.multiple" value="false"> | |
</div> | |
<script> | |
function ${baseId}_updateFontSize(val){ | |
var text=""; | |
text += val;""" | |
html_+= jq | |
html_+=""" | |
} | |
</script> | |
""" | |
return cleanHtml(html_) | |
} | |
String hubiForm_line_size(Map map){ | |
String title=sMs(map,sTIT) | |
String varname=sMs(map,sNM) | |
Integer default_=iMs(map,sDEFLT) | |
Integer min=iMs(map,sMIN) | |
Integer max=iMs(map,sMAX) | |
Boolean submit_on_change=map.submit_on_change | |
String baseId=varname | |
String varLineSize="${varname}_line_size" | |
Integer varVal; varVal=gtSetI(varLineSize) | |
if(varVal==null || (min!=null && varVal<min) || (max!=null && varVal>max)){ | |
app.updateSetting(varLineSize, default_) | |
settings[varLineSize]=default_ | |
varVal=default_ | |
} | |
String submitOnChange=submit_on_change ? "submitOnChange" : sBLK | |
String html_ = | |
""" | |
<table style="width:100%"> | |
<tr><td><label for="settings[${varLineSize}]" class="control-label"><b>${title} Width</b></td> | |
<td border=1 style="text-align:right;"> | |
<span id="${baseId}_line_size_text" name="testing" > | |
Width: ${varVal} <hr id='${baseId}_line_size_draw' style='background-color:#1A77C9; height:${varVal}px; border: 0;'> | |
</span> | |
</td> | |
</label> | |
</tr> | |
</table> | |
<input type="range" min="$min" max="$max" name="settings[${varLineSize}]" | |
class="mdl-slider ${submitOnChange}" | |
value="${varVal}" | |
id="settings[${varLineSize}]" | |
onchange="${baseId}_updateLineInput(this.value);"> | |
<div class="form-group"> | |
<input type="hidden" name="${varLineSize}.type" value="number"> | |
<input type="hidden" name="${varLineSize}.multiple" value="false"> | |
</div> | |
<script> | |
function ${baseId}_updateLineInput(val){ | |
var text=""; | |
text += "Width: "+val; | |
jQuery('#${baseId}_line_size_text').text(text); | |
jQuery('#${baseId}_line_size_draw').remove(); | |
jQuery('#${baseId}_line_size_text').after("<hr id='${baseId}_line_size_draw' style='background-color:#1A77C9; height:"+val+"px; border: 0;'>"); | |
} | |
</script> | |
""" | |
return cleanHtml(html_) | |
} | |
String hubiForm_slider(Map map){ | |
String title=sMs(map,sTIT) | |
String varname=sMs(map,sNM) | |
Integer default_=iMs(map,sDEFLT) | |
Integer min=iMs(map,sMIN) | |
Integer max=iMs(map,sMAX) | |
String units=sMs(map,sUNITS) | |
Boolean submit_on_change=map.submit_on_change | |
//def fontSize | |
String varSize=varname | |
String baseId=varname | |
Integer varVal; varVal=gtSetI(varSize) | |
if(varVal==null || (min!=null && varVal<min) || (max!=null && varVal>max)){ | |
settings[varSize]=default_ | |
app.updateSetting(varSize, default_) | |
varVal=default_ | |
} | |
String submitOnChange=submit_on_change ? "submitOnChange" : sBLK | |
String html_ = """ | |
<table style="width:100%"> | |
<tr> | |
<td> | |
<label for="settings[${varSize}]" class="control-label"><b>${title}</b> | |
</td> | |
<td border=1 style="text-align:right;"><span id="${baseId}_slider_val" name="testing" >${varVal}${units}</span></td> | |
</label> | |
</tr> | |
</table> | |
<input type="range" min="$min" max="$max" name="settings[${varSize}]" | |
class="mdl-slider $submitOnChange " | |
value="${varVal}" | |
id="settings[${varSize}]" | |
onchange="${baseId}_updateTextInput(this.value);"> | |
<div class="form-group"> | |
<input type="hidden" name="${varSize}.type" value="number"> | |
<input type="hidden" name="${varSize}.multiple" value="false"> | |
</div> | |
<script> | |
function ${baseId}_updateTextInput(val){ | |
var text=""; | |
text += val+"${units}"; | |
jQuery('#${baseId}_slider_val').text(text); | |
} | |
</script> | |
""" | |
return cleanHtml(html_) | |
} | |
/** | |
* gathers settings input (String)varname_color, (Boolean)varname_color_transparent | |
*/ | |
String hubiForm_color(String title, String varname, String defaultColorValue, Boolean defaultTransparentValue, Boolean submit=false){ | |
String varnameColor="${varname}_color" | |
String varnameTransparent= varnameColor+"_transparent" | |
String colorTitle="<b>${title} Color</b>" | |
String notTransparentTitle="Transparent" | |
String transparentTitle="${title}: Transparent" | |
String curColor; curColor= settings[varnameColor] | |
if(!curColor){ | |
app.updateSetting(varnameColor, defaultColorValue) | |
settings[varnameColor]= defaultColorValue | |
curColor= defaultColorValue | |
} | |
Boolean curTransparent= settings[varnameTransparent]!=null ? gtSetB(varnameTransparent) : defaultTransparentValue | |
if(settings[varnameTransparent]!=curTransparent){ | |
app.updateSetting(varnameTransparent, curTransparent) | |
settings[varnameTransparent]=curTransparent | |
} | |
Boolean isTransparent=curTransparent | |
String html_ = """ | |
<div style="display: flex; flex-flow: row wrap;"> | |
<div style="display: flex; flex-flow: row nowrap; flex-basis: 100%;"> | |
${!isTransparent ? """<label for="settings[${varnameColor}]" class="control-label" style="flex-grow: 1">${colorTitle}</label>""" : """"""} | |
<label for="settings[${varnameTransparent}]" class="control-label" style="width: auto;">${isTransparent ? transparentTitle: notTransparentTitle}</label> | |
</div> | |
${!isTransparent ? """ | |
<div style="flex-grow: 1; flex-basis: 1px; padding-right: 8px;"> | |
<input type="color" name="settings[${varnameColor}]" class="mdl-textfield__input ${submit ? "submitOnChange" : ""} " value="${curColor}" placeholder="Click to set" id="settings[${varnameColor}]" list="presetColors"> | |
<datalist id="presetColors"> | |
<option>#800000</option> | |
<option>#FF0000</option> | |
<option>#FFA500</option> | |
<option>#FFFF00</option> | |
<option>#808000</option> | |
<option>#008000</option> | |
<option>#00FF00</option> | |
<option>#800080</option> | |
<option>#FF00FF</option> | |
<option>#000080</option> | |
<option>#0000FF</option> | |
<option>#00FFFF</option> | |
<option>#FFFFFF</option> | |
<option>#C0C0C0</option> | |
<option>#000000</option> | |
</datalist> | |
</div> | |
""" : ""} | |
<div class="submitOnChange"> | |
<input name="checkbox[${varnameTransparent}]" id="settings[${varnameTransparent}]" style="width: 27.6px; height: 27.6px;" type="checkbox" onmousedown="((e) =>{ jQuery('#${varnameTransparent}').val('${!isTransparent}'); })()" ${isTransparent ? 'checked' : ''} /> | |
<input id="${varnameTransparent}" name="settings[${varnameTransparent}]" type="hidden" value="${isTransparent}" /> | |
</div> | |
<div class="form-group"> | |
<input type="hidden" name="${varnameColor}.type" value="color"> | |
<input type="hidden" name="${varnameColor}.multiple" value="false"> | |
<input type="hidden" name="${varnameTransparent}.type" value=${sBOOL}> | |
<input type="hidden" name="${varnameTransparent}.multiple" value="false"> | |
</div> | |
</div> | |
""" | |
return cleanHtml(html_) | |
} | |
String hubiForm_graph_preview(){ | |
// if(!state.count_) state.count_=7 | |
String html_ = """ | |
<style> | |
.iframe-container{ | |
overflow: hidden; | |
width: 45vmin; | |
height: 45vmin; | |
position: relative; | |
} | |
.iframe-container iframe{ | |
border: 0; | |
left: 0; | |
position: absolute; | |
top: 0; | |
} | |
</style> | |
<div class="iframe-container"> | |
<iframe id="preview_frame" style="width: 100%; height: 100%; position: relative; z-index: 1; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAEq2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIiCiAgIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSIyIgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMiIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMiIKICAgdGlmZjpSZXNvbHV0aW9uVW5pdD0iMiIKICAgdGlmZjpYUmVzb2x1dGlvbj0iNzIuMCIKICAgdGlmZjpZUmVzb2x1dGlvbj0iNzIuMCIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyMC0wNi0wMlQxOTo0NzowNS0wNDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMC0wNi0wMlQxOTo0NzowNS0wNDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjguMyIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMC0wNi0wMlQxOTo0NzowNS0wNDowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+IC4TuwAAAYRpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAACiRdZE7SwNBFEaPiRrxQQQFLSyCRiuVGEG0sUjwBWqRRPDVbDYvIYnLboIEW8E2oCDa+Cr0F2grWAuCoghiZWGtaKOy3k2EBIkzzL2Hb+ZeZr4BWyippoxqD6TSGT0w4XPNLyy6HM/UYqONfroU1dBmguMh/h0fd1RZ+abP6vX/uYqjIRI1VKiqEx5VNT0jPCk8vZbRLN4WblUTSkT4VLhXlwsK31p6uMgvFseL/GWxHgr4wdYs7IqXcbiM1YSeEpaX404ls+rvfayXNEbTc0HJnbI6MAgwgQ8XU4zhZ4gBRiQO0YdXHBoQ7yrXewr1s6xKrSpRI4fOCnESZOgVNSvdo5JjokdlJslZ/v/11YgNeovdG31Q82Sab93g2ILvvGl+Hprm9xHYH+EiXapfPYDhd9HzJc29D84NOLssaeEdON+E9gdN0ZWCZJdli8Xg9QSaFqDlGuqXip797nN8D6F1+aor2N2DHjnvXP4Bhcln9Ef7rWMAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAXSURBVAiZY7hw4cL///8Z////f/HiRQBMEQrfQiLDpgAAAABJRU5ErkJggg=='); background-size: 25px; background-repeat: repeat; image-rendering: pixelated;" src="${makeCallBackURL('graph/')}" data-fullscreen="false" | |
onload="(() =>{ | |
})()""></iframe> | |
</div> | |
""" | |
return cleanHtml(html_) | |
} | |
static String hubiForm_sub_section(String myText=sBLK){ | |
String id=myText.replaceAll("[^a-zA-Z0-9]", sBLK) | |
String newText=myText.replaceAll("'", "").replaceAll("`", "") | |
String html_=""" | |
<div class="mdl-layout__header" style="display: block; min-height: 0;"> | |
<div class="mdl-layout__header-row" style="height: 48px;"> | |
<span class="mdl-layout__title" style="margin-left: -32px; font-size: 9px; width: auto;"> | |
<h4 id="${id}" style="font-size: 16px;">${newText}</h4> | |
</span> | |
</div> | |
</div> | |
""" | |
return cleanHtml(html_) | |
} | |
/* | |
static String hubiForm_cell(List containers, Integer numPerRow){ | |
String html_ | |
html_ = """ | |
<div class="mdl-grid mdl-grid--no-spacing mdl-shadow--4dp" style="margin-top: 0px !important; margin: 0px; padding: 0px 0px;"> | |
""" | |
containers.each{container-> | |
html_ += """ | |
<div class="mdl-cell mdl-cell--${12/numPerRow}-col-desktop mdl-cell--${8/numPerRow}-col-tablet mdl-cell--${4/numPerRow}-col-phone"> | |
""" | |
html_ += container | |
html_ += """ | |
</div> | |
""" | |
} | |
html_ += """ | |
</div> | |
""" | |
return cleanHtml(html_) | |
} */ | |
def hubiForm_list_reorder(String var, String var_color, String solid_background=sBLK){ | |
Boolean result_; result_=null | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
String v=gtSetStr(var) | |
if(v){ | |
List<Map> list_=hubiTools_get_order(v) | |
//Check List | |
result_=hubiTools_check_list(dataSources, list_) | |
} | |
String nres | |
nres=gtSetStr(var) | |
if(!result_ && var){ | |
settings[var]=null | |
nres=sNL | |
wremoveSetting(var) | |
} | |
//build list order | |
//Setup Original Ordering | |
if(nres==sNL){ | |
nres="[" | |
//settings["${var}"]="[" | |
// TODO | |
if(dataSources){ | |
Integer count_; count_=iZ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
//settings["${var}"] += /"attribute_${sid}_${attribute}",/ | |
nres += ((nres.length()>i1 ? ',' : sBLK) + /"attribute_${sid}_${attribute}"/ ) | |
String tvar= "attribute_${sid}_${attribute}_${var_color}_color".toString() | |
if(settings[tvar] == null){ | |
String cl='color' | |
if(solid_background== sBLK){ | |
String c= hubiTools_rotating_colors(count_) | |
settings[tvar]=c | |
settingUpdate(tvar, c, cl) | |
}else{ | |
settings[tvar]=solid_background | |
app.updateSetting(tvar, solid_background, cl) | |
} | |
} | |
count_++ | |
} | |
} | |
//settings["${var}"]=settings["${var}"].substring(0, settings["${var}"].length() - i1) | |
nres== nres.substring(iZ, nres.length() - i1) | |
//settings["${var}"] += "]" | |
nres += "]" | |
settings[var]=nres | |
app.updateSetting(var, nres) | |
} | |
List<Map> list_data=[] | |
List<Map> order_=hubiTools_get_order(nres) | |
String title_ | |
for(Map device_ in order_){ | |
String deviceName_=hubiTools_get_name_from_id(sMs(device_,sID)) | |
title_="""<b>${deviceName_}</b><br><p style="float: right;">${device_[sATTR]}</p>""" | |
title_.replace("'", "").replace("`", "") | |
list_data << [(sTIT): title_, (sVAR): "attribute_${device_[sID]}_${device_[sATTR]}"] | |
} | |
String var_val_=nres.replace('"', '"') | |
String html_ | |
html_=""" | |
<script> | |
function onOrderChange(order){ | |
jQuery("#settings${var}").val(JSON.stringify(order)) | |
} | |
</script> | |
<script src="http://${location.hub.localIP}/local/${isSystemType() ? 'webcore/' : ''}a930f16d-d5f4-4f37-b874-6b0dcfd47ace-HubiGraph.js"></script> | |
<div id="moveable" class="mdl-grid" style="margin: 0; padding: 0; text-color: white !important"> | |
""" | |
for(Map data in list_data){ | |
String color_=settings["${data.var}_${var_color}_color"] | |
String id_="${data.var}" | |
html_ += """<div id="$id_" class="mdl-cell mdl-cell--12-col-desktop mdl-cell--8-col-tablet mdl-cell--4-col-phone mdl-shadow--4dp mdl-color-text--indigo-400" | |
draggable="true" ondragover="dragOver(event)" ondragstart="dragStart(event)" ondragend= "dragEnd(event)" | |
style="font-size: 16px !important; margin: 8px !important; padding: 14px !important;"> | |
<i class="mdl-icon-toggle__label material-icons" style="color: ${color_} !important;">fiber_manual_record</i> | |
""" | |
html_ += sMs(data,sTIT) | |
html_ += """</div> | |
""" | |
} | |
html_ += """</div> | |
<input type="text" id="settings${var}" name="settings[${var}]" value="${var_val_}" style="display: none;" disabled /> | |
<div class="form-group"> | |
<input type="hidden" name="${var}.type" value="text"> | |
<input type="hidden" name="${var}.multiple" value="false"> | |
</div> | |
""" | |
paragraph cleanHtml(html_) | |
} | |
/** Tools */ | |
void hubiTool_create_tile(){ | |
if(isInf()) info "Checking webCoRE Child Tile Device",null,iN2 | |
String dname | |
dname=gtSetStr('device_name') | |
if(!dname){ | |
dname= gtSetStr('app_name') ?: tDesc() | |
dname += ' Tile' | |
} | |
def childDevice | |
childDevice=getChildDevice("webCoRE_${app.id}") | |
if(!childDevice){ | |
if(isDbg()) debug "Creating Device $dname",null,iN2 | |
childDevice=addChildDevice("ady624", "webCoRE Graphs Tile Device", "webCoRE_${app.id}", null,[completedSetup: true, label: dname]) | |
if(childDevice) info "Created HTTP Switch [${childDevice}]",null | |
}else{ | |
if(childDevice.label!=dname){ | |
childDevice.label=dname | |
if(isDbg())debug "Device Label Updated to [${dname}]",null,iN2 | |
} | |
} | |
//Send the html | |
String s= "${makeCallBackURL('graph/')}" | |
childDevice.setGraph(s) | |
if(isDbg())debug "Sent setGraph: ${s}",null,iN2 | |
} | |
void hubiTools_validate_order(List<String> all){ | |
if(isEric())myDetail null,"_validate_order $all",i1 | |
List order | |
order=[] | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
// TODO need to include attribute to make unique | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
String sa="${sid}_${attribute}".toString() | |
String varn='displayOrder_'+sa | |
order << settings[varn] | |
} | |
} | |
if(isEric())myDetail null,"_validate_order $order",iN2 | |
//if we are initialized and need to check | |
if(isEric())myDetail null,"_validate_order ${state.lastOrder}",iN2 | |
if(state.lastOrder && ((List)state.lastOrder)[iZ]){ | |
List remains=all.findAll{ String it -> !order.contains(it) } | |
List dupes=[] | |
order.each{ ord -> | |
if(order.count(ord) > i1) dupes << ord | |
} | |
if(dataSources){ | |
Integer idx; idx=iZ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
String sa="${sid}_${attribute}".toString() | |
String varn='displayOrder_'+sa | |
if(((List)state.lastOrder)[idx] == order[idx] && dupes.contains(settings[varn])){ | |
settings[varn]=remains[iZ] | |
app.updateSetting(varn, [(sVAL): remains[iZ], (sTYPE): sENUM]) | |
remains.removeAt(0) | |
} | |
idx++ | |
} | |
} | |
} | |
//reconstruct order | |
order=[] | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
String sa="${sid}_${attribute}".toString() | |
String varn='displayOrder_'+sa | |
order << settings[varn] | |
} | |
} | |
if(isEric())myDetail null,"_validate_order $order" | |
state.lastOrder=order | |
} | |
static String hubiTools_rotating_colors(Integer c){ | |
String ret=sWHT | |
Integer color=c % 13 | |
switch (color){ | |
case 0: return hubiTools_get_color_code("RED") | |
case 1: return hubiTools_get_color_code("GREEN") | |
case 2: return hubiTools_get_color_code("BLUE") | |
case 3: return hubiTools_get_color_code("MAROON") | |
case 4: return hubiTools_get_color_code("YELLOW") | |
case 5: return hubiTools_get_color_code("OLIVE") | |
case 6: return hubiTools_get_color_code("AQUA") | |
case 7: return hubiTools_get_color_code("LIME") | |
case 8: return hubiTools_get_color_code("NAVY") | |
case 9: return hubiTools_get_color_code("FUCHSIA") | |
case 10: return hubiTools_get_color_code("PURPLE") | |
case 11: return hubiTools_get_color_code("TEAL") | |
case 12: return hubiTools_get_color_code("ORANGE") | |
} | |
return ret | |
} | |
static String hubiTools_get_color_code(String input_color){ | |
String new_color=input_color.toUpperCase() | |
switch (new_color){ | |
case "WHITE" : return sWHT | |
case "SILVER" : return sSILVER | |
case "GRAY" : return "#808080" | |
case "BLACK" : return sBLACK | |
case "RED" : return "#FF0000" | |
case "GREEN" : return "#008000" | |
case "BLUE" : return "#0000FF" | |
case "MAROON" : return "#800000" | |
case "YELLOW" : return "#FFFF00" | |
case "OLIVE" : return "#808000" | |
case "AQUA" : return "#00FFFF" | |
case "LIME" : return "#00FF00" | |
case "NAVY" : return "#000080" | |
case "FUCHSIA" :return "#FF00FF" | |
case "PURPLE" : return "#800080" | |
case "TEAL" : return "#008080" | |
case "ORANGE" : return "#FFA500" | |
} | |
return 'error_color_code' | |
} | |
String hubiTools_get_name_from_id(String id){ //, sensors){ | |
String return_val | |
return_val="Error" | |
// TODO | |
List<Map> dataSources=gtDataSources() | |
if(dataSources){ | |
for(Map ent in dataSources){ | |
if(id == sMs(ent,sID)){ | |
return_val=sMs(ent,sDISPNM) | |
break | |
} | |
} | |
} | |
return return_val | |
} | |
List<Map> hubiTools_get_order(String order){ | |
if(isEric())myDetail(null,"_get_order ${order}",i1) | |
List<String> split_=order.replace('"', sBLK).replace('[', sBLK).replace(']', sBLK).replace("attribute_", sBLK).split(',') | |
List<Map> list_=[] | |
split_.each{ String device-> | |
List<String> sub_=device.split('_') | |
list_ << [(sID): sub_[iZ], (sATTR):sub_[i1]] | |
} | |
if(isEric())myDetail null,"_get_order $order $list_" | |
return list_ | |
} | |
Boolean hubiTools_check_list(List<Map> dataSources, List<Map> list_){ | |
if(isEric())myDetail null,"_check_list $dataSources $list_",i1 | |
Boolean result; result=true | |
Integer count_; count_=iZ | |
Integer sz; sz=list_.size() | |
//check for addition/changes | |
if(dataSources){ | |
Boolean inner_result | |
Integer i | |
for(Map ent in dataSources){ | |
String sid=sMs(ent,sID) | |
String attribute=sMs(ent,sA) | |
count_++ | |
inner_result=false | |
for(i=iZ; i<sz; i++){ | |
if(sMs(list_[i],sID) == sid && sMs(list_[i],sATTR) == attribute){ | |
inner_result=true | |
break | |
} | |
} | |
result=result && inner_result | |
} | |
} | |
//check for smaller | |
Boolean count_result; count_result=false | |
if(sz == count_) | |
count_result=true | |
if(isEric())myDetail null,"_check_list $result $count_result" | |
return (result && count_result) | |
} | |
// TODO: Keep updated | |
@Field static final String sNL=(String)null | |
@Field static final String sSNULL='null' | |
@Field static final String sBOOLN='boolean' | |
@Field static final String sBLK='' | |
@Field static final String sCOMMA=',' | |
@Field static final String sSPC=' ' | |
@Field static final String sNM='name' | |
@Field static final String sID='id' | |
@Field static final String sICON='icon' | |
@Field static final String sREQ='required' | |
@Field static final String sTYPE='type' | |
@Field static final String sTIT='title' | |
@Field static final String sVAL='value' | |
@Field static final String sERROR='error' | |
@Field static final String sINFO='info' | |
@Field static final String sWARN='warn' | |
@Field static final String sTRC='trace' | |
@Field static final String sDBG='debug' | |
@Field static final String sON='on' | |
@Field static final String sOFF='off' | |
@Field static final String sSWITCH='switch' | |
@Field static final String sSTART='start' | |
@Field static final String sEND='end' | |
@Field static final String sTIME='time' | |
@Field static final String s0='0' | |
@Field static final String s1='1' | |
@Field static final String s2='2' | |
@Field static final Integer iZ=0 | |
@Field static final Integer i1=1 | |
@Field static final Integer i2=2 | |
@Field static final Integer i3=3 | |
@Field static final Integer i4=4 | |
@Field static final Integer i5=5 | |
@Field static final Integer i6=6 | |
@Field static final Integer i7=7 | |
@Field static final Integer i8=8 | |
@Field static final Integer i9=9 | |
@Field static final Integer i10=10 | |
@Field static final Integer i12=12 | |
@Field static final Integer i13=13 | |
@Field static final Integer i16=16 | |
@Field static final Integer i20=20 | |
@Field static final Integer i40=40 | |
@Field static final Integer i90=90 | |
@Field static final Integer i100=100 | |
@Field static final Integer i500=500 | |
@Field static final Integer i600=600 | |
@Field static final Integer i800=800 | |
@Field static final Integer i3000=3000 | |
@Field static final Long lZ=0L | |
@Field static final Integer iN1=-1 | |
@Field static final Integer iN2=-2 | |
private static TimeZone mTZ(){ return TimeZone.getDefault() } // (TimeZone)location.timeZone | |
import java.text.SimpleDateFormat | |
import java.util.zip.GZIPOutputStream | |
@CompileStatic | |
static String formatTime(Date t){ | |
return dateTimeFmt(t, "yyyy-MM-dd HH:mm:ss.SSS", true) | |
} | |
@CompileStatic | |
static String dateTimeFmt(Date dt, String fmt, Boolean tzChg=true){ | |
SimpleDateFormat tf = new SimpleDateFormat(fmt) | |
if(tzChg && mTZ()){ tf.setTimeZone(mTZ()) } | |
return tf.format(dt) | |
} | |
/** DEBUG FUNCTIONS */ | |
private Boolean isDbg(){ (Integer)state[sLOGNG]>i2 } | |
private Boolean isTrc(){ (Integer)state[sLOGNG]>i1 } | |
private Boolean isInf(){ (Integer)state[sLOGNG]>iZ } | |
private void myDetail(Map r9,String msg,Integer shift=iN1){ Map a=log(msg,r9,shift,null,sWARN,true,false) } | |
@Field static final String sTMSTMP='timestamp' | |
@Field static final String sDBGLVL='debugLevel' | |
@Field static final String sLOGNG='logging' | |
@Field static final String sLOGS='logs' | |
@Field static final String sTIMER='timer' | |
@Field static final String sENUM='enum' | |
@Field static final String sA='a' | |
@Field static final String sB='b' | |
@Field static final String sC='c' | |
@Field static final String sD='d' | |
@Field static final String sE='e' | |
@Field static final String sF='f' | |
@Field static final String sI='i' | |
@Field static final String sM='m' | |
@Field static final String sN='n' | |
@Field static final String sO='o' | |
@Field static final String sP='p' | |
@Field static final String sQ='q' | |
@Field static final String sS='s' | |
@Field static final String sT='t' | |
@Field static final String sV='v' | |
@Field static final Double d1=1.0D | |
private Map log(message,Map r9,Integer shift=iN2,Exception err=null,String cmd=sNL,Boolean force=false,Boolean svLog=true){ | |
if(cmd==sTIMER){ | |
return [(sM):message.toString(),(sT):wnow(),(sS):shift,(sE):err] | |
} | |
String myMsg | |
Exception merr | |
merr=err | |
Integer mshift | |
mshift=shift | |
if(message instanceof Map){ | |
mshift=iMs(message,sS) | |
merr=(Exception)message[sE] | |
myMsg=sMs(message,sM)+" (${elapseT(lMt(message))}ms)".toString() | |
}else myMsg=message.toString() | |
String mcmd=cmd!=sNL ? cmd:sDBG | |
Integer level | |
level=state[sDBGLVL] ? (Integer)state[sDBGLVL]:iZ | |
//shift is | |
// 0 initialize level,level set to 1 | |
// 1 start of routine,level up | |
// -1 end of routine,level down | |
// anything else: nothing happens | |
// Integer maxLevel=4 | |
String ss='╔' | |
String sb='║' | |
String se='╚' | |
String prefix | |
prefix=sb | |
String prefix2 | |
prefix2=sb | |
// String pad=sBLK //"░" | |
switch(mshift){ | |
case iZ: | |
level=iZ | |
case i1: | |
level+=i1 | |
prefix=se | |
prefix2=ss | |
// pad="═" | |
break | |
case iN1: | |
level-=i1 | |
// pad='═' | |
prefix=ss | |
prefix2=se | |
break | |
} | |
if(level>iZ){ | |
prefix=prefix.padLeft(level+(mshift==iN1 ? i1:iZ),sb) | |
prefix2=prefix2.padLeft(level+(mshift==iN1 ? i1:iZ),sb) | |
} | |
state[sDBGLVL]=level | |
Boolean hasErr=(merr!=null && !!merr) | |
myMsg=myMsg.replaceAll(/(\r\n|\r|\n|\\r\\n|\\r|\\n)+/,"\r") | |
if(myMsg.size()>1024){ | |
myMsg=myMsg[iZ..1023]+'...[TRUNCATED]' | |
} | |
List<String> msgs=!hasErr ? myMsg.tokenize("\r"):[myMsg] | |
if(r9 && r9[sTMSTMP]){ | |
if(svLog && r9[sLOGS] instanceof List){ | |
for(String msg in msgs){ | |
Boolean a=((List)r9[sLOGS]).push([(sO):elapseT(lMs(r9,sTMSTMP)),(sP):prefix2,(sM):msg+(hasErr ? " $merr".toString():sBLK),(sC):mcmd]) | |
} | |
} | |
} | |
String myPad=sSPC | |
if(hasErr) myMsg+="$merr".toString() | |
if((mcmd in [sERROR,sWARN]) || hasErr || force || !svLog || !r9 || bIs(r9,'logsToHE') || isEric())doLog(mcmd, myPad+prefix+sSPC+myMsg) | |
//}else log."$mcmd" myMsg | |
return [:] | |
} | |
void doLog(String mcmd, String msg){ | |
String clr | |
switch(mcmd){ | |
case sINFO: | |
clr= '#0299b1' | |
break | |
case sTRC: | |
clr= sCLRGRY | |
break | |
case sDBG: | |
clr= 'purple' | |
break | |
case sWARN: | |
clr= sCLRORG | |
break | |
case sERROR: | |
default: | |
clr= sCLRRED | |
} | |
String myMsg= msg.replaceAll(sLTH, '<').replaceAll(sGTH, '>') | |
log."$mcmd" span(myMsg,clr) | |
} | |
private void info(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sINFO)} | |
private void trace(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sTRC)} | |
private void debug(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sDBG)} | |
private void warn(message,Map r9,Integer shift=iN2,Exception err=null){ Map a=log(message,r9,shift,err,sWARN)} | |
private void error(message,Map r9,Integer shift=iN2,Exception err=null){ | |
String aa | |
aa=sNL | |
String bb | |
bb=sNL | |
try{ | |
if(err){ | |
aa=getExceptionMessageWithLine(err) | |
bb=getStackTrace(err) | |
} | |
Map a=log(message,r9,shift,err,sERROR) | |
}catch(ignored){} | |
if(aa||bb)log.error tDesc()+" exception: "+aa+" \n"+bb | |
} | |
//error "object: ${describeObject(e)}",r9 | |
@CompileStatic | |
private static Date dtMdt(Map m){ (Date)m[sDT] } | |
@CompileStatic | |
private static Date dtMs(Map m,String s){ (Date)m[s] } | |
@CompileStatic | |
private static Long lMs(Map m,String v){ (Long)m[v] } | |
@CompileStatic | |
private static Long lMt(Map m){ (Long)m[sT] } | |
private Map timer(String message,Map r9,Integer shift=iN2,Exception err=null){ log(message,r9,shift,err,sTIMER)} | |
@Field static final String sLTH='<' | |
@Field static final String sGTH='>' | |
@Field static final String sCLR4D9 = '#2784D9' | |
@Field static final String sCLRRED = 'red' | |
@Field static final String sCLRRED2 = '#cc2d3b' | |
@Field static final String sCLRGRY = 'gray' | |
@Field static final String sCLRGRN = 'green' | |
@Field static final String sCLRGRN2 = '#43d843' | |
@Field static final String sCLRORG = 'orange' | |
@Field static final String sLINEBR = '<br>' | |
static String span(String str,String clr=sNL,String sz=sNL,Boolean bld=false,Boolean br=false){ | |
return str ? "<span ${(clr || sz || bld) ? "style='${clr ? "color: ${clr};":sBLK}${sz ? "font-size: ${sz};":sBLK}${bld ? "font-weight: bold;":sBLK}'":sBLK}>${str}</span>${br ? sLINEBR:sBLK}": sBLK | |
} | |
@CompileStatic | |
private Long elapseT(Long t,Long n=wnow()){ return Math.round(d1*n-t) } | |
@Field static final String sSPCSB7=' │' | |
@Field static final String sSPCSB6=' │' | |
@Field static final String sSPCS6 =' ' | |
@Field static final String sSPCS5 =' ' | |
@Field static final String sSPCST='┌─ ' | |
@Field static final String sSPCSM='├─ ' | |
@Field static final String sSPCSE='└─ ' | |
@Field static final String sNWL='\n' | |
@Field static final String sDBNL='\n\n • ' | |
@CompileStatic | |
static String spanStr(Boolean html,String s){ return html? span(s) : s } | |
@CompileStatic | |
static String doLineStrt(Integer level,List<Boolean>newLevel){ | |
String lineStrt; lineStrt=sNWL | |
Boolean dB; dB=false | |
Integer i | |
for(i=iZ;i<level;i++){ | |
if(i+i1<level){ | |
if(!newLevel[i]){ | |
if(!dB){ lineStrt+=sSPCSB7; dB=true } | |
else lineStrt+=sSPCSB6 | |
}else lineStrt+= !dB ? sSPCS6:sSPCS5 | |
}else lineStrt+= !dB ? sSPCS6:sSPCS5 | |
} | |
return lineStrt | |
} | |
@CompileStatic | |
static String dumpListDesc(List data,Integer level,List<Boolean> lastLevel,String listLabel,Boolean html=false,Boolean reorder=true){ | |
String str; str=sBLK | |
Integer cnt; cnt=i1 | |
List<Boolean> newLevel=lastLevel | |
List list1=data?.collect{it} | |
Integer sz=list1.size() | |
for(Object par in list1){ | |
String lbl=listLabel+"[${cnt-i1}]".toString() | |
if(par instanceof Map){ | |
Map newmap=[:] | |
newmap[lbl]=(Map)par | |
Boolean t1=cnt==sz | |
newLevel[level]=t1 | |
str+=dumpMapDesc(newmap,level,newLevel,cnt,sz,!t1,html,reorder) | |
}else if(par instanceof List || par instanceof ArrayList){ | |
Map newmap=[:] | |
newmap[lbl]=par | |
Boolean t1=cnt==sz | |
newLevel[level]=t1 | |
str+=dumpMapDesc(newmap,level,newLevel,cnt,sz,!t1,html,reorder) | |
}else{ | |
String lineStrt | |
lineStrt=doLineStrt(level,lastLevel) | |
lineStrt+=cnt==i1 && sz>i1 ? sSPCST:(cnt<sz ? sSPCSM:sSPCSE) | |
str+=spanStr(html, lineStrt+lbl+": ${par} (${objType(par)})".toString() ) | |
} | |
cnt+=i1 | |
} | |
return str | |
} | |
@CompileStatic | |
static String dumpMapDesc(Map data,Integer level,List<Boolean> lastLevel,Integer listCnt=null,Integer listSz=null,Boolean listCall=false,Boolean html=false,Boolean reorder=true){ | |
String str; str=sBLK | |
Integer cnt; cnt=i1 | |
Integer sz=data?.size() | |
Map svMap,svLMap,newMap; svMap=[:]; svLMap=[:]; newMap=[:] | |
for(par in data){ | |
String k=(String)par.key | |
def v=par.value | |
if(reorder && v instanceof Map){ | |
svMap+=[(k): v] | |
}else if(reorder && (v instanceof List || v instanceof ArrayList)){ | |
svLMap+=[(k): v] | |
}else newMap+=[(k):v] | |
} | |
newMap+=svMap+svLMap | |
Integer lvlpls=level+i1 | |
for(par in newMap){ | |
String lineStrt | |
List<Boolean> newLevel=lastLevel | |
Boolean thisIsLast=cnt==sz && !listCall | |
if(level>iZ)newLevel[(level-i1)]=thisIsLast | |
Boolean theLast | |
theLast=thisIsLast | |
if(level==iZ)lineStrt=sDBNL | |
else{ | |
theLast=theLast && thisIsLast | |
lineStrt=doLineStrt(level,newLevel) | |
if(listSz && listCnt && listCall)lineStrt+=listCnt==i1 && listSz>i1 ? sSPCST:(listCnt<listSz ? sSPCSM:sSPCSE) | |
else lineStrt+=((cnt<sz || listCall) && !thisIsLast) ? sSPCSM:sSPCSE | |
} | |
String k=(String)par.key | |
def v=par.value | |
String objType=objType(v) | |
if(v instanceof Map){ | |
str+=spanStr(html, lineStrt+"${k}: (${objType})".toString() ) | |
newLevel[lvlpls]=theLast | |
str+=dumpMapDesc((Map)v,lvlpls,newLevel,null,null,false,html,reorder) | |
} | |
else if(v instanceof List || v instanceof ArrayList){ | |
str+=spanStr(html, lineStrt+"${k}: [${objType}]".toString() ) | |
newLevel[lvlpls]=theLast | |
str+=dumpListDesc((List)v,lvlpls,newLevel,sBLK,html,reorder) | |
} | |
else{ | |
str+=spanStr(html, lineStrt+"${k}: (${v}) (${objType})".toString() ) | |
} | |
cnt+=i1 | |
} | |
return str | |
} | |
@CompileStatic | |
static String objType(obj){ return span(myObj(obj),sCLRORG) } | |
@CompileStatic | |
static String getMapDescStr(Map data,Boolean reorder=true){ | |
List<Boolean> lastLevel=[true] | |
String str=dumpMapDesc(data,iZ,lastLevel,null,null,false,true,reorder) | |
return str!=sBLK ? str:'No Data was returned' | |
} | |
private static String sectionTitleStr(String title) { return '<h3>'+title+'</h3>' } | |
private static String inputTitleStr(String title) { return '<u>'+title+'</u>' } | |
//private static String pageTitleStr(String title) { return '<h1>'+title+'</h1>' } | |
//private static String paraTitleStr(String title) { return '<b>'+title+'</b>' } | |
@Field static final String sGITP='https://cdn.jsdelivr.net/gh/imnotbob/webCoRE@hubitat-patches/resources/icons/' | |
private static String gimg(String imgSrc){ return sGITP+imgSrc } | |
@CompileStatic | |
private static String imgTitle(String imgSrc,String titleStr,String color=sNL,Integer imgWidth=30,Integer imgHeight=iZ){ | |
String imgStyle | |
imgStyle=sBLK | |
String myImgSrc=gimg(imgSrc) | |
imgStyle+=imgWidth>iZ ? 'width: '+imgWidth.toString()+'px !important;':sBLK | |
imgStyle+=imgHeight>iZ ? imgWidth!=iZ ? sSPC:sBLK+'height:'+imgHeight.toString()+'px !important;':sBLK | |
if(color!=sNL) return """<div style="color: ${color}; font-weight:bold;"><img style="${imgStyle}" src="${myImgSrc}"> ${titleStr}</img></div>""".toString() | |
else return """<img style="${imgStyle}" src="${myImgSrc}"> ${titleStr}</img>""".toString() | |
} | |
static String myObj(obj){ | |
if(obj instanceof String)return 'String' | |
else if(obj instanceof Map)return 'Map' | |
else if(obj instanceof List)return 'List' | |
else if(obj instanceof ArrayList)return 'ArrayList' | |
else if(obj instanceof BigInteger)return 'BigInt' | |
else if(obj instanceof Long)return 'Long' | |
else if(obj instanceof Integer)return 'Int' | |
else if(obj instanceof Boolean)return 'Bool' | |
else if(obj instanceof BigDecimal)return 'BigDec' | |
else if(obj instanceof Double)return 'Double' | |
else if(obj instanceof Float)return 'Float' | |
else if(obj instanceof Byte)return 'Byte' | |
// else if(obj instanceof com.hubitat.app.DeviceWrapper)return 'Device' | |
else return 'unknown' | |
} | |
@Field volatile static Map<String,Long> lockTimesVFLD=[:] | |
@Field volatile static Map<String,String> lockHolderVFLD=[:] | |
@CompileStatic | |
void getTheLock(String qname,String meth=sNL,Boolean longWait=false){ | |
Boolean a=getTheLockW(qname,meth,longWait) | |
} | |
@Field static final Long lTHOUS=1000L | |
@CompileStatic | |
Boolean getTheLockW(String qname,String meth=sNL,Boolean longWait=false){ | |
Long waitT=longWait? lTHOUS:10L | |
Boolean wait | |
wait=false | |
Integer semaNum=semaNum(qname) | |
String semaSNum=semaNum.toString() | |
Semaphore sema=sema(semaNum) | |
while(!sema.tryAcquire()){ | |
// did not get lock | |
Long t | |
t=lockTimesVFLD[semaSNum] | |
if(t==null){ | |
t=wnow() | |
lockTimesVFLD[semaSNum]=t | |
lockTimesVFLD=lockTimesVFLD | |
} | |
if(isEric())warn "waiting for ${qname} ${semaSNum} lock access, $meth, long: $longWait, holder: ${lockHolderVFLD[semaSNum]}",null | |
wpauseExecution(waitT) | |
wait=true | |
if(elapseT(t)>30000L){ | |
releaseTheLock(qname) | |
if(isEric())warn "overriding lock $meth",null | |
} | |
} | |
lockTimesVFLD[semaSNum]=wnow() | |
lockTimesVFLD=lockTimesVFLD | |
lockHolderVFLD[semaSNum]=sAppId()+sSPC+meth | |
lockHolderVFLD=lockHolderVFLD | |
return wait | |
} | |
@CompileStatic | |
static void releaseTheLock(String qname){ | |
Integer semaNum=semaNum(qname) | |
String semaSNum=semaNum.toString() | |
Semaphore sema=sema(semaNum) | |
lockTimesVFLD[semaSNum]=(Long)null | |
lockTimesVFLD=lockTimesVFLD | |
// lockHolderVFLD[semaSNum]=sNL | |
// lockHolderVFLD=lockHolderVFLD | |
sema.release() | |
} | |
void clearSema(){ | |
String pNm=sAppId() | |
getTheLock(pNm,'updated') | |
theSemaphoresVFLD[pNm]=lZ | |
theSemaphoresVFLD=theSemaphoresVFLD | |
theQueuesVFLD[pNm]=[] | |
theQueuesVFLD=theQueuesVFLD // forces volatile cache flush | |
releaseTheLock(pNm) | |
} | |
@Field static Semaphore theLock0FLD=new Semaphore(1) | |
@Field static final Integer iStripes=1 | |
@CompileStatic | |
static Integer semaNum(String name){ | |
if(name.isNumber())return name.toInteger()%iStripes | |
Integer hash=smear(name.hashCode()) | |
return Math.abs(hash)%iStripes | |
} | |
@CompileStatic | |
static Semaphore sema(Integer snum){ | |
switch(snum){ | |
case 0: return theLock0FLD | |
default: //log.error "bad hash result $snum" | |
return null | |
} | |
} | |
private static Integer smear(Integer hashC){ | |
Integer hashCode | |
hashCode=hashC | |
hashCode ^= (hashCode >>> i20) ^ (hashCode >>> i12) | |
return hashCode ^ (hashCode >>> i7) ^ (hashCode >>> i4) | |
} | |
@Field volatile static Map<String,List<Map>> theQueuesVFLD=[:] | |
@Field volatile static Map<String,Long> theSemaphoresVFLD=[:] | |
// This can queue event | |
@CompileStatic | |
private Map queueSemaphore(Map event){ | |
Long tt1 | |
tt1=wnow() | |
Long startTime | |
startTime=tt1 | |
Long r_semaphore | |
r_semaphore=lZ | |
Long semaphoreDelay | |
semaphoreDelay=lZ | |
String semaphoreName | |
semaphoreName=sNL | |
Boolean didQ | |
didQ=false | |
Boolean waited | |
String mSmaNm=sAppId() | |
waited=getTheLockW(mSmaNm,'queue') | |
tt1=wnow() | |
Long lastSemaphore | |
Boolean clrC | |
clrC=false | |
Integer qsize | |
qsize=iZ | |
while(true){ | |
Long t0=theSemaphoresVFLD[mSmaNm] | |
Long tt0=t0!=null ? t0:lZ | |
lastSemaphore=tt0 | |
if(lastSemaphore==lZ || tt1-lastSemaphore>100000L){ | |
theSemaphoresVFLD[mSmaNm]=tt1 | |
theSemaphoresVFLD=theSemaphoresVFLD | |
semaphoreName=mSmaNm | |
semaphoreDelay=waited ? tt1-startTime:lZ | |
r_semaphore=tt1 | |
break | |
} | |
if(event!=null){ | |
Map mEvt=event | |
List<Map> evtQ | |
evtQ=theQueuesVFLD[mSmaNm] | |
evtQ=evtQ!=null ? evtQ:(List<Map>)[] | |
qsize=evtQ.size() | |
if(qsize>i12)clrC=true | |
else{ | |
Boolean a=evtQ.push(mEvt) | |
theQueuesVFLD[mSmaNm]=evtQ | |
theQueuesVFLD=theQueuesVFLD | |
didQ=true | |
} | |
} | |
break | |
} | |
releaseTheLock(mSmaNm) | |
if(clrC){ | |
error "large queue size ${qsize} clearing",null | |
//clear1(true,true,true,true) | |
} | |
return [ | |
semaphore:r_semaphore, | |
semaphoreName:semaphoreName, | |
semaphoreDelay:semaphoreDelay, | |
waited:waited, | |
exitOut:didQ | |
] | |
} | |
@Field static final String sAE='Accept-encoding' | |
@Field static final String sCE='Content-Encoding' | |
@Field static final String sGZIP='gzip' | |
@Field static final String sDATA='data' | |
@Field static final String sUTF8='UTF-8' | |
private Map wrender(Map options=[:]){ | |
//debug "wrender: options:: ${options} " | |
//debug "request: ${request} " | |
Map h=(Map)request?.headers | |
if(h && sMs(h,sAE)?.contains(sGZIP)){ | |
// debug "will accept gzip" | |
String s=sMs(options,sDATA) | |
Integer sz=s?.length() | |
if(sz>256){ | |
try{ | |
String a= string2gzip(s) | |
Integer nsz=a.size() | |
if(eric1())debug "options.data is $sz after compression $nsz saving ${Math.round((d1-(nsz/sz))*1000.0D)/10.0D}%",null | |
// options[sDATA]=a | |
// options[sCE]=sGZIP | |
}catch(ignored){} | |
} | |
} | |
render(options) | |
} | |
static String string2gzip(String s){ | |
ByteArrayOutputStream baos= new ByteArrayOutputStream() | |
GZIPOutputStream zipStream= new GZIPOutputStream(baos) | |
zipStream.write(s.getBytes(sUTF8)) | |
zipStream.close() | |
byte[] result= baos.toByteArray() | |
baos.close() | |
return result.encodeBase64() | |
} | |
private String makeCallBackURL(String path){ | |
return "${getEndpointURL()}${path}?access_token=${getEndpointSecret()}".toString() | |
} | |
private String getEndpointURL(){ | |
//state.remoteEndpointURL will give cloud endpoint | |
// but still need to be on local network due to js/css files on hub that are referenced | |
String ep | |
ep= useRemote() ? "${state.remoteEndpointURL}".toString() : "${state.localEndpointURL}".toString() | |
//if(!ep.contains('https') && ep.contains('http:')){ | |
//ep= ep.replace('http:', 'https:') | |
//} | |
return ep | |
} | |
private String getEndpointSecret(){ return "${state.endpointSecret}".toString() } | |
private Long wnow(){ return (Long)now() } | |
private Date wtoDateTime(String s){ return (Date)toDateTime(s) } | |
private String sAppId(){ return ((Long)app.id).toString() } | |
private void wpauseExecution(Long t){ pauseExecution(t) } | |
private void wremoveSetting(String s){ app.removeSetting(s) } | |
void settingUpdate(String name, value, String type=sNL){ | |
if(name && type){ app?.updateSetting(name, [(sTYPE): type, (sVAL): value]) } | |
else if(name && type == sNL){ app?.updateSetting(name, value) } | |
} | |
private gtSetting(String nm){ return settings."${nm}" } | |
private String gtSetStr(String nm){ return (String)settings[nm] } | |
private Boolean gtSetB(String nm){ return (Boolean)settings[nm] } | |
private Integer gtSetI(String nm){ return (Integer)settings[nm] } | |
private Boolean gtStB(String nm){ return (Boolean)state[nm] } | |
private gtSt(String nm){ return state.get(nm) } | |
private gtAS(String nm){ return atomicState.get(nm) } | |
/** assign to state */ | |
private void assignSt(String nm,v){ state."${nm}"=v } | |
/** assign to atomicState */ | |
private void assignAS(String nm,v){ atomicState."${nm}"=v } | |
private Map gtState(){ return state } | |
private gtLocation(){ return location } | |
//******************************************************************* | |
// CLONE CHILD LOGIC | |
//******************************************************************* | |
public Map getSettingsAndStateMap(){ | |
Map<String,Map> setObjs = [:] | |
def vv | |
String sk,typ | |
((Map<String,Object>)settings).keySet().each{ String theKey-> | |
sk= theKey | |
typ=getSettingType(sk) | |
vv= settings[sk] | |
if(setObjs[sk]!=null) warn "overwriting ${setObjs[sk]} with ${typ}",null | |
if(typ==sTIME) | |
vv= dateTimeFmt(wtoDateTime((String)vv), "HH:mm") | |
if(typ.startsWith('capability')){ | |
typ= 'capability' | |
vv= vv instanceof List ? ((List)vv)?.collect{ it?.id?.toString() } : vv?.id?.toString() | |
} | |
if(typ=='device') | |
vv= vv instanceof List ? ((List)vv)?.collect{ it?.id?.toString() } : vv?.id?.toString() | |
setObjs[sk]= [(sTYPE): typ, (sVAL): vv] | |
} | |
Map data= [:] | |
String newlbl= app?.getLabel()?.toString() //?.replace(" (A ${sPAUSESymFLD})", sBLK) | |
data.label= newlbl?.replace(" (A)", sBLK) | |
List<String> setSkip=['install_device','device_name'] | |
data.settings= setObjs.findAll{ !(it.key in setSkip) } | |
List<String> stateSkip= [ | |
/* "isInstalled", "isParent", */ | |
"accessToken", "debugLevel", "endpoint", "localEndpoint", "endpointSecret", "localEndpointURL", "remoteEndpointURL", | |
"dupPendingSetup", "dupOpenedByUser" | |
] | |
data.state= ((Map<String,Object>)state)?.findAll{ !((String)it?.key in stateSkip) } | |
return data | |
} | |
private String gtLbl(d){ return "${d?.label ?: d?.name}".toString() } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment