/**
* Hubigraph BarGraph 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 Bar Graph Changelog
// V 0.1 Intial release
// V 0.2 Ordering, Color and Common API Update
// V 1.8 Smoother sliders, bug fixes
import groovy.json.JsonOutput
def ignoredEvents() { return [ 'lastReceive' , 'reachable' ,
'buttonReleased' , 'buttonPressed', 'lastCheckinDate', 'lastCheckin', 'buttonHeld' ] }
def version() { return "v0.22" }
definition(
name: "Hubigraph Bar Graph",
namespace: "tchoward",
author: "Thomas Howard",
description: "Hubigraph Bar 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 {
page(name: "mainPage", install: true, uninstall: true)
page(name: "deviceSelectionPage", nextPage: "attributeConfigurationPage")
page(name: "attributeConfigurationPage", nextPage: "mainPage")
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();
}
def getAttributeType(attrib, title){
switch (attrib){
case "motion": return ["motion", "Motion (active/inactive)"];
case "switch": return ["switch", "Switch (on/off)"];
case "contact": return ["contact", "Contact (open/close)"];
case "acceleration": return ["acceleration", "Acceleration (active/inactive)"]
case "audioVolume":
case "number": return [title, "Number (Choose threshold)"];
}
}
def deviceSelectionPage() {
dynamicPage(name: "deviceSelectionPage") {
parent.hubiForm_section(this, "Device Selection", 1) {
input (type: "capability.*", name: "sensors", title: "Choose Sensors", multiple: true, submitOnChange: true)
if (sensors) {
def all = (1..sensors.size()).collect{ "" + it };
sensors.eachWithIndex {sensor, idx ->
id = sensor.id;
sensor_attributes = sensor.getSupportedAttributes().collect { it.getName() }.unique();
container = [];
container << parent.hubiForm_sub_section(this, "${sensor.displayName}");
parent.hubiForm_container(this, container, 1);
input( type: "enum", name: "attributes_${id}", title: "Attributes to graph", required: true, multiple: true, options: sensor_attributes, defaultValue: "1", submitOnChange: false )
}
}
}
}
}
def attributeConfigurationPage() {
dynamicPage(name: "attributeConfigurationPage") {
parent.hubiForm_section(this, "Directions", 1, "directions"){
container = [];
container << parent.hubiForm_text(this, "Choose Numeric Attributes Only");
parent.hubiForm_container(this, container, 1);
}
parent.hubiForm_section(this, "Graph Order", 1, "directions"){
parent.hubiForm_list_reorder(this, "graph_order", "background");
}
def decimalsEnum = [[0:"None (123)"], [1: "One (123.1)"], [2: "Two (123.12)"], [3: "Three (123.123)"], [4: "Four (123.1234"]];
count = 0;
sensors.each { sensor ->
attributes = settings["attributes_${sensor.id}"];
attributes.each { attribute ->
container = [];
parent.hubiForm_section(this, "${sensor.displayName} ${attribute}", 1, "directions"){
input( type: "enum", name: "attribute_${sensor.id}_${attribute}_decimals",
title: "Number of Decimal Places to Display",
multiple: false, required: false, options: decimalsEnum,
defaultValue: 1);
container << parent.hubiForm_text_input(this, "Scale Factor for Values
Example: To scale down by 10X, input 0.1
Leave as 1 for unchanged",
"attribute_${sensor.id}_${attribute}_scale",
"1", false);
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);
container << parent.hubiForm_color (this, "Bar Background", "attribute_${sensor.id}_${attribute}_background", "#3e4475", false, true);
container << parent.hubiForm_color (this, "Bar Border", "attribute_${sensor.id}_${attribute}_current_border", "#FFFFFF", false);
container << parent.hubiForm_slider (this, title: "Bar Opacity",
name: "attribute_${sensor.id}_${attribute}_opacity",
default_value: 100, min: 1, max: 100, units: "%");
container << parent.hubiForm_line_size (this, title: "Bar Border",
name: "attribute_${sensor.id}_${attribute}_current_border",
default: 2, min: 1, max: 10);
container << parent.hubiForm_switch (this, title: "Show Current Value on Bar",
name: "attribute_${sensor.id}_${attribute}_show_value",
default: false,
submit_on_change: true);
if (settings["attribute_${sensor.id}_${attribute}_show_value"]==true){
container<< parent.hubiForm_text_input(this, "Units", "attribute_${sensor.id}_${attribute}_annotation_units", "", false)
}
parent.hubiForm_container(this, container, 1);
}
}
}
}
}
def graphSetupPage(){
def rateEnum = [["-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 = [[0:"Live"], [1:"Hourly"], [2:"Daily"], [3:"Every Three Days"], [4:"Weekly"]];
dynamicPage(name: "graphSetupPage") {
parent.hubiForm_section(this, "General Options", 1){
container = [];
input( type: "enum", name: "graph_type", title: "Select graph type", multiple: false, required: false, options: [["1": "Bar Chart"],["2": "Column Chart"]], defaultValue: "1");
input( type: "enum", name: "graph_update_rate", title: "Select graph update rate", multiple: false, required: false, options: rateEnum, defaultValue: "0")
container << parent.hubiForm_color (this, "Graph Background", "graph_background", "#FFFFFF", false)
container << parent.hubiForm_slider (this, title: "Graph Bar Width (1%-100%)", name: "graph_bar_percent", default: 90, min: 1, max: 100, units: "%");
container << parent.hubiForm_text_input(this, "Graph Max", "graph_max", "", false);
container << parent.hubiForm_text_input(this, "Graph Min", "graph_min", "", false);
parent.hubiForm_container(this, container, 1);
}
parent.hubiForm_section(this, "Axes", 1){
container = [];
container << parent.hubiForm_color (this, "Axis", "haxis", "#000000", false);
container << parent.hubiForm_font_size (this, title: "Axis", name: "haxis", default: 9, min: 2, max: 20);
container << parent.hubiForm_slider (this, title: "Number of Pixels for Axis", name: "graph_h_buffer", default: 40, min: 10, max: 500, units: " pixels");
parent.hubiForm_container(this, container, 1);
}
parent.hubiForm_section(this, "Device Names", 1){
container = [];
container << parent.hubiForm_font_size (this, title: "Device Name", name: "graph_axis", default: 9, min: 2, max: 20);
container << parent.hubiForm_color (this, "Device Name","graph_axis", "#000000", false);
container << parent.hubiForm_slider (this, title: "Number of Pixels for Device Name Area", name: "graph_v_buffer", default: 100, min: 10, max: 500, units: " pixels");
parent.hubiForm_container(this, container, 1);
}
parent.hubiForm_section(this, "Graph Size", 1){
container = [];
input( type: "bool", name: "graph_static_size", title: "Set size of Graph?
(False = Fill Window)", defaultValue: false, submitOnChange: 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");
container << parent.hubiForm_slider (this, title: "Vertical dimension of the graph", name: "graph_v_size", default: 600, min: 100, max: 3000, units: " pixels");
}
parent.hubiForm_container(this, container, 1);
}
parent.hubiForm_section(this, "Annotations", 1){
container = [];
container << parent.hubiForm_font_size (this, title: "Annotation", name: "annotation", default: 16, min: 2, max: 40);
container << parent.hubiForm_switch (this, title: "Show Annotation Outside (true) or Inside (false) of Bars", name: "annotation_inside", default:false);
container << parent.hubiForm_color (this, "Annotation", "annotation", "#FFFFFF", false);
container << parent.hubiForm_color (this, "Annotation Aura", "annotation_aura", "#000000", false);
container << parent.hubiForm_switch (this, title: "Bold Annotation", name: "annotation_bold", default:false);
container << parent.hubiForm_switch (this, title: "Italic Annotation", name: "annotation_bold", default:false);
parent.hubiForm_container(this, container, 1);
}
}
}
def disableAPIPage() {
dynamicPage(name: "disableAPIPage") {
section() {
if (state.endpoint) {
try {
revokeAccessToken();
}
catch (e) {
log.debug "Unable to revoke access token: $e"
}
state.endpoint = null
}
paragraph "It has been done. Your token has been REVOKED. Tap Done to continue."
}
}
}
def enableAPIPage() {
dynamicPage(name: "enableAPIPage", title: "") {
section() {
if(!state.endpoint) initializeAppEndpoint();
if (!state.endpoint){
paragraph "Endpoint creation failed"
} else {
paragraph "It has been done. Your token has been CREATED. Tap 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_update_rate){
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
}
def installed() {
log.debug "Installed with settings: ${settings}"
updated();
}
def uninstalled() {
if (state.endpoint) {
try {
log.debug "Revoking API access token"
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);
state.dataName = attribute;
if (install_device == true){
parent.hubiTool_create_tile(this);
}
}
def buildData() {
def resp = [:]
def now = new Date();
def then = new Date();
if(sensors) {
sensors.each {sensor ->
def attributes = settings["attributes_${sensor.id}"];
resp[sensor.id] = [:];
attributes.each { attribute ->
latest = sensor.latestState(attribute);
resp[sensor.id][attribute] = [current: latest.getFloatValue(), date: latest.getDate()];
}
}
}
return resp
}
def getChartOptions(){
colors = [];
sensors.each {sensor->
def attributes = settings["attributes_${sensor.id}"];
attributes.each {attribute->
attrib_string = "attribute_${sensor.id}_${attribute}_color"
transparent_attrib_string = "attribute_${sensor.id}_${attribute}_color_transparent"
colors << (settings[transparent_attrib_string] ? "transparent" : settings[attrib_string]);
}
}
if (graph_type == "1"){
axis1 = "hAxis";
axis2 = "vAxis";
} else {
axis1 = "vAxis";
axis2 = "hAxis";
}
def options = [
"graphUpdateRate": Integer.parseInt(graph_update_rate),
"graphType": Integer.parseInt(graph_type),
"graphOptions": [
"bar" : [ "groupWidth" : "${graph_bar_percent}%",
],
"width": graph_static_size ? graph_h_size : "100%",
"height": graph_static_size ? graph_v_size: "90%",
"timeline": [
"rowLabelStyle": ["fontSize": graph_axis_font, "color": graph_axis_color_transparent ? "transparent" : graph_axis_color],
"barLabelStyle": ["fontSize": graph_axis_font]
],
"backgroundColor": graph_background_color_transparent ? "transparent" : graph_background_color,
"isStacked": false,
"chartArea": [ "left": graph_type == "1" ? graph_v_buffer : graph_h_buffer,
"right" : 10,
"top": 10,
"bottom": graph_type == "1" ? graph_h_buffer : graph_v_buffer ],
"legend" : [ "position" : "none" ],
"${axis1}": [ "viewWindow" : ["max" : graph_max,
"min" : graph_min],
"minValue" : graph_min,
"maxValue" : graph_max,
"textStyle" : ["color": haxis_color_transparent ? "transparent" : haxis_color,
"fontSize": haxis_font]
],
"${axis2}": [ "textStyle" : ["color": graph_axis_color_transparent ? "transparent" : graph_axis_color,
"fontSize": graph_axis_font]
],
"annotations" : [ "alwaysOutside": annotation_inside,
"textStyle": [
"fontSize": annotation_font,
"bold": annotation_bold,
"italic": annotation_italic,
"color": annotation_color_transparent ? "transparent" : annotation_color,
"auraColor":annotation_aura_color_transparent ? "transparent" : annotation_aura_color,
],
"stem": [ "color": "transparent" ],
"highContrast": "false"
],
],
"graphLow": graph_min,
"graphHigh": graph_max,
]
return options;
}
void removeLastChar(str) {
str.subSequence(0, str.length() - 1)
}
def getTimeLine() {
def fullSizeStyle = "margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden";
def html = """