/* * CSV Visualization * Description: Demostration of utilizing ChartJS to create graphs using CSV files from the Device Attribute Iterative Storage app * * Licensed Virtual 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, WIyTHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Date Who Description * ------------- ------------------- --------------------------------------------------------- * 08Jul2024 thebearmay v0.0.2 - Remove button if chart.js already in File Manager * 13Jul2024 v0.0.3 - Handle non-long timestamp * 15Jul2024 v0.0.4 - handle the device column, if present * 16Jul2024 v0.0.5 - Allow multiple CSVs */ static String version() { return '0.0.5' } import groovy.json.JsonSlurper import groovy.json.JsonOutput import groovy.transform.Field definition ( name: "CSV Visualization", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Demonstraion app to show how ChartJS could be used to create graphs using CSV files from Device Attribute Iterative Storage app", category: "Utility", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/apps/csvVisual.groovy", installOnOpen: true, oauth: true, iconUrl: "", iconX2Url: "" ) preferences { page name: "mainPage" page name: "pageRender" } mappings { path("/refresh") { action: [POST: "refresh", GET: "refresh"] } } def installed() { // log.trace "installed()" state?.isInstalled = true initialize() } def updated(){ // log.trace "updated()" if(!state?.isInstalled) { state?.isInstalled = true } if(debugEnable) runIn(1800,logsOff) } def initialize(){ } void logsOff(){ app.updateSetting("debugEnabled",[value:"false",type:"bool"]) } def mainPage(){ dynamicPage (name: "mainPage", title: "", install: true, uninstall: true) { if (app.getInstallationState() == 'COMPLETE') { section(name:"lhi",title:"API Information", hideable: true, hidden: true){ paragraph "Local Server API: ${getFullLocalApiServerUrl()}/refresh?access_token=${state.accessToken}" paragraph "Cloud Server API: ${getFullApiServerUrl()}/refresh?access_token=${state.accessToken}" if(state.accessToken == null) createAccessToken() paragraph "Access Token: ${state.accessToken}" } section(name:"visualData",title:"Meta Data", hideable: false, hidden: false){ fileList = getFiles() state.jsInstalled = false csvList = [] fileList.each { if("$it" == "chart.js") state.jsInstalled = true else if("$it".contains('.csv')) csvList.add("$it") } if(!state?.jsInstalled) input("jsInstall","button", title:"Install ChartJS", width:4) input("csvFile","enum", title:"Name of Local CSV File for Visualization", options:csvList, width: 4, submitOnChange:true, multiple:true) input("chartType","enum", title:"Type of Chart to Render", width: 4, options: cOptions, submitOnChange:true) href("pageRender", title:"Render Visualization", width:4) } section("Change Application Name", hideable: true, hidden: true){ input "nameOverride", "text", title: "New Name for Application", multiple: false, required: false, submitOnChange: true, defaultValue: app.getLabel() if(nameOverride != app.getLabel()) app.updateLabel(nameOverride) } } else { section("") { paragraph title: "Click Done", "Please click Done to install app before continuing" } } } } def pageRender(){ dynamicPage (name: "pageRender", title: "", install: false, uninstall: false) { section(name:"visualDisp",title:"", hideable: false, hidden: false){ paragraph "${buildPage()}" } } } ArrayList getFiles(){ fileList =[] params = [ uri : "http://127.0.0.1:8080", path : "/hub/fileManager/json", headers: [ accept : "application/json" ], ] httpGet(params) { resp -> resp.data.files.each { fileList.add(it.name) } } return fileList.sort() } String toCamelCase(init) { if (init == null) return null; init = init.replaceAll("[^a-zA-Z0-9]+","") String ret = "" List word = init.split(" ") if(word.size == 1) return init word.each{ ret+=Character.toUpperCase(it.charAt(0)) ret+=it.substring(1).toLowerCase() } ret="${Character.toLowerCase(ret.charAt(0))}${ret.substring(1)}" if(debugEnabled) log.debug "toCamelCase return $ret" return ret; } def appButtonHandler(btn) { switch(btn) { case "jsInstall": fetchJS() state.jsInstalled = true break default: log.error "Undefined button $btn pushed" break } } HashMap jsonResponse(retMap){ return JsonOutput.toJson(retMap) } ArrayList csvCombine() { csvArr = [[]] fArray = [] numCols = [] tsCol = [] i=0 csvFile.each{ fArray[i] = (new String (downloadHubFile("${csvFile[i]}"))).split("\n") cols=fArray[i][0].split(",") numCols[i] = cols.size() j=0 cols.each{ if("$it" == '\"timeStamp\"') tsCol[i] = j j++ } i++ } fInx = 0 narrInx = 0 fArray.each { files -> rInx = 0 files.each { records -> cols=records.split(',') cInx=0 cols.each{ if(cInx >= tsCol[fInx]){ if(debugEnabled) log.debug "$fInx:$rInx:$cInx $it" if(rInx == 0) { //Header Row if (cInx >= tsCol[fInx] && !(fInx > 0 && "$it".contains('timeStamp'))) { if(debugEnabled) log.debug "Header Before: ${csvArr[0]}" csvArr[0].add("$it") if(debugEnabled) log.debug "Header After: ${csvArr[0]}" } } else { if(debugEnabled) log.debug "cInx: $cInx cols:${numCols[fInx]}" if(cInx == tsCol[fInx]){ if(debugEnabled) log.debug "ts col" csvArr[narrInx]=[] csvArr[narrInx][0]="$it" if(debugEnabled) log.debug "$fInx Before" for(i=0;i tsCol[fInx] && cInx < numCols[fInx]-1){ if(debugEnabled)log.debug "middle" if(!csvArr[narrInx]) csvArr[narrInx] = [] csvArr[narrInx].add("$it") } else if(cInx == numCols[fInx]-1){ if(debugEnabled)log.debug "end" csvArr[narrInx].add("$it") if(debugEnabled) log.debug "$fInx After" for(i=fInx;i 0 || fInx == 0) narrInx++ rInx++ } fInx++ } if (debugEnabled) uploadHubFile ("csvCombWork.txt",csvArr.toString().getBytes("UTF-8")) csvWork = "${csvArr[0].toString().replace('[','').replace(']','')}\n" csvArr.remove(0) // csvArr = dSort(csvArr) /// replace nulls logic rInx = 0 csvArr.each{ row -> cInx = 0 row.each { col -> if("$col" == null || "$col" == ""){ if(debugEnabled) log.debug "null found $rInx $cInx" if (rInx == 0){ for(i=rInx+1;i 0){ minInx = -1 minLst = Long.MAX_VALUE for(i=0;i 0 && i == tsCol){ dataSet[i].add("\"${new Date(it.toLong()).toString()}\"") } else { dataSet[i].add(it) } } catch (e) { /* if(it != null) if(!dataSet[i]) dataSet[i]=[] dataSet[i].add("\"$it\"") */ } } i++ } r++ } if(devCol > -1) dataSet.remove(devCol) if(debugEnabled) uploadHubFile ("csvWork.txt",dataSet.toString().getBytes("UTF-8")) return dataSet } String buildPage(){ if(csvFile.size == 1) cols = csvParse("${csvFile[0]}") else { cols = csvCombine() deleteHubFile("csvWork${app.id}.txt") } labelData = [] lCol=-1 lr=0 cols.each { if(it[0] == "timeStamp"){ lCol = lr j=0 it.each{ if(j>0) labelData.add("$it") j++ } } lr++ } if(!labelData) labelData="[]" i=0 k=-1 dataPart="" cols.each{ if(k > 12) k=0 if(i != lCol) { cData = [] j=0 dLabel = it[0] it.each{ if(j> 0){ cData.add("$it") } j++ } if(i > 1) dataPart+=",\n" dataPart+= """ { label: "$dLabel", data: $cData, borderWidth: 2, borderColor: "${lineColor[k]}", fill:{ target:true } } """ j++ } i++ k++ } String visualRep = """
""" if(debugEnabled) uploadHubFile ("pageBuildWork.txt",visualRep.toString().getBytes("UTF-8")) return(visualRep) } def refresh(){ visualRep = buildPage() contentBlock = [ contentType: 'text/html', data: "$visualRep", gzipContent: true, status:200 ] render(contentBlock) } String readExtFile(fName){ def params = [ uri: fName, contentType: "text/html", textParser: true ] try { httpGet(params) { resp -> if(resp!= null) { /* int i = 0 String delim = "" i = resp.data.read() while (i != -1){ char c = (char) i delim+=c i = resp.data.read() } if(debugEnable) log.info "Read External File result: delim" return delim */ return """${resp.data}""" } else { log.error "Read External - Null Response" return null } } } catch (exception) { log.error "Read Ext Error: ${exception.message}" return null } } String insertJS(){ return """${new String(downloadHubFile('chart.js'))}""" } def fetchJS(){ jsFile = readExtFile("https://raw.githubusercontent.com/thebearmay/hubitat/main/libraries/chart.js") if(jsFile){ bArray = (jsFile.getBytes("UTF-8")) uploadHubFile("chart.js",bArray) } else log.error "chart.js not found" } @Field static ArrayList cOptions = ['bar','line','pie','doughnut','radar','polarArea'] @Field static ArrayList lineColor = ["#ff0000","#0000ff","#00ff00","#000f0f","#0f000f","#0f0f00","#0f0f0f","#0000cc","#cc0000","#00cc00","#000c0c","#0c000c","#0c0c00"]