Creating battery reports

Hi,
today I was thinking of creating a report (maybe persistence) with all of my batterie driven devices. I want to send this report in a email once a month to see which batteries might be changed soon. I am not quite sure of the howto. I mean it`s quite easy to send an attachment with the email plugin, but I don’t really know how to fill the file.
Maybe someone knows a good way to do this.

You don’t really need to create a file to attach.
You could setup a rule that checks (e.g. once a day/week/month/etc) the battery levels (Item states) and sends an email notification with the ones below a defined threshold.

Check the DPs for rule examples. Start with: Design Pattern: Working with Groups in Rules

I studied the DPs a lot and I am already working with groups in rules. I only thought there is a nicer solution for storing values in a file instead of building a very long string.

Here’s what I do. It depends on all battery level items being part of group gBattery and the battery level update time items being part of the group gBatteryLastUpdate.

val int NUMBER_HOURS = 8

rule "Battery Health Monitor"
when 
    Time cron "0 0 19 * * ?"
then
    logInfo("battery-health-check", "BATTERY: Checking health of battery devices")

    var SimpleDateFormat df = new SimpleDateFormat( "MM/dd" )
    var String timestamp = df.format( new Date() )

    val String mailTo = AlertEmailAddress.state.toString
    var String subject = "Battery Health Report for devices on " + timestamp
    val StringBuilder body = new StringBuilder("Battery health report:\n\n")

    // List battery levels from lowest to highest
    gBattery.members.sortBy[(state as Number).intValue].forEach [ NumberItem myItem | {
        body.append(String::format("%-30s%8.4s\n", myItem.name, myItem.state.toString))
    }]

    body.append("\n\n")

    // List battery levels that haven't been updated in NUMBER_HOURS hours
    gBatteryLastUpdate.members.forEach [ GenericItem myItem | {
        if (myItem.state !== NULL) {
            var DateTime dateTime = new DateTime((myItem.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)
            if (dateTime.isBefore(now.minusHours(NUMBER_HOURS))) {
                var SimpleDateFormat lastUpdateTime = new SimpleDateFormat( "MM/dd/YYYY HH:mm:ss" )
                var long t1 = (myItem.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli
                logWarn("battery-health-check", "BATTERY: No battery update from " + myItem.name + " since " + lastUpdateTime.format(new Date(t1)))
                body.append("No battery update from '")
                    .append(myItem.name)
                    .append("' in at least ")
                    .append(NUMBER_HOURS)
                    .append(" hours:\t")
                    .append(lastUpdateTime.format(new Date(t1)))
                    .append("\n")
            }
        }
    }]

    logInfo("battery-health-check", "BATTERY: Sending battery health check email")
    sendMail(mailTo, subject, body.toString())
end

Note: I wrote this rule a long time ago (in fact it’s one of the first rules I wrote), so there may be some things that could be done more efficiently.

5 Likes

I guess I also should include how I’ve defined the items, as well as the rule that maintains the update time.

Number Office_BatteryLevel            "Office Battery Level [%.0f %%]"                   <battery>           (gBattery)                          { channel="zwave:device:zstick:node42:battery-level" }
DateTime Office_BatteryLevelTime      "Last Updated [%1$tm/%1$td/%1$tY %1$tT]"           <clock>             (gBatteryLastUpdate)
import java.util.concurrent.locks.ReentrantLock

// Determine the time when a battery level was updated
// Post the time to an item called item.name + Time
// Define item like this:
// DateTime XXXXX_BatteryLastUpdate "Last Updated [%1$tm/%1$td/%1$tY %1$tT]"    <clock>     (gBatteryLastUpdate)
// where XXXXX is the name of the battery level item

val Procedure$Procedure1<NumberItem> handleBatteryUpdate = [
    NumberItem myItem |

    if (myItem !== null) {
        val itemName = myItem.name + "Time"
        var i = gBatteryLastUpdate.members.findFirst[ name.equals(itemName) ]
        if (i !== null) {
            //logInfo("handleBatteryUpdate", "BATTERY: Updating time of battery level change: " + itemName)
            i.postUpdate(new DateTimeType());
        }
    }
]


val ReentrantLock latch = new ReentrantLock

rule "Handle Battery Update"
when
    Member of gBattery received update
then
    try {
        latch.lock

        val NumberItem item = triggeringItem as NumberItem

        //logInfo("handle-battery-update", "BATTERY: Update received from " + item.name + ": " + item.state)
        handleBatteryUpdate.apply(item)
    }
    catch (Exception e) {
        logError("battery-last-update", "BATTERY: Exception occurred in latched rule! " + e.toString)
    }
    finally {
        latch.unlock
    }
end

2 Likes

My rule is a bit simpler:

All my batteries are part of the group Batteries
This will send an email with all the batteries with a level less than 15%
I do it at 10:55am because it’s convenient for me… :smile:

val Number lowBatteryThreshold = 15

rule "Battery Monitor"
when
    Time cron "0 55 10 * * ?"
then
    if (!Batteries.allMembers.filter( [ battery | (battery.state as Number) <= (lowBatteryThreshold) ] ).empty) {
        val String report = Batteries.allMembers.filter( [ (state as Number) <= (lowBatteryThreshold) ] ).sortBy( [ state as DecimalType ] ).map[ name + ": " + state.format("%d%%") ].join("\n")
        val message = "Battery levels:\n\n" + report + "\n\nRegards,\n\nOur House"
        sendMail("vzorglub@gmail.com", "Low battery alert !", message)
    }
end
6 Likes

This is very close to mine. I also trigger this when a device in group battery changes. So I get that immediate update. I also have a lookup table to remind me what kind of battery.

import org.openhab.core.library.types.DecimalType
import java.util.HashMap
import util.LinkedHashMap

val int lowBatteryThreshold = 10
var HashMap<String, String> batteries =
        newLinkedHashMap(
            "LivingRoomLightsSwitchBatteryLevel" -> "CR2032 Coin Cell",
            "HVAC_Battery" -> "4 AA",
            "PoolSensorTempBattery" -> "1 18650 li-ion"
        )





rule "Battery Monitor"
when
   Time cron "0 0 0 * * ?" or
   Item gBattery received update
then
    if (! gBattery.allMembers.filter([state < lowBatteryThreshold]).empty) {
        val report = gBattery.allMembers.filter([state instanceof DecimalType]).sortBy([state]).map[
name + ": " + state.format("%d%%") + ":Type=" + batteries.get(name)
        ].join("\n")

        val message = "Battery levels:\n\n" + report + "\n\nRegards,\n\nopenHab"
        sendMail("emailgoeshere", "[HA] Battery Report", report)
     
    }
end

3 Likes

It`s a nice idea to even tell what kind of batterie is needed. But I don’t understand where ‘name’ in .get(name) is defined as the second string of the hashmap. How is that working?

Hi

I’m bring life to this threwad again. I run OH 2.5 and would like to implement Marks solution. But get one error I couldn’t really sort out.

I made two diffrent files for rules one for survailence and one for report. Report rules semms to load correctly but survailence doesn’t. I paste log entrys below.

08:12:56.721 [INFO ] [del.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model ‘battery.rules’, using it anyway:
The field Tmp_batteryRules.handleBatteryUpdate refers to the missing type Object
08:12:56.749 [INFO ] [del.core.internal.ModelRepositoryImpl] - Refreshing model ‘battery.rules’

Just to share my approach here as well and I hope it’s useful for one or the other…

In general I work also with Groups, means all my Batteries are in a group.
Whenever one of these changes, I check the level and activate a Battery Alarm switch for the specific (_Sw) item.
I then also trigger to send the type of Battery which is needed on a daily report in the morning:

items:

Number:Dimensionless       Z_Bath_W_Batt         "Bad Fenster Batterie [%.0f %unit%]"            <batterylevel>    (G_Num,G_Batt,gBathBattery)                      ["Measurement","Energy"]          {channel="zwave:device:zwaveUSBCtrl:node18:battery-level", widgetOrder="04"}
Switch                     Z_Bath_W_Batt_Sw      "Bad Fenster Batterie Alarm"                    <siren>           (G_jdbc,G_Batt_Sw,gBathBattery)                  ["LowBattery"]                    	// {widgetOrder="05"}
Group:Number                G_Batt             "Batterien [%s]"
Group:Switch:OR(ON,OFF)     G_Batt_Sw          "Batterie-Alarm"
String                      Act_Batt_Alarm     "Aktive Batterie-Alarme [%s]"                             (G_jdbc)                               	// from G_Batt_Sw
String                      Req_Batt           "Benötigte Batterien [%s]"                                (G_jdbc)                               	// from G_Batt_Sw based on batteries.map

Rule for checking the level and set battery alarm accordingly

// ***************** Battery driven Device monitoring ***************//
rule "Battery Check"
when
    Member of G_Batt changed
then
	val action = getActions("telegram","telegram:telegramBot:MyBot")
// get triggering item and its state
	val itemName = triggeringItem.name.toString
	val Number itemState = triggeringItem.state as Number
// get the matching _Sw to triggering item and its state
	val String itemNameSw = itemName + "_Sw"
	val itemSwState = ScriptServiceUtil.getItemRegistry.getItem(itemNameSw).state
// skip accumulator driven devices
	if(itemName == "BerlingBatt" || itemName == "Xido_Batt") return; // failing fast
// special handling for Z_Lock
	if(itemName == "Z_Lock") {
		if(itemState < 40) {
			postUpdate(itemNameSw, "ON")
			logInfo("+++ SYSTEM", "Battery Alarm activated for " + itemName)
			action.sendTelegram("Die Batterien von " + itemName + " sind bald leer (" + itemState + " %%)")
		}
		else {
			if(itemSwState != OFF) {
				postUpdate(itemNameSw, "OFF")
				logInfo("+++ SYSTEM", "Battery Alarm DE-activated for " + itemName)
			}
		}
		return; // failing fast
	}
	if(itemState < 12) {
		if(itemSwState != ON) {
			postUpdate(itemNameSw, "ON")
			logInfo("+++ SYSTEM", "Battery Alarm activated for " + itemName)
		}
	}
	else {
		if(itemSwState != OFF) {
			postUpdate(itemNameSw, "OFF")
			logInfo("+++ SYSTEM", "Battery Alarm DE-activated for " + itemName)
		}
	}
end

Rule for checking the active alarms and set the type of batteries to be replaced

rule "Battery Alarm State"
when
    Member of G_Batt_Sw changed
then
	if(G_Batt_Sw.state == ON) {
	// screening of active battery alarms
		val String tmp = G_Batt_Sw.members.filter [ i | i.state == ON ].map[ label ].reduce[ s, label | s + ", " + label ]
		Act_Batt_Alarm.postUpdate(tmp)
	// checking associated / required batteries
		val String batt = G_Batt_Sw.members.filter [ i | i.state == ON ].map[ name String::format("%s",transform("MAP", "batteries.map", name)) ].reduce[ s, name | s + ", " + name ]
		Req_Batt.postUpdate(batt) // Required batteries for replacement
		logInfo("+++ SYSTEM", "Battery alarm active on: " + Act_Batt_Alarm.state.toString + " -> Required Batteries: " + Req_Batt.state.toString)
	} 
	else {
		Act_Batt_Alarm.postUpdate("Keine")
		Req_Batt.postUpdate("Keine") // No batteries for replacement required
	}
end

map.file:

BerlingBatt_Sw=Interner Akku
Xido_Batt_Sw=Interner Akku
Outd_Batt_Sw=2 x AAA
Rain_Batt_Sw=2 x AAA
Gard_Wat_Batt_Sw=3 x AA
Gard_Sens_Batt_Sw=2 x AA
Z_Bath_W_Batt_Sw=1 x ER 14250
Z_Shock_Batt_Sw=1 x CR123A
Z_Flood_Batt_Sw=1 x CR123A
Z_Motion_Batt_Sw=1 x CR123A

After getting up in the morning I get a Telegram message with the battery types to be replaced.

1 Like

I do it this way:

I just have a rule that triggers when a battery drops below a certain percentage, which sends a notification to the openHAB app.