Aeon Home Energy Monitor v2


(Thomas) #1

Has anyone tried integrating the Home Energy Monitor configured to read the clamps separately. The provided SmartThings device provides total wattage off both clamps. I changed the configuration of the meter to send the wattage from clamp 1 and clamp 2. After updating, it looks like when the parse function is called, zwave.parse is returning null.
It’s called with the original code:

zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3])

I expected the clamp wattage to be in the multi-channel message (0x60) which shows up in the raw data. Any ideas?


(Barry) #2

I don’t think the core device handler for the HEM supports the individual reporting command codes - I tried to get that data when I wrote my HEMv2 device, and never got it to report the individual values.


(Thomas) #3

The core handler is what’s provided by SmartThings? You’re talking about zwave.parse?


(Duncan) #4

Can you give an example of a value of description that parses to null?


(Jjhamb) #5

it is possible to get individual clamp readings


you need to look here: https://code.google.com/p/open-zwave/source/browse/wiki/Technical_Documents.wiki?spec=svn668&r=668


(Barry) #6

Would you mind sharing your code, please?

Thx,
Barry


(Jjhamb) #8

Sure, its not my code, I have just added and modified bits. I will try to clean it up for generic use. Key here is parsing the zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd)

Also note sections where you see “encapsulatedCommand.scaledMeterValue * 1.160437”, this is a correction that apply to my meter readings and you will not need to.

scaledConfigurationValue: in configure section for parameter 111, 112 ans 113 is all in seconds
zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 20).format()

1573640 at 103 asks the HEM to send Amps for each clamp, Watts for each clamp and Amps of combined clamps every 20s as set in 113.


/**
 *  HEM G2 V2.0
 *
 *  Author: Jayant Jhamb (This is a heavily borrowed code from 	"Barry A. Burke")
 *
 *  Date: 2014-11-06
 **/
 // for the UI
metadata {
	// Automatically generated. Make future change here.
	definition (name: "HEM G2 V2.0", author: "Jayant Jhamb") {
		capability "Power Meter"
		capability "Sensor"
		capability "Energy Meter"
		capability "Configuration"
	attribute "voltage", "string"
	attribute "amps", "string"
	attribute "powerOne", "string"
	attribute "powerTwo", "string"
	attribute "energyOne", "string"
	attribute "energyTwo", "string"
	attribute "ampsOne", "string"
	attribute "ampsTwo", "string"
	attribute "dataOne", "string"
	attribute "dataTwo", "string"

	command "refresh"
	command "reset"
	command "enableDelta"
	command "disableDelta"
	command "alertHighLoad"
	command "testConfiguration"
	command "WakeUp"
	command "appUpdate"

	fingerprint deviceId: "0x2101", inClusters: " 0x70,0x31,0x72,0x86,0x32,0x80,0x85,0x60"
}

    
// simulator metadata
simulator {
	for (int i = 0; i <= 10000; i += 1000) {
		status "power  ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport(
			scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()
	}
}

// tile definitions
tiles {

    valueTile("power", "device.power") {
        state (
            "default", 
            label:'${currentValue} Watt', 
            foregroundColors:[
                [value: 1, color: "#000000"],
                [value: 10000, color: "#ffffff"]
            ], 
            foregroundColor: "#000000",
            backgroundColors:[
                [value: "0 Watts",    color: "#000000"],
                [value: "500 Watts",  color: "#0056C9"],
                [value: "1000 Watts", color: "#118E8E"],
                [value: "2000 Watts", color: "#198E11"],
                [value: "3000 Watts", color: "#C9AE00"],
                [value: "4000 Watts", color: "#D15212"],
                [value: "5000 Watts", color: "#C91000"], 
                [value: "6000 Watts", color: "#8E071A"], 
                [value: "7000 Watts", color: "#8E0789"]
            ]
        )
    }
    valueTile("powerOne", "device.powerOne") {
        state (
            "default", 
            label:'${currentValue}W C1', 
            foregroundColors:[
                [value: 1, color: "#000000"],
                [value: 10000, color: "#ffffff"]
            ], 
            foregroundColor: "#000000",
            backgroundColors:[
                [value: "0 Watts",    color: "#000000"],
                [value: "500 Watts",  color: "#0056C9"],
                [value: "1000 Watts", color: "#118E8E"],
                [value: "2000 Watts", color: "#198E11"],
                [value: "3000 Watts", color: "#C9AE00"],
                [value: "4000 Watts", color: "#D15212"],
                [value: "5000 Watts", color: "#C91000"], 
                [value: "6000 Watts", color: "#8E071A"], 
                [value: "7000 Watts", color: "#8E0789"]
            ]
        )
    }
            valueTile("powerTwo", "device.powerTwo") {
        state (
            "default", 
            label:'${currentValue}W C2', 
            foregroundColors:[
                [value: 1, color: "#000000"],
                [value: 10000, color: "#ffffff"]
            ], 
            foregroundColor: "#000000",
            backgroundColors:[
                [value: "0 Watts",    color: "#000000"],
                [value: "500 Watts",  color: "#0056C9"],
                [value: "1000 Watts", color: "#118E8E"],
                [value: "2000 Watts", color: "#198E11"],
                [value: "3000 Watts", color: "#C9AE00"],
                [value: "4000 Watts", color: "#D15212"],
                [value: "5000 Watts", color: "#C91000"], 
                [value: "6000 Watts", color: "#8E071A"], 
                [value: "7000 Watts", color: "#8E0789"]
            ]
        )
    }
    valueTile("amps", "device.amps", decoration: "flat") {
		state "default", label:'${currentValue} A'
	}
    valueTile("ampsOne", "device.ampsOne", decoration: "flat") {
		state "default", label:'${currentValue}A C1'
	}
    valueTile("ampsTwo", "device.ampsTwo", decoration: "flat") {
		state "default", label:'${currentValue}A C2'
	}
	valueTile("energy", "device.energy", decoration: "flat") {
		state "default", label:'${currentValue}', unit:"kWh"
	}
	valueTile("energyOne", "device.energyOne", decoration: "flat") {
		state "default", label:'${currentValue}', unit:"kWh"
	}
	valueTile("energyTwo", "device.energyTwo", decoration: "flat") {
		state "default", label:'${currentValue}', unit:"kWh"
	}
	valueTile("dataOne", "device.consOne", decoration: "flat") {
		state "default", label:'${currentValue} Units'
	}
	valueTile("dataTwo", "device.consTwo", decoration: "flat") {
		state "default", label:'${currentValue} Units'
	}
	
	valueTile("voltage", "device.voltage", decoration: "flat") {
		state "default", label:'${currentValue} V'
	}

	standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") {
		state "default", label:'reset kWh', action:"reset"
	}
	standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
		state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
	}
	standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
		state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
	}

	main (["power","powerOne","powerTwo","energy"])
	details([
    	"power","powerOne","powerTwo",
        "amps","ampsOne","ampsTwo",
        "energy",
        "refresh","configure","reset"
        ])
}

}

def parse(String description) {
def result = null
def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3])
// log.debug cmd

if (cmd) {

// log.debug zwaveEvent(cmd)
result = createEvent(zwaveEvent(cmd))
}
if (result) {
log.debug “Parse returned ${result?.descriptionText}”
// log.debug "Parse returned result ${result}"
return result
} else {
// log.debug “No Result for ${cmd}”
}
}

def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) {
// log.debug cmd

if (cmd.meterType == 33 && cmd.scale == 0) {
	[name: "energy", value: Math.round(cmd.scaledMeterValue * 10) / 10 , unit: "kWh"]
} 
else if (cmd.meterType == 161 && cmd.scale == 0) {
	[name: "voltage", value: Math.round(cmd.scaledMeterValue), unit: "V"]
} 
else if (cmd.meterType == 161 && cmd.scale == 1) {
	[name: "amps", value: cmd.scaledMeterValue, unit: "A"]
}
	else if (cmd.meterType == 33 && cmd.scale == 2) {
    [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]
}

}

def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
// log.debug “MultiChannelCmdEncap $cmd”
// log.debug "cmd.commandClass == ${cmd.commandClass} // cmd.command == ${cmd.command} // cmd.parameter == ${cmd.parameter} // cmd.sourceEndPoint == ${cmd.sourceEndPoint}"
if (cmd.commandClass == 50) {
def encapsulatedCommand = cmd.encapsulatedCommand([0x30: 1, 0x31: 1]) // can specify command class versions here like in zwave.parse
if (encapsulatedCommand) {
if (cmd.sourceEndPoint == 1) {

        // -- Test

// log.debug encapsulatedCommand
// log.debug zwaveEvent(encapsulatedCommand)
//— Test

			if (encapsulatedCommand.scale == 2 ){
				[name: "powerOne", value: Math.round(encapsulatedCommand.scaledMeterValue) as String, unit: "W", descriptionText: "Clamp1 Power Usage ${Math.round(encapsulatedCommand.scaledMeterValue)} W"]
			} else if (encapsulatedCommand.scale == 0 ){
				[name: "energyOne", value: Math.round(encapsulatedCommand.scaledMeterValue) as String, unit: "kWh", descriptionText: "Clamp1 Energy Usage ${Math.round(encapsulatedCommand.scaledMeterValue)} kWh"]
			} else if (encapsulatedCommand.scale == 5 ){
				[name: "ampsOne", value: encapsulatedCommand.scaledMeterValue as String, unit: "A", descriptionText: "Clamp1 Current Draw ${encapsulatedCommand.scaledMeterValue} A"]
            } else if (encapsulatedCommand.scale == 4 ){
				[name: "voltsOne", value: encapsulatedCommand.scaledMeterValue as String, unit: "V"]
            }               
		} else if (cmd.sourceEndPoint == 2) {
			if (encapsulatedCommand.scale == 2 ){
				[name: "powerTwo", value: Math.round(encapsulatedCommand.scaledMeterValue) as String, unit: "W", descriptionText: "Clamp2 Power Usage ${Math.round(encapsulatedCommand.scaledMeterValue)} W"]
			} else if (encapsulatedCommand.scale == 0 ){
				[name: "energyTwo", value: Math.round(encapsulatedCommand.scaledMeterValue) as String, unit: "kWh", descriptionText: "Clamp2 Energy Usage ${Math.round(encapsulatedCommand.scaledMeterValue)} kWh"]
			} else if (encapsulatedCommand.scale == 5 ){
				[name: "ampsTwo", value: encapsulatedCommand.scaledMeterValue as String, unit: "A", descriptionText: "Clamp2 Current Draw ${encapsulatedCommand.scaledMeterValue} A"]
            } else if (encapsulatedCommand.scale == 4 ){
				[name: "voltsTwo", value: encapsulatedCommand.scaledMeterValue as String, unit: "V"]
            }               			
		}
	}
}

}

/*
def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) {
// log.debug cmd

if (cmd.meterType == 33 && cmd.scale == 0) {
	[name: "energy", value: Math.round(((cmd.scaledMeterValue - 4465.5) * 1.160437) * 10) / 10 + 96700, unit: "kWh"]
} 
else if (cmd.meterType == 161 && cmd.scale == 0) {
	[name: "voltage", value: Math.round(cmd.scaledMeterValue * 1.135), unit: "V"]
} 
else if (cmd.meterType == 161 && cmd.scale == 1) {
	[name: "amps", value: cmd.scaledMeterValue, unit: "A"]
}
	else if (cmd.meterType == 33 && cmd.scale == 2) {
    [name: "power", value: Math.round(cmd.scaledMeterValue * 1.160437), unit: "W"]
}

}

// MultiChannelCmdEncap and MultiInstanceCmdEncap are ways that devices can indicate that a message
// is coming from one of multiple subdevices or “endpoints” that would otherwise be indistinguishable
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
//log.debug “-----------------------------------------------”
// log.debug “MultiChannelCmdEncap $cmd”
// log.debug "cmd.commandClass == ${cmd.commandClass} // cmd.command == ${cmd.command} // cmd.parameter == ${cmd.parameter} // cmd.sourceEndPoint == ${cmd.sourceEndPoint}"
if (cmd.commandClass == 50) {
def encapsulatedCommand = cmd.encapsulatedCommand([0x30: 1, 0x31: 1]) // can specify command class versions here like in zwave.parse
if (encapsulatedCommand) {
if (cmd.sourceEndPoint == 1) {
// – Test
// log.debug encapsulatedCommand
// log.debug zwaveEvent(encapsulatedCommand)
//— Test
if (encapsulatedCommand.scale == 2 ){
//sendEvent(name: “powerOne”, value: Math.round((encapsulatedCommand.scaledMeterValue * 1.160437)) as String, unit: “W”, descriptionText: “Clamp1 Power Usage is ${Math.round((encapsulatedCommand.scaledMeterValue * 1.160437))} Watts”)
[name: “powerOne”, value: Math.round((encapsulatedCommand.scaledMeterValue * 1.160437)) as String, unit: “W”, descriptionText: “Clamp1 Power Usage ${Math.round((encapsulatedCommand.scaledMeterValue * 1.160437))} W”]
} else if (encapsulatedCommand.scale == 0 ){
[name: “energyOne”, value: (encapsulatedCommand.scaledMeterValue * 1.160437) as String, unit: “kWh”, descriptionText: “Clamp1 Energy Usage ${Math.round((encapsulatedCommand.scaledMeterValue * 1.160437))} kWh”]
} else if (encapsulatedCommand.scale == 5 ){
[name: “ampsOne”, value: encapsulatedCommand.scaledMeterValue as String, unit: “A”, descriptionText: “Clamp1 Current Draw ${encapsulatedCommand.scaledMeterValue} A”]
} else if (encapsulatedCommand.scale == 4 ){
[name: “voltsOne”, value: encapsulatedCommand.scaledMeterValue as String, unit: “V”]
}
} else if (cmd.sourceEndPoint == 2) {
if (encapsulatedCommand.scale == 2 ){
[name: “powerTwo”, value: Math.round((encapsulatedCommand.scaledMeterValue * 1.160437)) as String, unit: “W”, descriptionText: “Clamp2 Power Usage ${Math.round((encapsulatedCommand.scaledMeterValue * 1.160437))} W”]
} else if (encapsulatedCommand.scale == 0 ){
[name: “energyTwo”, value: (encapsulatedCommand.scaledMeterValue * 1.160437) as String, unit: “kWh”, descriptionText: “Clamp2 Energy Usage ${Math.round((encapsulatedCommand.scaledMeterValue * 1.160437))} kWh”]
} else if (encapsulatedCommand.scale == 5 ){
[name: “ampsTwo”, value: encapsulatedCommand.scaledMeterValue as String, unit: “A”, descriptionText: “Clamp2 Current Draw ${encapsulatedCommand.scaledMeterValue} A”]
} else if (encapsulatedCommand.scale == 4 ){
[name: “voltsTwo”, value: encapsulatedCommand.scaledMeterValue as String, unit: “V”]
}
}
}
}
}
*/

def zwaveEvent(physicalgraph.zwave.Command cmd) {
// Handles all Z-Wave commands we aren’t interested in
[:]
}

def refresh() {
log.debug "Doing Refresh"
def cmd = delayBetween([
zwave.meterV2.meterGet(scale: 0).format(),
zwave.meterV2.meterGet(scale: 2).format()
])
log.debug cmd
cmd
}

def reset() {
// No V1 available
return [
zwave.meterV2.meterReset().format(),
zwave.meterV2.meterGet(scale: 0).format()
]
}

def enableDelta() {
log.debug "Executing ‘enableDelta’"
def cmd = delayBetween([
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: 1).format()
])
log.debug cmd
cmd
}

def disableDelta() {
log.debug "Executing ‘disableDelta’"
def cmd = delayBetween([
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: 0).format()
])
log.debug cmd
cmd
}

def alertHighLoad() {
log.debug “Executing ‘alertHighLoad’”
// TODO: handle ‘alertHighLoad’ command
}

def testConfiguration() {
log.debug "Executing ‘testConfiguration’"
def cmd = delayBetween([
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: 1).format(), // 1 enable Delta
zwave.configurationV1.configurationSet(parameterNumber: 4, size: 2, scaledConfigurationValue: 30).format(), // 20 Trigger if load changes by 20 watts
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 2, scaledConfigurationValue: 30).format(), // 20 Trigger if load changes by 20 watts
zwave.configurationV1.configurationSet(parameterNumber: 6, size: 2, scaledConfigurationValue: 30).format(), // 20 Trigger if load changes by 20 watts
zwave.configurationV1.configurationSet(parameterNumber: 8, size: 1, scaledConfigurationValue: 5).format(), // 5% Load change
zwave.configurationV1.configurationSet(parameterNumber: 9, size: 1, scaledConfigurationValue: 5).format(), // 5% Load change
zwave.configurationV1.configurationSet(parameterNumber: 10, size: 1, scaledConfigurationValue: 5).format() // 5% Load change
])
log.debug cmd
cmd
}

def WakeUp() {
log.debug "Executing WakeUp"
configure()
refresh()
disableDelta()
}

def appUpdate(lastNoonReading, startMonthReading) {
log.debug "Executing appUpdate"
state.lastNoonReading = lastNoonReading
state.startMonthReading = startMonthReading
sendEvent(name: “dataOne”, value: state.lastNoonReading, unit: “”)
sendEvent(name: “dataTwo”, value: state.startMonthReading, unit: “”)
}

def configure() {
log.debug "Configuring"
def cmd = delayBetween([
// zwave.configurationV1.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: 240).format(), // Set voltage to 240
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 1).format(), // 1 combined power in kWh
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 600).format(), // every 10 min
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 14).format(), // 2 combined energy in W, 14 generates report for combined Amps, Power and volts
zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 20).format(), // every 20s
zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 1573640).format(), //0 no third report, (1573640 Amps combined and for each clsmp plus power for each clamp)
zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 20).format() // every 20s
])
log.debug cmd
cmd
}

@storageanarchy hey Barry, just realised, this is mostly your code :)

(Thomas) #9

I was in the process of modifying that code also. I came across the MultiChannelCmdEncap when searching the web and was able to get the individual clamp readings. I just haven’t been able to get them onto the tiles yet.


(Barry) #10

Well, it’s good to see my code evolving.

My original vision was to have a toggle for an alternate display that showed the L1/L2 values to either side of the total value column, replacing the high/low displays. I think I’ll try to finish that with the help of your code…stay tuned!


(Jjhamb) #11

Great Barry, I think I merged the code with AEON Smart Strip, I had used that code earlier with my dual relay inserts.
Also note that HEM goes to sleep arbitrarily, for that i have the wakeup function whoch I call from an app rhat also updates my readinds on xively. @storageanarchy


(Barry) #12

OK, well I’ve updated my HEMv2 driver to a new “Plus” version here on my GitHub:

WARNING

During development/test of this, I found that other “Things” stopped
working, including ALL of my MiMolite switches. Powering off the ST hub
for a minute seemed to fix the problem, but I have no idea why this
device would impact another. Until this gets tracked down, USE AT YOUR
OWN RISK!

Jayant - this may be related to the problem you had with this driver “going to sleep arbitrarily”, because I have NEVER seen this in my original code (my HEMv2 has been running untouched for more than 3 months). Looking through logs on devices that stopped working, I see references to endpoints - I wonder if there is a memory leak/collision in the encapsulation handlers…


(Jjhamb) #13
  • will watch out for this. I have 3 other devices with endpoints.

(Jjhamb) #14

Installed the new code @storageanarchy, toggledisplay is a neat trick. Works great. Few thoughts:
a. Xively code can be part of the device driver (but not needed for generic install)
b. I realised that HEM (both v1 and v2) are not very accurate. So it may be more useful if it measures weekly or monthly readings and have a setting (state) for triggering reset based on that.
c. Voltage readings on V1 are not available, but on V2 reading for L1, L2 are same as the combined reading. So there is really no need to report Volts for L1 & L2. HEM V2 measures the supply voltage only.
d. I use (in another APP) sendgrid.com send myself email alerts/reports and trigger IFTTT. This device handler. its as simple as this:

        emailadd = "first.last@host.com"
	emailname = "First Last"

    params = [
        uri: "https://api.sendgrid.com/api/mail.send.json",
        body: [api_user: "SmartThings", api_key: "xxxxxxx", to: emailadd, toname: emailname, subject: subject, text: msg, from: "noreply@anyhost.com", fromname: "SmartThings"]
    ]
	sendEmail(params)

I use this to post daily consumptions in my family’s in box :slight_smile:


(Barry) #15

Jayant -

Thanks for the suggestions!!!

It is difficult to do timed events from within a SmartDevice driver, as you cannot schedule() events. I guess we could check the time on every received transmission from the device itself and build a scheduler around that, but that’s a bit more than I want to bite off. Without that, sending to Xively or sending an email is best handled by an external SmartApp.

I’m going to drop the L1/L2 voltage display, just ran out of time yesterday. I’ll also add preferences for the kWh and Watts/Amps/Volts reporting frequency.


(Jjhamb) #16

You are welcome @storageanarchy, Will look forward to configurable intervals. I agree KWH need not be as often as 60S, I personally use 600s.


(Barry) #17

Check out the latest version on my GitHub. It has configurable delays and a few other updates.


(Barry) #18

Latest version of my Aeon HEMv2+ is now “completed” and available here:

Among other things,

  • now tracks all stats continuously, regardless of which are being displayed
  • Activity log and debug log are optimized with short texts (descriptionText: on EVERY sendEvent())
  • Preferences for both kWh and Watts/Amps/Volts update delays (default 120 & 30 seconds, respectively)
  • No longer requests or displays Voltage on L1/L2 screen, since these always match the Total Voltage display (HEMv2 measures Voltage from the line power only, not via the Clamps). Code is still there, just commented out.
  • Doubtful this will work for HEMv1 any more, but I’ve left all the code there if someone wants/needs to use it, they can with minimal editing
  • Still doesn’t work on Android devices (I guess I’m pushing the limits of the platform with color and pseudo-dynamic displays).

Let me know if you have any problems, suggestions or requests!


(Ian D) #19

My house frequently experience power outages. It would be nice if I could use my Home Energy Monitor to trigger some action when the power is lost and more importantly, when it’s back again. I have about 30+ Philips Hue bulbs and when the power comes back they all turn on at full brightness- not exactly fun a 2am. It happened to me five times last night!

My ST hub and my wifi router are on a UPS so they still run when the power is out.


(Greg) #20

@reward72

I use Scottinpollock’s power failure app to turn off my GE links and TCP lights after a power outage. It requires the use of a powered ST motion and it won’t work for blink outages.

I’m hoping to find another solution that will address blink outages that turns all my lights on, but it’s the best I’ve seen so far.


(Ian D) #21

Thanks Greg and thank Scottinpollock. I see it’s using the sensor’s notification of when it switches from AC power to battery and back. Not a bad idea.