Files
Nerivec de58711e11 feat: Add ability to abort running OTA (#32022)
Co-authored-by: Andy <andrei_lazarov@yahoo.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-03 21:01:44 +02:00

1268 lines
40 KiB
TypeScript

import assert from "node:assert";
import {type Mock, vi} from "vitest";
import type {AdapterTypes} from "zigbee-herdsman";
import {Zcl} from "zigbee-herdsman";
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
import type {OtaDataSettings, OtaExtraMetas, OtaSource, OtaUpdateAvailableResult} from "zigbee-herdsman/dist/controller/tstype";
import type {TClusterCommandPayload} from "zigbee-herdsman/dist/zspec/zcl/definition/clusters-types";
import type {BindingTableEntry, LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
import type {EventHandler} from "./utils";
type ZHConfiguredReporting = {
cluster: {name: string};
attribute: {name: string | undefined; ID?: number};
minimumReportInterval: number;
maximumReportInterval: number;
reportableChange: number;
};
type ZHEndpointCluster = {
ID?: number;
name: string;
};
type ZHBind = {
target: Endpoint | Group;
cluster: ZHEndpointCluster;
};
const CLUSTERS = {
genBasic: Zcl.Clusters.genBasic.ID,
genOta: Zcl.Clusters.genOta.ID,
genScenes: Zcl.Clusters.genScenes.ID,
genOnOff: Zcl.Clusters.genOnOff.ID,
genLevelCtrl: Zcl.Clusters.genLevelCtrl.ID,
lightingColorCtrl: Zcl.Clusters.lightingColorCtrl.ID,
closuresWindowCovering: Zcl.Clusters.closuresWindowCovering.ID,
hvacThermostat: Zcl.Clusters.hvacThermostat.ID,
msIlluminanceMeasurement: Zcl.Clusters.msIlluminanceMeasurement.ID,
msTemperatureMeasurement: Zcl.Clusters.msTemperatureMeasurement.ID,
msRelativeHumidity: Zcl.Clusters.msRelativeHumidity.ID,
msSoilMoisture: Zcl.Clusters.msSoilMoisture.ID,
msCO2: Zcl.Clusters.msCO2.ID,
};
export const CUSTOM_CLUSTERS = {
custom_1: {
ID: 64672,
manufacturerCode: 4617,
attributes: {
attribute_0: {ID: 0, type: 49},
},
commands: {
command_0: {ID: 0, response: 0, parameters: [{name: "reset", type: 40}]},
},
commandsResponse: {},
},
};
const CUSTOM_CLUSTER_BTHRA = {
custom_1: {
ID: 513,
attributes: {
attribute_0: {ID: 16391, type: 48, manufacturerCode: 4617},
attribute_1: {ID: 16416, type: 48, manufacturerCode: 4617},
attribute_2: {ID: 16418, type: 48, manufacturerCode: 4617},
attribute_3: {ID: 16448, type: 41, manufacturerCode: 4617},
attribute_4: {ID: 16450, type: 48, manufacturerCode: 4617},
attribute_5: {ID: 16451, type: 48, manufacturerCode: 4617},
},
commands: {},
commandsResponse: {},
},
custom_2: {
ID: 516,
attributes: {
attribute_0: {ID: 16395, type: 32, manufacturerCode: 4617},
attribute_1: {ID: 16441, type: 48, manufacturerCode: 4617},
attribute_2: {ID: 16442, type: 48, manufacturerCode: 4617},
attribute_3: {ID: 16443, type: 48, manufacturerCode: 4617},
},
commands: {},
commandsResponse: {},
},
};
function getClusterKey(value: unknown): string | undefined {
for (const key in CLUSTERS) {
if (CLUSTERS[key as keyof typeof CLUSTERS] === value) {
return key;
}
}
return undefined;
}
export class Endpoint {
deviceIeeeAddress: string;
clusterValues: Record<string, Record<string, unknown>>;
ID: number;
inputClusters: number[];
outputClusters: number[];
command: Mock;
commandResponse: Mock;
read: Mock;
write: Mock;
bind: Mock;
unbind: Mock;
save: Mock;
configureReporting: Mock;
readReportingConfig: Mock;
meta: Record<string, unknown>;
binds: ZHBind[];
profileID: number | undefined;
deviceID: number | undefined;
configuredReportings: ZHConfiguredReporting[];
addToGroup: Mock;
removeFromGroup: Mock;
getClusterAttributeValue: Mock;
constructor(
ID: number,
inputClusters: number[],
outputClusters: number[],
deviceIeeeAddress: string,
binds: ZHBind[] = [],
clusterValues: Record<string, Record<string, unknown>> = {},
configuredReportings: ZHConfiguredReporting[] = [],
profileID: number | undefined = undefined,
deviceID: number | undefined = undefined,
meta: Record<string, unknown> = {},
) {
this.deviceIeeeAddress = deviceIeeeAddress;
this.clusterValues = clusterValues;
this.ID = ID;
this.inputClusters = inputClusters;
this.outputClusters = outputClusters;
this.command = vi.fn();
this.commandResponse = vi.fn();
this.read = vi.fn();
this.write = vi.fn();
this.bind = vi.fn();
this.unbind = vi.fn();
this.save = vi.fn();
this.configureReporting = vi.fn();
this.readReportingConfig = vi.fn();
this.meta = meta;
this.binds = binds;
this.profileID = profileID;
this.deviceID = deviceID;
this.configuredReportings = configuredReportings;
this.addToGroup = vi.fn((group: Group) => {
if (!group.members.includes(this)) {
group.members.push(this);
}
});
this.removeFromGroup = vi.fn((group: Group) => {
const index = group.members.indexOf(this);
if (index !== -1) {
group.members.splice(index, 1);
}
});
this.getClusterAttributeValue = vi.fn((cluster: string, value: string) =>
!(cluster in this.clusterValues) ? undefined : this.clusterValues[cluster][value],
);
}
getInputClusters(): ZHEndpointCluster[] {
const clusters: ZHEndpointCluster[] = [];
for (const clusterId of this.inputClusters) {
const name = getClusterKey(clusterId);
if (name) {
clusters.push({ID: clusterId, name});
}
}
return clusters;
}
getOutputClusters(): ZHEndpointCluster[] {
const clusters: ZHEndpointCluster[] = [];
for (const clusterId of this.outputClusters) {
const name = getClusterKey(clusterId);
if (name) {
clusters.push({ID: clusterId, name});
}
}
return clusters;
}
supportsInputCluster(cluster: keyof typeof CLUSTERS): boolean {
assert(CLUSTERS[cluster] !== undefined, `Undefined '${cluster}'`);
return this.inputClusters.includes(CLUSTERS[cluster]);
}
supportsOutputCluster(cluster: keyof typeof CLUSTERS): boolean {
assert(CLUSTERS[cluster], `Undefined '${cluster}'`);
return this.outputClusters.includes(CLUSTERS[cluster]);
}
getDevice(): Device | undefined {
return Object.values(devices).find((d) => d.ieeeAddr === this.deviceIeeeAddress);
}
removeFromAllGroups(): void {
for (const key in groups) {
this.removeFromGroup(groups[key as keyof typeof groups]);
}
}
mockClear(): void {
this.command.mockClear();
this.commandResponse.mockClear();
this.read.mockClear();
this.write.mockClear();
this.bind.mockClear();
this.unbind.mockClear();
this.save.mockClear();
this.configureReporting.mockClear();
this.readReportingConfig.mockClear();
this.addToGroup.mockClear();
this.removeFromGroup.mockClear();
this.getClusterAttributeValue.mockClear();
}
}
export class Device {
type: string;
ieeeAddr: string;
dateCode: string | undefined;
networkAddress: number;
manufacturerID: number;
endpoints: Endpoint[];
powerSource: string | undefined;
softwareBuildID: string | undefined;
interviewState: InterviewState;
modelID: string | undefined;
interview: Mock;
meta: Record<string, unknown>;
ping: Mock<(disableRecovery?: boolean) => Promise<void>>;
removeFromNetwork: Mock;
removeFromDatabase: Mock;
customClusters: Record<string, unknown>;
addCustomCluster: Mock;
save: Mock;
manufacturerName: string | undefined;
lastSeen: number | undefined;
isDeleted: boolean;
linkquality?: number;
lqi: Mock<() => Promise<LQITableEntry[]>>;
routingTable: Mock<() => Promise<RoutingTableEntry[]>>;
bindingTable: Mock<() => Promise<BindingTableEntry[]>>;
clearAllBindings: Mock<() => Promise<void>>;
checkOta =
vi.fn<
(
source: OtaSource,
current: TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
extraMetas: OtaExtraMetas,
endpoint: Endpoint,
) => Promise<OtaUpdateAvailableResult>
>();
updateOta =
vi.fn<
(
source: Readonly<OtaSource> | undefined,
requestPayload: TClusterCommandPayload<"genOta", "queryNextImageRequest"> | undefined,
requestTsn: number | undefined,
extraMetas: Readonly<OtaExtraMetas>,
onProgress: (progress: number, remaining: number) => void,
dataSettings: OtaDataSettings,
endpoint: Endpoint,
) => Promise<[from: OtaUpdateAvailableResult["current"], to: OtaUpdateAvailableResult["current"] | undefined]>
>();
scheduleOta = vi.fn((source) => {
this.scheduledOta = source;
});
unscheduleOta = vi.fn(() => {
this.scheduledOta = undefined;
});
abortOta = vi.fn();
scheduledOta: OtaSource | undefined = undefined;
constructor(
type: string,
ieeeAddr: string,
networkAddress: number,
manufacturerID: number,
endpoints: Endpoint[],
interviewState: InterviewState,
powerSource: string | undefined = undefined,
modelID: string | undefined = undefined,
manufacturerName: string | undefined = undefined,
dateCode: string | undefined = undefined,
softwareBuildID: string | undefined = undefined,
customClusters = {},
) {
this.type = type;
this.ieeeAddr = ieeeAddr;
this.dateCode = dateCode;
this.networkAddress = networkAddress;
this.manufacturerID = manufacturerID;
this.endpoints = endpoints;
this.powerSource = powerSource;
this.softwareBuildID = softwareBuildID;
this.interviewState = interviewState;
this.modelID = modelID;
this.interview = vi.fn();
this.meta = {};
this.ping = vi.fn();
this.removeFromNetwork = vi.fn();
this.removeFromDatabase = vi.fn();
this.customClusters = customClusters;
this.addCustomCluster = vi.fn();
this.save = vi.fn();
this.manufacturerName = manufacturerName;
this.lastSeen = 1000;
this.isDeleted = false;
this.lqi = vi.fn(() => Promise.resolve([] as LQITableEntry[]));
this.routingTable = vi.fn(() => Promise.resolve([] as RoutingTableEntry[]));
this.bindingTable = vi.fn(() => Promise.resolve([] as BindingTableEntry[]));
this.clearAllBindings = vi.fn(() => {});
}
getEndpoint(ID: number): Endpoint | undefined {
return this.endpoints.find((e) => e.ID === ID);
}
mockClear(): void {
this.interview.mockClear();
this.ping.mockClear();
this.removeFromNetwork.mockClear();
this.removeFromDatabase.mockClear();
this.addCustomCluster.mockClear();
this.save.mockClear();
this.lqi.mockClear();
this.routingTable.mockClear();
this.bindingTable.mockClear();
this.clearAllBindings.mockClear();
this.checkOta.mockClear();
this.updateOta.mockClear();
this.scheduleOta.mockClear();
this.unscheduleOta.mockClear();
this.abortOta.mockClear();
this.meta = {};
this.scheduledOta = undefined;
for (const ep of this.endpoints) {
ep.mockClear();
}
}
}
export class Group {
groupID: number;
command: Mock;
meta: Record<string, unknown>;
members: Endpoint[];
removeFromDatabase: Mock;
removeFromNetwork: Mock;
constructor(groupID: number, members: Endpoint[]) {
this.groupID = groupID;
this.command = vi.fn();
this.meta = {};
this.members = members;
this.removeFromDatabase = vi.fn();
this.removeFromNetwork = vi.fn();
}
hasMember(endpoint: Endpoint): boolean {
return this.members.includes(endpoint);
}
}
export const events: Record<string, EventHandler> = {};
export const returnDevices: string[] = [];
const bulb_color = new Device(
"Router",
"0x000b57fffec6a5b3",
40399,
4107,
[
new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57fffec6a5b3", [], {
lightingColorCtrl: {colorCapabilities: 254},
}),
],
InterviewState.Successful,
"Mains (single phase)",
"LLC020",
);
const bulb_color_2 = new Device(
"Router",
"0x000b57fffec6a5b4",
401292,
4107,
[
new Endpoint(
1,
[0, 3, 4, 5, 6, 8, 768, 2821, 4096],
[5, 25, 32, 4096],
"0x000b57fffec6a5b4",
[],
{lightingColorCtrl: {colorCapabilities: 254}},
[],
undefined,
undefined,
{scenes: {"1_0": {name: "Chill scene", state: {state: "ON"}}, "4_9": {state: {state: "OFF"}}}},
),
],
InterviewState.Successful,
"Mains (single phase)",
"LLC020",
"Philips",
"2019.09",
"5.127.1.26581",
);
const bulb_2 = new Device(
"Router",
"0x000b57fffec6a5b7",
40369,
4476,
[new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57fffec6a5b7", [], {lightingColorCtrl: {colorCapabilities: 17}})],
InterviewState.Successful,
"Mains (single phase)",
"TRADFRI bulb E27 WS opal 980lm",
);
const hue_twilight = new Device(
"Router",
"0x000b57cdfec6a5b3",
40399,
4107,
[
new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57cdfec6a5b3", [], {
lightingColorCtrl: {colorCapabilities: 254},
}),
new Endpoint(11, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57cdfec6a5b3", [], {
lightingColorCtrl: {colorCapabilities: 254},
}),
new Endpoint(12, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57cdfec6a5b3", [], {
lightingColorCtrl: {colorCapabilities: 254},
}),
],
InterviewState.Successful,
"Mains (single phase)",
"LGT003",
);
const TS0601_thermostat = new Device(
"EndDevice",
"0x0017882104a44559",
6544,
4151,
[new Endpoint(1, [], [], "0x0017882104a44559")],
InterviewState.Successful,
"Mains (single phase)",
"kud7u2l",
);
const TS0601_switch = new Device(
"EndDevice",
"0x0017882104a44560",
6544,
4151,
[new Endpoint(1, [], [], "0x0017882104a44560")],
InterviewState.Successful,
"Mains (single phase)",
"kjintbl",
);
const TS0601_cover_switch = new Device(
"EndDevice",
"0x0017882104a44562",
6544,
4151,
[new Endpoint(1, [], [], "0x0017882104a44562")],
InterviewState.Successful,
"Mains (single phase)",
"TS0601",
"_TZE200_5nldle7w",
);
const ZNCZ02LM = new Device(
"Router",
"0x0017880104e45524",
6540,
4151,
[new Endpoint(1, [0, 6], [], "0x0017880104e45524")],
InterviewState.Successful,
"Mains (single phase)",
"lumi.plug",
);
const GLEDOPTO_2ID = new Device(
"Router",
"0x0017880104e45724",
6540,
4151,
[
new Endpoint(11, [0, 3, 4, 5, 6, 8, 768], [], "0x0017880104e45724", [], {}, [], 49246, 528),
new Endpoint(12, [0, 3, 4, 5, 6, 8, 768], [], "0x0017880104e45724", [], {}, [], 260, 258),
new Endpoint(13, [4096], [4096], "0x0017880104e45724", [], {}, [], 49246, 57694),
new Endpoint(15, [0, 3, 4, 5, 6, 8, 768], [], "0x0017880104e45724", [], {}, [], 49246, 256),
],
InterviewState.Successful,
"Mains (single phase)",
"GL-C-007",
"GLEDOPTO",
);
const QBKG03LM = new Device(
"Router",
"0x0017880104e45542",
6540,
4151,
[
new Endpoint(1, [0], [], "0x0017880104e45542"),
new Endpoint(2, [0, 6], [], "0x0017880104e45542"),
new Endpoint(3, [0, 6], [], "0x0017880104e45542"),
],
InterviewState.Successful,
"Mains (single phase)",
"lumi.ctrl_neutral2",
);
const zigfred_plus = new Device(
"Router",
"0xf4ce368a38be56a1",
6589,
0x129c,
[
new Endpoint(5, [0, 3, 4, 5, 6, 8, 0x0300, 0xfc42], [0xfc42], "0xf4ce368a38be56a1"),
new Endpoint(7, [0, 3, 4, 5, 6, 8], [], "0xf4ce368a38be56a1"),
new Endpoint(8, [0, 3, 4, 5, 6, 8], [], "0xf4ce368a38be56a1"),
new Endpoint(9, [0, 3, 4, 5, 6, 8], [], "0xf4ce368a38be56a1"),
new Endpoint(10, [0, 3, 4, 5, 6, 8], [], "0xf4ce368a38be56a1"),
new Endpoint(11, [0, 3, 4, 5, 0x0102], [], "0xf4ce368a38be56a1"),
new Endpoint(12, [0, 3, 4, 5, 0x0102], [], "0xf4ce368a38be56a1"),
],
InterviewState.Successful,
"Mains (single phase)",
"zigfred plus",
"Siglis",
);
export const groups = {
group_1: new Group(1, []),
group_2: new Group(2, []),
group_tradfri_remote: new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]),
"group/with/slashes": new Group(99, []),
group_with_tradfri: new Group(11, [bulb_2.endpoints[0]]),
thermostat_group: new Group(12, [TS0601_thermostat.endpoints[0]]),
group_with_switch: new Group(14, [ZNCZ02LM.endpoints[0], bulb_2.endpoints[0]]),
gledopto_group: new Group(21, [GLEDOPTO_2ID.endpoints[3]]),
default_bind_group: new Group(DEFAULT_BIND_GROUP_ID, []),
ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[2]]),
hue_twilight_group: new Group(19, [hue_twilight.endpoints[1]]),
};
const groupMembersBackup = Object.fromEntries(Object.entries(groups).map((v) => [v[0], [...v[1].members]]));
export function resetGroupMembers(): void {
for (const key in groupMembersBackup) {
groups[key as keyof typeof groups].members = [...groupMembersBackup[key]];
}
}
export const devices = {
coordinator: new Device("Coordinator", "0x00124b00120144ae", 0, 0, [new Endpoint(1, [], [], "0x00124b00120144ae")], InterviewState.Successful),
bulb: new Device(
"Router",
"0x000b57fffec6a5b2",
40369,
4476,
[
new Endpoint(
1,
[0, 3, 4, 5, 6, 8, 768, 2821, 4096],
[5, 25, 32, 4096],
"0x000b57fffec6a5b2",
[],
{lightingColorCtrl: {colorCapabilities: 17}},
[
{
cluster: {name: "genOnOff"},
attribute: {name: "onOff"},
minimumReportInterval: 1,
maximumReportInterval: 10,
reportableChange: 20,
},
],
),
],
InterviewState.Successful,
"Mains (single phase)",
"TRADFRI bulb E27 WS opal 980lm",
),
"RBSH-TRV0-ZB-EU": new Device(
"EndDevice",
"0x18fc2600000d7ae2",
35902,
4617, // 0x1209,
[new Endpoint(1, [0, 1, 3, 4, 32, 513, 516, 2821], [10, 25], "0x18fc2600000d7ae2")],
InterviewState.Successful,
"Battery",
"RBSH-TRV0-ZB-EU",
"BOSCH",
"20231122",
"3.05.09",
CUSTOM_CLUSTER_BTHRA,
),
bulb_color: bulb_color,
bulb_2: bulb_2,
hue_twilight,
bulb_color_2: bulb_color_2,
remote: new Device(
"EndDevice",
"0x0017880104e45517",
6535,
4107,
[
new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45517", [
{target: bulb_color.endpoints[0], cluster: {ID: 8, name: "genLevelCtrl"}},
{target: bulb_color.endpoints[0], cluster: {ID: 6, name: "genOnOff"}},
{target: bulb_color.endpoints[0], cluster: {ID: 768, name: "lightingColorCtrl"}},
{target: groups.group_1, cluster: {ID: 6, name: "genOnOff"}},
{target: groups.group_1, cluster: {ID: 6, name: "genLevelCtrl"}},
]),
new Endpoint(2, [0, 1, 3, 15, 64512], [25, 6], "0x0017880104e45517"),
],
InterviewState.Successful,
"Battery",
"RWL021",
),
unsupported: new Device(
"EndDevice",
"0x0017880104e45518",
6536,
0,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45518")],
InterviewState.Successful,
"Battery",
"notSupportedModelID",
"notSupportedMfg",
),
unsupported2: new Device(
"EndDevice",
"0x0017880104e45529",
6536,
0,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45529")],
InterviewState.Successful,
"Battery",
"notSupportedModelID",
),
interviewing: new Device(
"EndDevice",
"0x0017880104e45530",
6536,
0,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45530")],
InterviewState.InProgress,
"Battery",
undefined,
),
notInSettings: new Device(
"EndDevice",
"0x0017880104e45519",
6537,
0,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45519")],
InterviewState.Successful,
"Battery",
"lumi.sensor_switch.aq2",
),
WXKG11LM: new Device(
"EndDevice",
"0x0017880104e45520",
6537,
4151,
[
new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45520", [], {}, [
{
cluster: {name: "genOnOff"},
attribute: {name: undefined, ID: 1337},
minimumReportInterval: 1,
maximumReportInterval: 10,
reportableChange: 20,
},
]),
],
InterviewState.Successful,
"Battery",
"lumi.sensor_switch.aq2",
),
WXKG02LM_rev1: new Device(
"EndDevice",
"0x0017880104e45521",
6538,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45521"), new Endpoint(2, [0], [], "0x0017880104e45521")],
InterviewState.Successful,
"Battery",
"lumi.sensor_86sw2.es1",
),
WSDCGQ11LM: new Device(
"EndDevice",
"0x0017880104e45522",
6539,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45522")],
InterviewState.Successful,
"Battery",
"lumi.weather",
),
// This are not a real spammer device, just copy of previous to test the throttle filter
SPAMMER: new Device(
"EndDevice",
"0x0017880104e455fe",
6539,
4151,
[new Endpoint(1, [0], [], "0x0017880104e455fe")],
InterviewState.Successful,
"Battery",
"lumi.weather",
),
RTCGQ11LM: new Device(
"EndDevice",
"0x0017880104e45523",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45523")],
InterviewState.Successful,
"Battery",
"lumi.sensor_motion.aq2",
),
ZNCZ02LM: ZNCZ02LM,
E1743: new Device(
"Router",
"0x0017880104e45540",
6540,
4476,
[new Endpoint(1, [0], [], "0x0017880104e45540")],
InterviewState.Successful,
"Mains (single phase)",
"TRADFRI on/off switch",
),
QBKG04LM: new Device(
"Router",
"0x0017880104e45541",
6549,
4151,
[new Endpoint(1, [0], [25], "0x0017880104e45541"), new Endpoint(2, [0, 6], [], "0x0017880104e45541")],
InterviewState.Successful,
"Mains (single phase)",
"lumi.ctrl_neutral1",
),
QBKG03LM: QBKG03LM,
GLEDOPTO1112: new Device(
"Router",
"0x0017880104e45543",
6540,
4151,
[new Endpoint(11, [0], [], "0x0017880104e45543"), new Endpoint(13, [0], [], "0x0017880104e45543")],
InterviewState.Successful,
"Mains (single phase)",
"GL-C-008",
),
GLEDOPTO111213: new Device(
"Router",
"0x0017880104e45544",
6540,
4151,
[
new Endpoint(11, [0], [], "0x0017880104e45544"),
new Endpoint(13, [0], [], "0x0017880104e45544"),
new Endpoint(12, [0], [], "0x0017880104e45544"),
],
InterviewState.Successful,
"Mains (single phase)",
"GL-C-008",
),
GLEDOPTO_2ID: GLEDOPTO_2ID,
HGZB04D: new Device(
"Router",
"0x0017880104e45545",
6540,
4151,
[new Endpoint(1, [0], [25], "0x0017880104e45545")],
InterviewState.Successful,
"Mains (single phase)",
"FB56+ZSC05HG1.0",
),
ZNCLDJ11LM: new Device(
"Router",
"0x0017880104e45547",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45547"), new Endpoint(2, [0], [], "0x0017880104e45547")],
InterviewState.Successful,
"Mains (single phase)",
"lumi.curtain",
),
HAMPTON99432: new Device(
"Router",
"0x0017880104e45548",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45548"), new Endpoint(2, [0], [], "0x0017880104e45548")],
InterviewState.Successful,
"Mains (single phase)",
"HDC52EastwindFan",
),
HS2WD: new Device(
"Router",
"0x0017880104e45549",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45549")],
InterviewState.Successful,
"Mains (single phase)",
"WarningDevice",
),
"1TST_EU": new Device(
"Router",
"0x0017880104e45550",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45550")],
InterviewState.Successful,
"Mains (single phase)",
"Thermostat",
),
SV01: new Device(
"Router",
"0x0017880104e45551",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45551")],
InterviewState.Successful,
"Mains (single phase)",
"SV01-410-MP-1.0",
),
J1: new Device(
"Router",
"0x0017880104e45552",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45552")],
InterviewState.Successful,
"Mains (single phase)",
"J1 (5502)",
),
E11_G13: new Device(
"EndDevice",
"0x0017880104e45553",
6540,
4151,
[new Endpoint(1, [0, 6], [], "0x0017880104e45553")],
InterviewState.Successful,
"Mains (single phase)",
"E11-G13",
),
nomodel: new Device(
"Router",
"0x0017880104e45535",
6536,
0,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45535")],
InterviewState.InProgress,
"Mains (single phase)",
undefined,
),
unsupported_router: new Device(
"Router",
"0x0017880104e45525",
6536,
0,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45525")],
InterviewState.Successful,
"Mains (single phase)",
"notSupportedModelID",
"Boef",
),
CC2530_ROUTER: new Device(
"Router",
"0x0017880104e45559",
6540,
4151,
[new Endpoint(1, [0, 6], [], "0x0017880104e45559")],
InterviewState.Successful,
"Mains (single phase)",
"lumi.router",
),
LIVOLO: new Device(
"Router",
"0x0017880104e45560",
6541,
4152,
[new Endpoint(6, [0, 6], [], "0x0017880104e45560")],
InterviewState.Successful,
"Mains (single phase)",
"TI0001 ",
),
tradfri_remote: new Device(
"EndDevice",
"0x90fd9ffffe4b64ae",
33906,
4476,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64ae")],
InterviewState.Successful,
"Battery",
"TRADFRI remote control",
),
roller_shutter: new Device(
"EndDevice",
"0x90fd9ffffe4b64af",
33906,
4476,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64af")],
InterviewState.Successful,
"Battery",
"SCM-R_00.00.03.15TC",
),
ZNLDP12LM: new Device(
"Router",
"0x90fd9ffffe4b64ax",
33901,
4476,
[
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64ax", [], {
lightingColorCtrl: {colorCapabilities: 254},
}),
],
InterviewState.Successful,
"Mains (single phase)",
"lumi.light.aqcn02",
),
SP600_OLD: new Device(
"Router",
"0x90fd9ffffe4b64aa",
33901,
4476,
[
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {
seMetering: {multiplier: 1, divisor: 10000},
}),
],
InterviewState.Successful,
"Mains (single phase)",
"SP600",
"Salus",
"20160120",
),
SP600_NEW: new Device(
"Router",
"0x90fd9ffffe4b64ab",
33901,
4476,
[
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {
seMetering: {multiplier: 1, divisor: 10000},
}),
],
InterviewState.Successful,
"Mains (single phase)",
"SP600",
"Salus",
"20170220",
),
"MKS-CM-W5": new Device(
"Router",
"0x90fd9ffffe4b64ac",
33901,
4476,
[new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {})],
InterviewState.Successful,
"Mains (single phase)",
"qnazj70",
),
"GL-S-007ZS": new Device(
"Router",
"0x0017880104e45526",
6540,
4151,
[new Endpoint(1, [0], [], "0x0017880104e45526")],
InterviewState.Successful,
"Mains (single phase)",
"GL-S-007ZS",
),
U202DST600ZB: new Device(
"Router",
"0x0017880104e43559",
6540,
4151,
[new Endpoint(10, [0, 6], [], "0x0017880104e43559"), new Endpoint(11, [0, 6], [], "0x0017880104e43559")],
InterviewState.Successful,
"Mains (single phase)",
"U202DST600ZB",
),
zigfred_plus: zigfred_plus,
3157100: new Device(
"Router",
"0x0017880104e44559",
6542,
4151,
[new Endpoint(1, [], [], "0x0017880104e44559")],
InterviewState.Successful,
"Mains (single phase)",
"3157100",
"Centralite",
),
J1_cover: new Device(
"Router",
"0x0017880104a44559",
6543,
4151,
[new Endpoint(1, [], [], "0x0017880104a44559")],
InterviewState.Successful,
"Mains (single phase)",
"J1 (5502)",
),
TS130F_DUAL_COVER_SWITCH: new Device(
"Router",
"0xa4c138018cf95021",
46163,
4417,
[
new Endpoint(1, [0, 4, 5, 6, 258, 57345], [10, 25], "0xa4c138018cf95021"),
new Endpoint(2, [0, 4, 5, 6, 258, 57345], [10, 25], "0xa4c138018cf95021"),
],
InterviewState.Successful,
"Mains (single phase)",
"TS130F",
"_TZ3000_j1xl73iw",
),
TS0601_thermostat: TS0601_thermostat,
TS0601_switch: TS0601_switch,
TS0601_cover_switch: TS0601_cover_switch,
external_converter_device: new Device(
"EndDevice",
"0x0017880104e45511",
1114,
0xffff,
[new Endpoint(1, [], [], "0x0017880104e45511")],
InterviewState.Failed,
undefined,
"external_converter_device",
),
QS_Zigbee_D02_TRIAC_2C_LN: new Device(
"Router",
"0x0017882194e45543",
6549,
4151,
[new Endpoint(1, [0], [], "0x0017882194e45543"), new Endpoint(2, [0, 6], [], "0x0017882194e45543")],
InterviewState.Successful,
"Mains (single phase)",
"TS110F",
"_TYZB01_v8gtiaed",
),
unknown: new Device("Router", "0x0017980134e45545", 6540, 4151, [], InterviewState.Successful, "Mains (single phase)"),
temperature_sensor: new Device(
"EndDevice",
"0x0017880104e45561",
6544,
4151,
[new Endpoint(1, [0, 3, 4, 1026], [], "0x0017880104e45561")],
InterviewState.Successful,
"Battery",
"temperature.sensor",
),
heating_actuator: new Device(
"Router",
"0x0017880104e45562",
6545,
4151,
[new Endpoint(1, [0, 3, 4, 513], [1026], "0x0017880104e45562")],
InterviewState.Successful,
"Mains (single phase)",
"heating.actuator",
),
bj_scene_switch: new Device(
"EndDevice",
"0xd85def11a1002caa",
50117,
4398,
[
new Endpoint(10, [0, 4096], [3, 4, 5, 6, 8, 25, 768, 4096], "0xd85def11a1002caa", [
{target: bulb_color_2.endpoints[0], cluster: {ID: 8, name: "genLevelCtrl"}},
{target: bulb_color_2.endpoints[0], cluster: {ID: 6, name: "genOnOff"}},
{target: bulb_color_2.endpoints[0], cluster: {ID: 768, name: "lightingColorCtrl"}},
]),
new Endpoint(11, [0, 4096], [3, 4, 5, 6, 8, 25, 768, 4096], "0xd85def11a1002caa"),
],
InterviewState.Successful,
"Battery",
"RB01",
"Busch-Jaeger",
"20161222",
"1.2.0",
),
"GW003-AS-IN-TE-FC": new Device(
"Router",
"0x0017548104a44669",
6545,
4699,
[new Endpoint(1, [3], [0, 3, 513, 514], "0x0017548104a44669")],
InterviewState.Successful,
"Mains (single phase)",
"Adapter Zigbee FUJITSU",
),
"BMCT-SLZ": new Device(
"Router",
"0x18fc26000000cafe",
6546,
4617,
[new Endpoint(1, [0, 3, 4, 5, 258, 1794, 2820, 2821, 64672], [10, 25], "0x18fc26000000cafe")],
InterviewState.Successful,
"Mains (single phase)",
"RBSH-MMS-ZB-EU",
),
BMCT_SLZ: new Device(
"Router",
"0x0026decafe000473",
6546,
4617,
[new Endpoint(1, [0, 3, 4, 5, 258, 1794, 2820, 2821, 64672], [10, 25], "0x0026decafe000473")],
InterviewState.Successful,
"Mains (single phase)",
"RBSH-MMS-ZB-EU",
undefined,
undefined,
undefined,
CUSTOM_CLUSTERS,
),
bulb_custom_cluster: new Device(
"Router",
"0x000b57fffec6a5c2",
40369,
4476,
[new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57fffec6a5c2")],
InterviewState.Successful,
"Mains (single phase)",
"TRADFRI bulb E27 WS opal 980lm",
undefined,
undefined,
undefined,
CUSTOM_CLUSTERS,
),
InovelliVZM31SN: new Device(
"Router",
"0xb43a31fffe2f1f6a",
59545,
4655,
[
new Endpoint(1, [], [], "0xb43a31fffe2f1f6a", [], {}, [], 1, 1, {multiEndpointSkip: ["state", "power", "energy", "brightness"]}),
new Endpoint(2, [], [], "0xb43a31fffe2f1f6a", [], {}, [], 1, 1, {multiEndpointSkip: ["state", "power", "energy", "brightness"]}),
new Endpoint(3, [], [], "0xb43a31fffe2f1f6a", [], {}, [], 1, 1, {multiEndpointSkip: ["state", "power", "energy", "brightness"]}),
],
InterviewState.Successful,
"Mains (single phase)",
"VZM31-SN",
undefined,
undefined,
undefined,
CUSTOM_CLUSTERS,
),
fanbee: new Device(
"Router",
"0x00124b00cfcf3298",
18129,
0xfff1,
[new Endpoint(8, [0, 3, 4, 5, 6, 8], [], "0x00124b00cfcf3298"), new Endpoint(242, [], [33], "0x00124b00cfcf3298")],
InterviewState.Successful,
"DC Source",
"FanBee1",
),
BTH_RM230Z: new Device(
"Router",
"0x18fc2600000d7ae3",
35902,
4617,
[new Endpoint(1, [0, 3, 513, 516, 1029, 2821], [10, 25], "0x18fc2600000d7ae3")],
InterviewState.Successful,
"Mains (single phase)",
"RBSH-RTH0-ZB-EU",
undefined,
undefined,
undefined,
CUSTOM_CLUSTER_BTHRA,
),
};
export const mockController = {
on: (type: string, handler: EventHandler): void => {
events[type] = handler;
},
start: vi.fn(async (abortSignal?: AbortSignal): Promise<AdapterTypes.StartResult> => {
abortSignal?.throwIfAborted();
return await Promise.resolve("reset");
}),
stop: vi.fn(),
touchlink: {identify: vi.fn(), scan: vi.fn(), factoryReset: vi.fn(), factoryResetFirst: vi.fn()},
addInstallCode: vi.fn(),
permitJoin: vi.fn(),
getPermitJoin: vi.fn((): boolean => false),
getPermitJoinEnd: vi.fn((): number | undefined => undefined),
isStopping: vi.fn((): boolean => false),
backup: vi.fn(),
coordinatorCheck: vi.fn(),
getCoordinatorVersion: vi.fn(
(): Promise<AdapterTypes.CoordinatorVersion> => Promise.resolve({type: "z-Stack", meta: {version: 1, revision: 20190425}}),
),
getNetworkParameters: vi.fn(
(): Promise<AdapterTypes.NetworkParameters> =>
Promise.resolve({panID: 0x162a, extendedPanID: "0x64c5fd698daf0c00", channel: 15, nwkUpdateID: 0}),
),
getDevices: vi.fn((): Device[] => []),
getDevicesIterator: vi.fn(function* (predicate?: (value: Device) => boolean): Generator<Device> {
for (const key in devices) {
const device = devices[key as keyof typeof devices];
if ((returnDevices.length === 0 || returnDevices.includes(device.ieeeAddr)) && !device.isDeleted && (!predicate || predicate(device))) {
yield device;
}
}
}),
getDevicesByType: vi.fn((type: AdapterTypes.DeviceType): Device[] =>
Object.values(devices)
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.filter((d) => d.type === type),
),
getDeviceByIeeeAddr: vi.fn((ieeeAddr: string): Device | undefined =>
Object.values(devices)
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.find((d) => d.ieeeAddr === ieeeAddr),
),
getDeviceByNetworkAddress: vi.fn((networkAddress: number): Device | undefined =>
Object.values(devices)
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.find((d) => d.networkAddress === networkAddress),
),
getGroupByID: vi.fn((groupID: number): Group | undefined => Object.values(groups).find((g) => g.groupID === groupID)),
getGroups: vi.fn((): Group[] => []),
getGroupsIterator: vi.fn(function* (predicate?: (value: Group) => boolean): Generator<Group> {
for (const key in groups) {
const group = groups[key as keyof typeof groups];
if (!predicate || predicate(group)) {
yield group;
}
}
}),
createGroup: vi.fn((groupID: number): Group => {
const group = new Group(groupID, []);
groups[`group_${groupID}` as keyof typeof groups] = group;
return group;
}),
sendRaw: vi.fn(async (/*rawPayload: RawPayload, customClusters: CustomClusters = {}*/) =>
Promise.resolve([0x00, {nwkAddress: 0x1234, eui64: "", startIndex: 0, assocDevList: []}]),
),
};
vi.mock("zigbee-herdsman", async (importOriginal) => ({
...(await importOriginal()),
Controller: vi.fn().mockImplementation(() => mockController),
}));