diff --git a/src/controller/model/device.ts b/src/controller/model/device.ts index 3da6f712d0..d067574704 100755 --- a/src/controller/model/device.ts +++ b/src/controller/model/device.ts @@ -30,6 +30,8 @@ interface RoutingTable { table: {destinationAddress: number; status: string; nextHop: number}[]; } +type CustomReadResponse = (frame: Zcl.ZclFrame, endpoint: Endpoint) => boolean; + class Device extends Entity { private readonly ID: number; private _applicationVersion?: number; @@ -52,6 +54,7 @@ class Device extends Entity { private _linkquality?: number; private _skipDefaultResponse: boolean; private _skipTimeResponse: boolean; + private _customReadResponse?: CustomReadResponse; private _deleted: boolean; private _lastDefaultResponseSequenceNumber: number; private _checkinInterval: number; @@ -101,6 +104,8 @@ class Device extends Entity { set skipDefaultResponse(skipDefaultResponse: boolean) {this._skipDefaultResponse = skipDefaultResponse;} get skipTimeResponse(): boolean {return this._skipTimeResponse;} set skipTimeResponse(skipTimeResponse: boolean) {this._skipTimeResponse = skipTimeResponse;} + get customReadResponse(): CustomReadResponse {return this._customReadResponse;} + set customReadResponse(customReadResponse: CustomReadResponse) {this._customReadResponse = customReadResponse;} get checkinInterval(): number {return this._checkinInterval;} set checkinInterval(checkinInterval: number) { this._checkinInterval = checkinInterval; @@ -230,18 +235,20 @@ class Device extends Entity { } // Reponse to read requests - if (frame.isGlobal() && frame.isCommand('read')) { + if (frame.isGlobal() && frame.isCommand('read') && !(this._customReadResponse?.(frame, endpoint))) { const time = Math.round(((new Date()).getTime() - OneJanuary2000) / 1000); const attributes: {[s: string]: KeyValue} = { ...endpoint.clusters, - genTime: {attributes: { - timeStatus: 3, // Time-master + synchronised - time: time, - timeZone: ((new Date()).getTimezoneOffset() * -1) * 60, - localTime: time - (new Date()).getTimezoneOffset() * 60, - lastSetTime: time, - validUntilTime: time + (24 * 60 * 60), // valid for 24 hours - }}, + genTime: { + attributes: { + timeStatus: 3, // Time-master + synchronised + time: time, + timeZone: ((new Date()).getTimezoneOffset() * -1) * 60, + localTime: time - (new Date()).getTimezoneOffset() * 60, + lastSetTime: time, + validUntilTime: time + (24 * 60 * 60), // valid for 24 hours + }, + }, }; if (frame.Cluster.name in attributes && (frame.Cluster.name !== 'genTime' || !this._skipTimeResponse)) { diff --git a/test/controller.test.ts b/test/controller.test.ts index 40dc18a68b..3ac7800909 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -1985,6 +1985,30 @@ describe('Controller', () => { expect(deepClone(call[3])).toStrictEqual({"Header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":1,"disableDefaultResponse":true,"manufacturerSpecific":false},"transactionSequenceNumber":40,"manufacturerCode":null,"commandIdentifier":1},"Cluster":{"ID":10,"attributes":{"time":{"ID":0,"type":226,"name":"time"},"timeStatus":{"ID":1,"type":24,"name":"timeStatus"},"timeZone":{"ID":2,"type":43,"name":"timeZone"},"dstStart":{"ID":3,"type":35,"name":"dstStart"},"dstEnd":{"ID":4,"type":35,"name":"dstEnd"},"dstShift":{"ID":5,"type":43,"name":"dstShift"},"standardTime":{"ID":6,"type":35,"name":"standardTime"},"localTime":{"ID":7,"type":35,"name":"localTime"},"lastSetTime":{"ID":8,"type":226,"name":"lastSetTime"},"validUntilTime":{"ID":9,"type":226,"name":"validUntilTime"}},"name":"genTime","commands":{},"commandsResponse":{}},"Command":{"ID":1,"name":"readRsp","parameters":[{"name":"attrId","type":33},{"name":"status","type":32},{"name":"dataType","type":32,"conditions":[{"type":"statusEquals","value":0}]},{"name":"attrData","type":1000,"conditions":[{"type":"statusEquals","value":0}]}]}}); }); + it('Allow to override read response through `device.customReadResponse`', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + mocksendZclFrameToEndpoint.mockClear(); + + const device = controller.getDeviceByIeeeAddr('0x129'); + device.customReadResponse = jest.fn().mockReturnValue(true); + + const payload = { + wasBroadcast: false, + address: 129, + frame: ZclFrame.create(0, 0, true, null, 40, 0, 10, [{attrId: 0}, {attrId: 1}, {attrId: 7}, {attrId: 9}]), + endpoint: 1, + linkquality: 19, + groupID: 10, + }; + + await mockAdapterEvents['zclData'](payload); + + expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(0); + expect(device.customReadResponse).toHaveBeenCalledTimes(1); + expect(device.customReadResponse).toHaveBeenCalledWith(payload.frame, device.getEndpoint(1)) + }); + it('Respond to read of attribute', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});