all articles

APEX #4 — MQTT, Node-RED, REST API: the controller that watched the fish go by

21 October 2026 · 5 min

#distech-controls,#eclypse-apex,#node-red,#bacnet,#api-rest,#mqtt,#zigbee,#serie-apex

ECLYPSE APEX series — episode 4/6 · Previous: #3 Zigbee2MQTT

Here we are. Your data is happily landing in the MQTT broker. Meanwhile, the controller watches it all go by like a cat in front of an aquarium. It sees the fish. It can’t catch them. Beautiful, in a pathetic sort of way.

A robot cat in front of an MQTT aquarium full of swimming data fish, next to an ECLYPSE APEX on its DIN rail

Remember episode 1: the MQTT client built into ECLYPSE controllers can only publish. No subscribing. An MQTT client that doesn’t listen is a switchboard operator with no ears. So we work around it, with Node-RED and the APEX REST API.

Node-RED logo

This is not a tutorial

Let’s be clear right away: no “copy-paste and it works on your side” here. I’m telling you what I built, what held up on my test bench with a SONOFF ZBMINIR2, and what cost me my evenings. Your source, your point names, your instance ranges, the way you organise things: adapting them is up to you. The leads are here, the rest is your job.

And while we’re at it: Zigbee is just an example. What matters is the mechanism, and it fits in one line:

MQTT in → Node-RED in the middle → APEX REST API → very real BACnet objects at the end.

Zigbee2MQTT is simply what I had at hand. Swap it for a LoRaWAN sensor, an ESP32 hacked together in the garage, or anything that can publish over MQTT. Only the “discovery” part changes.

Credit where it’s due, and go check his work

Before going further, a mandatory detour: Sylvain Montagny has already done a big chunk of the work on the LoRaWAN side. His project LoRaBAC is an open source (MIT licence) LoRaWAN ↔ BACnet bridge built on Node-RED. It works both ways (uplink and downlink), with ChirpStack, Actility or The Things Stack, and it can talk native BACnet as well as the Distech REST API.

We worked on neighbouring topics at almost the same time, each in our own corner. He describes each device type in a JSON file and openly says it isn’t plug and play, it’s written in his README. I started from the other end: creating the points automatically from what the device announces. Two approaches, two philosophies. I’m not going to rewrite his docs, they’re better than my summary: go read them.

The base brick: Node-RED in a container

Here is the JSON the OPA agreed to swallow.

{
  "Image": "nodered/node-red:4.0",
  "ExposedPorts": { "1880/tcp": {} },
  "HostConfig": {
    "CpusetCpus": "0",
    "RestartPolicy": { "Name": "unless-stopped", "MaximumRetryCount": 0 },
    "PortBindings": { "1880/tcp": [{ "HostPort": "50080" }] },
    "Mounts": [
      { "Target": "/data", "Source": "nodered-data", "Type": "volume", "ReadOnly": false, "Consistency": "default" }
    ]
  }
}

The host port must be 50080. I tried 50880, the OPA said no without a word of explanation, as usual. The /data volume keeps your flows between restarts. UI on http://IP_APEX:50080, and protect the editor with adminAuth in settings.js as soon as you leave the test bench, unless you enjoy surprises.

The REST API v2: the lever

A Node-RED flow connected through REST API v2 to the ECLYPSE APEX, which creates BACnet AV and BV objects

All the magic rests on a few APEX REST API v2 routes, which manipulate the controller’s local BACnet objects:

ActionRequest
List AVs or BVsGET /api/rest/v2/services/bacnet/local/objects/analog-values (or binary-values)
Create an objectPOST /api/rest/v2/services/bacnet/local/objects/add
Write a value, a descriptionPOST /api/rest/v2/services/bacnet/local/objects/analog-values/{instance}
Read an objectGET /api/rest/v2/services/bacnet/local/objects/binary-values/{instance}

Basic authentication with a Facilities account, over HTTPS. The certificate is self-signed, so we turn off verification on the Node-RED side. On a network you control, obviously. Not on the Internet, okay?

Creating an object is as simple as this:

POST /api/rest/v2/services/bacnet/local/objects/add
{ "object-type": "AnalogValue", "instance-number": 3001, "name": "ZB_Salon_temperature" }

And this is what changes everything: these are real local objects of the controller, not Network Values. They show up in the GFX program and are exposed over BACnet like any other point. No hack, no wobbly bridge: real points.

Our flow, with Zigbee2MQTT as the guinea pig

Discovery

Zigbee2MQTT continuously publishes the list of devices on zigbee2mqtt/bridge/devices, with each device’s “exposes”: what it can measure, what it can be commanded to do. The flow reads this list and derives the points to create:

Zigbee2MQTT sideBACnet side
Binary attribute (on/off)BV
Numeric attribute (temperature, power…)AV
Enumerated attributeAV, value 1..n, the mapping is written in the description
Readable attributeState object ZB_{name}_{attribute}
Writable attributeObject ZB_{name}_{attribute}_Cmd
Device availabilityBV ZB_{name}_Online

We only keep the useful categories (main and diagnostic, not configuration), with exceptions by model or by name. Another source? Only this part needs rewriting.

Creation, one point at a time

BACnet objects numbered 3000, 3001, 3002 go one by one through a gate guarded by a robot dog before entering the ECLYPSE APEX, under the EC-gfxProgram screen

Objects are created in an instance range reserved for the flow: 3000 to 3999, on the first free instance of each type. That keeps us from stepping on the program’s own points. Another source, pick another range.

Two safeguards that spared me a lot of grief:

  • a real queue that creates one point at a time, with a watchdog. Otherwise two simultaneous creations fight over the same instance, and nobody wins;
  • if an object with the same name already exists in the APEX, it is found, not recreated. You can clear the cache and rerun the scan without duplicating everything.

Values, availability, commands

  • Each zigbee2mqtt/<name> message updates the present-value of the state objects, on change, with a forced refresh every 10 minutes.
  • zigbee2mqtt/<name>/availability feeds the _Online BV and sets the objects to out-of-service when the device disappears. An unplugged module is visible immediately in supervision.
  • Writes are capped at 10 requests per second, so as not to flatten the controller.
  • In the other direction, the GFX program writes to the _Cmd objects. Node-RED reads them back every 2 seconds and, on change, publishes to zigbee2mqtt/<name>/set. The first read only stores the state: otherwise, when Node-RED restarts, every relay in the building slams in unison. Fun times.

The result

With a SONOFF ZBMINIR2, within seconds I saw the relay state, its command, its availability and its diagnostic info appear in the APEX. All of it in the GFX program, ready to use. I won’t lie to you, it was a moment.

The complete flow is here: flow-zigbee2mqtt-apex.json. Import it into Node-RED, then adapt it: APEX address, Facilities account, instance range, exceptions, MQTT broker. I’m not holding your hand, you’re grown-ups.

The traps

  • No / in Zigbee2MQTT friendly names.
  • Renaming a device in Zigbee2MQTT creates new objects; the old ones have to be deleted by hand.
  • Don’t run an old manual flow in parallel on the same devices.
  • The principle holds for any MQTT source: only discovery changes. For LoRaWAN, you start from the ChirpStack uplinks and not from bridge/devices.

What’s next

Let’s recap. A BMS controller running an MQTT broker, and a Node-RED that turns everything flowing through it into BACnet points with no human intervention. All on a DIN rail.

And now that you have the recipe, you’ll see that it repeats. In the next episode, we set up a complete LoRaWAN server with ChirpStack… and you’re going to get a strong feeling of déjà vu.

Links

$ gtb react

Did this article resonate?

$ gtb comments --tail

Comments

// loading…

open/gtb · MIT · hosted in France
visitors github roadmap messages contact
SYS OK 15/18 tools online uptime — Rack temp 21.3 °C