From deee914573b8449501fcbadd432b9a988afed099 Mon Sep 17 00:00:00 2001 From: Bob Florian Date: Wed, 9 Sep 2015 08:25:17 -0400 Subject: [PATCH] Aeon home energy meter (graphing version) updates from production --- .../aeon-home-energy-meter-c3.groovy | 559 +----------------- 1 file changed, 7 insertions(+), 552 deletions(-) diff --git a/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy b/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy index 3d7d73c..90291d5 100644 --- a/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy +++ b/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy @@ -56,7 +56,7 @@ metadata { state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure" } - PLATFORM_graphTile(name: "powerGraph", attribute: "device.power") + graphTile(name: "powerGraph", attribute: "device.power") main(["power", "energy"]) details(["powerGraph", "power", "energy", "reset", "refresh", "configure"]) @@ -68,16 +68,8 @@ metadata { // ======================================================== preferences { - input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily" - input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions() -} - -def selectedGraphPrecision() { - graphPrecision ?: "Daily" -} - -def selectedGraphType() { - graphType ?: "line" + input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: graphPrecisionOptions(), defaultValue: "Daily" + input name: "graphType", type: "enum", title: "Graph Type", description: "line", required: false, options: graphTypeOptions() } // ======================================================== @@ -91,22 +83,6 @@ mappings { GET: "renderGraph" ] } - path("/graphDataSizes") { // for testing. remove before publishing - action: - [ - GET: "graphDataSizes" - ] - } -} - -def graphDataSizes() { // for testing. remove before publishing - state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData -> - attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData -> - dateTypes[dateTypeData.key] = dateTypeData.value.size() - dateTypes - } - attributes - } } // ======================================================== @@ -121,8 +97,7 @@ def parse(String description) { } log.debug "Parse returned ${result?.descriptionText}" - PLATFORM_migrateGraphDataIfNeeded() - PLATFORM_storeData(result.name, result.value) + storeGraphData(result.name, result.value) return result } @@ -176,535 +151,15 @@ def configure() { def renderGraph() { - def data = PLATFORM_fetchGraphData(params.attribute) + def data = fetchGraphData(params.attribute) def totalData = data*.runningSum def xValues = data*.unixTime def yValues = [ - Total: [color: "#49a201", data: totalData, type: selectedGraphType()] + Total: [color: "#49a201", data: totalData] ] - PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts") -} - -// TODO: // ======================================================== -// TODO: // PLATFORM CODE !!! DO NOT ALTER !!! -// TODO: // ======================================================== - -// ======================================================== -// PLATFORM TILES -// ======================================================== - -def PLATFORM_graphTile(Map tileParams) { - def cleanAttribute = tileParams.attribute - "device." - "capability." - htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams) -} - -// ======================================================== -// PLATFORM GRAPH RENDERING -// ======================================================== - -private PLATFORM_graphTypeOptions() { - [ - "line", // DEFAULT - "spline", - "step", - "area", - "area-spline", - "area-step", - "bar", - "scatter", - "pie", - "donut", - "gauge", - ] -} - -private PLATFORM_renderGraph(graphParams) { - - String attribute = graphParams.attribute - List xValues = graphParams.xValues - Map yValues = graphParams.yValues - String focus = graphParams.focus ?: "" - String label = graphParams.label ?: "" - - /* - def xValues = [1, 2] - - def yValues = [ - High: [type: "spline", data: [5, 6], color: "#bc2323"], - Low: [type: "spline", data: [0, 1], color: "#153591"] - ] - - Available type values: - line // DEFAULT - spline - step - area - area-spline - area-step - bar - scatter - pie - donut - gauge - -*/ - - def graphData = PLATFORM_buildGraphData(xValues, yValues, label) - - def legendData = yValues*.key - def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified" - def flowColumn = focus ?: yValues ? yValues.keySet().first() : null - - def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph" - renderHTML(htmlTitle) { html -> - html.head { - """ - - - - - - - - - - - """ - } - html.body { - """ -
-
- - - """ - } - } -} - -private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") { - - /* - def xValues = [1, 2] - - def yValues = [ - High: [type: "spline", data: [5, 6], color: "#bc2323"], - Low: [type: "spline", data: [0, 1], color: "#153591"] - ] - */ - - [ - interaction: [ - enabled: false - ], - bindto : '#chart', - padding : [ - left : 30, - right : 30, - bottom: 0, - top : 0 - ], - legend : [ - show: false, -// hide : false,//(yValues.keySet().size() < 2), -// position: 'inset', -// inset: [ -// anchor: "top-right" -// ], -// item: [ -// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing" -// ] - ], - data : [ - x : "x", - columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data }, - types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total }, - colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total } - ], - axis : [ - x: [ - type: 'timeseries', - tick: [ - centered: true, - culling : [max: 7], - fit : true, - format : PLATFORM_getGraphDateFormat() -// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript - ] - ], - y: [ - label : label, - padding: [ - top: 50 - ] - ] - ] - ] -} - -private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) { - // https://github.com/mbostock/d3/wiki/Time-Formatting - def graphDateFormat - switch (dateType) { - case "Live": - graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM - break - case "Hourly": - graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM - break - case "Daily": - graphDateFormat = "%a" // abbreviated weekday name - break - case "Monthly": - graphDateFormat = "%b" // abbreviated month name - break - case "Annually": - graphDateFormat = "%y" // year without century as a decimal number [00,99] - break - } - graphDateFormat -} - -private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) { - def graphDateFunction = "function(date) { return date; }" - switch (dateType) { - case "Live": - graphDateFunction = """ - function(date) { - return.getMinutes(); - } - """ - break; - case "Hourly": - graphDateFunction = """ function(date) { - var hour = date.getHours(); - if (hour == 0) { - return String(/12 am/).substring(1).slice(0,-1); - } else if (hour > 12) { - return hour -12 + String(/ pm/).substring(1).slice(0,-1); - } else { - return hour + String(/ am/).substring(1).slice(0,-1); - } - }""" - break - case "Daily": - graphDateFunction = """ function(date) { - var day = date.getDay(); - switch(day) { - case 0: return String(/Sun/).substring(1).slice(0,-1); - case 1: return String(/Mon/).substring(1).slice(0,-1); - case 2: return String(/Tue/).substring(1).slice(0,-1); - case 3: return String(/Wed/).substring(1).slice(0,-1); - case 4: return String(/Thu/).substring(1).slice(0,-1); - case 5: return String(/Fri/).substring(1).slice(0,-1); - case 6: return String(/Sat/).substring(1).slice(0,-1); - } - }""" - break - case "Monthly": - graphDateFunction = """ function(date) { - var month = date.getMonth(); - switch(month) { - case 0: return String(/Jan/).substring(1).slice(0,-1); - case 1: return String(/Feb/).substring(1).slice(0,-1); - case 2: return String(/Mar/).substring(1).slice(0,-1); - case 3: return String(/Apr/).substring(1).slice(0,-1); - case 4: return String(/May/).substring(1).slice(0,-1); - case 5: return String(/Jun/).substring(1).slice(0,-1); - case 6: return String(/Jul/).substring(1).slice(0,-1); - case 7: return String(/Aug/).substring(1).slice(0,-1); - case 8: return String(/Sep/).substring(1).slice(0,-1); - case 9: return String(/Oct/).substring(1).slice(0,-1); - case 10: return String(/Nov/).substring(1).slice(0,-1); - case 11: return String(/Dec/).substring(1).slice(0,-1); - } - }""" - break - case "Annually": - graphDateFunction = """ - function(date) { - return.getFullYear(); - } - """ - break - } - groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction) -} - -private jsEscapeString(str = "") { - "String(/${str}/).substring(1).slice(0,-1);" -} - -private PLATFORM_fetchGraphData(attribute) { - - log.debug "PLATFORM_fetchGraphData(${attribute})" - - /* - [ - [ - dateString: "2014-12-1", - unixTime: 1421931600000, - min: 0, - max: 10, - average: 5 - ], - ... - ] - */ - - def attributeBucket = state["measure.${attribute}"] ?: [:] - def dateType = selectedGraphPrecision() - attributeBucket[dateType] -} - -// ======================================================== -// PLATFORM DATA STORAGE -// ======================================================== - -private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] } - -private PLATFORM_storeData(attribute, value) { - PLATFORM_graphPrecisionOptions().each { dateType -> - PLATFORM_addDataToBucket(attribute, value, dateType) - } -} - -/* -[ - Hourly: [ - [ - dateString: "2014-12-1", - unixTime: 1421931600000, - min: 0, - max: 10, - average: 5 - ], - ... - ], - ... -] -*/ - -private PLATFORM_addDataToBucket(attribute, value, dateType) { - - def numberValue = value.toBigDecimal() - - def attributeKey = "measure.${attribute}" - def attributeBucket = state[attributeKey] ?: [:] - - def dateTypeBucket = attributeBucket[dateType] ?: [] - - def now = new Date() - def itemDateString = now.format("PLATFORM_get${dateType}Format"()) - def item = dateTypeBucket.find { it.dateString == itemDateString } - - if (!item) { - // no entry for this data point yet, fill with initial values - item = [:] - item.average = numberValue - item.runningSum = numberValue - item.runningCount = 1 - item.min = numberValue - item.max = numberValue - item.unixTime = now.getTime() - item.dateString = itemDateString - - // add the new data point - dateTypeBucket << item - - // clear out old data points - def old = PLATFORM_getOldDateString(dateType) - if (old) { // annual data never gets cleared - dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) } - } - - // limit the size of the bucket. Live data can stack up fast - def sizeLimit = 25 - if (dateTypeBucket.size() > sizeLimit) { - dateTypeBucket = dateTypeBucket[-sizeLimit..-1] - } - - } else { - //re-calculate average/min/max for this bucket - item.runningSum = (item.runningSum.toBigDecimal()) + numberValue - item.runningCount = item.runningCount.toInteger() + 1 - item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger() - - if (item.min == null) { - item.min = numberValue - } else if (numberValue < item.min.toBigDecimal()) { - item.min = numberValue - } - if (item.max == null) { - item.max = numberValue - } else if (numberValue > item.max.toBigDecimal()) { - item.max = numberValue - } - } - - attributeBucket[dateType] = dateTypeBucket - state[attributeKey] = attributeBucket -} - -private PLATFORM_getOldDateString(dateType) { - def now = new Date() - def date - switch (dateType) { - case "Live": - date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour - break - case "Hourly": - date = (now - 1).getTime() - break - case "Daily": - date = (now - 10).getTime() - break - case "Monthly": - date = (now - 30).getTime() - break - case "Annually": - break - } - date -} - -private PLATFORM_getLiveFormat() { "HH:mm:ss" } - -private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" } - -private PLATFORM_getDailyFormat() { "yyyy-MM-dd" } - -private PLATFORM_getMonthlyFormat() { "yyyy-MM" } - -private PLATFORM_getAnnuallyFormat() { "yyyy" } - -// ======================================================== -// PLATFORM GRAPH DATA MIGRATION -// ======================================================== - -private PLATFORM_migrateGraphDataIfNeeded() { - if (!state.hasMigratedOldGraphData) { - def acceptableKeys = PLATFORM_graphPrecisionOptions() - def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet() - needsMigration.each { PLATFORM_migrateGraphData(it) } - state.hasMigratedOldGraphData = true - } -} - -private PLATFORM_migrateGraphData(attribute) { - - log.trace "about to migrate ${attribute}" - - def attributeBucket = state[attribute] ?: [:] - def migratedAttributeBucket = [:] - - attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem -> - - def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find { - "PLATFORM_get${it}Format"().size() == oldDateString.size() - } - - def dateTypeFormat = "PLATFORM_get${dateType}Format"() - - def newBucket = attributeBucket[dateType] ?: [] -/* - def existingNewItem = newBucket.find { it.dateString == oldDateString } - if (existingNewItem) { - newBucket.remove(existingNewItem) - } -*/ - - def newItem = [ - min : oldItem.min, - max : oldItem.max, - average : oldItem.average, - runningSum : oldItem.runningSum, - runningCount: oldItem.runningCount, - dateString : oldDateString, - unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime() - ] - - newBucket << newItem - migratedAttributeBucket[dateType] = newBucket - } - - state[attribute] = migratedAttributeBucket + renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts") }