Skip to content

Commit ebb4385

Browse files
ccutrerNerivec
authored andcommitted
avoid calculating group custom clusters at all if unnecessary
1 parent 985a863 commit ebb4385

File tree

3 files changed

+115
-30
lines changed

3 files changed

+115
-30
lines changed

src/controller/model/group.ts

+28-16
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert from 'node:assert';
22

33
import {logger} from '../../utils/logger';
44
import * as Zcl from '../../zspec/zcl';
5-
import {CustomClusters} from '../../zspec/zcl/definition/tstype';
5+
import {Cluster, CustomClusters} from '../../zspec/zcl/definition/tstype';
66
import ZclTransactionSequenceNumber from '../helpers/zclTransactionSequenceNumber';
77
import {DatabaseEntry, KeyValue} from '../tstype';
88
import Device from './device';
@@ -186,7 +186,7 @@ export class Group extends Entity {
186186

187187
public async write(clusterKey: number | string, attributes: KeyValue, options?: Options): Promise<void> {
188188
const optionsWithDefaults = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER);
189-
const cluster = Zcl.Utils.getCluster(clusterKey, undefined, this.customClusters);
189+
const cluster = this.getCluster(clusterKey);
190190
const payload: {attrId: number; dataType: number; attrData: number | string | boolean}[] = [];
191191

192192
for (const [nameOrID, value] of Object.entries(attributes)) {
@@ -214,7 +214,7 @@ export class Group extends Entity {
214214
'write',
215215
cluster.ID,
216216
payload,
217-
this.customClusters,
217+
this._customClusters ?? {},
218218
optionsWithDefaults.reservedBits,
219219
);
220220

@@ -230,7 +230,7 @@ export class Group extends Entity {
230230

231231
public async read(clusterKey: number | string, attributes: (string | number)[], options?: Options): Promise<void> {
232232
const optionsWithDefaults = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER);
233-
const cluster = Zcl.Utils.getCluster(clusterKey, undefined, this.customClusters);
233+
const cluster = this.getCluster(clusterKey);
234234
const payload: {attrId: number}[] = [];
235235

236236
for (const attribute of attributes) {
@@ -246,7 +246,7 @@ export class Group extends Entity {
246246
'read',
247247
cluster.ID,
248248
payload,
249-
this.customClusters,
249+
this._customClusters ?? {},
250250
optionsWithDefaults.reservedBits,
251251
);
252252

@@ -267,7 +267,7 @@ export class Group extends Entity {
267267

268268
public async command(clusterKey: number | string, commandKey: number | string, payload: KeyValue, options?: Options): Promise<void> {
269269
const optionsWithDefaults = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER);
270-
const cluster = Zcl.Utils.getCluster(clusterKey, undefined, this.customClusters);
270+
const cluster = this.getCluster(clusterKey);
271271
const command = cluster.getCommand(commandKey);
272272

273273
const createLogMessage = (): string => `Command ${this.groupID} ${cluster.name}.${command.name}(${JSON.stringify(payload)})`;
@@ -283,7 +283,7 @@ export class Group extends Entity {
283283
command.ID,
284284
cluster.ID,
285285
payload,
286-
this.customClusters,
286+
this._customClusters ?? {},
287287
optionsWithDefaults.reservedBits,
288288
);
289289

@@ -309,15 +309,11 @@ export class Group extends Entity {
309309
}
310310

311311
/**
312-
* Get custom clusters that all members share.
312+
* Calculate, store, and return custom clusters that all members share.
313313
*/
314-
get customClusters(): CustomClusters {
315-
if (this._customClusters) {
316-
return this._customClusters;
317-
}
318-
314+
private calculateCustomClusters(): CustomClusters {
319315
if (this._members.size === 0) {
320-
return {};
316+
return (this._customClusters = {});
321317
}
322318

323319
const membersArray = Array.from(this._members);
@@ -331,8 +327,24 @@ export class Group extends Entity {
331327
}
332328
}
333329

334-
this._customClusters = commonClusters;
335-
return this._customClusters;
330+
return (this._customClusters = commonClusters);
331+
}
332+
333+
private getCluster(key: string | number): Cluster {
334+
if (this._customClusters) {
335+
return Zcl.Utils.getCluster(key, undefined, this._customClusters);
336+
}
337+
338+
// At first, don't fully calculate custom clusters
339+
const cluster = Zcl.Utils.findCluster(key, undefined, {});
340+
341+
// If no cluster was found, and we haven't calculated custom clusters,
342+
// do so now, and then retry
343+
if (!cluster) {
344+
return Zcl.Utils.getCluster(key, undefined, this.calculateCustomClusters());
345+
}
346+
347+
return cluster;
336348
}
337349
}
338350

src/zspec/zcl/utils.ts

+33-14
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function getClusterDefinition(
120120
key: string | number,
121121
manufacturerCode: number | undefined,
122122
customClusters: CustomClusters,
123-
): {name: string; cluster: ClusterDefinition} {
123+
): {name: string; cluster: ClusterDefinition} | undefined {
124124
let name: string | undefined;
125125

126126
if (typeof key === 'number') {
@@ -144,25 +144,23 @@ function getClusterDefinition(
144144
name = key;
145145
}
146146

147-
let cluster =
148-
name !== undefined && hasCustomClusters(customClusters)
147+
const hasCustomClustersResult = hasCustomClusters(customClusters);
148+
const cluster =
149+
name !== undefined && hasCustomClustersResult
149150
? {
150151
...Clusters[name as ClusterName],
151152
...customClusters[name], // should override Zcl clusters
152153
}
153154
: Clusters[name as ClusterName];
154155

155-
if (!cluster) {
156-
if (typeof key === 'number') {
157-
name = key.toString();
158-
cluster = {attributes: {}, commands: {}, commandsResponse: {}, manufacturerCode: undefined, ID: key};
159-
} else {
160-
name = undefined;
161-
}
156+
if (!name || !cluster) {
157+
return undefined;
162158
}
163-
164-
if (!name) {
165-
throw new Error(`Cluster with name '${key}' does not exist`);
159+
// If we have customClusters, we have to double check that we didn't end up with
160+
// an empty object due to the use of the spread operator above
161+
if (hasCustomClustersResult) {
162+
for (const k in cluster) return {name, cluster};
163+
return undefined;
166164
}
167165

168166
return {name, cluster};
@@ -281,7 +279,28 @@ function createCluster(name: string, cluster: ClusterDefinition, manufacturerCod
281279
}
282280

283281
export function getCluster(key: string | number, manufacturerCode: number | undefined, customClusters: CustomClusters): Cluster {
284-
const {name, cluster} = getClusterDefinition(key, manufacturerCode, customClusters);
282+
let nameAndCluster = getClusterDefinition(key, manufacturerCode, customClusters);
283+
284+
if (!nameAndCluster) {
285+
if (typeof key === 'number') {
286+
const name = key.toString();
287+
const cluster = {attributes: {}, commands: {}, commandsResponse: {}, manufacturerCode: undefined, ID: key};
288+
nameAndCluster = {name, cluster};
289+
} else {
290+
throw new Error(`Cluster with name '${key}' does not exist`);
291+
}
292+
}
293+
294+
const {name, cluster} = nameAndCluster;
295+
return createCluster(name, cluster, manufacturerCode);
296+
}
297+
298+
export function findCluster(key: string | number, manufacturerCode: number | undefined, customClusters: CustomClusters): Cluster | undefined {
299+
const nameAndCluster = getClusterDefinition(key, manufacturerCode, customClusters);
300+
if (!nameAndCluster) {
301+
return undefined;
302+
}
303+
const {name, cluster} = nameAndCluster;
285304
return createCluster(name, cluster, manufacturerCode);
286305
}
287306

test/controller.test.ts

+54
Original file line numberDiff line numberDiff line change
@@ -5715,6 +5715,60 @@ describe('Controller', () => {
57155715
),
57165716
);
57175717
expect(mocksendZclFrameToGroup.mock.calls[0][2]).toBeUndefined();
5718+
// Do another write, to ensure customClusters was cached
5719+
await group.write('myCustomCluster', {superAttribute: 3}, {});
5720+
expect(mocksendZclFrameToGroup).toHaveBeenCalledTimes(2);
5721+
expect(mocksendZclFrameToGroup.mock.calls[0][0]).toBe(2);
5722+
expect(deepClone(mocksendZclFrameToGroup.mock.calls[1][1])).toStrictEqual(
5723+
deepClone(
5724+
Zcl.Frame.create(
5725+
Zcl.FrameType.GLOBAL,
5726+
Zcl.Direction.CLIENT_TO_SERVER,
5727+
true,
5728+
undefined,
5729+
12,
5730+
'write',
5731+
9123,
5732+
[{attrData: 3, attrId: 0, dataType: 32}],
5733+
device.customClusters,
5734+
),
5735+
),
5736+
);
5737+
expect(mocksendZclFrameToGroup.mock.calls[1][2]).toBeUndefined();
5738+
});
5739+
5740+
it('Write to empty group with custom cluster should fail', async () => {
5741+
await controller.start();
5742+
const group = await controller.createGroup(2);
5743+
let error;
5744+
try {
5745+
await group.write('myCustomCluster', {superAttribute: 5}, {});
5746+
} catch (e) {
5747+
error = e;
5748+
}
5749+
expect(error).toStrictEqual(new Error(`Cluster with name 'myCustomCluster' does not exist`));
5750+
});
5751+
5752+
it('Write to group with unsupported custom cluster should fail', async () => {
5753+
await controller.start();
5754+
5755+
await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'});
5756+
const device = controller.getDeviceByIeeeAddr('0x129')!;
5757+
device.addCustomCluster('myCustomCluster', {
5758+
ID: 9123,
5759+
commands: {},
5760+
commandsResponse: {},
5761+
attributes: {superAttribute: {ID: 0, type: Zcl.DataType.UINT8}},
5762+
});
5763+
const group = await controller.createGroup(2);
5764+
group.addMember(device.getEndpoint(1)!);
5765+
let error;
5766+
try {
5767+
await group.write('otherCustomCluster', {superAttribute: 5}, {});
5768+
} catch (e) {
5769+
error = e;
5770+
}
5771+
expect(error).toStrictEqual(new Error(`Cluster with name 'otherCustomCluster' does not exist`));
57185772
});
57195773

57205774
it('Write to group with unknown attribute should fail', async () => {

0 commit comments

Comments
 (0)