import groovy.json.*; /** * 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 // 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 Line Graph", namespace: "tchoward", author: "Thomas Howard", description: "Hubigraph Line Graph", 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: "graphSetupPage") 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" ] } } } def call(Closure code) { code.setResolveStrategy(Closure.DELEGATE_ONLY); code.setDelegate(this); code.call(); } /******************************************************************************************************************************** ********************************************************************************************************************************* ****************************************** PAGES ******************************************************************************** ********************************************************************************************************************************* *********************************************************************************************************************************/ def graphSetupPage(){ def fontEnum = [["1":"1"], ["2":"2"], ["3":"3"], ["4":"4"], ["5":"5"], ["6":"6"], ["7":"7"], ["8":"8"], ["9":"9"], ["10":"10"], ["11":"11"], ["12":"12"], ["13":"13"], ["14":"14"], ["15":"15"], ["16":"16"], ["17":"17"], ["18":"18"], ["19":"19"], ["20":"20"]]; def updateEnum = [["-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"]]; def timespanEnum = [["60000":"1 Minute"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], ["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"]] def timespanEnum2 = [["60000":"1 Minute"], ["120000":"2 Minutes"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["2400000":"30 minutes"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], ["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"]]; def supportedTypes = [ "alarm": ["start": "on", "end": "off"], "contact": ["start": "open", "end": "closed"], "switch": ["start": "on", "end": "off"], "motion": ["start": "active", "end": "inactive"], "mute": ["start": "muted", "end": "unmuted"], "presence": ["start":"present", "end":"not present"], "holdableButton": ["start":"true", "end":"false"], "carbonMonoxide": ["start":"detected", "end":"clear"], "playing": ["start":"playing", "end":"stopped"], "door": ["start": "open", "end": "closed"], "speed": ["start": "on", "end": "off"], "lock": ["start": "unlocked", "end": "locked"], "shock": ["start": "detected", "end": "clear"], "sleepSensor": ["start": "sleeping", "end": "not sleeping"], "smoke": ["start":"detected", "end":"clear"], "sound": ["start":"detected", "end":"not detected"], "tamper": ["start":"detected", "end":"clear"], "valve": ["start": "open", "end": "closed"], "camera": ["start": "on", "end": "off"], "water": ["start": "wet", "end": "dry"], "windowShade": ["start": "open", "end": "closed"], "acceleration": ["start": "inactive", "end": "active"] ]; dynamicPage(name: "graphSetupPage") { def non_numeric = false; sensors.each { sensor -> settings["attributes_${sensor.id}"].each { attribute -> if (supportedTypes[attribute] != null) non_numeric = true; } } if (non_numeric) { app.updateSetting ("graph_max_points", 0); } parent.hubiForm_section(this,"General Options", 1) { input( type: "enum", name: "graph_type", title: "Graph Type", defaultValue: "Line Graph", options: ["Line Graph", "Area Graph", "Scatter Plot"], submitOnChange: true) input( type: "enum", name: "graph_update_rate", title: "Select graph update rate", multiple: false, required: true, options: updateEnum, defaultValue: "0") input( type: "enum", name: "graph_timespan", title: "Select Timespan to Graph", multiple: false, required: true, options: timespanEnum, defaultValue: "43200000") container = []; container << parent.hubiForm_color (this, "Graph Background", "graph_background", "#FFFFFF", false) container << parent.hubiForm_switch(this, title: "Smooth Graph Points", name: "graph_smoothing", default: false); container << parent.hubiForm_switch(this, title: "Flip Graph to Vertical?
(Rotate 90 degrees)", name: "graph_y_orientation", default: false); container << parent.hubiForm_switch(this, title: "Reverse Data Order?
(Flip data left to Right)", name: "graph_z_orientation", default: false) if (!non_numeric) container << parent.hubiForm_slider (this, title: "Maximum number of Data Points?
(Zero for ALL)", name: "graph_max_points", default: 0, min: 0, max: 1000, units: " data points", submit_on_change: false); parent.hubiForm_container(this, container, 1); } parent.hubiForm_section(this,"Graph Title", 1) { container = []; container << parent.hubiForm_switch(this, title: "Show Title on Graph", name: "graph_show_title", default: false, submit_on_change: true); if (graph_show_title==true) { container << parent.hubiForm_text_input (this, "Graph Title", "graph_title", "Graph Title", false); container << parent.hubiForm_font_size (this, title: "Title", name: "graph_title", default: 9, min: 2, max: 20); container << parent.hubiForm_color (this, "Title", "graph_title", "#000000", false); container << parent.hubiForm_switch (this, title: "Graph Title Inside Graph?", name: "graph_title_inside", default: false); } parent.hubiForm_container(this, container, 1); } parent.hubiForm_section(this, "Graph Size", 1){ container = []; container << parent.hubiForm_switch (this, title: "Set size of Graph?
(False = Fill Window)", name: "graph_static_size", default: false, submit_on_change: true); if (graph_static_size==true){ container << parent.hubiForm_slider (this, title: "Horizontal dimension of the graph", name: "graph_h_size", default_value: 800, min: 100, max: 3000, units: " pixels", submit_on_change: false); container << parent.hubiForm_slider (this, title: "Vertical dimension of the graph", name: "graph_v_size", default_value: 600, min: 100, max: 3000, units: " pixels", submit_on_change: false); } parent.hubiForm_container(this, container, 1); } parent.hubiForm_section(this,"Horizontal Axis", 1) { //Axis container = []; container << parent.hubiForm_font_size (this, title: "Horizontal Axis", name: "graph_haxis", default: 9, min: 2, max: 20); container << parent.hubiForm_color (this, "Horizonal Header", "graph_hh", "#C0C0C0", false); container << parent.hubiForm_color (this, "Horizonal Axis", "graph_ha", "#C0C0C0", false); container << parent.hubiForm_text_input (this, "Num Horizontal Gridlines
(Blank for auto)", "graph_h_num_grid", "", false); container << parent.hubiForm_switch (this, title: "Show String Formatting Help", name: "dummy", default: false, submit_on_change: true); if (dummy == true){ val = []; val <<"Name"; val << "Format" ; val <<"Result"; val <<"Year"; val << "Y"; val << "2020"; 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 << "1..23"; val <<"Hour (24)"; val << "HH"; val << "01..23"; val <<"Minute"; val << "m"; val << "1..59"; val <<"Minute"; val << "mm"; val << "01..59"; val <<"Seconds"; val << "s"; val << "1..59"; val <<"Seconds"; val << "ss"; val << "01..59 " container << parent.hubiForm_cell(this, val, 3); container << parent.hubiForm_text(this, """Example: "EEEE, MMM d, Y hh:mm:ss a"
= "Monday, June 2, 2020 08:21:33 AM
""") } container << parent.hubiForm_text_input (this, "Horizontal Axis Format", "graph_h_format", "", true); parent.hubiForm_container(this, container, 1); if (graph_h_format){ today = new Date(); paragraph "Horizontal Axis Sample: ${today.format(graph_h_format)}" } } //Vertical Axis parent.hubiForm_section(this,"Vertical Axis", 1) { container = []; container << parent.hubiForm_font_size (this, title: "Vertical Axis", name: "graph_vaxis", default: 9, min: 2, max: 20); container << parent.hubiForm_color (this, "Vertical Header", "graph_vh", "#000000", false); container << parent.hubiForm_color (this, "Vertical Axis", "graph_va", "#C0C0C0", false); parent.hubiForm_container(this, container, 1); } //Left Axis parent.hubiForm_section(this,"Left Axis", 1, "arrow_back"){ container = []; container << parent.hubiForm_text_input(this, "Minimum for left axis(Blank for auto)", "graph_vaxis_1_min", "", false); container << parent.hubiForm_text_input(this, "Maximum for left axis(Blank for auto)", "graph_vaxis_1_max", "", false); container << parent.hubiForm_text_input (this, "Num Vertical Gridlines
(Blank for auto)", "graph_vaxis_1_num_lines", "", false); container << parent.hubiForm_switch (this, title: "Show Left Axis Label on Graph", name: "graph_show_left_label", default: false, submit_on_change: true); if (graph_show_left_label==true){ container << parent.hubiForm_text_input (this, "Input Left Axis Label", "graph_left_label", "Left Axis Label", false); container << parent.hubiForm_font_size (this, title: "Left Axis", name: "graph_left", default: 9, min: 2, max: 20); container << parent.hubiForm_color (this, "Left Axis", "graph_left", "#FFFFFF", false); } parent.hubiForm_container(this, container, 1); } //Right Axis parent.hubiForm_section(this,"Right Axis", 1, "arrow_forward"){ container = []; container << parent.hubiForm_text_input(this, "Minimum for right axis(Blank for auto)", "graph_vaxis_2_min", "", false); container << parent.hubiForm_text_input(this, "Maximum for right axis(Blank for auto)", "graph_vaxis_2_max", "", false); container << parent.hubiForm_text_input (this, "Num Vertical Gridlines
(Blank for auto)", "graph_vaxis_2_num_lines", "", false); container << parent.hubiForm_switch (this, title: "Show Right Axis Label on Graph", name: "graph_show_right_label", default: false, submit_on_change: true); if (graph_show_right_label==true){ container << parent.hubiForm_text_input (this, "Input right Axis Label", "graph_right_label", "Right Axis Label", false); container << parent.hubiForm_font_size (this, title: "Right Axis", name: "graph_right", default: 9, min: 2, max: 20); container << parent.hubiForm_color (this, "Right Axis", "graph_right", "#FFFFFF", false); } parent.hubiForm_container(this, container, 1); } //Legend parent.hubiForm_section(this,"Legend", 1){ container = []; def legendPosition = [["top": "Top"], ["bottom":"Bottom"], ["in": "Inside Top"]]; def insidePosition = [["start": "Left"], ["center": "Center"], ["end": "Right"]]; container << parent.hubiForm_switch(this, title: "Show Legend on Graph", name: "graph_show_legend", default: false, submit_on_change: true); if (graph_show_legend==true){ container << parent.hubiForm_font_size (this, title: "Legend", name: "graph_legend", default: 9, min: 2, max: 20); container << parent.hubiForm_color (this, "Legend", "graph_legend", "#000000", false); parent.hubiForm_container(this, container, 1); input( type: "enum", name: "graph_legend_position", title: "Legend Position", defaultValue: "Bottom", options: legendPosition); input( type: "enum", name: "graph_legend_in side_position", title: "Legend Justification", defaultValue: "center", options: insidePosition); } else { parent.hubiForm_container(this, container, 1); } } state.num_devices = 0; sensors.each { sensor -> settings["attributes_${sensor.id}"].each { attribute -> state.num_devices++; } } def availableAxis = [["0" : "Left Axis"], ["1": "Right Axis"]]; if (state.num_devices == 1) { availableAxis = [["0" : "Left Axis"], ["1": "Right Axis"], ["2": "Both Axes"]]; } //Line cnt = 1; sensors.each { sensor -> settings["attributes_${sensor.id}"].each { attribute -> parent.hubiForm_section(this,"${sensor.displayName} - ${attribute}", 1){ container = []; input( type: "enum", name: "graph_axis_number_${sensor.id}_${attribute}", title: "Graph Axis Side", defaultValue: "0", options: availableAxis); container << parent.hubiForm_color(this, "Line", "graph_line_${sensor.id}_${attribute}", parent.hubiTools_rotating_colors(cnt), false); container << parent.hubiForm_line_size (this, title: "Line Thickness", name: "attribute_${sensor.id}_${attribute}", default: 2, min: 1, max: 20); container << parent.hubiForm_text_input(this, "Override Device Name
Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE
", "graph_name_override_${sensor.id}_${attribute}", "%deviceName%: %attributeName%", false); startVal = supportedTypes[attribute] ? supportedTypes[attribute].start : ""; endVal = supportedTypes[attribute] ? supportedTypes[attribute].end : ""; if (graph_type == "Area Graph"){ container << parent.hubiForm_slider (this, title: "Opacity of the area below the line", name: "attribute_${sensor.id}_${attribute}_opacity", default_value: 30, min: 0, max: 100, units: "%", submit_on_change: false); } if (startVal != ""){ app.updateSetting ("attribute_${sensor.id}_${attribute}_non_number", true); app.updateSetting ("attribute_${sensor.id}_${attribute}_startString", startVal); app.updateSetting ("attribute_${sensor.id}_${attribute}_endString", endVal); container << parent.hubiForm_text(this, "This Attribute ($attribute) is non-numerical, please choose values for the states below"); container << parent.hubiForm_text_input(this, "Value for $startVal", "attribute_${sensor.id}_${attribute}_${startVal}", "100", false); container << parent.hubiForm_text_input(this, "Value for $endVal", "attribute_${sensor.id}_${attribute}_${endVal}", "0", false); parent.hubiForm_container(this, container, 1); } else { container << parent.hubiForm_switch(this, title: "Display as a Drop Line", name: "attribute_${sensor.id}_${attribute}_drop_line", default: false, submit_on_change: true); if (settings["attribute_${sensor.id}_${attribute}_drop_line"]==true){ container << parent.hubiForm_text_input(this,"Value to drop the Line", "attribute_${sensor.id}_${attribute}_drop_value", "0", false); parent.hubiForm_container(this, container, 1); input( type: "enum", name: "attribute_${sensor.id}_${attribute}_drop_time", title: "Drop Line Time", defaultValue: "5 Minutes", options: timespanEnum2 ) } else { parent.hubiForm_container(this, container, 1); } } cnt += 1; } } } } } def deviceSelectionPage() { def supported_attrs; dynamicPage(name: "deviceSelectionPage") { parent.hubiForm_section(this,"Device Selection", 1){ input "sensors", "capability.*", title: "Sensors", multiple: true, required: true, submitOnChange: true if (sensors){ sensors.each { sensor_events = it.events([max:250]).name; supported_attrs = sensor_events.unique(false); container = []; container << parent.hubiForm_sub_section(this, it.displayName); parent.hubiForm_container(this, container, 1); input( type: "enum", name: "attributes_${it.id}", title: "Attributes to graph", required: true, multiple: true, options: supported_attrs, defaultValue: "1") } } } } } def disableAPIPage() { dynamicPage(name: "disableAPIPage") { section() { if (state.endpoint) { revokeAccessToken(); state.endpoint = null } paragraph "Token revoked. Click done to continue." } } } def enableAPIPage() { dynamicPage(name: "enableAPIPage", title: "") { section() { if(!state.endpoint) initializeAppEndpoint(); paragraph "Token created. Click done to continue." } } } def mainPage() { dynamicPage(name: "mainPage") { def container = []; if (!state.endpoint) { parent.hubiForm_section(this, "Please set up OAuth API", 1, "report"){ href name: "enableAPIPageLink", title: "Enable API", description: "", page: "enableAPIPage" } } else { parent.hubiForm_section(this, "Graph Options", 1, "tune"){ container = []; container << parent.hubiForm_page_button(this, "Select Device/Data", "deviceSelectionPage", "100%", "vibration"); container << parent.hubiForm_page_button(this, "Configure Graph", "graphSetupPage", "100%", "poll"); parent.hubiForm_container(this, container, 1); } parent.hubiForm_section(this, "Local Graph URL", 1, "link"){ container = []; container << parent.hubiForm_text(this, "${state.localEndpointURL}graph/?access_token=${state.endpointSecret}"); parent.hubiForm_container(this, container, 1); } if (graph_timespan){ parent.hubiForm_section(this, "Preview", 10, "show_chart"){ container = []; container << parent.hubiForm_graph_preview(this) parent.hubiForm_container(this, container, 1); } //graph_timespan parent.hubiForm_section(this, "Hubigraph Tile Installation", 2, "apps"){ container = []; container << parent.hubiForm_switch(this, title: "Install Hubigraph Tile Device?", name: "install_device", default: false, submit_on_change: true); if (install_device==true){ container << parent.hubiForm_text_input(this, "Name for HubiGraph Tile Device", "device_name", "Hubigraph Tile", "false"); } parent.hubiForm_container(this, container, 1); } } if (state.endpoint){ parent.hubiForm_section(this, "Hubigraph Application", 1, "settings"){ container = []; container << parent.hubiForm_sub_section(this, "Application Name"); container << parent.hubiForm_text_input(this, "Rename the Application?", "app_name", "Hubigraph Bar Graph", "false"); container << parent.hubiForm_sub_section(this, "Debugging"); container << parent.hubiForm_switch(this, title: "Enable Debug Logging?", name: "debug", default: false); container << parent.hubiForm_sub_section(this, "Disable Oauth Authorization"); container << parent.hubiForm_page_button(this, "Disable API", "disableAPIPage", "100%", "cancel"); parent.hubiForm_container(this, container, 1); } } } //else } //dynamicPage } /******************************************************************************************************************************** ********************************************************************************************************************************* ****************************************** END PAGES ******************************************************************************** ********************************************************************************************************************************* *********************************************************************************************************************************/ def installed() { initialize() } def uninstalled() { if (state.endpoint) { try { revokeAccessToken() } catch (e) { log.warn "Unable to revoke API access token: $e" } } removeChildDevices(getChildDevices()); } private removeChildDevices(delete) { delete.each {deleteChildDevice(it.deviceNetworkId)} } def updated() { app.updateLabel(app_name); if (install_device == true){ parent.hubiTool_create_tile(this); } } def initialize() { updated(); } private getValue(id, attr, val){ def reg = ~/[a-z,A-Z]+/; if (settings["attribute_${id}_${attr}_${val}"]!=null){ ret = Double.parseDouble(settings["attribute_${id}_${attr}_${val}"]); } else { ret = Double.parseDouble(val - reg ) } return ret; } private buildData() { def resp = [:] def today = new Date(); def then = new Date(); use (groovy.time.TimeCategory) { then -= Integer.parseInt(graph_timespan).milliseconds; } if(sensors) { sensors.each { sensor -> resp[sensor.id] = [:]; settings["attributes_${sensor.id}"].each { attribute -> def respEvents = []; respEvents << sensor.statesSince(attribute, then, [max: 50000]).collect{[ date: it.date.getTime(), value: getValue(sensor.id, attribute, it.value) ]} respEvents = respEvents.flatten(); respEvents = respEvents.reverse(); //Add drop lines for non-numerical devices if (settings["attribute_${sensor.id}_${attribute}_non_number"] && respEvents.size()>1){ start = settings["attribute_${sensor.id}_${attribute}_startString"]; end = settings["attribute_${sensor.id}_${attribute}_endString"]; startVal = Float.parseFloat(settings["attribute_${sensor.id}_${attribute}_${start}"]); endVal = Float.parseFloat(settings["attribute_${sensor.id}_${attribute}_${end}"]); tEvents = []; //Add Start Event currDate = then; if (respEvents[0].value == startVal){ tEvents.push([date: currDate, value: endVal]); } else { tEvents.push([date: currDate, value: startVal]); } for (i=0; i 0) { reduction = (int) Math.ceil((float)respEvents.size() / graph_max_points); respEvents = respEvents.collate(reduction).collect{ group -> group.inject([ date: 0, value: 0 ]){ col, it -> col.date += it.date / group.size(); col.value += it.value / group.size(); return col; } }; } //add drop line data tEvents = []; if (settings["attribute_${sensor.id}_${attribute}_drop_line"] && respEvents.size()>1){ def curr, prev, currDate, prevDate, drop_time, drop_value; drop_time = settings["attribute_${sensor.id}_${attribute}_drop_time"]; drop_value = settings["attribute_${sensor.id}_${attribute}_drop_value"]; tEvents.push(respEvents[0]); for (i=1; i Integer.parseInt(drop_time)){ //add first zero tEvents.push([date: prevDate-1000, value: Float.parseFloat(drop_value)]); tEvents.push([date: currDate+1000, value: Float.parseFloat(drop_value)]); } tEvents.push(curr); } respEvents = tEvents; } resp[sensor.id][attribute] = respEvents; } } } return resp; } def getChartOptions(){ def options = [ "graphReduction": graph_max_points, "graphTimespan": Integer.parseInt(graph_timespan), "graphUpdateRate": Integer.parseInt(graph_update_rate), "graphOptions": [ "width": graph_static_size ? graph_h_size : "100%", "height": graph_static_size ? graph_v_size: "100%", "chartArea": [ "width": graph_static_size ? graph_h_size : "80%", "height": graph_static_size ? graph_v_size: "80%"], "hAxis": ["textStyle": ["fontSize": graph_haxis_font, "color": graph_hh_color_transparent ? "transparent" : graph_hh_color ], "gridlines": ["color": graph_ha_color_transparent ? "transparent" : graph_ha_color, "count": graph_h_num_grid != "" ? graph_h_num_grid : null ], "format": graph_h_format==""?"":graph_h_format ], "vAxis": ["textStyle": ["fontSize": graph_vaxis_font, "color": graph_vh_color_transparent ? "transparent" : graph_vh_color], "gridlines": ["color": graph_va_color_transparent ? "transparent" : graph_va_color], ], "vAxes": [ 0: ["title" : graph_show_left_label ? graph_left_label: null, "titleTextStyle": ["color": graph_left_color_transparent ? "transparent" : graph_left_color, "fontSize": graph_left_font], "viewWindow": ["min": graph_vaxis_1_min != "" ? graph_vaxis_1_min : null, "max": graph_vaxis_1_max != "" ? graph_vaxis_1_max : null], "gridlines": ["count" : graph_vaxis_1_num_lines != "" ? graph_vaxis_1_num_lines : null ], "minorGridlines": ["count" : 0] ], 1: ["title": graph_show_right_label ? graph_right_label : null, "titleTextStyle": ["color": graph_right_color_transparent ? "transparent" : graph_right_color, "fontSize": graph_right_font], "viewWindow": ["min": graph_vaxis_2_min != "" ? graph_vaxis_2_min : null, "max": graph_vaxis_2_max != "" ? graph_vaxis_2_max : null], "gridlines": ["count" : graph_vaxis_2_num_lines != "" ? graph_vaxis_2_num_lines : null ], "minorGridlines": ["count" : 0] ] ], "legend": !graph_show_legend ? ["position": "none"] : ["position": graph_legend_position, "alignment": graph_legend_inside_position, "textStyle": ["fontSize": graph_legend_font, "color": graph_legend_color_transparent ? "transparent" : graph_legend_color]], "backgroundColor": graph_background_color_transparent ? "transparent" : graph_background_color, "curveType": !graph_smoothing ? "" : "function", "title": !graph_show_title ? "" : graph_title, "titleTextStyle": !graph_show_title ? "" : ["fontSize": graph_title_font, "color": graph_title_color_transparent ? "transparent" : graph_title_color], "titlePosition" : graph_title_inside ? "in" : "out", "interpolateNulls": true, //for null vals on our chart "orientation" : graph_y_orientation == true ? "vertical" : "horizontal", "reverseCategories" : graph_x_orientation, "series": [], ] ]; //add colors and thicknesses sensors.each { sensor -> settings["attributes_${sensor.id}"].each { attribute -> def axis = Integer.parseInt(settings["graph_axis_number_${sensor.id}_${attribute}"]); def text_color = settings["graph_line_${sensor.id}_${attribute}_color"]; def text_color_transparent = settings["graph_line_${sensor.id}_${attribute}_color_transparent"]; def line_thickness = settings["attribute_${sensor.id}_${attribute}_line_size"]; if (settings["attribute_${sensor.id}_${attribute}_opacity"]){ def opacity = settings["attribute_${sensor.id}_${attribute}_opacity"]/100.0; } def annotations = [ "targetAxisIndex": axis, "color": text_color_transparent ? "transparent" : text_color, "stroke": text_color_transparent ? "transparent" : "red", "lineWidth": line_thickness, "areaOpacity" : opacity, ]; options.graphOptions.series << annotations; } } return options; } def getDrawType(){ switch (graph_type){ case "Line Graph": return "google.visualization.LineChart"; case "Area Graph": return "google.visualization.AreaChart"; case "Scatter Plot": return "google.visualization.ScatterChart"; } } void removeLastChar(str) { str.subSequence(0, str.length() - 1) str } def getLineGraph() { def fullSizeStyle = "margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden"; def html = """
""" return html; } // Events come in Date format def getDateStringEvent(date) { def dateObj = date def yyyy = dateObj.getYear() + 1900 def MM = String.format("%02d", dateObj.getMonth()+1); def dd = String.format("%02d", dateObj.getDate()); def HH = String.format("%02d", dateObj.getHours()); def mm = String.format("%02d", dateObj.getMinutes()); def ss = String.format("%02d", dateObj.getSeconds()); def dateString = /$yyyy-$MM-$dd $HH:$mm:$ss.000/; dateString } def initializeAppEndpoint() { if (!state.endpoint) { try { def accessToken = createAccessToken() if (accessToken) { state.endpoint = getApiServerUrl() state.localEndpointURL = fullLocalApiServerUrl("") state.remoteEndpointURL = fullApiServerUrl("") state.endpointSecret = accessToken } } catch(e) { state.endpoint = null } } return state.endpoint } //oauth endpoints def getGraph() { render(contentType: "text/html", data: getLineGraph()); } def getDataMetrics() { def data; def then = new Date().getTime(); data = getData(); def now = new Date().getTime(); return data; } def getData() { def timeline = buildData(); return render(contentType: "text/json", data: JsonOutput.toJson(timeline)); } def getOptions() { render(contentType: "text/json", data: JsonOutput.toJson(getChartOptions())); } def getSubscriptions() { def ids = []; def sensors_ = [:]; def attributes = [:]; def labels = [:]; def drop_ = [:]; def non_num_ = [:]; sensors.each {sensor-> ids << sensor.idAsLong; //only take what we need sensors_[sensor.id] = [ id: sensor.id, idAsLong: sensor.idAsLong, displayName: sensor.displayName ]; attributes[sensor.id] = settings["attributes_${sensor.id}"]; labels[sensor.id] = [:]; settings["attributes_${sensor.id}"].each { attr -> labels[sensor.id][attr] = settings["graph_name_override_${sensor.id}_${attr}"]; } drop_[sensor.id] = [:]; non_num_[sensor.id] = [:]; settings["attributes_${sensor.id}"].each { attr -> if (settings["attribute_${sensor.id}_${attr}_non_number"]==true){ startString = settings["attribute_${sensor.id}_${attr}_startString"]; endString = settings["attribute_${sensor.id}_${attr}_endString"]; non_num_[sensor.id][attr] = [ valid: true, start: startString, startVal: settings["attribute_${sensor.id}_${attr}_${startString}"], end: endString, endVal: settings["attribute_${sensor.id}_${attr}_${endString}"] ]; } else { non_num_[sensor.id][attr] = [ valid: false, start: "", end: ""]; } drop_[sensor.id][attr] = [valid: settings["attribute_${sensor.id}_${attr}_drop_line"], time: settings["attribute_${sensor.id}_${attr}_drop_time"], value: settings["attribute_${sensor.id}_${attr}_drop_value"]]; } } def obj = [ ids: ids, sensors: sensors_, attributes: attributes, labels : labels, drop : drop_, non_num: non_num_ ] def subscriptions = obj; return render(contentType: "text/json", data: JsonOutput.toJson(subscriptions)); } def getColorCode(code){ ret = "#FFFFFF" switch (code){ case 7: ret = "#800000"; break; case 1: ret = "#FF0000"; break; case 6: ret = "#FFA500"; break; case 8: ret = "#FFFF00"; break; case 9: ret = "#808000"; break; case 2: ret = "#008000"; break; case 5: ret = "#800080"; break; case 4: ret = "#FF00FF"; break; case 10: ret = "#00FF00"; break; case 11: ret = "#008080"; break; case 12: ret = "#00FFFF"; break; case 3: ret = "#0000FF"; break; case 13: ret = "#000080"; break; } return ret; }