This document captures practical implementation conventions that proved useful while building the ZG-204ZV adapters:
It complements addapters.md.
addapters.md defines the normative adapter role and contract boundaries.
This document focuses on practical Node-RED integration patterns, cold-start behavior, flow topology, and known failure modes.
In practice, two different adapter classes appear in the system:
These consume vendor topics and publish canonical bus topics.
Example:
zigbee2mqtt/ZG-204ZV/... -> vad/home/...
These consume canonical bus topics and project them into another consumer model.
Example:
vad/home/... -> HomeKit
Rule:
Pipeline:
Device -> Ingress Adapter -> MQTT Bus -> Consumer Adapter -> Consumer System
Recommended structure:
mqtt in on vendor brokermqtt out on canonical brokerIf the adapter controls vendor subscription dynamically:
mqtt in control messagesPractical convention:
mqtt in control output as the last outputRecommended structure:
mqtt in on canonical brokerlast, value, and control messagesTypical example:
.../last and .../valuelast for cold startvalue.../last after bootstrap completeslastFor stateful consumers such as HomeKit, a practical pattern is:
.../last.../valuelastvalue.../last after bootstrap completesExample:
vad/home/balcon/+/south/lastvad/home/balcon/+/south/valuevad/home/balcon/+/south/lastBootstrap completion SHOULD be defined explicitly.
Good rules:
last or valueBad rule:
That rule is incomplete because a single live message does not imply that the consumer has received all required initial state.
mqtt in Control MessagesFor Node-RED dynamic mqtt in, control messages SHOULD remain simple.
Preferred forms:
{
"action": "subscribe",
"topic": "vad/home/balcon/+/south/last",
"qos": 2,
"rh": 0,
"rap": true
}
{
"action": "unsubscribe",
"topic": "vad/home/balcon/+/south/last"
}
Practical recommendation:
This keeps behavior easier to debug and makes sidebar traces clearer.
This was a critical lesson from the HomeKit consumer adapter.
If a dynamic consumer depends on retained last as its cold-start mechanism, it SHOULD use its own MQTT client session.
In Node-RED terms:
mqtt in its own mqtt-broker config nodeReason:
Observed failure mode:
.../last receives retained messageslast, creating the false impression that only live traffic worksRule:
This is especially important for:
lastIt is usually not necessary for:
Adapters must be careful not to publish device-generated derived semantics as if they were raw truth.
Example from ZG-204ZV:
presencefading_time = 0, that field is effectively raw motion detectionmotion/valuepresence should remain reserved for held or higher-level derived occupancy semanticsRule:
This prevents single-device vendor quirks from leaking into the semantic bus.
Adapter nodes SHOULD expose useful runtime status in the Node-RED editor.
Useful status content:
Adapters SHOULD also surface problems through:
node.warn for malformed input and validation failuresnode.error for internal exceptions<site>/sys/adapter/<adapter_id>/error<site>/sys/adapter/<adapter_id>/dlq<site>/sys/adapter/<adapter_id>/statsRule:
That combination makes debugging much faster than relying on only one channel.
While implementing a new adapter, use three debug perspectives:
What the source mqtt in actually receives.
What control messages are sent to a dynamic mqtt in.
What canonical bus publications are emitted.
For consumers using last bootstrap, it is useful to have:
.../last.../valuemqtt in outputThis makes it possible to distinguish:
ZG-204ZVSource:
zigbee2mqtt/ZG-204ZV/<site>/<location>/<device_id>
Output:
.../value for live semantic samples.../last retained for latest timestamped sample.../meta retained.../availability retained.../sys/adapter/... for operational topicsSource:
vad/home/<location>/<capability>/<device_id>/lastvad/home/<location>/<capability>/<device_id>/valueOutput:
mqtt in control for bootstrap subscribe/unsubscribeLessons learned:
motion and a fading timerlast bootstrap requires a dedicated MQTT session for the dynamic consumermqtt in control output as the last output when practicallast on first live message alonesys and in Node-RED flow messages