Skip to content

Commit

Permalink
feat: Use cluster definition manufacturerCode when available (#848)
Browse files Browse the repository at this point in the history
* feat: add tests against mixed manufacturerCode attribute read/write/configureReporting

* feat: add tests for custom manufacturerCode read/write

* feat: endpoint.read/write/configureEndpoint should use definition manufacturerCode if available

* fix: move duplicate code to private function

* Update controller.test.ts

* Update controller.test.ts

* fix

* fix: rename ensureManufacturerCodeIsUnique to ensureManufacturerCodeIsUniqueAndGet

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
  • Loading branch information
sjorge and Koenkk authored Jan 1, 2024
1 parent 189c921 commit 3b51b0b
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 4 deletions.
51 changes: 48 additions & 3 deletions src/controller/model/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ class Endpoint extends Entity {
): Promise<void> {
const cluster = Zcl.Utils.getCluster(clusterKey);
options = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
options.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(
cluster, Object.keys(attributes), options.manufacturerCode, 'write',
);

const payload: {attrId: number; dataType: number; attrData: number| string | boolean}[] = [];
for (const [nameOrID, value] of Object.entries(attributes)) {
if (cluster.hasAttribute(nameOrID)) {
Expand Down Expand Up @@ -441,6 +445,10 @@ class Endpoint extends Entity {
): Promise<KeyValue> {
const cluster = Zcl.Utils.getCluster(clusterKey);
options = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
options.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(
cluster, attributes, options.manufacturerCode, 'read',
);

const payload: {attrId: number}[] = [];
for (const attribute of attributes) {
payload.push({attrId: typeof attribute === 'number' ? attribute : cluster.getAttribute(attribute).ID});
Expand Down Expand Up @@ -621,6 +629,10 @@ class Endpoint extends Entity {
): Promise<void> {
const cluster = Zcl.Utils.getCluster(clusterKey);
options = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
options.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(
cluster, items, options.manufacturerCode, 'configureReporting',
);

const payload = items.map((item): KeyValue => {
let dataType, attrId;

Expand All @@ -633,9 +645,6 @@ class Endpoint extends Entity {
const attribute = cluster.getAttribute(item.attribute);
dataType = attribute.type;
attrId = attribute.ID;
if (attribute.hasOwnProperty('manufacturerCode')) {
options.manufacturerCode = attribute.manufacturerCode;
}
}
}

Expand Down Expand Up @@ -826,6 +835,42 @@ class Endpoint extends Entity {
};
}

private ensureManufacturerCodeIsUniqueAndGet(
cluster: Zcl.TsType.Cluster, attributes: (string|number)[]|ConfigureReportingItem[],
fallbackManufacturerCode: number, caller: string,
): number {
const manufacturerCodes = new Set(attributes.map((nameOrID): number => {
let attributeID;
if (typeof nameOrID == 'object') {
// ConfigureReportingItem
if (typeof nameOrID.attribute !== 'object') {
attributeID = nameOrID.attribute;
} else {
return fallbackManufacturerCode;
}
} else {
// string || number
attributeID = nameOrID;
}

// we fall back to caller|cluster provided manufacturerCode
if (cluster.hasAttribute(attributeID)) {
const attribute = cluster.getAttribute(attributeID);
return (attribute.manufacturerCode === undefined) ?
fallbackManufacturerCode :
attribute.manufacturerCode;
} else {
// unknown attribute, we should not fail on this here
return fallbackManufacturerCode;
}
}));
if (manufacturerCodes.size == 1) {
return manufacturerCodes.values().next().value;
} else {
throw new Error(`Cannot have attributes with different manufacturerCode in single '${caller}' call`);
}
}

public async addToGroup(group: Group): Promise<void> {
await this.command('genGroups', 'add', {groupid: group.groupID, groupname: ''});
group.addMember(this);
Expand Down
88 changes: 87 additions & 1 deletion test/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ let configureReportDefaultRsp = false;

const restoreMocksendZclFrameToEndpoint = () => {
mocksendZclFrameToEndpoint.mockImplementation((ieeeAddr, networkAddress, endpoint, frame: ZclFrame) => {
if (frame.isGlobal() && frame.isCommand('read') && (frame.isCluster('genBasic') || frame.isCluster('ssIasZone') || frame.isCluster('genPollCtrl'))) {
if (frame.isGlobal() && frame.isCommand('read') && (frame.isCluster('genBasic') || frame.isCluster('ssIasZone') || frame.isCluster('genPollCtrl') || frame.isCluster('hvacThermostat'))) {
const payload = [];
const cluster = frame.Cluster;
for (const item of frame.Payload) {
Expand Down Expand Up @@ -2757,6 +2757,33 @@ describe('Controller', () => {
expect({...endpoint.configuredReportings[0], cluster: undefined}).toStrictEqual({"attribute":{"ID":18,"type":16,"manufacturerCode":4338,"name":"ubisysVacationMode"},"minimumReportInterval":1,"maximumReportInterval":10,"reportableChange":1, "cluster": undefined});
});

it('Endpoint configure reporting with manufacturer attribute should throw exception', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
const device = controller.getDeviceByIeeeAddr('0x129');
device._manufacturerID = 0x10f2;
const endpoint = device.getEndpoint(1);
mocksendZclFrameToEndpoint.mockClear();
let error;
try {
await endpoint.configureReporting('hvacThermostat', [
{
attribute: 'localTemp',
minimumReportInterval: 1,
maximumReportInterval: 10,
reportableChange: 1,
},
{
attribute: 'ubisysRemoteTemperature',
minimumReportInterval: 1,
maximumReportInterval: 10,
reportableChange: 1,
},
]);
} catch (e) { error = e };
expect(error).toStrictEqual(new Error("Cannot have attributes with different manufacturerCode in single 'configureReporting' call"))
});

it('Save endpoint configure reporting', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
Expand Down Expand Up @@ -3103,6 +3130,23 @@ describe('Controller', () => {
expect(call[4]).toBe(12);
});

it('Write to endpoint custom attributes without specifying manufacturerCode', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
mocksendZclFrameToEndpoint.mockClear();
const device = controller.getDeviceByIeeeAddr('0x129');
device._manufacturerID = 0x10f2;
const endpoint = device.getEndpoint(1);
await endpoint.write('hvacThermostat', {'ubisysDefaultOccupiedHeatingSetpoint': 1800});
expect(mocksendZclFrameToEndpoint).toBeCalledTimes(1);
const call = mocksendZclFrameToEndpoint.mock.calls[0];
expect(call[0]).toBe('0x129');
expect(call[1]).toBe(129);
expect(call[2]).toBe(1);
expect({...deepClone(call[3]), Cluster: {}}).toStrictEqual({"Header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":true,"manufacturerSpecific":true},"transactionSequenceNumber":11,"manufacturerCode":4338,"commandIdentifier":2},"Payload":[{"attrId":17,"attrData":1800,"dataType":41}],"Cluster":{},"Command":{"ID":2,"name":"write","parameters":[{"name":"attrId","type":33},{"name":"dataType","type":32},{"name":"attrData","type":1000}],"response":4}});
expect(call[4]).toBe(10000);
});

it('WriteUndiv to endpoint custom attributes without response', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
Expand Down Expand Up @@ -3132,6 +3176,18 @@ describe('Controller', () => {
expect(mocksendZclFrameToEndpoint).toBeCalledTimes(0);
});

it('Write to endpoint with mixed manufacturer attributes', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
mocksendZclFrameToEndpoint.mockClear();
const device = controller.getDeviceByIeeeAddr('0x129');
device._manufacturerID = 0x10f2;
const endpoint = device.getEndpoint(1);
let error;
try {await endpoint.write('hvacThermostat', {'occupiedHeatingSetpoint': 2000, 'ubisysDefaultOccupiedHeatingSetpoint': 1800}) } catch (e) {error = e}
expect(error).toStrictEqual(new Error("Cannot have attributes with different manufacturerCode in single 'write' call"))
});

it('Write response to endpoint with non ZCL attribute', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
Expand Down Expand Up @@ -3227,6 +3283,36 @@ describe('Controller', () => {
expect(call[4]).toBe(10000);
});

it('Read from endpoint with custom attribute', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
mocksendZclFrameToEndpoint.mockClear();
const device = controller.getDeviceByIeeeAddr('0x129');
device._manufacturerID = 0x10f2;
const endpoint = device.getEndpoint(1);
await endpoint.read('hvacThermostat', ['ubisysDefaultOccupiedHeatingSetpoint']);
expect(mocksendZclFrameToEndpoint).toBeCalledTimes(1);
const call = mocksendZclFrameToEndpoint.mock.calls[0];
expect(call[0]).toBe('0x129');
expect(call[1]).toBe(129);
expect(call[2]).toBe(1);
expect({...deepClone(call[3]), Cluster: {}}).toStrictEqual({"Header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":true,"manufacturerSpecific":true},"transactionSequenceNumber":11,"manufacturerCode":4338,"commandIdentifier":0},"Payload":[{"attrId":17}],"Cluster":{},"Command":{"ID":0,"name":"read","parameters":[{"name":"attrId","type":33}],"response":1}});
expect(call[4]).toBe(10000);
});

it('Read mixed manufacturer attributes from endpoint', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
mocksendZclFrameToEndpoint.mockClear();
const device = controller.getDeviceByIeeeAddr('0x129');
device._manufacturerID = 0x10f2;
const endpoint = device.getEndpoint(1);
let error;
try { await endpoint.read('hvacThermostat', ['localTemp', 'ubisysRemoteTemperature']); } catch (e) { error = e };
expect(error).toStrictEqual(new Error("Cannot have attributes with different manufacturerCode in single 'read' call"))
});


it('Read from endpoint unknown attribute with options', async () => {
await controller.start();
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
Expand Down

0 comments on commit 3b51b0b

Please sign in to comment.