Zigbee2MQTT Device Health Check with Node-RED: Automatic Monitoring of Your Smart Home Devices
With this Node-RED flow, you can monitor the online status of your Zigbee devices and receive automatic notifications when devices go offline. The system logs Zigbee device data and stores the connection status in the Flow Context for reliable smart home monitoring.

Table of Contents
- What is Zigbee2MQTT and why do we need a Health Check?
- How the Device Health Check System works
- Log function for Zigbee devices
- Zigbee2MQTT Device Health Check
- Notifications with Pushover
- Setting up and customizing the flow
- The complete Node-RED flow
What is Zigbee2MQTT and why do we need a Health Check?
Zigbee2MQTT is software that acts as a bridge between your Zigbee devices and MQTT. It allows you to integrate your Zigbee devices into your smart home without proprietary hubs or bridges and control them via the standardized MQTT protocol.
In a smart home system with many Zigbee devices, however, individual devices may lose contact or go offline. The reasons for this can be varied: battery problems, signal interference, or simply faulty devices. Without automatic monitoring, you may not notice these problems until you want to use the affected device.
A Device Health Check solves this problem by continuously monitoring the online status of all devices and notifying you as soon as a device is no longer reachable. This allows you to act proactively before problems occur in your smart home.
How the Device Health Check System works
Our Node-RED flow for the Zigbee2MQTT Device Health Check consists of three main components:
- Log function: Logs all data sent by the Zigbee devices to understand and evaluate the available information.
- Device Health Check: Monitors the online/offline status of the devices and stores it in the Flow Context.
- Pushover notification: Sends you a message when devices go offline or are not functioning properly.
The flow receives data via MQTT from your Zigbee2MQTT installations, processes the “availability” messages, and can perform a status check at set times (in our example at 8 PM) and inform you about offline devices.
Log function for Zigbee devices
Before we dive into the health check, it’s important to understand what data our Zigbee devices actually provide. For this, we implement a simple log function that writes all MQTT messages to a file.
// Create an object that contains both the topic and the payload let logEntry = { timestamp: new Date().toISOString(), // Optional: adds a timestamp topic: msg.topic, payload: msg.payload };
// Convert the object to a JSON string and add a line break
msg.payload = JSON.stringify(logEntry) + "\n";
return msg;
This function captures the topic and payload for each MQTT message, adds a timestamp, and saves everything as a JSON entry in a log file. This allows you to see exactly what data your devices are sending and which of them are relevant for the health check.
Particularly important for the health check are messages with the suffix /availability, as they contain the online status of the respective device.
Zigbee2MQTT Device Health Check
The core of our system is the Device Health Check function. It monitors the availability of devices and stores their status in the Flow Context.
// Initialize the device object in the flow context if it doesn't exist yet let devices = flow.get('zigbee2mqtt_devices') || {};
// Function to extract the device name from the topic
function extractDeviceName(topic) {
// Assumption: Topic format is "zigbee2mqtt_X/DeviceName/availability"
let parts = topic.split('/');
return parts;
}
// Function to check if the topic has one of the allowed prefixes
function hasValidPrefix(topic) {
const prefixes = ["zigbee2mqtt_1/", "zigbee2mqtt_2/"];
return prefixes.some(prefix => topic.startsWith(prefix));
}
// Check if the message is an availability message
if (msg.topic && hasValidPrefix(msg.topic) && msg.topic.endsWith("/availability")) {
// This is an availability message
let deviceName = extractDeviceName(msg.topic);
text
// **New filter: Ignore devices whose names start with '0x'**
if (deviceName.startsWith("0x")) {
// Ignore device, no further processing
return null;
}
let state = msg.payload.state; // Assumption: payload is an object with 'state'
// Update the device object
devices[deviceName] = state;
// Save the updated object back to the flow context
flow.set('zigbee2mqtt_devices', devices);
// Don't forward any message
return null;
} else {
// Assumption: This is a trigger from an inject node
// Check if msg.payload.trigger is set to true
if (msg.payload && msg.payload.trigger === true) {
// Create a new message with the current device state
let newMsg = { payload: devices };
text
// Send the new message
return newMsg;
} else {
// If trigger is not true, do nothing more
return null;
}
}
Let’s take a closer look at how it works:
Storage in Flow Context
The function uses Node-RED’s Flow Context to store the status of devices between different calls. Each time it starts, the current device status is loaded from the context or an empty object is initialized if no data is available yet.

Two Operating Modes
The function works in two modes:
- Update mode: When an availability message is received, the status of the corresponding device is updated in the Flow Context.
- Query mode: Activated by a trigger and returns the current status of all known devices.
Intelligent Filtering
The code filters incoming messages in various ways:
- Only messages with valid prefixes (
zigbee2mqtt_1/
orzigbee2mqtt_2/
) are processed. - Only topics with the suffix
/availability
are considered for status tracking. - Devices with names starting with
0x
(typically internal Zigbee IDs) are ignored.
Notifications with Pushover
After the health check has determined the status of all devices, the notification function comes into play. It analyzes which devices are offline and sends a corresponding message via Pushover.
// Assumption: msg.payload is an object with device names as keys and status as values let devices = msg.payload; let offlineDevices = [];
// Iterate over all devices and filter those that are offline or undefined
for (let device in devices) {
if (devices.hasOwnProperty(device)) {
let status = devices[device];
text
if (status === undefined) {
// Device has no defined status
offlineDevices.push(`${device} (Status: undefined)`);
} else if (typeof status === 'string' && status.toLowerCase() === "offline") {
// Device is offline
offlineDevices.push(device);
}
}
}
let msgToSend = null;
if (offlineDevices.length > 0) {
// There is at least one device that is offline or has no defined status
let message = "The following devices are offline or have no defined status:\n" + offlineDevices.join("\n");
msgToSend = { payload: message };
} else {
// All devices are online
let message = "All devices are online";
msgToSend = { payload: message };
}
// Send the corresponding message
return msgToSend;
This function:
- Analyzes the status of all known devices
- Identifies devices that are offline or have an undefined status
- Creates a formatted message with a list of problematic devices
- If all devices are online, a positive confirmation message is sent
The message is then forwarded to a Pushover node, which sends it as a push notification to your smartphone. This way, you are immediately informed when a device is no longer reachable.

Setting up and customizing the flow
To implement the health check in your own Node-RED, you need to make some adjustments:
MQTT Configuration
In the example flow, two MQTT sources are monitored: zigbee2mqtt_1/#
and zigbee2mqtt_2/#
. You need to adapt these to your Zigbee2MQTT installation. If you only use one instance, you can remove one of the MQTT nodes.
Pushover Setup
For notifications, you need a Pushover account and must configure the corresponding node in Node-RED. Enter your API keys and customize the notification title.
Schedule Adjustment
The flow is configured to perform a status check every day at 8 PM. You can adjust this time in the settings of the inject node or add more times. For regular checks multiple times a day, it may be useful to use multiple inject nodes with different schedules.
Log File Path
In the example, the log file is saved under /data/zigbee2mqtt.log
. Adjust this path according to your Node-RED installation and make sure that the user under which Node-RED runs has write permissions for this directory.
The complete Node-RED flow
Here is the complete Node-RED flow to import. Copy the following code and import it into your Node-RED instance via the menu (Import → Clipboard).
[{"id":"b54a32a9e33f104d","type":"inject","z":"f48cf010.9333b","name":"Trigger 20 Uhr","props":[{"p":"payload.trigger","v":"true","vt":"bool"}],"repeat":"","crontab":"00 20 * * *","once":false,"onceDelay":0.1,"topic":"","x":800,"y":1020,"wires":[["db54f60627a316cf"]]},{"id":"309cd5f00343ef33","type":"mqtt in","z":"f48cf010.9333b","name":"","topic":"zigbee2mqtt_1/#","qos":"2","datatype":"auto-detect","broker":"a898461e.979668","nl":false,"rap":true,"rh":0,"inputs":0,"x":780,"y":800,"wires":[["ada861d5855e07f1","db54f60627a316cf"]]},{"id":"39880180350e465a","type":"mqtt in","z":"f48cf010.9333b","name":"","topic":"zigbee2mqtt_2/#","qos":"2","datatype":"auto-detect","broker":"a898461e.979668","nl":false,"rap":true,"rh":0,"inputs":0,"x":780,"y":840,"wires":[["ada861d5855e07f1","db54f60627a316cf"]]},{"id":"ada861d5855e07f1","type":"function","z":"f48cf010.9333b","name":"logs","func":"// Create an object that contains both the topic and the payload\nlet logEntry = {\n timestamp: new Date().toISOString(), // Optional: adds a timestamp\n topic: msg.topic,\n payload: msg.payload\n};\n\n// Convert the object to a JSON string and add a line break\nmsg.payload = JSON.stringify(logEntry) + \"\\n\";\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":840,"wires":[["d4bea2321993faba"]]},{"id":"d4bea2321993faba","type":"file","z":"f48cf010.9333b","name":"zigbee2mqtt log","filename":"/data/zigbee2mqtt.log","filenameType":"str","appendNewline":true,"createDir":false,"overwriteFile":"false","encoding":"none","x":1160,"y":840,"wires":[[]]},{"id":"db54f60627a316cf","type":"function","z":"f48cf010.9333b","name":"Zigbee2MQTT Device Health Check","func":"// Initialize the device object in the flow context if it doesn't exist yet\nlet devices = flow.get('zigbee2mqtt_devices') || {};\n\n// Function to extract the device name from the topic\nfunction extractDeviceName(topic) {\n // Assumption: Topic format is \"zigbee2mqtt_X/DeviceName/availability\"\n let parts = topic.split('/');\n return parts;\n}\n\n// Function to check if the topic has one of the allowed prefixes\nfunction hasValidPrefix(topic) {\n const prefixes = [\"zigbee2mqtt_1/\", \"zigbee2mqtt_2/\"];\n return prefixes.some(prefix => topic.startsWith(prefix));\n}\n\n// Check if the message is an availability message\nif (msg.topic && hasValidPrefix(msg.topic) && msg.topic.endsWith(\"/availability\")) {\n // This is an availability message\n let deviceName = extractDeviceName(msg.topic);\n\n // **New filter: Ignore devices whose names start with '0x'**\n if (deviceName.startsWith(\"0x\")) {\n // Ignore device, no further processing\n return null;\n }\n\n let state = msg.payload.state; // Assumption: payload is an object with 'state'\n\n // Update the device object\n devices[deviceName] = state;\n\n // Save the updated object back to the flow context\n flow.set('zigbee2mqtt_devices', devices);\n\n // Don't forward any message\n return null;\n} else {\n // Assumption: This is a trigger from an inject node\n // Check if msg.payload.trigger is set to true\n if (msg.payload && msg.payload.trigger === true) {\n // Create a new message with the current device state\n let newMsg = { payload: devices };\n\n // Send the new message\n return newMsg;\n } else {\n // If trigger is not true, do nothing more\n return null;\n }\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1090,"y":1020,"wires":[["57fb9007e3bf4b6d","0fbe7d51e75713d2"]]},{"id":"0fbe7d51e75713d2","type":"function","z":"f48cf010.9333b","name":"Pushover","func":"// Assumption: msg.payload is an object with device names as keys and status as values\nlet devices = msg.payload;\nlet offlineDevices = [];\n\n// Iterate over all devices and filter those that are offline or undefined\nfor (let device in devices) {\n if (devices.hasOwnProperty(device)) {\n let status = devices[device];\n \n if (status === undefined) {\n // Device has no defined status\n offlineDevices.push(`${device} (Status: undefined)`);\n } else if (typeof status === 'string' && status.toLowerCase() === \"offline\") {\n // Device is offline\n offlineDevices.push(device);\n }\n }\n}\n\nlet msgToSend = null;\n\nif (offlineDevices.length > 0) {\n // There is at least one device that is offline or has no defined status\n let message = \"The following devices are offline or have no defined status:\\n\" + offlineDevices.join(\"\\n\");\n msgToSend = { payload: message };\n} else {\n // All devices are online\n let message = \"All devices are online\";\n msgToSend = { payload: message };\n}\n\n// Send the corresponding message\nreturn msgToSend;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1340,"y":1020,"wires":[["04ccd4c344465c6e","dbd1c76a73ae9e1e"]]},{"id":"57fb9007e3bf4b6d","type":"debug","z":"f48cf010.9333b","name":"Debug Output","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1360,"y":980,"wires":[]},{"id":"dbd1c76a73ae9e1e","type":"pushover api","z":"f48cf010.9333b","keys":"c1da56b8.1433f8","title":"zigbee2mqtt #1 + #2 - Geräte Healthcheck","name":"","x":1500,"y":1020,"wires":[]},{"id":"04ccd4c344465c6e","type":"debug","z":"f48cf010.9333b","name":"debug 18","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1500,"y":1060,"wires":[]},{"id":"a898461e.979668","type":"mqtt-broker","name":"mqtt-server (fhem-main-ext - docker)","broker":"172.18.0.6","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"5","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"c1da56b8.1433f8","type":"pushover-keys","name":"Reinhards Pushover"}]
Note: translated from the German version of the article. Therefore some screenshots are in German.
Add comment