Page 1 of 1

Using Node Red with AD

Posted: Friday 10 October 2025 15:36
by heggink
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:

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;
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):

Code: Select all

msg.topic="tado/wk_tado_setpoint/state";
msg.payload = context.flow.tadosetpoint;
return msg;
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:

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
}
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.

Re: Using Node Red with AD

Posted: Friday 10 October 2025 16:40
by FireWizard
Hi @heggink,

Nice work.

You know that HA can use Node-RED as add-on, so some people has already created some integrations.
E.g. see: https://flows.nodered.org/node/node-red-contrib-ha-mqtt

This is may help you

Regards

Re: Using Node Red with AD

Posted: Saturday 11 October 2025 15:18
by waltervl
My plan is to build a wiki page for it where the flow itself is stored so stay tuned.
Better not document this on the wiki but on a GitHub repository or on a show your projects forum topic.

Especially when versioning is important and people need to download stuff you better use GitHub.

Re: Using Node Red with AD

Posted: Sunday 12 October 2025 8:16
by heggink
The plan was to explain how to effectively integrate between node red and domoticz, not to dump code with versions. Providing an example should suffice.

Re: Using Node Red with AD

Posted: Sunday 12 October 2025 9:59
by waltervl
heggink wrote: Sunday 12 October 2025 8:16 The plan was to explain how to effectively integrate between node red and domoticz, not to dump code with versions. Providing an example should suffice.
There is already a wiki page for this: https://wiki.domoticz.com/Node_Red_and_Flows

Re: Using Node Red with AD

Posted: Sunday 12 October 2025 10:07
by heggink
That page is not specific to AD integration.

Re: Using Node Red with AD

Posted: Sunday 12 October 2025 11:34
by waltervl
Good luck with documenting the MQTT AD interface of Domoticz ...
If you have MQTT devices that is not supporting MQTT AD yet you perhaps better look at the MQTT mapper plugin that also creates the Domoticz device for you.