Zigbee Low Battery Warning

I have about 20 Zigbee devices in my home. Some of them are linked to Xiaomi Hub, others link to Tuya, but most the vast majority of the sensors and buttons are connected to my Raspberry Pi via CC2531 Zigbee Stick. Mobile apps come with built-in low battery notifications, but I don’t have that option with my NodeRED server. It’s time for Zigbee Low Battery notification system?

Zigbee Low Battery Warning

The Zigbee Low Battery notification system will consist of 2 interfaces. There is a dashboard template, that displays all battery-powered devices at once, giving you an overview of the battery levels and a mobile notification system that warns you when the battery is running low and let you purchase batteries directly from the notification.

This setup will handle new devices, deal with friendly_names changes and post the battery levels as soon as device post the update back to the system. For the most part, you will be able to run this without an elaborate setup.

This NodeRED flow tackles the Zigbee devices linked via Zigbee2MQTT, but with small changes, it can handle any battery operating devices that report its state back to the server.

Imperfect readouts

IKEA Trädfri is great but is a bit marred with inconsistent battery readouts. I’m not 100$ sure what is the cause of this, but the readouts stuck on certain levels despite the battery swaps. It’s too early to say how much this system would be affected as it will take months to drain the CR2032 powered Zigbee sensors completely, but the software part of it seems to be working ok. Other Zigbee devices (Xiaomi, Tuya) seem to be reporting ok.

Imperfect Zigbee standards

The recent success in talks between Google Amazon and Apple in regards to Zigbee standards may bring a brighter future, but for now, there are 3 ways to report back about the battery levels:

  • battery_low
  • battery
  • voltage

Not every device supports all 3 values, but the system harvests all values should you want to use it. To monitor battery levels, I’m going to use the % info that is contained in the msg.payload.battery.

NodeRED Flow

I automated the system as much as possible so you don’t have to babysit anything. Each Zigbee battery-operated device will be added and monitored as soon as it submits battery value.

To search for devices and track the name changes, I load the values from configuration.yaml. If you don’t know how to add the custom names and edit this file, check the guide to adding new devices to Zigbee2MQTT. These values will create a library with this script:

FUNCTION NODE: Create Zigbee List
var x = {"devices":msg.payload.devices};
var devices = [];
var device = Object.keys(x.devices);
var name = Object.values(x.devices);

var arr = Object.entries(x.devices);
    for ([device,name] of arr) {
        var n = name.friendly_name;
        
        var a = {device, "name": n}
        
        devices.push(a);
        flow.set("ZigbeeDeviceNames", devices);
    }

return msg;

I will use that library to look up Zigbee device IDs and track name changes. The result is saved in a flow variable, so make sure you enabled storing the variables in NodeRED (tutorial).

I don’t want to set up a custom flow for each device, so to monitor all of the payloads sent from Zigbee2MQTT I used a wildcard in the main topic: zigbee2mqtt/#. That’s a lot of payloads, so to limit the activities, I will action only the ones that come with battery update (msg.payload.battery).

Each payload with battery info is evaluated against the library from the .yaml file and then looked up in my current device list. If the device name isn’t present, a new entry is added. Each device entry looks like this:

{
     "device": "0x14b457fffed411a5",
     "info": {
         "battery": 60,
         "timestamp": "2020-01-15T19:28:44.676Z",
         "batteryType": "CR2450",
         "batteryETA": "TBD",
         "battery_low": false,
         "voltage": false,
         "name": "IKEA_blue",
         "batteryURL": "https://amzn.to/2Tlaf5n"
     }
 }

New devices won’t have the batteryType, batteryURL assigned. These can be added based on the device name in the Add Battery Info flow. This flow will add the battery info to the list, link up a purchase link (affiliated in the project so change that if you don’t wish to support my work) which will show up as a button in Android notification.

The main script will try to find the device on the list, so if one is found, the battery information is updated.

FUNCTION NODE: Save Battery
var battery = msg.payload.battery;
var batteryLow = msg.payload.battery_low;
var voltage = msg.payload.voltage;
var deviceTopic  = msg.topic;
var ZigbeeDeviceNames = flow.get("ZigbeeDeviceNames");

var zigbeeBattery = flow.get("zigbeeBattery");

if(zigbeeBattery === undefined){
    zigbeeBattery = [];
}
if(zigbeeBattery === undefined){
    zigbeeBattery = [];
}

var test = isNaN(battery);

//battery info present
if(test === false){
    
    //get device ID
    var z1 = /zigbee2mqtt\/(.*)/.exec(deviceTopic);
    var id = z1[1];
    
    //get name and ID
    var posName = ZigbeeDeviceNames.map(function(e) { return e.name; }).indexOf(id);
    node.warn(posName);
    var deviceId = ZigbeeDeviceNames[posName].device;
    var deviceName = ZigbeeDeviceNames[posName].name;
    node.warn(posName);
    //optional voltage and low batt 
    var testBatt = isNaN(batteryLow);
    var testVolt = isNaN(voltage);
    
    if(testBatt === true){batteryLow = false;}
    if(testVolt === true){voltage = false;}
    
    //get time
    var time = new Date();
    
    var pos = zigbeeBattery.map(function(e) { return e.info.name; }).indexOf(deviceName);
    node.warn(pos);
    if(pos === -1){
        var a = {"device": deviceId, 
                  "info":{ 
                    "battery":    battery,
                    "timestamp":  time,
                    "batteryType": false,
                    "batteryETA":  "TBD",
                    "battery_low": false,
                    "voltage": voltage,
                    "name": deviceName}
                    };
        zigbeeBattery.push(a);
        flow.set("zigbeeBattery", zigbeeBattery);
    }
    if(pos => 0){
        var battType = zigbeeBattery[pos].info.batteryType
        var a = {"device": deviceId, 
                  "info":{ 
                    "battery":    battery,
                    "timestamp":  time,
                    "batteryType": battType,
                    "batteryETA":  "TBD",
                    "battery_low": batteryLow,
                    "voltage": voltage,
                    "name": deviceName}
                    };
    }
    zigbeeBattery[pos]= a;
    flow.set("zigbeeBattery", zigbeeBattery);
}
return msg;
Chart

I found node-red-contrib-dashboard-average-bars online which as a very handsome looking chart. You will need to add this from Palette Manager. The node isn’t ideal as it creates an average value for each payload submitted. I didn’t want to edit that since a simple workaround was possible.

Zigbee batteries will discharge slowly. This means that the daily average will be almost the same as the battery level for the device. If reset this chart daily, then next time it loads up, the battery charts will reflect the correct, un-averaged values.

As per chart itself, set the x-axis to msg.topic and use the script to send a payload for each device from the zigbeeBattery array. Link the output to a default template node to display the bars!

FUNCTION NODE: Update Chart
var data = flow.get("zigbeeBattery");

arr = [];

var count =  data.length;
    for ( i=0; i < count; i++){
        var nmsg = {payload:data[i].info.battery, topic:data[i].info.name};
        arr.push(nmsg);
    }

return [arr];
Android notification

I used the Perfect Notification and Credential system to send the information over to an Android device. Once a day, the script will check all Zigbee devices and sends a message if any of the devices goes below a custom threshold.

In settings specify the low battery level and the name of the device you want to notify. A list of the devices will be composed. This will consist of additional arrays storing the battery type info and purchase links.

That information is then sorted, processed and the JSON body for Perfect AutoNotification is composed.

FUNCTION NODE: Android Notification
var data = flow.get("zigbeeBattery");
var batteryWarning =  flow.get("BatteryWarning");
var device = flow.get("NotificationDevice");


var key = global.get(device);
var url = "https://autoremotejoaomgcd.appspot.com/sendmessage";
var command = "NOT20";



var battPush = [];
var buttons = {};
var battBuy = [];
var battLink = [];

for (var i=0; i < data.length; i++) {
var batt = data[i].info.battery;
var battType = data[i].info.batteryType;
if(batt <= batteryWarning){
var name = data[i].info.name;
if(battType === false){
var battTypeDisplay = "battery type configuration.\ ";

}
else { var battTypeDisplay = battType;
battBuy.push(battType);
battLink.push(data[i].info.batteryURL);
}
var lowBatt = "<li><strong>"+name + ":</strong><span style=\"color: #ff9900;\"> "+ batt +"%</span><span style=\"color: #333333;\"><em> needs </em><strong>" + battTypeDisplay + "</strong></span></li>";
battPush.push(lowBatt);
}
} if(battPush.length > 0){ var reg = battPush.toString(); var reg1 = reg.replace(/,/g, ""); var body = {
"text": {
"text": "A number of your Zigbee devices need a new battery" ,
"textexpanded": "<ul>"+ reg1 + "</ul>"
},
"title": {
"title": "Zigbee Low Battery Warning",
"titleexpanded": "Zigbee Low Battery Warning"
},
"icons": {
"navbaricon": "https://raw.githubusercontent.com/google/material-design-icons/master/device/2x_web/ic_battery_alert_black_48dp.png",
"bigicon": "https://raw.githubusercontent.com/google/material-design-icons/master/device/2x_web/ic_battery_alert_black_48dp.png",
"smallicon": "https://raw.githubusercontent.com/google/material-design-icons/master/device/2x_web/ic_battery_alert_black_48dp.png",
"iconexpanded": "https://raw.githubusercontent.com/google/material-design-icons/master/device/2x_web/ic_battery_alert_black_48dp.png"
},
"notificationid": "ZigbeeBattery",
"persistent": false,
"priority": 1,
"color": "#b9512c",
"backgroundcolor": "#fafafa",
"picture": "pictureurl",
"buttons": ""
};
var battBuy1 = [ ...new Set(battBuy) ]; var battLink1 = [ ...new Set(battLink) ]; var objButton ={}; for (var a = 0; a <battBuy1.length; a++) { var b = a +1 var keyButton = "button"+ b; objButton = { "icon": "iconURL", "label": "Buy " + battBuy1[a], "command": "buybattery*-*" + battLink1[a] }; node.warn(objButton); buttons[keyButton] = objButton; node.warn(buttons); } body["buttons"] = [buttons]; msg.data = body; var x = JSON.stringify(body); var encodedBody = encodeURIComponent(x); msg.url = url + "?key=" + key + "&message=" +command + "=:="+ encodedBody; return msg; }

Tasker

To create Zigbee Low Battery warning for Android I used Tasker and slightly modified version of Perfect AutoNotifications. You will need the following plugins to make this work:

  • AutoApps (for commands)
  • AutoRemote (to send messages between NodeRED and Android)
  • AutoNotification (to display notifications)
  • AutoTools (to handle JSON files and open websites)
Fixing Button Commands

Because AutoRemote (AR) is sending a command and case-sensitive URL, I have to use a trick to prevent my purchasing task from triggering. Both would trigger then the AR message is sent if the message contains the part of the filter used by the button command.

I set my button command to upper case, but the command looks like this: buybattery*-*htttp://SensitiveLink.com. To make it work, I split the command by *-* and convert the part one to upper case

 A4: If [ %buttons_button1_command Set ]
 A5: Variable Split [ Name:%buttons_button1_command Splitter:- Delete Base:Off ] 
 A6: Variable Convert [ Name:%buttons_button1_command(1) Function:To Upper Case Store Result In: Mode:Default ] 
 A7: Variable Set [ Name:%buttons_button1_command To:%buttons_button1_command(1)%buttons_button1_command(2) Recurse Variables:Off Do Maths:Off Append:Off Max Rounding Digits:3 ] 
 A8: End If 

My result is then posted as button command to the notification. When notification button is pressed the command looks like this: BUYBATTERYhtttp://SensitiveLink.com.

Do this for every button (at the moment you can set up up to 3 buttons with empty variables, setting up the 4th will give you an error - reported as a bug) that you wish you use. Don't forget to modify the AN AutoNotification action to contain the relevant variables.

Buy Batteries

Triggered by AutoApps filter BUYBATTERY, the %aamessage is split by :// to extract the URL. The 2nd element from that newly created array is passed as URL to AutoTools to open the custom Chrome window to complete the purchase.

TASKER TASK: Buy Batteries
Buy Batteries
	Abort Existing Task
	A1: Variable Set [ Name:%Url To:%aamessage 
		Recurse Variables:Off 
		Do Maths:Off 
		Append:Off 
		Max Rounding Digits:3 ] 
	A2: Variable Split [ Name:%Url Splitter::// 
		Delete Base:Off ] 
	A3: AutoTools Chrome Custom Tabs [ 
		Configuration:Url: http://%Url(2) 
		Timeout (Seconds):300 ]

Conclusion

With a bit of tweaking, the system can work with any battery-operated devices that report the battery levels back. A great way of making sure that all your devices never run out of juice. Zigbee Low Battery warning should keep tabs on my temperature sensors scattered around the house. These are used to keep my house warm in conjunction with the $5 NEST alike thermostat. If you have any questions about this tutorial consider visiting this Reddit thread.

Support NotEnoughTech
A lot of time and effort goes into keeping NotEnoughTech alive! If my work helped you out, consider buying me a coffee or check out exclusive rewards available to Patreon supporters.
SHARE