An adapter is a software component that translates data between an external system or device and the internal MQTT semantic bus used in the infrastructure.
The primary role of the adapter is to normalize heterogeneous protocols, topic structures, and payload formats into the canonical structure used by the system.
Adapters do not implement business logic, automation rules, aggregation, or storage. Their responsibility is strictly limited to protocol translation and semantic normalization.
Shared payload, metadata, quality, and operational namespace rules are defined in mqtt_contract.md and sys_bus.md.
Practical Node-RED conventions and worked examples are documented in adapter_implementation_examples.md.
In a heterogeneous environment (Zigbee, Tasmota, SNMP, Modbus, MikroTik APIs, custom firmware, etc.), devices publish data using incompatible conventions:
Adapters isolate these differences and expose a stable internal MQTT bus contract.
Adapters operate at the ingress boundary of the system.
Pipeline:
Device / External System ↓ Vendor / Protocol Topics ↓ Adapter ↓ Canonical MQTT Bus ↓ Consumers (HomeKit, historian, automation, analytics)
An adapter may perform the following transformations:
Example:
zigbee2mqtt/bedroom_sensor/temperature ↓ vad/home/bedroom/temperature/bedroom-sensor/value
Examples:
"23.4" → 23.4
{"temperature":23.4} ↓ 23.4
If the device provides an observation timestamp, the adapter must preserve it.
If the device does not provide timestamps, the adapter may attach ingestion time.
Example:
°F → °C
Adapters map vendor identifiers to canonical IDs used by bus contracts.
Adapters must route data to valid streams (value, last, set, meta, availability) according to bus rules. Legacy state and event topics remain compatibility-only during migration and SHOULD NOT be introduced by new adapters.
Adapters should publish enough retained meta for historian workers to ingest canonical topics without knowing vendor semantics.
Adapters MAY construct richer internal objects during normalization.
Example internal normalization shape:
{
"sourcePayload": {
"temperature": 23.6,
"battery": 91
},
"normalizedLocation": "bedroom",
"capability": "temperature",
"deviceId": "bedroom-sensor",
"stream": "value"
}
Rule:
msg object that continues through the hot-path pipelineAt the publish boundary, the message SHOULD contain only:
msg.topicmsg.payloadmsg.qos when QoS is not configured statically on the MQTT nodemsg.retain when retain is not configured statically on the MQTT nodeAdapters MUST NOT publish rich internal envelopes such as:
{
"topic": "vad/home/bedroom/temperature/bedroom-sensor/value",
"payload": 23.6,
"mapping": {
"source_field": "temperature"
},
"normalizedBus": {
"bus": "home",
"stream": "value"
},
"sourcePayload": {
"temperature": 23.6,
"battery": 91
},
"internalContext": {
"location": "bedroom"
}
}
Those fields may exist inside adapter logic, but MUST be removed before publish.
Many ingress protocols emit a single inbound payload containing multiple metrics.
Examples:
Adapters SHOULD normalize those payloads using fan-out:
1 inbound message ↓ multiple canonical MQTT messages
Example inbound payload:
{
"temperature": 23.4,
"humidity": 41
}
Canonical adapter output:
vad/home/bedroom/temperature/bedroom-sensor/value -> 23.4vad/home/bedroom/humidity/bedroom-sensor/value -> 41Rule:
msg object through multiple downstream stagesmeta topicSome physical devices expose multiple capabilities that belong to different semantic domains. In such cases an adapter may project different aspects of the same device onto different buses.
A common example is a smart socket (smart plug) which provides both:
These capabilities belong to different semantic models:
An adapter may therefore publish different streams derived from the same device to different buses.
Example:
Home bus (control semantics):
vad/home/living-room/power/tv/value vad/home/living-room/power/tv/set
Energy bus (load telemetry):
vad/energy/load/living-room-entertainment/active_power/value vad/energy/load/living-room-entertainment/energy_total/value
In this situation the adapter performs a capability projection, exposing each capability in the semantic domain where it belongs.
This approach prevents the spatial model of the home (home bus) from becoming coupled to the electrical topology represented by the energy bus, while still allowing a single physical device to participate in both domains.
Rule:
Adapters must NOT:
These functions belong to other components in the system.
Because adapters are implemented in Node-RED, the following constraints apply:
change/switch mapping before custom function logicvalue streams and publish metadata on retained metalast for cold-start samples that need timestamp/freshness evaluationvalue streams lightweight and deduplicated where appropriateNot all adapters are ingress adapters.
In practice, the system also includes consumer adapters that subscribe to canonical bus topics and project them into a downstream model such as HomeKit.
Examples:
Consumer adapters SHOULD follow these rules:
last for bootstrap when startup state mattersvalue after bootstraplast only after bootstrap completeness is explicitly satisfiedPractical recommendation:
mqtt in, keep that control output as the last output when possibleThis keeps semantic outputs stable and reduces rewiring churn.
If a consumer adapter depends on retained last for deterministic cold start, it SHOULD use a dedicated MQTT client session.
In Node-RED this means:
mqtt-broker config node for that dynamic mqtt inReason:
Observed failure mode:
lastlast, masking the real issueRule:
mqtt in Control RecommendationsFor Node-RED dynamic mqtt in, adapter control messages SHOULD remain simple and explicit.
Preferred pattern:
subscribe message per topicunsubscribe message per topicExample:
{
"action": "subscribe",
"topic": "vad/home/balcon/+/south/last",
"qos": 2,
"rh": 0,
"rap": true
}
{
"action": "subscribe",
"topic": "vad/home/balcon/+/south/value",
"qos": 2,
"rh": 0,
"rap": true
}
Recommendation:
See adapter_implementation_examples.md for the full flow pattern and debugging guidance.
- allow last to update whenever the latest observation timestamp changes, even if the scalar value is unchanged
- publish MQTT-ready messages as early as possible once normalization is complete
- keep temporary normalization structures in local variables or node-local context, not on forwarded msg objects
- delete temporary normalization fields before the MQTT publish node
- keep MQTT subscriptions narrow (avoid global # on hot pipelines)
- include error routing for malformed input and unknown mapping cases
Hot-path rule:
sys topics, not on bus payloadsRecommended final publish stage:
return {
topic: normalizedTopic,
payload: normalizedValue
};
If an existing msg object must be reused:
msg.topic = normalizedTopic;
msg.payload = normalizedValue;
delete msg.internal;
delete msg.internalContext;
delete msg.mapping;
delete msg.normalizedBus;
delete msg.sourcePayload;
return msg;
Operational requirement:
<site>/sys/adapter/<adapter_id>/dlq<site>/sys/adapter/<adapter_id>/error<site>/sys/adapter/<adapter_id>/availability<site>/sys/adapter/<adapter_id>/statsSee sys_bus.md for the shared contract of these operational topics.
Operational recommendation:
Adapters should be driven by declarative mapping data wherever possible.
Recommended mapping fields:
source_systemsource_topic_matchsource_fieldtarget_bustarget_location or target_entity_idtarget_capability or target_metrictarget_device_idstreampayload_profileunithistorian_enabledhistorian_modeExample mapping entry for a Zigbee room sensor:
{
"source_system": "zigbee2mqtt",
"source_topic_match": "zigbee2mqtt/bedroom_sensor",
"source_field": "temperature",
"target_bus": "home",
"target_location": "bedroom",
"target_capability": "temperature",
"target_device_id": "bedroom-sensor",
"stream": "value",
"payload_profile": "scalar",
"unit": "C",
"historian_enabled": true,
"historian_mode": "sample"
}
This keeps normalization logic deterministic and reviewable.
Adapters are typically organized by protocol or subsystem.
Examples:
zigbee_adapter
Transforms Zigbee2MQTT topics into the canonical bus structure.
network_adapter
Transforms SNMP / router telemetry into the network bus.
energy_adapter
Normalizes inverter, meter, and battery telemetry.
vehicle_adapter
Normalizes EV charger and vehicle telemetry.
Zigbee2MQTT is expected to be one of the first major ingress sources for the new broker, so its projection rules should be explicit.
Source shape:
zigbee2mqtt/<friendly_name>zigbee2mqtt/<friendly_name>/availabilityNormalization rules:
meta.source_ref, not exposed in canonical topic pathsvaluemeta.historian.mode to label whether a value stream is semantically a sample, state, or eventvalue publications when the semantic value did not changelast whenever the latest observed timestamp changes, even if the scalar value is unchangedRecommended initial Z2M field mapping:
temperature -> home/.../temperature/.../valuehumidity -> home/.../humidity/.../valuepressure -> home/.../pressure/.../valueilluminance -> home/.../illuminance/.../valuecontact -> home/.../contact/.../valueoccupancy from PIR devices -> home/.../motion/.../valuepresence from mmWave devices with fading_time=0 -> home/.../motion/.../valuebattery -> home/.../battery/.../valuestate on smart plugs or switches -> home/.../power/.../valuepower, energy, voltage, current on smart plugs -> energy/.../.../.../valueaction -> home/.../button/.../valuemmWave presence handling:
presence while fading_time=0, adapters SHOULD treat it as raw motion detection and publish motion, not presencepresence SHOULD usually be derived above the adapter layer through fusion or higher-level logicpresence/.../value directly only when the device is intentionally configured to expose held presence semantics, for example with non-zero fading_timeFields that SHOULD NOT go to home by default:
linkqualityThose belong on sys or a future network bus.
In Zigbee2MQTT the primary telemetry topic structure is:
zigbee2mqtt/<friendly_name>
The MQTT prefix is controlled by the Zigbee2MQTT base_topic configuration parameter.
Default:
zigbee2mqtt
Zigbee2MQTT allows the / character inside friendly_name, which means the MQTT topic hierarchy can be controlled by the device name itself.
Example:
kitchen/floor_lightzigbee2mqtt/kitchen/floor_lightThis project uses that feature intentionally to encode semantic information directly into the Zigbee2MQTT topic path.
For Zigbee devices in this repository, the friendly_name MUST use the following structure:
<device_type>/<site>/<location>/<device_id>
Example:
ZG-204ZV/vad/balcon/southzigbee2mqtt/ZG-204ZV/vad/balcon/southSegment meaning:
device_type: hardware model or device classsite: canonical site identifierlocation: canonical room/location identifierdevice_id: logical endpoint identifier within the locationStructuring Zigbee2MQTT topics this way allows adapters to perform deterministic translation without lookup tables.
Example inbound topic:
zigbee2mqtt/ZG-204ZV/vad/balcon/south
Example payload:
{ "illuminance": 704 }
Adapter output:
vad/home/balcon/illuminance/south/value
The adapter only needs to split the topic path and map payload fields to capabilities.
This aligns directly with the home bus contract defined in home_bus.md:
<site>/home/<location>/<capability>/<device_id>/<stream>
Example:
vad/home/balcon/illuminance/south/value
The Zigbee2MQTT topic structure intentionally mirrors the dimensions required by the semantic home bus grammar.
device_type should correspond to the hardware model when possiblelocation must match canonical location identifiers used by the home busdevice_id must be unique within the locationmqtt_contract.mdThis approach removes location mapping tables from adapters, enables deterministic topic parsing, simplifies Node-RED flows, and allows wildcard subscriptions by device type.
Useful subscriptions:
zigbee2mqtt/+/+/+/+zigbee2mqtt/ZG-204ZV/#The purpose of this section is to define how adapters automatically derive canonical MQTT bus topics from source topics without requiring a static mapping table.
The design goal is to keep adapters deterministic and lightweight while allowing configuration overrides for exceptional cases.
Adapters SHOULD attempt to derive semantic dimensions directly from the inbound topic structure.
For Zigbee2MQTT the expected inbound topic format is:
zigbee2mqtt/<device_type>/<site>/<location>/<device_id>
Adapters MUST parse the topic segments and attempt to infer:
These values are then used to construct canonical bus topics.
Example inbound topic:
zigbee2mqtt/ZG-204ZV/vad/balcon/south
Parsed dimensions:
source = zigbee2mqttdevice_type = ZG-204ZVsite = vadlocation = balcondevice_id = southExample inbound payload:
{ "illuminance": 704 }
Adapter output:
vad/home/balcon/illuminance/south/value
Adapters SHOULD follow the following processing order:
This ensures deterministic behavior while allowing controlled deviations.
Adapters MUST support a configuration object allowing explicit overrides.
Overrides allow correcting cases where the inbound topic does not match the canonical semantic model.
Example override configuration:
{
"location_map": {
"balcony": "balcon"
},
"bus_override": {
"power": "energy"
},
"device_id_map": {
"south": "radar-south"
}
}
Override categories:
location_map
Maps inbound location identifiers to canonical location identifiers.bus_override
Overrides which semantic bus a capability should be published to.device_id_map
Renames device identifiers when canonical IDs differ from source IDs.If a semantic dimension cannot be derived from the topic and no override exists, adapters MUST fall back to defaults.
Recommended defaults:
site = configured adapter site idbus = homelocation = unknowndevice_id = device_typestream = valueExample fallback result:
vad/home/unknown/temperature/ZG-204ZV/value
These defaults ensure the adapter continues operating even when topic information is incomplete.
Adapters implemented in Node-RED SHOULD:
<site>/sys/adapter/<adapter_id>/dlq
Adapter errors SHOULD be published to:
<site>/sys/adapter/<adapter_id>/error
This keeps semantic bus traffic deterministic while still exposing operational diagnostics.
Automatic provisioning from topic structure provides several advantages:
This approach maintains the principle that adapters perform normalization rather than interpretation.
Adapters should support a replay-friendly execution model so historian testing does not depend on live device timing.
Recommended modes:
For historian bootstrap, adapters SHOULD be able to emit:
last for streams that must support cold start from the busmetaavailabilityvalue traffic together with retained lastHomeKit integration is implemented through a separate adapter layer that consumes the canonical MQTT bus.
Pipeline:
Device → Protocol Adapter → MQTT Bus → HomeKit Adapter → HomeKit
This separation prevents HomeKit constraints from leaking into protocol translation logic.
Adapters should follow several key principles:
The same input must always produce the same output.
Adapters should avoid maintaining internal state unless strictly required.
Adapters should only normalize data, not reinterpret it.
Repeated messages should not produce inconsistent system state.
Adapters must validate outgoing topics against the target bus grammar.
Adapter behavior should minimize CPU and memory cost in Node-RED hot paths.
Adapter failures must be observable without inspecting raw vendor topics.
Vendor message:
Topic:
zigbee2mqtt/bedroom_sensor
Payload:
{ "temperature": 23.4, "humidity": 41 }
Adapter output:
Topic:
vad/home/bedroom/temperature/bedroom-sensor/value
Payload:
23.4
and
Topic:
vad/home/bedroom/humidity/bedroom-sensor/value
Payload:
41
The final published bus message should be MQTT-ready and minimal, for example:
Topic:
vad/home/balcon/illuminance/radar-south/value
Payload:
1315
It should NOT include diagnostic wrappers, mapping objects, or source payload copies.
If the same source is a smart plug, additional output may also be emitted on the energy bus:
Topic:
vad/energy/load/bedroom-heater/active_power/value
Payload:
126.8
This abstraction allows the rest of the system to operate independently of the device ecosystem and vendor protocols.