From 1e926b119c5b481b2f50de4e80057f69a8b27daf Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Tue, 27 Oct 2026 14:47:48 +0100 Subject: [PATCH] fix(cluster): Emit attr. for an unknown attribute in a report A reported attribute the cluster does not define has no name, so every one of them was emitted as attr.undefined. Fall back to its numeric id, which is also how readAttributes keys an attribute it has no name for. --- lib/Cluster.js | 2 +- test/virtualDevice.js | 60 ++++++++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/Cluster.js b/lib/Cluster.js index 9a8692c..14277f4 100644 --- a/lib/Cluster.js +++ b/lib/Cluster.js @@ -960,7 +960,7 @@ class Cluster extends EventEmitter { attributes = this.constructor.attributeArrayDataType.fromBuffer(attributes, 0); // Only expose fields chosen here, not the stack's frame meta const reportMeta = { transactionSequenceNumber: frame.trxSequenceNumber }; - attributes.forEach((attr) => this.emit(`attr.${attr.name}`, attr.value, reportMeta)); + attributes.forEach((attr) => this.emit(`attr.${attr.name ?? attr.id}`, attr.value, reportMeta)); } /** diff --git a/test/virtualDevice.js b/test/virtualDevice.js index b8c0d34..b36ba0d 100644 --- a/test/virtualDevice.js +++ b/test/virtualDevice.js @@ -158,33 +158,35 @@ describe('Virtual device: known bugs on master', function () { ); }); - it( - 'a report with an unknown attribute id does not emit attr.undefined', - { todo: 'onReportAttributes emits attr.undefined' }, - async function () { - const { zclNode, virtualDevice, settle } = createVirtualDevice({ - endpoints: [{ endpointId: 1, inputClusters: [OnOffCluster.ID] }], - }); - const onOff = zclNode.endpoints[1].clusters.onOff; - const onUndefined = mock.fn(); - const onOnOff = mock.fn(); - onOff.on('attr.undefined', onUndefined); - onOff.on('attr.onOff', onOnOff); - - const report = frameToClient({ - cmdId: 0x0a, // reportAttributes - disableDefaultResponse: true, - // 0x4242 uint8 (0x20) 1, then onOff bool (0x10) true - data: Buffer.from([0x42, 0x42, 0x20, 0x01, ON_OFF, 0x00, 0x10, 0x01]), - }); - await virtualDevice.sendFrame(1, OnOffCluster.ID, report); - await settle(); - - assert.deepStrictEqual( - onOnOff.mock.calls.map((call) => call.arguments[0]), - [true], - ); - assert.strictEqual(onUndefined.mock.callCount(), 0); - }, - ); + it('a report with an unknown attribute id emits attr., not attr.undefined', async function () { + const { zclNode, virtualDevice, settle } = createVirtualDevice({ + endpoints: [{ endpointId: 1, inputClusters: [OnOffCluster.ID] }], + }); + const onOff = zclNode.endpoints[1].clusters.onOff; + const onUndefined = mock.fn(); + const onUnknown = mock.fn(); + const onOnOff = mock.fn(); + onOff.on('attr.undefined', onUndefined); + onOff.on(`attr.${0x4242}`, onUnknown); + onOff.on('attr.onOff', onOnOff); + + const report = frameToClient({ + cmdId: 0x0a, // reportAttributes + disableDefaultResponse: true, + // 0x4242 uint8 (0x20) 1, then onOff bool (0x10) true + data: Buffer.from([0x42, 0x42, 0x20, 0x01, ON_OFF, 0x00, 0x10, 0x01]), + }); + await virtualDevice.sendFrame(1, OnOffCluster.ID, report); + await settle(); + + assert.deepStrictEqual( + onOnOff.mock.calls.map((call) => call.arguments[0]), + [true], + ); + assert.deepStrictEqual( + onUnknown.mock.calls.map((call) => call.arguments[0]), + [1], + ); + assert.strictEqual(onUndefined.mock.callCount(), 0); + }); });