Using Node Red with AD
Posted: Friday 10 October 2025 15:36
I use quite a number of Node Red based integration because of the async nature using MQTT messages between node-red and domoticz.
The beauty of mqtt-ad is that you can build many different interfaces and give each their own HW in domoticz. Very flexible. easy to change/adapt and all async so no devices will lock domotics causing the dreaded unresponsiveness messages.
It does require a bit work to define every device but, once done, it's a simple thing to operate. The only downside of node red is that it's not easy to do versioning on and node red is susceptible to installation/changes that may render it unfunctional (crash, lock). Make sure you have a decent backup before making substantial changes. I learnt the hard way
.
So now for the integration, I will use an example that I have built (well rigged) for Tado but the principle is the same for any other node red library:
Starup function:
1) you define the devices for which autodiscovery messages need to be sent to domoticz, all in one array
2) every type of device has its own function to create these messages
3) for all array items, messages are created and sent to a domoticz mqtt interface where these messages are sent as persistent
here's the code that goes with that:
As you can see, the discovery topic is tado_dev (so this is what you configure in the mqtt-ad hardware in domoticz) and updates for all devices will be received/sent on the tado topic. Create the HW in domoticz and it will immediately see the devices.
All the code above gets put in a function node in node red (in my case called "startup function") and triggered on start (so when when the flow in node red starts, it sends the device discovery messages).
Next is the handling of updates. For the tado example there is an update side where at specific intervals, devices are pinged from node red (setpoint, temperature, home/away, ..) and updates sent to domoticz. An example of reporting the setpoint state would be (function called Report setpoint state):
In order to not flood domoticz with constant updates, every response from Tado is checked against the last value that was sent to domoticz and only sent in case of a changed value (or at least, that's the idea). Hence the "context.flow" variables for global storage that keeps these states. You can see all of these that I keep for Tado mentioned in the first function above.
Nowe we are sending updates but we still need to receive messages from domoticz (like change setpoint). That gets done in a mqtt node that listens on the topic tado/set/wk_tado_setpoint which is what the autodiscovery message tells domoticz to send updates on. This node triggers a function that does tha Tado api call and then reports the result back to domoticz (without confirmation, domoticz does not assume it happens and will not change the state so don't forget to report every change back).
Function save setpoint:
Essentially, that's all there is: define devices, create autodiscovery messages, send these to domoticz, listen on topics to action commands and report back and send updates.
My plan is to build a wiki page for it where the flow itself is stored so stay tuned.
The beauty of mqtt-ad is that you can build many different interfaces and give each their own HW in domoticz. Very flexible. easy to change/adapt and all async so no devices will lock domotics causing the dreaded unresponsiveness messages.
It does require a bit work to define every device but, once done, it's a simple thing to operate. The only downside of node red is that it's not easy to do versioning on and node red is susceptible to installation/changes that may render it unfunctional (crash, lock). Make sure you have a decent backup before making substantial changes. I learnt the hard way
So now for the integration, I will use an example that I have built (well rigged) for Tado but the principle is the same for any other node red library:
Starup function:
1) you define the devices for which autodiscovery messages need to be sent to domoticz, all in one array
2) every type of device has its own function to create these messages
3) for all array items, messages are created and sent to a domoticz mqtt interface where these messages are sent as persistent
here's the code that goes with that:
Code: Select all
let discovery_topic = "tado_devs";
let root_topic = "tado";
context.flow.tadosetpoint = 15; // Thermostat setpoint
context.flow.presence="Home";
context.flow.tadoonoff="On";
context.flow.tadohum=50;
context.flow.tadotemp = 21;
let pobject = [
{ "WK Thermostaat Setpoint": ["climate", "wk_tado_setpoint", "0x9990000001", "C", "14|30", 1] },
{ "WK Thermostaat Presence": ["select", "wk_tado_presence", "0x9990000002", "Home|Away|Auto", "", 0] },
{ "WK Thermostaat State": ["switch", "wk_tado_onoff", "0x9990000003", "On", "Off", 0] },
{ "WK Thermostaat Resume Schedule": ["switch", "wk_tado_resume", "0x9990000004", 0] },
{ "WK Thermostaat Temp": ["sensor", "wk_tado_temp", "0x9990000005", "\u00b0c", "", 0] },
{ "WK Thermostaat Hum": ["sensor", "wk_tado_hum", "0x9990000006", "%", "", 0] },
];
function createBinaryObject(root, name, varname, unique_id, payload_on = "ON", payload_off = "OFF")
{
let device_identifier = unique_id;
if (device_identifier.indexOf("_") != -1)
{
device_identifier = unique_id.split("_")[0];
}
let vjson = {
"stat_t": "~/" + varname + "/state",
"dev": {
"manufacturer": "PA1DVB",
"ids": []
}
};
vjson.dev.ids.push(device_identifier);
vjson["~"] = root;
vjson["name"] = name;
vjson["payload_on"] = payload_on;
vjson["payload_off"] = payload_off;
vjson["uniq_id"] = unique_id;
vjson["val_tpl"] = "{{ value_json." + varname + " }}";
return vjson;
}
function createButtonObject(root, name, varname, unique_id, payload_on = "ON")
{
let device_identifier = unique_id;
if (device_identifier.indexOf("_") != -1)
{
device_identifier = unique_id.split("_")[0];
}
let vjson = {
"stat_t": "~/" + varname + "/state",
"dev": {
"manufacturer": "PA1DVB",
"ids": []
}
};
vjson.dev.ids.push(device_identifier);
vjson["~"] = root;
vjson["name"] = name;
vjson["payload_on"] = payload_on;
vjson["cmd_t"] = "~/set/" + varname;
vjson["uniq_id"] = unique_id;
vjson["pl_prs"] = "On";
vjson["val_tpl"] = "{{ value_json." + varname + " }}";
return vjson;
}
function createLightObject(root, name, varname, unique_id, payload_on = "ON", payload_off = "OFF")
{
let device_identifier = unique_id;
if (device_identifier.indexOf("_") != -1)
{
device_identifier = unique_id.split("_")[0];
}
let vjson = {
"stat_t": "~/" + varname + "/state",
"dev": {
"manufacturer": "PA1DVB",
"ids": []
}
};
vjson.dev.ids.push(device_identifier);
vjson["~"] = root;
vjson["name"] = name;
vjson["payload_on"] = payload_on;
vjson["payload_off"] = payload_off;
vjson["cmd_t"] = "~/set/" + varname;
vjson["uniq_id"] = unique_id;
vjson["val_tpl"] = "{{ value_json." + varname + " }}";
return vjson;
}
function createSelectObject(root, name, varname, unique_id, select_options)
{
let device_identifier = unique_id;
if (device_identifier.indexOf("_") != -1)
{
device_identifier = unique_id.split("_")[0];
}
let vjson = {
"stat_t": "~/" + varname + "/state",
"dev": {
"manufacturer": "PA1DVB",
"ids": []
}
};
vjson.dev.ids.push(device_identifier);
vjson["~"] = root;
vjson["name"] = name;
vjson["options"] = [];
select_options.split("|").forEach(function (item) {
vjson["options"].push(item);
});
vjson["cmd_t"] = "~/set/" + varname;
vjson["uniq_id"] = unique_id;
vjson["val_tpl"] = "{{ value_json." + varname + " }}";
return vjson;
}
function createNumberObject(root, name, varname, unique_id, step = 1)
{
let device_identifier = unique_id;
if (device_identifier.indexOf("_") != -1)
{
device_identifier = unique_id.split("_")[0];
}
let vjson = {
"stat_t": "~/" + varname + "/state",
"dev": {
"manufacturer": "PA1DVB",
"ids": []
}
};
vjson.dev.ids.push(device_identifier);
vjson["~"] = root;
vjson["name"] = name;
vjson["step"] = step;
vjson["cmd_t"] = "~/set/" + varname;
vjson["uniq_id"] = unique_id;
vjson["val_tpl"] = "{{ value_json." + varname + " }}";
return vjson;
}
function createSensorObject(root, name, varname, unique_id, unit_of_measurement)
{
let device_identifier = unique_id;
if (device_identifier.indexOf("_") != -1)
{
device_identifier = unique_id.split("_")[0];
}
let vjson = {
"stat_t": "~/" + varname + "/state",
"dev": {
"manufacturer": "PA1DVB",
"ids": []
}
};
vjson.dev.ids.push(device_identifier);
vjson["~"] = root;
vjson["name"] = name;
vjson["uniq_id"] = unique_id;
vjson["unit_of_meas"] = unit_of_measurement;
vjson["val_tpl"] = "{{ value_json." + varname + " }}";
return vjson
}
function createClimateObject(root, name, varname, unique_id, unit_of_measurement, min_value = 6.5, max_value = 35.0, step_size = 1.0)
{
let device_identifier = unique_id;
if (device_identifier.indexOf("_") != -1)
{
device_identifier = unique_id.split("_")[0];
}
let vjson = {
"temp_stat_t": "~/" + varname + "/state",
"dev": {
"manufacturer": "PA1DVB",
"ids": []
}
};
vjson.dev.ids.push(device_identifier);
vjson["~"] = root;
vjson["name"] = name;
vjson["uniq_id"] = unique_id;
vjson["temp_unit"] = unit_of_measurement;
vjson["temp_cmd_t"] = "~/set/" + varname;
vjson["temp_stat_tpl"] = "{{ value_json." + varname + " }}";
vjson["temp_step"] = step_size;
vjson["min_temp"] = min_value;
vjson["max_temp"] = max_value;
return vjson
}
node.log("Flow Initialisation");
pobject.forEach((vvar) => {
let name = Object.keys(vvar)[0];
let vobj = vvar[Object.keys(vvar)];
let object_type = vobj[0]
let varname = vobj[1]
let unique_id = vobj[2]
let jobj = null
if (object_type == "light") {
let payload_on = vobj[3]
let payload_off = vobj[4]
jobj = createLightObject(root_topic, name, varname, unique_id, payload_on, payload_off);
}
else if(object_type == "button") {
let payload_on = vobj[3]
jobj = createButtonObject(root_topic, name, varname, unique_id, payload_on);
}
else if (object_type == "switch") {
let payload_on = vobj[3]
let payload_off = vobj[4]
jobj = createLightObject(root_topic, name, varname, unique_id, payload_on, payload_off);
}
else if (object_type == "binary_sensor") {
let payload_on = vobj[3]
let payload_off = vobj[4]
jobj = createBinaryObject(root_topic, name, varname, unique_id, payload_on, payload_off);
}
else if (object_type == "select") {
let soptions = vobj[3]
jobj = createSelectObject(root_topic, name, varname, unique_id, soptions);
}
else if (object_type == "number") {
let step = vobj[5]
jobj = createNumberObject(root_topic, name, varname, unique_id, step);
}
else if (object_type == "sensor") {
let unit_of_measurement = vobj[3]
jobj = createSensorObject(root_topic, name, varname, unique_id, unit_of_measurement);
}
else if (object_type == "climate") {
let unit_of_measurement = vobj[3]
let minmax = vobj[4];
let step_size = vobj[5]
let min_value = 6.5;
let max_value = 35;
let sresults = minmax.split("|");
if (sresults.length == 2) {
min_value = parseFloat(sresults[0]);
max_value = parseFloat(sresults[1]);
}
jobj = createClimateObject(root_topic, name, varname, unique_id, unit_of_measurement, min_value, max_value, step_size);
}
if (jobj != null) {
if (unique_id.indexOf("_") != -1)
{
unique_id = unique_id.split("_")[0];
}
let dtopic = discovery_topic + "/" + object_type + "/" + unique_id + "/" + varname + "/config"
//node.log("publishing config ->: " + dtopic);
msg.payload = jobj;
msg.topic = dtopic;
msg.retain = true;
node.send(msg);
}
});
return null;
All the code above gets put in a function node in node red (in my case called "startup function") and triggered on start (so when when the flow in node red starts, it sends the device discovery messages).
Next is the handling of updates. For the tado example there is an update side where at specific intervals, devices are pinged from node red (setpoint, temperature, home/away, ..) and updates sent to domoticz. An example of reporting the setpoint state would be (function called Report setpoint state):
Code: Select all
msg.topic="tado/wk_tado_setpoint/state";
msg.payload = context.flow.tadosetpoint;
return msg;Nowe we are sending updates but we still need to receive messages from domoticz (like change setpoint). That gets done in a mqtt node that listens on the topic tado/set/wk_tado_setpoint which is what the autodiscovery message tells domoticz to send updates on. This node triggers a function that does tha Tado api call and then reports the result back to domoticz (without confirmation, domoticz does not assume it happens and will not change the state so don't forget to report every change back).
Function save setpoint:
Code: Select all
context.flow.tadosetpoint=msg.payload;
let setpoint=msg.payload;
//return msg;
return {
"apiCall": "manualControl",
"home_id": 1906297,
"room_id": 2,
"power": "ON",
"termination": "MANUAL",
"temperature": setpoint
}My plan is to build a wiki page for it where the flow itself is stored so stay tuned.