Skip to content

Register custom clusters on first pairing, not only after restart#2943

Open
krobipd wants to merge 1 commit into
ioBroker:masterfrom
krobipd:fix/2915-custom-cluster-pairing
Open

Register custom clusters on first pairing, not only after restart#2943
krobipd wants to merge 1 commit into
ioBroker:masterfrom
krobipd:fix/2915-custom-cluster-pairing

Conversation

@krobipd

@krobipd krobipd commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

The problem

deviceAddCustomCluster (herdsman modernExtend) registers a device's custom clusters via two paths: its configure chain, and an onEvent handler keyed on event.type === 'start'. In this adapter 'start' is only ever fired from onZigbeeStarted — i.e. for devices that already exist at adapter start (zbDeviceEvent.js). A freshly paired device only sees deviceJoined/deviceInterview, never 'start', so that handler never runs for it and its custom clusters aren't registered until the next restart. An incoming frame on an unregistered custom cluster then fails to parse in herdsman's getCluster() with UNSUPPORTED_CLUSTER.

This matches the report, including why a restart or re-pair fixes it: on restart onZigbeeStarted fires 'start' for every device and registers the clusters up front.

Verified without hardware (deterministic): running the real DeviceEvent extension against a real herdsman definition using deviceAddCustomCluster (Develco AQSZB-110) with a mocked device — device.customClusters stays empty after a deviceInterview on the current code, and is populated after 'start'. The regression test below fails on the current code and passes with the fix.

Not verified (timing — your hardware): whether, in practice, a frame actually lands on the custom cluster before the cluster is registered. configure also registers it, just later/queued, so the window is real but timing-dependent. That last link between this asymmetry and the exact error you observe is the one thing I'm asking you to confirm.

The fix

Mirror zigbee2mqtt's onEvent extension (lib/extension/onEvent.ts): track which devices already received a 'start', and the first time a device is seen with any non-start/stop event, synthesise 'start' first so its custom clusters are registered before the real event is dispatched. No-op + retried on the next event if the model isn't resolvable yet during a join.

if (type === 'start') {
    this.startCalled.add(device.ieeeAddr);
} else if (type === 'stop') {
    this.startCalled.delete(device.ieeeAddr);
} else if (md && md.onEvent && !this.startCalled.has(device.ieeeAddr)) {
    this.startCalled.add(device.ieeeAddr);
    await md.onEvent({ type: 'start', data: baseData });
}

Test

lib/zbDeviceEvent.test.js — Node's built-in runner, no framework, no hardware:

node --test lib/zbDeviceEvent.test.js

Covers: custom clusters registered on a fresh pairing; no second synthetic start once started; no-op + retry while the model is unresolved; re-arm after stop. (I used node:test because npm run test:js currently errors on the deprecated mocha --opts — I did not touch the test setup.)

Notes

  • npm run test:package (56) and npm run test:unit pass; ESLint clean.
  • I have no custom-cluster device to reproduce on hardware. If the timing matches what you see this should close it; if not, the asymmetry is still real and worth fixing on its own.

Addresses #2915

🤖 Generated with Claude Code

ioBroker#2915)

A freshly paired device only receives 'deviceJoined'/'deviceInterview'
events, never 'start'. modernExtend onEvent handlers that key on 'start'
-- notably deviceAddCustomCluster -- therefore never register their custom
clusters until the next adapter restart. An incoming frame on such an
unregistered custom cluster then fails to parse with UNSUPPORTED_CLUSTER,
which is exactly why a restart or re-pair fixes it: onZigbeeStarted fires
'start' for every known device and registers the clusters up front.

Mirror zigbee2mqtt's onEvent extension (lib/extension/onEvent.ts): track
which devices already got a 'start', and the first time a device is seen
with any other (non-stop) event, synthesise 'start' first so its custom
clusters are registered before the real event is dispatched. If the model
is not resolvable yet during a join this is a no-op and retried on the
next event.

Verified without hardware in lib/zbDeviceEvent.test.js (Node's built-in
test runner): the real DeviceEvent extension + a real herdsman definition
using deviceAddCustomCluster (Develco AQSZB-110) + a mocked device. The
test fails against the current code (custom clusters stay empty on first
pairing) and passes with the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant