From 0faac47011c7f0777505aed2500b198b549d98e8 Mon Sep 17 00:00:00 2001 From: Johannes Krobath Date: Sun, 28 Jun 2026 19:54:34 +0200 Subject: [PATCH] fix: guard undefined entity on the incoming-message path handleMessage and onZigbeeEvent run for every incoming zigbee message, bound fire-and-forget on the herdsman 'message' event and the controller 'event' event. resolveEntity() can return undefined for a message from a device that does not resolve (unknown/leaving device, DB race), and neither path guarded it: - handleMessage dereferenced data/entity (data.device.interviewState, data.device || data.ieeeAddr, ...) without guarding undefined. - onZigbeeEvent dereferenced entity.device at the top without guarding entity. Since both handlers are async and their callers don't await them, the throw became an unhandled promise rejection - fatal under compact mode. - onZigbeeEvent guards the entity and returns early, after its debug line so an unresolved message is still logged; the redundant `.device` half is dropped (resolveEntity returns a structure with `.device` or undefined). - handleMessage addresses data via `data?.` and uses `entity?.options ?? {}` and `entity?.name`. resolveEntity self-catches and returns undefined, callExtensionMethod guards each extension call, and event() dispatches via emit, so no wrapping try/catch is needed. - utils.entityData() is made null-safe (`name: e?.name`, the only field there not already using optional chaining) so the debug line is safe for an unresolved entity. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/statescontroller.js | 3 +++ lib/utils.js | 2 +- lib/zigbeecontroller.js | 16 ++++++++-------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/statescontroller.js b/lib/statescontroller.js index ab1fdede..6ff5415c 100644 --- a/lib/statescontroller.js +++ b/lib/statescontroller.js @@ -1414,6 +1414,9 @@ class StatesController extends EventEmitter { async onZigbeeEvent(type, entity, message) { if (this.debugActive) this.debug(`onZigbeeEvent: Type ${type} device ${JSON.stringify(utils.entityData(entity, true))} incoming event: ${JSON.stringify(utils.zigbeeMessageData(message))}`); + if (!entity) { + return; + } const device = entity.device; const mappedModel = entity.mapped; diff --git a/lib/utils.js b/lib/utils.js index 68e54627..0f3efdeb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -303,7 +303,7 @@ function entityData(e, detailed) { const rv = { type: e?.type, - name: e.name, + name: e?.name, options: e?.options, model: e?.mapped?.model, } diff --git a/lib/zigbeecontroller.js b/lib/zigbeecontroller.js index ccf3f383..60e6f0fe 100644 --- a/lib/zigbeecontroller.js +++ b/lib/zigbeecontroller.js @@ -1222,22 +1222,22 @@ class ZigbeeController extends EventEmitter { this.debug(`handleMessage`, { data: utils.zigbeeMessageData(data) }); } - const is = data.device.interviewState; + const is = data?.device?.interviewState; if (is != 'SUCCESSFUL' && is != 'FAILED') { this.debug(`message ${JSON.stringify(data)} received during interview.`) } - const entity = await this.resolveEntity(data.device || data.ieeeAddr); - const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr; + const entity = await this.resolveEntity(data?.device ?? data?.ieeeAddr); + const name = entity?.name ?? data?.device?.ieeeAddr; if (this.debugActive) this.debug( - `Received Zigbee message from '${name}', type '${data.type}', cluster '${data.cluster}'` + - `, data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` + - (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) + `Received Zigbee message from '${name}', type '${data?.type}', cluster '${data?.cluster}'` + + `, data '${JSON.stringify(data?.data)}' from endpoint ${data?.endpoint?.ID}` + + (data?.groupID != undefined ? ` with groupID ${data?.groupID}` : ``) ); - this.event(data.type, entity, data); + this.event(data?.type, entity, data); // Call extensions this.callExtensionMethod( 'onZigbeeEvent', - [{...data, options:entity.options || {}}, entity ? entity.mapped : null], + [{...data, options:entity?.options ?? {}}, entity ? entity.mapped : null], ); }