Skip to content

Commit 108ceb0

Browse files
committed
feat: add sendRaw API to Controller
1 parent b4d0b81 commit 108ceb0

12 files changed

Lines changed: 240 additions & 45 deletions

File tree

src/adapter/adapter.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,13 @@ export abstract class Adapter extends events.EventEmitter<AdapterEventMap> {
188188

189189
public abstract sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddress: string): Promise<void>;
190190

191-
public abstract sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<AdapterEvents.ZclPayload>;
191+
public abstract sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<AdapterEvents.ZclPayload>;
192+
public abstract sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
193+
public abstract sendZclFrameInterPANBroadcast(
194+
zclFrame: Zcl.Frame,
195+
timeout: number,
196+
disableResponse: boolean,
197+
): Promise<AdapterEvents.ZclPayload | undefined>;
192198

193199
public abstract restoreChannelInterPAN(): Promise<void>;
194200
}

src/adapter/deconz/adapter/deconzAdapter.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,13 @@ export class DeconzAdapter extends Adapter {
639639
public async sendZclFrameInterPANToIeeeAddr(_zclFrame: Zcl.Frame, _ieeeAddr: string): Promise<void> {
640640
await Promise.reject(new Error("not supported"));
641641
}
642-
public async sendZclFrameInterPANBroadcast(_zclFrame: Zcl.Frame, _timeout: number): Promise<Events.ZclPayload> {
642+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<Events.ZclPayload>;
643+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
644+
public async sendZclFrameInterPANBroadcast(
645+
_zclFrame: Zcl.Frame,
646+
_timeout: number,
647+
_disableResponse: boolean,
648+
): Promise<Events.ZclPayload | undefined> {
643649
return await Promise.reject(new Error("not supported"));
644650
}
645651
public async sendZclFrameInterPANBroadcastWithResponse(_zclFrame: Zcl.Frame, _timeout: number): Promise<Events.ZclPayload> {

src/adapter/ember/adapter/emberAdapter.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,7 +2171,9 @@ export class EmberAdapter extends Adapter {
21712171
}
21722172

21732173
// queued
2174-
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
2174+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<ZclPayload>;
2175+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
2176+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean): Promise<ZclPayload | undefined> {
21752177
const command = zclFrame.command;
21762178

21772179
if (command.response === undefined) {
@@ -2190,7 +2192,7 @@ export class EmberAdapter extends Adapter {
21902192
sequence: 0, // set by stack
21912193
};
21922194

2193-
return await this.queue.execute<ZclPayload>(async () => {
2195+
return await this.queue.execute<ZclPayload | undefined>(async () => {
21942196
const msgBuffalo = new EzspBuffalo(Buffer.alloc(MAXIMUM_INTERPAN_LENGTH));
21952197

21962198
// cache-enabled getters
@@ -2219,17 +2221,19 @@ export class EmberAdapter extends Adapter {
22192221

22202222
// NOTE: can use ezspRawTransmitCompleteHandler if needed here
22212223

2222-
const result = await this.oneWaitress.startWaitingFor<ZclPayload>(
2223-
{
2224-
target: undefined,
2225-
apsFrame: apsFrame,
2226-
zclSequence: zclFrame.header.transactionSequenceNumber,
2227-
commandIdentifier: command.response,
2228-
},
2229-
timeout,
2230-
);
2224+
if (!disableResponse) {
2225+
const result = await this.oneWaitress.startWaitingFor<ZclPayload>(
2226+
{
2227+
target: undefined,
2228+
apsFrame: apsFrame,
2229+
zclSequence: zclFrame.header.transactionSequenceNumber,
2230+
commandIdentifier: command.response,
2231+
},
2232+
timeout,
2233+
);
22312234

2232-
return result;
2235+
return result;
2236+
}
22332237
});
22342238
}
22352239

src/adapter/ezsp/adapter/ezspAdapter.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -526,16 +526,22 @@ export class EZSPAdapter extends Adapter {
526526
});
527527
}
528528

529-
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
530-
return await this.queue.execute<ZclPayload>(async () => {
529+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<ZclPayload>;
530+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
531+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean): Promise<ZclPayload | undefined> {
532+
return await this.queue.execute<ZclPayload | undefined>(async () => {
531533
logger.debug("sendZclFrameInterPANBroadcast", NS);
532534
const command = zclFrame.command;
533535

534536
if (command.response === undefined) {
535537
throw new Error(`Command '${command.name}' has no response, cannot wait for response`);
536538
}
537539

538-
const response = this.waitForInternal(undefined, 0xfe, undefined, zclFrame.cluster.ID, command.response, timeout);
540+
let response: ReturnType<typeof this.waitForInternal> | undefined;
541+
542+
if (!disableResponse) {
543+
this.waitForInternal(undefined, 0xfe, undefined, zclFrame.cluster.ID, command.response, timeout);
544+
}
539545

540546
try {
541547
const frame = this.driver.makeEmberRawFrame();
@@ -551,11 +557,13 @@ export class EZSPAdapter extends Adapter {
551557

552558
await this.driver.rawrequest(frame, zclFrame.toBuffer());
553559
} catch (error) {
554-
response.cancel();
560+
response?.cancel();
555561
throw error;
556562
}
557563

558-
return await response.start().promise;
564+
if (response) {
565+
return await response.start().promise;
566+
}
559567
});
560568
}
561569

src/adapter/z-stack/adapter/zStackAdapter.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,23 +1062,33 @@ export class ZStackAdapter extends Adapter {
10621062
});
10631063
}
10641064

1065-
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<Events.ZclPayload> {
1066-
return await this.queue.execute<Events.ZclPayload>(async () => {
1065+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<Events.ZclPayload>;
1066+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
1067+
public async sendZclFrameInterPANBroadcast(
1068+
zclFrame: Zcl.Frame,
1069+
timeout: number,
1070+
disableResponse: boolean,
1071+
): Promise<Events.ZclPayload | undefined> {
1072+
return await this.queue.execute<Events.ZclPayload | undefined>(async () => {
10671073
const command = zclFrame.command;
10681074
if (command.response === undefined) {
10691075
throw new Error(`Command '${command.name}' has no response, cannot wait for response`);
10701076
}
10711077

1072-
const response = this.waitForInternal(
1073-
undefined,
1074-
0xfe,
1075-
zclFrame.header.frameControl.frameType,
1076-
Zcl.Direction.SERVER_TO_CLIENT,
1077-
undefined,
1078-
zclFrame.cluster.ID,
1079-
command.response,
1080-
timeout,
1081-
);
1078+
let response: ReturnType<typeof this.waitForInternal> | undefined;
1079+
1080+
if (!disableResponse) {
1081+
response = this.waitForInternal(
1082+
undefined,
1083+
0xfe,
1084+
zclFrame.header.frameControl.frameType,
1085+
Zcl.Direction.SERVER_TO_CLIENT,
1086+
undefined,
1087+
zclFrame.cluster.ID,
1088+
command.response,
1089+
timeout,
1090+
);
1091+
}
10821092

10831093
try {
10841094
await this.dataRequestExtended(
@@ -1094,11 +1104,13 @@ export class ZStackAdapter extends Adapter {
10941104
false,
10951105
);
10961106
} catch (error) {
1097-
response.cancel();
1107+
response?.cancel();
10981108
throw error;
10991109
}
11001110

1101-
return await response.start().promise;
1111+
if (response) {
1112+
return await response.start().promise;
1113+
}
11021114
});
11031115
}
11041116

src/adapter/zboss/adapter/zbossAdapter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,12 @@ export class ZBOSSAdapter extends Adapter {
464464
return;
465465
}
466466

467-
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
468-
return await Promise.reject(new Error(`NOT SUPPORTED: sendZclFrameInterPANBroadcast(${JSON.stringify(zclFrame)},${timeout})`));
467+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<ZclPayload>;
468+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
469+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean): Promise<ZclPayload | undefined> {
470+
return await Promise.reject(
471+
new Error(`NOT SUPPORTED: sendZclFrameInterPANBroadcast(${JSON.stringify(zclFrame)},${timeout},${disableResponse})`),
472+
);
469473
}
470474

471475
public async restoreChannelInterPAN(): Promise<void> {

src/adapter/zigate/adapter/zigateAdapter.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,13 @@ export class ZiGateAdapter extends Adapter {
550550
public async sendZclFrameInterPANToIeeeAddr(_zclFrame: Zcl.Frame, _ieeeAddress: string): Promise<void> {
551551
await Promise.reject(new Error("Not supported"));
552552
}
553-
public async sendZclFrameInterPANBroadcast(_zclFrame: Zcl.Frame, _timeout: number): Promise<Events.ZclPayload> {
553+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<Events.ZclPayload>;
554+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
555+
public async sendZclFrameInterPANBroadcast(
556+
_zclFrame: Zcl.Frame,
557+
_timeout: number,
558+
_disableResponse: boolean,
559+
): Promise<Events.ZclPayload | undefined> {
554560
return await Promise.reject(new Error("Not supported"));
555561
}
556562

src/adapter/zoh/adapter/zohAdapter.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,8 +724,10 @@ export class ZoHAdapter extends Adapter {
724724
/* v8 ignore stop */
725725

726726
/* v8 ignore start */
727-
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
728-
return await Promise.reject(new Error(`not supported ${JSON.stringify(zclFrame)}, ${timeout}`));
727+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: false): Promise<ZclPayload>;
728+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: true): Promise<undefined>;
729+
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean): Promise<ZclPayload | undefined> {
730+
return await Promise.reject(new Error(`not supported ${JSON.stringify(zclFrame)}, ${timeout}, ${disableResponse}`));
729731
}
730732
/* v8 ignore stop */
731733

src/controller/controller.ts

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@ import * as ZSpec from "../zspec";
1010
import type {Eui64} from "../zspec/tstypes";
1111
import * as Zcl from "../zspec/zcl";
1212
import type {TPartialClusterAttributes} from "../zspec/zcl/definition/clusters-types";
13-
import type {FrameControl} from "../zspec/zcl/definition/tstype";
13+
import type {CustomClusters, FrameControl} from "../zspec/zcl/definition/tstype";
1414
import * as Zdo from "../zspec/zdo";
1515
import type * as ZdoTypes from "../zspec/zdo/definition/tstypes";
1616
import Database from "./database";
1717
import type * as Events from "./events";
1818
import GreenPower from "./greenPower";
1919
import {ZclFrameConverter} from "./helpers";
2020
import {checkInstallCode, parseInstallCode} from "./helpers/installCodes";
21+
import zclTransactionSequenceNumber from "./helpers/zclTransactionSequenceNumber";
2122
import {Device, Entity} from "./model";
2223
import {InterviewState} from "./model/device";
2324
import Group from "./model/group";
2425
import Touchlink from "./touchlink";
25-
import type {DeviceType, GreenPowerDeviceJoinedPayload} from "./tstype";
26+
import type {DeviceType, GreenPowerDeviceJoinedPayload, RawPayload} from "./tstype";
2627

2728
const NS = "zh:controller";
2829

@@ -229,6 +230,113 @@ export class Controller extends events.EventEmitter<ControllerEventMap> {
229230
return startResult;
230231
}
231232

233+
/**
234+
* Send a request according to given payload.
235+
* @param rawPayload Payload used to determine and build the request
236+
* @param customClusters Manually passed custom clusters used in ZCL serialization (if any, if matching)
237+
* @returns A response may or may not be returned depending on given payload (up to caller to verify)
238+
*/
239+
public async sendRaw(rawPayload: RawPayload, customClusters: CustomClusters = {}): Promise<unknown> {
240+
const {
241+
ieeeAddress,
242+
networkAddress,
243+
groupId,
244+
dstEndpoint,
245+
srcEndpoint = ZSpec.HA_ENDPOINT,
246+
interPan = false,
247+
profileId = ZSpec.HA_PROFILE_ID,
248+
clusterKey,
249+
zdoArgs,
250+
zcl,
251+
disableResponse = false,
252+
timeout = 10000,
253+
} = rawPayload;
254+
255+
if (profileId === Zdo.ZDO_PROFILE_ID) {
256+
assert(ieeeAddress);
257+
assert(networkAddress !== undefined);
258+
assert(clusterKey !== undefined && typeof clusterKey === "number");
259+
260+
// will fail if args are incorrect for request
261+
const buf = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterKey, ...(zdoArgs ?? []));
262+
263+
return await this.adapter.sendZdo(ieeeAddress, networkAddress, clusterKey, buf, disableResponse);
264+
}
265+
266+
assert(zcl);
267+
268+
if (interPan) {
269+
assert(zcl.commandKey);
270+
assert(zcl.payload);
271+
assert(zcl.frameType === undefined || zcl.frameType === Zcl.FrameType.GLOBAL || zcl.frameType === Zcl.FrameType.SPECIFIC);
272+
assert(
273+
zcl.direction === undefined || zcl.direction === Zcl.Direction.CLIENT_TO_SERVER || zcl.direction === Zcl.Direction.SERVER_TO_CLIENT,
274+
);
275+
276+
const zclFrame = Zcl.Frame.create(
277+
zcl.frameType ?? Zcl.FrameType.SPECIFIC,
278+
zcl.direction ?? Zcl.Direction.CLIENT_TO_SERVER,
279+
true,
280+
zcl.manufacturerCode,
281+
0,
282+
zcl.commandKey,
283+
clusterKey ?? Zcl.Clusters.touchlink.ID,
284+
zcl.payload,
285+
customClusters,
286+
);
287+
288+
return ieeeAddress
289+
? await this.adapter.sendZclFrameInterPANToIeeeAddr(zclFrame, ieeeAddress)
290+
: await this.adapter.sendZclFrameInterPANBroadcast(zclFrame, timeout, disableResponse);
291+
}
292+
293+
assert(clusterKey !== undefined);
294+
assert(zcl.frameType !== undefined);
295+
assert(zcl.direction !== undefined);
296+
297+
const zclFrame = Zcl.Frame.create(
298+
zcl.frameType,
299+
zcl.direction,
300+
zcl.disableDefaultResponse ?? false,
301+
zcl.manufacturerCode,
302+
zcl.tsn ?? zclTransactionSequenceNumber.next(),
303+
zcl.commandKey,
304+
clusterKey,
305+
zcl.payload,
306+
customClusters,
307+
);
308+
309+
if (groupId !== undefined) {
310+
assert(groupId >= 0x0000 && groupId <= 0xffff);
311+
312+
return await this.adapter.sendZclFrameToGroup(groupId, zclFrame, srcEndpoint, profileId);
313+
}
314+
315+
if (networkAddress !== undefined && networkAddress >= ZSpec.BROADCAST_MIN) {
316+
assert(dstEndpoint !== undefined && dstEndpoint >= 0x01 && dstEndpoint <= 0xff);
317+
318+
return await this.adapter.sendZclFrameToAll(dstEndpoint, zclFrame, srcEndpoint, networkAddress, profileId);
319+
}
320+
321+
if (ieeeAddress && dstEndpoint !== undefined) {
322+
assert(networkAddress !== undefined && networkAddress >= 0x0001 && networkAddress <= ZSpec.BROADCAST_MIN - 1);
323+
324+
return await this.adapter.sendZclFrameToEndpoint(
325+
ieeeAddress,
326+
networkAddress,
327+
dstEndpoint,
328+
zclFrame,
329+
timeout,
330+
disableResponse,
331+
false,
332+
srcEndpoint,
333+
profileId,
334+
);
335+
}
336+
337+
throw new Error("Invalid raw payload");
338+
}
339+
232340
public async touchlinkIdentify(ieeeAddr: string, channel: number): Promise<void> {
233341
await this.touchlink.identify(ieeeAddr, channel);
234342
}

0 commit comments

Comments
 (0)