/**
* Hubigraph HeatMap 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 Heat Map Changelog
// V 1.0 Intial release
import groovy.json.JsonOutput
def ignoredEvents() { return [ 'lastReceive' , 'reachable' ,
'buttonReleased' , 'buttonPressed', 'lastCheckinDate', 'lastCheckin', 'buttonHeld' ] }
def version() { return "v0.22" }
definition(
name: "Hubigraph Heat Map",
namespace: "tchoward",
author: "Thomas Howard",
description: "Hubigraph Heat Map",
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 getFilterName(filter){
switch (filter){
case "capability.*": return "Sensor";
case "capability.temperatureMeasurement": return "Temperature";
case "capability.relativeHumidityMeasurement": return "Humidity";
case "capability.battery": return "Battery";
case "capability.motionSensor": return "Motion";
case "capability.contactSensor": return "Contact";
case "capability.switch": return "Switch";
}
}
def deviceSelectionPage() {
def final_attrs;
filterText = "capability.*";
def filterEnum = [["capability.*": "All Capabilities"],
["capability.temperatureMeasurement": "Temperature"],
["capability.relativeHumidityMeasurement": "Humidity"],
["capability.battery": "Battery"],
["capability.motionSensor": "Motion"],
["capability.contactSensor": "Contact"],
["capability.switch": "Switch"],
];
def fillEnum = [["default": "Select to Fill...."],
["temperature": "Temperature"],
["humidity": "Humidity"],
["battery": "Battery"],
["motion": "Motion"],
["contact" : "Contact"],
["switch": "Switch"],
["lastupdate": "Last Update"],
];
dynamicPage(name: "deviceSelectionPage") {
parent.hubiForm_section(this,"Attribute Filter", 1){
input( type: "enum", name: "filter", title: "Attributes Filter", required: true, multiple: false, options: filterEnum, defaultValue: "All Capabilities", submitOnChange: true)
}
parent.hubiForm_section(this,"Device Selection", 1){
input "sensors", filter, title: getFilterName(filter)+" Devices", multiple: true, required: true, submitOnChange: true
//input "sensors", "capability.temperatureMeasurement", title: getFilterName(filter)+" Devices", multiple: true, required: true, submitOnChange: true
if (sensors){
def restValue;
resetValue = fill_value ? fill_value : "default";
if (resetValue != "default") {
app.updateSetting ("fill_value", ["default"]);
}
input( type: "enum", name: "fill_value", title: "Auto Fill Value
Selecting will cause page to refresh with selected value filled in below", multiple: false, required: false, options: fillEnum, defaultValue: "default", submitOnChange:true)
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);
final_attrs << ["lastupdate": "last activity ::: [${it.getLastActivity()}]"];
container = [];
container << parent.hubiForm_sub_section(this, it.displayName);
parent.hubiForm_container(this, container, 1);
default_ = getFilterName(filter).toLowerCase();
if (resetValue!="default") {
app.updateSetting ("attributes_${it.id}", [resetValue]);
}
input( type: "enum", name: "attributes_${it.id}", title: "Attributes to graph", required: true, multiple: true, options: final_attrs, defaultValue: default_);
}
}
}
}
}
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");
}
sensors.each { sensor ->
attributes = settings["attributes_${sensor.id}"];
attributes.each { attribute ->
container = [];
parent.hubiForm_section(this, "${sensor.displayName} ${attribute}", 1, "directions"){
container << parent.hubiForm_text_input(this, "Use %deviceName% for DEVICE and %attributeName% for ATTRIBUTE",
"graph_name_override_${sensor.id}_${attribute}",
"%deviceName%: %attributeName%", false);
parent.hubiForm_container(this, container, 1);
}
}
}
}
}
def dd(num){
if (num<10) return "0"+num.toInteger();
else return num.toInteger();
}
def convertToString(msec_){
def msec = msec_.toInteger();
if (msec == "0" || msec == 0) return "00:00:00";
def hours = Math.floor(msec/3600000);
def mins = Math.floor((msec%3600000)/60000);
def secs = Math.floor((msec%60000)/1000);
return dd(hours)+":"+dd(mins)+":"+dd(secs);
}
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 decayEnum = [["1000":"1 Second"], ["30000":"30 Seconds"], ["60000":"1 Minute"], ["300000":"5 Minutes"], ["600000":"10 Minutes"],
["1800000":"Half Hour"], ["3600000":"1 Hour"], ["7200000":"2 Hours"], ["21600000":"6 Hours"], ["43200000":"12 Hours"], ["86400000":"1 Day"],
["172800000":"2 Days"], ["259200000":"3 Days"], ["345600000":"4 Days"], ["432000000":"5 Days"], ["518400000":"6 Days"], ["604800000":"7 Days"]];
def timespanEnum = [[0:"Live"], [1:"Hourly"], [2:"Daily"], [3:"Every Three Days"], [4:"Weekly"]];
def typeEnum = [["value": "Value"], ["time" : "Trigger (Time Since Last Update)"]];
def count_ = 0;
//Get Device Count
sensors.each { sensor ->
attributes = settings["attributes_${sensor.id}"];
attributes.each { attribute ->
count_++;
}
}
app.updateSetting ("attribute_count", count_);
dynamicPage(name: "graphSetupPage") {
parent.hubiForm_section(this, "General Options", 1){
container = [];
input( type: "enum", name: "graph_update_rate", title: "Select graph update rate", multiple: false, required: false, options: rateEnum, defaultValue: "0");
input( type: "enum", name: "graph_type", title: "Select Graph Type", multiple: false, required: false, options: typeEnum, defaultValue: "value", submitOnChange: true);
if (!graph_type) graph_type = "value";
if (graph_type == "time"){
input( type: "enum", name: "graph_decay", title: "Decay Rate", multiple: false, required: false, options: decayEnum, defaultValue: "300000", submitOnChange: true);
}
container << parent.hubiForm_color (this, "Graph Background", "graph_background", "#FFFFFF", false)
container << parent.hubiForm_color (this, "Graph Line", "graph_line", "#000000", false)
container << parent.hubiForm_line_size (this, title: "Graph Line",
name: "graph",
default: 2,
min: 1,
max: count_,
);
parent.hubiForm_container(this, container, 1);
}
if (graph_num_gradients == null){
settings["graph_num_gradients"] = 2;
num_ = 2;
} else {
num_ = graph_num_gradients.toInteger();
}
parent.hubiForm_section(this, "Level Gradient", 1){
container = [];
container << parent.hubiForm_text_input(this, "Number of Gradient Levels",
"graph_num_gradients",
2,
"true");
if (graph_type == "value"){
for (gradient = 0; gradient < num_; gradient++){
subcontainer = [];
if (gradient == 0) titleString = "Start"
else if (gradient == num_-1) titleString = "End"
else titleString = "Mid"
subcontainer << parent.hubiForm_text_input(this, titleString+" Value",
"graph_gradient_${gradient}_value",
gradient*10,
false);
subcontainer << parent.hubiForm_color (this, "Gradient #"+gradient,
"graph_gradient_${gradient}",
parent.hubiTools_rotating_colors(gradient),
false);
container << parent.hubiForm_subcontainer(this, objects: subcontainer,
breakdown: [0.25, 0.75]);
}
} else {
def add_time = (graph_decay.toInteger()/(graph_num_gradients.toInteger()-1));
def curr_time = 0;
for (gradient = 0; gradient < num_; gradient++){
subcontainer = [];
subcontainer << parent.hubiForm_text_format(this, text: convertToString(curr_time),
horizontal_align: "right",
vertical_align: "20px",
size: 24,
);
app.updateSetting ("graph_gradient_${gradient}_value", curr_time);
subcontainer << parent.hubiForm_color (this, "Gradient #"+gradient,
"graph_gradient_${gradient}",
parent.hubiTools_rotating_colors(gradient),
false);
container << parent.hubiForm_subcontainer(this, objects: subcontainer,
breakdown: [0.25, 0.75]);
curr_time += add_time;
}
}
parent.hubiForm_container(this, container, 1);
}
parent.hubiForm_section(this, "Graph Size", 1){
container = [];
default_ = Math.ceil(Math.sqrt(count_)).intValue();
cols = graph_num_columns ? graph_num_columns : default_;
rows = Math.ceil(count_/cols).intValue();
container << parent.hubiForm_slider (this, title: "Number of Columns
"+count_+" Devices/Attributes -- "+cols+" X "+rows+"",
name: "graph_num_columns",
default: default_,
min: 1,
max: count_,
units: " columns",
submit_on_change: true);
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_switch(this, title: "Show values inside Heat Map?", name: "show_annotations", default: false, submit_on_change: true);
if (show_annotations==true){
container << parent.hubiForm_font_size (this, title: "Annotation", name: "annotation", default: 16, min: 2, max: 40);
container << parent.hubiForm_color (this, "Annotation", "annotation", "#FFFFFF", false);
container << parent.hubiForm_color (this, "Annotation Aura", "annotation_aura", "#000000", false);
container << parent.hubiForm_slider (this, title: "Number Decimal Places", name: "graph_decimals", default: 1, min: 0, max: 4, units: " decimal places");
container << parent.hubiForm_switch (this, title: "Bold Annotation", name: "annotation_bold", default:false);
container << parent.hubiForm_switch (this, title: "Italic Annotation", name: "annotation_italic", 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(0);
if(sensors) {
sensors.each {sensor ->
def attributes = settings["attributes_${sensor.id}"];
resp[sensor.id] = [:];
attributes.each { attribute ->
if (attribute == "lastupdate"){
lastEvent = sensor.getLastActivity();
latest = lastEvent ? Date.parse("yyyy-MM-dd hh:mm:ssZ", sensor.getLastActivity().toString()).getTime() : 0;
resp[sensor.id][attribute] = [current: (now.getTime()-latest), date: latest];
} else {
latest = sensor.latestState(attribute);
resp[sensor.id][attribute] = [current: latest.getValue(), 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": graph_type,
"graphOptions": [
"bar" : [ "groupWidth" : "100%",
],
"width": graph_static_size ? graph_h_size : "100%",
"height": graph_static_size ? graph_v_size: "100%",
"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": true,
"chartArea": [ "left": 10,
"right" : 10,
"top": 10,
"bottom": 10 ],
"legend" : [ "position" : "none" ],
"hAxis": [ "textPosition": "none",
"gridlines" : [ "count" : "0" ]
],
"vAxis": [ "textPosition": "none",
"gridlines" : [ "count" : "0" ]
],
"annotations" : [ "alwaysOutside": "false",
"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"
],
],
]
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 = """