Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ export abstract class Adapter extends events.EventEmitter<AdapterEventMap> {

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

public abstract sendZclFrameInterPANBroadcastWithoutResponse(zclFrame: Zcl.Frame): Promise<void>;

public abstract sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<AdapterEvents.ZclPayload>;

public abstract restoreChannelInterPAN(): Promise<void>;
Expand Down
4 changes: 2 additions & 2 deletions src/adapter/deconz/adapter/deconzAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,10 +647,10 @@ export class DeconzAdapter extends Adapter {
public async sendZclFrameInterPANToIeeeAddr(_zclFrame: Zcl.Frame, _ieeeAddr: string): Promise<void> {
await Promise.reject(new Error("not supported"));
}
public async sendZclFrameInterPANBroadcast(_zclFrame: Zcl.Frame, _timeout: number): Promise<Events.ZclPayload> {
public async sendZclFrameInterPANBroadcastWithoutResponse(_zclFrame: Zcl.Frame): Promise<void> {
return await Promise.reject(new Error("not supported"));
}
public async sendZclFrameInterPANBroadcastWithResponse(_zclFrame: Zcl.Frame, _timeout: number): Promise<Events.ZclPayload> {
public async sendZclFrameInterPANBroadcast(_zclFrame: Zcl.Frame, _timeout: number): Promise<Events.ZclPayload> {
return await Promise.reject(new Error("not supported"));
}
public async setChannelInterPAN(_channel: number): Promise<void> {
Expand Down
35 changes: 35 additions & 0 deletions src/adapter/ember/adapter/emberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,41 @@ export class EmberAdapter extends Adapter {
});
}

// queued
public async sendZclFrameInterPANBroadcastWithoutResponse(zclFrame: Zcl.Frame): Promise<void> {
return await this.queue.execute<void>(async () => {
const msgBuffalo = new EzspBuffalo(Buffer.alloc(MAXIMUM_INTERPAN_LENGTH));

// cache-enabled getters
const sourcePanId = await this.emberGetPanId();
const sourceEui64 = await this.emberGetEui64();

msgBuffalo.writeUInt16(SHORT_DEST_FRAME_CONTROL); // macFrameControl
msgBuffalo.writeUInt8(0); // sequence Skip Sequence number, stack sets the sequence number.
msgBuffalo.writeUInt16(ZSpec.INVALID_PAN_ID); // destPanId
msgBuffalo.writeUInt16(ZSpec.BroadcastAddress.SLEEPY); // destAddress (longAddress)
msgBuffalo.writeUInt16(sourcePanId); // sourcePanId
msgBuffalo.writeIeeeAddr(sourceEui64); // sourceAddress
msgBuffalo.writeUInt16(STUB_NWK_FRAME_CONTROL); // nwkFrameControl
msgBuffalo.writeUInt8(EmberInterpanMessageType.BROADCAST | INTERPAN_APS_FRAME_TYPE); // apsFrameControl
msgBuffalo.writeUInt16(zclFrame.cluster.ID);
msgBuffalo.writeUInt16(ZSpec.TOUCHLINK_PROFILE_ID);

logger.debug(() => `~~~> [ZCL TOUCHLINK BROADCAST NOREPLY header=${JSON.stringify(zclFrame.header)}]`, NS);
const status = await this.ezsp.ezspSendRawMessage(
Buffer.concat([msgBuffalo.getWritten(), zclFrame.toBuffer()]),
EmberTransmitPriority.NORMAL,
true,
);

if (status !== SLStatus.OK) {
throw new Error(`~x~> [ZCL TOUCHLINK BROADCAST NOREPLY] Failed to send with status=${SLStatus[status]}.`);
}

// NOTE: can use ezspRawTransmitCompleteHandler if needed here
});
}

// queued
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
const command = zclFrame.command;
Expand Down
19 changes: 19 additions & 0 deletions src/adapter/ezsp/adapter/ezspAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,25 @@ export class EZSPAdapter extends Adapter {
});
}

public async sendZclFrameInterPANBroadcastWithoutResponse(zclFrame: Zcl.Frame): Promise<void> {
return await this.queue.execute<void>(async () => {
logger.debug("sendZclFrameInterPANBroadcastWithoutResponse", NS);

const frame = this.driver.makeEmberRawFrame();
frame.ieeeFrameControl = 0xc801;
frame.destPanId = 0xffff;
frame.destNodeId = 0xffff;
frame.sourcePanId = this.driver.networkParams.panId;
frame.ieeeAddress = this.driver.ieee;
frame.nwkFrameControl = 0x000b;
frame.appFrameControl = 0x0b;
frame.clusterId = zclFrame.cluster.ID;
frame.profileId = 0xc05e;

await this.driver.rawrequest(frame, zclFrame.toBuffer());
});
}

public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
return await this.queue.execute<ZclPayload>(async () => {
logger.debug("sendZclFrameInterPANBroadcast", NS);
Expand Down
17 changes: 17 additions & 0 deletions src/adapter/z-stack/adapter/zStackAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,23 @@ export class ZStackAdapter extends Adapter {
});
}

public async sendZclFrameInterPANBroadcastWithoutResponse(zclFrame: Zcl.Frame): Promise<void> {
return await this.queue.execute<void>(async () => {
await this.dataRequestExtended(
AddressMode.ADDR_16BIT,
0xffff,
0xfe,
0xffff,
12,
zclFrame.cluster.ID,
30,
zclFrame.toBuffer(),
10000,
false,
);
});
}

public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<Events.ZclPayload> {
return await this.queue.execute<Events.ZclPayload>(async () => {
const command = zclFrame.command;
Expand Down
4 changes: 4 additions & 0 deletions src/adapter/zboss/adapter/zbossAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,10 @@ export class ZBOSSAdapter extends Adapter {
return;
}

public async sendZclFrameInterPANBroadcastWithoutResponse(zclFrame: Zcl.Frame): Promise<void> {
return await Promise.reject(new Error(`NOT SUPPORTED: sendZclFrameInterPANBroadcastWithoutResponse(${JSON.stringify(zclFrame)})`));
}

public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
return await Promise.reject(new Error(`NOT SUPPORTED: sendZclFrameInterPANBroadcast(${JSON.stringify(zclFrame)},${timeout})`));
}
Expand Down
3 changes: 3 additions & 0 deletions src/adapter/zigate/adapter/zigateAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,9 @@ export class ZiGateAdapter extends Adapter {
public async sendZclFrameInterPANToIeeeAddr(_zclFrame: Zcl.Frame, _ieeeAddress: string): Promise<void> {
await Promise.reject(new Error("Not supported"));
}
public async sendZclFrameInterPANBroadcastWithoutResponse(_zclFrame: Zcl.Frame): Promise<void> {
return await Promise.reject(new Error("Not supported"));
}
public async sendZclFrameInterPANBroadcast(_zclFrame: Zcl.Frame, _timeout: number): Promise<Events.ZclPayload> {
return await Promise.reject(new Error("Not supported"));
}
Expand Down
6 changes: 6 additions & 0 deletions src/adapter/zoh/adapter/zohAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,12 @@ export class ZoHAdapter extends Adapter {
}
/* v8 ignore stop */

/* v8 ignore start */
public async sendZclFrameInterPANBroadcastWithoutResponse(zclFrame: Zcl.Frame): Promise<void> {
return await Promise.reject(new Error(`not supported ${JSON.stringify(zclFrame)}`));
}
/* v8 ignore stop */

/* v8 ignore start */
public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise<ZclPayload> {
return await Promise.reject(new Error(`not supported ${JSON.stringify(zclFrame)}, ${timeout}`));
Expand Down
11 changes: 11 additions & 0 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ export class Controller extends events.EventEmitter<ControllerEventMap> {
return await this.touchlink.factoryResetFirst();
}

public async touchlinkFactoryResetHue(serialNumbers: number[]): Promise<void> {
const extendedPanID = this.options.network.extendedPanID;
if (!extendedPanID) {
throw new Error("Must supply an extendedPanID to use touchlinkFactoryResetHue");
}
if (!serialNumbers.length) {
throw new Error("An empty list of serial numbers was supplied to touchlinkFactoryResetHue");
}
return await this.touchlink.factoryResetHue(`0x${Buffer.from(extendedPanID).toString("hex")}`, serialNumbers);
}

public async addInstallCode(installCode: string): Promise<void> {
// will throw if code cannot be parsed
const [ieeeAddr, keyStr] = parseInstallCode(installCode);
Expand Down
42 changes: 42 additions & 0 deletions src/controller/touchlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,48 @@ export class Touchlink {
return done;
}

public async factoryResetHue(extendedPanID: string, serialNumbers: number[]): Promise<void> {
this.lock(true);

try {
for (const channel of scanChannels) {
logger.info(`Set InterPAN channel to '${channel}'`, NS);
await this.adapter.setChannelInterPAN(channel);

try {
await this.adapter.sendZclFrameInterPANBroadcastWithoutResponse(this.createHueResetRequestFrame(extendedPanID, serialNumbers));

// Try not to completely flood the airspace
await wait(1000);
} catch (error) {
logger.warning(`Hue reset request failed to send: '${error}'`, NS);
}
}
} finally {
logger.info("Restore InterPAN channel", NS);
await this.adapter.restoreChannelInterPAN();
this.lock(false);
}
}

private createHueResetRequestFrame(extendedPanID: string, serialNumbers: number[]): Zcl.Frame {
return Zcl.Frame.create(
Zcl.FrameType.SPECIFIC,
Zcl.Direction.CLIENT_TO_SERVER,
// disableDefaultResponse:
true,
Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
// transactionSequenceNumber:
0,
// commandKey: 0
"hueResetRequest",
// clusterId: 0x1000. Same as touchlink, but with manufacturerCode
"manuSpecificPhilipsPairing",
{extendedPANID: extendedPanID, serialCount: serialNumbers.length, serialNumbers},
{},
);
}

private createScanRequestFrame(transaction: number): Zcl.Frame {
return Zcl.Frame.create(
Zcl.FrameType.SPECIFIC,
Expand Down
17 changes: 17 additions & 0 deletions src/zspec/zcl/definition/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4909,6 +4909,23 @@ export const Clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>
},
commandsResponse: {},
},
// Used to pair lights by serial number. Same ID as touchlink, but different manufacturerCode.
manuSpecificPhilipsPairing: {
ID: 0x1000,
manufacturerCode: ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
attributes: {},
commands: {
hueResetRequest: {
ID: 0,
parameters: [
{name: "extendedPANID", type: DataType.IEEE_ADDR},
{name: "serialCount", type: DataType.UINT8},
{name: "serialNumbers", type: BuffaloZclDataType.LIST_UINT32},
],
},
},
commandsResponse: {},
},
manuSpecificSinope: {
ID: 65281,
manufacturerCode: ManufacturerCode.SINOPE_TECHNOLOGIES,
Expand Down
23 changes: 19 additions & 4 deletions src/zspec/zcl/definition/clusters-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5993,6 +5993,21 @@ export interface TClusters {
};
commandResponses: never;
};
manuSpecificPhilipsPairing: {
attributes: never;
commands: {
/** ID: 0 */
hueResetRequest: {
/** Type: IEEE_ADDR */
extendedPANID: string;
/** Type: UINT8 */
serialCount: number;
/** Type: LIST_UINT32 */
serialNumbers: number[];
};
};
commandResponses: never;
};
manuSpecificSinope: {
attributes: {
/** ID: 2 | Type: ENUM8 */
Expand Down Expand Up @@ -6194,8 +6209,8 @@ export interface TClusters {
/** Type: UINT8 */
payload: number;
};
/** ID: 96 */
tuyaWeatherRequest: {
/** ID: 97 */
tuyaWeatherSync: {
/** Type: BUFFER */
payload: Buffer;
};
Expand Down Expand Up @@ -6274,8 +6289,8 @@ export interface TClusters {
/** Type: UINT16 */
payloadSize: number;
};
/** ID: 97 */
tuyaWeatherSync: {
/** ID: 96 */
tuyaWeatherRequest: {
/** Type: BUFFER */
payload: Buffer;
};
Expand Down
1 change: 1 addition & 0 deletions src/zspec/zcl/definition/tstype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ export type ClusterName =
| "manuSpecificOsram"
| "manuSpecificPhilips"
| "manuSpecificPhilips2"
| "manuSpecificPhilipsPairing"
| "manuSpecificSinope"
| "manuSpecificLegrandDevices"
| "manuSpecificLegrandDevices2"
Expand Down
40 changes: 40 additions & 0 deletions test/adapter/ember/emberAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3386,6 +3386,46 @@ describe("Ember Adapter Layer", () => {
expect(mockEzspSendRawMessage).toHaveBeenCalledWith(expect.any(Buffer), 1, true);
});

it("Adapter impl: sendZclFrameInterPANBroadcastWithoutResponse", async () => {
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.SPECIFIC,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
0,
"scanRequest",
Zcl.Clusters.touchlink.ID,
{transactionID: 1, zigbeeInformation: 4, touchlinkInformation: 18},
{},
);

await adapter.sendZclFrameInterPANBroadcastWithoutResponse(zclFrame);

expect(mockEzspSendRawMessage).toHaveBeenCalledTimes(1);
expect(mockEzspSendRawMessage).toHaveBeenCalledWith(expect.any(Buffer), 1, true);
});

it("Adapter impl: sendZclFrameInterPANBroadcastWithoutResponse fails", async () => {
mockEzspSendRawMessage.mockResolvedValueOnce(SLStatus.BUSY);
const zclFrame = Zcl.Frame.create(
Zcl.FrameType.SPECIFIC,
Zcl.Direction.CLIENT_TO_SERVER,
true,
undefined,
0,
"scanRequest",
Zcl.Clusters.touchlink.ID,
{transactionID: 1, zigbeeInformation: 4, touchlinkInformation: 18},
{},
);

await expect(adapter.sendZclFrameInterPANBroadcastWithoutResponse(zclFrame)).rejects.toThrow(
`~x~> [ZCL TOUCHLINK BROADCAST NOREPLY] Failed to send with status=${SLStatus[SLStatus.BUSY]}.`,
);
expect(mockEzspSendRawMessage).toHaveBeenCalledTimes(1);
expect(mockEzspSendRawMessage).toHaveBeenCalledWith(expect.any(Buffer), 1, true);
});

it("Adapter impl: throws when sendZclFrameInterPANBroadcast command has no response", async () => {
const commandName = "readRsp";
const zclFrame = Zcl.Frame.create(
Expand Down
29 changes: 29 additions & 0 deletions test/adapter/z-stack/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3922,6 +3922,35 @@ describe("zstack-adapter", () => {
});
});

it("Send zcl frame interpan without response", async () => {
basicMocks();
await adapter.start();
mockZnpRequest.mockClear();
mockQueueExecute.mockClear();

await adapter.sendZclFrameInterPANBroadcastWithoutResponse(touchlinkScanRequest);

expect(mockZnpRequest).toHaveBeenCalledTimes(1);
expect(mockZnpRequest).toHaveBeenCalledWith(
4,
"dataRequestExt",
{
clusterid: 4096,
data: touchlinkScanRequest.toBuffer(),
destendpoint: 254,
dstaddr: "0x000000000000ffff",
len: 9,
options: 0,
radius: 30,
srcendpoint: 12,
transid: 1,
dstaddrmode: 2,
dstpanid: 65535,
},
undefined,
);
});

it("Send zcl frame interpan throw exception when command has no response", async () => {
basicMocks();
await adapter.start();
Expand Down
Loading