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
// 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 Time Graph",
namespace: "tchoward",
author: "Thomas Howard",
description: "Hubigraph Time 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: "longTermStoragePage", 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 getEvents(sensor, attribute, num){
def resp = [:]
def today = new Date();
def then = new Date();
use (groovy.time.TimeCategory) {
then -= 2.days;
}
def respEvents = [];
respEvents << sensor.statesSince(attribute, then, [max: 200]){ it.value };
respEvents = respEvents.flatten();
respEvents = respEvents.unique();
return respEvents;
}
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"], ["1200000":"20 Minutes"], ["1800000":"Half Hour"], ["3600000":"1 Hour"], ["6400000":"2 Hours"], ["19200000":"6 Hours"],
["43200000":"12 Hours"], ["86400000":"1 Day"]];
def timespanEnum = [["60000":"1 Minute"],
["3600000":"1 Hour"],
["43200000":"12 Hours"],
["86400000":"1 Day"],
["259200000":"3 Days"],
["604800000":"1 Week"],
["2419200000":"2 Weeks"],
["1814400000":"3 Weeks"],
["2419200000":"1 Month"],
["custom": "Custom Timespan"]];
def 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"],
["2400000":"30 minutes"], ["3600000":"1 Hour"], ["43200000":"12 Hours"], ["86400000":"1 Day"], ["259200000":"3 Days"], ["604800000":"1 Week"]];
def updateRateEnum = [["-1":"Never"], ["0":"Real Time"], ["1000":"1 Second"], ["60000":"1 Minute"], ["300000":"5 Minutes"], ["600000":"10 Minutes"], ["1200000":"20 Minutes"], ["1800000":"Half Hour"], ["3600000":"1 Hour"]];
dynamicPage(name: "graphSetupPage") {
parent.hubiForm_section(this,"General Options", 1)
{
input( type: "enum", name: "graph_update_rate", title: "Integration Time
(The amount of time each data point covers)",
multiple: false, required: true, options: timespanEnum2, defaultValue: "300000", submitOnChange: true)
input( type: "enum", name: "graph_refresh_rate", title: "Graph Update Rate(For panel viewing; the refresh rate of the graph)",
multiple: false, required: true, options: updateRateEnum, defaultValue: "300000")
container = [];
container << parent.hubiForm_sub_section(this, "Graph Time Span
Amount of time the graph covers");
if (graph_timespan_weeks == null){
app.updateSetting("graph_timespan_weeks", 0);
app.updateSetting("graph_timespan_days", 1);
app.updateSetting("graph_timespan_hours", 0);
app.updateSetting("graph_timespan_minutes", 0);
settings["graph_timespan_weeks"] = 0;
settings["graph_timespan_days"] = 1;
settings["graph_timespan_hours"] = 0;
settings["graph_timespan_minutes"] = 0;
}
container << parent.hubiForm_slider (this, title: "Weeks", name: "graph_timespan_weeks",
default: 0, min: 0, max: 104, units: " weeks", submit_on_change: true);
container << parent.hubiForm_slider (this, title: "Days", name: "graph_timespan_days",
default: 0, min: 0, max: 30, units: " days", submit_on_change: true);
container << parent.hubiForm_slider (this, title: "Hours", name: "graph_timespan_hours",
default: 0, min: 0, max: 24, units: " hours", submit_on_change: true);
container << parent.hubiForm_slider (this, title: "Minutes", name: "graph_timespan_minutes",
default: 0, min: 0, max: 60, units: " seconds", submit_on_change: true);
if (graph_timespan_weeks==null){
secs = 86400000;
} else {
secs = (long)((double)(graph_timespan_weeks)*604800000+
(double)(graph_timespan_days)*86400000+
(double)(graph_timespan_hours)*3600000+
(double)(graph_timespan_minutes)*60000);
}
app.updateSetting("graph_timespan", [type: "number", value: secs]);
points = graph_update_rate ? (long)(secs/Double.parseDouble(graph_update_rate)) : 280;
if (points > 2000) {
container << parent.hubiForm_text (this, """WARNING: ${(points)} Points will be generated per Attribute per Graph
Too many points will cause Hubigraphs to hang or take a long time to generate""");
} else {
container << parent.hubiForm_text (this, "NOTE: ${(points)} Points will be generated per Attribute per Graph");
}
container << parent.hubiForm_sub_section(this, "Other Options");
container << parent.hubiForm_color (this, "Graph Background", "graph_background", "#FFFFFF", false)
container << parent.hubiForm_switch(this, title: "Smooth Graph Points
(Enable Google Graph Smoothing)", 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)
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 Fill % of Graph?
(False = Default (80%) Fill)",
name: "graph_percent_fill", default: false, submit_on_change: true);
if (graph_percent_fill==true){
container << parent.hubiForm_slider (this, title: "Horizontal fill % of the graph", name: "graph_h_fill",
default: 80, min: 1, max: 100, units: "%", submit_on_change: false);
container << parent.hubiForm_slider (this, title: "Vertical fill % of the graph", name: "graph_v_fill",
default: 80, min: 1, max: 100, units: "%", submit_on_change: false);
}
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: 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: 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_text_input (this, "Horizontal Axis Format", "graph_h_format", "", "true");
if (graph_h_format){
today = new Date();
container << parent.hubiForm_text(this, """Horizontal Axis Sample: ${today.format(graph_h_format)}""");
}
container << parent.hubiForm_switch (this, title: "Show String Formatting Help", name: "dummy", default: false, submit_on_change: true);
if (dummy == true){
rows = [];
header = ["Name", "Format", "Result"];
rows << ["Year", "Y", "2020"];
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", "1..23"];
rows << ["Hour (24)", "HH", "01..23"];
rows << ["Minute", "m", "1..59"];
rows << ["Minute", "mm", "01..59"];
rows << ["Seconds", "s", "1..59"];
rows << ["Seconds", "ss", "01..59 "];
container << parent.hubiForm_table(this, header: header, rows: rows);
container << parent.hubiForm_text(this, """Example: "EEEE, MMM d, Y hh:mm:ss a"
= "Monday, June 2, 2020 08:21:33 AM""")
}
parent.hubiForm_container(this, container, 1);
}
//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
def formatEnum = [["": "No Formatting ::: 12345"], ["decimal":"Decimal ::: 12,345"], ["short": "Short ::: 12K"], ["scientific": "Scientific ::: 1e5"], ["percent": "Percent ::: 1234500%"], ["long": "Long ::: 12 Thousand"]];
parent.hubiForm_section(this,"Left Axis", 1, "arrow_back"){
input( type: "enum", name: "graph_vaxis_1_format", title: "Number Format", multiple: false, required: true, options: formatEnum, defaultValue: "")
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"){
input( type: "enum", name: "graph_vaxis_2_format", title: "Number Format", multiple: false, required: true, options: formatEnum, defaultValue: "")
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_inside_position", title: "Legend Justification", defaultValue: "center", options: insidePosition);
} else {
parent.hubiForm_container(this, container, 1);
}
}
parent.hubiForm_section(this, "Current Value Overlay", 1){
def horizonalAlignmentEnum = ["Left", "Middle", "Right"];
def veticalAlignmentEnum = ["Top", "Middle", "Bottom"];
container = [];
container << parent.hubiForm_switch (this, title: "Show Current Values on Graph?", name: "show_overlay", default: false, submit_on_change: true);
if (show_overlay == true){
container << parent.hubiForm_color (this, "Background", "overlay_background", "#000000", false);
container << parent.hubiForm_slider (this, title: "Background Opacity",
name: "overlay_background_opacity",
default: 90,
min: 0,
max: 100,
units: "%",
submit_on_change: false);
container << parent.hubiForm_font_size (this, title: "Device", name: "overlay", default: 12, min: 2, max: 40);
container << parent.hubiForm_color (this, "Device Text", "overlay_text", "#FFFFFF", false);
container << parent.hubiForm_enum (this, title: "Horizontal Placement",
name: "overlay_horizontal_placement",
list: horizonalAlignmentEnum,
default: "Right");
container << parent.hubiForm_enum (this, title: "Vertical Placement",
name: "overlay_vertical_placement",
list: veticalAlignmentEnum,
default: "Top");
container << parent.hubiForm_sub_section(this, "Display Order");
parent.hubiForm_container(this, container, 1);
container = [];
parent.hubiForm_list_reorder(this, "overlay_order", "background");
}
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;
def bar_size_shown = false;
//Deal with Global-Specific Settings (i.e bar spacing and plot-point size)
def show_tile = false;
def show_bar = false;
def show_scatter = false;
sensors.each { sensor ->
settings["attributes_${sensor.id}"].each { attribute ->
switch (settings["graph_type_${sensor.id}_${attribute}"]){
case "Bar" : show_title = true; show_bar = true; break;
}
}
}
if (show_title){
parent.hubiForm_section(this,"Overall Settings for Graph Types", 1){
container = [];
if (show_bar) {
container << parent.hubiForm_slider (this, title: "Bar Graphs:: Relative Width for Bars",
name: "graph_bar_width",
default: 90,
min: 0,
max: 100,
units: "%",
submit_on_change: false);
}
parent.hubiForm_container(this, container, 1);
}
}
sensors.each { sensor ->
settings["attributes_${sensor.id}"].each { attribute ->
parent.hubiForm_section(this,"${sensor.displayName} - ${attribute}", 1){
container = [];
if (parent.ltsAvailable(sensor.id, attribute)){
container << parent.hubiForm_sub_section(this, "Long Term Storage");
container << parent.hubiForm_switch(this, title: "Long Term Storage Available, Use it?", name: "var_${sensor.id}_${attribute}_lts", default: false, submit_on_change: false);
} else {
app.updateSetting ("var_${sensor.id}_${attribute}_lts", [type: "bool", value: "false"]);
}
container << parent.hubiForm_sub_section(this, "Plot Options");
container << parent.hubiForm_enum (this, title: "Plot Type",
name: "graph_type_${sensor.id}_${attribute}",
list: ["Line", "Area", "Scatter", "Bar", "Stepped"],
default: "Line",
submit_on_change: true);
container << parent.hubiForm_enum (this, title: "Time Integration Function",
name: "var_${sensor.id}_${attribute}_function",
list: ["Average", "Min", "Max", "Mid", "Sum"],
default: "Average");
container << parent.hubiForm_enum (this, title: "Axis Side",
name: "graph_axis_number_${sensor.id}_${attribute}",
list: ["Left", "Right"],
default: "Left");
def colorText = "";
def fillText = "";
def graphType = settings["graph_type_${sensor.id}_${attribute}"];
switch (graphType){
case "Line":
colorText = "Line";
break;
case "Area":
colorText = "Area Line";
fillText = "Fill";
break;
case "Bar":
colorText = "Bar Border";
fillText = "Fill";
break;
case "Scatter":
colorText = "Border";
fillText = "Fill";
break;
case "Stepped":
colorText = "Line";
fillText = "Fill";
break;
}
container << parent.hubiForm_sub_section(this, colorText+" Options");
container << parent.hubiForm_color(this, colorText,
"var_${sensor.id}_${attribute}_stroke",
parent.hubiTools_rotating_colors(cnt),
false);
container << parent.hubiForm_slider (this, title: colorText+" Opacity",
name: "var_${sensor.id}_${attribute}_stroke_opacity",
default: 90,
min: 0,
max: 100,
units: "%",
submit_on_change: false);
container << parent.hubiForm_line_size (this, title: colorText,
name: "var_${sensor.id}_${attribute}_stroke",
default: 2, min: 1, max: 20);
if (graphType == "Bar" || graphType == "Area" || graphType == "Stepped") {
container << parent.hubiForm_sub_section(this, graphType+" "+fillText+" Options");
container << parent.hubiForm_color(this, fillText,
"var_${sensor.id}_${attribute}_fill",
parent.hubiTools_rotating_colors(cnt),
false);
container << parent.hubiForm_slider (this, title: fillText+" Opacity",
name: "var_${sensor.id}_${attribute}_fill_opacity",
default: 90,
min: 0,
max: 100,
units: "%",
submit_on_change: false);
}
if (graphType == "Scatter" || graphType == "Line" || graphType == "Area"){
container << parent.hubiForm_sub_section(this, "Data Points");
if (graphType == "Line" || graphType == "Area"){
container << parent.hubiForm_switch(this, title: "Display Data Points on Line?", name: "var_${sensor.id}_${attribute}_line_plot_points", default: false, submit_on_change: true);
}
if (settings["var_${sensor.id}_${attribute}_line_plot_points"] || graphType == "Scatter"){
container << parent.hubiForm_enum (this,
title: "Point Type",
name: "var_${sensor.id}_${attribute}_point_type",
list: [ "Circle", "Triangle", "Square", "Diamond", "Star", "Polygon"],
default: "Circle");
container << parent.hubiForm_slider (this, title: "Point Size",
name: "var_${sensor.id}_${attribute}_point_size",
default: 5,
min: 0,
max: 60,
units: " points",
submit_on_change: false);
if (graphType == "Area") {
container << parent.hubiForm_text (this, "*Note, Area Plots use the same fill setting for Points and Area (Above)");
} else {
container << parent.hubiForm_color(this, "Point Fill",
"var_${sensor.id}_${attribute}_fill",
parent.hubiTools_rotating_colors(cnt),
false);
container << parent.hubiForm_slider (this, title: "Point Fill Opacity",
name: "var_${sensor.id}_${attribute}_fill_opacity",
default: 90,
min: 0,
max: 100,
units: "%",
submit_on_change: false);
}
} else {
app.updateSetting ("var_${sensor.id}_${attribute}_point_size", 0);
}
}
currentAttribute = null;
enumType = false;
sensor.getSupportedAttributes().each{attrib->
if (attrib.name == attribute){
currentAttribute = attrib;
if (attrib.dataType == "ENUM") enumType = true;
}
}
if (enumType == true){
possible_values = currentAttribute.getValues();
def count_ = 0;
container << parent.hubiForm_sub_section(this, """Numerical values for "$attribute" states""");
possible_values.each{value->
container << parent.hubiForm_text_input(this, "Value for $value",
"attribute_${sensor.id}_${attribute}_${value}",
"100",
false);
count_++;
}
app.updateSetting ("attribute_${sensor.id}_${attribute}_states", possible_values);
}
if (enumType == false){
container << parent.hubiForm_sub_section(this, """Custom State Values for "$attribute" """ );
if (settings["attribute_${sensor.id}_${attribute}_custom_states"] == null){
app.updateSetting("attribute_${sensor.id}_${attribute}_custom_states", [type: "bool", value: "false"]);
}
container << parent.hubiForm_switch(this, title: "Set Custom State Values?
(For custom drivers w/ non-numeric values)",
name: "attribute_${sensor.id}_${attribute}_custom_states",
default: false,
submit_on_change: true);
if (settings["attribute_${sensor.id}_${attribute}_custom_states"] == true){
if (!settings["attribute_${sensor.id}_${attribute}_num_custom_states"]){
}
container << parent.hubiForm_text_input(this,"Number of Custom States",
"attribute_${sensor.id}_${attribute}_num_custom_states",
"2", "true");
def numStates = Integer.parseInt(settings["attribute_${sensor.id}_${attribute}_num_custom_states"]);
for (def i=0; iState #"+(i)+"",
"attribute_${sensor.id}_${attribute}_custom_state_${i}",
"",
"true");
if (settings["attribute_${sensor.id}_${attribute}_custom_state_${i}"]){
subcontainer << parent.hubiForm_text_input(this, 'Value for "'+settings["attribute_${sensor.id}_${attribute}_custom_state_${i}"]+'"',
"attribute_${sensor.id}_${attribute}_custom_state_${i}_value",
"0",
"true");
}
container << parent.hubiForm_subcontainer(this, objects: subcontainer, breakdown: [0.5, 0.5]);
}
//Update Settings
possible_values = [];
for (i=0; i
app.updateSetting ("attribute_${sensor.id}_${attribute}_${val}", 0);
}
app.updateSetting ("attribute_${sensor.id}_${attribute}_states", 0);
}
}
}
//Line and Area Graphs can be "Drop-line"
if ((graphType == "Line" || graphType == "Area" || graphType == "Stepped") && enumType==false && settings["attribute_${sensor.id}_${attribute}_custom_states"] == false) {
container << parent.hubiForm_sub_section(this, "Handle Missing Values");
container << parent.hubiForm_switch(this, title: "Display Missing Data 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 of Missing Data",
"attribute_${sensor.id}_${attribute}_drop_value",
"0", false);
}
container << parent.hubiForm_switch(this, title: "Extend Left Value?
When values are unavailable, extend value to left",
name: "attribute_${sensor.id}_${attribute}_extend_left", default: false, submit_on_change: false);
container << parent.hubiForm_switch(this, title: "Extend Right Value?
When values are unavailable, extend value to right",
name: "attribute_${sensor.id}_${attribute}_extend_right", default: false, submit_on_change: false);
}
container << parent.hubiForm_sub_section(this, "Restrict Displayed Values");
container << parent.hubiForm_switch(this, title: "Restrict Displaying Bad Values?", name: "attribute_${sensor.id}_${attribute}_bad_value", default: false, submit_on_change: true);
if (settings["attribute_${sensor.id}_${attribute}_bad_value"]==true){
container << parent.hubiForm_text_input(this,"Min Value to Exclude
If the recorded sensor value is below this value it will be dropped",
"attribute_${sensor.id}_${attribute}_min_value",
"0", false);
container << parent.hubiForm_text_input(this,"Max Value to Exclude
If the recorded sensor value is above this value it will be dropped",
"attribute_${sensor.id}_${attribute}_max_value",
"100", false);
}
container << parent.hubiForm_sub_section(this, "Override Display Name on Graph");
container << parent.hubiForm_text_input(this, "Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE",
"graph_name_override_${sensor.id}_${attribute}",
"%deviceName%: %attributeName%", false);
container << parent.hubiForm_text_input(this, "Units for Pretty Display",
"units_${sensor.id}_${attribute}",
"", false);
parent.hubiForm_container(this, container, 1);
cnt += 1;
}//parent.hubiForm
}//attribute
}//sensor
}//page
}//function
def deviceSelectionPage() {
def final_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 {
attributes_ = it.getSupportedAttributes();
final_attrs = [];
attributes_.each{ attribute_->
name = attribute_.getName();
if (it.currentState(name)){
final_attrs << ["$name" : "$name ::: [${it.currentState(name).getValue()}]"];
}
}
final_attrs = final_attrs.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: final_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() {
unschedule();
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, "${fullLocalApiServerUrl("")}graph/?access_token=${state.endpointSecret}",
"${fullLocalApiServerUrl("")}graph/?access_token=${state.endpointSecret}");
parent.hubiForm_container(this, container, 1);
}
if (graph_timespan!=null){
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 Time 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 getDays(str){
switch (str){
case "1 Day": return 1; break;
case "2 Days": return 2; break;
case "3 Days": return 3; break;
case "4 Days": return 4; break;
case "5 Days": return 5; break;
case "6 Days": return 6; break;
case "1 Week": return 7; break;
case "2 Weeks": return 14; break;
case "3 Weeks": return 21; break;
case "1 Month": return 30; break;
case "2 Months": return 60; break;
case "Indefinite": return 0; break;
}
}
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]+/;
orig = val;
val = "${val}"
val = val.replaceAll("\\s","");
if (settings["attribute_${id}_${attr}_${val}"]!=null){
ret = Double.parseDouble(settings["attribute_${id}_${attr}_${val}"]);
} else {
try {
ret = Double.parseDouble(val - reg);
} catch (e) {
log.debug ("Bad value in Parse: "+orig);
ret = null;
}
}
return ret;
}
private cleanupData(data){
def then = new Date();
use (groovy.time.TimeCategory) {
then -= getDays(lts_time);
}
then_milliseconds = then.getTime();
return data.findAll{ it.date >= then_milliseconds };
}
private buildData() {
def resp = [:]
def graph_time;
def then = new Date();
use (groovy.time.TimeCategory) {
val = Double.parseDouble("${graph_timespan}")/1000.0;
then -= ((int)val).seconds;
graph_time = then.getTime();
}
if(sensors) {
sensors.each { sensor ->
resp[sensor.id] = [:];
settings["attributes_${sensor.id}"].each {attribute ->
start = new Date();
data = parent.getData(sensor, attribute, settings["var_${sensor.id}_${attribute}_lts"], then);
data = data.collect{[date: it.date.getTime(), value: getValue(sensor.id, attribute, it.value)]}
resp[sensor.id][attribute] = data.findAll{ it.date > graph_time};
//Restrict "bad" values
if (settings["attribute_${sensor.id}_${attribute}_bad_value"]==true){
min = Float.valueOf(settings["attribute_${sensor.id}_${attribute}_min_value"]);
max = Float.valueOf(settings["attribute_${sensor.id}_${attribute}_max_value"]);
resp[sensor.id][attribute] = resp[sensor.id][attribute].findAll{ it.value > min && it.value < max};
}
atomicState["history_${sensor.id}_${attribute}"] = null;
end = new Date();
}
}
}
return resp;
}
def getChartOptions(){
/*Setup Series*/
def series = ["series" : [:]];
def options = [
"graphReduction": graph_max_points,
"graphTimespan": Long.parseLong("${graph_timespan}"),
"graphUpdateRate": Integer.parseInt(graph_update_rate),
"graphRefreshRate" : Integer.parseInt(graph_refresh_rate),
"overlays": [ "display_overlays" : show_overlay,
"horizontal_alignment" : overlay_horizontal_placement,
"vertical_alignment" : overlay_vertical_placement,
"order" : overlay_order
],
"graphOptions": [
"tooltip" : ["format" : "short"],
"width": graph_static_size ? graph_h_size : "100%",
"height": graph_static_size ? graph_v_size : "100%",
"chartArea": [ "width": graph_percent_fill ? "${graph_h_fill}%" : "80%",
"height": graph_percent_fill ? "${graph_v_fill}%" : "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],
"format": graph_vaxis_1_format,
],
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],
"format": graph_vaxis_2_format,
]
],
"bar": [ "groupWidth" : graph_bar_width+"%", "fill-opacity" : 0.5],
"pointSize": graph_scatter_size,
"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": [:],
]
];
count_ = 0;
temp_sensors = sensors.sort{it.id.toInteger()};
temp_sensors.each { sensor ->
settings["attributes_${sensor.id}"].each { attribute ->
def type_ = settings["graph_type_${sensor.id}_${attribute}"].toLowerCase();
if (type_ == "stepped") type_ = "steppedArea";
def axes_ = settings["graph_axis_number_${sensor.id}_${attribute}"] == "Left" ? 0 : 1;
def stroke_color = settings["var_${sensor.id}_${attribute}_stroke_color"];
def stroke_opacity = settings["var_${sensor.id}_${attribute}_stroke_opacity"];
def stroke_line_size = settings["var_${sensor.id}_${attribute}_stroke_line_size"];
def fill_color = settings["var_${sensor.id}_${attribute}_fill_color"];
def fill_opacity = settings["var_${sensor.id}_${attribute}_fill_opacity"];
def point_size = settings["var_${sensor.id}_${attribute}_point_size"];
def point_type = settings["var_${sensor.id}_${attribute}_point_type"] != null ? settings["var_${sensor.id}_${attribute}_point_type"].toLowerCase() : "";
type_ = type_=="bar" ? "bars" : type_;
options.graphOptions.series << ["$count_" : [ "type" : type_,
"targetAxisIndex" : axes_,
"pointSize" : point_size,
"pointShape" : point_type,
"color" : stroke_color,
"opacity" : stroke_opacity,
]
];
count_ ++;
}
}
//add colors and thicknesses
sensors.each { sensor ->
settings["attributes_${sensor.id}"].each { attribute ->
def axis = settings["graph_axis_number_${sensor.id}_${attribute}"] == "Left" ? 0 : 1;
def text_color = settings["graph_line_${sensor.id}_${attribute}_color"];
def text_color_transparent = settings["graph_line_${sensor.id}_${attribute}_color_transparent"];
def annotations = [
"targetAxisIndex": axis,
"color": text_color_transparent ? "transparent" : text_color
];
options.graphOptions.series << annotations;
}
}
return options;
}
def getDrawType(){
return "google.visualization.LineChart"
}
def removeLastChar(str) {
str.subSequence(0, str.length() - 1)
str
}
def getRGBA(hex, opacity){
def c = hex-"#";
c = c.toUpperCase();
i = Integer.parseInt(c, 16);
r = (i & 0xFF0000) >> 16;
g = (i & 0xFF00) >> 8;
b = (i & 0xFF);
o = opacity/100.0;
s = sprintf("rgba( %d, %d, %d, %.2f)", r, g, b, o);
return s;
}
def getLineGraph() {
def fullSizeStyle = "margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden";
def html = """
"""
if (show_overlay==true) html+= getOverlay();
html+= """
"""
return html;
}
def getOverlay(){
def html = """"""
val = new JsonSlurper().parseText(overlay_order)
val.each{ str->
splitStr = str.split('_');
deviceId = splitStr[1];
attribute = splitStr[2];
sensor = sensors.find{ it.id == deviceId } ;
val = getValue(sensor.id, attribute, sensor.currentState(attribute).getValue());
units = settings["units_${sensor.id}_${attribute}"] ? settings["units_${sensor.id}_${attribute}"] : "";
name = settings["graph_name_override_${sensor.id}_${attribute}"];
name = name.replaceAll("%deviceName%", sensor.displayName).replaceAll("%attributeName%", attribute);
str = sprintf("%.1f%s", val, units);
html += """${name} |
${str} |
"""
}
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("/graph")
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 extend_ = [:];
def var_ = [:];
def graph_type_ = [:];
def states_ = [:];
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] = [:];
extend_[sensor.id] = [:];
graph_type_[sensor.id] = [:];
var_[sensor.id] = [:];
states_[sensor.id] = [:];
settings["attributes_${sensor.id}"].each { attr ->
def stroke_color = settings["var_${sensor.id}_${attr}_stroke_color"];
def stroke_opacity = settings["var_${sensor.id}_${attr}_stroke_opacity"];
def stroke_line_size = settings["var_${sensor.id}_${attr}_stroke_line_size"];
def fill_color = settings["var_${sensor.id}_${attr}_fill_color"];
def fill_opacity = settings["var_${sensor.id}_${attr}_fill_opacity"];
def function = settings["var_${sensor.id}_${attr}_function"];
if (settings["attribute_${sensor.id}_${attr}_states"] && settings["attribute_${sensor.id}_${attr}_custom_states"] == true){
states_[sensor.id][attr] = [:];
settings["attribute_${sensor.id}_${attr}_states"].each{states->
states_[sensor.id][attr][states] = settings["attribute_${sensor.id}_${attr}_${states}"];
}
}
drop_valid = false;
if (settings["attribute_${sensor.id}_${attr}_drop_line"] == true)
drop_valid = true;
drop_[sensor.id][attr] = [ valid: drop_valid ? "true" : "false",
value: drop_valid ? settings["attribute_${sensor.id}_${attr}_drop_value"] : "null"
];
extend_[sensor.id][attr] = [
right: settings["attribute_${sensor.id}_${attr}_extend_right"],
left: settings["attribute_${sensor.id}_${attr}_extend_left"]
];
graph_type_[sensor.id][attr] = settings["graph_type_${sensor.id}_${attr}"];
var_[sensor.id][attr] = [ stroke_color : stroke_color,
stroke_opacity : stroke_opacity,
stroke_width: stroke_line_size,
fill_color: fill_color,
fill_opacity: fill_opacity,
function: function,
units: settings["units_${sensor.id}_${attr}"] ? settings["units_${sensor.id}_${attr}"] : "",
];
}//settings
} //sensors
def obj = [
ids: ids.sort(),
sensors: sensors_,
attributes: attributes,
labels : labels,
drop : drop_,
extend: extend_,
graph_type: graph_type_,
var : var_,
states: states_
]
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;
}