Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support custom read responses #982

Merged
merged 3 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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