DEV Community

manthink
manthink

Posted on

Connecting ThinkLink to Home Assistant and Node-RED: One MQTT Forwarder Does the Whole Job

A LoRaWAN project reaches a familiar point: the data is already flowing into ThinkLink, but the interface the customer actually wants to look at is somewhere else — a Home Assistant dashboard, or a flow they have wired up in Node-RED.

That is a reasonable request. Home Assistant dashboards and automation rules are genuinely good, and Node-RED is genuinely handy for stitching systems together. But one fact has to be settled first: neither platform speaks LoRaWAN.

Home Assistant supports MQTT, Zigbee, Z-Wave and HTTP. LoRaWAN is not on that list. It cannot talk to a gateway and it does not parse LoRaWAN frames. Node-RED is in the same position — it is a flow engine, and what it consumes depends entirely on what input node you give it.

So something has to sit in the middle and turn radio frames into structured data. That is what ThinkLink does: the gateway receives the frame, the network server decrypts it, the thing model decoder turns bytes into named fields, and only then does the data become "temperature 24.6" — something an application can actually use.

Which leaves exactly one question: how do you hand that parsed data to Home Assistant or Node-RED?


1. Start with a version change: the built-in HA integration is gone

This belongs at the top, because it determines the whole approach.

Older manuals describe configuring Home Assistant fields in the thing model, or entering an HA address under server configuration. The official 2.00.066 notes are explicit: that release removed the built-in ThingsBoard / Home Assistant integrations and their related fields, and the older HA / ThingsBoard instructions "apply only to older versions and must not be used as operating steps for 2.00.066." The same passage adds that BACnet / Modbus are retained.

From 2.00.066 onwards, there is no "type in an HA address and let the platform push entities" path. What replaced it is a general capability: the MQTT forwarder (Broker + Forwarder).

This is not a regression. A built-in integration hard-codes a single integration shape: the platform pushes in its own format and the other side has to adapt. With a forwarder, topic rewriting, payload restructuring and filtering all live in one JavaScript function, and the mechanism is identical no matter what you are connecting to. Home Assistant, Node-RED, ThingsBoard, a custom service: from ThinkLink's side the only difference is the contents of that script.

One more distinction worth making. Product comparison material mentions that ThinkLink edge deployments (TKG, gateway-embedded) come with Home Assistant OS and ThingsBoard CE as preloaded components. That is a separate HA instance running on the same hardware — it is not "ThinkLink pushing data into Home Assistant." This article is about the latter: getting data out of ThinkLink and into an HA or Node-RED instance you already run.


2. Three possible paths — pick the right one before you start

There is a choice to make first. The documentation describes three push mechanisms, and they are not equally current.

Path Mechanism Status Use it?
Device-side "third-party forward" switch + server-side ThirdParty config legacy direct push legacy config, deactivated no
Server configuration → ThirdParty MQTT one Broker address configured platform-side documentation conflict, see below confirm against your version
Advanced → Broker + Forwarder subscribe to an internal topic, forward to a target broker the current supported method yes

One documentation conflict has to be reported honestly. The MQTT forwarder page states plainly: there is no need to enable any "third-party forward" switch on the device, and no need to configure any third-party platform under server configuration — "both of those settings from older versions are deactivated." Meanwhile the server configuration page still documents a ThirdParty section (Protocol / Host / Port / Telemetry Topic / Username / Password) plus an instruction to enable a ThirdParty switch on the device detail page.

The two pages are describing different things, or at least different versions. The practical resolution is simple: go by what your platform version actually exposes. If the interface has "Advanced → Broker / Forwarder", use the forwarder. That is the main path on current versions and it is what the rest of this article covers.


3. The forwarder: two steps, five fields

The data flow looks like this:

ThinkLink internal broker (AS or ThinkLink)
↓ Source Broker / Subscription Topic
Forwarder rule (optional JavaScript transform)
↓ Target Broker
Third-party MQTT broker (Customize)

Step 1: create two broker connections

Go to Advanced → Broker. One forwarder rule needs one source connection and one target connection.

Source broker — two internal types, and the choice determines what shape of data you get:

  • ThinkLink: telemetry after thing-model decoding. Fields already have names and units.
  • AS: raw uplink after LoRaWAN decryption, before the thing model.

Pick ThinkLink for almost everything. There is only one good reason to choose AS: you want to do your own parsing downstream, or you need RF-side values like RSSI and SNR for network quality analysis. If the goal is "show a temperature in Home Assistant," choosing AS means redoing work the platform has already done.

Addresses and tenant credentials for the internal brokers are filled in by the platform; you do not need to fetch them from server configuration.

Target broker — add a Customize entry with the other side's details:

Name and enabled state
MQTT URL, e.g. mqtt://192.168.1.50:1883 or mqtts://host:8883
Username and password (when required)
CA certificate, client certificate and other TLS material (when required)

If Home Assistant and Node-RED run on the same host — the common case — the target is the Mosquitto instance on that host, or the Mosquitto broker add-on inside HA.

After saving and enabling, check the broker list state first: RUNNING is healthy; ERROR means read ErrMsg and work through URL, port, credentials, certificates, and network reachability.

Step 2: create the forwarder rule

Go to Advanced → Forwarder → Add:

Field Meaning
Name Rule name
Enabled The rule only subscribes and forwards when this is on
Source Broker Broker to subscribe to
Target Broker Broker to publish to
Subscription Topic Topic on the source broker; supports MQTT + and # wildcards
Custom Script Optional; rewrites topic, message body or retain flag
Remark / commit note Optional; documents the rule and version changes

One hard constraint: Source Broker and Target Broker may not both be internal types (AS / ThinkLink), to avoid a pointless internal loop. The normal third-party combination is:

Source Broker = AS or ThinkLink
Target Broker = Customize

How to fill in Subscription Topic

This is the field most often got wrong, because there is no default and the placeholder must be replaced.

Source Broker Data Subscription Topic
ThinkLink telemetry after thing-model decoding /v32/{tenant}/tkl/up/telemetry/#
AS raw uplink after LoRaWAN decryption /v32/{tenant}/as/up/data/#
AS complete uplink after multi-gateway collection /v32/{tenant}/as/up/dataAll/#

{tenant} must be replaced with your actual organisation account, visible in your profile — do not leave the placeholder in place. # covers every device in the organisation; to forward a single device, replace # with that device's DevEUI.

Concretely: organisation account mtfac, forwarding decoded telemetry for all devices, gives /v32/mtfac/tkl/up/telemetry/#.


4. The transform script: translating ThinkLink into HA's vocabulary

With no script configured, the forwarder passes topic and payload straight through. That is a good starting point: let messages flow unchanged, capture one real message in Home Assistant or Node-RED, look at the actual field structure, and only then decide whether you need a script. Scripts written from imagination rarely work the first time.

When you do need to reshape the payload, there is exactly one entry function:

function forwardScript({ topic, msg, org_params }) {
if (!msg?.telemetry_data) return null

return {
topic: ${org_params.target_topic_prefix}/${msg.device_id},
option: {
retain: false
},
msg: {
temperature: msg.telemetry_data.T,
humidity: msg.telemetry_data.H
}
}
}

Three inputs: topic is the original topic the message arrived on, msg is the parsed message body, and org_params is read-only organisation parameters.

The returned object supports three fields: topic (the topic to publish on the target broker), msg (the body to publish), and option (MQTT publish options such as { retain: true }). Returning null, or returning nothing, discards the message — which is the intended way to filter, for example forwarding only certain devices or only when a value crosses a threshold.

The official example uses msg.device_id and msg.telemetry_data; the keys under telemetry_data (shown as T and H) are whatever your thing model defines. Do not guess field names — capture a real message and confirm. Article 21 covered thing model decoders: the keys defined there are the keys available here.

org_params deserves a note of its own. It is an organisation-level set of environment variables, maintained as key/value pairs under System Management → Server Configuration → Org Params, shared across the whole tenant and injected automatically when the platform runs a script. The point is to move addresses, prefixes and business identifiers out of scripts — change environments or topic prefixes in one place instead of editing every script. Values take effect immediately, with no service restart, and only organisation administrators can see them.


5. Home Assistant: two ways in

Note: the Home Assistant configuration below is general Home Assistant knowledge, not ThinkLink official documentation. HA moves quickly — check field names against the version you run.

Once data reaches the broker HA uses, HA has to turn it into entities. Two routes.

First: hand-written MQTT sensors. Declare sensors in configuration.yaml, point state_topic at whatever topic the forwarder publishes to, and pull the value out of the JSON with value_template. This is the most robust option and you control every field; the cost is a block of config per measurement per device. Well suited to a small, fixed set of devices.

Second: emit MQTT discovery messages yourself. Home Assistant's MQTT integration supports discovery: publish a message carrying device metadata to the agreed discovery topic (prefix homeassistant/ by default) and HA creates the entity automatically. ThinkLink does not emit that format natively, but you can build it inside the forwarder script — both topic and msg in the return value are yours to define, so the first uplink can trigger a discovery message.

That route wins when devices are numerous or change often: adding one requires no HA config edits. The cost is script complexity and one round of debugging.

One structural point worth planning for: ThinkLink sends one message per device containing multiple fields, while an HA entity holds one value. Either split it on the HA side with value_template, or split it in the forwarder script and publish one topic per field. Deciding which layer does the splitting up front saves a lot of rework.

And a practical warning: an entity showing "unknown" or "unavailable" in the HA dashboard is often not an HA problem — the data never reached the broker. Subscribe from the command line and confirm the message actually arrives before touching HA configuration.


6. Node-RED: three nodes and you are running

Node-RED is more straightforward than HA, because it imposes no entity model — it processes whatever arrives.

The minimal flow is three nodes: mqtt in (subscribe to the topic on the target broker) → json (parse the string into an object) → debug or a dashboard node.

The topic in mqtt in is whatever the forwarder published, i.e. the topic returned by forwardScript. If you left it pass-through with no script, that is the original /v32/mtfac/tkl/up/telemetry/{eui}.

Where Node-RED beats HA is the middle of the flow: you can drop in function nodes for arbitrary logic — unit conversion, thresholds, cross-device correlation, database writes, third-party API calls. For something like "when temperature and humidity exceed limits, check another sensor in the same zone and only alert if both are over," Node-RED reads more clearly than a platform-side trigger script and is more flexible than an HA automation.

Reverse control is equally a single mqtt out node — but read the next section first.


7. Writes: from HA / Node-RED back to the device

This is where misjudgement creeps in, and it is the same lesson as the BACnet write path in article 24.

From pressing a button in HA to the device actually acting, at least three things must happen:

  1. The command is accepted by the platform — immediate.
  2. The RPC reaches the device — on a Class A LoRaWAN device the downlink can only go out in the receive window after an uplink (RX1 ~1 s, RX2 ~2 s). With a ten-minute reporting interval, that command can sit for ten minutes before it lands.
  3. The device reports back — the result of the execution only shows up on the next uplink.

So clicking a button in HA and seeing nothing change does not mean the command failed. Acceptance criteria should read "read back and confirm within N reporting cycles after sending," not "click and look immediately."

One recommendation for integrators: if the project needs control latency measured in seconds, a Class A device is the wrong choice and Class C should be considered. That decision belongs at device selection time — it cannot be fixed during integration.


8. Online status: one switch you have to turn on yourself

Both HA and Node-RED need to know whether a sensor is still alive.

From 2.00.084, a forwarder with a ThinkLink source can subscribe to the device status topic:

/v32/{tenant}/tkl/device/status/{eui}

or, within an authorised organisation, /v32/{tenant}/tkl/device/status/+.

Two traps to know in advance:

First, status messages require notify_offline="TRUE" on the device, and that switch defaults to off. Leave it off and there are no status messages at all — no amount of forwarder configuration will produce them. Note that the forwarder itself needs no device-side "third-party forward" switch; whether status messages exist depends solely on notify_offline.

Second, this message has no telemetry_data. The example script above opens with if (!msg?.telemetry_data) return null, which discards status messages outright. To forward status, either leave the message pass-through or handle the status format explicitly in the script.

Also worth pinning down: "online" has three definitions on the platform and they are not interchangeable (article 22 covered this) — the activity time in the device list, server_attrs.online, and the boolean in MQTT notifications. Whichever one your integration uses should be written into the delivery document, so both sides do not hold different ideas of what "online" means.


9. After 2.00.070: do not just watch for RUNNING

Connection and subscription behaviour changed in 2.00.070, and it directly affects troubleshooting.

Registration is not subscription. When a rule is enabled, the subscription requirement is registered even if the source broker is offline, and the subscription is attempted once the connection recovers. But subscription is only confirmed on SUBACK. Failed subscriptions retry every 3 seconds while connected.

No buffering when the target is down. At startup, a rule waits for its own enabled target broker within the original connection timeout, then registers the source subscription anyway; when the target is unavailable it falls back to skipping the forward. There is no new message buffering and no offline redelivery guarantee. The forwarder is not a reliable queue — messages produced during an outage are not replayed afterwards.

No exactly-once. MQTT redelivery or duplicate sends at the source can produce duplicate messages; if the business needs deduplication, carry a message identifier.

In short: do not conclude that forwarding works from "rule enabled" or "broker connected" — verify the actual message flow.

Two more behaviours: forwarder brokers for the same organisation load in parallel, so one slow connection does not make the others queue. When several scripts share a topic, removing one does not affect the others; unsubscribe only happens once the last callback is removed.


10. Troubleshooting: work this order

The official five steps hold up well in practice:

  1. Confirm both source and target brokers are enabled and the target shows RUNNING.
  2. Check ErrMsg, host, port, credentials and TLS certificates.
  3. Confirm the forwarder itself is enabled (not merely saved).
  4. Confirm Subscription Topic matches the real message topic — especially whether {tenant} was substituted and whether a path level is missing or extra.
  5. Disable the custom script temporarily and confirm the raw message passes through; then debug the script's return value.

Step 5 is the one most often skipped and the one that most often finds the problem. The classic beginner error is a script returning null (because msg.telemetry_data was absent) without realising it — the symptom looks like "data isn't being forwarded," when in fact the script is throwing it away.

Two operational extras:

  • Every save of a forwarder script creates a version snapshot, viewable and restorable in the editor. Restoring creates a new version record and does not delete history — a broken script is recoverable.
  • The system platform maintains public forwarders: public rules are visible but read-only in the tenant; selecting a specific organisation lets a rule reference that organisation's brokers and org_params. Useful when multiple organisations share one forwarding pattern.

11. Acceptance checklist

# Item Pass criterion
1 Path Uses "Advanced → Broker + Forwarder"; does not rely on the deactivated legacy third-party switch
2 Source broker ThinkLink (decoded) or AS (raw) chosen deliberately, not by accident
3 Target broker Type Customize; state RUNNING; ErrMsg empty
4 Subscription topic {tenant} replaced with the real organisation account; path levels match the documented format
5 Rule enabled Forwarder "enabled" is on (not just saved); SUBACK received
6 Real message flow Verified by subscribing on the target broker, not inferred from status
7 Script behaviour Pass-through validated first; with the script on, no unintended null discards
8 Field naming Keys under telemetry_data confirmed against a real message, not guessed
9 HA / Node-RED side Entities or flow read values that match what the platform shows
10 Online status notify_offline="TRUE" set; status topic subscribed and not discarded by the script
11 Write control Read back and confirm within N reporting cycles; "send succeeded" is not acceptance
12 Disconnection behaviour Confirmed that messages during an outage are not redelivered; dedup or compensation designed
13 Versioning Forwarder script version, platform version and target broker config recorded

12. Closing

From 2.00.066, ThinkLink replaced "integrate with one specific platform" with "integrate with any MQTT endpoint." For Home Assistant and Node-RED that is not bad news — a built-in integration can only offer one fixed payload shape, whereas the forwarder offers somewhere to write your own translation.

The cost is that the script is yours to write and debug. But the boundaries are clear: topics follow a documented format, the script has one entry function, the return value has three fields, and there is a documented five-step troubleshooting order. What actually goes wrong is detail — {tenant} left as a placeholder, a script silently returning null, status messages carrying no telemetry_data, messages during a target outage never being replayed.

Put those details into an acceptance checklist. It is faster than trial and error.


Related reading

  • Article 21: Writing Payload Decoders in ThinkLink — From 15 Bytes to Readable Data
  • Article 22: ThinkLink Trigger Models and Alarm Rules — Alerts That Arrive on Time and Get Read
  • Article 24: Connecting ThinkLink to BACnet/IP and Modbus TCP — From Field Mapping to YABE Verification

Based on ThinkLink official documentation; paths and parameters follow your actual platform version. Official manuals:
https://www.think-link.net/docs/en/Advanced/MQTTForwarder.html
https://www.think-link.net/docs/en/QuickStart/SystemManagement/ServerConfiguration.html
https://www.think-link.net/docs/en/Protocol/ThinkLinkProtocol.html

Top comments (0)