Lights Usage Table

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...

3 Likes

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)
}
5 Likes

Wow, you guys are amazing, thank you so much!

1 Like

@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.

Total time for my plug sockets show more than 24 hours and changes when refreshed

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.

1 Like

Alright...think I got it squared away. I edited the code above, so just pull it from there.

2 Likes

Excellent work, now it is working as expected.
Thank you for the very fast response and fix!

1 Like

@FriedCheese2006,
Thanks for renaming the text to last on time & last off time!

Lights usage table

1 Like

Nicely done

1 Like

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?

lights usage table

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 :grin:

1 Like

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.

Summary

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.

1 Like

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.

1 Like

thanks - i'll give it a try

1 Like

There are no lines with "log.info" in them? Any other ideas?