import groovy.json.*; import java.text.DecimalFormat; import java.math.*; /** * Hubigraph Line Graph Child App * * Copyright 2020, but let's behonest, 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. * */ // Hubigraph Line Graph Changelog // *****ALPHA BUILD // v0.1 Initial release // v0.2 My son added webpage efficiencies, reduced load on hubitat by 75%. // v0.3 Loading Update; Removed ALL processing from Hub, uses websocket endpoint // v0.5 Multiple line support // v0.51 Select ANY device // v0.60 Select AXIS to graph on // v0.70 A lot more options // v0.80 Added Horizontal Axis Formatting // ****BETA BUILD // v0.1 Added Hubigraph Tile support with Auto-add Dashboard Tile // v0.2 Added Custom Device/Attribute Labels // v0.3 Added waiting screen for initial graph loading & sped up load times // v0.32 Bug Fixes // V 1.0 Released (not Beta) Cleanup and Preview Enabled // v 1.2 Complete UI Refactor // V 1.5 Ordering, Color and Common API Update // V 1.8 Smoother sliders, bug fixes // V 2.0 New Version to Support Combo Graphs. Support for Line Graphs is ended. // V 2.1 Long Term Storage Enabled // V 4.6 Added finer control for timespan, resize graph sizes, bug fixes // Credit to Alden Howard for optimizing the code. def ignoredEvents() { return [ 'lastReceive' , 'reachable' , 'buttonReleased' , 'buttonPressed', 'lastCheckinDate', 'lastCheckin', 'buttonHeld' ] } def version() { return "v1.0" } definition ( name: "Hubigraph Long Term Storage", namespace: "tchoward", author: "Thomas Howard", description: "Hubigraph Long Term Storage", category: "", parent: "tchoward:Hubigraphs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", ) preferences { section ("test"){ page(name: "mainPage", install: true, uninstall: true) page(name: "deviceSelectionPage", nextPage: "optionsPage") page(name: "optionsPage", nextPage: "mainPage") } } mappings { path("/graph/") { action: [ GET: "getGraph" ] } path("/getData/") { action: [ GET: "getData" ] } path("/getOptions/") { action: [ GET: "getOptions" ] } path("/getSubscriptions/") { action: [ GET: "getSubscriptions" ] } } def call(Closure code) { code.setResolveStrategy(Closure.DELEGATE_ONLY); code.setDelegate(this); code.call(); } /******************************************************************************************************************************** ********************************************************************************************************************************* ****************************************** PAGES ******************************************************************************** ********************************************************************************************************************************* *********************************************************************************************************************************/ def mainPage() { unschedule(); dynamicPage(name: "mainPage") { parent.hubiForm_section(this, "Graph Options", 1, "tune"){ container = []; container << parent.hubiForm_page_button(this, "Select Device/Data", "deviceSelectionPage", "100%", "vibration"); parent.hubiForm_container(this, container, 1); } if (sensors){ parent.hubiForm_section(this,"Current Attribute Storage", 1) { container = []; subcontainer = []; subcontainer << parent.hubiForm_text(this, "Sensor"); subcontainer << parent.hubiForm_text(this, "Attribute"); subcontainer << parent.hubiForm_text(this, "Number of Events"); subcontainer << parent.hubiForm_text(this, "First Event Time"); subcontainer << parent.hubiForm_text(this, "Last Event Time"); subcontainer << parent.hubiForm_text(this, "File Size"); container << parent.hubiForm_subcontainer(this, objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]); sensors.each { sensor-> if (settings["${sensor.id}_attributes"]){ settings["${sensor.id}_attributes"].each{attribute-> sensor_name = sensor.label != null ? sensor.label : sensor.name; subcontainer = []; //appendFile(sensor, attribute); storage = getCurrentDailyStorage(sensor, attribute); filename_ = getFileName(sensor, attribute); uri_ = "http://${location.hub.localIP}:8080/local/${filename_}"; subcontainer << parent.hubiForm_text(this, sensor_name, uri_); subcontainer << parent.hubiForm_text(this, attribute, uri_); subcontainer << parent.hubiForm_text(this, storage.num_events); subcontainer << parent.hubiForm_text(this, storage.first); subcontainer << parent.hubiForm_text(this, storage.last); subcontainer << parent.hubiForm_text(this, convertStorageSize(storage.size)); container << parent.hubiForm_subcontainer(this, objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]); getCronString(sensor, attribute); data = [id: sensor.id, attribute: attribute]; updateData(data); } } } parent.hubiForm_container(this, container, 1); } } } } def isStorage(id, attribute){ sensor = sensors.find{it.id == id}; if (sensor != null){ if (settings["${id}_attributes"].find{it == attribute} != null) return true; else return false; } else { return false; } } def updateData(data){ sensor = sensors.find{it.id == data.id}; appendFile(sensor, data.attribute); } def getCronString(sensor, attribute){ def dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; attr = attribute.replaceAll(" ", "_"); date = Date.parse(dateFormat, settings["${sensor.id}_${attr}_time"]); repeat = settings["${sensor.id}_${attr}_time_every"]; schedule("0 ${date.getMinutes()} ${date.getHours()}/${repeat} ? * * *", updateData, [overwrite: false, data: [id: sensor.id, attribute: attribute]]); } def averageFrequency(events){ sum=0; for (i=1; i sum += Float.valueOf(event.value); } tdate = [date : events[events.size()-1].date, boundary: round, granularity: granularity]; return [date: roundDate(tdate), value: sum.round(decimals)]; } def average(events, decimals, round, granularity){ sum = new Float(0); events.each{event-> sum += Float.valueOf(event.value); } sum /= events.size(); tdate = [date : events[events.size()-1].date, boundary: round, granularity: granularity]; return [date: roundDate(tdate), value: sum.round(decimals)]; } def min(events, decimals, round, granularity){ min = Float.valueOf(events[0].value); events.each{event-> min = Float.valueOf(event.value) < min ? Float.valueOf(event.value) : min; } tdate = [date : events[events.size()-1].date, boundary: round, granularity: granularity]; return [date: roundDate(tdate), value: min.round(decimals)]; } def max(events, decimals, round, granularity){ max = Float.valueOf(events[0].value); events.each{event-> max = Float.valueOf(event.value) > max ? Float.valueOf(event.value) : max; } tdate = [date : events[events.size()-1].date, boundary: round, granularity: granularity]; return [date: roundDate(tdate), value: max.round(decimals)]; } def count(events, decimals, round, granularity){ tdate = [date : events[events.size()-1].date, boundary: round, granularity: granularity]; return [date: roundDate(tdate), value: events.size()]; } def getTime(text){ def dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"; return Date.parse(dateFormat, text).getTime(); } def roundDate(Map map){ t = [["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"]]; date = map.date; boundary = map.boundary != null ? map.boundary : false; granularity = map.granularity as Integer; if (!boundary) return date; nearest = date; if (granularity > 60 && granularity < 1440) nearest = org.apache.commons.lang3.time.DateUtils.truncate(date, Calendar.HOUR); else if (granularity == 1440) nearest = org.apache.commons.lang3.time.DateUtils.truncate(date, Calendar.DAY); return nearest; } def quantizeData(events, mins, funct, dec, boundary){ minutes = mins as Integer; decimals = dec as Integer; microSeconds = minutes*1000*60; newEvents = []; if (microSeconds == 0) return events; stop = roundDate([date: events[0].date, granularity: minutes, boundary: boundary]).getTime() + microSeconds; tempEvents = []; idx = 0; while (idx < events.size()){ currTime = roundDate([date: events[idx].date, granularity: minutes, boundary: boundary]).getTime(); if (currTime > stop){ if (tempEvents.size() == 1){ newEntry = "${funct}"(tempEvents, decimals, boundary, minutes); newEvents.add(newEntry); } else if (tempEvents.size() != 0){ newEntry = "${funct}"(tempEvents, decimals, boundary, minutes); } stop += microSeconds; tempEvents = []; } tempEvents.add(events[idx]); idx++; } if (tempEvents.size() == 1){ newEntry = tempEvents[0]; } else if (tempEvents.size() != 0){ newEntry = "${funct}"(tempEvents, decimals, boundary, minutes); newEvents.add(newEntry); } return newEvents; } def optionsPage() { def 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"]]; def quantizationFunctionEnum = [["sum": "Sum Values"], ["average" : "Average Values"], ["count" : "Count Events"], ["min" : "Minimum Value"], ["max" : "Maximum Value"]]; def 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"], ["90" : "3 Months"], ["120" : "4 Months"], ["150" : "5 Months"], ["180" : "6 Months"], ["210" : "7 Months"], ["240" : "8 Months"], ["270" : "9 Months"], ["300" : "10 Months"], ["330" : "11 Months"], ["365" : "1 Year"], ["730" : "2 Years"]]; def hoursEnum = 0..23; def df = new DecimalFormat("#0.0"); dynamicPage(name: "optionsPage") { sensors.each { sensor-> id = sensor.id; if (settings["${id}_attributes"]){ settings["${id}_attributes"].each{ attribute-> attr = attribute.replaceAll(" ", "_"); sensor_name = sensor.label != null ? sensor.label : sensor.name; parent.hubiForm_section(this, "${sensor_name} (${attribute})", 1) { input( type: "enum", name: "${sensor.id}_${attr}_storage", title: "Amount of Storage to Maintain", required: false, multiple: false, options: storageEnum, submitOnChange: false, defaultValue: "7"); input( type: "bool", name: "${sensor.id}_${attr}_boundary", title: "Save Data on Hour/Day Boundary", required: false, multiple: false, submitOnChange: false, defaultValue: false); input ( type: "enum", name: "${sensor.id}_${attr}_time_every", title: "Store Data Every X Hours", required: true, multiple: false, options: hoursEnum, submitOnChange: false, defaultValue: 24); input ( type: "time", name: "${sensor.id}_${attr}_time", title: "Time to Start Storing Data", required: false, multiple: false, submitOnChange: false, defaultValue: "00:00"); input( type: "enum", name: "${sensor.id}_${attr}_quantization", title: "Data Quantization", required: false, multiple: false, options: quantizationEnum, submitOnChange: true, defaultValue: "0"); input( type: "enum", name: "${sensor.id}_${attr}_quantization_function", title: "Quantization Function", required: false, multiple: false, options: quantizationFunctionEnum, submitOnChange: true, defaultValue: "average"); input( type: "enum", name: "${sensor.id}_${attr}_quantization_decimals", title: "Quantization Decimals to Maintain", required: false, multiple: false, options: [[0: "Zero"], [1: "One"], [2: "Two"], [3: "Three"], [4: "Four"]], submitOnChange: true, defaultValue: "1"); container = []; events = getEvents(sensor: sensor, attribute: attribute, days: 1); num_events = events.size(); now = new Date(); if (num_events > 2){ span = (events[num_events-1].date.getTime()-events[0].date.getTime())/(1000*60*60*24); since = (now.getTime() - events[0].date.getTime())/(1000*60*60); quantization_minutes = settings["${sensor.id}_${attr}_quantization"] ? settings["${sensor.id}_${attr}_quantization"] : "0"; quantization_function = settings["${sensor.id}_${attr}_quantization_function"] ? settings["${sensor.id}_${attr}_quantization_function"] : "average"; quantization_decimals = settings["${sensor.id}_${attr}_quantization_decimals"] ? settings["${sensor.id}_${attr}_quantization_decimals"] : 1; quantization_boundary = settings["${sensor.id}_${attr}_boundary"] ? settings["${sensor.id}_${attr}_boundary"] : false; quantData = quantizeData(events, quantization_minutes, quantization_function, quantization_decimals, quantization_boundary); frequency = averageFrequency(events); container << parent.hubiForm_sub_section(this, "Estimated Storage Expense"); container << parent.hubiForm_text(this, "Total Events: ${quantData.size()} quantized (${num_events} raw data)"); container << parent.hubiForm_text(this, "First Event: ${events[0].date} (${round(since)} hours ago)"); container << parent.hubiForm_text(this, "Frequency of raw data: 1 event every ${round(frequency/(1000*60))} minutes"); subcontainer = []; subcontainer << parent.hubiForm_text(this, ""); subcontainer << parent.hubiForm_text(this, "Daily Storage"); subcontainer << parent.hubiForm_text(this, "Weekly Storage"); subcontainer << parent.hubiForm_text(this, "Monthly Storage"); subcontainer << parent.hubiForm_text(this, "Yearly Storage"); container << parent.hubiForm_subcontainer(this, objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]); subcontainer = []; daily = (num_events/span)*50; subcontainer << parent.hubiForm_text(this, "Raw Data"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily)}"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily*7)}"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily*30)}"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily*365)}"); container << parent.hubiForm_subcontainer(this, objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]); subcontainer = []; daily = (quantData.size()/span)*50; subcontainer << parent.hubiForm_text(this, "Quantized Data"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily)}"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily*7)}"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily*30)}"); subcontainer << parent.hubiForm_text(this, "${convertStorageSize(daily*365)}"); container << parent.hubiForm_subcontainer(this, objects: subcontainer, breakdown: [0.2, 0.2, 0.2, 0.2, 0.2]); } parent.hubiForm_container(this, container, 1); } } } } } } def deviceSelectionPage(){ if (password && username){ log.debug("Username and Password Valid"); } dynamicPage(name: "deviceSelectionPage"){ parent.hubiForm_section(this,"Login Information", 1) { if (settings["hpmSecurity"]==null){ settings["hpmSecurity"] = true; app.updateSetting("hpmSecurity", [type: "bool", value: "true"]); } container = []; container << parent.hubiForm_switch (this, title: "Use Hubitat Security?", name: "hpmSecurity", default: true, submit_on_change: true); parent.hubiForm_container(this, container, 1); if (settings["hpmSecurity"] == true){ input "username", "string", title: "Hub Security username", required: false, submitOnChange: true input "password", "password", title: "Hub Security password", required: false, submitOnChange: true } } if (settings["hpmSecurity"] == true && !login()){ parent.hubiForm_section(this,"Login Error", 1) { container = []; container << parent.hubiForm_text(this, """CANNOT LOGIN
If you have Hub Security Enabled, please put in correct login credentials
If not, please deselect Use Hubitat Security"""); parent.hubiForm_container(this, container, 1); } } else { parent.hubiForm_section(this,"Sensor and Attribute Selection", 1) { input "sensors", "capability.*", title: "Sensor Selection for Long Term Storage", multiple: true, submitOnChange: true if (sensors){ final_attrs = []; sensors.each { sensor-> try { final_attrs = []; attributes_ = sensor.getSupportedAttributes(); attributes_.each{ attribute_-> name = attribute_.getName(); if (sensor.currentState(name)){ final_attrs << ["${name}" : "${name} ::: [${sensor.currentState(name).getValue()}]"]; } } final_attrs = final_attrs.unique(false); } catch (e) { final_attrs = [["1" : "ERROR"]]; log.debug("Error: ${e}"); } sensor_name = sensor.label != null ? sensor.label : sensor.name; input( type: "enum", name: "${sensor.id}_attributes", title: "${sensor_name} attribute(s) to Store", required: true, multiple: true, options: final_attrs, submitOnChange: false); } } } } } } def getEvents(Map map){ sensor = map.sensor; attribute = map.attribute; days = map.days; if (map.start_time){ then = map.start_time; } else { now = new Date(); then = now; use (groovy.time.TimeCategory) { then -= days.days; } } respEvents = sensor.statesSince(attribute, then, [max: 2000]).collect{[ date: it.date, value: it.value]} respEvents = respEvents.flatten(); respEvents = respEvents.reverse(); return respEvents; } def login() { if (settings["hpmSecurity"] && settings["hpmSecurity"]==true) { def 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 { atomicState.cookie = resp?.headers?.'Set-Cookie'?.split(';')?.getAt(0) result = true } } } catch (e) { log.error "Error logging in: ${e}" result = false } return result } else return true } def fileExists(sensor, attribute){ filename_ = getFileName(sensor, attribute); uri = "http://${location.hub.localIP}:8080/local/${filename_}"; def params = [ uri: uri, textParser: true, ] try { httpGet(params) { resp -> if (resp != null){ return true; } else { return false; } } } catch (exception){ if (exception.message == "Not Found"){ log.debug("File DOES NOT Exists for ${sensor.name} (${attribute})"); } else { log.error("Find file ${sensor.name} (${attribute}) :: Connection Exception: ${exception.message}"); } return false; } } def readFile(sensor, attribute){ filename_ = getFileName(sensor, attribute); uri = "http://${location.hub.localIP}:8080/local/${filename_}" def params = [ uri: uri, textParser: true, ] try { httpGet(params) { resp -> if(resp!= null) { data = resp.getData(); def jsonSlurper = new JsonSlurper(); parse = jsonSlurper.parseText("${data}"); return [size: data.length, data: parse]; } else { log.error "Null Response" } } } catch (exception) { log.error "Connection Exception: ${exception.message}" return null; } } def getFileName(sensor, attribute){ attr = attribute.replaceAll(" ", "_"); return "HubiGraph_LTS_${sensor.id}_${attr}.json"; } def pruneData(input_data, time){ days = time as Integer; if (days == 0) return input_data; then = new Date(); use (groovy.time.TimeCategory) { then -= days.days; } startDate = then.getTime(); return_data = input_data; date = input_data[0].date.getTime(); while (date < startDate){ return_data.remove(0); date = return_data[0].date.getTime(); } return return_data; } def addData(main, append){ return_data = main; append.each{data-> return_data << data; } return return_data; } def convertToMap(json){ def dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"; return_data = []; json.each{data-> date = Date.parse(dateFormat, data.date); return_data << [date: date, value: data.value]; } return return_data; } def getFileData(sensor, attribute){ filename_ = getFileName(sensor, attribute); uri = "http://${location.hub.localIP}:8080/local/${filename_}" def params = [ uri: uri, textParser: true, ] parse_data =[]; try { httpGet(params) { resp -> file_data = resp.getData(); jsonSlurper = new JsonSlurper(); parse_data = convertToMap(jsonSlurper.parseText("${file_data}")); } } catch (e) { log.debug("Cannot Get File data for ${sensor.name} (${attribute}) :: ${e}"); } return parse_data; } def appendFile(sensor, attribute){ filename_ = getFileName(sensor, attribute); attr = attribute.replaceAll(" ", "_"); quantization_minutes = settings["${sensor.id}_${attr}_quantization"]; quantization_function = settings["${sensor.id}_${attr}_quantization_function"]; quantization_decimals = settings["${sensor.id}_${attr}_quantization_decimals"]; quantization_boundary = settings["${sensor.id}_${attr}_boundary"] ? settings["${sensor.id}_${attr}_boundary"] : false; storage = settings["${sensor.id}_${attr}_storage"] as Integer; uri = "http://${location.hub.localIP}:8080/local/${filename_}" def params = [ uri: uri, textParser: true, ] try { httpGet(params) { resp -> //File exists and is good file_data = resp.getData(); jsonSlurper = new JsonSlurper(); parse_data = convertToMap(jsonSlurper.parseText("${file_data}")); parse_data = pruneData(parse_data, storage); //Get the most Current Data then = parse_data[parse_data.size()-1].date; respEvents = getEvents(sensor: sensor, attribute: attribute, start_time: then); if (respEvents != []){ write_data = addData(parse_data, respEvents); } else { write_data = parse_data; } write_data = quantizeData(write_data, quantization_minutes, quantization_function, quantization_decimals, quantization_boundary); writeFile(sensor, attribute, JsonOutput.toJson(write_data)) } } catch (exception){ if (exception.message == "Not Found"){ then = new Date(); use (groovy.time.TimeCategory) { then -= storage.days; } respEvents = getEvents(sensor: sensor, attribute: attribute, start_time: then); respEvents = quantizeData(respEvents, quantization_minutes, quantization_function, quantization_decimals, quantization_boundary); write_data = respEvents == null ? "" : JsonOutput.toJson(respEvents); writeFile(sensor, attribute, write_data) } else { log.error("Find file ${sensor.name} (${attribute}) :: Connection Exception: ${exception}"); } } } def writeFile(sensor, attribute, contents) { filename_ = getFileName(sensor, attribute); if (!login()) return; try { def params = [ uri: "http://127.0.0.1:8080", path: "/hub/fileManager/upload", query: [ "folder": "/" ], headers: [ "Cookie": atomicState.cookie, "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryDtoO2QfPwfhTjOuS" ], body: """------WebKitFormBoundaryDtoO2QfPwfhTjOuS Content-Disposition: form-data; name="uploadFile"; filename="${filename_}" Content-Type: text/plain ${contents} ------WebKitFormBoundaryDtoO2QfPwfhTjOuS Content-Disposition: form-data; name="folder" ------WebKitFormBoundaryDtoO2QfPwfhTjOuS--""", timeout: 300, ignoreSSLIssues: true ] httpPost(params) { resp -> } return true } catch (e) { log.error "Error installing file: ${e}" } return false } def getOpenWeatherData(){ childDevice = getChildDevice("OPEN_WEATHER${app.id}"); if (!childDevice){ log.debug("Error: No Child Found"); return null; } return(childDevice.getWeatherData()); } def getOpenWeatherChild(){ return getChildDevice("OPEN_WEATHER${app.id}"); } def convertToJSON(obj){ def jsonSlurper = new JsonSlurper(); return jsonSlurper.parseText(obj); } def getCurrentDailyStorage(sensor, attribute){ if (fileExists(sensor, attribute) && readFile(sensor, attribute)?.data){ json = readFile(sensor, attribute); data = json.data; size = json.size; def dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"; first = Date.parse(dateFormat, data[0].date); then = Date.parse(dateFormat, data[data.size()-1].date); respEvents = getEvents(sensor: sensor, attribute: attribute, start_time: then) file_string = respEvents == null ? "" : JsonOutput.toJson(respEvents); return [num_events: data.size(), first: first, last: then, size: size]; } else { try{ respEvents = getEvents(sensor: sensor, attribute: attribute, days: 30) file_string = respEvents == null ? "" : JsonOutput.toJson(respEvents); writeFile(sensor, attribute, file_string); return [num_events: respEvents.size(), first: respEvents[0].date, last: respEvents[respEvents.size()-1].date, size: respEvents.size()]; } catch (e) { log.debug("Error: ${e}") return -1; } } } def getSensor(str){ split = str.tokenize('.'); sensor = sensors.find{ it.id == split[0]}; return [ sensor: sensor, attribute: split[1] ]; } def convertStorageSize(num){ def 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"; } } def round(num){ def df = new DecimalFormat("#0.0") return "${df.format(num)}" }