Sure, no problem.
I am not a coder, so any guidance will be much appreciated.
No rush, let me know when you have some spare time. Thanks again
okay my friend - It's an easy hack even for an old school coder like myself. I'll give it a whirl over the coming weekend. Gotta pay the bills during the week...
I took the liberty. Also found an issue where every time the refresh button was hit, it was updating the "start" time for the devices to the current time.
Code
/**
* Usage and Count Table
*
* Copyright 2022 Hubitat, Inc. All Rights Reserved.
*
*/
definition(
name: "Usage and Count Table",
namespace: "hubitat",
author: "Bruce Ravenel",
description: "Show Time and Frequency Usage of Lights and Contacts",
category: "Convenience",
iconUrl: "",
iconX2Url: ""
)
preferences {
page(name: "mainPage")
}
def mainPage() {
if(state.lights == null) state.lights = [:]
if(state.lightsList == null) state.lightsList = []
if(state.contacts == null) state.contacts = [:]
if(state.contactsList == null) state.contactsList = []
dynamicPage(name: "mainPage", title: "Usage and Count Table", uninstall: true, install: true) {
section {
input "lights", "capability.switch", title: "Select Lights to Measure Usage", multiple: true, submitOnChange: true, width: 4
lights.each {dev ->
if(!state.lights["$dev.id"]) {
state.lights["$dev.id"] = [start: dev.currentSwitch == "on" ? now() : 0, total: 0, var: "", time: "", count: 0,lastOn: dev.currentSwitch == "on" ? now() : 0,lastOff: dev.currentSwitch == "off" ? now() : 0]
state.lightsList += dev.id
}
}
input "resetVar", "enum", title: "Select Boolean Variable to Reset Light Timers and Counters", submitOnChange: true, width: 4, style: 'margin-left:10px',
options: getAllGlobalVars().findAll{it.value.type == "boolean"}.keySet().collect().sort{it.capitalize()}
if(lights) {
if(lights.id.sort() != state.lightsList.sort()) { //something was removed
state.lightsList = lights.id
Map newState = [:]
lights.each{d -> newState["$d.id"] = state.lights["$d.id"]}
state.lights = newState
}
updated()
paragraph displayLightsTable()
if(state.newVar) {
List vars = getAllGlobalVars().findAll{it.value.type == "string"}.keySet().collect().sort{it.capitalize()}
input "newVar", "enum", title: "Select Variable", submitOnChange: true, width: 4, options: vars, newLineAfter: true
if(newVar) {
state.lights[state.newVar].var = newVar
state.remove("newVar")
app.removeSetting("newVar")
paragraph "<script>{changeSubmit(this)}</script>"
}
} else if(state.remVar) {
state.lights[state.remVar].var = ""
state.remove("remVar")
paragraph "<script>{changeSubmit(this)}</script>"
}
input "refresh", "button", title: "Refresh Table", width: 2
input "reset", "button", title: "Reset Table", width: 2
}
}
section {
input "contacts", "capability.contactSensor", title: "Select Contacts to Measure Openings", multiple: true, submitOnChange: true, width: 4
contacts.each {dev ->
if(!state.contacts["$dev.id"]) {
state.contacts["$dev.id"] = [start: dev.currentContact == "open" ? now() : 0, total: 0, var: "", time: "", count: 0,lastOpen: dev.currentContact == "open" ? now() : 0,lastClosed: dev.currentContact == "closed" ? now() : 0]
state.contactsList += dev.id
}
}
input "resetContactVar", "enum", title: "Select Boolean Variable to Reset Contact Timers and Counters", submitOnChange: true, width: 4, style: 'margin-left:10px',
options: getAllGlobalVars().findAll{it.value.type == "boolean"}.keySet().collect().sort{it.capitalize()}
if(contacts) {
if(contacts.id.sort() != state.contactsList.sort()) { //something was removed
state.contactsList = contacts.id
Map newState = [:]
contacts.each{d -> newState["$d.id"] = state.contacts["$d.id"]}
state.contacts = newState
}
updated()
paragraph displayContactsTable()
if(state.newContactVar) {
List vars = getAllGlobalVars().findAll{it.value.type == "string"}.keySet().collect().sort{it.capitalize()}
input "newContactVar", "enum", title: "Select Variable", submitOnChange: true, width: 4, options: vars, newLineAfter: true
if(newContactVar) {
state.contacts[state.newContactVar].var = newContactVar
state.remove("newContactVar")
app.removeSetting("newContactVar")
paragraph "<script>{changeSubmit(this)}</script>"
}
} else if(state.remContactVar) {
state.contacts[state.remContactVar].var = ""
state.remove("remContactVar")
paragraph "<script>{changeSubmit(this)}</script>"
}
input "refreshc", "button", title: "Refresh Table", width: 2
input "resetc", "button", title: "Reset Table", width: 2
}
}
}
}
String displayLightsTable() {
if(state.reset) {
def dev = lights.find{"$it.id" == state.reset}
state.lights[state.reset].start = dev.currentSwitch == "on" ? now() : 0
state.lights[state.reset].time = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}")
state.lights[state.reset].total = 0
state.lights[state.reset].count = dev.currentSwitch == "on" ? 1 : 0
state.remove("reset")
}
String str = "<script src='https://code.iconify.design/iconify-icon/1.0.0/iconify-icon.min.js'></script>"
str += "<style>.mdl-data-table tbody tr:hover{background-color:inherit} .tstat-col td,.tstat-col th { padding:8px 8px;text-align:center;font-size:12px} .tstat-col td {font-size:15px }" +
"</style><div style='overflow-x:auto'><table class='mdl-data-table tstat-col' style=';border:2px solid black'>" +
"<thead><tr style='border-bottom:2px solid black'><th style='border-right:2px solid black'>Light</th>" +
"<th>Total On Time</th>" +
"<th>Total Count</th>" +
"<th>Reset</th>" +
"<th>Last On Time</th>" +
"<th>Last Off Time</th>" +
"<th>Last Reset Time</th>" +
"<th>Variable</th></tr></thead>"
lights.sort{it.displayName.toLowerCase()}.each {dev ->
int total = state.lights["$dev.id"].total / 1000
String thisVar = state.lights["$dev.id"].var
String count = state.lights["$dev.id"].count
String startstr = state.lights["$dev.id"].lastOn ? new Date(state.lights["$dev.id"].lastOn).format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") : ""
String stopstr = state.lights["$dev.id"].lastOff ? new Date(state.lights["$dev.id"].lastOff).format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") : ""
int hours = total / 3600
total = total % 3600
int mins = total / 60
int secs = total % 60
String time = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs"
if(thisVar) setGlobalVar(thisVar, "$time $count")
String devLink = "<a href='/device/edit/$dev.id' target='_blank' title='Open Device Page for $dev'>$dev"
String reset = buttonLink("d$dev.id", "<iconify-icon icon='bx:reset'></iconify-icon>", "black", "20px")
String var = thisVar ? buttonLink("r$dev.id", thisVar, "purple") : buttonLink("n$dev.id", "Select", "green")
str += "<tr style='color:black'><td style='border-right:2px solid black'>$devLink</td>" +
"<td title='Switch usage time since last Reset' style='color:${dev.currentSwitch == "on" ? "green" : "red"}'>$time</td>" +
"<td title='Activation count since last Reset' style='color:${dev.currentSwitch == "on" ? "green" : "red"}'>$count</td>" +
"<td title='Reset Total Time and Total Count for $dev' style='padding:0px 0px'>$reset</td>" +
"<td title='Time of last On for $dev'>$startstr</td>" +
"<td title='Time of last Off for $dev'>$stopstr</td>" +
"<td title='Time of last Reset for $dev'>${state.lights["$dev.id"].time ?: ""}</td>" +
"<td title='${thisVar ? "Deselect $thisVar" : "Select String Hub Variable"}'>$var</td></tr>"
}
str += "</table></div>"
str
}
String displayContactsTable() {
if(state.contactreset) {
def dev = contacts.find{"$it.id" == state.contactreset}
state.contacts[state.contactreset].start = dev.currentContact == "open" ? now() : 0
state.contacts[state.contactreset].time = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}")
state.contacts[state.contactreset].total = 0
state.contacts[state.contactreset].count = dev.currentContact == "open" ? 1 : 0
state.remove("contactreset")
}
String str = "<script src='https://code.iconify.design/iconify-icon/1.0.0/iconify-icon.min.js'></script>"
str += "<style>.mdl-data-table tbody tr:hover{background-color:inherit} .tstat-col td,.tstat-col th { padding:8px 8px;text-align:center;font-size:12px} .tstat-col td {font-size:15px }" +
"</style><div style='overflow-x:auto'><table class='mdl-data-table tstat-col' style=';border:2px solid black'>" +
"<thead><tr style='border-bottom:2px solid black'><th style='border-right:2px solid black'>Contact</th>" +
"<th>Total Open Time</th>" +
"<th>Total Count</th>" +
"<th>Reset</th>" +
"<th>Last Open Time</th>" +
"<th>Last Close Time</th>" +
"<th>Last Reset Time</th>" +
"<th>Variable</th></tr></thead>"
contacts.sort{it.displayName.toLowerCase()}.each {dev ->
int total = state.contacts["$dev.id"].total / 1000
String thisVar = state.contacts["$dev.id"].var
String count = state.contacts["$dev.id"].count
String startstr = state.contacts["$dev.id"].lastOpen ? new Date(state.contacts["$dev.id"].lastOpen).format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") : ""
String stopstr = state.contacts["$dev.id"].lastClosed ? new Date(state.contacts["$dev.id"].lastClosed).format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") : ""
int hours = total / 3600
total = total % 3600
int mins = total / 60
int secs = total % 60
String time = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs"
if(thisVar) setGlobalVar(thisVar, "$time $count")
String devLink = "<a href='/device/edit/$dev.id' target='_blank' title='Open Device Page for $dev'>$dev"
String reset = buttonLink("e$dev.id", "<iconify-icon icon='bx:reset'></iconify-icon>", "black", "20px")
String var = thisVar ? buttonLink("s$dev.id", thisVar, "purple") : buttonLink("o$dev.id", "Select", "green")
str += "<tr style='color:black'><td style='border-right:2px solid black'>$devLink</td>" +
"<td title='Contact open time since last Reset' style='color:${dev.currentContact == "open" ? "green" : "red"}'>$time</td>" +
"<td title='Open count since last Reset' style='color:${dev.currentContact == "open" ? "green" : "red"}'>$count</td>" +
"<td title='Reset Total Time and Total Count for $dev' style='padding:0px 0px'>$reset</td>" +
"<td title='Time of last Open for $dev'>$startstr</td>" +
"<td title='Time of last Close for $dev'>$stopstr</td>" +
"<td title='Time of last Reset for $dev'>${state.contacts["$dev.id"].time ?: ""}</td>" +
"<td title='${thisVar ? "Deselect $thisVar" : "Select String Hub Variable"}'>$var</td></tr>"
}
str += "</table></div>"
str
}
String buttonLink(String btnName, String linkText, color = "#1A77C9", font = "15px") {
"<div class='form-group'><input type='hidden' name='${btnName}.type' value='button'></div><div><div class='submitOnChange' onclick='buttonClick(this)' style='color:$color;cursor:pointer;font-size:$font'>$linkText</div></div><input type='hidden' name='settings[$btnName]' value=''>"
}
void appButtonHandler(btn) {
if(btn == "reset") resetTimers()
else if(btn == "resetc") resetContactTimers()
else if(btn == "refresh") {
state.lights.each{k, v ->
def dev = lights.find{"$it.id" == k}
if(dev.currentSwitch == "on") {
state.lights[k].total += now() - state.lights[k].start
state.lights[k].start = now()
}
}
} else if(btn =="refreshc") {
state.contacts.each{k, v ->
def dev = contacts.find{"$it.id" == k}
if(dev.currentContact == "open") {
state.contacts[k].total += now() - state.contacts[k].start
state.contacts[k].start = now()
}
}
} else if(btn.startsWith("n")) state.newVar = btn.minus("n")
else if(btn.startsWith("r")) state.remVar = btn.minus("r")
else if(btn.startsWith("o")) state.newContactVar = btn.minus("o")
else if(btn.startsWith("s")) state.remContactVar = btn.minus("s")
else if(btn.startsWith('e')) state.contactreset = btn.minus("e")
else if(btn.startsWith('d')) state.reset = btn.minus("d")
else log.warn "Unrecognized button pressed"
}
def updated() {
unsubscribe()
initialize()
}
def installed() {
}
def uninstalled() {
unsubscribe()
}
void initialize() {
subscribe(lights, "switch.on", "onHandler")
subscribe(lights, "switch.off", "offHandler")
if(resetVar) {
subscribe(location, "variable:${resetVar}.true", resetTimers)
setGlobalVar(resetVar, false)
}
// subscribe(contacts, "contact.open", "openHandler")
// subscribe(contacts, "contact.closed", "closeHandler")
contacts?.each { device ->
subscribe(device, "contact", "contactHandler")
}
if(resetContactVar) {
subscribe(location, "variable:${resetContactVar}.true", resetContactTimers)
setGlobalVar(resetContactVar, false)
}
}
void contactHandler(evt) {
def value = evt.value
if (value == "open") openHandler(evt)
else if (value== "closed") closeHandler(evt)
}
void onHandler(evt) {
if ( state.lights[evt.device.id] ) {
state.lights[evt.device.id].start = now()
state.lights[evt.device.id].lastOn = now()
state.lights[evt.device.id].count++
String startstr = new Date(state.lights[evt.device.id].start).format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}")
log.info "device ${evt.device.displayName} turned on at $startstr"
}
}
void offHandler(evt) {
if ( state.lights[evt.device.id] ) {
state.lights[evt.device.id].total += now() - state.lights[evt.device.id].start
state.lights[evt.device.id].lastOff = now()
String thisVar = state.lights[evt.device.id].var
int total = state.lights[evt.device.id].total / 1000
int hours = total / 3600
total = total % 3600
int mins = total / 60
int secs = total % 60
String thisTime = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs"
log.info "device ${evt.device.displayName} total = $thisTime"
if(thisVar) setGlobalVar(thisVar, thisTime)
}
}
void openHandler(evt) {
if ( state.contacts[evt.device.id] ) {
state.contacts[evt.device.id].start = now()
state.contacts[evt.device.id].lastOpen = now()
state.contacts[evt.device.id].count++
String startstr = new Date(state.contacts[evt.device.id].start).format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}")
log.info "device ${evt.device.displayName} opened at $startstr"
}
}
void closeHandler(evt) {
if ( state.contacts[evt.device.id] ) {
state.contacts[evt.device.id].total += now() - state.contacts[evt.device.id].start
state.contacts[evt.device.id].lastClosed = now()
String thisVar = state.contacts[evt.device.id].var
int total = state.contacts[evt.device.id].total / 1000
int hours = total / 3600
total = total % 3600
int mins = total / 60
int secs = total % 60
String thisTime = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs"
log.info "device ${evt.device.displayName} total = $thisTime"
if(thisVar) setGlobalVar(thisVar, thisTime)
}
}
void resetTimers(evt = null) {
state.lights.each{k, v ->
def dev = lights.find{"$it.id" == k}
if ( dev ) {
state.lights[k].start = dev.currentSwitch == "on" ? now() : 0
state.lights[k].time = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}")
state.lights[k].total = 0
state.lights[k].count = dev.currentSwitch == "on" ? 1 : 0
}
}
if(resetVar) setGlobalVar(resetVar, false)
}
void resetContactTimers(evt = null) {
state.contacts.each{k, v ->
def dev = contacts.find{"$it.id" == k}
if ( dev ) {
state.contacts[k].start = dev.currentContact == "open" ? now() : 0
state.contacts[k].time = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}")
state.contacts[k].total = 0
state.contacts[k].count = dev.currentContact == "open" ? 1 : 0
}
}
if(resetContactVar) setGlobalVar(resetContactVar, false)
}
Wow, you guys are amazing, thank you so much!
@FriedCheese2006,
The last activation & deactivation time is working great.
Unfortunately, it seems that every time the refresh button is pressed then the total time for active devices are calculated again by xx number of hours?
Could you please check, thanks again.
I'm not sure what you mean is the issue. The "total on time" should update every time the refresh button is pressed.
OOOOHHHHH....weird...it didn't come up in limited testing, but, yeah, I see now that the time seems to exponentially update.
Alright...think I got it squared away. I edited the code above, so just pull it from there.
Excellent work, now it is working as expected.
Thank you for the very fast response and fix!
Nicely done
Hi all,
I am slightly confused as to why the total on time for my bathroom light1 and light 2, and hallway light 1 and light 2 show different count and on time although they are programmed to activate together in room lighting?
My first thought is that it's not possible to say. Maybe the lights aren't updating their status. Maybe, somehow, they were turned on outside of the Room Lighting instance. Maybe something else. You'd have to have other way to view their individually reported statuses over the time drift periods to figure out what happened.
Or the code is busted...who knows
Thanks for your reply.
I normally view the lights usage table to get an overview of the lights usage, and it is a good indicator.
However, I've not reported it before but this discrepancy is now bugging me and I will need to dig further.
I would accept few seconds here and there and even a few minutes difference, but see below there is a 15 mins discrepancy for hallway lights being activated by room light only this morning from 6.30am-8am.
So, I would go to the events list for those two lights, get the reported on and off times, and manually calculate. If it matches the table, the it's an issue with the lights reporting. If they're different, then it'll be worth a look at the app code.
Is there a way I could disable the reporting of events to my log? Perhaps strip out some section of the code or comment out a block or two?
Which one are you using?
both
Just comment out or delete any "log.info" line.
Comment is just adding"//" to the beginning of the line.
thanks - i'll give it a try
There are no lines with "log.info" in them? Any other ideas?