Low Battery Alert (Example Rule Not Working)

I’m looking to check a group that i have all my battery devices assigned to, and alert me if the battery level is below a specific threshold.

I tried the example code located here:

But it doesn’t seem to filter out the devices that are below the thresthold. Where is it going wrong? I’m on OH2.

Here is my rule and output:

val int lowBatteryThreshold = 10

rule "Battery Monitor"
//when Time cron "0 0 19 * * ?"
when
	Item vTest changed
then
	logInfo("RULE.RUNNING.BATTERYCHECK", "### CHECKING BATTERY AND ALERTING ###")
	 if (!gBatteryStatus.allMembers.filter([state < lowBatteryThreshold]).empty) {
        val report = gBatteryStatus.allMembers.filter([state instanceof DecimalType]).sortBy([state]).map[
            name + ": " + state.format("%d%%")
        ].join("\n")
        
        val message = "Battery levels:\n\n" + report + "\n\nRegards,\n\nopenHab"
        
        logInfo("RULE.RUNNING.BATTERYCHECK", message)
    }
	  	
end

==> /var/log/openhab2/events.log <==
2017-10-21 21:08:30.931 [ome.event.ItemCommandEvent] - Item ‘vTest’ received command OFF
2017-10-21 21:08:30.938 [vent.ItemStateChangedEvent] - vTest changed from ON to OFF
==> /var/log/openhab2/openhab.log <==
2017-10-21 21:08:30.940 [INFO ] [del.script.RULE.RUNNING.BATTERYCHECK] - ### CHECKING BATTERY AND ALERTING ###
2017-10-21 21:08:31.030 [INFO ] [del.script.RULE.RUNNING.BATTERYCHECK] - Battery levels:
vGarageRearLock_Battery: 0%
vSiren_Battery: 60%
vGarageLock_Battery: 64%
Kitchen_PIR_Battery: 66%
vHouseLock_Battery: 97%
EntranceStair_PIR_Battery: 100%
vGarageDoor_Battery: 100%
vHouseDoor_Battery: 100%
vPatioDoor1_Battery: 100%
Regards,
openHab

As you can see it listed all devices that are still above the 10% threshold

You are not using filter correctly.

filter[battery | battery.state as Number < lowBatteryThreshold]

You have to give the lambda an argument and then reference the argument in the body of the lambda passed to filter.

That is one problem.

I’m not sure about the map and join stuff either but I’m pretty sure that is wrong too because your code inside the if statement doesn’t filter anything out and will always produce a report with all the items.

Start with fixing the filters and see where that gets you. Then apply the same filter you use to see if any item is below the threshold to the line that builds the report.

1 Like

Did someone else figured this out?

I also want to get notified when batteries are getting low.

when I use a foreach, I only get tge items I wany, but I can’t get it into one string

battery.members.forEach [i | if (i.state < 20) {logInfo("Debug", i.name + " - " + i.state.toString) } ]

Here is a solution in my usecase that works fine. Only the the notification in sitemap is lack. :slight_smile:
Instead of telegram you can simple adapt a other service.

import java.util.HashMap
import java.util.ArrayList
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock

var Lock lock = new ReentrantLock()

//
// init values
// -----------------------------------------------------------------------------------------------------------
// customize for you own enviroment

var HashMap<String, Object> batteryNotificationInits = newLinkedHashMap(
	"debounceDelay" -> 300,		 						// debounce time state updates in group [ms]
	"message" -> "Batterietausch\n",                    // message subject then batterychange notice
	"telegramBot" -> "TelegramBotName",						// if the telegram bot available then bot name else ""
    "triggerValues" -> newArrayList(7,10,15),           // trigger values
	"debounceGroupTimer" -> null as Timer				// debounce timer						- no config
)

//
// data structure
// -----------------------------------------------------------------------------------------------------------
// you will get a telegram message then the batterylevel reached a specified level 
// triggerValues specified the level
// the first value (i.e. 7) means, you will get a message for 1%,2%,3%,4%,5%,6%,7%
// all other value means, you will get a message at this point 
//
// the battery battery values should be from 1% to 100%
//
// in item file:
// all battery obtained devices must be assigned to the group gBatteryStatus


rule "battery status and send notifications"
when
    Item gBatteryStatus received update
then
	try {
		lock.lock()
		var Timer debTimer = batteryNotificationInits.get("debounceGroupTimer")

        if (debTimer === null) {
            var expTime = now.plusMillis(batteryNotificationInits.get("debounceDelay"))
            debTimer = createTimerWithArgument(expTime, "debounce", [ p |
                try {
                    lock.lock()
                    val String bot = batteryNotificationInits.get("telegramBot")
                    var boolean telegramBotExist = false
                    var String notificationMessage = batteryNotificationInits.get("message")
                    var String notificationMessageItems = ""
                    var ArrayList<Integer> triggerValues = batteryNotificationInits.get("triggerValues") as ArrayList<Integer>
                    val Integer minToTrigger = triggerValues.get(triggerValues.size()-1)
                    if (batteryNotificationInits.get("telegramBot") != "") {
                        telegramBotExist = true
                    }

                    var triggertDevices = gBatteryStatus.members.filter[s|s.state <= minToTrigger]

                    triggertDevices.forEach [ i |
                        var Boolean addItem = false
                        if (i.state <= triggerValues.get(0)) {
                            addItem = true
                        }
                        if (triggerValues.contains((i.state as DecimalType).intValue)) {
                            addItem = true
                        }

                        if (addItem) {
                            notificationMessageItems = notificationMessageItems + i.name + ': ' + i.state.toString + '%\n'
                        }
                    ]

                    if (telegramBotExist && (notificationMessageItems != "")) {
                        sendTelegram(bot,notificationMessage + notificationMessageItems)
                    }

                    // insert message in Sitemap

					batteryNotificationInits.put("debounceGroupTimer",null)
					lock.unlock()
				}
				catch(Throwable t) {
			        logError("RuleBattery","E0201 something is wrong: " + t.toString)
        			lock.unlock()
    			}
			])
			batteryNotificationInits.put("debounceGroupTimer",debTimer)
        } else {
            debTimer.reschedule(now.plusMillis(batteryNotificationInits.get("debounceDelay")))
        }
        lock.unlock()
	}
    catch(Throwable t) {
        logError("RuleBattery","E0202 something is wrong: " + t.toString)
		smokeAlarmInits.put("debounceGroupTimer",null)
        lock.unlock()
    }
end
1 Like

A little simpler rule that checks the battery status every day at 17:00.
All battery powered devices must be assigned to the group gBattery.

rule "Battery Status Check"
when
    Time cron "0 0 17 * * ?"
then
    var String msg = ""
    var triggertDevices = gBattery.members.filter[s|s.state <= 20]

    triggertDevices.forEach [ i |
        msg = msg + i.name + ': ' + i.state.toString + '%\n'
        logInfo("battery-check.rules","Low battery at " + i.name + ": " + i.state.toString + "%")
    ]

    if (msg != "") {
      sendMail("email@domain.com", "Battery warning", msg)
    }
end
4 Likes

Hi All,

My filter looks right but i think the map component is incorrect in my rule as I get the notification with the correct percentage of the battery but it lacks the device name at the start of the alert.


rule "Battery Status Check"
when
    Item gBatteries changed
then
    var String msg = ""
    var triggertDevices = gBatteries.members.filter[s|s.state <= 20]

    triggertDevices.forEach [ i |
        msg = msg + (transform("MAP", "batteries.map", i.name) + ': ' + i.state.toString) + '%\n'
        logInfo("Battery Check","Low battery at " + i.name + ": " + i.state.toString + "%")
    ]

    if (msg != "") {
sendBroadcastNotification(msg + " of your battery remaining")
    }
end

batteries.map

100=Full
90=90%
80=80%
70=70%
60=60%
50=50%
40=40%
30=30%
25=25%
20=20%
15=15%
10=10%
5=5%

I cant see anything obvious im missing. Does anyone have a working low battery rule using a transform map?

Thanks

1 Like

What is the type of the members of gBatteries? Number? Dimmer? Number:Dimensionless? What type it is will dictate what is appropriate for the filter.

Secondly, you are passing an Item name to the transform but your MAP only has numbers in it. Furthermore, the MAP will only work if what you pass to it is exactly those values. You will get an error if the battery happens to be 91 or 90.000001. So unless you have Items named 100, 90, 80, etc. this is almost certainly not what you want. You should have the name of the item on the left and a “human readable” version of the name on the right. See Design Pattern: Human Readable Names in Messages

Hi Rich, the type is Number

Apologies, I only pasted in a portion of the map

the entire ‘batteries.map’ is:


FrontDoor_Battery=Front Door Lock
GarageRollerDoor_Battery=Roller Door Sensor
BedRoom1_AeoButton_Battery=Bedroom Remote
Kitchen_AeoButton_Battery=Kitchen Remote
FrontDoor_AeoButton_Battery=Front Door Remote
LivingRoom_Zrc_Battery=Fridge Scene Remote
100=Full
90=90%
80=80%
70=70%
60=60%
50=50%
40=40%
30=30%
25=25%
20=20%
15=15%
10=10%
5=5%

And in doing so I’ve realised I’m missing a statement in the map for the device! woops

FrontDoor_DoorLock_Battery
1 Like

You might have further errors. You can’t access a var from inside a forEach. That’s why the OP uses a map/join to build the message. Other approaches are to use map/reduce or a StringBuilder to construct the message.

And it is still the case that the number entries in your map file will only work if the numbers happen to be exactly those on the left. If all you are doing is appending a “%” and using “Full” if the value is 100, you are far better off doing this in code rather than using the map.

I get what your saying about the map file, but the rule certainly works (as does MriX’s post, which is the same as my rule?)

Yes, all I want to really do is append a % but I don’t want to do that for say anything above 20% if that makes sense. How would I do that in code?

You’ve already filtered out everything over 20 so just append the % like you already are doing.

1 Like

This rule doesn’t use a transform, but here is a modern example using scripted automation, Jython, and the helper libraries

from core.rules import rule
from core.triggers import when
from personal.utils import notification

@rule("Power: Device battery monitor")
@when("Member of gBattery changed")
def device_battery_monitor(event):
    low_battery_threshold = 20
    if event.itemState <= DecimalType(low_battery_threshold):
        message = "Warning! Low battery alert:\n{}".format(",\n".join("{}: {}%".format(lowBattery.label, lowBattery.state) for lowBattery in sorted(battery for battery in ir.getItem("gBattery").getMembers() if battery.state < DecimalType(33), key = lambda battery: battery.state)))
        notification("Battery monitor", message)

You’ll need to setup your own personal.utils.notification function.

1 Like

Here is an rule I’m using for battery note that my threshold is set to 35%
My battery group is called batteryStatus
One thing if you have missed a mapping the item name will be used instead until you fixed your mapping

Rule

//////////////////////////// Battery status //////////////////////////////////
rule "Low battery alert"
when
    Item batteryStatus changed or
    System started
then

    var String msg = ""
    var triggeredDevices = batteryStatus.members.filter[s|s.state <= 35]

    triggeredDevices.forEach [ i |
      var name = transform("MAP", "batteries.map", i.name)
      if(name == ""){
        name = i.name
      }
      msg = msg + name + ': ' + i.state.toString + '%\n'
      logInfo("batteryStatus","Low battery at " + i.name + ": " + i.state.toString + "%")
    ]

    if (msg != "") {
      sendBroadcastNotification("Low battery \n"+msg)
    }
end
//////////////////////////// End Battery status //////////////////////////////

batteries.map

frontdoorHallwaySensorBattery=Dörrsensor hall
sensorHallwayBattery=Sensor hall
frontdoorLaundryRoomBattery=Dörrsensor tvättstuga
sensorStairwayBattery=Sensor trapp
batteryLockLaundryRoom=Lås tvättstuga
garageDoorBattery=Sensor garageport
doorAltanMovieRoomBattery=Dörrsensor biorum
doorBalconyUpstairBattery=Dörrsensor balkongdörr
doorFriggebodBattery=Dörrsensor friggebod
doorBalconyBattery=Dörrsensor sovrum
frysBattery=Sensor Frys
kylBattery=Sensor Kyl
frysFriggebodBattery=Sensor frys friggebod
utomhusTempBattery=Termometer Entre
sensorWalkInClosetBattery= Sensor Walk-In-Closet
NULL=-

Since we are all sharing :wink:

I don’t actually have a battery alert rule any more. I just have a Group and I periodically look at my sitemap to see what the minimum charge is. My smoke alarms start to beep at around 40% anyway and the door deadbolts have a flashing status light we see every day.

But I do have a Rule to monitor the online/offline status of various servers and services that are relevant to my home automation. And every morning at 8 I generate a report with a list of all those that are offline.

from core.rules import rule
from core.triggers import when
from core.metadata import set_metadata
from personal.util import send_info, get_name

@rule("System status reminder",
      description=("Send a message with a list of offline sensors at 08:00 and "
                   "System start"),
      tags=["admin"])
@when("Time cron 0 0 8 * * ?")
@when("System started")
def status_reminder(event):
    """
    Called at system start and at 8 AM and generates a report of the known
    offline sensors
    """

    numNull = len([i for i in ir.getItem("gSensorStatus").members
                   if isinstance(i.state, UnDefType)])
    if numNull > 0:
        status_reminder.log.warning("There are {} sensors in an unknown state!"
                                    .format(numNull))

    offline = [i for i in ir.getItem("gSensorStatus").members if i.state == OFF]
    offline.sort()
    if len(offline) == 0:
        status_reminder.log.info("All sensors are online")
        return

    offline_str = ", ".join(["{}".format(get_name(s.name)) for s in offline ])
    offline_message = ("The following sensors are known to be offline: {}"
                       .format(offline_str))

    for sensor in offline:
        set_metadata(sensor.name, "Alert", { "alerted" : "ON"}, overwrite=False)
    send_info(offline_message, status_reminder.log)

send_info and get_name are personal library functions.

from core.actions import NotificationAction
from core.jsr223.scope import actions
from configuration import admin_email, alert_email
from core.metadata import get_value

def send_info(message, logger):
    """
    Sends an info level message by sending an email and logging the message
    at the info level.

    Arguments:
        - message: The String to deliver and log out at the info level.
        - logger: The logger used to log out the info level alert.
    """
    out = str(message)
    logger.info("[INFO ALERT] {}".format(message))
    NotificationAction.sendNotification(admin_email, out)
    (actions.get("mail", "mail:smtp:gmail")
        .sendMail(admin_email,  "openHAB Info", out))

def get_name(itemName):
    """
    Returns the 'name' metadata value or the itemName if there isn't one.

    Arguments:
        itemName: The name of the Item
    Returns:
        None if the item doesn't exist.
    """
    return get_value(itemName, "name") or itemName

See:

Does the js script also works in oh3?

Here is my check batteries that is in OH3 and in Javascript: