diff --git a/src/controller/controller.ts b/src/controller/controller.ts index 0e8741b37d..b76f06442c 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -683,18 +683,18 @@ class Controller extends events.EventEmitter { if (frame.isGlobal()) { if (frame.isCommand('report')) { type = 'attributeReport'; - data = ZclFrameConverter.attributeKeyValue(dataPayload.frame); + data = ZclFrameConverter.attributeKeyValue(dataPayload.frame, device.manufacturerID); } else if (frame.isCommand('read')) { type = 'read'; - data = ZclFrameConverter.attributeList(dataPayload.frame); + data = ZclFrameConverter.attributeList(dataPayload.frame, device.manufacturerID); } else if (frame.isCommand('write')) { type = 'write'; - data = ZclFrameConverter.attributeKeyValue(dataPayload.frame); + data = ZclFrameConverter.attributeKeyValue(dataPayload.frame, device.manufacturerID); } else { /* istanbul ignore else */ if (frame.isCommand('readRsp')) { type = 'readResponse'; - data = ZclFrameConverter.attributeKeyValue(dataPayload.frame); + data = ZclFrameConverter.attributeKeyValue(dataPayload.frame, device.manufacturerID); } } } else { diff --git a/src/controller/helpers/zclFrameConverter.ts b/src/controller/helpers/zclFrameConverter.ts index 5f45304790..54c1af97b5 100644 --- a/src/controller/helpers/zclFrameConverter.ts +++ b/src/controller/helpers/zclFrameConverter.ts @@ -1,34 +1,49 @@ -import {ZclFrame} from '../../zcl'; +import {ZclFrame, Utils as ZclUtils} from '../../zcl'; +import {Cluster} from '../../zcl/tstype'; interface KeyValue {[s: string]: number | string} -function attributeKeyValue(frame: ZclFrame): KeyValue { +// Certain devices (e.g. Legrand/4129) fail to set the manufacturerSpecific flag and +// manufacturerCode in the frame header, despite using specific attributes. +// This leads to incorrect reported attribute names. +// Remap the attributes using the target device's manufacturer ID +// if the header is lacking the information. +function getCluster(frame: ZclFrame, deviceManufacturerID: number): Cluster { + let cluster = frame.Cluster; + + if (!frame?.Header?.manufacturerCode && frame?.Cluster && Number.isInteger(deviceManufacturerID)) { + cluster = ZclUtils.getCluster(frame.Cluster.ID, deviceManufacturerID); + } + return cluster; +} + +function attributeKeyValue(frame: ZclFrame, deviceManufacturerID: number): KeyValue { const payload: KeyValue = {}; + const cluster = getCluster(frame, deviceManufacturerID); for (const item of frame.Payload) { try { - const attribute = frame.Cluster.getAttribute(item.attrId); + const attribute = cluster.getAttribute(item.attrId); payload[attribute.name] = item.attrData; } catch (error) { payload[item.attrId] = item.attrData; } } - return payload; } -function attributeList(frame: ZclFrame): Array { +function attributeList(frame: ZclFrame, deviceManufacturerID: number): Array { const payload: Array = []; + const cluster = getCluster(frame, deviceManufacturerID); for (const item of frame.Payload) { try { - const attribute = frame.Cluster.getAttribute(item.attrId); + const attribute = cluster.getAttribute(item.attrId); payload.push(attribute.name); } catch (error) { payload.push(item.attrId); } } - return payload; } diff --git a/src/controller/model/device.ts b/src/controller/model/device.ts index 756334d3b8..df05008350 100755 --- a/src/controller/model/device.ts +++ b/src/controller/model/device.ts @@ -213,8 +213,8 @@ class Device extends Entity { // Update reportable properties if (frame.isCluster('genBasic') && (frame.isCommand('readRsp') || frame.isCommand('report'))) { - for (const [key, value] of Object.entries(ZclFrameConverter.attributeKeyValue(frame))) { - Device.ReportablePropertiesMapping[key]?.set(value, this); + for (const [key, val] of Object.entries(ZclFrameConverter.attributeKeyValue(frame, this.manufacturerID))) { + Device.ReportablePropertiesMapping[key]?.set(val, this); } } diff --git a/src/controller/model/endpoint.ts b/src/controller/model/endpoint.ts index d6e013c08d..11a42534dc 100644 --- a/src/controller/model/endpoint.ts +++ b/src/controller/model/endpoint.ts @@ -561,7 +561,7 @@ class Endpoint extends Entity { if (!options.disableResponse) { this.checkStatus(result.frame.Payload); - return ZclFrameConverter.attributeKeyValue(result.frame); + return ZclFrameConverter.attributeKeyValue(result.frame, this.getDevice().manufacturerID); } else { return null; } diff --git a/test/controller.test.ts b/test/controller.test.ts index cb6f60dc9b..3eae1930e9 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -259,6 +259,14 @@ const mockDevices = { 1: {modelId: 'lumi.plug', manufacturerName: 'LUMI', zclVersion: 1, appVersion: 2, hwVersion: 3, dateCode: '201901', swBuildId: '1.01', powerSource: 1, stackVersion: 101}, }, }, + 177: { + nodeDescriptor: {type: 'Router', manufacturerCode: 4129}, + activeEndpoints: {endpoints: [1]}, + simpleDescriptor: {1: {endpointID: 1, deviceID: 514, inputClusters: [0, 3, 258, 4, 5, 15, 64513], outputClusters: [258, 0, 64513, 5, 25], profileID: 260}}, + attributes: { + 1: {modelId: ' Shutter switch with neutral', manufacturerName: 'Legrand', zclVersion: 2, appVersion: 0, hwVersion: 8, dateCode: '231030', swBuildId: '0038', powerSource: 1, stackVersion: 67}, + }, + }, } const mockZclFrame = ZclFrame; @@ -4971,6 +4979,76 @@ describe('Controller', () => { expect(result.missingRouters.length).toBe(1); expect(result.missingRouters[0].ieeeAddr).toBe('0x129'); }); + + // ZCLFrame with manufacturer specific flag and manufacturer code defined, to generic device + // ZCLFrameConverter should not modify specific frames! + it('Should resolve manufacturer specific cluster attribute names on specific ZCL frames: generic target device', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + await mockAdapterEvents['zclData']({ + wasBroadcast: false, + address: '0x129', + frame: ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Buffer.from([28,33,16,13,1,2,240,0,48,4])), + endpoint: 1, + linkquality: 50, + groupID: 0, + }); + expect(events.message.length).toBe(1); + expect(events.message[0].data).toMatchObject({calibrationMode:4}); + expect(events.message[0].data).not.toMatchObject({tuyaMotorReversal:4}); + }); + + // ZCLFrame with manufacturer specific flag and manufacturer code defined, to specific device + // ZCLFrameConverter should not modify specific frames! + it('Should resolve manufacturer specific cluster attribute names on specific ZCL frames: specific target device', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 177, ieeeAddr: '0x177'}); + await mockAdapterEvents['zclData']({ + wasBroadcast: false, + address: '0x177', + frame: ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Buffer.from([28,33,16,13,1,2,240,0,48,4])), + endpoint: 1, + linkquality: 50, + groupID: 0, + }); + expect(events.message.length).toBe(1); + expect(events.message[0].data).toMatchObject({calibrationMode:4}); + expect(events.message[0].data).not.toMatchObject({tuyaMotorReversal:4}); + }); + + // ZCLFrame without manufacturer specific flag or manufacturer code set, to generic device + it('Should resolve generic cluster attribute names on generic ZCL frames: generic target device', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + await mockAdapterEvents['zclData']({ + wasBroadcast: false, + address: '0x129', + frame: ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Buffer.from([24,242,10,2,240,48,4])), + endpoint: 1, + linkquality: 50, + groupID: 0, + }); + expect(events.message.length).toBe(1); + expect(events.message[0].data).toMatchObject({tuyaMotorReversal:4}); + expect(events.message[0].data).not.toMatchObject({calibrationMode:4}); + }); + + // ZCLFrame without manufacturer specific flag set or manufacturer code set, to specific device + it('Should resolve manufacturer specific cluster attribute names on generic ZCL frames: specific target device', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 177, ieeeAddr: '0x177'}); + await mockAdapterEvents['zclData']({ + wasBroadcast: false, + address: '0x177', + frame: ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Buffer.from([24,242,10,2,240,48,4])), + endpoint: 1, + linkquality: 50, + groupID: 0, + }); + expect(events.message.length).toBe(1); + expect(events.message[0].data).toMatchObject({calibrationMode:4}); + expect(events.message[0].data).not.toMatchObject({tuyaMotorReversal:4}); + }); });