mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-18 17:46:31 +00:00
fix: Various fixes to align behaviors (+add benchmarks for dev) (#28011)
This commit is contained in:
@@ -3,9 +3,10 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
branches-ignore:
|
||||
- dev-types
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
@@ -47,6 +48,13 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm run test:coverage
|
||||
|
||||
- name: Bench
|
||||
if: github.ref == 'refs/heads/dev' || github.event_name == 'pull_request'
|
||||
uses: CodSpeedHQ/action@v3
|
||||
with:
|
||||
run: pnpm run bench
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
- name: Log in to the Docker container registry
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -77,6 +77,7 @@ tsconfig.types.tsbuildinfo
|
||||
# Ignore config
|
||||
data/*
|
||||
!data/configuration.example.yaml
|
||||
data-bench
|
||||
|
||||
# commit-user-lookup.json
|
||||
scripts/commit-user-lookup.json
|
||||
|
||||
+4
-4
@@ -28,10 +28,10 @@ import utils from "./util/utils";
|
||||
import Zigbee from "./zigbee";
|
||||
|
||||
export class Controller {
|
||||
private eventBus: EventBus;
|
||||
private zigbee: Zigbee;
|
||||
private state: State;
|
||||
private mqtt: Mqtt;
|
||||
public readonly eventBus: EventBus;
|
||||
public readonly zigbee: Zigbee;
|
||||
public readonly state: State;
|
||||
public readonly mqtt: Mqtt;
|
||||
private restartCallback: () => Promise<void>;
|
||||
private exitCallback: (code: number, restart: boolean) => Promise<void>;
|
||||
public readonly extensions: Set<Extension>;
|
||||
|
||||
@@ -9,10 +9,9 @@ import Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
import utils, {DEFAULT_BIND_GROUP_ID} from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
|
||||
const ALL_CLUSTER_CANDIDATES: readonly ClusterName[] = [
|
||||
"genScenes",
|
||||
"genOnOff",
|
||||
@@ -28,7 +27,7 @@ const ALL_CLUSTER_CANDIDATES: readonly ClusterName[] = [
|
||||
];
|
||||
|
||||
// See zigbee-herdsman-converters
|
||||
const DEFAULT_BIND_GROUP = {type: "group_number", ID: 901, name: "default_bind_group"};
|
||||
const DEFAULT_BIND_GROUP = {type: "group_number", ID: DEFAULT_BIND_GROUP_ID, name: "default_bind_group"};
|
||||
const DEFAULT_REPORT_CONFIG = {minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1};
|
||||
|
||||
const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean; colorXY: boolean}> => {
|
||||
@@ -204,6 +203,7 @@ interface ParsedMQTTMessage {
|
||||
}
|
||||
|
||||
export default class Bind extends Extension {
|
||||
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
|
||||
private pollDebouncers: {[s: string]: () => void} = {};
|
||||
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
@@ -216,7 +216,7 @@ export default class Bind extends Extension {
|
||||
private parseMQTTMessage(
|
||||
data: eventdata.MQTTMessage,
|
||||
): [raw: KeyValue | undefined, parsed: ParsedMQTTMessage | undefined, error: string | undefined] {
|
||||
if (data.topic.match(TOPIC_REGEX)) {
|
||||
if (data.topic.match(this.#topicRegex)) {
|
||||
const type = data.topic.endsWith("unbind") ? "unbind" : "bind";
|
||||
let skipDisableReporting = false;
|
||||
const message = JSON.parse(data.message) as Zigbee2MQTTAPI["bridge/request/device/bind"];
|
||||
|
||||
@@ -14,12 +14,11 @@ import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTT
|
||||
import data from "../util/data";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils, {assertString} from "../util/utils";
|
||||
import utils, {assertString, DEFAULT_BIND_GROUP_ID} from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
const REQUEST_REGEX = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
export default class Bridge extends Extension {
|
||||
#requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
// set on `start`
|
||||
#osInfo!: Zigbee2MQTTAPI["bridge/info"]["os"];
|
||||
private zigbee2mqttVersion!: {commitHash?: string; version: string};
|
||||
@@ -197,7 +196,7 @@ export default class Bridge extends Extension {
|
||||
}
|
||||
|
||||
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
const match = data.topic.match(REQUEST_REGEX);
|
||||
const match = data.topic.match(this.#requestRegex);
|
||||
|
||||
if (!match) {
|
||||
return;
|
||||
@@ -797,7 +796,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
groups.push({
|
||||
id: group.ID,
|
||||
friendly_name: group.ID === 901 ? "default_bind_group" : group.name,
|
||||
friendly_name: group.ID === DEFAULT_BIND_GROUP_ID ? "default_bind_group" : group.name,
|
||||
description: group.options.description,
|
||||
scenes: utils.getScenes(group.zh),
|
||||
members,
|
||||
|
||||
@@ -11,8 +11,6 @@ import * as settings from "../util/settings";
|
||||
import utils, {isLightExpose} from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
|
||||
|
||||
const STATE_PROPERTIES: Readonly<Record<string, (value: string, exposes: zhc.Expose[]) => boolean>> = {
|
||||
state: () => true,
|
||||
brightness: (_value, exposes) => exposes.some((e) => isLightExpose(e) && e.features.some((f) => f.name === "brightness")),
|
||||
@@ -38,6 +36,7 @@ interface ParsedMQTTMessage {
|
||||
}
|
||||
|
||||
export default class Groups extends Extension {
|
||||
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
|
||||
private lastOptimisticState: {[s: string]: KeyValue} = {};
|
||||
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
@@ -185,7 +184,7 @@ export default class Groups extends Extension {
|
||||
private parseMQTTMessage(
|
||||
data: eventdata.MQTTMessage,
|
||||
): [raw: KeyValue | undefined, parsed: ParsedMQTTMessage | undefined, error: string | undefined] {
|
||||
const topicRegexMatch = data.topic.match(TOPIC_REGEX);
|
||||
const topicRegexMatch = data.topic.match(this.#topicRegex);
|
||||
|
||||
if (topicRegexMatch) {
|
||||
const type = topicRegexMatch[1] as "remove" | "add" | "remove_all";
|
||||
@@ -273,7 +272,7 @@ export default class Groups extends Extension {
|
||||
try {
|
||||
if (type === "add") {
|
||||
assert(resolvedGroup, "`resolvedGroup` is missing");
|
||||
logger.info(`Adding '${resolvedDevice.name}' to '${resolvedGroup.name}'`);
|
||||
logger.info(`Adding endpoint '${resolvedEndpoint.ID}' of device '${resolvedDevice.name}' to group '${resolvedGroup.name}'`);
|
||||
await resolvedEndpoint.addToGroup(resolvedGroup.zh);
|
||||
changedGroups.push(resolvedGroup);
|
||||
// biome-ignore lint/style/noNonNullAssertion: valid from resolved asserts
|
||||
@@ -281,7 +280,7 @@ export default class Groups extends Extension {
|
||||
await this.publishResponse<"bridge/response/group/members/add">(parsed.type, raw, respPayload);
|
||||
} else if (type === "remove") {
|
||||
assert(resolvedGroup, "`resolvedGroup` is missing");
|
||||
logger.info(`Removing '${resolvedDevice.name}' from '${resolvedGroup.name}'`);
|
||||
logger.info(`Removing endpoint '${resolvedEndpoint.ID}' of device '${resolvedDevice.name}' from group '${resolvedGroup.name}'`);
|
||||
await resolvedEndpoint.removeFromGroup(resolvedGroup.zh);
|
||||
changedGroups.push(resolvedGroup);
|
||||
// biome-ignore lint/style/noNonNullAssertion: valid from resolved asserts
|
||||
@@ -289,7 +288,7 @@ export default class Groups extends Extension {
|
||||
await this.publishResponse<"bridge/response/group/members/remove">(parsed.type, raw, respPayload);
|
||||
} else {
|
||||
// remove_all
|
||||
logger.info(`Removing '${resolvedDevice.name}' from all groups`);
|
||||
logger.info(`Removing endpoint '${resolvedEndpoint.ID}' of device '${resolvedDevice.name}' from all groups`);
|
||||
|
||||
for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEndpoint))) {
|
||||
changedGroups.push(group);
|
||||
@@ -301,7 +300,7 @@ export default class Groups extends Extension {
|
||||
await this.publishResponse<"bridge/response/group/members/remove_all">(parsed.type, raw, respPayload);
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to ${type} from group (${(e as Error).message})`;
|
||||
const errorMsg = `Failed to ${type} ${type === "add" ? "to" : "from"} group (${(e as Error).message})`;
|
||||
await this.publishResponse(parsed.type, raw, {}, errorMsg);
|
||||
// biome-ignore lint/style/noNonNullAssertion: always Error
|
||||
logger.debug((e as Error).stack!);
|
||||
|
||||
@@ -13,7 +13,7 @@ const SUPPORTED_FORMATS = ["raw", "graphviz", "plantuml"];
|
||||
* This extension creates a network map
|
||||
*/
|
||||
export default class NetworkMap extends Extension {
|
||||
private topic = `${settings.get().mqtt.base_topic}/bridge/request/networkmap`;
|
||||
#topic = `${settings.get().mqtt.base_topic}/bridge/request/networkmap`;
|
||||
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
override async start(): Promise<void> {
|
||||
@@ -21,7 +21,7 @@ export default class NetworkMap extends Extension {
|
||||
}
|
||||
|
||||
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
if (data.topic === this.topic) {
|
||||
if (data.topic === this.#topic) {
|
||||
const message = utils.parseJSON(data.message, data.message) as Zigbee2MQTTAPI["bridge/request/networkmap"];
|
||||
|
||||
try {
|
||||
|
||||
@@ -25,12 +25,11 @@ export interface UpdatePayload {
|
||||
};
|
||||
}
|
||||
|
||||
const topicRegex = new RegExp(
|
||||
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
|
||||
"i",
|
||||
);
|
||||
|
||||
export default class OTAUpdate extends Extension {
|
||||
#topicRegex = new RegExp(
|
||||
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
|
||||
"i",
|
||||
);
|
||||
private inProgress = new Set<string>();
|
||||
private lastChecked = new Map<string, number>();
|
||||
private scheduledUpgrades = new Set<string>();
|
||||
@@ -252,7 +251,7 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
const topicMatch = data.topic.match(topicRegex);
|
||||
const topicMatch = data.topic.match(this.#topicRegex);
|
||||
|
||||
if (!topicMatch) {
|
||||
return;
|
||||
|
||||
@@ -9,12 +9,12 @@ import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
// TODO: get rid of this, use class member
|
||||
let topicGetSetRegex: RegExp;
|
||||
// Used by `publish.test.js` to reload regex when changing `mqtt.base_topic`.
|
||||
// Used by `publish.test.ts` to reload regex when changing `mqtt.base_topic`.
|
||||
export const loadTopicGetSetRegex = (): void => {
|
||||
topicGetSetRegex = new RegExp(`^${settings.get().mqtt.base_topic}/(?!bridge)(.+?)/(get|set)(?:/(.+))?$`);
|
||||
};
|
||||
loadTopicGetSetRegex();
|
||||
|
||||
const STATE_VALUES: ReadonlyArray<string> = ["on", "off", "toggle", "open", "close", "stop", "lock", "unlock"];
|
||||
const SCENE_CONVERTER_KEYS: ReadonlyArray<string> = ["scene_store", "scene_add", "scene_remove", "scene_remove_all", "scene_rename"];
|
||||
@@ -29,6 +29,7 @@ interface ParsedTopic {
|
||||
export default class Publish extends Extension {
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
override async start(): Promise<void> {
|
||||
loadTopicGetSetRegex();
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import * as settings from "../util/settings";
|
||||
import {DEFAULT_BIND_GROUP_ID} from "../util/utils";
|
||||
|
||||
export default class Group {
|
||||
public zh: zh.Group;
|
||||
@@ -23,6 +23,12 @@ export default class Group {
|
||||
this.resolveDevice = resolveDevice;
|
||||
}
|
||||
|
||||
ensureInSettings(): void {
|
||||
if (this.ID !== DEFAULT_BIND_GROUP_ID && !settings.getGroup(this.ID)) {
|
||||
settings.addGroup(this.name, this.ID.toString());
|
||||
}
|
||||
}
|
||||
|
||||
hasMember(device: Device): boolean {
|
||||
return !!device.zh.endpoints.find((e) => this.zh.members.includes(e));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import data from "./data";
|
||||
|
||||
const BASE64_IMAGE_REGEX = /data:image\/(?<extension>.+);base64,(?<data>.+)/;
|
||||
|
||||
export const DEFAULT_BIND_GROUP_ID = 901;
|
||||
|
||||
function pad(num: number): string {
|
||||
const norm = Math.floor(Math.abs(num));
|
||||
return (norm < 10 ? "0" : "") + norm;
|
||||
|
||||
+13
-6
@@ -15,8 +15,7 @@ import utils from "./util/utils";
|
||||
const entityIDRegex = /^(.+?)(?:\/([^/]+))?$/;
|
||||
|
||||
export default class Zigbee {
|
||||
// @ts-expect-error initialized in start
|
||||
private herdsman: Controller;
|
||||
private herdsman!: Controller;
|
||||
private eventBus: EventBus;
|
||||
private groupLookup = new Map<number /* group ID */, Group>();
|
||||
private deviceLookup = new Map<string /* IEEE address */, Device>();
|
||||
@@ -266,12 +265,20 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
private resolveGroup(groupID: number): Group | undefined {
|
||||
const group = this.herdsman.getGroupByID(Number(groupID));
|
||||
if (group && !this.groupLookup.has(groupID)) {
|
||||
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
|
||||
if (!this.groupLookup.has(groupID)) {
|
||||
const group = this.herdsman.getGroupByID(groupID);
|
||||
|
||||
if (group) {
|
||||
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
|
||||
}
|
||||
}
|
||||
|
||||
return this.groupLookup.get(groupID);
|
||||
const group = this.groupLookup.get(groupID);
|
||||
|
||||
if (group) {
|
||||
group.ensureInSettings();
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
resolveEntity(key: string | number | zh.Device): Device | Group | undefined {
|
||||
|
||||
+3
-1
@@ -31,6 +31,7 @@
|
||||
"test": "vitest run --config ./test/vitest.config.mts",
|
||||
"test:coverage": "vitest run --config ./test/vitest.config.mts --coverage",
|
||||
"test:watch": "vitest watch --config ./test/vitest.config.mts",
|
||||
"bench": "vitest bench --run --config ./test/vitest.config.mts",
|
||||
"prepack": "pnpm run clean && pnpm run build",
|
||||
"clean": "rimraf coverage dist tsconfig.tsbuildinfo"
|
||||
},
|
||||
@@ -69,6 +70,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.0.5",
|
||||
"@codspeed/vitest-plugin": "^4.0.1",
|
||||
"@types/finalhandler": "^1.2.3",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
@@ -99,4 +101,4 @@
|
||||
"optionalDependencies": {
|
||||
"unix-dgram": "^2.0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+261
@@ -90,6 +90,9 @@ importers:
|
||||
'@biomejs/biome':
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.6
|
||||
'@codspeed/vitest-plugin':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(vite@6.3.5(@types/node@24.0.10))(vitest@3.2.4(@types/node@24.0.10))
|
||||
'@types/finalhandler':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.4
|
||||
@@ -215,6 +218,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@codspeed/core@4.0.1':
|
||||
resolution: {integrity: sha512-fJ53arfgtzCDZa8DuGJhpTZ3Ll9A1uW5nQ2jSJnfO4Hl5MRD2cP8P4vPvIUAGbdbjwCxR1jat6cW8OloMJkJXw==}
|
||||
|
||||
'@codspeed/vitest-plugin@4.0.1':
|
||||
resolution: {integrity: sha512-aqmrPJzX9cD50UWDsOyih5L5WcEYlNQg3u84sJJ9ZuuLApA51w+LGxk6Xbyb8LJF9n/CwM94HKHV/qArfnvDoQ==}
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
|
||||
vitest: '>=1.2.2'
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
@@ -644,6 +656,12 @@ packages:
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
axios@1.10.0:
|
||||
resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -682,6 +700,10 @@ packages:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
chai@5.2.0:
|
||||
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -712,6 +734,10 @@ packages:
|
||||
colorspace@1.1.4:
|
||||
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
commist@3.2.0:
|
||||
resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==}
|
||||
|
||||
@@ -752,6 +778,10 @@ packages:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -760,6 +790,10 @@ packages:
|
||||
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
@@ -779,9 +813,25 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-module-lexer@1.7.0:
|
||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.25.5:
|
||||
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -840,13 +890,30 @@ packages:
|
||||
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
find-up@6.3.0:
|
||||
resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
fn.name@1.1.0:
|
||||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||
|
||||
follow-redirects@1.15.9:
|
||||
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.3:
|
||||
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -856,6 +923,17 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
hasBin: true
|
||||
@@ -869,10 +947,26 @@ packages:
|
||||
resolution: {integrity: sha512-mTCC51QFadK75MvAhrL5nPVIP291NjML1guo10Sa7Yj04tJU4V++Vgm780NIddg9etQD9D8FM67hFGqM8EE2HQ==}
|
||||
engines: {node: '>= 0.2.5'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
@@ -971,6 +1065,10 @@ packages:
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
locate-path@7.2.0:
|
||||
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
logform@2.7.0:
|
||||
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -995,10 +1093,22 @@ packages:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@3.0.1:
|
||||
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1070,6 +1180,14 @@ packages:
|
||||
one-time@1.0.0:
|
||||
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
||||
|
||||
p-limit@4.0.0:
|
||||
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
p-locate@6.0.0:
|
||||
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
@@ -1080,6 +1198,10 @@ packages:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-exists@5.0.0:
|
||||
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1117,6 +1239,9 @@ packages:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1465,6 +1590,10 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
yocto-queue@1.2.1:
|
||||
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
zigbee-herdsman-converters@24.3.0:
|
||||
resolution: {integrity: sha512-gGqA7A6/ru6lcNPPKmOreJzGl0JayXgcOPfwSxkw0LUtn9Wtct/rAQj5BMa/Rz1Zzwa++Amn8jEJSdx5eCT3sg==}
|
||||
|
||||
@@ -1542,6 +1671,23 @@ snapshots:
|
||||
'@biomejs/cli-win32-x64@2.0.6':
|
||||
optional: true
|
||||
|
||||
'@codspeed/core@4.0.1':
|
||||
dependencies:
|
||||
axios: 1.10.0
|
||||
find-up: 6.3.0
|
||||
form-data: 4.0.3
|
||||
node-gyp-build: 4.8.4
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@codspeed/vitest-plugin@4.0.1(vite@6.3.5(@types/node@24.0.10))(vitest@3.2.4(@types/node@24.0.10))':
|
||||
dependencies:
|
||||
'@codspeed/core': 4.0.1
|
||||
vite: 6.3.5(@types/node@24.0.10)
|
||||
vitest: 3.2.4(@types/node@24.0.10)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
'@dabh/diagnostics@2.0.3':
|
||||
@@ -1884,6 +2030,16 @@ snapshots:
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.10.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9
|
||||
form-data: 4.0.3
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
@@ -1926,6 +2082,11 @@ snapshots:
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
chai@5.2.0:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
@@ -1963,6 +2124,10 @@ snapshots:
|
||||
color: 3.2.1
|
||||
text-hex: 1.0.0
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
commist@3.2.0: {}
|
||||
|
||||
concat-stream@2.0.0:
|
||||
@@ -1992,12 +2157,20 @@ snapshots:
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dns-packet@5.6.1:
|
||||
dependencies:
|
||||
'@leichtgewicht/ip-codec': 2.0.5
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
@@ -2010,8 +2183,23 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
esbuild@0.25.5:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.5
|
||||
@@ -2091,18 +2279,53 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
find-up@6.3.0:
|
||||
dependencies:
|
||||
locate-path: 7.2.0
|
||||
path-exists: 5.0.0
|
||||
|
||||
fn.name@1.1.0: {}
|
||||
|
||||
follow-redirects@1.15.9: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.3:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
glob@10.4.5:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
@@ -2123,8 +2346,20 @@ snapshots:
|
||||
|
||||
glossy@0.1.7: {}
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
@@ -2222,6 +2457,10 @@ snapshots:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
locate-path@7.2.0:
|
||||
dependencies:
|
||||
p-locate: 6.0.0
|
||||
|
||||
logform@2.7.0:
|
||||
dependencies:
|
||||
'@colors/colors': 1.6.0
|
||||
@@ -2251,8 +2490,16 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mime-types@3.0.1:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
@@ -2337,12 +2584,22 @@ snapshots:
|
||||
dependencies:
|
||||
fn.name: 1.1.0
|
||||
|
||||
p-limit@4.0.0:
|
||||
dependencies:
|
||||
yocto-queue: 1.2.1
|
||||
|
||||
p-locate@6.0.0:
|
||||
dependencies:
|
||||
p-limit: 4.0.0
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-exists@5.0.0: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
@@ -2373,6 +2630,8 @@ snapshots:
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
@@ -2747,6 +3006,8 @@ snapshots:
|
||||
|
||||
ws@8.18.3: {}
|
||||
|
||||
yocto-queue@1.2.1: {}
|
||||
|
||||
zigbee-herdsman-converters@24.3.0:
|
||||
dependencies:
|
||||
buffer-crc32: 1.0.0
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
import {existsSync, mkdirSync} from "node:fs";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import {afterEach, beforeEach, bench, describe} from "vitest";
|
||||
import {Zcl, Zdo, ZSpec} from "zigbee-herdsman";
|
||||
import type Adapter from "zigbee-herdsman/dist/adapter/adapter";
|
||||
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
|
||||
import {Device, InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
import {Endpoint} from "zigbee-herdsman/dist/controller/model/endpoint";
|
||||
import Entity from "zigbee-herdsman/dist/controller/model/entity";
|
||||
import {Group} from "zigbee-herdsman/dist/controller/model/group";
|
||||
import type {DeviceType} from "zigbee-herdsman/dist/controller/tstype";
|
||||
import {Foundation} from "zigbee-herdsman/dist/zspec/zcl/definition/foundation";
|
||||
import type {RequestToResponseMap} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
|
||||
import data from "../lib/util/data";
|
||||
|
||||
process.env.ZIGBEE2MQTT_DATA = "data-bench";
|
||||
data._testReload();
|
||||
|
||||
if (!existsSync(data.getPath())) {
|
||||
mkdirSync(data.getPath(), {recursive: true});
|
||||
}
|
||||
|
||||
const createEndpoint = (id: number, ieeeAddr: string, networkAddress: number) => {
|
||||
const ep = Endpoint.create(
|
||||
id,
|
||||
id === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID,
|
||||
id === ZSpec.GP_ENDPOINT ? 0x66 : 0x65,
|
||||
id === ZSpec.GP_ENDPOINT ? [Zcl.Clusters.greenPower.ID] : [Zcl.Clusters.genBasic.ID, Zcl.Clusters.genOnOff.ID],
|
||||
id === ZSpec.GP_ENDPOINT ? [Zcl.Clusters.greenPower.ID] : [Zcl.Clusters.genBasic.ID],
|
||||
networkAddress,
|
||||
ieeeAddr,
|
||||
);
|
||||
ep.save = () => {};
|
||||
|
||||
return ep;
|
||||
};
|
||||
// `Device.create` requires `Database`, bypass it by using the private constructor directly
|
||||
const createDevice = (
|
||||
dbId: number,
|
||||
type: DeviceType,
|
||||
ieeeAddr: string,
|
||||
networkAddress: number,
|
||||
manufacturerID: number | undefined,
|
||||
manufacturerName: string | undefined,
|
||||
powerSource: string | undefined,
|
||||
modelID: string | undefined,
|
||||
): Device => {
|
||||
const haEp = createEndpoint(ZSpec.HA_ENDPOINT, ieeeAddr, networkAddress);
|
||||
const ep2 = createEndpoint(2, ieeeAddr, networkAddress);
|
||||
const endpoints = [haEp, ep2];
|
||||
|
||||
if (type === "Coordinator" || type === "Router") {
|
||||
const gpEp = createEndpoint(ZSpec.GP_ENDPOINT, ieeeAddr, networkAddress);
|
||||
|
||||
endpoints.push(gpEp);
|
||||
}
|
||||
|
||||
// @ts-expect-error mocking private
|
||||
const device = new Device(
|
||||
dbId,
|
||||
type,
|
||||
ieeeAddr,
|
||||
networkAddress,
|
||||
manufacturerID,
|
||||
endpoints,
|
||||
manufacturerName,
|
||||
powerSource,
|
||||
modelID,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
InterviewState.Successful,
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// in-memory only
|
||||
device.save = () => {};
|
||||
|
||||
return device;
|
||||
};
|
||||
// `Group.create` requires `Database`, bypass it by using the private constructor directly
|
||||
const createGroup = (dbId: number, id: number): Group => {
|
||||
// @ts-expect-error mocking private
|
||||
const group = new Group(dbId, id, [], {});
|
||||
|
||||
// in-memory only
|
||||
group.save = () => {};
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const COORD_IEEE = "0x0101010101010101";
|
||||
const EXT_PAN_ID = [0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd];
|
||||
const NETWORK_PARAMS = {
|
||||
panID: 0x1a62,
|
||||
extendedPanID: `0x${Buffer.from(EXT_PAN_ID).toString("hex")}`,
|
||||
channel: 11,
|
||||
nwkUpdateID: 1,
|
||||
};
|
||||
const NETWORK_KEY = [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13];
|
||||
const ZH_DEVICES: Device[] = [];
|
||||
const ZH_GROUPS: Group[] = [];
|
||||
const MANY_DEVICES = 100;
|
||||
|
||||
const initDevices = () => {
|
||||
ZH_DEVICES.splice(0, ZH_DEVICES.length);
|
||||
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
0,
|
||||
"Coordinator",
|
||||
COORD_IEEE,
|
||||
ZSpec.COORDINATOR_ADDRESS,
|
||||
Zcl.ManufacturerCode.SILICON_LABORATORIES,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(1, "Router", "0xf1f1f1f1f1f1f1f1", 0x0001, Zcl.ManufacturerCode.INNR_LIGHTING_BV, "Innr", "Mains (single phase)", "AE 262"),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(2, "EndDevice", "0xe2e2e2e2e2e2e2e2", 0x0002, Zcl.ManufacturerCode.TUYA_GLOBAL_INC, "_TYZB01_kvwjujy9", "Battery", "TS0222"),
|
||||
);
|
||||
ZH_DEVICES.push(createDevice(3, "GreenPower", "0x00000000015d3d3d", 0x0003, undefined, undefined, undefined, "GreenPower_7"));
|
||||
// these have configure, without setTimeout, they hammer really badly (# of fn calls), so, only one of each
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
4,
|
||||
"Router",
|
||||
"0xd3d3d3d3d3d3d3d3",
|
||||
0x0004,
|
||||
Zcl.ManufacturerCode.LEDVANCE_GMBH,
|
||||
"LEDVANCE",
|
||||
"Mains (single phase)",
|
||||
"PLUG OUTDOOR EU T",
|
||||
),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(5, "Router", "0xc4c4c4c4c4c4c4c4", 0x0005, Zcl.ManufacturerCode.INOVELLI, "Inovelli", "Mains (single phase)", "VZM35-SN"),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
6,
|
||||
"Router",
|
||||
"0xb5b5b5b5b5b5b5b5",
|
||||
0x0006,
|
||||
Zcl.ManufacturerCode.SILICON_LABORATORIES,
|
||||
"SMLIGHT",
|
||||
"Mains (single phase)",
|
||||
"SLZB-06Mg24",
|
||||
),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
7,
|
||||
"Router",
|
||||
"0xa6a6a6a6a6a6a6a6",
|
||||
0x0007,
|
||||
Zcl.ManufacturerCode.TUYA_GLOBAL_INC,
|
||||
"_TZE200_p0gzbqct",
|
||||
"Mains (single phase)",
|
||||
"TS0601",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const initGroups = () => {
|
||||
ZH_GROUPS.splice(0, ZH_GROUPS.length);
|
||||
|
||||
ZH_GROUPS.push(createGroup(0, 1));
|
||||
};
|
||||
|
||||
const addManyDevices = () => {
|
||||
for (let i = 0; i < MANY_DEVICES; i++) {
|
||||
const ieee = `0xf1f1f1f1f1f1f1${i.toString(16).padStart(2, "0")}`;
|
||||
|
||||
// device without `configure` (too many calls otherwise)
|
||||
ZH_DEVICES.push(createDevice(1, "Router", ieee, 0x0008 + i, Zcl.ManufacturerCode.INNR_LIGHTING_BV, "Innr", "Mains (single phase)", "AE 262"));
|
||||
}
|
||||
};
|
||||
|
||||
const getMidDeviceIeee = () =>
|
||||
`0xf1f1f1f1f1f1f1${Math.floor(MANY_DEVICES / 2)
|
||||
.toString(16)
|
||||
.padStart(2, "0")}`;
|
||||
|
||||
Device.byIeeeAddr = (ieeeAddr, _includeDeleted) => ZH_DEVICES.find((device) => device.ieeeAddr === ieeeAddr);
|
||||
Device.byType = (type) => ZH_DEVICES.filter((device) => device.type === type);
|
||||
|
||||
const adapter = {
|
||||
sendZclFrameToEndpoint: async (
|
||||
_ieeeAddr: string,
|
||||
networkAddress: number,
|
||||
endpoint: number,
|
||||
zclFrame: Zcl.Frame,
|
||||
_timeout: number,
|
||||
disableResponse: boolean,
|
||||
_disableRecovery: boolean,
|
||||
sourceEndpoint?: number,
|
||||
): Promise<ZclPayload | undefined> => {
|
||||
const payload: {[key: string]: unknown}[] = [];
|
||||
|
||||
if (!disableResponse) {
|
||||
if (zclFrame.header.isGlobal) {
|
||||
switch (zclFrame.command.ID) {
|
||||
case Foundation.read.ID: {
|
||||
for (const attr of zclFrame.payload) {
|
||||
try {
|
||||
const attribute = zclFrame.cluster.getAttribute(attr.attrId);
|
||||
|
||||
if (attribute.type !== Zcl.DataType.NO_DATA && attribute.type < Zcl.DataType.OCTET_STR) {
|
||||
payload.push({
|
||||
attrId: attr.attrId,
|
||||
dataType: attribute.type,
|
||||
attrData: 1,
|
||||
status: 0,
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const messageContents = Zcl.Frame.create(
|
||||
0,
|
||||
Zcl.Direction.SERVER_TO_CLIENT,
|
||||
true,
|
||||
undefined,
|
||||
10,
|
||||
Foundation.readRsp.ID,
|
||||
zclFrame.cluster.ID,
|
||||
payload,
|
||||
{},
|
||||
).toBuffer();
|
||||
|
||||
return await Promise.resolve({
|
||||
clusterID: zclFrame.cluster.ID,
|
||||
header: Zcl.Header.fromBuffer(messageContents),
|
||||
address: networkAddress,
|
||||
data: messageContents,
|
||||
endpoint: sourceEndpoint ?? 1,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
wasBroadcast: false,
|
||||
destinationEndpoint: endpoint,
|
||||
});
|
||||
}
|
||||
case Foundation.configReport.ID: {
|
||||
for (const item of zclFrame.payload) {
|
||||
payload.push({attrId: item.attrId, status: 0, direction: 1});
|
||||
}
|
||||
|
||||
const messageContents = Zcl.Frame.create(
|
||||
0,
|
||||
Zcl.Direction.SERVER_TO_CLIENT,
|
||||
true,
|
||||
undefined,
|
||||
10,
|
||||
Foundation.configReportRsp.ID,
|
||||
zclFrame.cluster.ID,
|
||||
payload,
|
||||
{},
|
||||
).toBuffer();
|
||||
|
||||
return await Promise.resolve({
|
||||
clusterID: zclFrame.cluster.ID,
|
||||
header: Zcl.Header.fromBuffer(messageContents),
|
||||
address: networkAddress,
|
||||
data: messageContents,
|
||||
endpoint: sourceEndpoint ?? 1,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
wasBroadcast: false,
|
||||
destinationEndpoint: endpoint,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.resolve(undefined);
|
||||
},
|
||||
sendZclFrameToGroup: async (_groupID: number, _zclFrame: Zcl.Frame, _sourceEndpoint?: number): Promise<void> => await Promise.resolve(),
|
||||
sendZdo: async (
|
||||
_ieeeAddress: string,
|
||||
_networkAddress: number,
|
||||
clusterId: Zdo.ClusterId,
|
||||
_payload: Buffer,
|
||||
_disableResponse: boolean,
|
||||
): Promise<RequestToResponseMap[keyof RequestToResponseMap] | undefined> => {
|
||||
switch (clusterId) {
|
||||
case Zdo.ClusterId.BIND_REQUEST:
|
||||
case Zdo.ClusterId.UNBIND_REQUEST: {
|
||||
return await Promise.resolve([Zdo.Status.SUCCESS, undefined]);
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.resolve(undefined);
|
||||
},
|
||||
};
|
||||
|
||||
Entity.injectAdapter(adapter as Adapter);
|
||||
|
||||
// use plain type to avoid early import that otherwise messes with data path
|
||||
let controller: import("../lib/controller.js").Controller;
|
||||
const origSetImmediate = global.setImmediate;
|
||||
const origSetTimeout = global.setTimeout;
|
||||
|
||||
const mockGlobalThis = () => {
|
||||
const setImmediateProms: (void | Promise<void>)[] = [];
|
||||
const setTimeoutProms: (void | Promise<void>)[] = [];
|
||||
|
||||
// @ts-expect-error mock
|
||||
globalThis.setImmediate = (callback: () => void) => {
|
||||
setImmediateProms.push(callback());
|
||||
};
|
||||
// @ts-expect-error mock
|
||||
globalThis.setTimeout = (callback: () => void) => {
|
||||
setTimeoutProms.push(callback());
|
||||
};
|
||||
|
||||
return {setImmediateProms, setTimeoutProms};
|
||||
};
|
||||
|
||||
const unmockGlobalThis = () => {
|
||||
globalThis.setImmediate = origSetImmediate;
|
||||
globalThis.setTimeout = origSetTimeout;
|
||||
};
|
||||
|
||||
const settle = async (mockedGlobal: ReturnType<typeof mockGlobalThis>) => {
|
||||
await Promise.allSettled(mockedGlobal.setImmediateProms);
|
||||
await Promise.allSettled(mockedGlobal.setTimeoutProms);
|
||||
await new Promise((resolve) => origSetImmediate(resolve));
|
||||
};
|
||||
|
||||
const initSettings = async (pathValuePairs?: [string[], string | number | boolean][]) => {
|
||||
const settings = await import("../lib/util/settings.js");
|
||||
|
||||
settings.writeMinimalDefaults();
|
||||
// disable logging, too much influence on perf
|
||||
settings.set(["advanced", "log_level"], "error");
|
||||
settings.set(["advanced", "log_output"], []);
|
||||
settings.set(["advanced", "pan_id"], NETWORK_PARAMS.panID);
|
||||
settings.set(["advanced", "ext_pan_id"], EXT_PAN_ID);
|
||||
settings.set(["advanced", "network_key"], NETWORK_KEY);
|
||||
|
||||
if (pathValuePairs) {
|
||||
for (const [path, value] of pathValuePairs) {
|
||||
settings.set(path, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initController = async () => {
|
||||
const {Controller} = await import("../lib/controller.js");
|
||||
|
||||
controller = new Controller(
|
||||
async () => {},
|
||||
async () => {},
|
||||
);
|
||||
|
||||
controller.zigbee.start = async () => {
|
||||
await controller.zigbee.resolveDevicesDefinitions();
|
||||
|
||||
return "resumed";
|
||||
};
|
||||
controller.zigbee.stop = async () => {
|
||||
await Promise.resolve();
|
||||
};
|
||||
// @ts-expect-error mocking private
|
||||
controller.zigbee.herdsman = {
|
||||
isStopping: () => false,
|
||||
getCoordinatorVersion: async () =>
|
||||
Promise.resolve({
|
||||
type: "Dummy",
|
||||
meta: {revision: "9.9.9"},
|
||||
}),
|
||||
getNetworkParameters: async () => Promise.resolve({...NETWORK_PARAMS}),
|
||||
getPermitJoin: () => false,
|
||||
getPermitJoinEnd: () => undefined,
|
||||
getDeviceByIeeeAddr: (ieeeAddr) => ZH_DEVICES.find((device) => device.ieeeAddr === ieeeAddr),
|
||||
getGroupByID: (id) => ZH_GROUPS.find((group) => group.groupID === id),
|
||||
getDevicesByType: (type) => ZH_DEVICES.filter((device) => device.type === type),
|
||||
getDeviceByNetworkAddress: (networkAddress) => ZH_DEVICES.find((device) => device.networkAddress === networkAddress),
|
||||
*getDevicesIterator(predicate) {
|
||||
for (const device of ZH_DEVICES) {
|
||||
if (!predicate || predicate(device)) {
|
||||
yield device;
|
||||
}
|
||||
}
|
||||
},
|
||||
*getGroupsIterator(predicate) {
|
||||
for (const group of ZH_GROUPS) {
|
||||
if (!predicate || predicate(group)) {
|
||||
yield group;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// all dummies, can trigger `controller.mqtt.onMessage(topic, message)` as needed
|
||||
// @ts-expect-error mocking private
|
||||
controller.mqtt.client = {
|
||||
options: {
|
||||
protocolVersion: 5,
|
||||
protocol: "mqtt",
|
||||
host: "localhost",
|
||||
port: 1883,
|
||||
},
|
||||
queue: [],
|
||||
reconnecting: false,
|
||||
disconnecting: false,
|
||||
disconnected: false,
|
||||
endAsync: async () => {},
|
||||
// @ts-expect-error Z2M does not make use of return
|
||||
publishAsync: async () => {},
|
||||
};
|
||||
controller.mqtt.connect = async () => {
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.onConnect();
|
||||
};
|
||||
controller.mqtt.subscribe = async () => {};
|
||||
controller.mqtt.unsubscribe = async () => {};
|
||||
|
||||
// will be in-memory only
|
||||
controller.state.start = () => {};
|
||||
controller.state.stop = () => {};
|
||||
};
|
||||
|
||||
describe("Controller with dummy zigbee/mqtt", () => {
|
||||
describe("defaults start & stop", () => {
|
||||
beforeEach(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings();
|
||||
await initController();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[defaults] start & stop controller",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
await settle(mockedGlobal);
|
||||
|
||||
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
|
||||
throw new Error("Invalid");
|
||||
}
|
||||
|
||||
await controller.stop();
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("HA start & stop", () => {
|
||||
beforeEach(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings([[["homeassistant", "enabled"], true]]);
|
||||
await initController();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[HA] start & stop controller",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
|
||||
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
|
||||
throw new Error("Invalid");
|
||||
}
|
||||
|
||||
await controller.stop();
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("defaults runtime", () => {
|
||||
beforeAll(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings();
|
||||
await initController();
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
await settle(mockedGlobal);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.stop();
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[defaults] receive device message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitDeviceMessage({
|
||||
type: "attributeReport",
|
||||
device: controller.zigbee.resolveEntity("0xf1f1f1f1f1f1f1f1"),
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
cluster: "genOnOff",
|
||||
data: {onOff: 1},
|
||||
meta: {},
|
||||
});
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[defaults] receive MQTT message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[defaults] add group member",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitMQTTMessage({
|
||||
topic: "zigbee2mqtt/bridge/request/group/members/add",
|
||||
message: stringify({
|
||||
device: "0xf1f1f1f1f1f1f1f1",
|
||||
group: `${ZH_GROUPS[0].groupID}`,
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
}),
|
||||
});
|
||||
|
||||
await settle(mockedGlobal);
|
||||
|
||||
if (ZH_GROUPS[0].members.length !== 1) {
|
||||
throw new Error("Invalid state");
|
||||
}
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("defaults/stress runtime", () => {
|
||||
beforeAll(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
addManyDevices();
|
||||
await initSettings();
|
||||
await initController();
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
await settle(mockedGlobal);
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.stop();
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
// this is mostly just to confirm the number of devices does not influence the processing (much)
|
||||
bench(
|
||||
"[defaults/stress] receive device message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitDeviceMessage({
|
||||
type: "attributeReport",
|
||||
device: controller.zigbee.resolveEntity(getMidDeviceIeee()),
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
cluster: "genOnOff",
|
||||
data: {onOff: 1},
|
||||
meta: {},
|
||||
});
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("HA runtime", () => {
|
||||
beforeAll(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings([[["homeassistant", "enabled"], true]]);
|
||||
await initController();
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.stop();
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[HA] receive device message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitDeviceMessage({
|
||||
type: "attributeReport",
|
||||
device: controller.zigbee.resolveEntity("0xf1f1f1f1f1f1f1f1"),
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
cluster: "genOnOff",
|
||||
data: {onOff: 1},
|
||||
meta: {},
|
||||
});
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[HA] receive MQTT message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[HA] receive MQTT discovery message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.mqtt.onMessage(
|
||||
"homeassistant/sensor/0xe2e2e2e2e2e2e2e2/update/config",
|
||||
Buffer.from(stringify({availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}]}), "utf8"),
|
||||
);
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-18
@@ -55,7 +55,6 @@ describe("Controller", () => {
|
||||
let mockExit: Mock;
|
||||
|
||||
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDevice)! as Device;
|
||||
};
|
||||
|
||||
@@ -295,11 +294,9 @@ describe("Controller", () => {
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.publish("z2m/#/status", "empty");
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(`Topic 'z2m/#/status' includes wildcard characters, skipping publish.`);
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.publish("z2m/+/status", "empty");
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(`Topic 'z2m/+/status' includes wildcard characters, skipping publish.`);
|
||||
@@ -503,7 +500,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("Handle mqtt message", async () => {
|
||||
// @ts-expect-error private
|
||||
const spyEventbusEmitMQTTMessage = vi.spyOn(controller.eventBus, "emitMQTTMessage").mockImplementation(vi.fn());
|
||||
|
||||
await controller.start();
|
||||
@@ -514,7 +510,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("Skip MQTT messages on topic we published to", async () => {
|
||||
// @ts-expect-error private
|
||||
const spyEventbusEmitMQTTMessage = vi.spyOn(controller.eventBus, "emitMQTTMessage").mockImplementation(vi.fn());
|
||||
|
||||
await controller.start();
|
||||
@@ -522,7 +517,6 @@ describe("Controller", () => {
|
||||
await mockMQTTEvents.message("zigbee2mqtt/skip-this-topic", "skipped");
|
||||
expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: "zigbee2mqtt/skip-this-topic", message: "skipped"});
|
||||
mockLogger.debug.mockClear();
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.publish("skip-this-topic", "", {});
|
||||
await mockMQTTEvents.message("zigbee2mqtt/skip-this-topic", "skipped");
|
||||
expect(mockLogger.debug).toHaveBeenCalledTimes(0);
|
||||
@@ -844,13 +838,11 @@ describe("Controller", () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
const device = getZ2MDevice("bulb");
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON"});
|
||||
|
||||
await controller.publishEntityState(device, {state: "ON", brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 200, color_temp: 370, state: "ON"});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {qos: 0, retain: true});
|
||||
@@ -870,13 +862,11 @@ describe("Controller", () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
const device = getZ2MDevice("bulb");
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON"});
|
||||
|
||||
await controller.publishEntityState(device, {state: "ON", brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 200, color_temp: 370, state: "ON"});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {qos: 0, retain: true});
|
||||
@@ -1059,10 +1049,7 @@ describe("Controller", () => {
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
|
||||
const retainedMessages = Object.keys(
|
||||
// @ts-expect-error private
|
||||
controller.mqtt.retainedMessages,
|
||||
).length;
|
||||
const retainedMessages = Object.keys(controller.mqtt.retainedMessages).length;
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
await vi.advanceTimersByTimeAsync(2500); // before any startup configure triggers
|
||||
@@ -1083,7 +1070,6 @@ describe("Controller", () => {
|
||||
|
||||
it("Should prevent any message being published with retain flag when force_disable_retain is set", async () => {
|
||||
settings.set(["mqtt", "force_disable_retain"], true);
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.connect();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
@@ -1141,7 +1127,6 @@ describe("Controller", () => {
|
||||
it("Should remove state of removed device when stopped", async () => {
|
||||
await controller.start();
|
||||
const device = getZ2MDevice("bulb");
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON"});
|
||||
device.zh.isDeleted = true;
|
||||
await controller.stop();
|
||||
@@ -1150,7 +1135,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("EventBus should handle sync errors", async () => {
|
||||
// @ts-expect-error private
|
||||
const eventbus = controller.eventBus;
|
||||
const callback = vi.fn().mockImplementation(() => {
|
||||
throw new Error("Whoops!");
|
||||
@@ -1163,7 +1147,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("EventBus should handle async errors", async () => {
|
||||
// @ts-expect-error private
|
||||
const eventbus = controller.eventBus;
|
||||
const callback = vi.fn().mockRejectedValue(new Error("Whoops!"));
|
||||
eventbus.onStateChange({constructor: {name: "Test"}}, callback);
|
||||
|
||||
@@ -281,9 +281,7 @@ describe("Extension: Availability", () => {
|
||||
});
|
||||
|
||||
it("Should retrieve device state when it reconnects", async () => {
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "OFF"});
|
||||
|
||||
const endpoint = devices.bulb_color.getEndpoint(1);
|
||||
|
||||
@@ -10,6 +10,7 @@ import stringify from "json-stable-stringify-without-jsonify";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Bind from "../../lib/extension/bind";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
|
||||
const mocksClear = [
|
||||
mockDebounce,
|
||||
@@ -648,9 +649,9 @@ describe("Extension: Bind", () => {
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/unbind", stringify({from: "remote", to: target}));
|
||||
await flushPromises();
|
||||
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", DEFAULT_BIND_GROUP_ID);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", DEFAULT_BIND_GROUP_ID);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", DEFAULT_BIND_GROUP_ID);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/unbind",
|
||||
stringify({
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {Mock} from "vitest";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Bridge from "../../lib/extension/bridge";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
import utils from "../../lib/util/utils";
|
||||
import utils, {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
|
||||
returnDevices.push(devices.coordinator.ieeeAddr);
|
||||
returnDevices.push(devices.bulb.ieeeAddr);
|
||||
@@ -2288,7 +2288,7 @@ describe("Extension: Bridge", () => {
|
||||
scenes: [],
|
||||
},
|
||||
{friendly_name: "gledopto_group", id: 21, members: [{endpoint: 15, ieee_address: "0x0017880104e45724"}], scenes: []},
|
||||
{friendly_name: "default_bind_group", id: 901, members: [], scenes: []},
|
||||
{friendly_name: "default_bind_group", id: DEFAULT_BIND_GROUP_ID, members: [], scenes: []},
|
||||
{
|
||||
friendly_name: "ha_discovery_group",
|
||||
id: 9,
|
||||
@@ -3147,7 +3147,6 @@ describe("Extension: Bridge", () => {
|
||||
});
|
||||
|
||||
it("Should allow interviewing a device by ieeeAddr", async () => {
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb)!;
|
||||
assert("resolveDefinition" in device);
|
||||
device.resolveDefinition = vi.fn();
|
||||
|
||||
@@ -120,11 +120,9 @@ describe("Extension: Configure", () => {
|
||||
|
||||
it("Should reconfigure reporting on reconfigure event", async () => {
|
||||
expectBulbConfigured();
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb)!;
|
||||
mockClear(device.zh);
|
||||
expectBulbNotConfigured();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitReconfigure({device});
|
||||
await flushPromises();
|
||||
expectBulbConfigured();
|
||||
|
||||
@@ -49,7 +49,6 @@ describe("Extension: ExternalConverters", () => {
|
||||
};
|
||||
|
||||
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDevice)! as Device;
|
||||
};
|
||||
|
||||
@@ -72,7 +71,6 @@ describe("Extension: ExternalConverters", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
zhc.removeExternalDefinitions(); // remove all external converters
|
||||
// @ts-expect-error private - clear cached
|
||||
await controller.zigbee.resolveDevicesDefinitions(true);
|
||||
for (const mock of mocksClear) mock.mockClear();
|
||||
data.writeDefaultConfiguration();
|
||||
|
||||
@@ -44,7 +44,6 @@ describe("Extension: Groups", () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
groups.gledopto_group.command.mockClear();
|
||||
zhcGlobalStore.clear();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
});
|
||||
|
||||
@@ -459,7 +458,6 @@ describe("Extension: Groups", () => {
|
||||
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
|
||||
await flushPromises();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
|
||||
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
|
||||
@@ -506,7 +504,7 @@ describe("Extension: Groups", () => {
|
||||
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/group/members/add",
|
||||
stringify({data: {}, status: "error", error: "Failed to add from group (timeout)"}),
|
||||
stringify({data: {}, status: "error", error: "Failed to add to group (timeout)"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
@@ -591,6 +589,26 @@ describe("Extension: Groups", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("Remove from group via MQTT fails", async () => {
|
||||
const device = devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
const group = groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
endpoint.removeFromGroup.mockImplementationOnce(() => {
|
||||
throw new Error("timeout");
|
||||
});
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1", device: "bulb_color"}));
|
||||
await flushPromises();
|
||||
expect(group.members.length).toStrictEqual(1);
|
||||
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/group/members/remove",
|
||||
stringify({data: {}, status: "error", error: "Failed to remove from group (timeout)"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("Remove from group via MQTT keeping device reporting", async () => {
|
||||
const device = devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
|
||||
@@ -55,7 +55,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
};
|
||||
|
||||
const getZ2MEntity = (zhDeviceOrGroup: string | number | ZhDevice): Device | Group => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDeviceOrGroup)!;
|
||||
};
|
||||
|
||||
@@ -1464,7 +1463,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
assert("ieeeAddr" in device);
|
||||
resetDiscoveryPayloads(device.ieeeAddr);
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitEntityOptionsChanged({entity: device, from: {}, to: {test: 123}});
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(`homeassistant/light/${device.ID}/light/config`, expect.any(String), expect.any(Object));
|
||||
@@ -2384,7 +2382,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
resetDiscoveryPayloads(device.ieeeAddr);
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitScenesChanged({entity: device});
|
||||
await flushPromises();
|
||||
|
||||
@@ -2421,7 +2418,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
resetDiscoveryPayloads("9");
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitScenesChanged({entity: group});
|
||||
await flushPromises();
|
||||
|
||||
@@ -2483,7 +2479,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
availability_mode: "all",
|
||||
};
|
||||
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitMQTTMessage({
|
||||
topic: topic,
|
||||
message: stringify(payload),
|
||||
|
||||
@@ -24,7 +24,6 @@ describe("Extension: OnEvent", () => {
|
||||
let deviceOnEventSpy: MockInstance<DefinitionOnEvent>;
|
||||
|
||||
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDevice)! as Device;
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ describe("Extension: OTAUpdate", () => {
|
||||
devices.bulb.mockClear();
|
||||
updateSpy.mockClear();
|
||||
isUpdateAvailableSpy.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
});
|
||||
|
||||
@@ -747,12 +746,9 @@ describe("Extension: OTAUpdate", () => {
|
||||
});
|
||||
|
||||
it("clear update state on startup", async () => {
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {update: {progress: 100, remaining: 10, state: "updating"}});
|
||||
await resetExtension();
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({update: {state: "available"}});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ describe("Extension: Publish", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
data.writeDefaultConfiguration();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
settings.reRead();
|
||||
loadTopicGetSetRegex();
|
||||
@@ -1256,11 +1255,8 @@ describe("Extension: Publish", () => {
|
||||
|
||||
it("Home Assistant: should not set state when color temperature is also set and device is already on", async () => {
|
||||
settings.set(["homeassistant"], {enabled: true});
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!;
|
||||
// @ts-expect-error private
|
||||
controller.state.remove(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "ON"});
|
||||
const endpoint = device.zh.getEndpoint(1)!;
|
||||
const payload = {state: "ON", color_temp: 100};
|
||||
@@ -1275,11 +1271,8 @@ describe("Extension: Publish", () => {
|
||||
|
||||
it("Home Assistant: should set state when color temperature is also set and device is off", async () => {
|
||||
settings.set(["homeassistant"], {enabled: true});
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!;
|
||||
// @ts-expect-error private
|
||||
controller.state.remove(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "OFF"});
|
||||
const endpoint = device.zh.getEndpoint(1)!;
|
||||
const payload = {state: "ON", color_temp: 100};
|
||||
@@ -1298,11 +1291,8 @@ describe("Extension: Publish", () => {
|
||||
|
||||
it("Home Assistant: should not set state when color is also set", async () => {
|
||||
settings.set(["homeassistant"], {enabled: true});
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!;
|
||||
// @ts-expect-error private
|
||||
controller.state.remove(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "ON"});
|
||||
const endpoint = device.zh.getEndpoint(1)!;
|
||||
const payload = {state: "ON", color: {x: 0.41, y: 0.25}};
|
||||
|
||||
@@ -24,7 +24,6 @@ describe("Extension: Receive", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
@@ -339,7 +338,6 @@ describe("Extension: Receive", () => {
|
||||
type: "attributeReport",
|
||||
linkquality: 10,
|
||||
});
|
||||
// @ts-expect-error private
|
||||
const realDevice = controller.zigbee.resolveEntity(device);
|
||||
|
||||
// Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache.
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {AdapterTypes} from "zigbee-herdsman";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
import type {EventHandler} from "./utils";
|
||||
|
||||
type ZHConfiguredReporting = {
|
||||
@@ -505,7 +505,7 @@ export const groups = {
|
||||
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(901, []),
|
||||
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]]),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import codspeedPlugin from "@codspeed/vitest-plugin";
|
||||
import {defineConfig} from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [codspeedPlugin()],
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(_log: string, _type: "stdout" | "stderr"): boolean | undefined {
|
||||
|
||||
Reference in New Issue
Block a user