mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-30 07:38:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0cd2c6b96 | ||
|
|
4a4ee8888d | ||
|
|
5285b7b95e | ||
|
|
c8dc26b241 | ||
|
|
25ea77e038 | ||
|
|
50c7d2d6ad | ||
|
|
3ccbaf21d2 | ||
|
|
6930fa6f3f | ||
|
|
16faef1511 | ||
|
|
c87d4524d5 | ||
|
|
33234d9ce4 | ||
|
|
4190ba43b4 | ||
|
|
b7d9c78309 | ||
|
|
50b0ffd7e7 | ||
|
|
15f3eaf966 | ||
|
|
0d5de750ff | ||
|
|
7109900bcf | ||
|
|
281e10c15f | ||
|
|
cdad6ccd75 | ||
|
|
b537f9a93a | ||
|
|
1b56e63aab | ||
|
|
f33d3606be | ||
|
|
fb60cfde38 | ||
|
|
de800450ad | ||
|
|
977672976f | ||
|
|
cd9b752ede | ||
|
|
242815e139 | ||
|
|
87ff0f3711 | ||
|
|
8ecb9be98e | ||
|
|
0e66b29983 | ||
|
|
6a8d20858e | ||
|
|
e4b253e08b | ||
|
|
56d3a8f9d8 | ||
|
|
f6c3887c81 | ||
|
|
2affe188c5 | ||
|
|
587a02f663 | ||
|
|
0181263d32 | ||
|
|
eaa3e458b5 | ||
|
|
252bfbaa20 | ||
|
|
66e1318a79 | ||
|
|
9a4ce6fcd0 | ||
|
|
4b95b54be7 | ||
|
|
f637e7d59c | ||
|
|
018fad27af | ||
|
|
4594142c09 | ||
|
|
ac80bb6430 | ||
|
|
1765fe8925 | ||
|
|
7e82a3d1e9 | ||
|
|
9df6cccd31 |
@@ -1,6 +1,12 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, push]
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
branches-ignore:
|
||||
- dev-types
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -83,6 +89,22 @@ jobs:
|
||||
VERSION=dev
|
||||
DATE=${{ github.event.repository.updated_at }}
|
||||
|
||||
- name: dev - Publish types
|
||||
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
|
||||
run: |
|
||||
pnpm run build:types
|
||||
temp_dir=$(mktemp -d)
|
||||
cp -R package.json LICENSE dist $temp_dir
|
||||
git fetch --unshallow
|
||||
git checkout dev-types
|
||||
rm -rf *
|
||||
cp -R $temp_dir/* .
|
||||
git add -A
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "$GITHUB_SHA" || echo 'Nothing to commit'
|
||||
git push --force --set-upstream origin dev-types
|
||||
|
||||
- name: release - Docker meta
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
|
||||
uses: docker/metadata-action@v5
|
||||
|
||||
@@ -53,6 +53,7 @@ typings/
|
||||
|
||||
# Build cache
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.types.tsbuildinfo
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.4.0"
|
||||
".": "2.5.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
# Changelog
|
||||
|
||||
## [2.5.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.4.0...2.5.0) (2025-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* New health extension & extras in `bridge/info` ([#27164](https://github.com/Koenkk/zigbee2mqtt/issues/27164)) ([cd9b752](https://github.com/Koenkk/zigbee2mqtt/commit/cd9b752ede91db1d02c567dded92c4e52c05b88d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Bump Alpine to 3.22 ([#27690](https://github.com/Koenkk/zigbee2mqtt/issues/27690)) ([f6c3887](https://github.com/Koenkk/zigbee2mqtt/commit/f6c3887c812ea6e15e950e6d55e0274356f2eea5))
|
||||
* Fix devices not immediately marked as offline when timeout expires ([#27691](https://github.com/Koenkk/zigbee2mqtt/issues/27691)) ([e4b253e](https://github.com/Koenkk/zigbee2mqtt/commit/e4b253e08beb147367872aa4517df93a03ef1a4f))
|
||||
* Generate group ID if empty string given ([#27702](https://github.com/Koenkk/zigbee2mqtt/issues/27702)) ([0e66b29](https://github.com/Koenkk/zigbee2mqtt/commit/0e66b29983ae691a2dab44f6649e20a9e00a43cd))
|
||||
* **ignore:** API correctness ([#27637](https://github.com/Koenkk/zigbee2mqtt/issues/27637)) ([018fad2](https://github.com/Koenkk/zigbee2mqtt/commit/018fad27afb5861b1d9c7adff3161c926ccbbaa4))
|
||||
* **ignore:** bump @types/node from 22.15.30 to 24.0.3 ([#27745](https://github.com/Koenkk/zigbee2mqtt/issues/27745)) ([de80045](https://github.com/Koenkk/zigbee2mqtt/commit/de800450ad31e5d8ad5c342b061dc3c59f0d565e))
|
||||
* **ignore:** bump the minor-patch group across 1 directory with 4 updates ([#27858](https://github.com/Koenkk/zigbee2mqtt/issues/27858)) ([c8dc26b](https://github.com/Koenkk/zigbee2mqtt/commit/c8dc26b241ae6562271d56fe59a13f05927b2bb4))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#27797](https://github.com/Koenkk/zigbee2mqtt/issues/27797)) ([50b0ffd](https://github.com/Koenkk/zigbee2mqtt/commit/50b0ffd7e7ef343fec983a62d2268feda28b01ba))
|
||||
* **ignore:** bump the minor-patch group with 3 updates ([#27598](https://github.com/Koenkk/zigbee2mqtt/issues/27598)) ([7e82a3d](https://github.com/Koenkk/zigbee2mqtt/commit/7e82a3d1e960d4fa2d89558b6d6a20534e7d6ecf))
|
||||
* **ignore:** bump the minor-patch group with 4 updates ([#27610](https://github.com/Koenkk/zigbee2mqtt/issues/27610)) ([ac80bb6](https://github.com/Koenkk/zigbee2mqtt/commit/ac80bb6430007e494c544089af323c3fb99c3541))
|
||||
* **ignore:** bump the minor-patch group with 6 updates ([#27678](https://github.com/Koenkk/zigbee2mqtt/issues/27678)) ([587a02f](https://github.com/Koenkk/zigbee2mqtt/commit/587a02f66319688cb93df82fde02539bce0263ea))
|
||||
* **ignore:** bump zigbee2mqtt-windfront from 1.0.5 to 1.0.6 in the minor-patch group ([#27744](https://github.com/Koenkk/zigbee2mqtt/issues/27744)) ([9776729](https://github.com/Koenkk/zigbee2mqtt/commit/977672976f06f644ed6e02e4a17c07b63873c8da))
|
||||
* **ignore:** Migrate to Biome 2 ([#27785](https://github.com/Koenkk/zigbee2mqtt/issues/27785)) ([c87d452](https://github.com/Koenkk/zigbee2mqtt/commit/c87d4524d599302a0728b85799c9bf7c0784a8f4))
|
||||
* **ignore:** update zigbee-herdsman to 4.1.1 ([#27640](https://github.com/Koenkk/zigbee2mqtt/issues/27640)) ([4b95b54](https://github.com/Koenkk/zigbee2mqtt/commit/4b95b54be7abc53a7f7a83814994f9cdde90134e))
|
||||
* **ignore:** update zigbee-herdsman to 4.1.2 ([#27729](https://github.com/Koenkk/zigbee2mqtt/issues/27729)) ([242815e](https://github.com/Koenkk/zigbee2mqtt/commit/242815e1390e9e3a5ded929e8bfc4a9a4329a526))
|
||||
* **ignore:** update zigbee-herdsman to 4.2.0 ([#27786](https://github.com/Koenkk/zigbee2mqtt/issues/27786)) ([15f3eaf](https://github.com/Koenkk/zigbee2mqtt/commit/15f3eaf96629caa5fbd7e02ae002883becc1b49f))
|
||||
* **ignore:** update zigbee-herdsman to 4.2.1 ([#27835](https://github.com/Koenkk/zigbee2mqtt/issues/27835)) ([16faef1](https://github.com/Koenkk/zigbee2mqtt/commit/16faef1511e9bfb1cf76c5faaa1008e195ef67fc))
|
||||
* **ignore:** update zigbee-herdsman to 4.3.0 ([#27853](https://github.com/Koenkk/zigbee2mqtt/issues/27853)) ([50c7d2d](https://github.com/Koenkk/zigbee2mqtt/commit/50c7d2d6adb9c9366d5c7f829003c84a140f7a19))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.54.0 ([#27600](https://github.com/Koenkk/zigbee2mqtt/issues/27600)) ([1765fe8](https://github.com/Koenkk/zigbee2mqtt/commit/1765fe8925e359b917c1e2e08d3691d4c8bdcf3f))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.55.0 ([#27618](https://github.com/Koenkk/zigbee2mqtt/issues/27618)) ([4594142](https://github.com/Koenkk/zigbee2mqtt/commit/4594142c09ee8d19af2d32238ffa58512694d2ed))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.56.0 ([#27639](https://github.com/Koenkk/zigbee2mqtt/issues/27639)) ([f637e7d](https://github.com/Koenkk/zigbee2mqtt/commit/f637e7d59cc71f32b8d04e8f4bfc414027e7d23a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.57.0 ([#27646](https://github.com/Koenkk/zigbee2mqtt/issues/27646)) ([252bfba](https://github.com/Koenkk/zigbee2mqtt/commit/252bfbaa20beccd315592dac811cb23309cdee01))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.57.1 ([#27689](https://github.com/Koenkk/zigbee2mqtt/issues/27689)) ([56d3a8f](https://github.com/Koenkk/zigbee2mqtt/commit/56d3a8f9d8bc01e30a6e469b517545d3406941e2))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.58.0 ([#27695](https://github.com/Koenkk/zigbee2mqtt/issues/27695)) ([6a8d208](https://github.com/Koenkk/zigbee2mqtt/commit/6a8d20858ede85de72c34ce58e9df41fa2e91d0d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.59.0 ([#27712](https://github.com/Koenkk/zigbee2mqtt/issues/27712)) ([87ff0f3](https://github.com/Koenkk/zigbee2mqtt/commit/87ff0f3711e043eb47319e9fb8abcdfa878d9868))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.60.0 ([#27752](https://github.com/Koenkk/zigbee2mqtt/issues/27752)) ([f33d360](https://github.com/Koenkk/zigbee2mqtt/commit/f33d3606be9e916989d08c7d418c4505dcbfe4ea))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.61.0 ([#27759](https://github.com/Koenkk/zigbee2mqtt/issues/27759)) ([b537f9a](https://github.com/Koenkk/zigbee2mqtt/commit/b537f9a93a61095b6b0b82d04d85f7e264aec27c))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.62.0 ([#27767](https://github.com/Koenkk/zigbee2mqtt/issues/27767)) ([281e10c](https://github.com/Koenkk/zigbee2mqtt/commit/281e10c15fa09358e999f1f6b506fe9b08295608))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.63.0 ([#27784](https://github.com/Koenkk/zigbee2mqtt/issues/27784)) ([0d5de75](https://github.com/Koenkk/zigbee2mqtt/commit/0d5de750ffed0674c5a23c2c4d8d1e954c602197))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.64.0 ([#27799](https://github.com/Koenkk/zigbee2mqtt/issues/27799)) ([b7d9c78](https://github.com/Koenkk/zigbee2mqtt/commit/b7d9c78309a8c3e3cf551948b6542d2b71b70697))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.65.0 ([#27804](https://github.com/Koenkk/zigbee2mqtt/issues/27804)) ([4190ba4](https://github.com/Koenkk/zigbee2mqtt/commit/4190ba43b43e1f688e246f9236bfe7c60230a927))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.66.0 ([#27831](https://github.com/Koenkk/zigbee2mqtt/issues/27831)) ([33234d9](https://github.com/Koenkk/zigbee2mqtt/commit/33234d9ce45323a2a10b7839c5f7aebc84a9cc32))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.67.0 ([#27842](https://github.com/Koenkk/zigbee2mqtt/issues/27842)) ([6930fa6](https://github.com/Koenkk/zigbee2mqtt/commit/6930fa6f3fce8f415302c8db32294b1a9d0ea333))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.68.0 ([#27852](https://github.com/Koenkk/zigbee2mqtt/issues/27852)) ([3ccbaf2](https://github.com/Koenkk/zigbee2mqtt/commit/3ccbaf21d227b7c4ad7952522a980a9083f06463))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.69.0 ([#27860](https://github.com/Koenkk/zigbee2mqtt/issues/27860)) ([5285b7b](https://github.com/Koenkk/zigbee2mqtt/commit/5285b7b95e3c977cca14d28444b9f4f2a9d8996c))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.14 ([#27645](https://github.com/Koenkk/zigbee2mqtt/issues/27645)) ([66e1318](https://github.com/Koenkk/zigbee2mqtt/commit/66e1318a79502cb723cf88d888c9ac23daeb7fd8))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.15 ([#27711](https://github.com/Koenkk/zigbee2mqtt/issues/27711)) ([8ecb9be](https://github.com/Koenkk/zigbee2mqtt/commit/8ecb9be98efb44db23f386e05509389c7d681025))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.16 ([#27783](https://github.com/Koenkk/zigbee2mqtt/issues/27783)) ([7109900](https://github.com/Koenkk/zigbee2mqtt/commit/7109900bcfeff48fdea48456455cd1c6cebf380a))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.0.5 ([#27656](https://github.com/Koenkk/zigbee2mqtt/issues/27656)) ([eaa3e45](https://github.com/Koenkk/zigbee2mqtt/commit/eaa3e458b5a8a9c5ebf31e12ae7e68fcf60f762f))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.1.0 ([#27763](https://github.com/Koenkk/zigbee2mqtt/issues/27763)) ([cdad6cc](https://github.com/Koenkk/zigbee2mqtt/commit/cdad6ccd75200061ded795af032f0fed0bb21484))
|
||||
* Rename fix typo in backoff settings https://github.com/Koenkk/zigbee2mqtt/issues/27642 ([0181263](https://github.com/Koenkk/zigbee2mqtt/commit/0181263d32dd1099676ed92226a3fc2ee6ac701e))
|
||||
* update zigbee2mqtt-windfront to 1.0.4 ([#27641](https://github.com/Koenkk/zigbee2mqtt/issues/27641)) ([9a4ce6f](https://github.com/Koenkk/zigbee2mqtt/commit/9a4ce6fcd0fce6fea9c195ff48a41096b738dccb))
|
||||
|
||||
## [2.4.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.3.0...2.4.0) (2025-06-01)
|
||||
|
||||
|
||||
|
||||
+14
-5
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
@@ -12,10 +12,10 @@
|
||||
"bracketSpacing": false
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["package.json"]
|
||||
"includes": ["**", "!package.json"]
|
||||
},
|
||||
"linter": {
|
||||
"ignore": [],
|
||||
"includes": ["**"],
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUnusedImports": "error"
|
||||
@@ -55,7 +55,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": "off"
|
||||
@@ -68,7 +77,7 @@
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["test/**"],
|
||||
"includes": ["test/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
FROM alpine:3.21 AS base
|
||||
FROM alpine:3.22 AS base
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
@@ -12,7 +12,7 @@ FROM base AS deps
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
# Make and such are needed to compile serialport for riscv64
|
||||
RUN apk add make gcc g++ python3 linux-headers npm && \
|
||||
npm install -g pnpm@10.4.1 && \
|
||||
npm install -g pnpm@$(sed -n 's/.*"packageManager": "pnpm@\([0-9.]*\)".*/\1/p' package.json) && \
|
||||
pnpm install --frozen-lockfile --no-optional && \
|
||||
# serialport has outdated prebuilds that appear to fail on some archs, force build on target platform
|
||||
rm -rf `find ./node_modules/.pnpm/ -wholename "*/@serialport/bindings-cpp/prebuilds" -type d` && \
|
||||
|
||||
+4
-5
@@ -1,21 +1,18 @@
|
||||
import type Extension from "./extension/extension";
|
||||
import type {Zigbee2MQTTAPI} from "./types/api";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {setLogger as zhSetLogger} from "zigbee-herdsman";
|
||||
import {setLogger as zhcSetLogger} from "zigbee-herdsman-converters";
|
||||
|
||||
import EventBus from "./eventBus";
|
||||
// Extensions
|
||||
import ExtensionAvailability from "./extension/availability";
|
||||
import ExtensionBind from "./extension/bind";
|
||||
import ExtensionBridge from "./extension/bridge";
|
||||
import ExtensionConfigure from "./extension/configure";
|
||||
import type Extension from "./extension/extension";
|
||||
import ExtensionExternalConverters from "./extension/externalConverters";
|
||||
import ExtensionExternalExtensions from "./extension/externalExtensions";
|
||||
import ExtensionGroups from "./extension/groups";
|
||||
import ExtensionHealth from "./extension/health";
|
||||
import ExtensionNetworkMap from "./extension/networkMap";
|
||||
import ExtensionOnEvent from "./extension/onEvent";
|
||||
import ExtensionOTAUpdate from "./extension/otaUpdate";
|
||||
@@ -23,6 +20,7 @@ import ExtensionPublish from "./extension/publish";
|
||||
import ExtensionReceive from "./extension/receive";
|
||||
import Mqtt, {type MqttPublishOptions} from "./mqtt";
|
||||
import State from "./state";
|
||||
import type {Zigbee2MQTTAPI} from "./types/api";
|
||||
import logger from "./util/logger";
|
||||
import {initSdNotify} from "./util/sd-notify";
|
||||
import * as settings from "./util/settings";
|
||||
@@ -76,6 +74,7 @@ export class Controller {
|
||||
new ExtensionOTAUpdate(...this.extensionArgs),
|
||||
new ExtensionExternalExtensions(...this.extensionArgs),
|
||||
new ExtensionAvailability(...this.extensionArgs),
|
||||
new ExtensionHealth(...this.extensionArgs),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,25 @@ type EventBusListener<K> = K extends keyof EventBusMap
|
||||
: never
|
||||
: never;
|
||||
|
||||
type Stats = {
|
||||
devices: Map<
|
||||
string, // IEEE address
|
||||
{
|
||||
lastSeenChanges?: {messages: number; first: number};
|
||||
leaveCounts: number;
|
||||
networkAddressChanges: number;
|
||||
}
|
||||
>;
|
||||
mqtt: {
|
||||
published: number;
|
||||
received: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default class EventBus {
|
||||
private callbacksByExtension = new Map<string, {event: keyof EventBusMap; callback: EventBusListener<keyof EventBusMap>}[]>();
|
||||
private emitter = new events.EventEmitter<EventBusMap>();
|
||||
readonly stats: Stats = {devices: new Map(), mqtt: {published: 0, received: 0}};
|
||||
|
||||
constructor() {
|
||||
this.emitter.setMaxListeners(100);
|
||||
@@ -72,6 +88,18 @@ export default class EventBus {
|
||||
|
||||
public emitLastSeenChanged(data: eventdata.LastSeenChanged): void {
|
||||
this.emitter.emit("lastSeenChanged", data);
|
||||
|
||||
const device = this.stats.devices.get(data.device.ieeeAddr);
|
||||
|
||||
if (device?.lastSeenChanges) {
|
||||
device.lastSeenChanges.messages += 1;
|
||||
} else {
|
||||
this.stats.devices.set(data.device.ieeeAddr, {
|
||||
lastSeenChanges: {messages: 1, first: Date.now()},
|
||||
leaveCounts: 0,
|
||||
networkAddressChanges: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
public onLastSeenChanged(key: ListenerKey, callback: (data: eventdata.LastSeenChanged) => void): void {
|
||||
this.on("lastSeenChanged", callback, key);
|
||||
@@ -79,6 +107,14 @@ export default class EventBus {
|
||||
|
||||
public emitDeviceNetworkAddressChanged(data: eventdata.DeviceNetworkAddressChanged): void {
|
||||
this.emitter.emit("deviceNetworkAddressChanged", data);
|
||||
|
||||
const device = this.stats.devices.get(data.device.ieeeAddr);
|
||||
|
||||
if (device) {
|
||||
device.networkAddressChanges += 1;
|
||||
} else {
|
||||
this.stats.devices.set(data.device.ieeeAddr, {leaveCounts: 0, networkAddressChanges: 1});
|
||||
}
|
||||
}
|
||||
public onDeviceNetworkAddressChanged(key: ListenerKey, callback: (data: eventdata.DeviceNetworkAddressChanged) => void): void {
|
||||
this.on("deviceNetworkAddressChanged", callback, key);
|
||||
@@ -121,6 +157,14 @@ export default class EventBus {
|
||||
|
||||
public emitDeviceLeave(data: eventdata.DeviceLeave): void {
|
||||
this.emitter.emit("deviceLeave", data);
|
||||
|
||||
const device = this.stats.devices.get(data.ieeeAddr);
|
||||
|
||||
if (device) {
|
||||
device.leaveCounts += 1;
|
||||
} else {
|
||||
this.stats.devices.set(data.ieeeAddr, {leaveCounts: 1, networkAddressChanges: 0});
|
||||
}
|
||||
}
|
||||
public onDeviceLeave(key: ListenerKey, callback: (data: eventdata.DeviceLeave) => void): void {
|
||||
this.on("deviceLeave", callback, key);
|
||||
@@ -135,6 +179,8 @@ export default class EventBus {
|
||||
|
||||
public emitMQTTMessage(data: eventdata.MQTTMessage): void {
|
||||
this.emitter.emit("mqttMessage", data);
|
||||
|
||||
this.stats.mqtt.received += 1;
|
||||
}
|
||||
public onMQTTMessage(key: ListenerKey, callback: (data: eventdata.MQTTMessage) => void): void {
|
||||
this.on("mqttMessage", callback, key);
|
||||
@@ -142,6 +188,8 @@ export default class EventBus {
|
||||
|
||||
public emitMQTTMessagePublished(data: eventdata.MQTTMessagePublished): void {
|
||||
this.emitter.emit("mqttMessagePublished", data);
|
||||
|
||||
this.stats.mqtt.published += 1;
|
||||
}
|
||||
public onMQTTMessagePublished(key: ListenerKey, callback: (data: eventdata.MQTTMessagePublished) => void): void {
|
||||
this.on("mqttMessagePublished", callback, key);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import debounce from "debounce";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
@@ -76,7 +73,7 @@ export default class Availability extends Extension {
|
||||
if (entity.isDevice()) {
|
||||
const lastSeen = entity.zh.lastSeen ?? /* v8 ignore next */ 0;
|
||||
|
||||
return Date.now() - lastSeen < this.getTimeout(entity) + this.getMaxJitter(entity);
|
||||
return Date.now() - lastSeen < this.getTimeout(entity);
|
||||
}
|
||||
|
||||
for (const memberDevice of entity.membersDevices()) {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import debounce from "debounce";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
|
||||
import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
import Device from "../model/device";
|
||||
import Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
|
||||
+19
-11
@@ -1,21 +1,16 @@
|
||||
import type winston from "winston";
|
||||
|
||||
import type Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import JSZip from "jszip";
|
||||
import objectAssignDeep from "object-assign-deep";
|
||||
import type winston from "winston";
|
||||
import Transport from "winston-transport";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import Device from "../model/device";
|
||||
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 * as settings from "../util/settings";
|
||||
@@ -25,6 +20,8 @@ import Extension from "./extension";
|
||||
const REQUEST_REGEX = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
export default class Bridge extends Extension {
|
||||
// set on `start`
|
||||
#osInfo!: Zigbee2MQTTAPI["bridge/info"]["os"];
|
||||
private zigbee2mqttVersion!: {commitHash?: string; version: string};
|
||||
private zigbeeHerdsmanVersion!: {version: string};
|
||||
private zigbeeHerdsmanConvertersVersion!: {version: string};
|
||||
@@ -92,6 +89,15 @@ export default class Bridge extends Extension {
|
||||
|
||||
logger.addTransport(this.logTransport);
|
||||
|
||||
const os = await import("node:os");
|
||||
const process = await import("node:process");
|
||||
const logicalCpuCores = os.cpus();
|
||||
this.#osInfo = {
|
||||
version: `${os.version()} - ${os.release()} - ${os.arch()}`,
|
||||
node_version: process.version,
|
||||
cpus: `${[...new Set(logicalCpuCores.map((cpu) => cpu.model))].join(" | ")} (x${logicalCpuCores.length})`,
|
||||
memory_mb: Math.round(os.totalmem() / 1024 / 1024),
|
||||
};
|
||||
this.zigbee2mqttVersion = await utils.getZigbee2MQTTVersion();
|
||||
this.zigbeeHerdsmanVersion = await utils.getDependencyVersion("zigbee-herdsman");
|
||||
this.zigbeeHerdsmanConvertersVersion = await utils.getDependencyVersion("zigbee-herdsman-converters");
|
||||
@@ -691,6 +697,8 @@ export default class Bridge extends Extension {
|
||||
|
||||
const networkParams = await this.zigbee.getNetworkParameters();
|
||||
const payload: Zigbee2MQTTAPI["bridge/info"] = {
|
||||
os: this.#osInfo,
|
||||
mqtt: this.mqtt.info,
|
||||
version: this.zigbee2mqttVersion.version,
|
||||
commit: this.zigbee2mqttVersion.commitHash,
|
||||
zigbee_herdsman_converters: this.zigbeeHerdsmanConvertersVersion,
|
||||
@@ -824,8 +832,8 @@ export default class Bridge extends Extension {
|
||||
|
||||
if (icon) {
|
||||
/* v8 ignore next */
|
||||
icon = icon.replace("${zigbeeModel}", utils.sanitizeImageParameter(device.zh.modelID ?? ""));
|
||||
icon = icon.replace("${model}", utils.sanitizeImageParameter(device.definition.model));
|
||||
icon = icon.replace("$zigbeeModel", utils.sanitizeImageParameter(device.zh.modelID ?? ""));
|
||||
icon = icon.replace("$model", utils.sanitizeImageParameter(device.definition.model));
|
||||
}
|
||||
|
||||
const payload: Zigbee2MQTTDevice["definition"] = {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import Device from "../model/device";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
@@ -59,7 +56,7 @@ export default class Configure extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
override start(): Promise<void> {
|
||||
setImmediate(async () => {
|
||||
// Only configure routers on startup, end devices are likely sleeping and
|
||||
// will reconfigure once they send a message
|
||||
@@ -82,6 +79,8 @@ export default class Configure extends Extension {
|
||||
this.eventBus.onLastSeenChanged(this, (data) => this.configure(data.device, "zigbee_event"));
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
this.eventBus.onReconfigure(this, this.onReconfigure);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async configure(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type Extension from "./extension";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import type Extension from "./extension";
|
||||
import ExternalJSExtension from "./externalJS";
|
||||
|
||||
type TModule = new (...args: ConstructorParameters<typeof Extension>) => Extension;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from "../types/api";
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from "../types/api";
|
||||
|
||||
import data from "../util/data";
|
||||
import logger from "../util/logger";
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type {IncomingMessage, Server, ServerResponse} from "node:http";
|
||||
import type {Socket} from "node:net";
|
||||
|
||||
import type {RequestHandler} from "express-static-gzip";
|
||||
|
||||
import assert from "node:assert";
|
||||
import {existsSync, readFileSync} from "node:fs";
|
||||
import type {IncomingMessage, Server, ServerResponse} from "node:http";
|
||||
import {createServer} from "node:http";
|
||||
import {createServer as createSecureServer} from "node:https";
|
||||
import type {Socket} from "node:net";
|
||||
import {posix} from "node:path";
|
||||
import {parse} from "node:url";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import type {RequestHandler} from "express-static-gzip";
|
||||
import expressStaticGzip from "express-static-gzip";
|
||||
import finalhandler from "finalhandler";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import equals from "fast-deep-equal/es6";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
import Device from "../model/device";
|
||||
import Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils, {isLightExpose} from "../util/utils";
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as os from "node:os";
|
||||
import * as process from "node:process";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
/** Round with 2 decimals */
|
||||
const round2 = (n: number): number => Math.round(n * 100.0) / 100.0;
|
||||
/** Round with 4 decimals */
|
||||
const round4 = (n: number): number => Math.round(n * 10000.0) / 10000.0;
|
||||
|
||||
export default class Health extends Extension {
|
||||
#checkTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
override async start(): Promise<void> {
|
||||
await super.start();
|
||||
|
||||
this.#checkTimer = setInterval(this.#checkHealth.bind(this), utils.minutes(settings.get().health.interval));
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
clearInterval(this.#checkTimer);
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
clearStats(): void {
|
||||
this.eventBus.stats.devices.clear();
|
||||
this.eventBus.stats.mqtt.published = 0;
|
||||
this.eventBus.stats.mqtt.received = 0;
|
||||
}
|
||||
|
||||
async #checkHealth(): Promise<void> {
|
||||
const sysMemTotalKb = os.totalmem() / 1024;
|
||||
const sysMemFreeKb = os.freemem() / 1024;
|
||||
const procMemUsedKb = process.memoryUsage().rss / 1024;
|
||||
const healthcheck: Zigbee2MQTTAPI["bridge/health"] = {
|
||||
response_time: Date.now(),
|
||||
os: {
|
||||
load_average: os.loadavg(), // will be [0,0,0] on Windows (not supported)
|
||||
memory_used_mb: round2((sysMemTotalKb - sysMemFreeKb) / 1024),
|
||||
memory_percent: round4((sysMemFreeKb / sysMemTotalKb) * 100.0),
|
||||
},
|
||||
process: {
|
||||
uptime_sec: Math.floor(process.uptime()),
|
||||
memory_used_mb: round2(procMemUsedKb / 1024),
|
||||
memory_percent: round4((procMemUsedKb / sysMemTotalKb) * 100.0),
|
||||
},
|
||||
mqtt: {...this.mqtt.stats, ...this.eventBus.stats.mqtt},
|
||||
devices: {},
|
||||
};
|
||||
|
||||
for (const [ieeeAddr, device] of this.eventBus.stats.devices) {
|
||||
let messages = 0;
|
||||
let mps = 0;
|
||||
|
||||
if (device.lastSeenChanges) {
|
||||
const timeDiff = Date.now() - device.lastSeenChanges.first;
|
||||
messages = device.lastSeenChanges.messages;
|
||||
mps = timeDiff > 0 ? round4(messages / (timeDiff / 1000.0)) : 0;
|
||||
}
|
||||
|
||||
healthcheck.devices[ieeeAddr] = {
|
||||
messages,
|
||||
messages_per_sec: mps,
|
||||
leave_count: device.leaveCounts,
|
||||
network_address_changes: device.networkAddressChanges,
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.get().health.reset_on_check) {
|
||||
this.clearStats();
|
||||
}
|
||||
|
||||
await this.mqtt.publish("bridge/health", JSON.stringify(healthcheck), {clientOptions: {retain: true, qos: 1}});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type {Ota} from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
import path from "node:path";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import type {Ota} from "zigbee-herdsman-converters";
|
||||
import {ota} from "zigbee-herdsman-converters";
|
||||
|
||||
import Device from "../model/device";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
import dataDir from "../util/data";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import Device from "../model/device";
|
||||
import Group from "../model/group";
|
||||
|
||||
+4
-6
@@ -1,10 +1,8 @@
|
||||
import type {CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import {Numeric, access} from "zigbee-herdsman-converters";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
import type {CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import {access, Numeric} from "zigbee-herdsman-converters";
|
||||
|
||||
import * as settings from "../util/settings";
|
||||
|
||||
@@ -108,7 +106,7 @@ export default class Device {
|
||||
}
|
||||
|
||||
endpointName(endpoint: zh.Endpoint): string | undefined {
|
||||
let epName = undefined;
|
||||
let epName: string | undefined;
|
||||
|
||||
if (this.definition?.endpoint) {
|
||||
const mapping = this.definition?.endpoint(this.zh);
|
||||
|
||||
+16
-5
@@ -1,11 +1,8 @@
|
||||
import type {IClientOptions, IClientPublishOptions, MqttClient} from "mqtt";
|
||||
|
||||
import type {Zigbee2MQTTAPI} from "./types/api";
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import type {IClientOptions, IClientPublishOptions, MqttClient} from "mqtt";
|
||||
import {connectAsync} from "mqtt";
|
||||
import type {Zigbee2MQTTAPI} from "./types/api";
|
||||
|
||||
import logger from "./util/logger";
|
||||
import * as settings from "./util/settings";
|
||||
@@ -30,6 +27,20 @@ export default class Mqtt {
|
||||
private defaultPublishOptions: MqttPublishOptions;
|
||||
public retainedMessages: {[s: string]: {topic: string; payload: string; options: MqttPublishOptions}} = {};
|
||||
|
||||
get info() {
|
||||
return {
|
||||
version: this.client.options.protocolVersion,
|
||||
server: `${this.client.options.protocol}://${this.client.options.host}:${this.client.options.port}`,
|
||||
};
|
||||
}
|
||||
|
||||
get stats() {
|
||||
return {
|
||||
connected: this.isConnected(),
|
||||
queued: this.client.queue.length,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(eventBus: EventBus) {
|
||||
this.eventBus = eventBus;
|
||||
this.defaultPublishOptions = {
|
||||
|
||||
+53
-8
@@ -1,7 +1,7 @@
|
||||
import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters";
|
||||
import type {Base} from "zigbee-herdsman-converters/lib/exposes";
|
||||
import type * as zigbeeHerdsman from "zigbee-herdsman/dist";
|
||||
import type {ClusterDefinition, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters";
|
||||
import type {Base} from "zigbee-herdsman-converters/lib/exposes";
|
||||
|
||||
export type * as ZSpec from "zigbee-herdsman/dist/zspec";
|
||||
export type * as Zcl from "zigbee-herdsman/dist/zspec/zcl";
|
||||
@@ -183,6 +183,11 @@ export interface Zigbee2MQTTSettings {
|
||||
output: "json" | "attribute" | "attribute_and_json";
|
||||
transmit_power?: number;
|
||||
};
|
||||
health: {
|
||||
/** in minutes */
|
||||
interval: number;
|
||||
reset_on_check: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Zigbee2MQTTScene {
|
||||
@@ -229,8 +234,8 @@ export interface Zigbee2MQTTDevice {
|
||||
supported: boolean;
|
||||
friendly_name: string;
|
||||
disabled: boolean;
|
||||
description: string | undefined;
|
||||
definition: Zigbee2MQTTDeviceDefinition | undefined;
|
||||
description?: string;
|
||||
definition?: Zigbee2MQTTDeviceDefinition;
|
||||
power_source: zigbeeHerdsman.Models.Device["powerSource"];
|
||||
software_build_id: zigbeeHerdsman.Models.Device["softwareBuildID"];
|
||||
date_code: zigbeeHerdsman.Models.Device["dateCode"];
|
||||
@@ -250,7 +255,7 @@ export interface Zigbee2MQTTGroupMember {
|
||||
export interface Zigbee2MQTTGroup {
|
||||
id: number;
|
||||
friendly_name: "default_bind_group" | string;
|
||||
description: string | undefined;
|
||||
description?: string;
|
||||
scenes: Zigbee2MQTTScene[];
|
||||
members: Zigbee2MQTTGroupMember[];
|
||||
}
|
||||
@@ -261,10 +266,10 @@ export interface Zigbee2MQTTNetworkMap {
|
||||
friendlyName: string;
|
||||
type: string;
|
||||
networkAddress: number;
|
||||
manufacturerName: string | undefined;
|
||||
modelID: string | undefined;
|
||||
manufacturerName?: string;
|
||||
modelID?: string;
|
||||
failed?: string[];
|
||||
lastSeen: number | undefined;
|
||||
lastSeen?: number;
|
||||
definition?: {model: string; vendor: string; supports: string; description: string};
|
||||
}[];
|
||||
links: {
|
||||
@@ -330,6 +335,16 @@ export interface Zigbee2MQTTAPI {
|
||||
};
|
||||
|
||||
"bridge/info": {
|
||||
os: {
|
||||
version: string;
|
||||
node_version: string;
|
||||
cpus: string;
|
||||
memory_mb: number;
|
||||
};
|
||||
mqtt: {
|
||||
version: number | undefined;
|
||||
server: string;
|
||||
};
|
||||
version: string;
|
||||
commit: string | undefined;
|
||||
zigbee_herdsman_converters: {version: string};
|
||||
@@ -355,6 +370,36 @@ export interface Zigbee2MQTTAPI {
|
||||
config_schema: typeof schemaJson;
|
||||
};
|
||||
|
||||
"bridge/health": {
|
||||
/** time of message, msec from epoch, UTC */
|
||||
response_time: number;
|
||||
os: {
|
||||
load_average: number[];
|
||||
memory_used_mb: number;
|
||||
memory_percent: number;
|
||||
};
|
||||
process: {
|
||||
uptime_sec: number;
|
||||
memory_used_mb: number;
|
||||
memory_percent: number;
|
||||
};
|
||||
mqtt: {
|
||||
connected: boolean;
|
||||
queued: number;
|
||||
received: number;
|
||||
published: number;
|
||||
};
|
||||
devices: Record<
|
||||
string /* ieee */,
|
||||
{
|
||||
messages: number;
|
||||
messages_per_sec: number;
|
||||
leave_count: number;
|
||||
network_address_changes: number;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
"bridge/devices": Zigbee2MQTTDevice[];
|
||||
|
||||
"bridge/groups": Zigbee2MQTTGroup[];
|
||||
|
||||
@@ -18,4 +18,5 @@ function _testReload(): void {
|
||||
dataPath = setPath();
|
||||
}
|
||||
|
||||
// biome-ignore lint/style/useNamingConvention: test
|
||||
export default {joinPath, getPath, _testReload};
|
||||
|
||||
@@ -498,7 +498,7 @@ async function startFailureServer(errors: string): Promise<void> {
|
||||
let server: ReturnType<typeof createServer> | undefined;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server = createServer(async (req, res) => {
|
||||
server = createServer((req, res) => {
|
||||
if (req.method === "POST") {
|
||||
res.end(() => {
|
||||
resolve();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {UnixDgramSocket} from "unix-dgram";
|
||||
|
||||
import {platform} from "node:os";
|
||||
import type {UnixDgramSocket} from "unix-dgram";
|
||||
|
||||
import logger from "./logger";
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
},
|
||||
"backoff": {
|
||||
"type": "boolean",
|
||||
"title": "Enabled",
|
||||
"title": "Backoff",
|
||||
"description": "Enable timeout backoff on failed availability pings (x1.5, x3, x6, x12...)",
|
||||
"default": true
|
||||
},
|
||||
@@ -545,7 +545,7 @@
|
||||
"title": "Log Namespaced Levels",
|
||||
"description": "Set individual log levels for certain namespaces",
|
||||
"default": {},
|
||||
"examples": [{ "z2m:mqtt": "warning" }, { "zh:ember:uart:ash": "info" }]
|
||||
"examples": [{"z2m:mqtt": "warning"}, {"zh:ember:uart:ash": "info"}]
|
||||
},
|
||||
"log_debug_to_mqtt_frontend": {
|
||||
"type": "boolean",
|
||||
@@ -770,6 +770,27 @@
|
||||
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"health": {
|
||||
"title": "Health",
|
||||
"description": "Periodically check the health of Zigbee2MQTT",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"interval": {
|
||||
"type": "number",
|
||||
"title": "Interval",
|
||||
"description": "Interval between checks in minutes",
|
||||
"default": 10,
|
||||
"requiresRestart": true
|
||||
},
|
||||
"reset_on_check": {
|
||||
"type": "boolean",
|
||||
"title": "Reset on check",
|
||||
"description": "If true, will reset stats every time the health check is executed (only applicable to stats that can be reset).",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"required": ["mqtt"],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {ValidateFunction} from "ajv";
|
||||
|
||||
import path from "node:path";
|
||||
import type {ValidateFunction} from "ajv";
|
||||
|
||||
import Ajv from "ajv";
|
||||
import objectAssignDeep from "object-assign-deep";
|
||||
@@ -113,6 +112,10 @@ export const defaults = {
|
||||
timestamp_format: "YYYY-MM-DD HH:mm:ss",
|
||||
output: "json",
|
||||
},
|
||||
health: {
|
||||
interval: 10,
|
||||
reset_on_check: false,
|
||||
},
|
||||
} satisfies RecursivePartial<Settings>;
|
||||
|
||||
let _settings: Partial<Settings> | undefined;
|
||||
@@ -596,7 +599,7 @@ export function addGroup(name: string, id?: string): GroupOptions {
|
||||
settings.groups = {};
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
if (id == null || (typeof id === "string" && id.trim() === "")) {
|
||||
// look for free ID
|
||||
id = "1";
|
||||
|
||||
|
||||
+5
-9
@@ -1,15 +1,12 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints, Zigbee2MQTTScene} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
import {exec} from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import equals from "fast-deep-equal/es6";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints, Zigbee2MQTTScene} from "../types/api";
|
||||
|
||||
import data from "./data";
|
||||
|
||||
@@ -87,7 +84,6 @@ function formatDate(time: number, type: "ISO_8601" | "ISO_8601_local" | "epoch"
|
||||
|
||||
function objectIsEmpty(object: object): boolean {
|
||||
// much faster than checking `Object.keys(object).length`
|
||||
// biome-ignore lint/style/useNamingConvention: bad detection
|
||||
for (const _k in object) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -279,9 +275,9 @@ function isZHGroup(obj: unknown): obj is zh.Group {
|
||||
return obj?.constructor.name.toLowerCase() === "group";
|
||||
}
|
||||
|
||||
const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
const seconds = (seconds: number): number => 1000 * seconds;
|
||||
export const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
export const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
export const seconds = (seconds: number): number => 1000 * seconds;
|
||||
|
||||
async function publishLastSeen(
|
||||
data: eventdata.LastSeenChanged,
|
||||
|
||||
+2
-5
@@ -1,12 +1,9 @@
|
||||
import type {Events as ZHEvents} from "zigbee-herdsman";
|
||||
import type {StartResult} from "zigbee-herdsman/dist/adapter/tstype";
|
||||
|
||||
import {randomInt} from "node:crypto";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import type {Events as ZHEvents} from "zigbee-herdsman";
|
||||
import {Controller} from "zigbee-herdsman";
|
||||
import type {StartResult} from "zigbee-herdsman/dist/adapter/tstype";
|
||||
|
||||
import Device from "./model/device";
|
||||
import Group from "./model/group";
|
||||
|
||||
+11
-10
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "2.4.0",
|
||||
"version": "2.5.0",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"types": "dist/types/api.d.ts",
|
||||
"packageManager": "pnpm@10.4.1",
|
||||
"packageManager": "pnpm@10.12.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
@@ -23,8 +23,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && node index.js writehash",
|
||||
"build:types": "pnpm run clean && tsc --project tsconfig.types.json && cp lib/util/settings.schema.json dist/util",
|
||||
"build:watch": "tsc --watch",
|
||||
"check": "biome check",
|
||||
"check": "biome check --error-on-warnings",
|
||||
"check:w": "biome check --write",
|
||||
"start": "node index.js",
|
||||
"test": "vitest run --config ./test/vitest.config.mts",
|
||||
@@ -61,19 +62,19 @@
|
||||
"winston-syslog": "^2.7.1",
|
||||
"winston-transport": "^4.9.0",
|
||||
"ws": "^8.18.1",
|
||||
"zigbee-herdsman": "4.1.0",
|
||||
"zigbee-herdsman-converters": "23.53.0",
|
||||
"zigbee2mqtt-frontend": "0.9.13",
|
||||
"zigbee2mqtt-windfront": "^1.0.2"
|
||||
"zigbee-herdsman": "4.3.0",
|
||||
"zigbee-herdsman-converters": "23.69.0",
|
||||
"zigbee2mqtt-frontend": "0.9.16",
|
||||
"zigbee2mqtt-windfront": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@biomejs/biome": "^2.0.5",
|
||||
"@types/finalhandler": "^1.2.3",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/node": "^24.0.3",
|
||||
"@types/object-assign-deep": "^0.4.3",
|
||||
"@types/readable-stream": "4.0.20",
|
||||
"@types/readable-stream": "4.0.21",
|
||||
"@types/serve-static": "^1.15.7",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
|
||||
Generated
+450
-405
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
import {mockLogger} from "./mocks/logger";
|
||||
import {
|
||||
@@ -9,23 +10,19 @@ import {
|
||||
mockMQTTUnsubscribeAsync,
|
||||
} from "./mocks/mqtt";
|
||||
import {flushPromises} from "./mocks/utils";
|
||||
import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from "./mocks/zigbeeHerdsman";
|
||||
|
||||
import type {Mock, MockInstance} from "vitest";
|
||||
|
||||
import type Device from "../lib/model/device";
|
||||
import type {Device as ZhDevice} from "./mocks/zigbeeHerdsman";
|
||||
import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from "./mocks/zigbeeHerdsman";
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import tmp from "tmp";
|
||||
|
||||
import type {Mock, MockInstance} from "vitest";
|
||||
import {Controller as ZHController} from "zigbee-herdsman";
|
||||
|
||||
import {Controller} from "../lib/controller";
|
||||
import type Device from "../lib/model/device";
|
||||
import * as settings from "../lib/util/settings";
|
||||
|
||||
const LOG_MQTT_NS = "z2m:mqtt";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -5,9 +6,7 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Availability from "../../lib/extension/availability";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockDebounce} from "../mocks/debounce";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
@@ -6,7 +7,6 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {type Device, devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Bind from "../../lib/extension/bind";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockJSZipFile, mockJSZipGenerateAsync} from "../mocks/jszip";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
@@ -5,14 +6,11 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {CUSTOM_CLUSTERS, devices, groups, mockController as mockZHController, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import type {Mock} from "vitest";
|
||||
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import type {Mock} from "vitest";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Bridge from "../../lib/extension/bridge";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
@@ -41,6 +39,19 @@ const mocksClear = [
|
||||
|
||||
const deviceIconsDir = path.join(data.mockDir, "device_icons");
|
||||
|
||||
vi.mock("node:os", async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
version: vi.fn(() => "Linux"),
|
||||
release: vi.fn(() => "0.0.1"),
|
||||
arch: vi.fn(() => "x64"),
|
||||
cpus: vi.fn(() => [{model: "Intel Core i7-9999"}]),
|
||||
totalmem: vi.fn(() => 10485760),
|
||||
}));
|
||||
vi.mock("node:process", async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
version: "v1.2.3",
|
||||
}));
|
||||
|
||||
describe("Extension: Bridge", () => {
|
||||
let controller: Controller;
|
||||
let mockRestart: Mock;
|
||||
@@ -94,12 +105,16 @@ describe("Extension: Bridge", () => {
|
||||
const zhVersion = await utils.getDependencyVersion("zigbee-herdsman");
|
||||
const zhcVersion = await utils.getDependencyVersion("zigbee-herdsman-converters");
|
||||
const directory = settings.get().advanced.log_directory;
|
||||
// console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')![1]);
|
||||
// console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "zigbee2mqtt/bridge/info")![1]);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/info",
|
||||
stringify({
|
||||
commit: version.commitHash,
|
||||
config: {
|
||||
health: {
|
||||
interval: 10,
|
||||
reset_on_check: false,
|
||||
},
|
||||
advanced: {
|
||||
adapter_concurrent: undefined,
|
||||
adapter_delay: undefined,
|
||||
@@ -315,6 +330,16 @@ describe("Extension: Bridge", () => {
|
||||
version: version.version,
|
||||
zigbee_herdsman: zhVersion,
|
||||
zigbee_herdsman_converters: zhcVersion,
|
||||
os: {
|
||||
version: "Linux - 0.0.1 - x64",
|
||||
node_version: "v1.2.3",
|
||||
cpus: "Intel Core i7-9999 (x1)",
|
||||
memory_mb: 10,
|
||||
},
|
||||
mqtt: {
|
||||
server: "mqtt://localhost:1883",
|
||||
version: 5,
|
||||
},
|
||||
}),
|
||||
{retain: true},
|
||||
);
|
||||
@@ -3939,7 +3964,7 @@ describe("Extension: Bridge", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("Icon link handling", async () => {
|
||||
it("Icon link handling", () => {
|
||||
const bridge = controller.getExtension("Bridge")! as Bridge;
|
||||
expect(bridge).toBeDefined();
|
||||
|
||||
@@ -3970,14 +3995,14 @@ describe("Extension: Bridge", () => {
|
||||
expect(payload.icon).not.toBeUndefined();
|
||||
expect(payload.icon).toBe(svg_icon);
|
||||
|
||||
definition.icon = "_${model}_";
|
||||
definition.icon = "_$model_";
|
||||
// @ts-expect-error bare minimum mock
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
|
||||
assert(payload);
|
||||
expect(payload.icon).not.toBeUndefined();
|
||||
expect(payload.icon).toBe("_lumi.plug_");
|
||||
|
||||
definition.icon = "_${model}_${zigbeeModel}_";
|
||||
definition.icon = "_$model_$zigbeeModel_";
|
||||
// @ts-expect-error bare minimum mock
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
|
||||
assert(payload);
|
||||
@@ -3993,7 +4018,7 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
device.modelID = "?._Z\\NC+Z02*LM";
|
||||
definition.model = "&&&&*+";
|
||||
definition.icon = "_${model}_${zigbeeModel}_";
|
||||
definition.icon = "_$model_$zigbeeModel_";
|
||||
// @ts-expect-error bare minimum mock
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
|
||||
assert(payload);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {type Device, type Endpoint, devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
import {type Device, devices, type Endpoint, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Configure from "../../lib/extension/configure";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTEndAsync, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {devices, mockController as mockZHController, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import type Device from "../../lib/model/device";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
import {devices, mockController as mockZHController, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import ExternalConverters from "../../lib/extension/externalConverters";
|
||||
import type Device from "../../lib/model/device";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
const BASE_DIR = "external_converters";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTEndAsync, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,9 +7,7 @@ import {devices, mockController as mockZHController, returnDevices} from "../moc
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import ExternalExtensions from "../../lib/extension/externalExtensions";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -5,11 +6,9 @@ import {type EventHandler, flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Mock} from "vitest";
|
||||
import ws from "ws";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -23,7 +22,6 @@ const mockHTTP = {
|
||||
close: vi.fn<(cb: (err?: Error) => void) => void>((cb) => cb()),
|
||||
};
|
||||
|
||||
let mockHTTPSOnRequest: (request: {url: string}, response: number) => void;
|
||||
const mockHTTPSEvents: Record<string, EventHandler> = {};
|
||||
const mockHTTPS = {
|
||||
listen: vi.fn(),
|
||||
@@ -56,7 +54,7 @@ const mockWS = {
|
||||
handleUpgrade: vi.fn().mockImplementation((_request, _socket, _head, cb) => {
|
||||
cb(mockWSocket);
|
||||
}),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
emit: vi.fn<(eventName: string, ...args: any[]) => void>(),
|
||||
close: vi.fn<(code?: number, data?: string | Buffer) => void>(),
|
||||
};
|
||||
@@ -76,10 +74,7 @@ vi.mock("node:http", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("node:https", () => ({
|
||||
createServer: vi.fn().mockImplementation((onRequest) => {
|
||||
mockHTTPSOnRequest = onRequest;
|
||||
return mockHTTPS;
|
||||
}),
|
||||
createServer: vi.fn().mockImplementation(() => mockHTTPS),
|
||||
Agent: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -5,9 +6,7 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhcGlobalStore from "zigbee-herdsman-converters/lib/store";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Health from "../../lib/extension/health";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
import {minutes, seconds} from "../../lib/util/utils";
|
||||
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.info];
|
||||
|
||||
returnDevices.push(devices.bulb_color.ieeeAddr, devices.bulb_color_2.ieeeAddr, devices.coordinator.ieeeAddr);
|
||||
|
||||
describe("Extension: Health", () => {
|
||||
let controller: Controller;
|
||||
|
||||
const getExtension = (): Health => controller.getExtension("Health") as Health;
|
||||
|
||||
const resetExtension = async (): Promise<void> => {
|
||||
await controller.removeExtension(getExtension());
|
||||
await controller.addExtension(new Health(...controller.extensionArgs));
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.useFakeTimers();
|
||||
settings.reRead();
|
||||
|
||||
controller = new Controller(vi.fn(), vi.fn());
|
||||
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
settings.set(["devices", devices.bulb_color_2.ieeeAddr, "health"], false);
|
||||
|
||||
for (const mock of mocksClear) {
|
||||
mock.mockClear();
|
||||
}
|
||||
|
||||
getExtension().clearStats();
|
||||
});
|
||||
|
||||
afterEach(async () => {});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller?.stop();
|
||||
await flushPromises();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("checks health at default interval", async () => {
|
||||
await resetExtension();
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2});
|
||||
await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr});
|
||||
await mockZHEvents.deviceJoined({device: devices.bulb_color});
|
||||
await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(seconds(1));
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(seconds(1));
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(seconds(1));
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await mockMQTTEvents.message("zigbee2mqtt/mock", "mocked");
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
let calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 1,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 1,
|
||||
messages: 4,
|
||||
messages_per_sec: 0.0067,
|
||||
network_address_changes: 1,
|
||||
},
|
||||
[devices.bulb_color_2.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 1,
|
||||
messages_per_sec: 0.0017,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
await mockMQTTEvents.message("zigbee2mqtt/mock2", "mocked2");
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 2,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 1,
|
||||
messages: 4,
|
||||
messages_per_sec: 0.0033,
|
||||
network_address_changes: 1,
|
||||
},
|
||||
[devices.bulb_color_2.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 1,
|
||||
messages_per_sec: 0.0008,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("checks health at given interval", async () => {
|
||||
settings.set(["health", "interval"], 20);
|
||||
await resetExtension();
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
let calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(0);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(minutes(10));
|
||||
|
||||
calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("init device health from leave", async () => {
|
||||
await resetExtension();
|
||||
await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr});
|
||||
await mockZHEvents.deviceJoined({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
const calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 1,
|
||||
messages: 0,
|
||||
messages_per_sec: 0,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("init device health from network address change", async () => {
|
||||
await resetExtension();
|
||||
await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
const calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 0,
|
||||
messages_per_sec: 0,
|
||||
network_address_changes: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("checks health then resets possible stats", async () => {
|
||||
settings.set(["health", "reset_on_check"], true);
|
||||
await resetExtension();
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); // coverage no time diff first/last
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2});
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
let calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 2,
|
||||
messages_per_sec: 0.0033,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
[devices.bulb_color_2.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 1,
|
||||
messages_per_sec: 0.0017,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,22 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync, mockMQTTSubscribeAsync, mockMQTTUnsubscribeAsync} from "../mocks/mqtt";
|
||||
import * as mockSleep from "../mocks/sleep";
|
||||
import {flushPromises, getZhcBaseDefinitions} from "../mocks/utils";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {MockInstance} from "vitest";
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import type {KeyValueAny} from "zigbee-herdsman-converters/lib/types";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import HomeAssistant from "../../lib/extension/homeassistant";
|
||||
|
||||
import type Device from "../../lib/model/device";
|
||||
import type Group from "../../lib/model/group";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import type {KeyValueAny} from "zigbee-herdsman-converters/lib/types";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import HomeAssistant from "../../lib/extension/homeassistant";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.debug, mockLogger.warning, mockLogger.error];
|
||||
@@ -921,7 +918,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Should discover devices with speed-controlled fan", async () => {
|
||||
it("Should discover devices with speed-controlled fan", () => {
|
||||
const payload = {
|
||||
state_topic: "zigbee2mqtt/fanbee",
|
||||
state_value_template: "{{ value_json.state }}",
|
||||
@@ -1564,7 +1561,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
await flushPromises();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/health", expect.any(String), expect.any(Object));
|
||||
});
|
||||
|
||||
it("Shouldnt send all status when home assistant comes online with different topic", async () => {
|
||||
@@ -1577,7 +1575,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
await flushPromises();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/health", expect.any(String), expect.any(Object));
|
||||
});
|
||||
|
||||
it("Should discover devices with availability", async () => {
|
||||
@@ -2456,7 +2455,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
stringify(payload),
|
||||
{retain: true, qos: 1},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(6);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(7);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/health", expect.any(String), expect.any(Object));
|
||||
});
|
||||
|
||||
it("Should not clear bridge entities unnecessarily", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -7,9 +8,7 @@ import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHer
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import type {MockInstance} from "vitest";
|
||||
import type {OnEvent as DefinitionOnEvent} from "zigbee-herdsman-converters/lib/types";
|
||||
|
||||
import type Device from "../../lib/model/device";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {OnEvent as DefinitionOnEvent} from "zigbee-herdsman-converters/lib/types";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import OnEvent from "../../lib/extension/onEvent";
|
||||
import type Device from "../../lib/model/device";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,11 +7,8 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import OTAUpdate from "../../lib/extension/otaUpdate";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,9 +7,7 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {clearGlobalStore} from "zigbee-herdsman-converters";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import {loadTopicGetSetRegex} from "../../lib/extension/publish";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,7 +7,6 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -183,10 +183,11 @@ describe("Extension: Receive", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should debounce and retain messages when set via device_options", async () => {
|
||||
@@ -229,10 +230,11 @@ describe("Extension: Receive", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should debounce messages only with the same payload values for provided debounce_ignore keys", async () => {
|
||||
@@ -281,8 +283,9 @@ describe("Extension: Receive", () => {
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2});
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03});
|
||||
expect(mockMQTTPublishAsync.mock.calls[2][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should NOT publish old messages from State cache during debouncing", async () => {
|
||||
@@ -322,9 +325,10 @@ describe("Extension: Receive", () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
// Test that only one MQTT is sent out and test its values.
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1 + 1 /* "bridge/health" */);
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
|
||||
// Send another Zigbee message...
|
||||
await mockZHEvents.message({
|
||||
@@ -343,12 +347,14 @@ describe("Extension: Receive", () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
// Total of 3 messages should have triggered.
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
// Test that message pushed by asynchronous message contains NEW measurement and not old.
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
// Test that messages after debouncing contains NEW measurement and not old.
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
// Test that messages after debouncing contains NEW measurement and not old.
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[3][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[4][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should throttle multiple messages from spamming devices", async () => {
|
||||
@@ -439,9 +445,10 @@ describe("Extension: Receive", () => {
|
||||
await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify({state: "ON"}));
|
||||
await flushPromises();
|
||||
vi.runOnlyPendingTimers();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2 + 1);
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON"});
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON"});
|
||||
expect(mockMQTTPublishAsync.mock.calls[2][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should handle a zigbee message with 1 precision", async () => {
|
||||
|
||||
+2
-4
@@ -1,15 +1,13 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import type {MockInstance} from "vitest";
|
||||
|
||||
import fs from "node:fs";
|
||||
import {platform} from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {rimrafSync} from "rimraf";
|
||||
import tmp from "tmp";
|
||||
import type {MockInstance} from "vitest";
|
||||
import Transport from "winston-transport";
|
||||
|
||||
import logger from "../lib/util/logger";
|
||||
import * as settings from "../lib/util/settings";
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ export const mockMQTTConnectAsync = vi.fn(() => ({
|
||||
events[type] = handler;
|
||||
}),
|
||||
stream: {setMaxListeners: vi.fn()},
|
||||
options: {
|
||||
protocolVersion: 5,
|
||||
protocol: "mqtt",
|
||||
host: "localhost",
|
||||
port: 1883,
|
||||
},
|
||||
queue: [],
|
||||
}));
|
||||
|
||||
vi.mock("mqtt", () => ({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import assert from "node:assert";
|
||||
import type {Mock} from "vitest";
|
||||
import type {AdapterTypes} from "zigbee-herdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import type {IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, RequestListener, Server, ServerResponse} from "node:http";
|
||||
|
||||
import {rmSync} from "node:fs";
|
||||
|
||||
import type {IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, RequestListener, Server, ServerResponse} from "node:http";
|
||||
import type {findAllDevices} from "zigbee-herdsman/dist/adapter/adapterDiscovery";
|
||||
|
||||
import {onboard} from "../lib/util/onboarding";
|
||||
import * as settings from "../lib/util/settings";
|
||||
|
||||
@@ -188,10 +187,10 @@ describe("Onboarding", () => {
|
||||
expectWriteMinimal: boolean,
|
||||
expectFailure: boolean,
|
||||
): Promise<[getHtml: string, postHtml: string]> => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
const reqDataListener = vi.fn<(chunk: any) => void>();
|
||||
const reqEndListener = vi.fn<() => void>();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
const resEnd = vi.fn<(chunk: any | (() => void), cb?: () => void) => ServerResponse<IncomingMessage>>(
|
||||
// @ts-expect-error return not used
|
||||
(chunk, cb) => {
|
||||
@@ -299,7 +298,7 @@ describe("Onboarding", () => {
|
||||
};
|
||||
|
||||
const runFailure = async (): Promise<string> => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
const resEnd = vi.fn<(chunk: any | (() => void), cb?: () => void) => ServerResponse<IncomingMessage>>(
|
||||
// @ts-expect-error return not used
|
||||
(chunk, cb) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {mockLogger} from "./mocks/logger";
|
||||
|
||||
import {initSdNotify} from "../lib/util/sd-notify";
|
||||
|
||||
+37
-36
@@ -506,7 +506,7 @@ describe("Settings", () => {
|
||||
it("Should read groups", () => {
|
||||
const content = {
|
||||
groups: {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
},
|
||||
},
|
||||
@@ -515,18 +515,17 @@ describe("Settings", () => {
|
||||
write(configurationFile, content);
|
||||
|
||||
const group = settings.getGroup("1");
|
||||
const expected = {
|
||||
|
||||
expect(group).toStrictEqual({
|
||||
ID: 1,
|
||||
friendly_name: "123",
|
||||
};
|
||||
|
||||
expect(group).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it("Throw if removing non-existing group", () => {
|
||||
const content = {
|
||||
groups: {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
},
|
||||
},
|
||||
@@ -543,7 +542,7 @@ describe("Settings", () => {
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
},
|
||||
};
|
||||
@@ -552,12 +551,11 @@ describe("Settings", () => {
|
||||
write(groupsFile, contentGroups);
|
||||
|
||||
const group = settings.getGroup("1");
|
||||
const expected = {
|
||||
|
||||
expect(group).toStrictEqual({
|
||||
ID: 1,
|
||||
friendly_name: "123",
|
||||
};
|
||||
|
||||
expect(group).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it("Combine everything! groups and devices from separate file :)", () => {
|
||||
@@ -567,9 +565,8 @@ describe("Settings", () => {
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
devices: [],
|
||||
},
|
||||
};
|
||||
write(configurationFile, contentConfiguration);
|
||||
@@ -586,44 +583,48 @@ describe("Settings", () => {
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
|
||||
|
||||
const expectedDevice = {
|
||||
expect(read(devicesFile)).toStrictEqual({
|
||||
"0x1234": {
|
||||
friendly_name: "0x1234",
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(devicesFile)).toStrictEqual(expectedDevice);
|
||||
});
|
||||
|
||||
const group = settings.getGroup("1");
|
||||
const expectedGroup = {
|
||||
|
||||
expect(group).toStrictEqual({
|
||||
ID: 1,
|
||||
friendly_name: "123",
|
||||
devices: [],
|
||||
};
|
||||
|
||||
expect(group).toStrictEqual(expectedGroup);
|
||||
});
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
|
||||
|
||||
const expectedDevice2 = {
|
||||
expect(settings.getDevice("0x1234")).toStrictEqual({
|
||||
ID: "0x1234",
|
||||
friendly_name: "0x1234",
|
||||
};
|
||||
|
||||
expect(settings.getDevice("0x1234")).toStrictEqual(expectedDevice2);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should add groups", () => {
|
||||
write(configurationFile, {});
|
||||
|
||||
settings.addGroup("test123");
|
||||
const expected = {
|
||||
1: {
|
||||
|
||||
expect(settings.get().groups).toStrictEqual({
|
||||
"1": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(settings.get().groups).toStrictEqual(expected);
|
||||
settings.addGroup("test456", "");
|
||||
|
||||
expect(settings.get().groups).toStrictEqual({
|
||||
"1": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
"2": {
|
||||
friendly_name: "test456",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Should allow username without password", () => {
|
||||
@@ -650,7 +651,7 @@ describe("Settings", () => {
|
||||
|
||||
settings.addGroup("test123", "123");
|
||||
const expected = {
|
||||
123: {
|
||||
"123": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
@@ -682,7 +683,7 @@ describe("Settings", () => {
|
||||
settings.addGroup("test123");
|
||||
}).toThrow(`friendly_name 'test123' is already in use`);
|
||||
const expected = {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
@@ -698,7 +699,7 @@ describe("Settings", () => {
|
||||
settings.addGroup("test_id_123", "123");
|
||||
}).toThrow(new Error("Group ID '123' is already in use"));
|
||||
const expected = {
|
||||
123: {
|
||||
"123": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
@@ -835,7 +836,7 @@ describe("Settings", () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {"0x0017880104e45519": {friendly_name: "myname", retain: false}},
|
||||
groups: {1: {friendly_name: "myname", retain: false}},
|
||||
groups: {"1": {friendly_name: "myname", retain: false}},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -924,7 +925,7 @@ describe("Settings", () => {
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it("Configuration shouldnt be valid when duplicate friendly_name are used", async () => {
|
||||
it("Configuration shouldnt be valid when duplicate friendly_name are used", () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
"0x0017880104e45519": {friendly_name: "myname", retain: false},
|
||||
@@ -939,7 +940,7 @@ describe("Settings", () => {
|
||||
}).toThrowError(`friendly_name 'myname' is already in use`);
|
||||
});
|
||||
|
||||
it("Should throw when removing device which doesnt exist", async () => {
|
||||
it("Should throw when removing device which doesnt exist", () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
"0x0017880104e45519": {friendly_name: "myname", retain: false},
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// side-effect ensures using mock paths
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import {existsSync, readFileSync, rmSync} from "node:fs";
|
||||
|
||||
import objectAssignDeep from "object-assign-deep";
|
||||
|
||||
import mockedData from "../lib/util/data";
|
||||
import * as settings from "../lib/util/settings";
|
||||
import * as settingsMigration from "../lib/util/settingsMigration";
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@
|
||||
"rootDir": "..",
|
||||
"noEmit": true
|
||||
},
|
||||
"references": [{ "path": ".." }]
|
||||
"references": [{"path": ".."}]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user