Lights Usage Table

Hi Bruce @bravenel and folks. I've started using HE dashboards so decided to give this table a whirl. It's pretty cool - works like a champ. However, I also wanted the table to show a count of how many times the light turned on, and when it was last turned on. I also wanted a second table showing similar stats for doors/windows/contacts. I studied Bruce's clever little code and decided to take a whack at updating it myself. Seems to work although I confess I didn't spend a ton of time pressure testing it.

New tables look as follows:

And here's the code.
[EDIT]Found and squashed a few silly bugs - mostly in contacts table
and the variable was updated to include the count

 *  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]
					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]
					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 Activation 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"].start ? new Date(state.lights["$dev.id"].start).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 Activation for $dev'>$startstr</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 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"].start ? new Date(state.contacts["$dev.id"].start).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 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].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
        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].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
        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)
}
4 Likes