Compare commits

..
Author SHA1 Message Date
Koen Kanters 0259cc11f0 chore(generateChangelog): make calls authenticated to avoid rate limits 2026-08-30 18:48:13 +00:00
Koen Kanters b1aa5274e1 chore(generateChangelog): improve error logging 2026-08-30 18:46:11 +00:00
Koen Kanters ce064c6d5f fix(ignore): update zigbee-herdsman-converters to 26.102.0 (#32976) 2026-08-30 17:54:33 +00:00
Koen Kanters af932b0b41 fix: Require Node <=26.2 for serialport (#32975) 2026-08-30 19:49:25 +02:00
Koen Kanters 467ef4f405 fix(ignore): update zigbee-herdsman-converters to 26.101.0 (#32973) 2026-08-29 18:35:33 +00:00
Koen Kanters 01b25f15bf fix(ignore): update zigbee-herdsman-converters to 26.100.0 (#32960) 2026-08-28 19:12:35 +00:00
Andy b441091f6f fix: Warn when setting unsupported device option (#32928) 2026-08-28 20:12:23 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9a6b25afa5 fix(ignore): bump the minor-patch group with 3 updates (#32949)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-28 19:48:10 +02:00
Koen Kanters 9473312667 fix(ignore): update zigbee-herdsman to 10.9.1 (#32959) 2026-08-28 17:46:29 +00:00
Michal Feix 22de3f10d8 Solved linting issues in PR from CI test reports. 2026-08-26 12:08:50 +02:00
Michal Feix c867ca5182 fix: Improve Home Assistant cover state discovery 2026-08-26 12:08:50 +02:00
Luan TaraschiandKoen Kanters 7fbbbc3b17 fix: Preserve duration in state (#32900)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-08-26 12:02:15 +02:00
venglinandClaude Opus 5 dccf0a98c6 fix: Clamp availability ping delay to the maximum supported timeout (#32937)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:00:09 +02:00
Koen Kanters cf11b1a188 fix(ignore): update zigbee-herdsman-converters to 26.99.0 (#32899) 2026-08-22 02:57:58 +00:00
Koen Kanters 75bb5eed00 fix(ignore): update zigbee-herdsman-converters to 26.98.0 (#32879) 2026-08-18 19:58:29 +00:00
Miguel CabralandKoen Kanters 95d072a788 feat: Allow to preserve device config when removing device (#32765)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-08-18 21:13:38 +02:00
AT0myks be1cbb5f3b fix: Home Assistant: fix motor_state not exposed for SONOFF MINI-ZBRBS (#32702) 2026-08-18 20:59:53 +02:00
Koen Kanters 07ae13ad2d fix(ignore): update zigbee-herdsman-converters to 26.97.0 (#32848) 2026-08-14 19:09:33 +00:00
burmistrzak c649d04b1c fix: Home Assistant: Improve various discovery mappings (#32842) 2026-08-14 19:14:31 +02:00
Koen Kanters d3ba21828c fix(ignore): update zigbee-herdsman-converters to 26.96.0 (#32841) 2026-08-13 19:11:09 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d276190ab8 fix(ignore): bump the minor-patch group across 1 directory with 2 updates (#32827)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-12 21:51:16 +02:00
Koen Kanters e7deca42bf fix(ignore): update zigbee-herdsman-converters to 26.95.0 (#32829) 2026-08-12 19:44:01 +00:00
Koen Kanters 07a4a0bc75 fix(ignore): update zigbee-herdsman to 10.9.0 (#32828) 2026-08-12 19:37:54 +00:00
Koen Kanters 368e31b1e3 fix(ignore): update zigbee-herdsman-converters to 26.94.0 (#32808) 2026-08-10 19:36:01 +00:00
ChillZwixandChillZwix 10611654c7 fix: Apply debounce to messages without payload (#32791)
Co-authored-by: ChillZwix <ChillZwix@users.noreply.github.com>
2026-08-10 20:21:48 +02:00
Igor ClemencovandKoen Kanters f06673506f fix: Home Assistant: avoid duplicate door names (#32752)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-08-08 21:42:16 +02:00
Koen Kanters 06290da5be fix(ignore): update zigbee-herdsman-converters to 26.93.0 (#32790) 2026-08-08 21:37:21 +02:00
Luan Taraschi 86dfc90869 fix: Fix stringify don't emit invalid JSON for an empty typed array (#32780) 2026-08-08 21:29:08 +02:00
Luan Taraschi 60786528ae fix: Fix stringify emit every element of a typed array (#32779) 2026-08-08 21:28:21 +02:00
Luan Taraschi 3aaa8c1546 fix: Fix crash on a null color in attribute output (#32784) 2026-08-08 21:27:36 +02:00
Koen Kanters 046817624f fix(ignore): update zigbee-herdsman-converters to 26.92.0 (#32757) 2026-08-05 20:09:18 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 106a4bdef2 fix(ignore): bump the minor-patch group with 2 updates (#32755)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-05 21:34:31 +02:00
Koen Kanters a8171a5464 fix(ignore): update zigbee-herdsman-converters to 26.91.0 (#32746) 2026-08-03 19:15:56 +00:00
Koen Kanters 2f976b7dca fix(ignore): update zigbee-herdsman to 10.8.1 (#32745) 2026-08-03 21:10:16 +02:00
Alexander ChepurnoyandKoen Kanters 29b041ce57 fix: Replace express-static-gzip and finalhandler with srvx (#32685)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-08-03 21:05:35 +02:00
Alexander Chepurnoy 828038717f fix: Replace object-assign-deep with local utility (#32684) 2026-08-03 20:54:54 +02:00
github-actions[bot] 5fd4d6b37a chore: promote to dev 2026-08-01 06:04:30 +00:00
35 changed files with 1052 additions and 962 deletions
+4 -1
View File
@@ -465,7 +465,10 @@ export class Controller {
let message: string | undefined;
// Special cases
if (key === "color" && utils.objectHasProperties(subPayload, ["r", "g", "b"])) {
// `objectHasProperties` indexes its argument, so it has to be given an object.
// The null check three lines below is too late: `color` is nullable like any
// other attribute, and a null one reaches here before that branch runs.
if (key === "color" && subPayload != null && utils.objectHasProperties(subPayload, ["r", "g", "b"])) {
subPayload = [subPayload.r, subPayload.g, subPayload.b];
}
+13 -1
View File
@@ -9,6 +9,12 @@ import * as settings from "../util/settings";
import utils from "../util/utils";
import Extension from "./extension";
/**
* Upper bound for a `setTimeout` delay. Node.js stores the delay as a 32-bit signed integer; anything above this
* is coerced to `1`, which would turn an ever-growing backoff into a tight loop instead of an ever-longer wait.
*/
const MAX_TIMEOUT = 2147483647;
const RETRIEVE_ON_RECONNECT: readonly {keys: string[]; condition?: (state: KeyValue) => boolean}[] = [
{keys: ["state"]},
{keys: ["brightness"], condition: (state: KeyValue): boolean => state.state === "ON"},
@@ -108,7 +114,10 @@ export default class Availability extends Extension {
// If device did not check in, ping it, if that fails it will be marked as offline
this.timers.set(
device.ieeeAddr,
setTimeout(this.addToPingQueue.bind(this, device), (this.getTimeout(device) + utils.seconds(1) + jitter) * backoff),
setTimeout(
this.addToPingQueue.bind(this, device),
Math.min((this.getTimeout(device) + utils.seconds(1) + jitter) * backoff, MAX_TIMEOUT),
),
);
}
} else {
@@ -322,6 +331,9 @@ export default class Availability extends Extension {
options,
state,
device: device.zh,
/* v8 ignore start */
deviceExposesChanged: (): void => this.eventBus.emitExposesAndDevicesChanged(device),
/* v8 ignore stop */
/* v8 ignore next */
publish: (payload: KeyValue) => this.publishEntityState(device, payload),
};
+28 -4
View File
@@ -2,7 +2,6 @@ import fs from "node:fs";
import path from "node:path";
import bind from "bind-decorator";
import {zip} from "fflate";
import objectAssignDeep from "object-assign-deep";
import type winston from "winston";
import Transport from "winston-transport";
import {Zcl} from "zigbee-herdsman";
@@ -13,6 +12,7 @@ import type Group from "../model/group";
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from "../types/api";
import data from "../util/data";
import logger from "../util/logger";
import {objectAssignDeep} from "../util/objectAssignDeep";
import * as settings from "../util/settings";
import {stringify} from "../util/stringify";
import utils, {assertString, DEFAULT_BIND_GROUP_ID} from "../util/utils";
@@ -466,6 +466,20 @@ export default class Bridge extends Extension {
const ID = message.id;
const entity = this.getEntity(entityType, ID);
if (entity instanceof Device) {
const supportedOptions = new Set(Object.keys(settings.schemaJson.definitions.device.properties));
for (const option of entity.definition?.options ?? []) {
supportedOptions.add(option.property);
}
for (const option of Object.keys(message.options)) {
if (!supportedOptions.has(option)) {
logger.warning(`Device '${ID}' does not support option '${option}'`);
}
}
}
const oldOptions = objectAssignDeep({}, cleanup(entity.options));
if (message.options.icon) {
@@ -677,6 +691,7 @@ export default class Bridge extends Extension {
const friendlyName = entity.name;
let block = false;
let force = false;
let keepConfig = false;
let clearCache = false;
let blockForceLog = "";
@@ -684,8 +699,9 @@ export default class Bridge extends Extension {
const payload = message as Zigbee2MQTTAPI["bridge/request/device/remove"];
block = !!payload.block;
force = !!payload.force;
keepConfig = !!payload.keep_config;
clearCache = !!payload.clear_cache;
blockForceLog = ` (block: ${block}, force: ${force}, clear cache: ${clearCache})`;
blockForceLog = ` (block: ${block}, force: ${force}, keep config: ${keepConfig}, clear cache: ${clearCache})`;
} else if (entityType === "group" && messageIsObject) {
const payload = message as Zigbee2MQTTAPI["bridge/request/group/remove"];
force = !!payload.force;
@@ -710,7 +726,9 @@ export default class Bridge extends Extension {
this.zigbee.removeDeviceFromLookup(entity.ID);
}
settings.removeDevice(entity.ID as string);
if (!keepConfig) {
settings.removeDevice(entity.ID as string);
}
} else {
if (force) {
entity.zh.removeFromDatabase();
@@ -739,7 +757,13 @@ export default class Bridge extends Extension {
// Refresh Cluster definition
await this.publishDefinitions();
const responseData: Zigbee2MQTTAPI["bridge/response/device/remove"] = {id: ID, block, force, clear_cache: clearCache};
const responseData: Zigbee2MQTTAPI["bridge/response/device/remove"] = {
id: ID,
block,
force,
keep_config: keepConfig,
clear_cache: clearCache,
};
return utils.getResponse(message, responseData);
}
+18 -26
View File
@@ -6,12 +6,11 @@ import {createServer as createSecureServer} from "node:https";
import type {Socket} from "node:net";
import {posix} from "node:path";
import bind from "bind-decorator";
import expressStaticGzip from "express-static-gzip";
import finalhandler from "finalhandler";
import WebSocket from "ws";
import data from "../util/data";
import logger from "../util/logger";
import * as settings from "../util/settings";
import {createStaticFileServer, sendNotFound} from "../util/staticFileServer";
import {stringify} from "../util/stringify";
import utils from "../util/utils";
import Extension from "./extension";
@@ -65,46 +64,39 @@ export class Frontend extends Extension {
return false;
};
const options: expressStaticGzip.ExpressStaticGzipOptions = {
enableBrotli: true,
serveStatic: {
/* v8 ignore start */
setHeaders: (res: ServerResponse, path: string): void => {
if (path.endsWith("index.html")) {
res.setHeader("Cache-Control", "no-store");
}
},
/* v8 ignore stop */
},
};
const frontend = (await import(settings.get().frontend.package)) as typeof import("zigbee2mqtt-frontend");
const fileServer = expressStaticGzip(frontend.default.getPath(), options);
const deviceIconsFileServer = expressStaticGzip(data.joinPath("device_icons"), options);
const logError = logger.error.bind(logger);
const fileServer = createStaticFileServer(frontend.default.getPath(), logError);
const deviceIconsFileServer = createStaticFileServer(data.joinPath("device_icons"), logError);
const onRequest = (request: IncomingMessage, response: ServerResponse): void => {
const next = finalhandler(request, response);
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
const newUrl = posix.relative(this.baseUrl, request.url!);
const url = request.url!;
const newUrl = posix.relative(this.baseUrl, url);
// The request url is not within the frontend base url, so the relative path starts with '..'
if (newUrl.startsWith(".")) {
next();
sendNotFound(request, response);
return;
}
// The base url itself is a directory, redirect to its trailing slash form so the browser resolves the
// relative asset paths in `index.html` against the frontend root instead of against its parent.
if (newUrl === "" && !url.endsWith("/")) {
response.writeHead(301, {Location: `${url}/`});
response.end();
return;
}
// Attach originalUrl so that static-server can perform a redirect to '/' when serving the root directory.
// This is necessary for the browser to resolve relative assets paths correctly.
request.originalUrl = request.url;
request.url = `/${newUrl}`;
request.path = request.url;
if (newUrl.startsWith("device_icons/")) {
request.path = request.path.replace("device_icons/", "");
request.url = request.url.replace("/device_icons", "");
deviceIconsFileServer(request, response, next);
deviceIconsFileServer(request, response);
} else {
fileServer(request, response, next);
fileServer(request, response);
}
};
+49 -4
View File
@@ -157,6 +157,18 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
boost_heating_countdown_time_set: {entity_category: "config", icon: "mdi:timer"},
boost_time: {entity_category: "config", icon: "mdi:timer"},
calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_button_hold_time: {
enabled_by_default: false,
entity_category: "config",
icon: "mdi:wrench-clock",
},
calibration_closing_time: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_motor_start_delay: {
enabled_by_default: false,
entity_category: "config",
icon: "mdi:wrench-clock",
},
calibration_opening_time: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time_left: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time_right: {entity_category: "config", icon: "mdi:wrench-clock"},
@@ -188,6 +200,11 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
duration: {entity_category: "config", icon: "mdi:timer"},
eco2: {device_class: "volatile_organic_compounds_parts", state_class: "measurement"},
eco_temperature: {entity_category: "config", icon: "mdi:thermometer"},
effect_speed: {
enabled_by_default: false,
entity_category: "config",
icon: "mdi:motion-outline",
},
energy: {device_class: "energy", state_class: "total_increasing"},
external_temperature_input: {device_class: "temperature", icon: "mdi:thermometer"},
external_temperature: {device_class: "temperature", icon: "mdi:thermometer", state_class: "measurement"},
@@ -323,7 +340,7 @@ const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
effect: {enabled_by_default: false, icon: "mdi:palette"},
force: {entity_category: "config", icon: "mdi:valve"},
keep_time: {entity_category: "config", icon: "mdi:av-timer"},
identify: {device_class: "identify"},
identify: {entity_category: "diagnostic", device_class: "identify"},
keypad_lockout: {entity_category: "config", icon: "mdi:lock"},
load_detection_mode: {entity_category: "config", icon: "mdi:tune"},
load_dimmable: {entity_category: "config", icon: "mdi:chart-bell-curve"},
@@ -333,6 +350,7 @@ const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
mode: {entity_category: "config", icon: "mdi:tune"},
mode_switch: {icon: "mdi:tune"},
motor_direction: {entity_category: "config", icon: "mdi:arrow-left-right"},
motor_state: {entity_category: "diagnostic", icon: "mdi:state-machine"},
motion_sensitivity: {entity_category: "config", icon: "mdi:tune"},
operation_mode: {entity_category: "config", icon: "mdi:tune"},
power_on_behavior: {entity_category: "config", icon: "mdi:power-settings"},
@@ -359,6 +377,11 @@ const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
const LIST_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
action: {icon: "mdi:gesture-double-tap"},
color_options: {icon: "mdi:palette"},
effect_color: {
enabled_by_default: false,
entity_category: "config",
icon: "mdi:palette-swatch",
},
level_config: {entity_category: "diagnostic"},
programming_mode: {icon: "mdi:calendar-clock"},
schedule_settings: {entity_category: "config", icon: "mdi:calendar-clock"},
@@ -401,6 +424,10 @@ const applyHomeAssistantExposeMetadata = (payload: DiscoveryEntry, homeAssistant
payload.discovery_payload.icon = homeAssistant.icon;
}
if (homeAssistant.name !== undefined) {
payload.discovery_payload.name = homeAssistant.name;
}
if (homeAssistant.valueTemplate !== undefined) {
if (homeAssistant.valueTemplate === null) {
delete payload.discovery_payload.value_template;
@@ -934,7 +961,7 @@ export class HomeAssistant extends Extension {
?.features.find((f) => f.name === "tilt");
const motorState = allExposes
?.filter(isEnumExpose)
.find((e) => ["motor_state", "moving"].includes(e.name) && e.access === ACCESS_STATE);
.find((e) => ["motor_state", "moving"].includes(e.name) && e.access & ACCESS_STATE);
const running = allExposes?.filter(isBinaryExpose)?.find((e) => e.name === "running");
const discoveryEntry: DiscoveryEntry = {
@@ -960,6 +987,9 @@ export class HomeAssistant extends Extension {
// If curtains have `motor_state` or `moving` property, lookup for possible
// state names to detect movement direction and use this in discovery.
if (motorState) {
const motorStateProperty = featurePropertyWithoutEndpoint(motorState);
const stateProperty = featurePropertyWithoutEndpoint(state);
const openingState = motorState.values.find((s) => COVER_OPENING_LOOKUP.includes(s.toString().toLowerCase()));
const closingState = motorState.values.find((s) => COVER_CLOSING_LOOKUP.includes(s.toString().toLowerCase()));
const stoppedState = motorState.values.find((s) => COVER_STOPPED_LOOKUP.includes(s.toString().toLowerCase()));
@@ -967,8 +997,19 @@ export class HomeAssistant extends Extension {
if (openingState && closingState && stoppedState) {
discoveryEntry.discovery_payload.state_opening = openingState;
discoveryEntry.discovery_payload.state_closing = closingState;
discoveryEntry.discovery_payload.state_open = "OPEN";
discoveryEntry.discovery_payload.state_closed = "CLOSE";
discoveryEntry.discovery_payload.state_stopped = stoppedState;
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json and value_json["${featurePropertyWithoutEndpoint(motorState)}"] %} {{ value_json["${featurePropertyWithoutEndpoint(motorState)}"] }} {% else %} ${stoppedState} {% endif %}`;
discoveryEntry.discovery_payload.value_template =
`{% if "${motorStateProperty}" in value_json and value_json["${motorStateProperty}"] == "${openingState}" %}` +
`${openingState}` +
`{% elif "${motorStateProperty}" in value_json and value_json["${motorStateProperty}"] == "${closingState}" %}` +
`${closingState}` +
`{% elif "${stateProperty}" in value_json %}` +
`{{ value_json["${stateProperty}"] }}` +
"{% else %}" +
`${stoppedState}` +
"{% endif %}";
}
}
@@ -1443,7 +1484,11 @@ export class HomeAssistant extends Extension {
// Let Home Assistant generate entity name when device_class is present.
// preserve_name allows device_class and explicit name to coexist (e.g. derived sensors).
if (entry.discovery_payload.device_class && !NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.preserve_name) {
if (
entry.discovery_payload.device_class &&
entry.discovery_payload.name !== null &&
!NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.preserve_name
) {
delete entry.discovery_payload.name;
}
+5
View File
@@ -224,6 +224,11 @@ export default class Publish extends Extension {
state: entityState,
membersState,
mapped: definition,
/* v8 ignore start */
deviceExposesChanged: (): void => {
if (re instanceof Device) this.eventBus.emitExposesAndDevicesChanged(re);
},
/* v8 ignore stop */
/* v8 ignore next */
publish: (payload: KeyValue) => this.publishEntityState(re, payload),
};
+5 -2
View File
@@ -180,8 +180,11 @@ export default class Receive extends Extension {
if (!utils.objectIsEmpty(payload)) {
await publish(payload);
} else {
await utils.publishLastSeen({device: data.device, reason: "messageEmitted"}, settings.get(), true, this.publishEntityState);
} else if (settings.get().advanced.last_seen && settings.get().advanced.last_seen !== "disable") {
// A message was received that produced no payload (e.g. a frame the converter has no data
// for). Publish through the regular publish() path so the per-device debounce/throttle
// still applies, instead of publishing the full cached state immediately via publishLastSeen.
await publish({});
}
}
}
+1 -3
View File
@@ -1,9 +1,8 @@
import {existsSync, readFileSync, writeFileSync} from "node:fs";
import objectAssignDeep from "object-assign-deep";
import data from "./util/data";
import logger from "./util/logger";
import {objectAssignDeep} from "./util/objectAssignDeep";
import * as settings from "./util/settings";
import utils from "./util/utils";
@@ -23,7 +22,6 @@ const CACHE_IGNORE_PROPERTIES = [
"no_occupancy_since",
"step_mode",
"transition_time",
"duration",
"elapsed",
"from_side",
"to_side",
+8 -6
View File
@@ -59,7 +59,7 @@ export type OnboardData = OnboardInitData | OnboardDoneData | OnboardFailureData
export type OnboardSubmitResponse = {success: true; frontendUrl: string | null} | {success: false; error: string};
export interface Zigbee2MQTTDeviceOptions {
export type Zigbee2MQTTDeviceOptions = {
disabled?: boolean;
retention?: number;
availability?:
@@ -83,9 +83,9 @@ export interface Zigbee2MQTTDeviceOptions {
description?: string;
qos?: 0 | 1 | 2;
disable_automatic_update_check?: boolean;
}
};
export interface Zigbee2MQTTGroupOptions {
export type Zigbee2MQTTGroupOptions = {
ID: number;
optimistic?: boolean;
off_state?: "all_members_off" | "last_member_state";
@@ -96,9 +96,9 @@ export interface Zigbee2MQTTGroupOptions {
friendly_name: string;
description?: string;
qos?: 0 | 1 | 2;
}
};
export interface Zigbee2MQTTSettings {
export type Zigbee2MQTTSettings = {
version?: number;
/** only used internally during startup, removed on successful Z2M start */
onboarding?: true;
@@ -224,7 +224,7 @@ export interface Zigbee2MQTTSettings {
interval: number;
reset_on_check: boolean;
};
}
};
export interface Zigbee2MQTTScene {
id: number;
@@ -622,6 +622,7 @@ export interface Zigbee2MQTTAPI {
id: string;
block?: boolean;
force?: boolean;
keep_config?: boolean;
clear_cache?: boolean;
};
@@ -629,6 +630,7 @@ export interface Zigbee2MQTTAPI {
id: string;
block: boolean;
force: boolean;
keep_config: boolean;
clear_cache: boolean;
};
+6
View File
@@ -10,6 +10,12 @@ declare global {
const removeEventListener: import("node:events").EventEmitter["removeListener"];
/** @deprecated DOM SHIM, DO NOT USE */
const postMessage: import("node:worker_threads").MessagePort["postMessage"];
/**
* Required by `srvx` <= 0.12.5, remove once a release including https://github.com/h3js/srvx/pull/288 is out.
*
* @deprecated DOM SHIM, DO NOT USE
*/
type HeadersInit = string[][] | Record<string, string> | Headers;
}
export {};
+3
View File
@@ -26,6 +26,9 @@ declare global {
type PublishEntityState = (entity: Device | Group, payload: KeyValue, stateChangeReason?: StateChangeReason) => Promise<void>;
type RecursivePartial<T> = {[P in keyof T]?: RecursivePartial<T[P]>};
type MakePartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
/** Convert `A | B | C` into `A & B & C` */
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional requires `any`
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
interface KeyValue {
// biome-ignore lint/suspicious/noExplicitAny: API
[s: string]: any;
-7
View File
@@ -5,10 +5,3 @@ declare module "zigbee2mqtt-frontend" {
export default frontend;
}
declare module "node:http" {
interface IncomingMessage {
originalUrl?: string;
path?: string;
}
}
+67
View File
@@ -0,0 +1,67 @@
/** Anything mergeable: a plain-ish object, explicitly not an array or another iterable. */
export type UnknownRecord = Record<string | number, unknown> & {[Symbol.iterator]?: never};
function isUnknownRecord(value: unknown): value is UnknownRecord {
return value != null && typeof value === "object" && !Array.isArray(value);
}
function cloneArray(input: readonly unknown[]): unknown[] {
const len = input.length;
const output: unknown[] = new Array(len);
for (let i = 0; i < len; i++) {
const val = input[i];
output[i] = isUnknownRecord(val) ? cloneObject(val) : Array.isArray(val) ? cloneArray(val) : val;
}
return output;
}
function cloneObject(input: UnknownRecord): UnknownRecord {
const output: UnknownRecord = {};
for (const key of Object.keys(input)) {
if (key !== "__proto__" && key !== "constructor" && key !== "prototype") {
const val = input[key];
output[key] = isUnknownRecord(val) ? cloneObject(val) : Array.isArray(val) ? cloneArray(val) : val;
}
}
return output;
}
/**
* Merge all sources into `target` recursively.
*
* Key behavior:
* - ignore properties `__proto__`, `constructor` & `prototype`
* - assumes no infinite circular possible (unhandled for perf)
*
* Pass empty object `{}` as `target` to return a new object without modifying any existing objects.
*/
export function objectAssignDeep<T extends UnknownRecord, S extends readonly UnknownRecord[]>(
target: T,
...sources: S
): T & UnionToIntersection<S[number]> {
for (const source of sources) {
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue;
}
const value = source[key];
if (isUnknownRecord(value)) {
const existing = target[key];
(target as UnknownRecord)[key] = isUnknownRecord(existing) ? objectAssignDeep({}, existing, value) : cloneObject(value);
} else if (Array.isArray(value)) {
(target as UnknownRecord)[key] = cloneArray(value);
} else {
(target as UnknownRecord)[key] = value;
}
}
}
return target as T & UnionToIntersection<S[number]>;
}
+5 -25
View File
@@ -1,31 +1,15 @@
import {existsSync, mkdirSync, writeFileSync} from "node:fs";
import type {ServerResponse} from "node:http";
import {createServer} from "node:http";
import path from "node:path";
import expressStaticGzip from "express-static-gzip";
import {type Unzipped, unzip} from "fflate";
import finalhandler from "finalhandler";
import {findAllDevices} from "zigbee-herdsman/dist/adapter/adapterDiscovery";
import type {OnboardData, OnboardFailureData, OnboardSubmitResponse, Zigbee2MQTTSettings} from "../types/api";
import {stringify} from "../util/stringify";
import data from "./data";
import * as settings from "./settings";
import {createStaticFileServer} from "./staticFileServer";
import {YAMLFileException} from "./yaml";
/** same as extension/frontend */
const FILE_SERVER_OPTIONS: expressStaticGzip.ExpressStaticGzipOptions = {
enableBrotli: true,
serveStatic: {
/* v8 ignore start */
setHeaders: (res: ServerResponse, path: string): void => {
if (path.endsWith("index.html")) {
res.setHeader("Cache-Control", "no-store");
}
},
/* v8 ignore stop */
},
};
function getServerUrl(): URL {
return new URL(process.env.Z2M_ONBOARD_URL ?? "http://0.0.0.0:8080");
}
@@ -72,7 +56,7 @@ async function startOnboardingServer(): Promise<boolean> {
const currentSettings = settings.get();
const serverUrl = getServerUrl();
let server: ReturnType<typeof createServer> | undefined;
const fileServer = expressStaticGzip((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), FILE_SERVER_OPTIONS);
const fileServer = createStaticFileServer((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), console.error);
const success = await new Promise<boolean>((resolve) => {
server = createServer(async (req, res) => {
@@ -196,9 +180,7 @@ async function startOnboardingServer(): Promise<boolean> {
}
}
const next = finalhandler(req, res);
fileServer(req, res, next);
fileServer(req, res);
});
server.on("error", (error: Error) => {
@@ -219,7 +201,7 @@ async function startOnboardingServer(): Promise<boolean> {
async function startFailureServer(errors: string[]): Promise<void> {
const serverUrl = getServerUrl();
let server: ReturnType<typeof createServer> | undefined;
const fileServer = expressStaticGzip((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), FILE_SERVER_OPTIONS);
const fileServer = createStaticFileServer((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), console.error);
await new Promise<void>((resolve) => {
server = createServer((req, res) => {
@@ -244,9 +226,7 @@ async function startFailureServer(errors: string[]): Promise<void> {
return;
}
const next = finalhandler(req, res);
fileServer(req, res, next);
fileServer(req, res);
});
server.listen(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, () => {
+7 -7
View File
@@ -1,8 +1,8 @@
import path from "node:path";
import type {ValidateFunction} from "ajv";
import Ajv from "ajv";
import objectAssignDeep from "object-assign-deep";
import data from "./data";
import {objectAssignDeep} from "./objectAssignDeep";
import schemaJson from "./settings.schema.json";
import utils from "./utils";
import yaml from "./yaml";
@@ -231,7 +231,10 @@ export function write(): void {
const writeDevicesOrGroups = (type: "devices" | "groups"): void => {
if (typeof actual[type] === "string" || (Array.isArray(actual[type]) && actual[type].length > 0)) {
const fileToWrite = Array.isArray(actual[type]) ? actual[type][0] : actual[type];
const content = objectAssignDeep({}, settings[type]);
// `readDevicesOrGroups()` already set this to an object whenever the config points at separate files, but the
// persisted settings are `Partial`, so the fallback is only here to satisfy the type
/* v8 ignore next */
const content = objectAssignDeep({}, settings[type] ?? {});
// If an array, only write to first file and only devices which are not in the other files.
if (Array.isArray(actual[type])) {
@@ -368,8 +371,7 @@ function read(): Partial<Settings> {
s[type] = {};
for (const file of files) {
const content = yaml.readIfExists(data.joinPath(file));
// @ts-expect-error noMutate not typed properly
s[type] = objectAssignDeep.noMutate(s[type], content);
s[type] = objectAssignDeep({}, s[type], content);
}
}
};
@@ -479,9 +481,7 @@ export function set(path: string[], value: string | number | boolean | KeyValue)
}
export function apply(settings: Record<string, unknown>, throwOnError = true): boolean {
getPersistedSettings(); // Ensure _settings is initialized.
// @ts-expect-error noMutate not typed properly
const newSettings = objectAssignDeep.noMutate(_settings, settings);
const newSettings = objectAssignDeep({}, getPersistedSettings(), settings);
utils.removeNullPropertiesFromObject(newSettings, NULLABLE_SETTINGS);
+65
View File
@@ -0,0 +1,65 @@
import type {IncomingMessage, ServerResponse} from "node:http";
import {NodeRequest, sendNodeResponse} from "srvx/node";
import {staticMiddleware} from "srvx/static";
export type StaticFileServer = (request: IncomingMessage, response: ServerResponse) => void;
const escapeHtml = (value: string): string => value.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
/** Terminal `404` handler for requests no file matched, mirroring the response `finalhandler` used to produce. */
export function sendNotFound(request: IncomingMessage, response: ServerResponse): void {
const method = request.method /* v8 ignore next */ ?? "GET";
const url = request.url /* v8 ignore next */ ?? "/";
const message = escapeHtml(`Cannot ${method} ${encodeURI(url)}`);
const body = `<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<title>Error</title>\n</head>\n<body>\n<pre>${message}</pre>\n</body>\n</html>\n`;
response.setHeader("Content-Security-Policy", "default-src 'none'");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.setHeader("Content-Length", Buffer.byteLength(body));
response.writeHead(404);
response.end(body);
}
/**
* Serves `dir` on top of a plain `node:http` server, preferring the precompressed `.br`/`.gz` variant of a file when the client accepts it.
*
* Requests that match no file are answered by {@link sendNotFound}.
*/
export function createStaticFileServer(dir: string, logError: (message: string) => void): StaticFileServer {
// `compress: false` restricts serving to the precompressed variants shipped on disk, never compressing on the fly
const serveDir = staticMiddleware({dir, encodings: true, compress: false});
const handle = async (request: IncomingMessage, response: ServerResponse): Promise<void> => {
let matched = true;
const staticResponse = await serveDir(new NodeRequest({req: request, res: response}), () => {
matched = false;
return new Response(null, {status: 404});
});
if (!matched) {
sendNotFound(request, response);
return;
}
// the HTML entry document must never be cached, so a newly installed frontend version is picked up right away
if (staticResponse.headers.get("Content-Type")?.startsWith("text/html")) {
staticResponse.headers.set("Cache-Control", "no-store");
}
await sendNodeResponse(response, staticResponse);
};
return (request, response) => {
handle(request, response).catch((error) => {
logError(`Failed to serve '${request.url}': ${(error as Error).message}`);
if (!response.headersSent) {
response.writeHead(500);
}
response.end();
});
};
}
+7 -2
View File
@@ -40,11 +40,15 @@ function isTypedArray(value: unknown): value is unknown[] {
}
function stringifyTypedArray(array: unknown[]): string {
if (array.length === 0) {
return "";
}
const isBigInt = typeof array[0] === "bigint";
let res = `"0":${isBigInt ? `"${array[0]}"` : array[0]}`;
for (let i = 1; i < array.length; i++) {
res += `,"${i}":${isBigInt ? `"${array[1]}"` : array[1]}`;
res += `,"${i}":${isBigInt ? `"${array[i]}"` : array[i]}`;
}
return res;
@@ -115,7 +119,8 @@ function stringifySimple(key: string, value: unknown, stack: unknown[]): string
res += stringifyTypedArray(value);
keys = keys.slice(value.length);
propsToStringify -= value.length;
separator = ",";
// Only separate from something that was actually written.
separator = value.length > 0 ? "," : "";
}
sort(keys);
+11 -16
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "2.13.0",
"version": "2.13.0-dev",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"types": "dist/types/api.d.ts",
@@ -10,7 +10,7 @@
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
},
"engines": {
"node": "^22.2.0 || ^24 || ^26"
"node": "^22.2.0 || ^24 || <=26.2"
},
"keywords": [
"xiaomi",
@@ -45,33 +45,28 @@
"ajv": "^8.20.0",
"bind-decorator": "^1.0.11",
"debounce": "^3.0.0",
"express-static-gzip": "^3.0.1",
"fast-deep-equal": "^3.1.3",
"fflate": "^0.8.3",
"finalhandler": "^2.1.1",
"humanize-duration": "^3.34.0",
"js-yaml": "^5.2.2",
"humanize-duration": "^3.34.1",
"js-yaml": "^5.3.0",
"mqtt": "^5.15.2",
"object-assign-deep": "^0.4.0",
"semver": "^7.8.5",
"srvx": "^0.12.7",
"throttleit": "^3.0.0",
"winston": "^3.19.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.21.1",
"zigbee-herdsman": "10.8.0",
"zigbee-herdsman-converters": "26.90.0",
"ws": "^8.21.3",
"zigbee-herdsman": "10.9.1",
"zigbee-herdsman-converters": "26.102.0",
"zigbee2mqtt-frontend": "0.9.21",
"zigbee2mqtt-windfront": "2.14.0"
"zigbee2mqtt-windfront": "2.14.1"
},
"devDependencies": {
"@biomejs/biome": "^2.5.3",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/node": "^26.1.2",
"@types/object-assign-deep": "^0.4.3",
"@types/node": "^26.2.0",
"@types/readable-stream": "4.0.24",
"@types/serve-static": "^2.2.0",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "^3.1.1",
"tmp": "^0.2.7",
@@ -95,4 +90,4 @@
"optionalDependencies": {
"unix-dgram": "^2.0.7"
}
}
}
+65 -260
View File
@@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false
overrides:
zigbee-herdsman: 10.8.0
zigbee-herdsman: 10.9.1
importers:
@@ -20,33 +20,27 @@ importers:
debounce:
specifier: ^3.0.0
version: 3.0.0
express-static-gzip:
specifier: ^3.0.1
version: 3.0.1
fast-deep-equal:
specifier: ^3.1.3
version: 3.1.3
fflate:
specifier: ^0.8.3
version: 0.8.3
finalhandler:
specifier: ^2.1.1
version: 2.1.1
humanize-duration:
specifier: ^3.34.0
version: 3.34.0
specifier: ^3.34.1
version: 3.34.1
js-yaml:
specifier: ^5.2.2
version: 5.2.2
specifier: ^5.3.0
version: 5.3.0
mqtt:
specifier: ^5.15.2
version: 5.15.2
object-assign-deep:
specifier: ^0.4.0
version: 0.4.0
semver:
specifier: ^7.8.5
version: 7.8.5
srvx:
specifier: ^0.12.7
version: 0.12.7
throttleit:
specifier: ^3.0.0
version: 3.0.0
@@ -60,48 +54,39 @@ importers:
specifier: ^4.9.0
version: 4.9.0
ws:
specifier: ^8.21.1
version: 8.21.1
specifier: ^8.21.3
version: 8.21.3
zigbee-herdsman:
specifier: 10.8.0
version: 10.8.0
specifier: 10.9.1
version: 10.9.1
zigbee-herdsman-converters:
specifier: 26.90.0
version: 26.90.0
specifier: 26.102.0
version: 26.102.0
zigbee2mqtt-frontend:
specifier: 0.9.21
version: 0.9.21
zigbee2mqtt-windfront:
specifier: 2.14.0
version: 2.14.0
specifier: 2.14.1
version: 2.14.1
devDependencies:
'@biomejs/biome':
specifier: ^2.5.3
version: 2.5.3
'@types/finalhandler':
specifier: ^1.2.3
version: 1.2.4
'@types/humanize-duration':
specifier: ^3.27.4
version: 3.27.4
'@types/node':
specifier: ^26.1.2
version: 26.1.2
'@types/object-assign-deep':
specifier: ^0.4.3
version: 0.4.3
specifier: ^26.2.0
version: 26.2.0
'@types/readable-stream':
specifier: 4.0.24
version: 4.0.24
'@types/serve-static':
specifier: ^2.2.0
version: 2.2.0
'@types/ws':
specifier: 8.18.1
version: 8.18.1
'@vitest/coverage-v8':
specifier: ^3.1.1
version: 3.2.4(vitest@3.2.4(@types/node@26.1.2))
version: 3.2.4(vitest@3.2.4(@types/node@26.2.0))
tmp:
specifier: ^0.2.7
version: 0.2.7
@@ -110,7 +95,7 @@ importers:
version: 7.0.2
vitest:
specifier: ^3.1.1
version: 3.2.4(@types/node@26.1.2)
version: 3.2.4(@types/node@26.2.0)
optionalDependencies:
unix-dgram:
specifier: ^2.0.7
@@ -525,27 +510,15 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/finalhandler@1.2.4':
resolution: {integrity: sha512-ojpQ5ywnKZko/+tw8lR4xvUN5Uvfnar4ZtfpoLG1TdxlmiIqcQGUbXmc9iV5ud7J6rRtacNbwTkCE98NmsBPYw==}
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/humanize-duration@3.27.4':
resolution: {integrity: sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==}
'@types/node@26.1.2':
resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==}
'@types/object-assign-deep@0.4.3':
resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
'@types/node@26.2.0':
resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==}
'@types/readable-stream@4.0.24':
resolution: {integrity: sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==}
'@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
'@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
@@ -848,10 +821,6 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dns-packet@5.6.1:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
engines: {node: '>=6'}
@@ -859,9 +828,6 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -871,10 +837,6 @@ packages:
enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -883,16 +845,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
@@ -905,9 +860,6 @@ packages:
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
engines: {node: '>=12.0.0'}
express-static-gzip@3.0.1:
resolution: {integrity: sha512-LMeU/3YjFlFUa4vrPX+RoMMRW5mIpF4Iysgs6gX7A59WCY4BzyF3O28mBr4eMlWuW4DU9wVAVuVcfx29ln1N6g==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -935,10 +887,6 @@ packages:
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
finalhandler@2.1.1:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'}
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
@@ -946,10 +894,6 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -974,12 +918,8 @@ packages:
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
humanize-duration@3.34.0:
resolution: {integrity: sha512-NDxhYxiiRzsST6xULIHuYWRvsCtviJXRdjarhyWyh78S4Ou+MWhM2k4HQ+y7iegdrzreXe11isd0au1N2PB/pQ==}
humanize-duration@3.34.1:
resolution: {integrity: sha512-YIiigjQ+O31rvcyDJPK1ptTZtvSErjBtmHy93VhbwxB/VwMG0FszKs2IB667tFxmOhNapePQQxa24luQRDkKFQ==}
iconv-lite@0.7.3:
resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==}
@@ -1031,8 +971,8 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@5.2.2:
resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==}
js-yaml@5.3.0:
resolution: {integrity: sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==}
hasBin: true
json-schema-traverse@1.0.0:
@@ -1061,14 +1001,6 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -1114,24 +1046,12 @@ packages:
number-allocator@1.0.14:
resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==}
object-assign-deep@0.4.0:
resolution: {integrity: sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==}
engines: {node: '>=6'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -1165,10 +1085,6 @@ packages:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -1204,17 +1120,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
send@1.2.1:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
serve-static@2.2.1:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -1249,16 +1154,17 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
srvx@0.12.7:
resolution: {integrity: sha512-PIaq1pDGg3EU2OGSN3pzRfYVu9MJDzLdojlGN6E3lvhAdxqn/lNMJFMA1xGA38jYkBb0V9xw02QEVVMDb0PExA==}
engines: {node: '>=20.16.0'}
hasBin: true
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
std-env@3.9.0:
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
@@ -1328,10 +1234,6 @@ packages:
resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==}
engines: {node: '>=14.14'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
@@ -1478,8 +1380,8 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
ws@8.21.1:
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
ws@8.21.3:
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -1490,12 +1392,12 @@ packages:
utf-8-validate:
optional: true
zigbee-herdsman-converters@26.90.0:
resolution: {integrity: sha512-a5yn+ZjWdRenp9ax3hfh+blz5QO4i7WbW7XXygo+Hnz4QG9wp8TkS0zzZbvuQHjhddLzgcBzXMiJl8++Rrpd2Q==}
zigbee-herdsman-converters@26.102.0:
resolution: {integrity: sha512-OJ4j6/0PlirjQu48RdTocEisuNTQjJyJHNt5j6HPCcoN/dEqBrX8UJc3Vo6EWqIApG3xOb8qek7KAgpFXAj6ew==}
engines: {node: '>=20.15.0'}
zigbee-herdsman@10.8.0:
resolution: {integrity: sha512-m9bVPYjGVT8kqGWjHCbMfpA4jUuSNAq3kH9Xx6i9Y56OXXg9zpBD+Z6xXDCwZL6VS+iranZAX2d7EwqFSm4JZw==}
zigbee-herdsman@10.9.1:
resolution: {integrity: sha512-Tq4wSk4JhQgBOhPwlpq/SU1V3tAtd66u/VVhQfnTfcoomvA331JbspmasAsT6Ba5UnASJvXz7jViNHHCicjhRA==}
zigbee-on-host@0.2.4:
resolution: {integrity: sha512-NIG6CWp+Yfn7PjqEIRvenHqpwT1U7rSkyimnFOUIFpnmxQOJKrmcwWDfn6WjbU/YIYYWSQPlj+g13icu1xwlyg==}
@@ -1505,8 +1407,8 @@ packages:
resolution: {integrity: sha512-ClHYlG7g0v/tnIsD6kycNCi1ZEKIbQuU7lGJR3WqYcDmuqEMVcHDGbRiRwYRQcZGMxkqCf7guxNsXiKnC10+kg==}
engines: {node: '>=20.11'}
zigbee2mqtt-windfront@2.14.0:
resolution: {integrity: sha512-Xoqce+q3R1OWl8Gaaqtw0VH0J2hxFZEyYGX7+4y2G8H2ZNPDAdS8goF15PD5cm5x+gRP6jq2YOhePv6vZoquFA==}
zigbee2mqtt-windfront@2.14.1:
resolution: {integrity: sha512-p7AyI5oqnGzQ+D3gC2Xk+GTBnbDoNEyORG9IXYy4TwvhXmO8dPhzRtb/XB8a+Bt41bjVni/PnFKDiRH7/G6KUQ==}
engines: {node: '>=22.12.0'}
snapshots:
@@ -1784,34 +1686,21 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/finalhandler@1.2.4':
dependencies:
'@types/node': 26.1.2
'@types/http-errors@2.0.5': {}
'@types/humanize-duration@3.27.4': {}
'@types/node@26.1.2':
'@types/node@26.2.0':
dependencies:
undici-types: 8.3.0
'@types/object-assign-deep@0.4.3': {}
'@types/readable-stream@4.0.24':
dependencies:
'@types/node': 26.1.2
'@types/serve-static@2.2.0':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 26.1.2
'@types/node': 26.2.0
'@types/triple-beam@1.3.5': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 26.1.2
'@types/node': 26.2.0
'@typescript/typescript-aix-ppc64@7.0.2':
optional: true
@@ -1873,7 +1762,7 @@ snapshots:
'@typescript/typescript-win32-x64@7.0.2':
optional: true
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@26.1.2))':
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@26.2.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -1888,7 +1777,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/node@26.1.2)
vitest: 3.2.4(@types/node@26.2.0)
transitivePeerDependencies:
- supports-color
@@ -1900,13 +1789,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@26.1.2))':
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@26.2.0))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.3.5(@types/node@26.1.2)
vite: 6.3.5(@types/node@26.2.0)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -2068,24 +1957,18 @@ snapshots:
deep-eql@5.0.2: {}
depd@2.0.0: {}
dns-packet@5.6.1:
dependencies:
'@leichtgewicht/ip-codec': 2.0.5
eastasianwidth@0.2.0: {}
ee-first@1.1.1: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
enabled@2.0.0: {}
encodeurl@2.0.0: {}
es-module-lexer@1.7.0: {}
esbuild@0.25.5:
@@ -2116,28 +1999,16 @@ snapshots:
'@esbuild/win32-ia32': 0.25.5
'@esbuild/win32-x64': 0.25.5
escape-html@1.0.3: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
etag@1.8.1: {}
event-target-shim@5.0.1: {}
events@3.3.0: {}
expect-type@1.2.1: {}
express-static-gzip@3.0.1:
dependencies:
mime-types: 3.0.2
parseurl: 1.3.3
serve-static: 2.2.1
transitivePeerDependencies:
- supports-color
fast-deep-equal@3.1.3: {}
fast-unique-numbers@9.0.27:
@@ -2158,17 +2029,6 @@ snapshots:
file-uri-to-path@1.0.0:
optional: true
finalhandler@2.1.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
fn.name@1.1.0: {}
foreground-child@3.3.1:
@@ -2176,8 +2036,6 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fresh@2.0.0: {}
fsevents@2.3.3:
optional: true
@@ -2198,15 +2056,7 @@ snapshots:
html-escaper@2.0.2: {}
http-errors@2.0.1:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.2
toidentifier: 1.0.1
humanize-duration@3.34.0: {}
humanize-duration@3.34.1: {}
iconv-lite@0.7.3:
dependencies:
@@ -2255,7 +2105,7 @@ snapshots:
js-tokens@9.0.1: {}
js-yaml@5.2.2:
js-yaml@5.3.0:
dependencies:
argparse: 2.0.1
@@ -2290,12 +2140,6 @@ snapshots:
dependencies:
semver: 7.8.5
mime-db@1.54.0: {}
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
minimatch@9.0.5:
dependencies:
brace-expansion: 2.1.0
@@ -2329,7 +2173,7 @@ snapshots:
socks: 2.8.9
split2: 4.2.0
worker-timers: 8.0.33
ws: 8.21.1
ws: 8.21.3
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -2358,20 +2202,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
object-assign-deep@0.4.0: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
one-time@1.0.0:
dependencies:
fn.name: 1.1.0
package-json-from-dist@1.0.1: {}
parseurl@1.3.3: {}
path-key@3.1.1: {}
path-scurry@1.11.1:
@@ -2397,8 +2233,6 @@ snapshots:
process@0.11.10: {}
range-parser@1.2.1: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@@ -2451,33 +2285,6 @@ snapshots:
semver@7.8.5: {}
send@1.2.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
serve-static@2.2.1:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.1
transitivePeerDependencies:
- supports-color
setprototypeof@1.2.0: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -2501,12 +2308,12 @@ snapshots:
split2@4.2.0: {}
srvx@0.12.7: {}
stack-trace@0.0.10: {}
stackback@0.0.2: {}
statuses@2.0.2: {}
std-env@3.9.0: {}
string-width@4.2.3:
@@ -2570,8 +2377,6 @@ snapshots:
tmp@0.2.7: {}
toidentifier@1.0.1: {}
triple-beam@1.4.1: {}
tslib@2.8.1: {}
@@ -2617,13 +2422,13 @@ snapshots:
util-deprecate@1.0.2: {}
vite-node@3.2.4(@types/node@26.1.2):
vite-node@3.2.4(@types/node@26.2.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@26.1.2)
vite: 6.3.5(@types/node@26.2.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -2638,7 +2443,7 @@ snapshots:
- tsx
- yaml
vite@6.3.5(@types/node@26.1.2):
vite@6.3.5(@types/node@26.2.0):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@@ -2647,14 +2452,14 @@ snapshots:
rollup: 4.44.0
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 26.1.2
'@types/node': 26.2.0
fsevents: 2.3.3
vitest@3.2.4(@types/node@26.1.2):
vitest@3.2.4(@types/node@26.2.0):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@26.1.2))
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@26.2.0))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -2672,11 +2477,11 @@ snapshots:
tinyglobby: 0.2.14
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.3.5(@types/node@26.1.2)
vite-node: 3.2.4(@types/node@26.1.2)
vite: 6.3.5(@types/node@26.2.0)
vite-node: 3.2.4(@types/node@26.2.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 26.1.2
'@types/node': 26.2.0
transitivePeerDependencies:
- jiti
- less
@@ -2768,17 +2573,17 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.2
ws@8.21.1: {}
ws@8.21.3: {}
zigbee-herdsman-converters@26.90.0:
zigbee-herdsman-converters@26.102.0:
dependencies:
iconv-lite: 0.7.3
semver: 7.8.5
zigbee-herdsman: 10.8.0
zigbee-herdsman: 10.9.1
transitivePeerDependencies:
- supports-color
zigbee-herdsman@10.8.0:
zigbee-herdsman@10.9.1:
dependencies:
'@date-fns/tz': 1.5.0
'@serialport/bindings-cpp': 13.0.1
@@ -2796,4 +2601,4 @@ snapshots:
zigbee2mqtt-frontend@0.9.21: {}
zigbee2mqtt-windfront@2.14.0: {}
zigbee2mqtt-windfront@2.14.1: {}
-418
View File
@@ -1,418 +0,0 @@
:robot: I have created a release *beep* *boop*
---
## [2.14.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.13.0...2.14.0) (2026-08-01)
### Features
* Add `action` bridge/request API ([#29730](https://github.com/Koenkk/zigbee2mqtt/issues/29730)) ([a61646d](https://github.com/Koenkk/zigbee2mqtt/commit/a61646d584c0a0de46abb3545cd964f65d84312d))
* Add `clear_cache` option to device remove request ([#32631](https://github.com/Koenkk/zigbee2mqtt/issues/32631)) ([83ab522](https://github.com/Koenkk/zigbee2mqtt/commit/83ab5228ff24779773e67bfc217d89f7c7520daa))
* Add ability to abort running OTA ([#32022](https://github.com/Koenkk/zigbee2mqtt/issues/32022)) ([de58711](https://github.com/Koenkk/zigbee2mqtt/commit/de58711e11a5ca07c5b55a0c17a1d9f3f452c110))
* Add new bind/reporting/map features ([#29750](https://github.com/Koenkk/zigbee2mqtt/issues/29750)) ([f26ade4](https://github.com/Koenkk/zigbee2mqtt/commit/f26ade4f938350d10ed22a32a2e30365f7514a72))
* allow to disable external JS extensions ([#31826](https://github.com/Koenkk/zigbee2mqtt/issues/31826)) ([15fd9b3](https://github.com/Koenkk/zigbee2mqtt/commit/15fd9b371e30ed352cf2de2d241a0d0f7fafa9d1))
* Home Assistant: add discovery support for Tuya infrared receiver (learn mode) and emitter features ([#32625](https://github.com/Koenkk/zigbee2mqtt/issues/32625)) ([32506b4](https://github.com/Koenkk/zigbee2mqtt/commit/32506b4e8abf81996966632719384f85bad7a66c))
* Home Assistant: add group entities in discovery config ([#31663](https://github.com/Koenkk/zigbee2mqtt/issues/31663)) ([0419726](https://github.com/Koenkk/zigbee2mqtt/commit/041972669a6a8108e8fee1a0fffb555292371285))
* Improve onboarding ([#31152](https://github.com/Koenkk/zigbee2mqtt/issues/31152)) ([8ecf353](https://github.com/Koenkk/zigbee2mqtt/commit/8ecf353dd634cc56f8cbbfbe21646945c262a11e))
* Improve OTA ([#30566](https://github.com/Koenkk/zigbee2mqtt/issues/30566)) ([dd1c449](https://github.com/Koenkk/zigbee2mqtt/commit/dd1c44979667360d77f05ac30729011f9f2d6cd6))
### Bug Fixes
* `experimental_event_entities` and `legacy_action_sensor` require restart ([#32535](https://github.com/Koenkk/zigbee2mqtt/issues/32535)) ([2ca80b8](https://github.com/Koenkk/zigbee2mqtt/commit/2ca80b8572293a7c63c019c1b433e942772220c4))
* Add `mqtt.server_name` option to override TLS SNI ([#32417](https://github.com/Koenkk/zigbee2mqtt/issues/32417)) ([2f26083](https://github.com/Koenkk/zigbee2mqtt/commit/2f26083ce13c30227ac8d1d891751d4c253b3572))
* Add pi cooling demand to Home Assistant auto discovery ([#28843](https://github.com/Koenkk/zigbee2mqtt/issues/28843)) ([09fd675](https://github.com/Koenkk/zigbee2mqtt/commit/09fd675d0f215af11e64515ea60b91ed28b5e683))
* Add port 8080 `EXPOSE` to container ([#29754](https://github.com/Koenkk/zigbee2mqtt/issues/29754)) ([4c21b66](https://github.com/Koenkk/zigbee2mqtt/commit/4c21b660794ee3b85c9a9a6d01b0df5ec8bce639))
* Avoid running resolveDefinition in parallel ([#32662](https://github.com/Koenkk/zigbee2mqtt/issues/32662)) ([b46c77b](https://github.com/Koenkk/zigbee2mqtt/commit/b46c77bff0612527bdfc7540ea31ec258bd002b1))
* Biome floating promises detection ([#30137](https://github.com/Koenkk/zigbee2mqtt/issues/30137)) ([0025ef8](https://github.com/Koenkk/zigbee2mqtt/commit/0025ef87c502970b94e1b720516d42631e21323c))
* Clarify units of pause_on_backoff_gt ([#31668](https://github.com/Koenkk/zigbee2mqtt/issues/31668)) ([4a63e65](https://github.com/Koenkk/zigbee2mqtt/commit/4a63e65981b6bbd746a730e7ca2291aba7a6c975))
* **deps-dev:** bump tmp from 0.2.5 to 0.2.6 ([#32122](https://github.com/Koenkk/zigbee2mqtt/issues/32122)) ([7b9407d](https://github.com/Koenkk/zigbee2mqtt/commit/7b9407d020b9db1093b360f5f6e217bbab7afb63))
* **deps:** bump actions/dependency-review-action from 4 to 5 ([#31980](https://github.com/Koenkk/zigbee2mqtt/issues/31980)) ([4d9e950](https://github.com/Koenkk/zigbee2mqtt/commit/4d9e9504ef9c857a1985c79d3a6799552abc3aea))
* **deps:** bump brace-expansion from 2.0.2 to 5.0.6 ([#32038](https://github.com/Koenkk/zigbee2mqtt/issues/32038)) ([897aa8f](https://github.com/Koenkk/zigbee2mqtt/commit/897aa8fb7c82dbc9c906f986477c4c1673bb3199))
* **deps:** bump googleapis/release-please-action from 4 to 5 ([#31805](https://github.com/Koenkk/zigbee2mqtt/issues/31805)) ([4e527bb](https://github.com/Koenkk/zigbee2mqtt/commit/4e527bbce8612c85fd636a1c493059af9793991f))
* **deps:** bump pnpm/action-setup from 5 to 6 ([#31698](https://github.com/Koenkk/zigbee2mqtt/issues/31698)) ([1a1c094](https://github.com/Koenkk/zigbee2mqtt/commit/1a1c09449c9d077ea06048d7e2ecea90ff850c9b))
* **deps:** bump ws from 8.20.0 to 8.20.1 ([#32041](https://github.com/Koenkk/zigbee2mqtt/issues/32041)) ([bf964cc](https://github.com/Koenkk/zigbee2mqtt/commit/bf964cc6d976d70e687844e5a09c4577763dc0d3))
* Docker: bump alpine from 3.23 to 3.24 ([#32311](https://github.com/Koenkk/zigbee2mqtt/issues/32311)) ([758a80b](https://github.com/Koenkk/zigbee2mqtt/commit/758a80b30c5162b9adb0c2be1785670276905e09))
* Don't fail to start when removing old log directory fails ([#30216](https://github.com/Koenkk/zigbee2mqtt/issues/30216)) ([b39b8d4](https://github.com/Koenkk/zigbee2mqtt/commit/b39b8d478e99c5a5ee5a3a610547168b3724ca91))
* Drop redundant types js yaml ([#32619](https://github.com/Koenkk/zigbee2mqtt/issues/32619)) ([c9ddef7](https://github.com/Koenkk/zigbee2mqtt/commit/c9ddef703a1f83a2dc5c84a1a1eaa087bee3f671))
* Export definition version ([#31270](https://github.com/Koenkk/zigbee2mqtt/issues/31270)) ([9f1eb76](https://github.com/Koenkk/zigbee2mqtt/commit/9f1eb764a62b041a7d9782066a8ffcaa3562eaad))
* Fix `onEvent` called twice first time when device options are changed ([#29322](https://github.com/Koenkk/zigbee2mqtt/issues/29322)) ([e037a2c](https://github.com/Koenkk/zigbee2mqtt/commit/e037a2c21cb985b6e71d9c1fa169f0886e8af832))
* Fix default value of "optimistic" group setting ([#32054](https://github.com/Koenkk/zigbee2mqtt/issues/32054)) ([b31e8c2](https://github.com/Koenkk/zigbee2mqtt/commit/b31e8c286e7a77517a88e3e7eb2247e9a0fd2cc1))
* Fix Home Assistant options cannot be updated while running ([#31151](https://github.com/Koenkk/zigbee2mqtt/issues/31151)) ([a77f48f](https://github.com/Koenkk/zigbee2mqtt/commit/a77f48f5c464903712f7143bb6a85ea701a39c5e))
* Fix input onboarding environment variable disabled check ([#29354](https://github.com/Koenkk/zigbee2mqtt/issues/29354)) ([c306300](https://github.com/Koenkk/zigbee2mqtt/commit/c30630091d1ac14fed6df9c5ee43a275db877821))
* Fix onboarding ([#31228](https://github.com/Koenkk/zigbee2mqtt/issues/31228)) ([7f0a9aa](https://github.com/Koenkk/zigbee2mqtt/commit/7f0a9aa2a30ab79c182d8ea9e875339e9031fdbd))
* Fix restartRequired flag ([#31947](https://github.com/Koenkk/zigbee2mqtt/issues/31947)) ([4174e9c](https://github.com/Koenkk/zigbee2mqtt/commit/4174e9cb783b73527349182b35e06eda4512b101))
* Home Assisant: set state class to `measurement` for `illuminane_raw` https://github.com/Koenkk/zigbee2mqtt/issues/30439 ([3e60f91](https://github.com/Koenkk/zigbee2mqtt/commit/3e60f915ffe1de640690695ccf6536ad235c336e))
* Home Assistant: Accept multiple access levels for `current_humidity` source property ([#29936](https://github.com/Koenkk/zigbee2mqtt/issues/29936)) ([fa99c6d](https://github.com/Koenkk/zigbee2mqtt/commit/fa99c6dcca97d2c8c90136249293dea3d42e0998))
* Home Assistant: add `gas` device class ([#30653](https://github.com/Koenkk/zigbee2mqtt/issues/30653)) ([4815d51](https://github.com/Koenkk/zigbee2mqtt/commit/4815d5149d3fbc70fe374fc4aa99213ac5f8a8c7))
* Home Assistant: add `measurement` `state_class` to `_count` entities ([#30978](https://github.com/Koenkk/zigbee2mqtt/issues/30978)) ([d475aa5](https://github.com/Koenkk/zigbee2mqtt/commit/d475aa5be5caa85a5dc3f0883dc40b2bcddc09b2))
* Home Assistant: add `state_class: measurement` for `external_humidity` and `occupancy_level` https://github.com/Koenkk/zigbee2mqtt/issues/31022 ([02a2fd8](https://github.com/Koenkk/zigbee2mqtt/commit/02a2fd8fef786be9488c1adc97093b1ec6bff9e1))
* Home Assistant: add conductivity discovery for soil_fertility ([#32356](https://github.com/Koenkk/zigbee2mqtt/issues/32356)) ([29ee3a7](https://github.com/Koenkk/zigbee2mqtt/commit/29ee3a7552017e648c534708a93329e8b3fc52c5))
* Home Assistant: add weather station sensor discovery lookup entries ([#31136](https://github.com/Koenkk/zigbee2mqtt/issues/31136)) ([8d1240e](https://github.com/Koenkk/zigbee2mqtt/commit/8d1240eba161e8eb92ce6fdda757710a07c4456e))
* Home Assistant: allow discovery user overrides to win over converter ([#32361](https://github.com/Koenkk/zigbee2mqtt/issues/32361)) ([20ab0a2](https://github.com/Koenkk/zigbee2mqtt/commit/20ab0a2a4f44b760b6db73d105dc8578426b27a0))
* Home Assistant: apply expose-level Home Assistant discovery metadata ([#32380](https://github.com/Koenkk/zigbee2mqtt/issues/32380)) ([0140708](https://github.com/Koenkk/zigbee2mqtt/commit/0140708076fbd99d33d896e8f214b0e2a8aad8f3))
* Home Assistant: Change `device_class` of `eco2` to `volatile_organic_compounds_parts` ([#30840](https://github.com/Koenkk/zigbee2mqtt/issues/30840)) ([4f412cb](https://github.com/Koenkk/zigbee2mqtt/commit/4f412cb4da6217fcca57b5ea1afc40d34d662e19))
* Home Assistant: discover `temperature_probe` as `temperature` `device_class` https://github.com/Koenkk/zigbee2mqtt/issues/30862 ([8d8d37e](https://github.com/Koenkk/zigbee2mqtt/commit/8d8d37e3888cbfa4899831874ee9df6e306ddb52))
* Home Assistant: discover device trigger when mqtt output = attribute_and_json and add warnings about incompatible settings ([#32603](https://github.com/Koenkk/zigbee2mqtt/issues/32603)) ([d6dea17](https://github.com/Koenkk/zigbee2mqtt/commit/d6dea17961e05b8ca8ce05d7852efd483031260d))
* Home Assistant: discover temperature sensor for thermostats ([#30804](https://github.com/Koenkk/zigbee2mqtt/issues/30804)) ([26a0fd4](https://github.com/Koenkk/zigbee2mqtt/commit/26a0fd4de0f39d10a404517f42d4c37aff1eff8e))
* Home Assistant: expose `current_humidity` for `climate` devices exposing `humidity` ([#29842](https://github.com/Koenkk/zigbee2mqtt/issues/29842)) ([631c4f6](https://github.com/Koenkk/zigbee2mqtt/commit/631c4f6a5729888d65a1125e0cca8f9ba2ff0e59))
* Home Assistant: expose group settings override ([#30627](https://github.com/Koenkk/zigbee2mqtt/issues/30627)) ([6f0b02e](https://github.com/Koenkk/zigbee2mqtt/commit/6f0b02e0093cc7fd61d30c00fb1cd61d0cfdf09f))
* Home Assistant: expose siren entity for IAS warning devices ([#31000](https://github.com/Koenkk/zigbee2mqtt/issues/31000)) ([c7dae39](https://github.com/Koenkk/zigbee2mqtt/commit/c7dae397999102ec30c6681bc6a7577e97c8b707))
* Home Assistant: fix action published to wrong topic ([#32544](https://github.com/Koenkk/zigbee2mqtt/issues/32544)) ([a80c2db](https://github.com/Koenkk/zigbee2mqtt/commit/a80c2db4c6038426fa5102477f6da34355f54134))
* Home Assistant: fix device and group configuration URL ([#29211](https://github.com/Koenkk/zigbee2mqtt/issues/29211)) ([c0190c1](https://github.com/Koenkk/zigbee2mqtt/commit/c0190c18ede7accb407b805f6016c3242c11146a))
* Home Assistant: fix entity names for derived weather sensors by removing `device_class` ([#31234](https://github.com/Koenkk/zigbee2mqtt/issues/31234)) ([b7b4303](https://github.com/Koenkk/zigbee2mqtt/commit/b7b43030eb8aaa144dbb5477805638d860efdd38))
* Home Assistant: make PI heating demand from writable ([#29188](https://github.com/Koenkk/zigbee2mqtt/issues/29188)) ([174ba64](https://github.com/Koenkk/zigbee2mqtt/commit/174ba64449b04777d1d5743fbb7b498a29c40ae2))
* Home Assistant: mark `load_estimate` as `state_class` `measurement` https://github.com/Koenkk/zigbee-herdsman-converters/issues/11240 ([a32c8ee](https://github.com/Koenkk/zigbee2mqtt/commit/a32c8eec3604a3ad0d16e87edf027581db7f6920))
* Home Assistant: mark device settings as config ([#32439](https://github.com/Koenkk/zigbee2mqtt/issues/32439)) ([e8a7e6f](https://github.com/Koenkk/zigbee2mqtt/commit/e8a7e6f29da3773ffc2bda63623298a307fbb597))
* Home Assistant: mark legacy action sensors diagnostic ([#32377](https://github.com/Koenkk/zigbee2mqtt/issues/32377)) ([72be1d7](https://github.com/Koenkk/zigbee2mqtt/commit/72be1d746bb79fda65b056e4441dcd73bf6c69c6))
* Home Assistant: mark thermostat configuration switches as config ([#32378](https://github.com/Koenkk/zigbee2mqtt/issues/32378)) ([98a0f94](https://github.com/Koenkk/zigbee2mqtt/commit/98a0f9487072cf5a64a901f305c20a73e25c594b))
* Home Assistant: pass device options to HA discovery overrides ([#32379](https://github.com/Koenkk/zigbee2mqtt/issues/32379)) ([814552a](https://github.com/Koenkk/zigbee2mqtt/commit/814552a615d33ac24c38160f7a24514c7b358623))
* Home Assistant: set `state_class` `measurement` for `external_temperature` https://github.com/Koenkk/zigbee2mqtt/issues/31022 ([28a9f39](https://github.com/Koenkk/zigbee2mqtt/commit/28a9f39dbcd5a04e5eb5630855de2b82ec676dac))
* Home Assistant: support cooling setpoint in climate discovery ([#32411](https://github.com/Koenkk/zigbee2mqtt/issues/32411)) ([0018ca8](https://github.com/Koenkk/zigbee2mqtt/commit/0018ca8f809708d22221c9d557b3208ba2e5c057))
* Home Assistant: Translate boolean to state topic for `current_humidity_topic` ([#30014](https://github.com/Koenkk/zigbee2mqtt/issues/30014)) ([9ca85df](https://github.com/Koenkk/zigbee2mqtt/commit/9ca85dfac8b0f0421ad0622dfdbe36c8766d22b4))
* Home Assistant: treat `Area1-4Occupancy` as `occupancy` ([#30712](https://github.com/Koenkk/zigbee2mqtt/issues/30712)) ([2e36084](https://github.com/Koenkk/zigbee2mqtt/commit/2e36084852e84f26dd375820a87c0a8520d4c3e7))
* Home Assistant: unit conversion for derived weather sensors by restoring device_class with name preservation ([#32392](https://github.com/Koenkk/zigbee2mqtt/issues/32392)) ([7775ac5](https://github.com/Koenkk/zigbee2mqtt/commit/7775ac5cd2ae3929580e401640d97b20facf1491))
* Home Assistant: Use `temperature_delta` for calibration ([#30784](https://github.com/Koenkk/zigbee2mqtt/issues/30784)) ([8e68295](https://github.com/Koenkk/zigbee2mqtt/commit/8e682956e5977101ed4ca80e795f413a5bfb4f09))
* **ignore:** bump @biomejs/biome from 2.3.10 to 2.3.11 in the minor-patch group ([#30511](https://github.com/Koenkk/zigbee2mqtt/issues/30511)) ([ed52c49](https://github.com/Koenkk/zigbee2mqtt/commit/ed52c49e34a200f9adf5deb2065c78fbe8df3189))
* **ignore:** bump @biomejs/biome from 2.3.12 to 2.3.13 in the minor-patch group ([#30810](https://github.com/Koenkk/zigbee2mqtt/issues/30810)) ([14bffd5](https://github.com/Koenkk/zigbee2mqtt/commit/14bffd58b1ae90cf5c32b654ddbb7a43d12a36cc))
* **ignore:** bump @biomejs/biome from 2.3.3 to 2.3.4 in the minor-patch group ([#29571](https://github.com/Koenkk/zigbee2mqtt/issues/29571)) ([517b286](https://github.com/Koenkk/zigbee2mqtt/commit/517b2865835a7c21b4c5ce68018fd30ae45d1a3f))
* **ignore:** bump @biomejs/biome from 2.4.1 to 2.4.4 in the minor-patch group ([#31159](https://github.com/Koenkk/zigbee2mqtt/issues/31159)) ([1d5184f](https://github.com/Koenkk/zigbee2mqtt/commit/1d5184f2bd46afa50faa470a97e807888ed81a78))
* **ignore:** bump @biomejs/biome from 2.4.10 to 2.4.11 in the minor-patch group ([#31699](https://github.com/Koenkk/zigbee2mqtt/issues/31699)) ([cdd7357](https://github.com/Koenkk/zigbee2mqtt/commit/cdd73575227527ae7eca5732d96687a30cea8538))
* **ignore:** bump @biomejs/biome from 2.4.13 to 2.4.14 in the minor-patch group across 1 directory ([#31881](https://github.com/Koenkk/zigbee2mqtt/issues/31881)) ([55f12ff](https://github.com/Koenkk/zigbee2mqtt/commit/55f12ff52063d576be0fe82891678c52181f3b8e))
* **ignore:** bump @biomejs/biome from 2.4.4 to 2.4.5 in the minor-patch group ([#31237](https://github.com/Koenkk/zigbee2mqtt/issues/31237)) ([aefbb8b](https://github.com/Koenkk/zigbee2mqtt/commit/aefbb8be14ae5d84495278cbe9a6ca9d750553cb))
* **ignore:** bump @types/node from 24.10.2 to 24.10.4 in the minor-patch group ([#30390](https://github.com/Koenkk/zigbee2mqtt/issues/30390)) ([93c9704](https://github.com/Koenkk/zigbee2mqtt/commit/93c9704fcd5552e234dd03a7ae1cddc11403622b))
* **ignore:** bump @types/node from 24.10.7 to 24.10.9 in the minor-patch group ([#30713](https://github.com/Koenkk/zigbee2mqtt/issues/30713)) ([043dd0f](https://github.com/Koenkk/zigbee2mqtt/commit/043dd0ff1cc94ffa8ea59f6de674e0363f9f6920))
* **ignore:** bump @types/node from 24.12.0 to 24.12.2 in the minor-patch group across 1 directory ([#31628](https://github.com/Koenkk/zigbee2mqtt/issues/31628)) ([e6dd5c7](https://github.com/Koenkk/zigbee2mqtt/commit/e6dd5c73bc2d76b32c45ef19633626dbcdde8379))
* **ignore:** bump @types/node from 24.13.2 to 26.1.0 ([#32453](https://github.com/Koenkk/zigbee2mqtt/issues/32453)) ([83ca274](https://github.com/Koenkk/zigbee2mqtt/commit/83ca2746a789bb92bfdb51506e875edfe5cb06b3))
* **ignore:** bump @types/node from 24.7.2 to 24.8.1 in the minor-patch group ([#29191](https://github.com/Koenkk/zigbee2mqtt/issues/29191)) ([d8fc69d](https://github.com/Koenkk/zigbee2mqtt/commit/d8fc69dac3cba33f3d69e377e338e03facbc19e7))
* **ignore:** bump @types/serve-static from 1.15.10 to 2.2.0 ([#29394](https://github.com/Koenkk/zigbee2mqtt/issues/29394)) ([b2c31f8](https://github.com/Koenkk/zigbee2mqtt/commit/b2c31f89c69cfe04d0d671235bd18eb3cfa7ed72))
* **ignore:** bump actions/cache from 5 to 6 ([#32394](https://github.com/Koenkk/zigbee2mqtt/issues/32394)) ([280442b](https://github.com/Koenkk/zigbee2mqtt/commit/280442b6710c03cfa4776d69a14396b29d32c4ba))
* **ignore:** bump actions/checkout from 6 to 7 ([#32368](https://github.com/Koenkk/zigbee2mqtt/issues/32368)) ([679436e](https://github.com/Koenkk/zigbee2mqtt/commit/679436e09b40edea5bb17094e9aa9115733b916f))
* **ignore:** bump actions/setup-node from 6 to 7 ([#32574](https://github.com/Koenkk/zigbee2mqtt/issues/32574)) ([e889592](https://github.com/Koenkk/zigbee2mqtt/commit/e889592baf9b2738c7d2088247ea3f888cd08629))
* **ignore:** bump debounce from 2.2.0 to 3.0.0 ([#29444](https://github.com/Koenkk/zigbee2mqtt/issues/29444)) ([8099322](https://github.com/Koenkk/zigbee2mqtt/commit/8099322cc2e93e5c1faf355d41485f2c673e0cea))
* **ignore:** bump express-static-gzip from 3.0.0 to 3.0.1 in the minor-patch group ([#31924](https://github.com/Koenkk/zigbee2mqtt/issues/31924)) ([24e9027](https://github.com/Koenkk/zigbee2mqtt/commit/24e90273c9e712e122887e74ccf8208a8b42e762))
* **ignore:** bump finalhandler from 2.1.0 to 2.1.1 in the minor-patch group ([#29920](https://github.com/Koenkk/zigbee2mqtt/issues/29920)) ([ce27ff3](https://github.com/Koenkk/zigbee2mqtt/commit/ce27ff3cbfac204579b38d034e71733e4e4bb18f))
* **ignore:** bump js-yaml from 4.2.0 to 5.0.0 ([#32371](https://github.com/Koenkk/zigbee2mqtt/issues/32371)) ([262139a](https://github.com/Koenkk/zigbee2mqtt/commit/262139a43f878ceef2f327369432e33b74fbd972))
* **ignore:** bump mqtt from 5.14.1 to 5.15.0 in the minor-patch group ([#30918](https://github.com/Koenkk/zigbee2mqtt/issues/30918)) ([4b03910](https://github.com/Koenkk/zigbee2mqtt/commit/4b039103c0a9bd32458011dbc2e7bea0e609d0c7))
* **ignore:** bump semver from 7.8.4 to 7.8.5 in the minor-patch group ([#32369](https://github.com/Koenkk/zigbee2mqtt/issues/32369)) ([9a7cac1](https://github.com/Koenkk/zigbee2mqtt/commit/9a7cac1e4c82c1b0ef566b13620e2d4d438d725e))
* **ignore:** bump the minor-patch group across 1 directory with 3 updates ([#32236](https://github.com/Koenkk/zigbee2mqtt/issues/32236)) ([41f6886](https://github.com/Koenkk/zigbee2mqtt/commit/41f688659119012a9a84063bd67c2769a1b1892c))
* **ignore:** bump the minor-patch group across 1 directory with 4 updates ([#29395](https://github.com/Koenkk/zigbee2mqtt/issues/29395)) ([dc7749b](https://github.com/Koenkk/zigbee2mqtt/commit/dc7749b57e188835f1243fad4b1d9dcccd7dca7d))
* **ignore:** bump the minor-patch group across 1 directory with 4 updates ([#30117](https://github.com/Koenkk/zigbee2mqtt/issues/30117)) ([ad3c090](https://github.com/Koenkk/zigbee2mqtt/commit/ad3c09036b804eb108238036f22776d83ea371b2))
* **ignore:** bump the minor-patch group with 2 updates ([#29443](https://github.com/Koenkk/zigbee2mqtt/issues/29443)) ([619b939](https://github.com/Koenkk/zigbee2mqtt/commit/619b939b148394bd6a083c17cc5cf254c8eb3aea))
* **ignore:** bump the minor-patch group with 2 updates ([#29859](https://github.com/Koenkk/zigbee2mqtt/issues/29859)) ([c02ed09](https://github.com/Koenkk/zigbee2mqtt/commit/c02ed092116c7454220c0611bff5d078e12a8ecc))
* **ignore:** bump the minor-patch group with 2 updates ([#30207](https://github.com/Koenkk/zigbee2mqtt/issues/30207)) ([1d15c21](https://github.com/Koenkk/zigbee2mqtt/commit/1d15c2142fefb9c7ce96c947e5ac7ddc6101439f))
* **ignore:** bump the minor-patch group with 2 updates ([#30309](https://github.com/Koenkk/zigbee2mqtt/issues/30309)) ([b10fa93](https://github.com/Koenkk/zigbee2mqtt/commit/b10fa93700febe84634040dbdeb4857258cd2882))
* **ignore:** bump the minor-patch group with 2 updates ([#30617](https://github.com/Koenkk/zigbee2mqtt/issues/30617)) ([1d87d2f](https://github.com/Koenkk/zigbee2mqtt/commit/1d87d2f2349f9eed6351a032645eec075476f148))
* **ignore:** bump the minor-patch group with 2 updates ([#30790](https://github.com/Koenkk/zigbee2mqtt/issues/30790)) ([da1f77e](https://github.com/Koenkk/zigbee2mqtt/commit/da1f77ec6eb599a480f66cc82edb12acc6c22ca8))
* **ignore:** bump the minor-patch group with 2 updates ([#31211](https://github.com/Koenkk/zigbee2mqtt/issues/31211)) ([89192f9](https://github.com/Koenkk/zigbee2mqtt/commit/89192f9decdf41338ec925c88860b560906b3ad4))
* **ignore:** bump the minor-patch group with 2 updates ([#31411](https://github.com/Koenkk/zigbee2mqtt/issues/31411)) ([67aa509](https://github.com/Koenkk/zigbee2mqtt/commit/67aa5099df2371eb9ee220be7a8821f0d4f8b38b))
* **ignore:** bump the minor-patch group with 2 updates ([#31758](https://github.com/Koenkk/zigbee2mqtt/issues/31758)) ([7ba232e](https://github.com/Koenkk/zigbee2mqtt/commit/7ba232e27d973119530119ca136aaab3e8e22ab6))
* **ignore:** bump the minor-patch group with 2 updates ([#32137](https://github.com/Koenkk/zigbee2mqtt/issues/32137)) ([ac8c45f](https://github.com/Koenkk/zigbee2mqtt/commit/ac8c45f67f0a4aae2688e55b98dabdc23f73a259))
* **ignore:** bump the minor-patch group with 2 updates ([#32183](https://github.com/Koenkk/zigbee2mqtt/issues/32183)) ([ef8dcb4](https://github.com/Koenkk/zigbee2mqtt/commit/ef8dcb4dc2800defd97fda7523297eab8d2566dc))
* **ignore:** bump the minor-patch group with 2 updates ([#32504](https://github.com/Koenkk/zigbee2mqtt/issues/32504)) ([8f0d981](https://github.com/Koenkk/zigbee2mqtt/commit/8f0d9817652f97414d927e2d3e4f2b101e3d55ca))
* **ignore:** bump the minor-patch group with 2 updates ([#32514](https://github.com/Koenkk/zigbee2mqtt/issues/32514)) ([74478fd](https://github.com/Koenkk/zigbee2mqtt/commit/74478fd955dc434e039fac547bfe69db5a2f55ae))
* **ignore:** bump the minor-patch group with 3 updates ([#29068](https://github.com/Koenkk/zigbee2mqtt/issues/29068)) ([aa916ad](https://github.com/Koenkk/zigbee2mqtt/commit/aa916ad60451fea471217e65fbdbb38635879c8e))
* **ignore:** bump the minor-patch group with 3 updates ([#29804](https://github.com/Koenkk/zigbee2mqtt/issues/29804)) ([e2232e9](https://github.com/Koenkk/zigbee2mqtt/commit/e2232e9e9173ea9ec55152ac33bc0ee0141471bb))
* **ignore:** bump the minor-patch group with 3 updates ([#31011](https://github.com/Koenkk/zigbee2mqtt/issues/31011)) ([15e385b](https://github.com/Koenkk/zigbee2mqtt/commit/15e385bac6dbfd87869df87f1be9a66cf4db76f1))
* **ignore:** bump the minor-patch group with 3 updates ([#31295](https://github.com/Koenkk/zigbee2mqtt/issues/31295)) ([b7f1cec](https://github.com/Koenkk/zigbee2mqtt/commit/b7f1cecb49f088b7fc79dcef068034ca1d012c1d))
* **ignore:** bump the minor-patch group with 3 updates ([#31489](https://github.com/Koenkk/zigbee2mqtt/issues/31489)) ([52b063b](https://github.com/Koenkk/zigbee2mqtt/commit/52b063bb32429e14b30319c905e596b7632bd674))
* **ignore:** bump the minor-patch group with 3 updates ([#31559](https://github.com/Koenkk/zigbee2mqtt/issues/31559)) ([73d8ae8](https://github.com/Koenkk/zigbee2mqtt/commit/73d8ae895613af0d77f0a684f7305d688af11146))
* **ignore:** bump the minor-patch group with 3 updates ([#31806](https://github.com/Koenkk/zigbee2mqtt/issues/31806)) ([b3983b3](https://github.com/Koenkk/zigbee2mqtt/commit/b3983b35de0b6242d8e4d01f0d1d1e535e2d0b60))
* **ignore:** bump the minor-patch group with 3 updates ([#31982](https://github.com/Koenkk/zigbee2mqtt/issues/31982)) ([9426c3e](https://github.com/Koenkk/zigbee2mqtt/commit/9426c3ea118de9e60326e8dc257a0c2474380f8c))
* **ignore:** bump the minor-patch group with 3 updates ([#32106](https://github.com/Koenkk/zigbee2mqtt/issues/32106)) ([e5f70d5](https://github.com/Koenkk/zigbee2mqtt/commit/e5f70d5ba82923f9f37fbda79bed08d581706129))
* **ignore:** bump the minor-patch group with 3 updates ([#32312](https://github.com/Koenkk/zigbee2mqtt/issues/32312)) ([719de7a](https://github.com/Koenkk/zigbee2mqtt/commit/719de7adcfffc64e2e9133142045e5dd56e4e8d4))
* **ignore:** bump the minor-patch group with 3 updates ([#32692](https://github.com/Koenkk/zigbee2mqtt/issues/32692)) ([3348cea](https://github.com/Koenkk/zigbee2mqtt/commit/3348cea190125b9a466434cc5ea95ab7a857559d))
* **ignore:** bump the minor-patch group with 4 updates ([#28778](https://github.com/Koenkk/zigbee2mqtt/issues/28778)) ([c9d01f4](https://github.com/Koenkk/zigbee2mqtt/commit/c9d01f4118cdef2a54bae43b09edb5e54c4f3042))
* **ignore:** bump the minor-patch group with 4 updates ([#28916](https://github.com/Koenkk/zigbee2mqtt/issues/28916)) ([e24ec79](https://github.com/Koenkk/zigbee2mqtt/commit/e24ec79980535d62b02e849359e23a189f2255ed))
* **ignore:** bump the minor-patch group with 4 updates ([#31081](https://github.com/Koenkk/zigbee2mqtt/issues/31081)) ([42d88fe](https://github.com/Koenkk/zigbee2mqtt/commit/42d88fe90d6272f2c8241ac7a6e6b2c5ce5a55af))
* **ignore:** bump the minor-patch group with 5 updates ([#29314](https://github.com/Koenkk/zigbee2mqtt/issues/29314)) ([82f1092](https://github.com/Koenkk/zigbee2mqtt/commit/82f10923027806f5cd3b31e27184883e07768c71))
* **ignore:** bump the minor-patch group with 5 updates ([#32452](https://github.com/Koenkk/zigbee2mqtt/issues/32452)) ([e8ba0b2](https://github.com/Koenkk/zigbee2mqtt/commit/e8ba0b24f8b8528e98904234a32515713f457781))
* **ignore:** bump throttleit from 2.1.0 to 3.0.0 ([#32551](https://github.com/Koenkk/zigbee2mqtt/issues/32551)) ([3f7c0ca](https://github.com/Koenkk/zigbee2mqtt/commit/3f7c0ca71502a2a522a11587a878534f1fd42fe1))
* **ignore:** bump typescript from 5.9.3 to 6.0.2 ([#31560](https://github.com/Koenkk/zigbee2mqtt/issues/31560)) ([9e27181](https://github.com/Koenkk/zigbee2mqtt/commit/9e27181b64b771250db283b5a4b1da7835470ee6))
* **ignore:** bump typescript from 6.0.3 to 7.0.2 ([#32552](https://github.com/Koenkk/zigbee2mqtt/issues/32552)) ([fa12ebe](https://github.com/Koenkk/zigbee2mqtt/commit/fa12ebe27c1598e7ac1108f3e3d7d3ffbe2eccab))
* **ignore:** bump ws from 8.21.0 to 8.21.1 in the minor-patch group across 1 directory ([#32624](https://github.com/Koenkk/zigbee2mqtt/issues/32624)) ([bbad134](https://github.com/Koenkk/zigbee2mqtt/commit/bbad134e8ea420200e6b5f75a17bd5a5a9d601fe))
* **ignore:** bump zigbee2mqtt-windfront from 2.1.0 to 2.2.0 in the minor-patch group ([#28764](https://github.com/Koenkk/zigbee2mqtt/issues/28764)) ([1166341](https://github.com/Koenkk/zigbee2mqtt/commit/116634193a0c7693c6431ceccb373cf5e3f99e40))
* **ignore:** bump zigbee2mqtt-windfront from 2.12.1 to 2.13.0 in the minor-patch group ([#32550](https://github.com/Koenkk/zigbee2mqtt/issues/32550)) ([66db1bd](https://github.com/Koenkk/zigbee2mqtt/commit/66db1bdbcd8b6303c82d7c77679860537b0868e1))
* **ignore:** bump zigbee2mqtt-windfront from 2.4.0 to 2.4.1 in the minor-patch group ([#29905](https://github.com/Koenkk/zigbee2mqtt/issues/29905)) ([4a020fd](https://github.com/Koenkk/zigbee2mqtt/commit/4a020fd8de2dab2973b859c34b934416a15e82a5))
* **ignore:** bump zigbee2mqtt-windfront from 2.6.1 to 2.6.2 in the minor-patch group ([#30412](https://github.com/Koenkk/zigbee2mqtt/issues/30412)) ([4d6269e](https://github.com/Koenkk/zigbee2mqtt/commit/4d6269e7e89af52df0b2576925abade2f6b60e09))
* **ignore:** bump zigbee2mqtt-windfront from 2.8.0 to 2.8.1 in the minor-patch group ([#30879](https://github.com/Koenkk/zigbee2mqtt/issues/30879)) ([9ddbb2a](https://github.com/Koenkk/zigbee2mqtt/commit/9ddbb2a21a0d302ed39ac4c658587d9f33c48960))
* **ignore:** bump zigbee2mqtt-windfront in the minor-patch group ([8d980ae](https://github.com/Koenkk/zigbee2mqtt/commit/8d980ae6ecd759c6c6fb42fc7fbb1d701a3317fa))
* **ignore:** bump zigbee2mqtt-windfront to 2.4.2 ([#29919](https://github.com/Koenkk/zigbee2mqtt/issues/29919)) ([3dce854](https://github.com/Koenkk/zigbee2mqtt/commit/3dce8548ceee111f49a5b3460fdb2cbb4f47da62))
* **ignore:** Changes for new ZHC `definition.version` ([#30402](https://github.com/Koenkk/zigbee2mqtt/issues/30402)) ([b71461e](https://github.com/Koenkk/zigbee2mqtt/commit/b71461e03cd8ceea5365f4c0b2e794e34341325e))
* **ignore:** Fix enableDisableExtension ([#31176](https://github.com/Koenkk/zigbee2mqtt/issues/31176)) ([e1e856d](https://github.com/Koenkk/zigbee2mqtt/commit/e1e856d7da9c92952c3de94853794337fea1945d))
* **ignore:** missing type for new OTA endpoint ([#32195](https://github.com/Koenkk/zigbee2mqtt/issues/32195)) ([6c2c874](https://github.com/Koenkk/zigbee2mqtt/commit/6c2c874641ada9cae1c9bdd963a3201cb1cf0fef))
* **ignore:** prop name convention on new API endpoints ([#29820](https://github.com/Koenkk/zigbee2mqtt/issues/29820)) ([ad06968](https://github.com/Koenkk/zigbee2mqtt/commit/ad0696856a9a63fc74b9c2115f08d7fd97850545))
* **ignore:** Remove attribute_and_json warnings for HA ([#32616](https://github.com/Koenkk/zigbee2mqtt/issues/32616)) ([5e56c45](https://github.com/Koenkk/zigbee2mqtt/commit/5e56c454c3fee03c43152c807ec3e449d6211728))
* **ignore:** Replace multiple area occupancy fields with camelCase instead of PascalCase ([#30745](https://github.com/Koenkk/zigbee2mqtt/issues/30745)) ([d9e4244](https://github.com/Koenkk/zigbee2mqtt/commit/d9e4244f801aaa24b8347ab684521391df1ce0e2))
* **ignore:** update zigbee-herdsman to 10.0.1 ([#31383](https://github.com/Koenkk/zigbee2mqtt/issues/31383)) ([6fca374](https://github.com/Koenkk/zigbee2mqtt/commit/6fca3744c9715dd4afb7fc777cf783c84bfb91fc))
* **ignore:** update zigbee-herdsman to 10.0.2 ([#31413](https://github.com/Koenkk/zigbee2mqtt/issues/31413)) ([075f87c](https://github.com/Koenkk/zigbee2mqtt/commit/075f87c7822c29c74fc1c4031866aca8c445c9be))
* **ignore:** update zigbee-herdsman to 10.0.3 ([#31450](https://github.com/Koenkk/zigbee2mqtt/issues/31450)) ([6fb9d56](https://github.com/Koenkk/zigbee2mqtt/commit/6fb9d563e69c2db3b31fd0f163e5f30924047e3f))
* **ignore:** update zigbee-herdsman to 10.0.4 ([#31478](https://github.com/Koenkk/zigbee2mqtt/issues/31478)) ([867179c](https://github.com/Koenkk/zigbee2mqtt/commit/867179c430eacf9bd66ef359b9a7435f2299d944))
* **ignore:** update zigbee-herdsman to 10.0.5 ([#31562](https://github.com/Koenkk/zigbee2mqtt/issues/31562)) ([fb3189b](https://github.com/Koenkk/zigbee2mqtt/commit/fb3189b7431cb203092bbe235bd35194e2b44a44))
* **ignore:** update zigbee-herdsman to 10.0.6 ([#31636](https://github.com/Koenkk/zigbee2mqtt/issues/31636)) ([aef3242](https://github.com/Koenkk/zigbee2mqtt/commit/aef32423195914aa9ef08875d4fe6de36df71f11))
* **ignore:** update zigbee-herdsman to 10.0.7 ([#31673](https://github.com/Koenkk/zigbee2mqtt/issues/31673)) ([a0204ad](https://github.com/Koenkk/zigbee2mqtt/commit/a0204ad3f00ed366e4c41455ae4189bd31870a95))
* **ignore:** update zigbee-herdsman to 10.0.8 ([#31945](https://github.com/Koenkk/zigbee2mqtt/issues/31945)) ([04a4365](https://github.com/Koenkk/zigbee2mqtt/commit/04a43652d6e131694e29e06ca646eeac37ed3c7d))
* **ignore:** update zigbee-herdsman to 10.1.0 ([#32027](https://github.com/Koenkk/zigbee2mqtt/issues/32027)) ([36c4db7](https://github.com/Koenkk/zigbee2mqtt/commit/36c4db7f8f4e3438f6ebb82cd6ecadf6c54e231e))
* **ignore:** update zigbee-herdsman to 10.2.0 ([#32179](https://github.com/Koenkk/zigbee2mqtt/issues/32179)) ([f9e1bac](https://github.com/Koenkk/zigbee2mqtt/commit/f9e1bac588e65b223e75f4a2b2e9d528933fdbd8))
* **ignore:** update zigbee-herdsman to 10.3.0 ([#32193](https://github.com/Koenkk/zigbee2mqtt/issues/32193)) ([ea6ccac](https://github.com/Koenkk/zigbee2mqtt/commit/ea6ccac9380af1d9f5672174b2545409f36c2769))
* **ignore:** update zigbee-herdsman to 10.4.0 ([#32249](https://github.com/Koenkk/zigbee2mqtt/issues/32249)) ([582f153](https://github.com/Koenkk/zigbee2mqtt/commit/582f1538961af9c1b6434e6a1bc4acebc63a2541))
* **ignore:** update zigbee-herdsman to 10.4.1 ([#32333](https://github.com/Koenkk/zigbee2mqtt/issues/32333)) ([2f44449](https://github.com/Koenkk/zigbee2mqtt/commit/2f4444995361257cbeef0c780532381ec6cc254f))
* **ignore:** update zigbee-herdsman to 10.4.2 ([#32388](https://github.com/Koenkk/zigbee2mqtt/issues/32388)) ([628f97f](https://github.com/Koenkk/zigbee2mqtt/commit/628f97f498747cd2e6be5808c7c609b948af528f))
* **ignore:** update zigbee-herdsman to 10.5.0 ([#32406](https://github.com/Koenkk/zigbee2mqtt/issues/32406)) ([6ab52df](https://github.com/Koenkk/zigbee2mqtt/commit/6ab52df08af1e9b80eb4b58752b7f403d52844a7))
* **ignore:** update zigbee-herdsman to 10.6.0 ([#32422](https://github.com/Koenkk/zigbee2mqtt/issues/32422)) ([dcb16b8](https://github.com/Koenkk/zigbee2mqtt/commit/dcb16b8271349390deee9dc0a0ca4d02239a54bc))
* **ignore:** update zigbee-herdsman to 10.6.1 ([#32442](https://github.com/Koenkk/zigbee2mqtt/issues/32442)) ([f867694](https://github.com/Koenkk/zigbee2mqtt/commit/f86769483102762798272655c4fb117f33263489))
* **ignore:** update zigbee-herdsman to 10.6.2 ([#32500](https://github.com/Koenkk/zigbee2mqtt/issues/32500)) ([6e55716](https://github.com/Koenkk/zigbee2mqtt/commit/6e557162cbf548735c5d907231cec69fa94f6c53))
* **ignore:** update zigbee-herdsman to 10.6.3 ([#32623](https://github.com/Koenkk/zigbee2mqtt/issues/32623)) ([b8d388c](https://github.com/Koenkk/zigbee2mqtt/commit/b8d388ca7afce29cc6f09c452cbe5c01e79d8244))
* **ignore:** update zigbee-herdsman to 10.8.0 ([#32701](https://github.com/Koenkk/zigbee2mqtt/issues/32701)) ([28e410b](https://github.com/Koenkk/zigbee2mqtt/commit/28e410bbf442f32e9be4298d19fe45ac586b8a88))
* **ignore:** update zigbee-herdsman to 6.1.4 ([#28762](https://github.com/Koenkk/zigbee2mqtt/issues/28762)) ([508318d](https://github.com/Koenkk/zigbee2mqtt/commit/508318de57267791422d731fcb9a423fa01c8062))
* **ignore:** update zigbee-herdsman to 6.1.5 ([#28773](https://github.com/Koenkk/zigbee2mqtt/issues/28773)) ([a0d91cc](https://github.com/Koenkk/zigbee2mqtt/commit/a0d91cc063437cc595dd693e33fe5b1f044cd6bf))
* **ignore:** update zigbee-herdsman to 6.2.0 ([#28900](https://github.com/Koenkk/zigbee2mqtt/issues/28900)) ([8b4a4be](https://github.com/Koenkk/zigbee2mqtt/commit/8b4a4be2a4404a8b165bb62c43bca73894d6b13f))
* **ignore:** update zigbee-herdsman to 6.3.0 ([#29113](https://github.com/Koenkk/zigbee2mqtt/issues/29113)) ([354d06a](https://github.com/Koenkk/zigbee2mqtt/commit/354d06aebc8fc786309ce9d66403e335799d46ab))
* **ignore:** update zigbee-herdsman to 6.3.1 ([#29153](https://github.com/Koenkk/zigbee2mqtt/issues/29153)) ([feac6aa](https://github.com/Koenkk/zigbee2mqtt/commit/feac6aa677cb169bf3c9b47aa4e6687ea8e6432d))
* **ignore:** update zigbee-herdsman to 6.3.2 ([#29246](https://github.com/Koenkk/zigbee2mqtt/issues/29246)) ([ef15924](https://github.com/Koenkk/zigbee2mqtt/commit/ef15924260cf2aa0094f2f93db0ddd8fcfa0ac6f))
* **ignore:** update zigbee-herdsman to 6.3.3 ([#29500](https://github.com/Koenkk/zigbee2mqtt/issues/29500)) ([39c5bc8](https://github.com/Koenkk/zigbee2mqtt/commit/39c5bc8c0446a7ccdffe6f82ae9e7f31f60f20ba))
* **ignore:** update zigbee-herdsman to 6.4.0 ([#29522](https://github.com/Koenkk/zigbee2mqtt/issues/29522)) ([e788c52](https://github.com/Koenkk/zigbee2mqtt/commit/e788c520a9268498daf0f88d7bedb289546d226e))
* **ignore:** update zigbee-herdsman to 6.4.1 ([#29595](https://github.com/Koenkk/zigbee2mqtt/issues/29595)) ([61c6ec6](https://github.com/Koenkk/zigbee2mqtt/commit/61c6ec611174529e4c041a56146ba8405cfe9ed8))
* **ignore:** update zigbee-herdsman to 6.4.2 ([#29683](https://github.com/Koenkk/zigbee2mqtt/issues/29683)) ([53b7eaa](https://github.com/Koenkk/zigbee2mqtt/commit/53b7eaabf0647c8d1edb0de24b89776991af576c))
* **ignore:** update zigbee-herdsman to 7.0.0 ([3e2aefb](https://github.com/Koenkk/zigbee2mqtt/commit/3e2aefbcf84f43d45c1d6ca33397c8bb76648822))
* **ignore:** update zigbee-herdsman to 7.0.1 ([#29855](https://github.com/Koenkk/zigbee2mqtt/issues/29855)) ([802271e](https://github.com/Koenkk/zigbee2mqtt/commit/802271ed37d404944d315f83f3b623ba9e424bb9))
* **ignore:** update zigbee-herdsman to 7.0.2 ([#29956](https://github.com/Koenkk/zigbee2mqtt/issues/29956)) ([403bd4a](https://github.com/Koenkk/zigbee2mqtt/commit/403bd4a72541bf7342de246b033c5090b186df32))
* **ignore:** update zigbee-herdsman to 7.0.3 ([#30008](https://github.com/Koenkk/zigbee2mqtt/issues/30008)) ([bae9bb8](https://github.com/Koenkk/zigbee2mqtt/commit/bae9bb8812c81e46d4068b4284e0f9957821f88a))
* **ignore:** update zigbee-herdsman to 7.0.4 ([#30041](https://github.com/Koenkk/zigbee2mqtt/issues/30041)) ([334fa2a](https://github.com/Koenkk/zigbee2mqtt/commit/334fa2a60d04c66749978b8b8f4b9b5af6c7c7eb))
* **ignore:** update zigbee-herdsman to 7.0.5 ([#30116](https://github.com/Koenkk/zigbee2mqtt/issues/30116)) ([4574ffc](https://github.com/Koenkk/zigbee2mqtt/commit/4574ffc71a799846c6a04121c3b5908f968ff0be))
* **ignore:** update zigbee-herdsman to 7.0.6 ([#30142](https://github.com/Koenkk/zigbee2mqtt/issues/30142)) ([ecb0af4](https://github.com/Koenkk/zigbee2mqtt/commit/ecb0af40575ebaadf887685c50cd1a9968cc1c68))
* **ignore:** update zigbee-herdsman to 8.0.0 and zigbee-herdsman-converters to 25.87.0 ([#30186](https://github.com/Koenkk/zigbee2mqtt/issues/30186)) ([7f5f6a5](https://github.com/Koenkk/zigbee2mqtt/commit/7f5f6a5849d940a6e5288865edf7999c41af12e2))
* **ignore:** update zigbee-herdsman to 8.0.1 ([#30240](https://github.com/Koenkk/zigbee2mqtt/issues/30240)) ([1688a3b](https://github.com/Koenkk/zigbee2mqtt/commit/1688a3b5323a8774ad2c36775fdb99f48057f72a))
* **ignore:** update zigbee-herdsman to 8.0.2 ([#30480](https://github.com/Koenkk/zigbee2mqtt/issues/30480)) ([1147cf1](https://github.com/Koenkk/zigbee2mqtt/commit/1147cf16dd62dce5863a93983835c46e8e92e95d))
* **ignore:** update zigbee-herdsman to 8.0.3 ([#30632](https://github.com/Koenkk/zigbee2mqtt/issues/30632)) ([d44cdbd](https://github.com/Koenkk/zigbee2mqtt/commit/d44cdbde4f5247f69af4c79fd45a62c03037eca7))
* **ignore:** update zigbee-herdsman to 8.1.0 ([#30725](https://github.com/Koenkk/zigbee2mqtt/issues/30725)) ([9d266bd](https://github.com/Koenkk/zigbee2mqtt/commit/9d266bda9a34efbe726f8d984da48e5ba3c46a83))
* **ignore:** update zigbee-herdsman to 9.0.0 ([#30787](https://github.com/Koenkk/zigbee2mqtt/issues/30787)) ([971ddaa](https://github.com/Koenkk/zigbee2mqtt/commit/971ddaa3cbc2132150f7cde4cdbe0575349eabac))
* **ignore:** update zigbee-herdsman to 9.0.1 ([#30788](https://github.com/Koenkk/zigbee2mqtt/issues/30788)) ([6d5e7e3](https://github.com/Koenkk/zigbee2mqtt/commit/6d5e7e334a9a560b9f6293cf448ad84de8504b67))
* **ignore:** update zigbee-herdsman to 9.0.10 ([#31245](https://github.com/Koenkk/zigbee2mqtt/issues/31245)) ([d632382](https://github.com/Koenkk/zigbee2mqtt/commit/d6323825e4ae3168d52a096e7647bc7174d3cdb1))
* **ignore:** update zigbee-herdsman to 9.0.11 ([#31257](https://github.com/Koenkk/zigbee2mqtt/issues/31257)) ([1747618](https://github.com/Koenkk/zigbee2mqtt/commit/1747618afa3948ac9fb8899d94fba8456198e4b5))
* **ignore:** update zigbee-herdsman to 9.0.2 ([10478e7](https://github.com/Koenkk/zigbee2mqtt/commit/10478e797e1733f9bf66defce0e84b6cc1f23020))
* **ignore:** update zigbee-herdsman to 9.0.3 ([#30960](https://github.com/Koenkk/zigbee2mqtt/issues/30960)) ([30d311e](https://github.com/Koenkk/zigbee2mqtt/commit/30d311e8494c00e6ade7759a0a2bfdc9ab79022d))
* **ignore:** update zigbee-herdsman to 9.0.4 ([#30996](https://github.com/Koenkk/zigbee2mqtt/issues/30996)) ([bf888de](https://github.com/Koenkk/zigbee2mqtt/commit/bf888de4789b01e824ca6b5f82506691b4f689a9))
* **ignore:** update zigbee-herdsman to 9.0.5 ([#31117](https://github.com/Koenkk/zigbee2mqtt/issues/31117)) ([5957085](https://github.com/Koenkk/zigbee2mqtt/commit/5957085fcc8d6acdedf426a3f16632f1de55a061))
* **ignore:** update zigbee-herdsman to 9.0.6 ([#31149](https://github.com/Koenkk/zigbee2mqtt/issues/31149)) ([8560c11](https://github.com/Koenkk/zigbee2mqtt/commit/8560c11e0667304a643eac907d3c62e35bcc2e23))
* **ignore:** update zigbee-herdsman to 9.0.7 ([#31174](https://github.com/Koenkk/zigbee2mqtt/issues/31174)) ([79bcf27](https://github.com/Koenkk/zigbee2mqtt/commit/79bcf276f57454a77ea76b6d8286544abd4d7cca))
* **ignore:** update zigbee-herdsman to 9.0.8 ([#31207](https://github.com/Koenkk/zigbee2mqtt/issues/31207)) ([c7d54ec](https://github.com/Koenkk/zigbee2mqtt/commit/c7d54ec360ee34ba4cb062dcd474154fb70467a5))
* **ignore:** update zigbee-herdsman to 9.0.9 ([#31215](https://github.com/Koenkk/zigbee2mqtt/issues/31215)) ([8ee5b1f](https://github.com/Koenkk/zigbee2mqtt/commit/8ee5b1f03bdc06062d58913e827f086b6bbcb026))
* **ignore:** update zigbee-herdsman-converters to 25.100.0 ([#30499](https://github.com/Koenkk/zigbee2mqtt/issues/30499)) ([a2b81ab](https://github.com/Koenkk/zigbee2mqtt/commit/a2b81ab89491f8babf40001404969817f19c5b5e))
* **ignore:** update zigbee-herdsman-converters to 25.101.0 ([#30520](https://github.com/Koenkk/zigbee2mqtt/issues/30520)) ([4313bac](https://github.com/Koenkk/zigbee2mqtt/commit/4313baca09b8476466b8134c551b59f92996b53f))
* **ignore:** update zigbee-herdsman-converters to 25.102.0 ([#30537](https://github.com/Koenkk/zigbee2mqtt/issues/30537)) ([57245e5](https://github.com/Koenkk/zigbee2mqtt/commit/57245e52343568678a4dace9bf569c05f508decc))
* **ignore:** update zigbee-herdsman-converters to 25.103.0 ([#30552](https://github.com/Koenkk/zigbee2mqtt/issues/30552)) ([d4522c5](https://github.com/Koenkk/zigbee2mqtt/commit/d4522c53caca86ba3a087fa67839e2ebf5d9deca))
* **ignore:** update zigbee-herdsman-converters to 25.104.0 ([#30564](https://github.com/Koenkk/zigbee2mqtt/issues/30564)) ([e68403d](https://github.com/Koenkk/zigbee2mqtt/commit/e68403d8d3ac68f2d09df066d1be8db87f2e7977))
* **ignore:** update zigbee-herdsman-converters to 25.105.0 ([#30605](https://github.com/Koenkk/zigbee2mqtt/issues/30605)) ([29c7571](https://github.com/Koenkk/zigbee2mqtt/commit/29c757145d6ed16a8629b68379cd85fabe45a8f1))
* **ignore:** update zigbee-herdsman-converters to 25.106.0 ([#30631](https://github.com/Koenkk/zigbee2mqtt/issues/30631)) ([9a41ec2](https://github.com/Koenkk/zigbee2mqtt/commit/9a41ec2a39ca2373dbdcb15b09aa0eb09116c31e))
* **ignore:** update zigbee-herdsman-converters to 25.107.0 ([#30662](https://github.com/Koenkk/zigbee2mqtt/issues/30662)) ([535f75e](https://github.com/Koenkk/zigbee2mqtt/commit/535f75e90ca936afcac0c79e4c2452dce7b9b08d))
* **ignore:** update zigbee-herdsman-converters to 25.108.0 ([#30672](https://github.com/Koenkk/zigbee2mqtt/issues/30672)) ([5e1b7d4](https://github.com/Koenkk/zigbee2mqtt/commit/5e1b7d4830b5597eb993f32e3a6d1edd84b48e4c))
* **ignore:** update zigbee-herdsman-converters to 25.109.0 ([#30709](https://github.com/Koenkk/zigbee2mqtt/issues/30709)) ([ee6afaf](https://github.com/Koenkk/zigbee2mqtt/commit/ee6afaf1c608b89efef452d7cb337345b035d76a))
* **ignore:** update zigbee-herdsman-converters to 25.110.0 ([#30726](https://github.com/Koenkk/zigbee2mqtt/issues/30726)) ([20af502](https://github.com/Koenkk/zigbee2mqtt/commit/20af50201ef1e081223ee8b2f0b6304777c7b7f1))
* **ignore:** update zigbee-herdsman-converters to 25.111.0 ([#30739](https://github.com/Koenkk/zigbee2mqtt/issues/30739)) ([2bb44f7](https://github.com/Koenkk/zigbee2mqtt/commit/2bb44f7a8b8bd9fc1a21b127b764a5d697e65c3e))
* **ignore:** update zigbee-herdsman-converters to 25.112.0 ([#30767](https://github.com/Koenkk/zigbee2mqtt/issues/30767)) ([ad51d12](https://github.com/Koenkk/zigbee2mqtt/commit/ad51d12a2d1325e0b0c1b56a633b229f0f3d048b))
* **ignore:** update zigbee-herdsman-converters to 25.113.0 ([#30789](https://github.com/Koenkk/zigbee2mqtt/issues/30789)) ([036ee47](https://github.com/Koenkk/zigbee2mqtt/commit/036ee471ddfda094a3ae3f970dedb5600c9d8808))
* **ignore:** update zigbee-herdsman-converters to 25.114.0 ([#30803](https://github.com/Koenkk/zigbee2mqtt/issues/30803)) ([999df29](https://github.com/Koenkk/zigbee2mqtt/commit/999df2913ff7e47fe9438ed9398a0922c0b40916))
* **ignore:** update zigbee-herdsman-converters to 25.115.0 ([#30826](https://github.com/Koenkk/zigbee2mqtt/issues/30826)) ([99e06a8](https://github.com/Koenkk/zigbee2mqtt/commit/99e06a8d3ea66ed2a6acf6cddece2f2070cce6ae))
* **ignore:** update zigbee-herdsman-converters to 25.116.0 ([#30839](https://github.com/Koenkk/zigbee2mqtt/issues/30839)) ([5a4d6b5](https://github.com/Koenkk/zigbee2mqtt/commit/5a4d6b56ad2ea62378b7dd161eba31916f08c65b))
* **ignore:** update zigbee-herdsman-converters to 25.117.0 ([#30849](https://github.com/Koenkk/zigbee2mqtt/issues/30849)) ([4afbce6](https://github.com/Koenkk/zigbee2mqtt/commit/4afbce62ebc667fc7d94b8b23c0bb0125f065d79))
* **ignore:** update zigbee-herdsman-converters to 25.118.0 ([#30865](https://github.com/Koenkk/zigbee2mqtt/issues/30865)) ([f688340](https://github.com/Koenkk/zigbee2mqtt/commit/f68834049cde265f731ab271b50fcfa6f85fa971))
* **ignore:** update zigbee-herdsman-converters to 25.119.0 ([#30874](https://github.com/Koenkk/zigbee2mqtt/issues/30874)) ([788b0d3](https://github.com/Koenkk/zigbee2mqtt/commit/788b0d3993c877adc0361a4b2f9d2df79cea0b8d))
* **ignore:** update zigbee-herdsman-converters to 25.120.0 ([#30876](https://github.com/Koenkk/zigbee2mqtt/issues/30876)) ([49b752d](https://github.com/Koenkk/zigbee2mqtt/commit/49b752da3527920dcdfae6a617f4aa084f6a8901))
* **ignore:** update zigbee-herdsman-converters to 25.121.0 ([#30898](https://github.com/Koenkk/zigbee2mqtt/issues/30898)) ([09d010e](https://github.com/Koenkk/zigbee2mqtt/commit/09d010ea067446d26b6c77411aac78d045dbc6ea))
* **ignore:** update zigbee-herdsman-converters to 25.122.0 ([#30935](https://github.com/Koenkk/zigbee2mqtt/issues/30935)) ([402ed60](https://github.com/Koenkk/zigbee2mqtt/commit/402ed6094eedb40ecdb841b4b1f7d29d59afc89c))
* **ignore:** update zigbee-herdsman-converters to 25.33.0 ([#28758](https://github.com/Koenkk/zigbee2mqtt/issues/28758)) ([26c74b5](https://github.com/Koenkk/zigbee2mqtt/commit/26c74b57786e0feffe5d1bb3ae7406e25449a591))
* **ignore:** update zigbee-herdsman-converters to 25.34.0 ([#28763](https://github.com/Koenkk/zigbee2mqtt/issues/28763)) ([f6c0ce7](https://github.com/Koenkk/zigbee2mqtt/commit/f6c0ce735ff582e8d07a11f951a1faa9720367c1))
* **ignore:** update zigbee-herdsman-converters to 25.35.1 ([#28774](https://github.com/Koenkk/zigbee2mqtt/issues/28774)) ([1283859](https://github.com/Koenkk/zigbee2mqtt/commit/1283859a12d09ed1b7ea2605acbf2293adc80df0))
* **ignore:** update zigbee-herdsman-converters to 25.36.0 ([#28790](https://github.com/Koenkk/zigbee2mqtt/issues/28790)) ([ca489da](https://github.com/Koenkk/zigbee2mqtt/commit/ca489daad8def2683f4a5c248e8098d01cc7cc6f))
* **ignore:** update zigbee-herdsman-converters to 25.37.0 ([#28837](https://github.com/Koenkk/zigbee2mqtt/issues/28837)) ([d1ce146](https://github.com/Koenkk/zigbee2mqtt/commit/d1ce1465421f81a68e03ffe76e928e2ea33cdde0))
* **ignore:** update zigbee-herdsman-converters to 25.37.1 ([#28857](https://github.com/Koenkk/zigbee2mqtt/issues/28857)) ([c66008b](https://github.com/Koenkk/zigbee2mqtt/commit/c66008b36bccdc1626a7f095f1af498b100610e3))
* **ignore:** update zigbee-herdsman-converters to 25.37.2 ([#28859](https://github.com/Koenkk/zigbee2mqtt/issues/28859)) ([1d38da6](https://github.com/Koenkk/zigbee2mqtt/commit/1d38da6ac5ac6a54c1c13a09003545f9accbfa7a))
* **ignore:** update zigbee-herdsman-converters to 25.38.0 ([#28872](https://github.com/Koenkk/zigbee2mqtt/issues/28872)) ([a75a40b](https://github.com/Koenkk/zigbee2mqtt/commit/a75a40b042c8276e2b52c666b555fae4f656fa04))
* **ignore:** update zigbee-herdsman-converters to 25.39.0 ([#28899](https://github.com/Koenkk/zigbee2mqtt/issues/28899)) ([548f646](https://github.com/Koenkk/zigbee2mqtt/commit/548f646f2fc1f53e18f84aec817f845de8edb04d))
* **ignore:** update zigbee-herdsman-converters to 25.40.0 ([#28928](https://github.com/Koenkk/zigbee2mqtt/issues/28928)) ([5f08c3e](https://github.com/Koenkk/zigbee2mqtt/commit/5f08c3e83beaa98f6e5cf5fd32e106730e86f611))
* **ignore:** update zigbee-herdsman-converters to 25.41.0 ([#28945](https://github.com/Koenkk/zigbee2mqtt/issues/28945)) ([79a27e3](https://github.com/Koenkk/zigbee2mqtt/commit/79a27e3b060dd552110a1851252dd6a17524be9b))
* **ignore:** update zigbee-herdsman-converters to 25.42.0 ([#28963](https://github.com/Koenkk/zigbee2mqtt/issues/28963)) ([8b1f5a5](https://github.com/Koenkk/zigbee2mqtt/commit/8b1f5a555c571990e9e895b285060872ba27402f))
* **ignore:** update zigbee-herdsman-converters to 25.43.0 ([#29014](https://github.com/Koenkk/zigbee2mqtt/issues/29014)) ([9c19aa9](https://github.com/Koenkk/zigbee2mqtt/commit/9c19aa94948205489b50ac79063b2a065495a8a8))
* **ignore:** update zigbee-herdsman-converters to 25.44.0 ([#29050](https://github.com/Koenkk/zigbee2mqtt/issues/29050)) ([bafc8e4](https://github.com/Koenkk/zigbee2mqtt/commit/bafc8e4284d9493388c8ad8497bb55781238c547))
* **ignore:** update zigbee-herdsman-converters to 25.46.0 ([#29096](https://github.com/Koenkk/zigbee2mqtt/issues/29096)) ([ad9b114](https://github.com/Koenkk/zigbee2mqtt/commit/ad9b114b1af8225e8e1414ffa23b322271fb1694))
* **ignore:** update zigbee-herdsman-converters to 25.47.0 ([#29130](https://github.com/Koenkk/zigbee2mqtt/issues/29130)) ([03a81c6](https://github.com/Koenkk/zigbee2mqtt/commit/03a81c6ab14d9ae7b2a9fdd7513dcbbe55cdc16c))
* **ignore:** update zigbee-herdsman-converters to 25.48.0 ([#29141](https://github.com/Koenkk/zigbee2mqtt/issues/29141)) ([1d69715](https://github.com/Koenkk/zigbee2mqtt/commit/1d69715a5a75779509f81788a1541dd4c492ca9e))
* **ignore:** update zigbee-herdsman-converters to 25.49.0 ([#29154](https://github.com/Koenkk/zigbee2mqtt/issues/29154)) ([3ecb0c5](https://github.com/Koenkk/zigbee2mqtt/commit/3ecb0c5e41b514bfb824926fc9cf19636ec68ff5))
* **ignore:** update zigbee-herdsman-converters to 25.50.0 ([#29180](https://github.com/Koenkk/zigbee2mqtt/issues/29180)) ([df70f11](https://github.com/Koenkk/zigbee2mqtt/commit/df70f11a154e27208188432be453fac821f73597))
* **ignore:** update zigbee-herdsman-converters to 25.51.0 ([#29195](https://github.com/Koenkk/zigbee2mqtt/issues/29195)) ([4080522](https://github.com/Koenkk/zigbee2mqtt/commit/408052282d9a28f46df33266112cf52dbc0f98c0))
* **ignore:** update zigbee-herdsman-converters to 25.51.1 ([#29212](https://github.com/Koenkk/zigbee2mqtt/issues/29212)) ([238ea7c](https://github.com/Koenkk/zigbee2mqtt/commit/238ea7c4c8073cb0164281cc0cdec05a3212d6ad))
* **ignore:** update zigbee-herdsman-converters to 25.52.0 ([#29247](https://github.com/Koenkk/zigbee2mqtt/issues/29247)) ([05382a9](https://github.com/Koenkk/zigbee2mqtt/commit/05382a969232d5c1a286ba1a85c6422d0a81a39f))
* **ignore:** update zigbee-herdsman-converters to 25.53.0 ([#29266](https://github.com/Koenkk/zigbee2mqtt/issues/29266)) ([d719c1f](https://github.com/Koenkk/zigbee2mqtt/commit/d719c1f36848c08efaad3eda8e90fb5d61c4c657))
* **ignore:** update zigbee-herdsman-converters to 25.54.0 ([#29306](https://github.com/Koenkk/zigbee2mqtt/issues/29306)) ([003c81b](https://github.com/Koenkk/zigbee2mqtt/commit/003c81b745be7f3262d6e778957aec6a9cf3a404))
* **ignore:** update zigbee-herdsman-converters to 25.55.0 ([#29323](https://github.com/Koenkk/zigbee2mqtt/issues/29323)) ([592c256](https://github.com/Koenkk/zigbee2mqtt/commit/592c256f96cca5219eb8620422f3ebf73fba3c1e))
* **ignore:** update zigbee-herdsman-converters to 25.56.0 ([#29338](https://github.com/Koenkk/zigbee2mqtt/issues/29338)) ([61d99b7](https://github.com/Koenkk/zigbee2mqtt/commit/61d99b7ece63af35055fabbd9c07d43b3cbc80f9))
* **ignore:** update zigbee-herdsman-converters to 25.57.0 ([#29370](https://github.com/Koenkk/zigbee2mqtt/issues/29370)) ([faa65f7](https://github.com/Koenkk/zigbee2mqtt/commit/faa65f78a29f5738a83d6fd67dd749f7d02adb39))
* **ignore:** update zigbee-herdsman-converters to 25.59.0 ([#29382](https://github.com/Koenkk/zigbee2mqtt/issues/29382)) ([40bf14b](https://github.com/Koenkk/zigbee2mqtt/commit/40bf14b93cb94292cce954916eba30f48d3170e2))
* **ignore:** update zigbee-herdsman-converters to 25.60.0 ([#29426](https://github.com/Koenkk/zigbee2mqtt/issues/29426)) ([eeed56e](https://github.com/Koenkk/zigbee2mqtt/commit/eeed56ef2c01dfe67e7a0cd7cafd35ef1876f5b4))
* **ignore:** update zigbee-herdsman-converters to 25.62.0 ([#29449](https://github.com/Koenkk/zigbee2mqtt/issues/29449)) ([ea1c5a8](https://github.com/Koenkk/zigbee2mqtt/commit/ea1c5a8f4e8537a2f8c59988bb712f62a0adffd7))
* **ignore:** update zigbee-herdsman-converters to 25.63.0 ([#29482](https://github.com/Koenkk/zigbee2mqtt/issues/29482)) ([5a6a9cf](https://github.com/Koenkk/zigbee2mqtt/commit/5a6a9cf4d2de803a10cd38bc19f3ec256eff5929))
* **ignore:** update zigbee-herdsman-converters to 25.64.0 ([#29499](https://github.com/Koenkk/zigbee2mqtt/issues/29499)) ([5c4e43f](https://github.com/Koenkk/zigbee2mqtt/commit/5c4e43fd109321ddcfad3a67b60423f0197c51a6))
* **ignore:** update zigbee-herdsman-converters to 25.65.0 ([#29523](https://github.com/Koenkk/zigbee2mqtt/issues/29523)) ([8822aed](https://github.com/Koenkk/zigbee2mqtt/commit/8822aedb5e254f5be5f6cda0cfe18fc6f69c84d6))
* **ignore:** update zigbee-herdsman-converters to 25.66.0 ([#29558](https://github.com/Koenkk/zigbee2mqtt/issues/29558)) ([940fff2](https://github.com/Koenkk/zigbee2mqtt/commit/940fff244c39da04d0de5d7e4076359dc6245aed))
* **ignore:** update zigbee-herdsman-converters to 25.67.0 ([#29572](https://github.com/Koenkk/zigbee2mqtt/issues/29572)) ([4794b3c](https://github.com/Koenkk/zigbee2mqtt/commit/4794b3c81f9e38660f8549995b7f3ef8b4cd4318))
* **ignore:** update zigbee-herdsman-converters to 25.68.0 ([#29606](https://github.com/Koenkk/zigbee2mqtt/issues/29606)) ([564e9ab](https://github.com/Koenkk/zigbee2mqtt/commit/564e9ab9eb49c3517913917af8b23d4758c6b6c7))
* **ignore:** update zigbee-herdsman-converters to 25.69.0 ([#29659](https://github.com/Koenkk/zigbee2mqtt/issues/29659)) ([16d0e56](https://github.com/Koenkk/zigbee2mqtt/commit/16d0e5696859e79be4701154dd35f7780e47acae))
* **ignore:** update zigbee-herdsman-converters to 25.70.0 ([#29684](https://github.com/Koenkk/zigbee2mqtt/issues/29684)) ([73225e2](https://github.com/Koenkk/zigbee2mqtt/commit/73225e2436bb56ff4ed47d787e8a1793fd10998d))
* **ignore:** update zigbee-herdsman-converters to 25.71.0 ([#29716](https://github.com/Koenkk/zigbee2mqtt/issues/29716)) ([d6bf517](https://github.com/Koenkk/zigbee2mqtt/commit/d6bf5174be905128432e90d54b3c43c2a68e75ac))
* **ignore:** update zigbee-herdsman-converters to 25.72.0 ([#29735](https://github.com/Koenkk/zigbee2mqtt/issues/29735)) ([7b7b988](https://github.com/Koenkk/zigbee2mqtt/commit/7b7b9889607a6e48001ae3cda5950386c24f5fd6))
* **ignore:** update zigbee-herdsman-converters to 25.73.0 ([#29752](https://github.com/Koenkk/zigbee2mqtt/issues/29752)) ([be774a4](https://github.com/Koenkk/zigbee2mqtt/commit/be774a4d0d70122ced0f6ca79f49c060a7d2d2cb))
* **ignore:** update zigbee-herdsman-converters to 25.74.0 ([#29770](https://github.com/Koenkk/zigbee2mqtt/issues/29770)) ([ba77ce7](https://github.com/Koenkk/zigbee2mqtt/commit/ba77ce7c8df0b29845a090c9defb4ce8aaf0f924))
* **ignore:** update zigbee-herdsman-converters to 25.75.0 ([#29811](https://github.com/Koenkk/zigbee2mqtt/issues/29811)) ([854df49](https://github.com/Koenkk/zigbee2mqtt/commit/854df498bd3c8999393b683052dfba6b906ba669))
* **ignore:** update zigbee-herdsman-converters to 25.76.0 ([#29823](https://github.com/Koenkk/zigbee2mqtt/issues/29823)) ([72d862f](https://github.com/Koenkk/zigbee2mqtt/commit/72d862f4a080bfcd91bf99d4c13387e719dfdb8e))
* **ignore:** update zigbee-herdsman-converters to 25.77.0 ([#29838](https://github.com/Koenkk/zigbee2mqtt/issues/29838)) ([35d8a5d](https://github.com/Koenkk/zigbee2mqtt/commit/35d8a5da7f1d817727f8b029da1992901f5b4115))
* **ignore:** update zigbee-herdsman-converters to 25.78.0 ([#29856](https://github.com/Koenkk/zigbee2mqtt/issues/29856)) ([49b4876](https://github.com/Koenkk/zigbee2mqtt/commit/49b4876768c5d15fcff1ad0c9c2d02dda96543d0))
* **ignore:** update zigbee-herdsman-converters to 25.79.0 ([#29903](https://github.com/Koenkk/zigbee2mqtt/issues/29903)) ([d411fc5](https://github.com/Koenkk/zigbee2mqtt/commit/d411fc5c693300863a19c2798f0a7a05cfc7e55f))
* **ignore:** update zigbee-herdsman-converters to 25.80.0 ([#29924](https://github.com/Koenkk/zigbee2mqtt/issues/29924)) ([445890a](https://github.com/Koenkk/zigbee2mqtt/commit/445890a0710ba7f6b101b651bf379f10390f9e7e))
* **ignore:** update zigbee-herdsman-converters to 25.81.0 ([#29959](https://github.com/Koenkk/zigbee2mqtt/issues/29959)) ([fd4282b](https://github.com/Koenkk/zigbee2mqtt/commit/fd4282b98d9630f9a98e82f627c12c5920d810b4))
* **ignore:** update zigbee-herdsman-converters to 25.82.0 ([#29990](https://github.com/Koenkk/zigbee2mqtt/issues/29990)) ([740edd8](https://github.com/Koenkk/zigbee2mqtt/commit/740edd8b58b7d72b7a2a5247f7f6ac8ee99ff0fa))
* **ignore:** update zigbee-herdsman-converters to 25.83.0 ([#30011](https://github.com/Koenkk/zigbee2mqtt/issues/30011)) ([a7f1345](https://github.com/Koenkk/zigbee2mqtt/commit/a7f1345a9164f6c83cb81688c33511b14646fab8))
* **ignore:** update zigbee-herdsman-converters to 25.83.1 ([#30050](https://github.com/Koenkk/zigbee2mqtt/issues/30050)) ([9b60353](https://github.com/Koenkk/zigbee2mqtt/commit/9b6035387be6f285fab455f7845baa75f3f722be))
* **ignore:** update zigbee-herdsman-converters to 25.84.0 ([#30081](https://github.com/Koenkk/zigbee2mqtt/issues/30081)) ([c18d132](https://github.com/Koenkk/zigbee2mqtt/commit/c18d1329c801ce48cf5277c00682ad2f9098f383))
* **ignore:** update zigbee-herdsman-converters to 25.85.0 ([#30120](https://github.com/Koenkk/zigbee2mqtt/issues/30120)) ([5db6ae0](https://github.com/Koenkk/zigbee2mqtt/commit/5db6ae0a197a180abc5e497aebe6e59fcb9b6b3c))
* **ignore:** update zigbee-herdsman-converters to 25.86.0 ([#30143](https://github.com/Koenkk/zigbee2mqtt/issues/30143)) ([3460f15](https://github.com/Koenkk/zigbee2mqtt/commit/3460f157df84d10f8de87a37d6b0b0b243de6874))
* **ignore:** update zigbee-herdsman-converters to 25.88.0 ([#30196](https://github.com/Koenkk/zigbee2mqtt/issues/30196)) ([3818c50](https://github.com/Koenkk/zigbee2mqtt/commit/3818c50b8ae516e56ad07e8fce3c451f35e1b678))
* **ignore:** update zigbee-herdsman-converters to 25.89.0 ([#30215](https://github.com/Koenkk/zigbee2mqtt/issues/30215)) ([01304cf](https://github.com/Koenkk/zigbee2mqtt/commit/01304cfc6604b86acf181dc3f07688f56ecbe8a1))
* **ignore:** update zigbee-herdsman-converters to 25.90.0 ([#30267](https://github.com/Koenkk/zigbee2mqtt/issues/30267)) ([ab08947](https://github.com/Koenkk/zigbee2mqtt/commit/ab0894715a002bd5c14d8af45a67e5e546889590))
* **ignore:** update zigbee-herdsman-converters to 25.91.0 ([#30279](https://github.com/Koenkk/zigbee2mqtt/issues/30279)) ([4c02e3d](https://github.com/Koenkk/zigbee2mqtt/commit/4c02e3da8f66cafbdcde89303bb1aea6eaf73bcd))
* **ignore:** update zigbee-herdsman-converters to 25.92.0 ([#30300](https://github.com/Koenkk/zigbee2mqtt/issues/30300)) ([c28a024](https://github.com/Koenkk/zigbee2mqtt/commit/c28a024664989fe8a044babe3cdf43403b0ee2b5))
* **ignore:** update zigbee-herdsman-converters to 25.93.0 ([#30331](https://github.com/Koenkk/zigbee2mqtt/issues/30331)) ([c61f67f](https://github.com/Koenkk/zigbee2mqtt/commit/c61f67f061d2766e99c5cb9c99c02b172832271c))
* **ignore:** update zigbee-herdsman-converters to 25.94.0 ([#30355](https://github.com/Koenkk/zigbee2mqtt/issues/30355)) ([7cb5c58](https://github.com/Koenkk/zigbee2mqtt/commit/7cb5c58dc028f86c59935b64a738444950ef8e12))
* **ignore:** update zigbee-herdsman-converters to 25.95.0 ([#30371](https://github.com/Koenkk/zigbee2mqtt/issues/30371)) ([4c48e9c](https://github.com/Koenkk/zigbee2mqtt/commit/4c48e9cfbcedb9cd6aedee9f694db2993038eaee))
* **ignore:** update zigbee-herdsman-converters to 25.96.0 ([#30383](https://github.com/Koenkk/zigbee2mqtt/issues/30383)) ([2721b0e](https://github.com/Koenkk/zigbee2mqtt/commit/2721b0ea78f4e86e6fe3b5f4831c5787a28585a8))
* **ignore:** update zigbee-herdsman-converters to 25.97.0 ([#30396](https://github.com/Koenkk/zigbee2mqtt/issues/30396)) ([3727f07](https://github.com/Koenkk/zigbee2mqtt/commit/3727f078ecde73a5c444c42909f36b4a0f35d8c3))
* **ignore:** update zigbee-herdsman-converters to 25.98.0 ([#30417](https://github.com/Koenkk/zigbee2mqtt/issues/30417)) ([0eba0c9](https://github.com/Koenkk/zigbee2mqtt/commit/0eba0c953d7543b2850a9b9672ffbc3d87840fb3))
* **ignore:** update zigbee-herdsman-converters to 25.99.0 ([#30481](https://github.com/Koenkk/zigbee2mqtt/issues/30481)) ([3adf7d5](https://github.com/Koenkk/zigbee2mqtt/commit/3adf7d5d8ec6da3fabbd9f293d5d0bcaef3c9144))
* **ignore:** update zigbee-herdsman-converters to 26.0.0 ([6cce05a](https://github.com/Koenkk/zigbee2mqtt/commit/6cce05a5141089e24211d85d1d77d09f8d2b36f0))
* **ignore:** update zigbee-herdsman-converters to 26.1.0 ([#30981](https://github.com/Koenkk/zigbee2mqtt/issues/30981)) ([f1156db](https://github.com/Koenkk/zigbee2mqtt/commit/f1156db227bf937354d6ab4e70d6da59307dedad))
* **ignore:** update zigbee-herdsman-converters to 26.10.1 ([#31175](https://github.com/Koenkk/zigbee2mqtt/issues/31175)) ([b9d9e5e](https://github.com/Koenkk/zigbee2mqtt/commit/b9d9e5e75c3f175ec0fc69f6315c5ca75a8e8128))
* **ignore:** update zigbee-herdsman-converters to 26.11.0 ([#31185](https://github.com/Koenkk/zigbee2mqtt/issues/31185)) ([c880d36](https://github.com/Koenkk/zigbee2mqtt/commit/c880d36f29a8f146170cae34bef15b86d01f5889))
* **ignore:** update zigbee-herdsman-converters to 26.12.0 ([#31208](https://github.com/Koenkk/zigbee2mqtt/issues/31208)) ([a9a6ebe](https://github.com/Koenkk/zigbee2mqtt/commit/a9a6ebe00743b47bd00667ae64d0f0fb25f5e38d))
* **ignore:** update zigbee-herdsman-converters to 26.13.0 ([#31231](https://github.com/Koenkk/zigbee2mqtt/issues/31231)) ([fc3a196](https://github.com/Koenkk/zigbee2mqtt/commit/fc3a1968f6635d246ad2d68e5c6b79d495fd96bb))
* **ignore:** update zigbee-herdsman-converters to 26.14.0 ([#31246](https://github.com/Koenkk/zigbee2mqtt/issues/31246)) ([284d86e](https://github.com/Koenkk/zigbee2mqtt/commit/284d86e93ba8cd83139d6dd0217519f67c01e565))
* **ignore:** update zigbee-herdsman-converters to 26.16.1 ([#31268](https://github.com/Koenkk/zigbee2mqtt/issues/31268)) ([05946b5](https://github.com/Koenkk/zigbee2mqtt/commit/05946b59552608c6ae8a8b02098552fe611f027a))
* **ignore:** update zigbee-herdsman-converters to 26.17.0 ([#31314](https://github.com/Koenkk/zigbee2mqtt/issues/31314)) ([9143924](https://github.com/Koenkk/zigbee2mqtt/commit/91439244823d9897998a57448321d2e8af9b083f))
* **ignore:** update zigbee-herdsman-converters to 26.18.0 ([#31362](https://github.com/Koenkk/zigbee2mqtt/issues/31362)) ([f60db2d](https://github.com/Koenkk/zigbee2mqtt/commit/f60db2d7a641ab5fccbb3a7b6a0e1e73983559ef))
* **ignore:** update zigbee-herdsman-converters to 26.19.1 ([#31384](https://github.com/Koenkk/zigbee2mqtt/issues/31384)) ([69641d5](https://github.com/Koenkk/zigbee2mqtt/commit/69641d5467ec5e3ddfebb4ff98142d0ff990c814))
* **ignore:** update zigbee-herdsman-converters to 26.2.0 ([#30998](https://github.com/Koenkk/zigbee2mqtt/issues/30998)) ([b585e31](https://github.com/Koenkk/zigbee2mqtt/commit/b585e3130431c7c27b18b9779ed038b9e6dd30c6))
* **ignore:** update zigbee-herdsman-converters to 26.2.1 ([#31015](https://github.com/Koenkk/zigbee2mqtt/issues/31015)) ([4e360d0](https://github.com/Koenkk/zigbee2mqtt/commit/4e360d0bf2fed651f7c801bcce8455dc8cf4d649))
* **ignore:** update zigbee-herdsman-converters to 26.20.0 ([#31415](https://github.com/Koenkk/zigbee2mqtt/issues/31415)) ([9f49d0e](https://github.com/Koenkk/zigbee2mqtt/commit/9f49d0ec6738ebd1ea65a3bf1aae553548217f2e))
* **ignore:** update zigbee-herdsman-converters to 26.21.0 ([#31427](https://github.com/Koenkk/zigbee2mqtt/issues/31427)) ([e054d71](https://github.com/Koenkk/zigbee2mqtt/commit/e054d7124bd4dc5bdb18c9de4a6923b06df6b929))
* **ignore:** update zigbee-herdsman-converters to 26.22.0 ([#31451](https://github.com/Koenkk/zigbee2mqtt/issues/31451)) ([64286eb](https://github.com/Koenkk/zigbee2mqtt/commit/64286ebb3054bc15e020aab431c1d3af76e2c914))
* **ignore:** update zigbee-herdsman-converters to 26.23.0 ([#31462](https://github.com/Koenkk/zigbee2mqtt/issues/31462)) ([2e5e362](https://github.com/Koenkk/zigbee2mqtt/commit/2e5e3628188765cddbd4116f41703eab491670df))
* **ignore:** update zigbee-herdsman-converters to 26.24.0 ([#31479](https://github.com/Koenkk/zigbee2mqtt/issues/31479)) ([dde637a](https://github.com/Koenkk/zigbee2mqtt/commit/dde637af2f8449ae24684fa34a32bec106c24913))
* **ignore:** update zigbee-herdsman-converters to 26.25.0 ([#31497](https://github.com/Koenkk/zigbee2mqtt/issues/31497)) ([66985a8](https://github.com/Koenkk/zigbee2mqtt/commit/66985a8ce8d4cefbc4ddacc754e182e0b5953cea))
* **ignore:** update zigbee-herdsman-converters to 26.26.0 ([#31510](https://github.com/Koenkk/zigbee2mqtt/issues/31510)) ([969db9a](https://github.com/Koenkk/zigbee2mqtt/commit/969db9ae85838b955e07a80f5f72f655f6beffcf))
* **ignore:** update zigbee-herdsman-converters to 26.27.0 ([#31563](https://github.com/Koenkk/zigbee2mqtt/issues/31563)) ([62ab814](https://github.com/Koenkk/zigbee2mqtt/commit/62ab814d73b7de957fb8b28679b9dea8ae087133))
* **ignore:** update zigbee-herdsman-converters to 26.28.0 ([#31574](https://github.com/Koenkk/zigbee2mqtt/issues/31574)) ([08e4e9e](https://github.com/Koenkk/zigbee2mqtt/commit/08e4e9edeb048a07f89d6283e5bea04274a3fb47))
* **ignore:** update zigbee-herdsman-converters to 26.29.0 ([#31586](https://github.com/Koenkk/zigbee2mqtt/issues/31586)) ([23f9847](https://github.com/Koenkk/zigbee2mqtt/commit/23f9847994da94fa8b38ba823a8e55418b006e7c))
* **ignore:** update zigbee-herdsman-converters to 26.3.0 ([#31028](https://github.com/Koenkk/zigbee2mqtt/issues/31028)) ([c599e19](https://github.com/Koenkk/zigbee2mqtt/commit/c599e19bf2252b90554ec3a68abd64487acd8882))
* **ignore:** update zigbee-herdsman-converters to 26.30.0 ([#31608](https://github.com/Koenkk/zigbee2mqtt/issues/31608)) ([6b567c1](https://github.com/Koenkk/zigbee2mqtt/commit/6b567c1286201aa9fef4744f7378798bc852c9ff))
* **ignore:** update zigbee-herdsman-converters to 26.31.0 ([#31637](https://github.com/Koenkk/zigbee2mqtt/issues/31637)) ([a50065c](https://github.com/Koenkk/zigbee2mqtt/commit/a50065cbb40bb149a87046bb83a7602a5587b3b0))
* **ignore:** update zigbee-herdsman-converters to 26.32.0 ([#31653](https://github.com/Koenkk/zigbee2mqtt/issues/31653)) ([4818cc4](https://github.com/Koenkk/zigbee2mqtt/commit/4818cc4163d578990339daf6c5e725bb40a08216))
* **ignore:** update zigbee-herdsman-converters to 26.33.1 ([#31674](https://github.com/Koenkk/zigbee2mqtt/issues/31674)) ([0c9ec72](https://github.com/Koenkk/zigbee2mqtt/commit/0c9ec722db3f53c57870de7f09de9971d69cbda8))
* **ignore:** update zigbee-herdsman-converters to 26.34.0 ([#31702](https://github.com/Koenkk/zigbee2mqtt/issues/31702)) ([55d8d1f](https://github.com/Koenkk/zigbee2mqtt/commit/55d8d1f4d0a5895ce146dbec98630d82adcd4e22))
* **ignore:** update zigbee-herdsman-converters to 26.35.0 ([#31713](https://github.com/Koenkk/zigbee2mqtt/issues/31713)) ([0a7dfce](https://github.com/Koenkk/zigbee2mqtt/commit/0a7dfcef596c3201f9f0cd81960c0e4ad8add0c4))
* **ignore:** update zigbee-herdsman-converters to 26.36.0 ([#31737](https://github.com/Koenkk/zigbee2mqtt/issues/31737)) ([a54775d](https://github.com/Koenkk/zigbee2mqtt/commit/a54775dea10a14b6d2f25ebb3c79a8a9d979e970))
* **ignore:** update zigbee-herdsman-converters to 26.37.0 ([#31749](https://github.com/Koenkk/zigbee2mqtt/issues/31749)) ([78f440b](https://github.com/Koenkk/zigbee2mqtt/commit/78f440bada9a14abb1d84af12fc77b1af7d250ea))
* **ignore:** update zigbee-herdsman-converters to 26.38.0 ([#31768](https://github.com/Koenkk/zigbee2mqtt/issues/31768)) ([305b7bc](https://github.com/Koenkk/zigbee2mqtt/commit/305b7bcafab59250173eddb9c08cb2fed0d966a0))
* **ignore:** update zigbee-herdsman-converters to 26.38.1 ([#31777](https://github.com/Koenkk/zigbee2mqtt/issues/31777)) ([3bb3d56](https://github.com/Koenkk/zigbee2mqtt/commit/3bb3d56c05b92e349d2b117973df6cf9414b8450))
* **ignore:** update zigbee-herdsman-converters to 26.39.1 ([#31783](https://github.com/Koenkk/zigbee2mqtt/issues/31783)) ([e715078](https://github.com/Koenkk/zigbee2mqtt/commit/e71507835b2c0673ec0df488e9d1db39a7b64a05))
* **ignore:** update zigbee-herdsman-converters to 26.4.0 ([#31036](https://github.com/Koenkk/zigbee2mqtt/issues/31036)) ([142754b](https://github.com/Koenkk/zigbee2mqtt/commit/142754b71cf99ac97d5d25d6a124c90d17bf321d))
* **ignore:** update zigbee-herdsman-converters to 26.40.0 ([#31800](https://github.com/Koenkk/zigbee2mqtt/issues/31800)) ([429c5ae](https://github.com/Koenkk/zigbee2mqtt/commit/429c5aea585da1525347adc13712bc556b4ea2ae))
* **ignore:** update zigbee-herdsman-converters to 26.41.0 ([#31820](https://github.com/Koenkk/zigbee2mqtt/issues/31820)) ([c581623](https://github.com/Koenkk/zigbee2mqtt/commit/c58162344fddc1cdb49027f6dcc9f1f35030bc61))
* **ignore:** update zigbee-herdsman-converters to 26.42.0 ([#31830](https://github.com/Koenkk/zigbee2mqtt/issues/31830)) ([df26459](https://github.com/Koenkk/zigbee2mqtt/commit/df26459b46e67f4f3b1079eb6fa6a439bf1979af))
* **ignore:** update zigbee-herdsman-converters to 26.43.0 ([#31878](https://github.com/Koenkk/zigbee2mqtt/issues/31878)) ([19eb05a](https://github.com/Koenkk/zigbee2mqtt/commit/19eb05a0e023c7e2e8639c1dcda18399e96ae74d))
* **ignore:** update zigbee-herdsman-converters to 26.44.0 ([#31900](https://github.com/Koenkk/zigbee2mqtt/issues/31900)) ([845bdb7](https://github.com/Koenkk/zigbee2mqtt/commit/845bdb7dba5fe0f5e67e1b92c68872da0f3abf6a))
* **ignore:** update zigbee-herdsman-converters to 26.45.0 ([#31925](https://github.com/Koenkk/zigbee2mqtt/issues/31925)) ([8727abd](https://github.com/Koenkk/zigbee2mqtt/commit/8727abdefc9114a5e2e1036d764271c90065e0a1))
* **ignore:** update zigbee-herdsman-converters to 26.46.0 ([#31937](https://github.com/Koenkk/zigbee2mqtt/issues/31937)) ([cc84566](https://github.com/Koenkk/zigbee2mqtt/commit/cc8456617a4a389e7297e380901bfd6bb0223b32))
* **ignore:** update zigbee-herdsman-converters to 26.47.0 ([#31946](https://github.com/Koenkk/zigbee2mqtt/issues/31946)) ([7a05464](https://github.com/Koenkk/zigbee2mqtt/commit/7a05464f0355380ee855415bc270632263e3f5d3))
* **ignore:** update zigbee-herdsman-converters to 26.48.0 ([#31959](https://github.com/Koenkk/zigbee2mqtt/issues/31959)) ([502030c](https://github.com/Koenkk/zigbee2mqtt/commit/502030c0168c8ba6eaf7495a9014489f2f7a7c50))
* **ignore:** update zigbee-herdsman-converters to 26.49.0 ([#31976](https://github.com/Koenkk/zigbee2mqtt/issues/31976)) ([32499d1](https://github.com/Koenkk/zigbee2mqtt/commit/32499d14d5f0c9eada160d596d6be92da6c4af95))
* **ignore:** update zigbee-herdsman-converters to 26.5.0 ([#31058](https://github.com/Koenkk/zigbee2mqtt/issues/31058)) ([061262f](https://github.com/Koenkk/zigbee2mqtt/commit/061262ff97c5f30571f77df5b06362023c282fa6))
* **ignore:** update zigbee-herdsman-converters to 26.50.0 ([#31988](https://github.com/Koenkk/zigbee2mqtt/issues/31988)) ([6ea21c3](https://github.com/Koenkk/zigbee2mqtt/commit/6ea21c383b4801f28e30b022194e71375db94b17))
* **ignore:** update zigbee-herdsman-converters to 26.51.0 ([#31999](https://github.com/Koenkk/zigbee2mqtt/issues/31999)) ([744e4a6](https://github.com/Koenkk/zigbee2mqtt/commit/744e4a6d5c4dd179e7c3d1a2b4fbcdc820195547))
* **ignore:** update zigbee-herdsman-converters to 26.51.1 ([#32016](https://github.com/Koenkk/zigbee2mqtt/issues/32016)) ([5458e50](https://github.com/Koenkk/zigbee2mqtt/commit/5458e5084def411aaad9af08e415acb6bc16d181))
* **ignore:** update zigbee-herdsman-converters to 26.52.0 ([#32040](https://github.com/Koenkk/zigbee2mqtt/issues/32040)) ([1e114de](https://github.com/Koenkk/zigbee2mqtt/commit/1e114dee70b8079eda31d4d0cc52fa073a772798))
* **ignore:** update zigbee-herdsman-converters to 26.53.0 ([#32051](https://github.com/Koenkk/zigbee2mqtt/issues/32051)) ([52201c9](https://github.com/Koenkk/zigbee2mqtt/commit/52201c96bf897af9bcad30435575923a0452b6dd))
* **ignore:** update zigbee-herdsman-converters to 26.54.0 ([#32065](https://github.com/Koenkk/zigbee2mqtt/issues/32065)) ([2c5b536](https://github.com/Koenkk/zigbee2mqtt/commit/2c5b53613448ae4c6548f874a36acc1226d53ec3))
* **ignore:** update zigbee-herdsman-converters to 26.55.0 ([#32068](https://github.com/Koenkk/zigbee2mqtt/issues/32068)) ([7075b78](https://github.com/Koenkk/zigbee2mqtt/commit/7075b78a462a0d1f76cf91cc78601d90edfd8b4e))
* **ignore:** update zigbee-herdsman-converters to 26.56.0 ([#32087](https://github.com/Koenkk/zigbee2mqtt/issues/32087)) ([56aab4e](https://github.com/Koenkk/zigbee2mqtt/commit/56aab4e59c4ea7bdbd93a1ed6632171994d8ed56))
* **ignore:** update zigbee-herdsman-converters to 26.57.0 ([#32113](https://github.com/Koenkk/zigbee2mqtt/issues/32113)) ([7a1c2d5](https://github.com/Koenkk/zigbee2mqtt/commit/7a1c2d52f0b0699854f3c8c9375761cb0b22ac55))
* **ignore:** update zigbee-herdsman-converters to 26.58.0 ([#32126](https://github.com/Koenkk/zigbee2mqtt/issues/32126)) ([e2b4911](https://github.com/Koenkk/zigbee2mqtt/commit/e2b49113c7932c4cd8ef7b23a27e3cd5d4e7150d))
* **ignore:** update zigbee-herdsman-converters to 26.59.1 ([#32136](https://github.com/Koenkk/zigbee2mqtt/issues/32136)) ([a8364ad](https://github.com/Koenkk/zigbee2mqtt/commit/a8364ad67b0dcbde93b1d6445c269a821321e9b3))
* **ignore:** update zigbee-herdsman-converters to 26.6.0 ([#31076](https://github.com/Koenkk/zigbee2mqtt/issues/31076)) ([909d2b3](https://github.com/Koenkk/zigbee2mqtt/commit/909d2b32b7ee7fe9144351b82d174e31fd0bad40))
* **ignore:** update zigbee-herdsman-converters to 26.60.0 ([#32150](https://github.com/Koenkk/zigbee2mqtt/issues/32150)) ([c6e13cc](https://github.com/Koenkk/zigbee2mqtt/commit/c6e13ccb45666f9604e8c9c9655bbe7ea682712d))
* **ignore:** update zigbee-herdsman-converters to 26.61.1 ([#32156](https://github.com/Koenkk/zigbee2mqtt/issues/32156)) ([58feb78](https://github.com/Koenkk/zigbee2mqtt/commit/58feb783626fd2429bea0f8a0db37bdc107c431b))
* **ignore:** update zigbee-herdsman-converters to 26.61.2 ([#32180](https://github.com/Koenkk/zigbee2mqtt/issues/32180)) ([babf7a1](https://github.com/Koenkk/zigbee2mqtt/commit/babf7a17d4617217297eef45f6b8307265f9b921))
* **ignore:** update zigbee-herdsman-converters to 26.62.0 ([#32194](https://github.com/Koenkk/zigbee2mqtt/issues/32194)) ([120f6ac](https://github.com/Koenkk/zigbee2mqtt/commit/120f6ac779c9fda6914fc68c56674e589c0e82ba))
* **ignore:** update zigbee-herdsman-converters to 26.63.0 ([#32252](https://github.com/Koenkk/zigbee2mqtt/issues/32252)) ([68daf4f](https://github.com/Koenkk/zigbee2mqtt/commit/68daf4f1deafad3ee6e542acbd77d403340e2caa))
* **ignore:** update zigbee-herdsman-converters to 26.64.0 ([#32282](https://github.com/Koenkk/zigbee2mqtt/issues/32282)) ([a68419f](https://github.com/Koenkk/zigbee2mqtt/commit/a68419f0f58324c1809055938cc59bdd07442aa9))
* **ignore:** update zigbee-herdsman-converters to 26.65.0 ([#32298](https://github.com/Koenkk/zigbee2mqtt/issues/32298)) ([f4f97d1](https://github.com/Koenkk/zigbee2mqtt/commit/f4f97d19208ab951256b69fd7673bfaba667af65))
* **ignore:** update zigbee-herdsman-converters to 26.66.0 ([#32315](https://github.com/Koenkk/zigbee2mqtt/issues/32315)) ([299b9e6](https://github.com/Koenkk/zigbee2mqtt/commit/299b9e64f52c99d1d3034321e82fc40a49bec604))
* **ignore:** update zigbee-herdsman-converters to 26.67.0 ([#32334](https://github.com/Koenkk/zigbee2mqtt/issues/32334)) ([95c67c0](https://github.com/Koenkk/zigbee2mqtt/commit/95c67c0e2271e24a236dabe717772c16f52a3017))
* **ignore:** update zigbee-herdsman-converters to 26.68.0 ([#32340](https://github.com/Koenkk/zigbee2mqtt/issues/32340)) ([1de8609](https://github.com/Koenkk/zigbee2mqtt/commit/1de860945645e748d6173a0368c4ccb44e9f5f13))
* **ignore:** update zigbee-herdsman-converters to 26.69.0 ([#32350](https://github.com/Koenkk/zigbee2mqtt/issues/32350)) ([0377f96](https://github.com/Koenkk/zigbee2mqtt/commit/0377f965a3a3c21e42afb885f8b3685d6de2fdd1))
* **ignore:** update zigbee-herdsman-converters to 26.7.0 ([#31090](https://github.com/Koenkk/zigbee2mqtt/issues/31090)) ([9d566f4](https://github.com/Koenkk/zigbee2mqtt/commit/9d566f4c015e7a31adaeef3b04bcc3c2312d583f))
* **ignore:** update zigbee-herdsman-converters to 26.70.0 ([#32357](https://github.com/Koenkk/zigbee2mqtt/issues/32357)) ([c6e7d2d](https://github.com/Koenkk/zigbee2mqtt/commit/c6e7d2dd16b1a7369b8f784caf3ceed6296a9da2))
* **ignore:** update zigbee-herdsman-converters to 26.71.0 ([#32375](https://github.com/Koenkk/zigbee2mqtt/issues/32375)) ([8768952](https://github.com/Koenkk/zigbee2mqtt/commit/87689523ae685258fa20ee8c03b7a09e8df0f4ba))
* **ignore:** update zigbee-herdsman-converters to 26.72.0 ([#32387](https://github.com/Koenkk/zigbee2mqtt/issues/32387)) ([b58503b](https://github.com/Koenkk/zigbee2mqtt/commit/b58503bb57054e24855bcde353b43be6a61273c6))
* **ignore:** update zigbee-herdsman-converters to 26.73.0 ([#32407](https://github.com/Koenkk/zigbee2mqtt/issues/32407)) ([8d5fdad](https://github.com/Koenkk/zigbee2mqtt/commit/8d5fdada38ce32f8e9ee86b3e39b2e00e2e76529))
* **ignore:** update zigbee-herdsman-converters to 26.74.0 ([#32420](https://github.com/Koenkk/zigbee2mqtt/issues/32420)) ([31fb3a6](https://github.com/Koenkk/zigbee2mqtt/commit/31fb3a6e895170993e893e9ea0d63c6600859259))
* **ignore:** update zigbee-herdsman-converters to 26.75.0 ([#32431](https://github.com/Koenkk/zigbee2mqtt/issues/32431)) ([3760e63](https://github.com/Koenkk/zigbee2mqtt/commit/3760e631835c8e6e11cea5241bb2c8cf3d374722))
* **ignore:** update zigbee-herdsman-converters to 26.76.0 ([#32441](https://github.com/Koenkk/zigbee2mqtt/issues/32441)) ([740de2d](https://github.com/Koenkk/zigbee2mqtt/commit/740de2d363109707a90cdfa8c019361f4423a1aa))
* **ignore:** update zigbee-herdsman-converters to 26.77.0 ([#32462](https://github.com/Koenkk/zigbee2mqtt/issues/32462)) ([4b0c306](https://github.com/Koenkk/zigbee2mqtt/commit/4b0c3067ffeef42d66417445480048a8e546bc68))
* **ignore:** update zigbee-herdsman-converters to 26.78.0 ([#32489](https://github.com/Koenkk/zigbee2mqtt/issues/32489)) ([f88d992](https://github.com/Koenkk/zigbee2mqtt/commit/f88d99294b8dc42f8657673e80b7ed721220db40))
* **ignore:** update zigbee-herdsman-converters to 26.79.0 ([#32499](https://github.com/Koenkk/zigbee2mqtt/issues/32499)) ([a2973f2](https://github.com/Koenkk/zigbee2mqtt/commit/a2973f21f6cfebc175b6b1acfcb73ca7cab95c5f))
* **ignore:** update zigbee-herdsman-converters to 26.8.0 ([#31118](https://github.com/Koenkk/zigbee2mqtt/issues/31118)) ([396eb38](https://github.com/Koenkk/zigbee2mqtt/commit/396eb38e4b9a329a651dec46ec816be537a3c997))
* **ignore:** update zigbee-herdsman-converters to 26.80.0 ([#32522](https://github.com/Koenkk/zigbee2mqtt/issues/32522)) ([912fe4c](https://github.com/Koenkk/zigbee2mqtt/commit/912fe4c250f514f6a0e3c97cf1438b15cef1ec6a))
* **ignore:** update zigbee-herdsman-converters to 26.81.0 ([#32543](https://github.com/Koenkk/zigbee2mqtt/issues/32543)) ([e475de1](https://github.com/Koenkk/zigbee2mqtt/commit/e475de15c901c493dc93f4194fe55b9359712ce3))
* **ignore:** update zigbee-herdsman-converters to 26.82.0 ([#32572](https://github.com/Koenkk/zigbee2mqtt/issues/32572)) ([576e195](https://github.com/Koenkk/zigbee2mqtt/commit/576e1954efcbe39d8209284e37e4df223a3f37a8))
* **ignore:** update zigbee-herdsman-converters to 26.83.0 ([#32581](https://github.com/Koenkk/zigbee2mqtt/issues/32581)) ([22d85a6](https://github.com/Koenkk/zigbee2mqtt/commit/22d85a6729f5bbba72308c58123b0b6345f6095a))
* **ignore:** update zigbee-herdsman-converters to 26.84.0 ([#32598](https://github.com/Koenkk/zigbee2mqtt/issues/32598)) ([e78b23f](https://github.com/Koenkk/zigbee2mqtt/commit/e78b23f5ebeb388402cae31db982759385e74fc5))
* **ignore:** update zigbee-herdsman-converters to 26.85.0 ([#32612](https://github.com/Koenkk/zigbee2mqtt/issues/32612)) ([752ab37](https://github.com/Koenkk/zigbee2mqtt/commit/752ab37302cffaa2cad664e5f43c61451e099ee6))
* **ignore:** update zigbee-herdsman-converters to 26.86.0 ([756a824](https://github.com/Koenkk/zigbee2mqtt/commit/756a824488377f973b710d6293b8deadc310ce42))
* **ignore:** update zigbee-herdsman-converters to 26.87.0 ([#32660](https://github.com/Koenkk/zigbee2mqtt/issues/32660)) ([e6e0b6f](https://github.com/Koenkk/zigbee2mqtt/commit/e6e0b6f8f2b498b771625019b52d42ccbd4aa967))
* **ignore:** update zigbee-herdsman-converters to 26.88.0 ([#32674](https://github.com/Koenkk/zigbee2mqtt/issues/32674)) ([3bec9e8](https://github.com/Koenkk/zigbee2mqtt/commit/3bec9e87b17985b59e8a50d3c27677056838d26b))
* **ignore:** update zigbee-herdsman-converters to 26.89.0 ([#32700](https://github.com/Koenkk/zigbee2mqtt/issues/32700)) ([b29fc0e](https://github.com/Koenkk/zigbee2mqtt/commit/b29fc0e49352f583d418a36bc6708d5745d1a788))
* **ignore:** update zigbee-herdsman-converters to 26.9.0 ([#31137](https://github.com/Koenkk/zigbee2mqtt/issues/31137)) ([4d287ee](https://github.com/Koenkk/zigbee2mqtt/commit/4d287eed0c2b746e1cfbd18ad61ed7b5129a98d9))
* **ignore:** update zigbee-herdsman-converters to 26.90.0 ([#32715](https://github.com/Koenkk/zigbee2mqtt/issues/32715)) ([e5e36ac](https://github.com/Koenkk/zigbee2mqtt/commit/e5e36ac79c2e1b89ac0fb81f95bcd655455531c1))
* **ignore:** update zigbee2mqtt-windfront to 2.3.1 ([#29788](https://github.com/Koenkk/zigbee2mqtt/issues/29788)) ([17520f7](https://github.com/Koenkk/zigbee2mqtt/commit/17520f7cfb69b577ebd986b86da80931a15a39db))
* Improve configure attempts lazy+cleanup ([#31126](https://github.com/Koenkk/zigbee2mqtt/issues/31126)) ([0c43c98](https://github.com/Koenkk/zigbee2mqtt/commit/0c43c983063c1aa01b85318c85bf7101f0085430))
* Improve startup signals behavior ([#31130](https://github.com/Koenkk/zigbee2mqtt/issues/31130)) ([f8385b8](https://github.com/Koenkk/zigbee2mqtt/commit/f8385b88993cfcf3a27c315999c86b961fd8fbfa))
* Improve transmit power description ([#31735](https://github.com/Koenkk/zigbee2mqtt/issues/31735)) ([5974286](https://github.com/Koenkk/zigbee2mqtt/commit/59742864457a9e29783363ebde8ede0e0aca5ea2))
* improve zigbee2mqtt maintenance path ([#32255](https://github.com/Koenkk/zigbee2mqtt/issues/32255)) ([ced6d93](https://github.com/Koenkk/zigbee2mqtt/commit/ced6d93d20502f115d4d0b17b5d292fee7ad30e4))
* log dir tz format not working on some systems ([#30324](https://github.com/Koenkk/zigbee2mqtt/issues/30324)) ([58d98c7](https://github.com/Koenkk/zigbee2mqtt/commit/58d98c7263dbe687eb677199eb760799ecff8e8f))
* Log error before renaming failed to load converters ([#30436](https://github.com/Koenkk/zigbee2mqtt/issues/30436)) ([1eaa2f3](https://github.com/Koenkk/zigbee2mqtt/commit/1eaa2f3e759351bc746dec2ccc57b92cdcf1b5d7))
* Match new cluster-related typing from ZH ([#31298](https://github.com/Koenkk/zigbee2mqtt/issues/31298)) ([0f781ee](https://github.com/Koenkk/zigbee2mqtt/commit/0f781ee9c03ae502745841f4dd7d6f3775c620d4))
* Network map: escape double-quotes and backslashes in device attributes ([#30746](https://github.com/Koenkk/zigbee2mqtt/issues/30746)) ([5de47a0](https://github.com/Koenkk/zigbee2mqtt/commit/5de47a0d93f5fdc187e4f8a7489580a107a2fd49))
* OTA availability detection ([#30815](https://github.com/Koenkk/zigbee2mqtt/issues/30815)) ([476efaa](https://github.com/Koenkk/zigbee2mqtt/commit/476efaa6e18a1d2ea069ea9c128536356df547ac))
* Prevent invalid external JS file name on save ([#32037](https://github.com/Koenkk/zigbee2mqtt/issues/32037)) ([bbcbed1](https://github.com/Koenkk/zigbee2mqtt/commit/bbcbed12d53408eaa8ceec110c7071fff8e158e4))
* Proper timezone in logs dir ([#30297](https://github.com/Koenkk/zigbee2mqtt/issues/30297)) ([8549b37](https://github.com/Koenkk/zigbee2mqtt/commit/8549b372283da8b55804c4049c7341294ced1538))
* Publish groups on device leave ([#32676](https://github.com/Koenkk/zigbee2mqtt/issues/32676)) ([fe96204](https://github.com/Koenkk/zigbee2mqtt/commit/fe96204f0b0f2b858ec664d408eb21545dc791d2))
* Refresh exposes after manual device configure ([#32486](https://github.com/Koenkk/zigbee2mqtt/issues/32486)) ([344776b](https://github.com/Koenkk/zigbee2mqtt/commit/344776b63379ab91bf7b35e9c208cff0f88bb84b))
* Reintroduce onboarding improvements ([#31273](https://github.com/Koenkk/zigbee2mqtt/issues/31273)) ([4a83692](https://github.com/Koenkk/zigbee2mqtt/commit/4a8369295c5d6c07ff1ac66f90a35b0e8b915296))
* Remove extra `>` in onboarding([#30065](https://github.com/Koenkk/zigbee2mqtt/issues/30065)) ([ed1e7b3](https://github.com/Koenkk/zigbee2mqtt/commit/ed1e7b333f481a4b3a37b021f2058331b86ebd84))
* Remove json-stable-stringify-without-jsonify dep ([#32643](https://github.com/Koenkk/zigbee2mqtt/issues/32643)) ([4f18b4e](https://github.com/Koenkk/zigbee2mqtt/commit/4f18b4e38c276c515eb3b132e3328b88ff62bf95))
* Remove Moment.js dependency ([#28797](https://github.com/Koenkk/zigbee2mqtt/issues/28797)) ([74335fb](https://github.com/Koenkk/zigbee2mqtt/commit/74335fbb686edd63e5965c50b6cfc853abd07717))
* Rename `ZigBee` -&gt; `Zigbee` ([#29131](https://github.com/Koenkk/zigbee2mqtt/issues/29131)) ([eaaed1f](https://github.com/Koenkk/zigbee2mqtt/commit/eaaed1fec79401cb0b15b6e4bb4ddfba7bcf3d39))
* Replace deprecated `url.parse` ([#31845](https://github.com/Koenkk/zigbee2mqtt/issues/31845)) ([9f7ea9b](https://github.com/Koenkk/zigbee2mqtt/commit/9f7ea9b7c79db7a781b431fbe43568e15647f8e5))
* Replace jszip with fflate ([#32683](https://github.com/Koenkk/zigbee2mqtt/issues/32683)) ([316413b](https://github.com/Koenkk/zigbee2mqtt/commit/316413b31c760c4b34c5afb14222bca11236d9d0))
* replace rimraf with native fs.rmSync ([#32579](https://github.com/Koenkk/zigbee2mqtt/issues/32579)) ([a9ce4b2](https://github.com/Koenkk/zigbee2mqtt/commit/a9ce4b2522c2d0a5bfcae3497cd98d3889b6f440))
* Replace source-map-support with native Node source map support ([#32620](https://github.com/Koenkk/zigbee2mqtt/issues/32620)) ([2d94100](https://github.com/Koenkk/zigbee2mqtt/commit/2d941000a73e83fbee983e98121510a6710183fc))
* Reporting payload detection fixes ([#29854](https://github.com/Koenkk/zigbee2mqtt/issues/29854)) ([42fc6e0](https://github.com/Koenkk/zigbee2mqtt/commit/42fc6e03a7a3092502e3c3d1defbcb3ed6438230))
* Republish bridge/state online when HA comes online ([#32258](https://github.com/Koenkk/zigbee2mqtt/issues/32258)) ([a81e90b](https://github.com/Koenkk/zigbee2mqtt/commit/a81e90b9812acd795a11a78b6d0489c080dc8f5b))
* Require at least Node ^20.15.0 ([#29284](https://github.com/Koenkk/zigbee2mqtt/issues/29284)) ([8ac2f55](https://github.com/Koenkk/zigbee2mqtt/commit/8ac2f55d87c3bebbda31cf1e9f1806e98b78c114))
* Require at least Node ^22.2.0 ([#29285](https://github.com/Koenkk/zigbee2mqtt/issues/29285)) ([6c48498](https://github.com/Koenkk/zigbee2mqtt/commit/6c48498e0d1cbf49ab5ccc4d7539c932ea92a22e))
* Support Node 26, remove Node 20 support ([#32508](https://github.com/Koenkk/zigbee2mqtt/issues/32508)) ([5591207](https://github.com/Koenkk/zigbee2mqtt/commit/5591207deaea177c0019fe679f4e197e2efcb286))
* update zigbee2mqtt-windfront to 2.9.0 ([#31162](https://github.com/Koenkk/zigbee2mqtt/issues/31162)) ([93dc9a3](https://github.com/Koenkk/zigbee2mqtt/commit/93dc9a3664f23a4a13a55e54c16714d721e596d7))
* Use Jinja-safe property access in HA discovery templates ([#31930](https://github.com/Koenkk/zigbee2mqtt/issues/31930)) ([c4fd415](https://github.com/Koenkk/zigbee2mqtt/commit/c4fd415cbe7b01719679b1a96d2b6e07b0793125))
---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
+10 -2
View File
@@ -10,6 +10,8 @@ const zhcTillVersion = process.argv[3];
const zhTillVersion = process.argv[4];
const frontendTillVersion = process.argv[5];
const windfrontTillVersion = process.argv[6];
const githubToken = process.env.GH_TOKEN;
const githubHeaders = githubToken ? {Authorization: `Bearer ${githubToken}`} : {};
const changelogs = [
{
@@ -70,8 +72,12 @@ const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1);
for (const changelog of changelogs) {
if (changelog.project === "Nerivec/zigbee2mqtt-windfront") {
const releaseRsp = await fetch("https://api.github.com/repos/Nerivec/zigbee2mqtt-windfront/releases");
const releaseRsp = await fetch("https://api.github.com/repos/Nerivec/zigbee2mqtt-windfront/releases", {headers: githubHeaders});
const releases = await releaseRsp.json();
if (!releaseRsp.ok || !Array.isArray(releases)) {
const message = typeof releases.message === "string" ? `: ${releases.message}` : "";
throw new Error(`Failed to retrieve releases for ${changelog.project} (${releaseRsp.status})${message}`);
}
for (const release of releases) {
if (release.name === `v${windfrontTillVersion}`) {
break;
@@ -108,7 +114,9 @@ for (const changelog of changelogs) {
let user =
commitUserKey in commitUserLookup
? commitUserLookup[commitUserKey]
: execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`)
: execSync(
`curl -s ${githubToken ? `-H "Authorization: Bearer ${githubToken}" ` : ""}https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`,
)
.toString()
.trim();
if (user !== "null") commitUserLookup[commitUserKey] = user;
+33
View File
@@ -1061,6 +1061,17 @@ describe("Controller", () => {
);
});
it("Publish entity state attribute output with a null color", async () => {
await controller.start();
settings.set(["advanced", "output"], "attribute_and_json");
mockMQTTPublishAsync.mockClear();
const device = getZ2MDevice("bulb");
await controller.publishEntityState(device, {state: "ON", color: null});
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/color", "", {qos: 0, retain: true});
});
it("Publish entity state attribute_json output filtered", async () => {
await controller.start();
settings.set(["advanced", "output"], "attribute_and_json");
@@ -1089,6 +1100,28 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({state: "ON", brightness: 200}), {qos: 0, retain: true});
});
it("Publish entity state caches a duration reported by the device", async () => {
await controller.start();
mockMQTTPublishAsync.mockClear();
const device = getZ2MDevice("bulb");
await controller.publishEntityState(device, {state: "ON", duration: 30});
await flushPromises();
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON", duration: 30});
});
it("Publish entity state keeps an action_duration out of the cache", async () => {
await controller.start();
mockMQTTPublishAsync.mockClear();
const device = getZ2MDevice("bulb");
await controller.publishEntityState(device, {state: "ON", action_duration: 1500});
await flushPromises();
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON"});
});
it("Publish entity state attribute_json output filtered cache", async () => {
await controller.start();
settings.set(["advanced", "output"], "attribute_and_json");
+13
View File
@@ -633,6 +633,19 @@ describe("Extension: Availability", () => {
expect(devices.QBKG03LM.ping).toHaveBeenCalledTimes(4);
});
it("clamps the ping delay to the maximum supported timeout", async () => {
// `setTimeout` takes a 32-bit signed integer and coerces anything above it to `1`. A delay can exceed
// that either directly, through a long `timeout`, or gradually, once `backoff` has multiplied a normal
// one over successive failures. Unclamped, that turns an ever-longer wait into a tight ping loop.
settings.set(["devices", devices.bulb_color.ieeeAddr, "availability"], {timeout: 40000, max_jitter: 0}); // ~27.8 days
await resetExtension();
// unclamped, the delay collapses to 1ms, so pings would already be looping by now
await setTimeAndAdvanceTimers(utils.seconds(1));
expect(devices.bulb_color.ping).not.toHaveBeenCalled();
});
it("allows to disable backoff", async () => {
settings.set(["availability", "active", "max_jitter"], 0); // easier testing
settings.set(["availability", "active", "backoff"], false);
+99 -8
View File
@@ -784,6 +784,16 @@ describe("Extension: Bridge", () => {
property: "effect_color",
type: "text",
},
{
access: 2,
category: "config",
description: "Initiate device identification",
label: "Identify",
name: "identify",
property: "identify",
type: "enum",
values: ["identify"],
},
{
access: 1,
category: "diagnostic",
@@ -831,6 +841,17 @@ describe("Extension: Bridge", () => {
value_min: 0,
value_step: 0.1,
},
{
access: 2,
description:
"Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).",
label: "Identify timeout",
name: "identify_timeout",
property: "identify_timeout",
type: "numeric",
value_max: 30,
value_min: 1,
},
{
access: 2,
description: "State actions will also be published as 'action' when true (default false).",
@@ -1162,6 +1183,17 @@ describe("Extension: Bridge", () => {
property: "power_outage_count",
type: "numeric",
},
{
access: 2,
category: "config",
description:
"Initiate device identification. This device is asleep by default.You may need to wake it up first before sending the identify command.",
label: "Identify",
name: "identify",
property: "identify",
type: "enum",
values: ["identify"],
},
{
access: 1,
category: "diagnostic",
@@ -1196,6 +1228,17 @@ describe("Extension: Bridge", () => {
type: "numeric",
value_step: 0.1,
},
{
access: 2,
description:
"Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).",
label: "Identify timeout",
name: "identify_timeout",
property: "identify_timeout",
type: "numeric",
value_max: 30,
value_min: 1,
},
],
supports_ota: false,
vendor: "Aqara",
@@ -1962,7 +2005,7 @@ describe("Extension: Bridge", () => {
},
{
access: 2,
description: "Inverts the cover position, false: open=100,close=0, true: open=0,close=100 (default false).",
description: "Inverts the cover position and state, false: open=100,close=0, true: open=0,close=100 (default false).",
label: "Invert cover",
name: "invert_cover",
property: "invert_cover",
@@ -2988,7 +3031,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}),
stringify({data: {id: "bulb", block: false, force: false, keep_config: false, clear_cache: false}, status: "ok"}),
{},
);
expect(settings.get().blocklist).toStrictEqual([]);
@@ -3009,7 +3052,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false, clear_cache: false}, status: "ok"}),
stringify({data: {id: "bulb", block: false, force: false, keep_config: false, clear_cache: false}, status: "ok"}),
{},
);
});
@@ -3027,7 +3070,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: true, clear_cache: false}, status: "ok"}),
stringify({data: {id: "bulb", block: false, force: true, keep_config: false, clear_cache: false}, status: "ok"}),
{},
);
});
@@ -3044,12 +3087,30 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: true, force: true, clear_cache: false}, status: "ok"}),
stringify({data: {id: "bulb", block: true, force: true, keep_config: false, clear_cache: false}, status: "ok"}),
{},
);
expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]);
});
it("Should allow to keep configuration when removing device", async () => {
const device = devices.bulb;
const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/remove", stringify({id: "bulb", keep_config: true}));
await flushPromises();
expect(device.removeFromDatabase).not.toHaveBeenCalled();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(settings.getDevice("bulb")).toBeDefined();
expect(removeSpy).not.toHaveBeenCalled();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false, keep_config: true, clear_cache: false}, status: "ok"}),
{},
);
});
it("Should allow to clear cache when removing device", async () => {
const device = devices.bulb;
const removeSpy = vi.spyOn(controller.zigbee, "removeDeviceFromLookup");
@@ -3063,7 +3124,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false, clear_cache: true}, status: "ok"}),
stringify({data: {id: "bulb", block: false, force: false, keep_config: false, clear_cache: true}, status: "ok"}),
{},
);
});
@@ -3135,7 +3196,7 @@ describe("Extension: Bridge", () => {
stringify({
data: {},
status: "error",
error: "Failed to remove device 'bulb' (block: false, force: false, clear cache: false) (Error: device timeout)",
error: "Failed to remove device 'bulb' (block: false, force: false, keep config: false, clear cache: false) (Error: device timeout)",
}),
{},
);
@@ -3402,7 +3463,7 @@ describe("Extension: Bridge", () => {
" model: 'lumi.plug',\n" +
" vendor: '',\n" +
" description: 'Automatically generated definition',\n" +
' extend: [m.onOff({"powerOnBehavior":false})],\n' +
" extend: [m.onOff()],\n" +
"};\n",
},
status: "ok",
@@ -3585,6 +3646,36 @@ describe("Extension: Bridge", () => {
);
});
it("Should warn on unsupported device option", async () => {
mockMQTTPublishAsync.mockClear();
mockLogger.warning.mockClear();
const device = controller.zigbee.resolveEntity(devices.bulb.ieeeAddr);
assert(device && "definition" in device);
const definitionOptions = device.definition?.options;
device.definition!.options = undefined;
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/options", stringify({options: {unsupported: true}, id: "bulb"}));
await flushPromises();
device.definition!.options = definitionOptions;
expect(settings.getDevice("bulb")).toHaveProperty("unsupported");
expect(mockLogger.warning).toHaveBeenCalledWith("Device 'bulb' does not support option 'unsupported'");
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/options",
stringify({
data: {
from: {retain: true, description: "this is my bulb"},
to: {retain: true, description: "this is my bulb", unsupported: true},
id: "bulb",
restart_required: false,
},
status: "ok",
}),
{},
);
});
it("Should allow to add group by string", async () => {
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/add", "group_193");
+46 -52
View File
@@ -13,7 +13,12 @@ import ws from "ws";
import {Controller} from "../../lib/controller";
import * as settings from "../../lib/util/settings";
let mockHTTPOnRequest: (request: {url: string}, response: number) => void;
const mockRedirectResponse = {
writeHead: vi.fn<(statusCode: number, headers: Record<string, string>) => void>(),
end: vi.fn<() => void>(),
};
let mockHTTPOnRequest: (request: {url: string}, response: number | typeof mockRedirectResponse) => void;
const mockHTTPEvents: Record<string, EventHandler> = {};
const mockHTTP = {
listen: vi.fn(),
@@ -64,7 +69,7 @@ const frontendPath = "frontend-path";
const deviceIconsPath = path.join(data.mockDir, "device_icons");
let mockNodeStatic: {[s: string]: Mock} = {};
const mockFinalHandler = vi.fn();
const mockSendNotFound = vi.fn();
vi.mock("node:http", () => ({
createServer: vi.fn().mockImplementation((onRequest) => {
@@ -79,11 +84,12 @@ vi.mock("node:https", () => ({
Agent: vi.fn(),
}));
vi.mock("express-static-gzip", () => ({
default: vi.fn().mockImplementation((path: string) => {
vi.mock("../../lib/util/staticFileServer", () => ({
createStaticFileServer: vi.fn().mockImplementation((path: string) => {
mockNodeStatic[path] = vi.fn();
return mockNodeStatic[path];
}),
sendNotFound: vi.fn().mockImplementation((...args: unknown[]) => mockSendNotFound(...args)),
}));
vi.mock("zigbee2mqtt-windfront", () => ({
@@ -101,12 +107,6 @@ vi.mock("ws", () => ({
},
}));
vi.mock("finalhandler", () => ({
default: vi.fn().mockImplementation(() => {
return mockFinalHandler;
}),
}));
const mocksClear = [
mockHTTP.close,
mockHTTP.listen,
@@ -118,7 +118,9 @@ const mocksClear = [
mockWS.emit,
mockWSClient.send,
mockWSClient.terminate,
mockFinalHandler,
mockSendNotFound,
mockRedirectResponse.writeHead,
mockRedirectResponse.end,
mockMQTTPublishAsync,
mockLogger.error,
];
@@ -247,6 +249,7 @@ describe("Extension: Frontend", () => {
effect: null,
effect_color: null,
effect_speed: null,
identify: null,
power_on_behavior: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
@@ -273,6 +276,7 @@ describe("Extension: Frontend", () => {
effect: null,
effect_color: null,
effect_speed: null,
identify: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
},
@@ -302,6 +306,7 @@ describe("Extension: Frontend", () => {
effect: null,
effect_color: null,
effect_speed: null,
identify: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
},
@@ -340,11 +345,7 @@ describe("Extension: Frontend", () => {
mockHTTPOnRequest({url: "/file.txt"}, 2);
expect(mockNodeStatic[deviceIconsPath]).toHaveBeenCalledTimes(0);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledTimes(1);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith(
{originalUrl: "/file.txt", path: "/file.txt", url: "/file.txt"},
2,
expect.any(Function),
);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith({url: "/file.txt"}, 2);
});
it("Should serve device icons", async () => {
@@ -354,11 +355,7 @@ describe("Extension: Frontend", () => {
mockHTTPOnRequest({url: "/device_icons/my_device.png"}, 2);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledTimes(0);
expect(mockNodeStatic[deviceIconsPath]).toHaveBeenCalledTimes(1);
expect(mockNodeStatic[deviceIconsPath]).toHaveBeenCalledWith(
{originalUrl: "/device_icons/my_device.png", path: "/my_device.png", url: "/my_device.png"},
2,
expect.any(Function),
);
expect(mockNodeStatic[deviceIconsPath]).toHaveBeenCalledWith({url: "/my_device.png"}, 2);
});
it("Static server", async () => {
@@ -402,34 +399,33 @@ describe("Extension: Frontend", () => {
expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: "/z2m/api"});
mockHTTPOnRequest({url: "/z2m"}, 2);
// the base url without trailing slash points at a directory, redirect so relative asset paths resolve against it
mockHTTPOnRequest({url: "/z2m"}, mockRedirectResponse);
expect(mockNodeStatic[frontendPath]).not.toHaveBeenCalled();
expect(mockRedirectResponse.writeHead).toHaveBeenCalledWith(301, {Location: "/z2m/"});
expect(mockRedirectResponse.end).toHaveBeenCalledTimes(1);
expect(mockSendNotFound).not.toHaveBeenCalled();
mockHTTPOnRequest({url: "/z2m/"}, 2);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledTimes(1);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith({originalUrl: "/z2m", path: "/", url: "/"}, 2, expect.any(Function));
expect(mockFinalHandler).not.toHaveBeenCalledWith();
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith({url: "/"}, 2);
expect(mockSendNotFound).not.toHaveBeenCalledWith();
mockNodeStatic[frontendPath].mockReset();
expect(mockFinalHandler).not.toHaveBeenCalledWith();
expect(mockSendNotFound).not.toHaveBeenCalledWith();
mockHTTPOnRequest({url: "/z2m/file.txt"}, 2);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledTimes(1);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith(
{originalUrl: "/z2m/file.txt", path: "/file.txt", url: "/file.txt"},
2,
expect.any(Function),
);
expect(mockFinalHandler).not.toHaveBeenCalledWith();
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith({url: "/file.txt"}, 2);
expect(mockSendNotFound).not.toHaveBeenCalledWith();
mockNodeStatic[frontendPath].mockReset();
mockHTTPOnRequest({url: "/z/file.txt"}, 2);
expect(mockNodeStatic[frontendPath]).not.toHaveBeenCalled();
expect(mockFinalHandler).toHaveBeenCalled();
expect(mockSendNotFound).toHaveBeenCalled();
mockHTTPOnRequest({url: "/z2m/device_icons/my-device.png"}, 2);
expect(mockNodeStatic[deviceIconsPath]).toHaveBeenCalledTimes(1);
expect(mockNodeStatic[deviceIconsPath]).toHaveBeenCalledWith(
{originalUrl: "/z2m/device_icons/my-device.png", path: "/my-device.png", url: "/my-device.png"},
2,
expect.any(Function),
);
expect(mockNodeStatic[deviceIconsPath]).toHaveBeenCalledWith({url: "/my-device.png"}, 2);
});
it("Works with non-default complex base url", async () => {
@@ -440,30 +436,28 @@ describe("Extension: Frontend", () => {
expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: "/z2m-more++/c0mplex.url/api"});
mockHTTPOnRequest({url: "/z2m-more++/c0mplex.url"}, 2);
mockHTTPOnRequest({url: "/z2m-more++/c0mplex.url"}, mockRedirectResponse);
expect(mockNodeStatic[frontendPath]).not.toHaveBeenCalled();
expect(mockRedirectResponse.writeHead).toHaveBeenCalledWith(301, {Location: "/z2m-more++/c0mplex.url/"});
expect(mockRedirectResponse.end).toHaveBeenCalledTimes(1);
expect(mockSendNotFound).not.toHaveBeenCalled();
mockHTTPOnRequest({url: "/z2m-more++/c0mplex.url/"}, 2);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledTimes(1);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith(
{originalUrl: "/z2m-more++/c0mplex.url", path: "/", url: "/"},
2,
expect.any(Function),
);
expect(mockFinalHandler).not.toHaveBeenCalledWith();
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith({url: "/"}, 2);
expect(mockSendNotFound).not.toHaveBeenCalledWith();
mockNodeStatic[frontendPath].mockReset();
expect(mockFinalHandler).not.toHaveBeenCalledWith();
expect(mockSendNotFound).not.toHaveBeenCalledWith();
mockHTTPOnRequest({url: "/z2m-more++/c0mplex.url/file.txt"}, 2);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledTimes(1);
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith(
{originalUrl: "/z2m-more++/c0mplex.url/file.txt", path: "/file.txt", url: "/file.txt"},
2,
expect.any(Function),
);
expect(mockFinalHandler).not.toHaveBeenCalledWith();
expect(mockNodeStatic[frontendPath]).toHaveBeenCalledWith({url: "/file.txt"}, 2);
expect(mockSendNotFound).not.toHaveBeenCalledWith();
mockNodeStatic[frontendPath].mockReset();
mockHTTPOnRequest({url: "/z/file.txt"}, 2);
expect(mockNodeStatic[frontendPath]).not.toHaveBeenCalled();
expect(mockFinalHandler).toHaveBeenCalled();
expect(mockSendNotFound).toHaveBeenCalled();
});
it("prevents mismatching setting/extension state", async () => {
+42 -3
View File
@@ -281,6 +281,25 @@ describe("Extension: HomeAssistant", () => {
expect(configs.find((config) => config.object_id === "voltage")?.discovery_payload).not.toHaveProperty("type");
});
it("Should set discovery name to null when expose specifies homeassistant name null", () => {
const createDevice = (exposes: zhc.Expose[]): Device =>
({
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => exposes,
zh: {endpoints: []},
}) as Device;
const contactExpose = new zhc.Binary("contact", zhc.access.STATE, false, true).withHomeAssistant({name: null});
// @ts-expect-error private
const configs = extension.getConfigs(createDevice([contactExpose]));
expect(configs.find((config) => config.object_id === "contact")?.discovery_payload.name).toBeNull();
});
it("Should discover devices and groups", async () => {
settings.set(["homeassistant", "experimental_event_entities"], true);
settings.set(["groups", "9", "homeassistant"], {name: "HA Discovery Group", icon: "mdi:lightbulb-group"});
@@ -1650,12 +1669,15 @@ describe("Extension: HomeAssistant", () => {
position_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
set_position_template: '{ "position_left": {{ position }} }',
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/left/set",
state_closed: "CLOSE",
state_closing: "DOWN",
state_open: "OPEN",
state_opening: "UP",
state_stopped: "STOP",
state_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
unique_id: "0xa4c138018cf95021_cover_left_zigbee2mqtt",
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
value_template:
'{% if "moving" in value_json and value_json["moving"] == "UP" %}UP{% elif "moving" in value_json and value_json["moving"] == "DOWN" %}DOWN{% elif "state" in value_json %}{{ value_json["state"] }}{% else %}STOP{% endif %}',
};
const payload_right = {
availability: [
@@ -1681,14 +1703,25 @@ describe("Extension: HomeAssistant", () => {
position_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
set_position_template: '{ "position_right": {{ position }} }',
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/right/set",
state_closed: "CLOSE",
state_closing: "DOWN",
state_open: "OPEN",
state_opening: "UP",
state_stopped: "STOP",
state_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
unique_id: "0xa4c138018cf95021_cover_right_zigbee2mqtt",
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
value_template:
'{% if "moving" in value_json and value_json["moving"] == "UP" %}UP{% elif "moving" in value_json and value_json["moving"] == "DOWN" %}DOWN{% elif "state" in value_json %}{{ value_json["state"] }}{% else %}STOP{% endif %}',
};
const coverLeftCalls = mockMQTTPublishAsync.mock.calls.filter(
([topic]) => topic === "homeassistant/cover/0xa4c138018cf95021/cover_left/config",
);
for (const [, actualPayload] of coverLeftCalls) {
console.log(JSON.parse(actualPayload));
}
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xa4c138018cf95021/cover_left/config", stringify(payload_left), {
retain: true,
qos: 1,
@@ -1855,6 +1888,7 @@ describe("Extension: HomeAssistant", () => {
effect: null,
effect_color: null,
effect_speed: null,
identify: null,
linkquality: null,
state: null,
power_on_behavior: null,
@@ -1880,6 +1914,7 @@ describe("Extension: HomeAssistant", () => {
effect: null,
effect_color: null,
effect_speed: null,
identify: null,
linkquality: null,
state: null,
power_on_behavior: null,
@@ -1904,6 +1939,7 @@ describe("Extension: HomeAssistant", () => {
effect: null,
effect_color: null,
effect_speed: null,
identify: null,
state: "ON",
power_on_behavior: null,
update: {state: null, installed_version: -1, latest_version: -1},
@@ -2375,7 +2411,7 @@ describe("Extension: HomeAssistant", () => {
it("Should discover trigger when action is published", async () => {
const discovered = mockMQTTPublishAsync.mock.calls.filter((c) => c[0].includes("0x0017880104e45520")).map((c) => c[0]);
expect(discovered.length).toBe(5);
expect(discovered.length).toBe(6);
mockMQTTPublishAsync.mockClear();
@@ -2412,6 +2448,7 @@ describe("Extension: HomeAssistant", () => {
stringify({
action: "single",
battery: null,
identify: null,
linkquality: null,
voltage: null,
power_outage_count: null,
@@ -3327,6 +3364,7 @@ describe("Extension: HomeAssistant", () => {
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({
action: "single",
battery: null,
identify: null,
linkquality: null,
voltage: null,
power_outage_count: null,
@@ -3337,6 +3375,7 @@ describe("Extension: HomeAssistant", () => {
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({
action: "",
battery: null,
identify: null,
linkquality: null,
voltage: null,
power_outage_count: null,
+3 -3
View File
@@ -286,7 +286,7 @@ describe("Extension: NetworkMap", () => {
description: "Hue Go",
model: "7146060PH",
supports:
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, identify, linkquality",
vendor: "Philips",
},
failed: [],
@@ -616,7 +616,7 @@ describe("Extension: NetworkMap", () => {
description: "Hue Go",
model: "7146060PH",
supports:
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, identify, linkquality",
vendor: "Philips",
},
failed: [],
@@ -785,7 +785,7 @@ describe("Extension: NetworkMap", () => {
description: "Hue Go",
model: "7146060PH",
supports:
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, identify, linkquality",
vendor: "Philips",
},
failed: [],
+24
View File
@@ -190,6 +190,30 @@ describe("Extension: Receive", () => {
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
});
it("Should not bypass the debounce when a message produces no payload", async () => {
const device = devices.WSDCGQ11LM;
settings.set(["devices", device.ieeeAddr, "debounce"], 0.1);
settings.set(["advanced", "last_seen"], "ISO_8601");
// Attribute report without measuredValue: the lumi_temperature converter returns nothing.
const payload = {
data: {},
cluster: "msTemperatureMeasurement",
device,
endpoint: device.getEndpoint(1),
type: "attributeReport",
linkquality: 10,
};
await mockZHEvents.message(payload);
await flushPromises();
// The empty payload must not be published immediately (bypassing the debounce).
vi.advanceTimersByTime(50);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
vi.runOnlyPendingTimers();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
});
it("Should debounce and retain messages when set via device_options", async () => {
const device = devices.WSDCGQ11LM;
settings.set(["device_options", "debounce"], 0.1);
+1
View File
@@ -34,6 +34,7 @@ const CLUSTERS = {
lightingColorCtrl: Zcl.Clusters.lightingColorCtrl.ID,
closuresWindowCovering: Zcl.Clusters.closuresWindowCovering.ID,
hvacThermostat: Zcl.Clusters.hvacThermostat.ID,
hvacFanCtrl: Zcl.Clusters.hvacFanCtrl.ID,
msIlluminanceMeasurement: Zcl.Clusters.msIlluminanceMeasurement.ID,
msTemperatureMeasurement: Zcl.Clusters.msTemperatureMeasurement.ID,
msRelativeHumidity: Zcl.Clusters.msRelativeHumidity.ID,
+161
View File
@@ -0,0 +1,161 @@
import {describe, expect, it} from "vitest";
import {objectAssignDeep} from "../lib/util/objectAssignDeep";
/** Creates an object with a real own `__proto__`/`constructor`/`prototype` property, like a parsed YAML/JSON payload can. */
const parse = (json: string): Record<string, unknown> => JSON.parse(json);
describe("objectAssignDeep", () => {
it("Mutates and returns the target", () => {
const target = {a: 1};
const result = objectAssignDeep(target, {b: 2});
expect(result).toBe(target);
expect(result).toStrictEqual({a: 1, b: 2});
});
it("Applies sources in order, later ones win", () => {
expect(objectAssignDeep({}, {a: 1, b: 1}, {b: 2, c: 2})).toStrictEqual({a: 1, b: 2, c: 2});
});
it("Copies keys missing from the target", () => {
expect(objectAssignDeep({}, {nested: {deep: {value: 1}}})).toStrictEqual({nested: {deep: {value: 1}}});
});
it("Deep merges nested objects present in both", () => {
const target = {mqtt: {base_topic: "zigbee2mqtt", server: "old"}, advanced: {channel: 11}};
const result = objectAssignDeep(target, {mqtt: {server: "new"}});
expect(result).toStrictEqual({mqtt: {base_topic: "zigbee2mqtt", server: "new"}, advanced: {channel: 11}});
});
it("Replaces nested objects of the target instead of mutating them", () => {
const nested = {a: 1};
const target = {nested};
objectAssignDeep(target, {nested: {b: 2}});
expect(nested).toStrictEqual({a: 1});
expect(target.nested).not.toBe(nested);
expect(target.nested).toStrictEqual({a: 1, b: 2});
});
it("Replaces an existing non-object value with a clone of the source object", () => {
expect(objectAssignDeep({a: 5}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
expect(objectAssignDeep({a: "str"}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
expect(objectAssignDeep({a: [1, 2]}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
// `null` is not `undefined`, so it takes the "existing value" path but is not merged into
expect(objectAssignDeep({a: null}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
});
it("Overwrites with null and undefined", () => {
expect(objectAssignDeep({a: {b: 1}, c: 1}, {a: null, c: null})).toStrictEqual({a: null, c: null});
expect(objectAssignDeep({a: {b: 1}, c: 1}, {a: undefined, c: undefined})).toStrictEqual({a: undefined, c: undefined});
});
it("Replaces arrays instead of concatenating them", () => {
expect(objectAssignDeep({a: [1, 2, 3]}, {a: [4]})).toStrictEqual({a: [4]});
expect(objectAssignDeep({a: [1, 2, 3]}, {a: []})).toStrictEqual({a: []});
// no existing array either
expect(objectAssignDeep({a: 1}, {a: [4]})).toStrictEqual({a: [4]});
expect(objectAssignDeep({}, {a: [4]})).toStrictEqual({a: [4]});
});
it("Clones arrays and the objects nested inside them", () => {
const source = {a: [{b: 1}, [{c: 2}]]};
const result = objectAssignDeep({}, source) as typeof source;
expect(result).toStrictEqual(source);
expect(result.a).not.toBe(source.a);
expect(result.a[0]).not.toBe(source.a[0]);
expect((result.a[1] as {c: number}[])[0]).not.toBe((source.a[1] as {c: number}[])[0]);
});
it("Breaks all references to the sources", () => {
const source = {a: {b: {c: 1}}};
const result = objectAssignDeep({}, source) as typeof source;
source.a.b.c = 99;
expect(result.a.b.c).toStrictEqual(1);
});
it("Does not mutate the sources", () => {
const source = {a: {b: 1}};
objectAssignDeep({a: {c: 2}}, source);
expect(source).toStrictEqual({a: {b: 1}});
});
it("Copies functions and primitives by value/reference", () => {
const fn = (): number => 1;
const symbol = Symbol("s");
const result = objectAssignDeep({}, {fn, symbol, big: 1n, nan: Number.NaN});
expect(result.fn).toBe(fn);
expect(result.symbol).toBe(symbol);
expect(result.big).toStrictEqual(1n);
expect(result.nan).toBeNaN();
});
it("Reduces non-plain objects to their own enumerable properties", () => {
// documented (inherited) behaviour: only own enumerable properties survive, the prototype is lost
expect(objectAssignDeep({}, {date: new Date(0)})).toStrictEqual({date: {}});
expect(objectAssignDeep({}, {regexp: /abc/g})).toStrictEqual({regexp: {}});
expect(objectAssignDeep({}, {map: new Map([["k", 1]])})).toStrictEqual({map: {}});
expect(objectAssignDeep({}, {set: new Set([1])})).toStrictEqual({set: {}});
class Device {
id = 1;
get computed(): number {
return 2;
}
}
const result = objectAssignDeep({}, {device: new Device()});
expect(result.device).toStrictEqual({id: 1});
expect(result.device).not.toBeInstanceOf(Device);
});
it("Merges deeply nested objects coming from multiple sources", () => {
const result = objectAssignDeep({}, {a: {b: {c: 1}}}, {a: {b: {d: 2}, e: 3}});
expect(result).toStrictEqual({a: {b: {c: 1, d: 2}, e: 3}});
});
it("Never copies keys that could tamper with the prototype chain", () => {
const result = objectAssignDeep({}, parse('{"__proto__": {"polluted": "yes"}, "constructor": {"x": 1}, "prototype": {"y": 2}, "safe": 1}'));
expect(result).toStrictEqual({safe: 1});
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
expect(({} as {polluted?: string}).polluted).toBeUndefined();
});
it("Never copies unsafe keys nested inside cloned objects", () => {
const result = objectAssignDeep({}, {nested: parse('{"__proto__": {"polluted": "yes"}, "constructor": 1, "prototype": 2, "safe": 1}')});
expect(result).toStrictEqual({nested: {safe: 1}});
expect(Object.getPrototypeOf(result.nested)).toBe(Object.prototype);
});
it("Never copies unsafe keys when merging into an existing object", () => {
const result = objectAssignDeep({nested: {safe: 1}}, {nested: parse('{"__proto__": {"polluted": "yes"}, "other": 2}')});
expect(result).toStrictEqual({nested: {safe: 1, other: 2}});
expect(Object.getPrototypeOf(result.nested)).toBe(Object.prototype);
});
it("Leaves every source untouched when given an empty target", () => {
const first = {a: {b: 1}};
const second = {a: {c: 2}};
const result = objectAssignDeep({}, first, second);
expect(result).toStrictEqual({a: {b: 1, c: 2}});
expect(result).not.toBe(first);
expect(result).not.toBe(second);
expect(result.a).not.toBe(first.a);
expect(first).toStrictEqual({a: {b: 1}});
expect(second).toStrictEqual({a: {c: 2}});
});
});
+5 -19
View File
@@ -31,16 +31,10 @@ const mockHttpClose = vi.fn<Server["close"]>(
},
);
const mockFindAllDevices = vi.fn<typeof findAllDevices>(async () => []);
const mockStaticFileServer = vi.fn((_req, res, next) => {
if (typeof next === "function") {
next();
}
const mockStaticFileServer = vi.fn((_req, res) => {
res.end();
});
const mockExpressStaticGzip = vi.fn((_path: unknown, _options: unknown) => mockStaticFileServer);
const mockFinalHandlerNext = vi.fn();
const mockFinalhandler = vi.fn((_req: unknown, _res: unknown) => mockFinalHandlerNext);
const mockCreateStaticFileServer = vi.fn((_dir: unknown, _logError: unknown) => mockStaticFileServer);
vi.mock("node:fs", {spy: true});
vi.mock("node:http", () => ({
@@ -62,11 +56,8 @@ vi.mock("node:http", () => ({
};
}),
}));
vi.mock("express-static-gzip", () => ({
default: vi.fn((path, options) => mockExpressStaticGzip(path, options)),
}));
vi.mock("finalhandler", () => ({
default: vi.fn((req, res) => mockFinalhandler(req, res)),
vi.mock("../lib/util/staticFileServer", () => ({
createStaticFileServer: vi.fn((dir, logError) => mockCreateStaticFileServer(dir, logError)),
}));
vi.mock("zigbee-herdsman/dist/adapter/adapterDiscovery", () => ({
findAllDevices: vi.fn(() => mockFindAllDevices()),
@@ -194,10 +185,7 @@ describe("Onboarding", () => {
mockFindAllDevices.mockClear();
mockHttpErrorListener = undefined;
mockStaticFileServer.mockClear();
mockExpressStaticGzip.mockClear();
mockFinalHandlerNext.mockClear();
mockFinalhandler.mockClear();
mockStaticFileServer.mockClear();
mockCreateStaticFileServer.mockClear();
settings.reRead();
});
@@ -735,7 +723,6 @@ describe("Onboarding", () => {
});
await expect(p).resolves.toStrictEqual(true);
expect(mockFinalhandler).toHaveBeenCalled();
expect(mockStaticFileServer).toHaveBeenCalled();
});
@@ -757,7 +744,6 @@ describe("Onboarding", () => {
});
await expect(p).resolves.toStrictEqual(false);
expect(mockFinalhandler).toHaveBeenCalled();
expect(mockStaticFileServer).toHaveBeenCalled();
});
+10 -11
View File
@@ -4,11 +4,15 @@ import "./mocks/data";
import fs from "node:fs";
import {dump, load} from "js-yaml";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
import {objectAssignDeep} from "../lib/util/objectAssignDeep";
import * as settings from "../lib/util/settings";
// mirrors the global `KeyValue`, which is not visible from the test project, previously implied by the untyped `object-assign-deep`
// biome-ignore lint/suspicious/noExplicitAny: freely mutated to build the expected settings
type ExpectedSettings = Record<string, any>;
const configurationFile = mockedData.joinPath("configuration.yaml");
const devicesFile = mockedData.joinPath("devices.yaml");
const devicesFile2 = mockedData.joinPath("devices2.yaml");
@@ -86,8 +90,7 @@ describe("Settings", () => {
it("Should return default settings", () => {
write(configurationFile, {});
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.devices = {};
expected.groups = {};
expect(s).toStrictEqual(expected);
@@ -96,8 +99,7 @@ describe("Settings", () => {
it("Should return settings", () => {
write(configurationFile, {serial: {disable_led: true}});
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.devices = {};
expected.groups = {};
expected.serial = {disable_led: true};
@@ -124,8 +126,7 @@ describe("Settings", () => {
},
};
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.devices = {
"0x00158d00018255df": {
friendly_name: "0x00158d00018255df",
@@ -178,8 +179,7 @@ describe("Settings", () => {
write(configurationFile, {});
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, settings.testing.defaults);
expected.frontend.enabled = true;
expected.frontend.port = 8099;
expected.homeassistant.enabled = true;
@@ -212,8 +212,7 @@ describe("Settings", () => {
expect(settings.validate()).toStrictEqual([]);
const s = settings.get();
// @ts-expect-error workaround
const expected = objectAssignDeep.noMutate({groups: {}, devices: {}}, settings.testing.defaults);
const expected: ExpectedSettings = objectAssignDeep({}, {groups: {}, devices: {}}, settings.testing.defaults);
expected.mqtt.password = "password-in-env-var";
expected.mqtt.server = "server";
expect(s).toStrictEqual(expected);
+40 -79
View File
@@ -3,8 +3,8 @@ import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} fr
import * as data from "./mocks/data";
import {existsSync, readFileSync, rmSync, writeFileSync} from "node:fs";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
import {objectAssignDeep} from "../lib/util/objectAssignDeep";
import * as settings from "../lib/util/settings";
import * as settingsMigration from "../lib/util/settingsMigration";
import path from "node:path";
@@ -278,8 +278,7 @@ describe("Settings Migration", () => {
});
it("no change needed - only add version", () => {
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
settingsMigration.migrateIfNecessary();
@@ -290,10 +289,8 @@ describe("Settings Migration", () => {
});
it("remove all", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
settings.set(["homeassistant", "legacy_triggers"], true);
@@ -320,8 +317,7 @@ describe("Settings Migration", () => {
settings.set(["external_converters"], ["zyx.js"]);
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
permit_join: true,
homeassistant: {
legacy_triggers: true,
@@ -395,10 +391,8 @@ describe("Settings Migration", () => {
});
it("remove partial", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
settings.set(["advanced", "homeassistant_legacy_triggers"], true);
@@ -417,8 +411,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
permit_join: true,
advanced: {
homeassistant_legacy_triggers: true,
@@ -472,10 +465,8 @@ describe("Settings Migration", () => {
});
it("changes log_level", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {log_level: "warning"};
@@ -484,8 +475,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
log_level: "warn",
},
@@ -505,10 +495,8 @@ describe("Settings Migration", () => {
});
it("does not changes already migrated log_level", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {log_level: "warning"};
@@ -517,8 +505,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
log_level: "warning",
},
@@ -538,10 +525,8 @@ describe("Settings Migration", () => {
});
it("does not changes other log_level", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {log_level: "info"};
@@ -550,8 +535,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
log_level: "info",
},
@@ -571,10 +555,8 @@ describe("Settings Migration", () => {
});
it("transfer all", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {
transmit_power: 12,
@@ -604,8 +586,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
advanced: {
homeassistant_discovery_topic: "ha_disc",
homeassistant_status_topic: "ha_stat",
@@ -649,10 +630,8 @@ describe("Settings Migration", () => {
});
it("transfer partial", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 2;
afterSettings.advanced = {}; // caused by pushing to key and removing all
afterSettings.serial.baudrate = 115200;
@@ -673,8 +652,7 @@ describe("Settings Migration", () => {
// console.log(JSON.stringify(settings.getWrittenSettings(), undefined, 2));
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: {discovery_topic: "ha_disc_newer"},
advanced: {
homeassistant_discovery_topic: "ha_disc",
@@ -718,10 +696,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 3;
afterSettings.homeassistant = {enabled: false};
afterSettings.frontend = {enabled: true};
@@ -739,8 +715,7 @@ describe("Settings Migration", () => {
settings.set(["experimental", "transmit_power"], 12);
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: false,
frontend: true,
availability: {active: {timeout: 15}},
@@ -773,10 +748,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 3;
afterSettings.homeassistant = {enabled: false};
afterSettings.frontend = {enabled: true};
@@ -787,8 +760,7 @@ describe("Settings Migration", () => {
settings.set(["availability"], {active: {timeout: 15}});
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: false,
frontend: true,
availability: {active: {timeout: 15}},
@@ -809,18 +781,15 @@ describe("Settings Migration", () => {
});
it("Update when not set, tests that frontend/availability is not added when not set", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 3;
afterSettings.homeassistant = {enabled: false};
settings.set(["homeassistant"], false);
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
homeassistant: false,
}),
);
@@ -854,10 +823,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 4;
afterSettings.devices = {
"0x123127fffe8d96bc": {
@@ -888,8 +855,7 @@ describe("Settings Migration", () => {
});
expect(settings.getPersistedSettings()).toStrictEqual(
// @ts-expect-error workaround
objectAssignDeep.noMutate(beforeSettings, {
objectAssignDeep({}, beforeSettings, {
devices: {
"0x123127fffe8d96bc": {
friendly_name: "0x847127fffe8d96bc",
@@ -950,10 +916,8 @@ describe("Settings Migration", () => {
});
it("Update", () => {
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
@@ -964,8 +928,7 @@ describe("Settings Migration", () => {
const migratedSettings = settings.getPersistedSettings();
expect(migratedSettings).toStrictEqual(afterSettings);
// @ts-expect-error workaround
const migratedState = objectAssignDeep.noMutate({}, DEFAULT_STATE);
const migratedState = objectAssignDeep({}, DEFAULT_STATE);
delete (migratedState["0x0017880104e45517"] as Record<string, unknown>).update;
delete (migratedState[1] as Record<string, unknown>).update;
@@ -976,10 +939,8 @@ describe("Settings Migration", () => {
const consoleErrorSpy = vi.spyOn(console, "error");
writeFileSync(path.join(data.mockDir, "state.json"), "notjson", "utf8");
// @ts-expect-error workaround
const beforeSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
// @ts-expect-error workaround
const afterSettings = objectAssignDeep.noMutate({}, settings.getPersistedSettings());
const beforeSettings = objectAssignDeep({}, settings.getPersistedSettings());
const afterSettings = objectAssignDeep({}, settings.getPersistedSettings());
afterSettings.version = 5;
expect(settings.getPersistedSettings()).toStrictEqual(beforeSettings);
+185
View File
@@ -0,0 +1,185 @@
import {mkdirSync, writeFileSync} from "node:fs";
import {createServer, type Server} from "node:http";
import {type AddressInfo, connect} from "node:net";
import {join} from "node:path";
import {brotliCompressSync, gzipSync} from "node:zlib";
import tmp from "tmp";
import {afterAll, beforeAll, describe, expect, it, vi} from "vitest";
import {createStaticFileServer, type sendNotFound} from "../lib/util/staticFileServer";
const INDEX_HTML = "<!DOCTYPE html><html lang='en'><body>index</body></html>";
const APP_JS = `console.log("${"x".repeat(2048)}");`;
/** Written next to the served directory, never inside it, so a traversal that succeeds is actually observable. */
const SECRET = "topsecret-must-never-be-served";
const mockLogError = vi.fn<(message: string) => void>();
let dir: string;
let server: Server;
let baseUrl: string;
/** Starts a `node:http` server serving `dir`, mirroring how the frontend/onboarding extensions wire it up. */
function listen(handler: (request: Parameters<typeof sendNotFound>[0], response: Parameters<typeof sendNotFound>[1]) => void): Promise<void> {
server = createServer(handler);
return new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => {
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
resolve();
});
});
}
/** Writes a request line verbatim, bypassing the path normalization `fetch` applies before sending. */
function rawRequest(target: string): Promise<string> {
return new Promise((resolve, reject) => {
const socket = connect((server.address() as AddressInfo).port, "127.0.0.1", () => {
socket.write(`GET ${target} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n`);
});
let received = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
received += chunk;
});
socket.on("end", () => resolve(received));
socket.on("error", reject);
});
}
describe("StaticFileServer", () => {
beforeAll(async () => {
const root = tmp.dirSync().name;
dir = join(root, "public");
// outside the served directory: only a working traversal could reach it
writeFileSync(join(root, "secret.txt"), SECRET);
mkdirSync(join(dir, "sub"), {recursive: true});
writeFileSync(join(dir, "index.html"), INDEX_HTML);
writeFileSync(join(dir, "app.js"), APP_JS);
// precompressed variants, as shipped by the frontend packages
writeFileSync(join(dir, "app.js.gz"), gzipSync(APP_JS));
writeFileSync(join(dir, "app.js.br"), brotliCompressSync(APP_JS));
writeFileSync(join(dir, "sub", "icon.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
await listen(createStaticFileServer(dir, mockLogError));
});
afterAll(async () => {
await new Promise((resolve) => server.close(resolve));
});
it("serves a file with its content type", async () => {
const response = await fetch(`${baseUrl}/sub/icon.png`);
expect(response.status).toStrictEqual(200);
expect(response.headers.get("content-type")).toStrictEqual("image/png");
expect(response.headers.get("content-encoding")).toBeNull();
});
it("serves index.html for the root, never cached", async () => {
const response = await fetch(`${baseUrl}/`);
expect(response.status).toStrictEqual(200);
expect(response.headers.get("content-type")).toStrictEqual("text/html; charset=utf-8");
expect(response.headers.get("cache-control")).toStrictEqual("no-store");
await expect(response.text()).resolves.toStrictEqual(INDEX_HTML);
});
it("serves the precompressed brotli variant", async () => {
const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "br"}});
expect(response.status).toStrictEqual(200);
expect(response.headers.get("content-encoding")).toStrictEqual("br");
expect(response.headers.get("content-type")).toStrictEqual("text/javascript; charset=utf-8");
expect(response.headers.get("vary")).toStrictEqual("Accept-Encoding");
// decoded by fetch, so the served bytes must be the brotli variant of the original file
await expect(response.text()).resolves.toStrictEqual(APP_JS);
});
it("serves the precompressed gzip variant", async () => {
const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "gzip"}});
expect(response.status).toStrictEqual(200);
expect(response.headers.get("content-encoding")).toStrictEqual("gzip");
await expect(response.text()).resolves.toStrictEqual(APP_JS);
});
it("serves the identity file when no encoding is accepted", async () => {
const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "identity"}});
expect(response.status).toStrictEqual(200);
expect(response.headers.get("content-encoding")).toBeNull();
expect(response.headers.get("content-length")).toStrictEqual(String(Buffer.byteLength(APP_JS)));
await expect(response.text()).resolves.toStrictEqual(APP_JS);
});
it("revalidates with an etag", async () => {
const response = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "identity"}});
const etag = response.headers.get("etag");
expect(etag).toBeTruthy();
const revalidated = await fetch(`${baseUrl}/app.js`, {headers: {"Accept-Encoding": "identity", "If-None-Match": etag as string}});
expect(revalidated.status).toStrictEqual(304);
});
it("returns 404 for an unknown file", async () => {
const response = await fetch(`${baseUrl}/nope.js`);
expect(response.status).toStrictEqual(404);
expect(response.headers.get("content-type")).toStrictEqual("text/html; charset=utf-8");
expect(response.headers.get("content-security-policy")).toStrictEqual("default-src 'none'");
expect(response.headers.get("x-content-type-options")).toStrictEqual("nosniff");
await expect(response.text()).resolves.toContain("Cannot GET /nope.js");
});
it("escapes the url in the 404 body", async () => {
const response = await fetch(`${baseUrl}/%3Cscript%3E`);
expect(response.status).toStrictEqual(404);
await expect(response.text()).resolves.not.toContain("<script>");
});
it("does not serve files outside of the served directory", async () => {
// `fetch` resolves `..` and `%2e%2e` segments away before they ever reach the server, so these have to go out raw
for (const target of ["/../secret.txt", "/sub/../../secret.txt", "/%2e%2e/secret.txt", "/..%2fsecret.txt"]) {
const response = await rawRequest(target);
expect(response).toContain("404 Not Found");
expect(response).not.toContain(SECRET);
}
});
it("reports a failure to serve with a 500", async () => {
const failing = createStaticFileServer(dir, mockLogError);
const failingServer = createServer((request, response) => {
const setHeader = response.setHeader.bind(response);
response.setHeader = (name: string, value: number | string | readonly string[]): never => {
if (name === "Content-Security-Policy") {
throw new Error("socket gone");
}
setHeader(name, value);
return undefined as never;
};
failing(request, response);
});
await new Promise<void>((resolve) => failingServer.listen(0, "127.0.0.1", resolve));
const port = (failingServer.address() as AddressInfo).port;
const response = await fetch(`http://127.0.0.1:${port}/nope.js`);
expect(response.status).toStrictEqual(500);
expect(mockLogError).toHaveBeenCalledWith("Failed to serve '/nope.js': socket gone");
await new Promise((resolve) => failingServer.close(resolve));
});
});
+13 -3
View File
@@ -164,14 +164,19 @@ describe("Utils", () => {
b: 2,
3: "c",
d: Buffer.from([1, 2]),
e: new Int16Array([0xfffd, 0xff11]),
e: new Int16Array([0xfffd, 0xff11, 0x0001, 0x7fff]),
beef: 0xfacen,
zed: new BigUint64Array([1n, 0xffffffffn]),
zed: new BigUint64Array([1n, 0xffffffffn, 42n]),
ris: [1, undefined, "b", 0xfeefn, Number.NaN, undefined],
ls: undefined,
// one and two elements, on both the number and the bigint branch
one: new Uint8Array([7]),
two: new Int16Array([0x0001, 0x7fff]),
oneBig: new BigInt64Array([-9n]),
twoBig: new BigUint64Array([1n, 42n]),
}),
).toStrictEqual(
`{"3":"c","a":"a","b":2,"beef":"64206","d":{"data":[1,2],"type":"Buffer"},"e":{"0":-3,"1":-239},"ris":[1,null,"b","65263",null,null],"zed":{"0":"1","1":"4294967295"}}`,
`{"3":"c","a":"a","b":2,"beef":"64206","d":{"data":[1,2],"type":"Buffer"},"e":{"0":-3,"1":-239,"2":1,"3":32767},"one":{"0":7},"oneBig":{"0":"-9"},"ris":[1,null,"b","65263",null,null],"two":{"0":1,"1":32767},"twoBig":{"0":"1","1":"42"},"zed":{"0":"1","1":"4294967295","2":"42"}}`,
);
// @ts-expect-error intentional to reach code for coverage
expect(stringify(undefined)).toStrictEqual("null");
@@ -188,5 +193,10 @@ describe("Utils", () => {
const toJSONIsNull = {a: 1, toJSON: () => null};
expect(stringify(toJSONIsNull)).toStrictEqual("null");
const emptyTypedArrayWithProperty = Object.assign(new Uint8Array(0), {unit: "raw"});
expect(stringify({data: emptyTypedArrayWithProperty})).toStrictEqual(JSON.stringify({data: {unit: "raw"}}));
expect(stringify({data: new Uint8Array(0)})).toStrictEqual(JSON.stringify({data: {}}));
});
});