Skip to content

Commit

Permalink
feat: Add zclCommand (#978)
Browse files Browse the repository at this point in the history
* Description of additional discover commands.
Allow general commands to be sent through the endpoint.

* Simple generalCommand test for coverage

* Simple generalCommand with error test for coverage

* Rewritten calls to use zclCommand

* fix test

* fix!: Remove `skipTimeResponse` in favour `customReadResponse` (#984)

* Logging and error handling has been moved to zclCommand

* Update endpoint.ts

* Pass the command parameters in a readable form to logPayload

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
  • Loading branch information
kirovilya and Koenkk authored Mar 21, 2024
1 parent 3152080 commit b688219
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 219 deletions.
265 changes: 68 additions & 197 deletions src/controller/model/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,6 @@ class Endpoint extends Entity {
clusterKey: number | string, attributes: KeyValue, options?: Options
): Promise<void> {
const cluster = Zcl.Utils.getCluster(clusterKey);
options = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
const payload: {attrId: number; dataType: number; attrData: number| string | boolean}[] = [];
for (const [nameOrID, value] of Object.entries(attributes)) {
if (cluster.hasAttribute(nameOrID)) {
Expand All @@ -336,23 +335,7 @@ class Endpoint extends Entity {
}
}

const log = `Report to ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}(${JSON.stringify(attributes)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(),
"report", cluster.ID, payload, options.reservedBits
);

await this.sendRequest(frame, options);
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}
await this.zclCommand(clusterKey, "report", payload, options, attributes);
}

public async write(
Expand All @@ -375,35 +358,15 @@ class Endpoint extends Entity {
throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
}
}

const log = `Write ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}(${JSON.stringify(attributes)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(),
options.writeUndiv ? "writeUndiv" : "write", cluster.ID, payload, options.reservedBits
);

const result = await this.sendRequest(frame, options);
if (!options.disableResponse) {
this.checkStatus(result.frame.Payload);
}
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}

await this.zclCommand(clusterKey, options.writeUndiv ? "writeUndiv" : "write", payload, options, attributes, true);
}

public async writeResponse(
clusterKey: number | string, transactionSequenceNumber: number, attributes: KeyValue, options?: Options
): Promise<void> {
assert(!options || !options.hasOwnProperty('transactionSequenceNumber'), 'Use parameter');
const cluster = Zcl.Utils.getCluster(clusterKey);
options = this.getOptionsWithDefaults(options, true, Zcl.Direction.SERVER_TO_CLIENT, cluster.manufacturerCode);
const payload: {status: number; attrId: number}[] = [];
for (const [nameOrID, value] of Object.entries(attributes)) {
if (value.hasOwnProperty('status')) {
Expand All @@ -420,22 +383,8 @@ class Endpoint extends Entity {
}
}

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, transactionSequenceNumber, 'writeRsp', cluster.ID, payload, options.reservedBits
);

const log = `WriteResponse ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}(${JSON.stringify(attributes)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
await this.sendRequest(frame, options);
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}
await this.zclCommand(clusterKey, 'writeRsp', payload,
{direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber}, attributes);
}

public async read(
Expand All @@ -452,38 +401,21 @@ class Endpoint extends Entity {
payload.push({attrId: typeof attribute === 'number' ? attribute : cluster.getAttribute(attribute).ID});
}

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(), 'read',
cluster.ID, payload, options.reservedBits
);
const result = await this.zclCommand(clusterKey, 'read', payload, options, attributes, true);

const log = `Read ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}(${JSON.stringify(attributes)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
const result = await this.sendRequest(frame, options);

if (!options.disableResponse) {
this.checkStatus(result.frame.Payload);
return ZclFrameConverter.attributeKeyValue(result.frame, this.getDevice().manufacturerID);
} else {
return null;
}
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
if (result) {
return ZclFrameConverter.attributeKeyValue(result.frame, this.getDevice().manufacturerID);
} else {
return null;
}
}

public async readResponse(
clusterKey: number | string, transactionSequenceNumber: number, attributes: KeyValue, options?: Options
): Promise<void> {
assert(!options || !options.hasOwnProperty('transactionSequenceNumber'), 'Use parameter');

const cluster = Zcl.Utils.getCluster(clusterKey);
options = this.getOptionsWithDefaults(options, true, Zcl.Direction.SERVER_TO_CLIENT, cluster.manufacturerCode);
const payload: {attrId: number; status: number; dataType: number; attrData: number | string}[] = [];
for (const [nameOrID, value] of Object.entries(attributes)) {
if (cluster.hasAttribute(nameOrID)) {
Expand All @@ -496,22 +428,8 @@ class Endpoint extends Entity {
}
}

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, transactionSequenceNumber, 'readRsp', cluster.ID, payload, options.reservedBits
);

const log = `ReadResponse ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}(${JSON.stringify(attributes)}, ${JSON.stringify({...options, transactionSequenceNumber})})`;
debug.info(log);

try {
await this.sendRequest(frame, options);
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}
await this.zclCommand(clusterKey, 'readRsp', payload,
{direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber}, attributes);
}

public addBinding(clusterKey: number | string, target: Endpoint | Group | number): void {
Expand Down Expand Up @@ -602,24 +520,9 @@ class Endpoint extends Entity {
commandID: number, status: number, clusterID: number, transactionSequenceNumber: number, options?: Options
): Promise<void> {
assert(!options || !options.hasOwnProperty('transactionSequenceNumber'), 'Use parameter');
options = this.getOptionsWithDefaults(options, true, Zcl.Direction.SERVER_TO_CLIENT, null);
const payload = {cmdId: commandID, statusCode: status};
const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, transactionSequenceNumber, 'defaultRsp', clusterID, payload, options.reservedBits
);

const log = `DefaultResponse ${this.deviceIeeeAddress}/${this.ID} ` +
`${clusterID}(${commandID}, ${JSON.stringify(options)})`;
debug.info(log);

try {
await this.sendRequest(frame, options);
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}
await this.zclCommand(clusterID, 'defaultRsp', payload,
{direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber});
}

public async configureReporting(
Expand Down Expand Up @@ -655,103 +558,39 @@ class Endpoint extends Entity {
};
});

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(),
'configReport', cluster.ID, payload, options.reservedBits
);

const log = `ConfigureReporting ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}(${JSON.stringify(items)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
const result = await this.sendRequest(frame, options);

if (!options.disableResponse) {
this.checkStatus(result.frame.Payload);
}
await this.zclCommand(clusterKey, 'configReport', payload, options, items, true);

for (const e of payload) {
this._configuredReportings = this._configuredReportings.filter((c) => !(
c.attrId === e.attrId && c.cluster === cluster.ID &&
(!('manufacturerCode' in c) || c.manufacturerCode === options.manufacturerCode)
));
}
for (const e of payload) {
this._configuredReportings = this._configuredReportings.filter((c) => !(
c.attrId === e.attrId && c.cluster === cluster.ID &&
(!('manufacturerCode' in c) || c.manufacturerCode === options.manufacturerCode)
));
}

for (const entry of payload) {
if (entry.maxRepIntval !== 0xFFFF) {
this._configuredReportings.push({
cluster: cluster.ID, attrId: entry.attrId, minRepIntval: entry.minRepIntval,
maxRepIntval: entry.maxRepIntval, repChange: entry.repChange,
manufacturerCode: options.manufacturerCode,
});
}
for (const entry of payload) {
if (entry.maxRepIntval !== 0xFFFF) {
this._configuredReportings.push({
cluster: cluster.ID, attrId: entry.attrId, minRepIntval: entry.minRepIntval,
maxRepIntval: entry.maxRepIntval, repChange: entry.repChange,
manufacturerCode: options.manufacturerCode,
});
}

this.save();
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}

this.save();
}

public async writeStructured(clusterKey: number | string, payload: KeyValue, options?: Options): Promise<void> {
const cluster = Zcl.Utils.getCluster(clusterKey);
options = this.getOptionsWithDefaults(
options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.GLOBAL, options.direction, options.disableDefaultResponse,
options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(),
`writeStructured`, cluster.ID, payload, options.reservedBits
);

const log = `WriteStructured ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}(${JSON.stringify(payload)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
await this.sendRequest(frame, options);

// TODO: support `writeStructuredResponse`
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}
await this.zclCommand(clusterKey, 'writeStructured', payload, options);
// TODO: support `writeStructuredResponse`
}

public async command(
clusterKey: number | string, commandKey: number | string, payload: KeyValue, options?: Options,
): Promise<void | KeyValue> {
const cluster = Zcl.Utils.getCluster(clusterKey);
const command = cluster.getCommand(commandKey);
const hasResponse = command.hasOwnProperty('response');
options = this.getOptionsWithDefaults(
options, hasResponse, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, options.direction, options.disableDefaultResponse,
options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(),
command.name, cluster.name, payload, options.reservedBits
);

const log = `Command ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}.${command.name}(${JSON.stringify(payload)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
const result = await this.sendRequest(frame, options);

if (result) {
return result.frame.Payload;
}
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
const result = await this.zclCommand(clusterKey, commandKey, payload, options, null, false, Zcl.FrameType.SPECIFIC);
if (result) {
return result.frame.Payload;
}
}

Expand Down Expand Up @@ -897,6 +736,38 @@ class Endpoint extends Entity {
}
}
}

public async zclCommand(
clusterKey: number | string, commandKey: number | string, payload: KeyValue, options?: Options,
logPayload?: KeyValue, checkStatus: boolean = false, frameType: Zcl.FrameType = Zcl.FrameType.GLOBAL
): Promise<void | AdapterEvents.ZclDataPayload> {
const cluster = Zcl.Utils.getCluster(clusterKey);
const command = (frameType == Zcl.FrameType.GLOBAL) ? Zcl.Utils.getGlobalCommand(commandKey) : cluster.getCommand(commandKey);
const hasResponse = (frameType == Zcl.FrameType.GLOBAL) ? true : command.hasOwnProperty('response');
options = this.getOptionsWithDefaults(options, hasResponse, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);

const frame = Zcl.ZclFrame.create(
frameType, options.direction, options.disableDefaultResponse,
options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(),
command.name, cluster.name, payload, options.reservedBits
);

const log = `ZCL command ${this.deviceIeeeAddress}/${this.ID} ` +
`${cluster.name}.${command.name}(${JSON.stringify((logPayload) ? logPayload : payload)}, ${JSON.stringify(options)})`;
debug.info(log);

try {
const result = await this.sendRequest(frame, options);
if (result && checkStatus && !options.disableResponse) {
this.checkStatus(result.frame.Payload);
}
return result;
} catch (error) {
error.message = `${log} failed (${error.message})`;
debug.error(error.message);
throw error;
}
}
}

export default Endpoint;
Loading

0 comments on commit b688219

Please sign in to comment.