Node-RED – one controller for all relays
In my Node-RED project I have many flows. For example the Bluetooth, Zigbee, and WiFi devices are organised across multiple flows, depending on their function as sensors, devices like switches or actuators like lamps. Additionally, I devote separate flows to facilitate communication through email, Telegram and various APIs.
Initially, I utilized links in and links out to establish connections between nodes of different flows. However, as the complexity of the system grew, the extensive network of links began to introduce clutter and a heavy reliance on intricate link configurations.
To address this challenge, I redesigned the setup: A singular controller now serves as a centralized hub for all messages to actuators. In this article, I will explain this architecture with a singular controller and how it enhanced the manageability of the entire Node-RED ecosystem.
Abstraction Layer for actuators
An actuator serves the purpose of executing actions. In my scenario, actuators primarily encompass lamps, spots, and power plugs. However, it’s worth noting that actuators can extend to devices like motors or solenoids as well. These actuators employ diverse communication techniques, such as Tasmota and Zigbee. While each technique may employ distinct message structures, both can be effectively communicated via MQTT.
I’ve developed a subflow that serves as the Abstraction Layor for all types of actuators. I designed it to accept configurations and manage underlying variations discreetly, shielded from the user’s perspective. The configuration primarily encompasses the communication technology and the device’s name. Furthermore, there’s an option to configure a subdevice, as certain instances involve one device governing multiple lamps or plugs. Additionally, a common label can be appended when utilizing Node-RED and logging systems like InfluxDB, ensuring a user-friendly name is employed.
The capabilities of this subflow extend beyond merely interfacing with Zigbee and Tasmota protocols. They also include functions like state preservation, temporary state configuration, logging actions in a database, as well as returning and logging the device’s current state.
The advantage of having this abstraction layer is that the central controller can ignore all underlying differences.
Singular controller for actuators
I standardized the device names within Node-RED. For Zigbee devices, I could leverage their existing technical names, which I had previously standardized. As for Tasmota devices, I underwent a renaming process. The technical names of Tasmota devices involve a segment of their MAC addresses. However, I opted for user-friendlier names, incorporating the last IP address for better recall.
Moreover, I aimed to facilitate addressing multiple devices simultaneously. Thus, I determined that the controller would be designed to accommodate multiple device names. These names can either be concatenated as strings or included within an array.
The first step of the controller iterates all devices, which is the topic attribute.
if (!msg.hasOwnProperty("topic")) return null;
const sendMessages = (topics) => {
topics.forEach((mytopic) => {
if (typeof mytopic === 'string') {
const newMsg = { ...msg, topic: mytopic };
node.send(newMsg);
}
});
};
if (typeof msg.topic === 'string') {
const topics = msg.topic.split(";");
sendMessages(topics);
}
if (Array.isArray(msg.topic)) {
sendMessages(msg.topic);
}
return null;
All device names conform to a consistent format, comprising a base device name accompanied by a numerical identifier. The second step of the controller serves a practical purpose, involving the segmentation of streams into distinct groups based on the base device name.
// List of topics that are checked for message matching
const topics = ["power", "spot", "lamp", "switch", "tasmota"];
// 1 more output than topics count: the last output is connected to a debug node for exception handling
const exceptionMsgIndex = topics.length;
// Determine the index of the selected topic, or use the exception index
const selectedMsgIndex = topics.findIndex(mytopic => msg.topic.startsWith(mytopic));
const finalMsgIndex = selectedMsgIndex !== -1 ? selectedMsgIndex : exceptionMsgIndex;
// Create an array with null values and replace the chosen index with the message
const msgs = Array.from({ length: topics.length + 1 }, (_, index) => (index === finalMsgIndex ? msg : null));
node.status({ text: msg.topic });
return msgs;
The third name splits the stream based on the number of the device.
const topicPrefix = "tasmota";
const ids = [168, 1691, 1692, 170, 172, 174, 206, 207, 208, 209, 216];
// 1 more output than ids count: the last output is connected to a debug node for exception handling
const exceptionMsgIndex = ids.length;
const id = parseInt(msg.topic.slice(topicPrefix.length));
const selectedMsgIndex = ids.indexOf(id);
const finalMsgIndex = selectedMsgIndex !== -1 ? selectedMsgIndex : exceptionMsgIndex;
// Create an array with null values and replace the chosen index with the message
const msgs = Array.from({ length: ids.length + 1 }, (_, index) => (index === finalMsgIndex ? msg : null));
node.status({ text: msg.topic });
return msgs;
That is all. Now all messages can be sent to the controller using just one ‘link in’, and based on the msg.topic the right actuators receive the message for processing.