Skip to content

Commit

Permalink
feat: Support custom read responses (#982)
Browse files Browse the repository at this point in the history
* feat: Support custom read responses

* add test
  • Loading branch information
Koenkk authored Mar 19, 2024
1 parent 5693789 commit 1c196df
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 9 deletions.
25 changes: 16 additions & 9 deletions src/controller/model/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
24 changes: 24 additions & 0 deletions test/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'});
Expand Down

0 comments on commit 1c196df

Please sign in to comment.