mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 15:18:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bdc3fd1f7 | ||
|
|
376554943d | ||
|
|
9fcc41b199 | ||
|
|
fe5639ce40 | ||
|
|
0182c4b32e | ||
|
|
c12f07c5a0 | ||
|
|
31d3e19f38 | ||
|
|
ee1c884031 | ||
|
|
e5f677bdd1 | ||
|
|
6dd812f71c | ||
|
|
71cf2b4160 | ||
|
|
e9f2a3f532 | ||
|
|
0725374db7 | ||
|
|
6f7c6eec04 | ||
|
|
29e56adc15 | ||
|
|
caf91cd7ff | ||
|
|
6432571c61 | ||
|
|
713551b525 | ||
|
|
9a215efe26 | ||
|
|
65eef1b690 | ||
|
|
81bafa5520 | ||
|
|
d322dc2047 | ||
|
|
5f9019a95f | ||
|
|
4bc6a59ff3 | ||
|
|
e505a93a40 | ||
|
|
bc74d0b11b | ||
|
|
cc6f7b02bc | ||
|
|
74e9357a59 | ||
|
|
8b876823da | ||
|
|
db20369447 | ||
|
|
0e0a5618af | ||
|
|
21fa5cbded | ||
|
|
e2b7df9f84 | ||
|
|
ef07505b31 | ||
|
|
435a6cceb9 | ||
|
|
a51216e3c7 | ||
|
|
6ede403688 | ||
|
|
ecadda45e7 | ||
|
|
a837702b2c | ||
|
|
57e04bb547 | ||
|
|
ffdd353e16 | ||
|
|
0215e76118 | ||
|
|
833605b71f | ||
|
|
f4d821deca | ||
|
|
c465ef8352 | ||
|
|
0dd4b6a991 | ||
|
|
1b37baefd3 | ||
|
|
b4a9424d60 | ||
|
|
93c1824b1d | ||
|
|
8ceb08704f | ||
|
|
eb82973067 | ||
|
|
a3e7f69b3f | ||
|
|
373fa43136 | ||
|
|
e04732462c | ||
|
|
e663e86e09 | ||
|
|
163a15d664 | ||
|
|
4afdd4e892 | ||
|
|
190cfe99ac | ||
|
|
c9284f6b33 |
@@ -69,6 +69,12 @@ body:
|
||||
placeholder: Add-on on Home Assistant OS on Intel NUC, Plain on Docker container, ...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device `database.db` entry
|
||||
description: If the problem is related to a specific device, paste the `database.db` entry here
|
||||
placeholder: '{"id":117,"type":"EndDevice","ieeeAddr":"0x00158d0001d8e1d2","nwkAddr":25887 ...'
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
versioning-strategy: increase
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -14,7 +15,7 @@ updates:
|
||||
- minor
|
||||
- patch
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
directory: /docker
|
||||
schedule:
|
||||
interval: weekly
|
||||
target-branch: dev
|
||||
|
||||
@@ -3,9 +3,10 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
branches-ignore:
|
||||
- dev-types
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
@@ -47,6 +48,13 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm run test:coverage
|
||||
|
||||
- name: Bench
|
||||
if: github.ref == 'refs/heads/dev' || (github.event_name == 'pull_request' && !startsWith(github.head_ref, 'release-please-'))
|
||||
uses: CodSpeedHQ/action@v3
|
||||
with:
|
||||
run: pnpm run bench
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
- name: Log in to the Docker container registry
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -77,6 +77,7 @@ tsconfig.types.tsbuildinfo
|
||||
# Ignore config
|
||||
data/*
|
||||
!data/configuration.example.yaml
|
||||
data-bench
|
||||
|
||||
# commit-user-lookup.json
|
||||
scripts/commit-user-lookup.json
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.5.1"
|
||||
".": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## [2.6.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.5.1...2.6.0) (2025-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Allow to disable internal frontend UI serving for standalone serving ([#27851](https://github.com/Koenkk/zigbee2mqtt/issues/27851)) ([0215e76](https://github.com/Koenkk/zigbee2mqtt/commit/0215e76118c8dcbe77c1ed4890846aaf7da85bae))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* erroneous Health OS memory percent calculation ([#27892](https://github.com/Koenkk/zigbee2mqtt/issues/27892)) ([190cfe9](https://github.com/Koenkk/zigbee2mqtt/commit/190cfe99ac62b9dc6db4dc59a4389afda85e8177))
|
||||
* Exit with code 2 when adapters disconnects ([#28013](https://github.com/Koenkk/zigbee2mqtt/issues/28013)) ([0e0a561](https://github.com/Koenkk/zigbee2mqtt/commit/0e0a5618af9a1b5707ddccbca2cde983cafb40b5))
|
||||
* Fix settings schema defaults ([#27961](https://github.com/Koenkk/zigbee2mqtt/issues/27961)) ([ecadda4](https://github.com/Koenkk/zigbee2mqtt/commit/ecadda45e7c5665f9e242c956f5f140b7caa6eed))
|
||||
* Ignore symbolic links in `data` during backup ([#28132](https://github.com/Koenkk/zigbee2mqtt/issues/28132)) ([0182c4b](https://github.com/Koenkk/zigbee2mqtt/commit/0182c4b32efafc85d1f4a38840345944501feaef))
|
||||
* **ignore:** add test to specifically check HA envs ([#27969](https://github.com/Koenkk/zigbee2mqtt/issues/27969)) ([833605b](https://github.com/Koenkk/zigbee2mqtt/commit/833605b71f110b4e7aeb6632694820cdd1110e25))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#27924](https://github.com/Koenkk/zigbee2mqtt/issues/27924)) ([8ceb087](https://github.com/Koenkk/zigbee2mqtt/commit/8ceb08704f7e5d637b661c745c804beebf38eed9))
|
||||
* **ignore:** bump the minor-patch group with 4 updates ([#28046](https://github.com/Koenkk/zigbee2mqtt/issues/28046)) ([e505a93](https://github.com/Koenkk/zigbee2mqtt/commit/e505a93a4036affe9e531b24e29ee543ed4cb0f5))
|
||||
* **ignore:** fix 71cf2b41603a9923024c0472ffc1f7bfe3d14fc7 ([6dd812f](https://github.com/Koenkk/zigbee2mqtt/commit/6dd812f71cf9ca6737777c3d4acfab35c52a76b8))
|
||||
* **ignore:** fix breaking changes of https://github.com/Koenkk/zigbee2mqtt/pull/28077 ([713551b](https://github.com/Koenkk/zigbee2mqtt/commit/713551b5257f8624ed03f7901f0addf6864faf1f))
|
||||
* **ignore:** Remove retain/qos from mqtt publish logging https://github.com/Koenkk/zigbee2mqtt/pull/27983 ([e2b7df9](https://github.com/Koenkk/zigbee2mqtt/commit/e2b7df9f84550aa413f3453bb97a13ba2eee4187))
|
||||
* **ignore:** update zigbee-herdsman to 4.3.2 ([#27900](https://github.com/Koenkk/zigbee2mqtt/issues/27900)) ([4afdd4e](https://github.com/Koenkk/zigbee2mqtt/commit/4afdd4e89241eddeb27e5f6c1c7b6604ce4a41cb))
|
||||
* **ignore:** update zigbee-herdsman to 4.4.0 ([#27906](https://github.com/Koenkk/zigbee2mqtt/issues/27906)) ([e663e86](https://github.com/Koenkk/zigbee2mqtt/commit/e663e86e09cf86078877407e2233447468d66899))
|
||||
* **ignore:** update zigbee-herdsman to 4.4.1 ([#27918](https://github.com/Koenkk/zigbee2mqtt/issues/27918)) ([eb82973](https://github.com/Koenkk/zigbee2mqtt/commit/eb829730678dad354d3feede2155f142d58a140f))
|
||||
* **ignore:** update zigbee-herdsman to 4.5.0 ([#28044](https://github.com/Koenkk/zigbee2mqtt/issues/28044)) ([cc6f7b0](https://github.com/Koenkk/zigbee2mqtt/commit/cc6f7b02bc2d7548f142ec162696eae06027d6a3))
|
||||
* **ignore:** update zigbee-herdsman to 5.0.0 ([#28077](https://github.com/Koenkk/zigbee2mqtt/issues/28077)) ([9a215ef](https://github.com/Koenkk/zigbee2mqtt/commit/9a215efe26be6fafde0f0a779b651ded860cd90c))
|
||||
* **ignore:** update zigbee-herdsman to 5.0.1 ([#28080](https://github.com/Koenkk/zigbee2mqtt/issues/28080)) ([6432571](https://github.com/Koenkk/zigbee2mqtt/commit/6432571c61b6e2d4bcc17cc952eeacdaf72213f6))
|
||||
* **ignore:** update zigbee-herdsman to 5.0.2 ([#28110](https://github.com/Koenkk/zigbee2mqtt/issues/28110)) ([e9f2a3f](https://github.com/Koenkk/zigbee2mqtt/commit/e9f2a3f5326954f361199bd4d73ed944f72f6ebe))
|
||||
* **ignore:** update zigbee-herdsman to 5.0.3 ([#28121](https://github.com/Koenkk/zigbee2mqtt/issues/28121)) ([e5f677b](https://github.com/Koenkk/zigbee2mqtt/commit/e5f677bdd1cc3df4d05dd1f8164220ae2258f081))
|
||||
* **ignore:** update zigbee-herdsman to 5.0.4 ([#28141](https://github.com/Koenkk/zigbee2mqtt/issues/28141)) ([9fcc41b](https://github.com/Koenkk/zigbee2mqtt/commit/9fcc41b199082e47d96de4cad996c55c39da83a5))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.70.0 ([#27905](https://github.com/Koenkk/zigbee2mqtt/issues/27905)) ([163a15d](https://github.com/Koenkk/zigbee2mqtt/commit/163a15d664432631f100814927647b918563d85b))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.70.1 ([#27916](https://github.com/Koenkk/zigbee2mqtt/issues/27916)) ([e047324](https://github.com/Koenkk/zigbee2mqtt/commit/e04732462c218cd42c2b617aefba37a5f61d1ce6))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.71.0 ([#27931](https://github.com/Koenkk/zigbee2mqtt/issues/27931)) ([93c1824](https://github.com/Koenkk/zigbee2mqtt/commit/93c1824b1d81c997f0e5a445acac421300f55758))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.72.0 ([#27943](https://github.com/Koenkk/zigbee2mqtt/issues/27943)) ([b4a9424](https://github.com/Koenkk/zigbee2mqtt/commit/b4a9424d604c47609071ef33e88ee55a55a6c8d5))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.0.0 ([#27946](https://github.com/Koenkk/zigbee2mqtt/issues/27946)) ([c465ef8](https://github.com/Koenkk/zigbee2mqtt/commit/c465ef83526c434cac6555836d82e7707072b754))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.1.0 ([#27973](https://github.com/Koenkk/zigbee2mqtt/issues/27973)) ([a837702](https://github.com/Koenkk/zigbee2mqtt/commit/a837702b2c1f517dcfa3a0f578d9b5b3700b171d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.10.0 ([#28122](https://github.com/Koenkk/zigbee2mqtt/issues/28122)) ([ee1c884](https://github.com/Koenkk/zigbee2mqtt/commit/ee1c884031fdef516393ce68754a9dbcc8ad093d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.11.0 ([#28143](https://github.com/Koenkk/zigbee2mqtt/issues/28143)) ([3765549](https://github.com/Koenkk/zigbee2mqtt/commit/376554943d15c45a15d30713002d825286204581))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.2.0 ([#27986](https://github.com/Koenkk/zigbee2mqtt/issues/27986)) ([6ede403](https://github.com/Koenkk/zigbee2mqtt/commit/6ede4036886066bf91953b13714a16ba96217b6d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.3.0 ([#28015](https://github.com/Koenkk/zigbee2mqtt/issues/28015)) ([21fa5cb](https://github.com/Koenkk/zigbee2mqtt/commit/21fa5cbdeddb51d55364c4d28b887791384a951b))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.4.0 ([#28022](https://github.com/Koenkk/zigbee2mqtt/issues/28022)) ([74e9357](https://github.com/Koenkk/zigbee2mqtt/commit/74e9357a5989c6b1eeb25aa91c0da27b7b54fa67))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.5.0 ([#28045](https://github.com/Koenkk/zigbee2mqtt/issues/28045)) ([bc74d0b](https://github.com/Koenkk/zigbee2mqtt/commit/bc74d0b11b58bb2cb2bcc07f5ae9c47b1ca7a13e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.6.0 ([#28051](https://github.com/Koenkk/zigbee2mqtt/issues/28051)) ([4bc6a59](https://github.com/Koenkk/zigbee2mqtt/commit/4bc6a59ff38f9a102259afbff2606a75409042a3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.7.0 ([#28070](https://github.com/Koenkk/zigbee2mqtt/issues/28070)) ([81bafa5](https://github.com/Koenkk/zigbee2mqtt/commit/81bafa55201d6d7a34b364a85c19423a56eceeee))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.8.0 ([#28081](https://github.com/Koenkk/zigbee2mqtt/issues/28081)) ([caf91cd](https://github.com/Koenkk/zigbee2mqtt/commit/caf91cd7ffb8ae4961c3f43875f051b182f1a379))
|
||||
* **ignore:** update zigbee-herdsman-converters to 24.9.0 ([71cf2b4](https://github.com/Koenkk/zigbee2mqtt/commit/71cf2b41603a9923024c0472ffc1f7bfe3d14fc7))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.17 ([#27917](https://github.com/Koenkk/zigbee2mqtt/issues/27917)) ([a3e7f69](https://github.com/Koenkk/zigbee2mqtt/commit/a3e7f69b3ff6173a59b4d5e2ad1f09c2c414b416))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.19 ([#28021](https://github.com/Koenkk/zigbee2mqtt/issues/28021)) ([8b87682](https://github.com/Koenkk/zigbee2mqtt/commit/8b876823da7fbde4f980fefa54cec7c3fa52c423))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.3.0 ([#27953](https://github.com/Koenkk/zigbee2mqtt/issues/27953)) ([1b37bae](https://github.com/Koenkk/zigbee2mqtt/commit/1b37baefd3e6817fc9dbf36c9ec9e8aec4481643))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.3.1 ([#27999](https://github.com/Koenkk/zigbee2mqtt/issues/27999)) ([a51216e](https://github.com/Koenkk/zigbee2mqtt/commit/a51216e3c76335b66a9383b1734a213ad948d70a))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.4.1 ([#28062](https://github.com/Koenkk/zigbee2mqtt/issues/28062)) ([d322dc2](https://github.com/Koenkk/zigbee2mqtt/commit/d322dc20479faffa8f923e2190c1a5f82d17d5e4))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.5.0 ([#28082](https://github.com/Koenkk/zigbee2mqtt/issues/28082)) ([29e56ad](https://github.com/Koenkk/zigbee2mqtt/commit/29e56adc159268038727d955135d3edbedbaae8b))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.5.1 ([#28123](https://github.com/Koenkk/zigbee2mqtt/issues/28123)) ([31d3e19](https://github.com/Koenkk/zigbee2mqtt/commit/31d3e19f3862b067555bcbc9bbcd3a5ded6df211))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.5.2 ([#28140](https://github.com/Koenkk/zigbee2mqtt/issues/28140)) ([fe5639c](https://github.com/Koenkk/zigbee2mqtt/commit/fe5639ce40cf0a4035cd996a558c952dc065fcd0))
|
||||
* Improve startup settings validation ([#27988](https://github.com/Koenkk/zigbee2mqtt/issues/27988)) ([435a6cc](https://github.com/Koenkk/zigbee2mqtt/commit/435a6cceb9aea1ac6941c99603d7785ef91e274a))
|
||||
* log error message when external JS fails to load ([#27837](https://github.com/Koenkk/zigbee2mqtt/issues/27837)) ([f4d821d](https://github.com/Koenkk/zigbee2mqtt/commit/f4d821deca71540f830f38f631d1a3759c1d0f40))
|
||||
* Publish definition source in `bridge/devices` ([#28076](https://github.com/Koenkk/zigbee2mqtt/issues/28076)) ([65eef1b](https://github.com/Koenkk/zigbee2mqtt/commit/65eef1b6900afdf831c809047ab68e93df59b4b0))
|
||||
* Publish endpoint names in `bridge/devices` ([#28057](https://github.com/Koenkk/zigbee2mqtt/issues/28057)) ([5f9019a](https://github.com/Koenkk/zigbee2mqtt/commit/5f9019a95fb460f1dc4a31d09e2424ff56049c14))
|
||||
* Set MQTT retain and qos=1 on (re)connect ([#27983](https://github.com/Koenkk/zigbee2mqtt/issues/27983)) ([ef07505](https://github.com/Koenkk/zigbee2mqtt/commit/ef07505b31782936a3ae517688c7e1eb9e541daf))
|
||||
* Various fixes to align behaviors (+add benchmarks for dev) ([#28011](https://github.com/Koenkk/zigbee2mqtt/issues/28011)) ([db20369](https://github.com/Koenkk/zigbee2mqtt/commit/db203694477ff718386a3d97ca260dccd0dc0597))
|
||||
|
||||
## [2.5.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.5.0...2.5.1) (2025-07-02)
|
||||
|
||||
|
||||
|
||||
@@ -5,33 +5,27 @@
|
||||
<br>
|
||||
<br>
|
||||
<div style="display: flex;">
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/actions?query=workflow%3Aci">
|
||||
<img src="https://github.com/koenkk/zigbee2mqtt/workflows/ci/badge.svg">
|
||||
</a>
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/releases">
|
||||
<img src="https://img.shields.io/github/release/koenkk/zigbee2mqtt.svg">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/zigbee2mqtt">
|
||||
<img src="https://img.shields.io/npm/v/zigbee2mqtt">
|
||||
</a>
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/Koenkk/zigbee2mqtt/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/actions/workflows/github-code-scanning/codeql">
|
||||
<img src="https://github.com/Koenkk/zigbee2mqtt/actions/workflows/github-code-scanning/codeql/badge.svg">
|
||||
</a>
|
||||
<a href="https://discord.gg/dadfWYE">
|
||||
<img src="https://img.shields.io/discord/556563650429583360.svg">
|
||||
</a>
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/koenkk/zigbee2mqtt.svg">
|
||||
</a>
|
||||
<a href="https://www.paypal.me/koenkk">
|
||||
<img src="https://img.shields.io/badge/donate-PayPal-blue.svg">
|
||||
</a>
|
||||
<a href="https://discord.gg/dadfWYE">
|
||||
<img src="https://img.shields.io/discord/556563650429583360.svg">
|
||||
</a>
|
||||
<a href="http://zigbee2mqtt.discourse.group/">
|
||||
<img src="https://img.shields.io/discourse/https/zigbee2mqtt.discourse.group/status.svg">
|
||||
</a>
|
||||
<a>
|
||||
<img src="https://img.shields.io/badge/Coverage-100%25-brightgreen.svg">
|
||||
</a>
|
||||
<a href="https://www.codacy.com/manual/Koenkk/zigbee2mqtt?utm_source=github.com&utm_medium=referral&utm_content=Koenkk/zigbee2mqtt&utm_campaign=Badge_Grade">
|
||||
<img src="https://api.codacy.com/project/badge/Grade/24f1e0fe39f04daa810e8a1416693d3f">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/zigbee2mqtt">
|
||||
<img src="https://img.shields.io/npm/v/zigbee2mqtt">
|
||||
</a>
|
||||
</div>
|
||||
<h1>Zigbee2MQTT 🌉 🐝</h1>
|
||||
<p>
|
||||
@@ -52,59 +46,67 @@ If you aren't familiar with **Zigbee** terminology make sure you [read this](htt
|
||||
|
||||
Zigbee2MQTT integrates well with (almost) every home automation solution because it uses MQTT. However the following integrations are worth mentioning:
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/7738048/40914297-49e6e560-6800-11e8-8904-36cce896e5a8.png">
|
||||
|
||||
### [Home Assistant](https://www.home-assistant.io/)
|
||||
|
||||
- [Home Assistant OS](https://www.home-assistant.io/installation/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
|
||||
- Other installation: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
|
||||
<img align="left" height="75px" width="75px" src="https://user-images.githubusercontent.com/7738048/40914297-49e6e560-6800-11e8-8904-36cce896e5a8.png">
|
||||
|
||||
<br>
|
||||
<br clear="right">
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://etc.athom.com/logo/white/256.png">
|
||||
[Home Assistant OS](https://www.home-assistant.io/installation/) using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt) ([other installations](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html))
|
||||
|
||||
<br clear="both">
|
||||
|
||||
### [Homey](https://homey.app/)
|
||||
|
||||
- Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruijter.zigbee2mqtt/)
|
||||
- Documentation and support in the [Homey Forum](https://community.homey.app/t/83214)
|
||||
<img align="left" height="75px" width="75px" src="https://etc.athom.com/logo/white/256.png">
|
||||
|
||||
<br>
|
||||
<br clear="right">
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
|
||||
Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruijter.zigbee2mqtt/) ([documentation & support](https://community.homey.app/t/83214))
|
||||
|
||||
<br clear="both">
|
||||
|
||||
### [Domoticz](https://www.domoticz.com/)
|
||||
|
||||
- Integration implemented in Domoticz ([documentation](https://www.domoticz.com/wiki/Zigbee2MQTT)).
|
||||
<img align="left" height="75px" width="75px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
|
||||
|
||||
<br>
|
||||
<br clear="right">
|
||||
|
||||
<img align="left" height="100px" width="100px" src="./images/gladys-assistant-logo.jpg">
|
||||
Integration implemented in Domoticz ([documentation](https://www.domoticz.com/wiki/Zigbee2MQTT)).
|
||||
|
||||
<br clear="both">
|
||||
|
||||
### [Gladys Assistant](https://gladysassistant.com/)
|
||||
|
||||
- Integration implemented natively in Gladys Assistant ([documentation](https://gladysassistant.com/docs/integrations/zigbee2mqtt/)).
|
||||
<img align="left" height="75px" width="75px" src="./images/gladys-assistant-logo.jpg">
|
||||
|
||||
<br>
|
||||
<br clear="right">
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://forum.iobroker.net/assets/uploads/system/site-logo.png">
|
||||
Integration implemented natively in Gladys Assistant ([documentation](https://gladysassistant.com/docs/integrations/zigbee2mqtt/)).
|
||||
|
||||
<br clear="both">
|
||||
|
||||
### [IoBroker](https://www.iobroker.net/)
|
||||
|
||||
- Integration implemented in IoBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)).
|
||||
<img align="left" height="75px" width="75px" src="https://forum.iobroker.net/assets/uploads/system/site-logo.png">
|
||||
|
||||
<br>
|
||||
<br clear="right">
|
||||
|
||||
Integration implemented in IoBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)).
|
||||
|
||||
<br clear="both">
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
### Internal Architecture
|
||||
|
||||
Zigbee2MQTT is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your Zigbee adapter and makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/koenkk/zigbee-herdsman/raw/master/docs/Z-Stack%20Monitor%20and%20Test%20API.pdf) to communicate with the adapter. Zigbee-herdsman handles the core Zigbee communication. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/07-5123-08-Zigbee-Cluster-Library.pdf) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the Zigbee2MQTT module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2MQTT also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities. Zigbee2MQTT provides a [web-based interface](https://github.com/nurikk/zigbee2mqtt-frontend) that allows monitoring and configuration.
|
||||
Zigbee2MQTT is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your adapter to handle Zigbee communication and makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/Koenkk/zigbee-herdsman/wiki/References#texas-instruments-zstack) to communicate with the adapter. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/wiki/References#csa-zigbee-alliance-spec) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the Zigbee2MQTT module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2MQTT also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities. Zigbee2MQTT provides several web-based interfaces ([zigbee2mqtt-frontend](https://github.com/nurikk/zigbee2mqtt-frontend), [zigbee2mqtt-windfront](https://github.com/Nerivec/zigbee2mqtt-windfront)) that allows monitoring and configuration.
|
||||
|
||||
### Developing
|
||||
|
||||
Zigbee2MQTT uses TypeScript (partially for now). Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `pnpm run build`. For faster development instead of running `pnpm run build` you can run `pnpm run build-watch` in another terminal session, this will recompile as you change files.
|
||||
Zigbee2MQTT uses TypeScript. Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `pnpm run build`. For faster development instead of running `pnpm run build` you can run `pnpm run build-watch` in another terminal session, this will recompile as you change files.
|
||||
|
||||
Before running any of the commands, you'll first need to run `pnpm install --include=dev`.
|
||||
Before submitting changes run `pnpm run check:w` then `pnpm run test:coverage`.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
||||
+6
-2
@@ -10,12 +10,16 @@ RUN apk add --no-cache tzdata eudev tini nodejs
|
||||
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 && \
|
||||
# Install pnpm, configure a tmp-based store, then use it under a cache mount so each arch gets its own store
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/tmp/pnpm-store \
|
||||
apk add --no-cache make gcc g++ python3 linux-headers npm && \
|
||||
npm install -g pnpm@$(sed -n 's/.*"packageManager": "pnpm@\([0-9.]*\)".*/\1/p' package.json) && \
|
||||
pnpm config set store-dir /tmp/pnpm-store && \
|
||||
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` && \
|
||||
rm -rf $(find ./node_modules/.pnpm/ -wholename "*/@serialport/bindings-cpp/prebuilds" -type d) && \
|
||||
pnpm rebuild @serialport/bindings-cpp
|
||||
|
||||
# Release
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
+10
-11
@@ -28,10 +28,10 @@ import utils from "./util/utils";
|
||||
import Zigbee from "./zigbee";
|
||||
|
||||
export class Controller {
|
||||
private eventBus: EventBus;
|
||||
private zigbee: Zigbee;
|
||||
private state: State;
|
||||
private mqtt: Mqtt;
|
||||
public readonly eventBus: EventBus;
|
||||
public readonly zigbee: Zigbee;
|
||||
public readonly state: State;
|
||||
public readonly mqtt: Mqtt;
|
||||
private restartCallback: () => Promise<void>;
|
||||
private exitCallback: (code: number, restart: boolean) => Promise<void>;
|
||||
public readonly extensions: Set<Extension>;
|
||||
@@ -281,17 +281,16 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
async stop(restart = false): Promise<void> {
|
||||
async stop(restart = false, code = 0): Promise<void> {
|
||||
this.sdNotify?.notifyStopping();
|
||||
|
||||
let code = 0;
|
||||
|
||||
let localCode = 0;
|
||||
for (const extension of this.extensions) {
|
||||
try {
|
||||
await extension.stop();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to stop '${extension.constructor.name}' (${(error as Error).stack})`);
|
||||
code = 1;
|
||||
localCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,11 +305,11 @@ export class Controller {
|
||||
logger.info("Stopped Zigbee2MQTT");
|
||||
} catch (error) {
|
||||
logger.error(`Failed to stop Zigbee2MQTT (${(error as Error).stack})`);
|
||||
code = 1;
|
||||
localCode = 1;
|
||||
}
|
||||
|
||||
this.sdNotify?.stop();
|
||||
return await this.exit(code, restart);
|
||||
return await this.exit(code !== 0 ? code : localCode, restart);
|
||||
}
|
||||
|
||||
async exit(code: number, restart = false): Promise<void> {
|
||||
@@ -320,7 +319,7 @@ export class Controller {
|
||||
|
||||
@bind async onZigbeeAdapterDisconnected(): Promise<void> {
|
||||
logger.error("Adapter disconnected, stopping");
|
||||
await this.stop();
|
||||
await this.stop(false, 2);
|
||||
}
|
||||
|
||||
@bind async publishEntityState(entity: Group | Device, payload: KeyValue, stateChangeReason?: StateChangeReason): Promise<void> {
|
||||
|
||||
@@ -9,10 +9,9 @@ import Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
import utils, {DEFAULT_BIND_GROUP_ID} from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
|
||||
const ALL_CLUSTER_CANDIDATES: readonly ClusterName[] = [
|
||||
"genScenes",
|
||||
"genOnOff",
|
||||
@@ -28,7 +27,7 @@ const ALL_CLUSTER_CANDIDATES: readonly ClusterName[] = [
|
||||
];
|
||||
|
||||
// See zigbee-herdsman-converters
|
||||
const DEFAULT_BIND_GROUP = {type: "group_number", ID: 901, name: "default_bind_group"};
|
||||
const DEFAULT_BIND_GROUP = {type: "group_number", ID: DEFAULT_BIND_GROUP_ID, name: "default_bind_group"};
|
||||
const DEFAULT_REPORT_CONFIG = {minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1};
|
||||
|
||||
const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean; colorXY: boolean}> => {
|
||||
@@ -204,6 +203,7 @@ interface ParsedMQTTMessage {
|
||||
}
|
||||
|
||||
export default class Bind extends Extension {
|
||||
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
|
||||
private pollDebouncers: {[s: string]: () => void} = {};
|
||||
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
@@ -216,7 +216,7 @@ export default class Bind extends Extension {
|
||||
private parseMQTTMessage(
|
||||
data: eventdata.MQTTMessage,
|
||||
): [raw: KeyValue | undefined, parsed: ParsedMQTTMessage | undefined, error: string | undefined] {
|
||||
if (data.topic.match(TOPIC_REGEX)) {
|
||||
if (data.topic.match(this.#topicRegex)) {
|
||||
const type = data.topic.endsWith("unbind") ? "unbind" : "bind";
|
||||
let skipDisableReporting = false;
|
||||
const message = JSON.parse(data.message) as Zigbee2MQTTAPI["bridge/request/device/bind"];
|
||||
|
||||
@@ -14,12 +14,11 @@ import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTT
|
||||
import data from "../util/data";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils, {assertString} from "../util/utils";
|
||||
import utils, {assertString, DEFAULT_BIND_GROUP_ID} from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
const REQUEST_REGEX = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
export default class Bridge extends Extension {
|
||||
#requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
// set on `start`
|
||||
#osInfo!: Zigbee2MQTTAPI["bridge/info"]["os"];
|
||||
private zigbee2mqttVersion!: {commitHash?: string; version: string};
|
||||
@@ -197,7 +196,7 @@ export default class Bridge extends Extension {
|
||||
}
|
||||
|
||||
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
const match = data.topic.match(REQUEST_REGEX);
|
||||
const match = data.topic.match(this.#requestRegex);
|
||||
|
||||
if (!match) {
|
||||
return;
|
||||
@@ -731,6 +730,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
for (const endpoint of device.zh.endpoints) {
|
||||
const data: (typeof endpoints)[keyof typeof endpoints] = {
|
||||
name: device.endpointName(endpoint),
|
||||
scenes: utils.getScenes(endpoint),
|
||||
bindings: [],
|
||||
configured_reportings: [],
|
||||
@@ -797,7 +797,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
groups.push({
|
||||
id: group.ID,
|
||||
friendly_name: group.ID === 901 ? "default_bind_group" : group.name,
|
||||
friendly_name: group.ID === DEFAULT_BIND_GROUP_ID ? "default_bind_group" : group.name,
|
||||
description: group.options.description,
|
||||
scenes: utils.getScenes(group.zh),
|
||||
members,
|
||||
@@ -837,6 +837,7 @@ export default class Bridge extends Extension {
|
||||
}
|
||||
|
||||
const payload: Zigbee2MQTTDevice["definition"] = {
|
||||
source: device.definition.externalConverterName ? "external" : device.definition.generated ? "generated" : "native",
|
||||
model: device.definition.model,
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
|
||||
@@ -193,7 +193,7 @@ export default abstract class ExternalJSExtension<M> extends Extension {
|
||||
fs.renameSync(filePath, `${filePath}.invalid`);
|
||||
|
||||
logger.error(
|
||||
`Invalid external ${this.mqttTopic} '${extension.name}' was ignored and renamed to prevent interference with Zigbee2MQTT.`,
|
||||
`Invalid external ${this.mqttTopic} '${extension.name}' was ignored and renamed to prevent interference with Zigbee2MQTT. (${(error as Error).message})`,
|
||||
);
|
||||
// biome-ignore lint/style/noNonNullAssertion: always Error
|
||||
logger.debug((error as Error).stack!);
|
||||
|
||||
+86
-79
@@ -7,7 +7,6 @@ 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";
|
||||
@@ -24,9 +23,7 @@ import Extension from "./extension";
|
||||
*/
|
||||
export class Frontend extends Extension {
|
||||
private mqttBaseTopic: string;
|
||||
private server!: Server;
|
||||
private fileServer!: RequestHandler;
|
||||
private deviceIconsFileServer!: RequestHandler;
|
||||
private server: Server | undefined;
|
||||
private wss!: WebSocket.Server;
|
||||
private baseUrl: string;
|
||||
|
||||
@@ -49,60 +46,97 @@ export class Frontend extends Extension {
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
const hasSSL = (val: string | undefined, key: string): val is string => {
|
||||
if (val) {
|
||||
if (!existsSync(val)) {
|
||||
logger.error(`Defined ${key} '${val}' file path does not exists, server won't be secured.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const {host, port, ssl_key: sslKey, ssl_cert: sslCert} = settings.get().frontend;
|
||||
const options = {
|
||||
enableBrotli: true,
|
||||
// TODO: https://github.com/Koenkk/zigbee2mqtt/issues/24654 - enable compressed index serving when express-static-gzip is fixed.
|
||||
index: false,
|
||||
serveStatic: {
|
||||
index: "index.html",
|
||||
/* v8 ignore start */
|
||||
setHeaders: (res: ServerResponse, path: string): void => {
|
||||
if (path.endsWith("index.html")) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
if (settings.get().frontend.disable_ui_serving) {
|
||||
const {host, port} = settings.get().frontend;
|
||||
this.wss = new WebSocket.Server({port, host, path: posix.join(this.baseUrl, "api")});
|
||||
|
||||
logger.info(
|
||||
/* v8 ignore next */
|
||||
`Frontend UI serving is disabled. WebSocket at: ${this.wss.options.host ?? "0.0.0.0"}:${this.wss.options.port}${this.wss.options.path}`,
|
||||
);
|
||||
} else {
|
||||
const {host, port, ssl_key: sslKey, ssl_cert: sslCert} = settings.get().frontend;
|
||||
const hasSSL = (val: string | undefined, key: string): val is string => {
|
||||
if (val) {
|
||||
if (existsSync(val)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.error(`Defined ${key} '${val}' file path does not exists, server won't be secured.`);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
const options: expressStaticGzip.ExpressStaticGzipOptions = {
|
||||
enableBrotli: true,
|
||||
serveStatic: {
|
||||
/* v8 ignore start */
|
||||
setHeaders: (res: ServerResponse, path: string): void => {
|
||||
if (path.endsWith("index.html")) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
}
|
||||
},
|
||||
/* v8 ignore stop */
|
||||
},
|
||||
/* v8 ignore stop */
|
||||
},
|
||||
};
|
||||
const frontend = (await import(settings.get().frontend.package)) as typeof import("zigbee2mqtt-frontend");
|
||||
this.fileServer = expressStaticGzip(frontend.default.getPath(), options);
|
||||
this.deviceIconsFileServer = expressStaticGzip(data.joinPath("device_icons"), options);
|
||||
this.wss = new WebSocket.Server({noServer: true, path: posix.join(this.baseUrl, "api")});
|
||||
};
|
||||
const frontend = (await import(settings.get().frontend.package)) as typeof import("zigbee2mqtt-frontend");
|
||||
const fileServer = expressStaticGzip(frontend.default.getPath(), options);
|
||||
const deviceIconsFileServer = expressStaticGzip(data.joinPath("device_icons"), options);
|
||||
const onRequest = (request: IncomingMessage, response: ServerResponse): void => {
|
||||
const next = finalhandler(request, response);
|
||||
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
|
||||
const newUrl = posix.relative(this.baseUrl, request.url!);
|
||||
|
||||
// The request url is not within the frontend base url, so the relative path starts with '..'
|
||||
if (newUrl.startsWith(".")) {
|
||||
next();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach originalUrl so that static-server can perform a redirect to '/' when serving the root directory.
|
||||
// This is necessary for the browser to resolve relative assets paths correctly.
|
||||
request.originalUrl = request.url;
|
||||
request.url = `/${newUrl}`;
|
||||
request.path = request.url;
|
||||
|
||||
if (newUrl.startsWith("device_icons/")) {
|
||||
request.path = request.path.replace("device_icons/", "");
|
||||
request.url = request.url.replace("/device_icons", "");
|
||||
|
||||
deviceIconsFileServer(request, response, next);
|
||||
} else {
|
||||
fileServer(request, response, next);
|
||||
}
|
||||
};
|
||||
|
||||
if (hasSSL(sslKey, "ssl_key") && hasSSL(sslCert, "ssl_cert")) {
|
||||
const serverOptions = {key: readFileSync(sslKey), cert: readFileSync(sslCert)};
|
||||
this.server = createSecureServer(serverOptions, onRequest);
|
||||
} else {
|
||||
this.server = createServer(onRequest);
|
||||
}
|
||||
|
||||
this.server.on("upgrade", this.onUpgrade);
|
||||
|
||||
if (!host) {
|
||||
this.server.listen(port);
|
||||
logger.info(`Started frontend on port ${port}`);
|
||||
} else if (host.startsWith("/")) {
|
||||
this.server.listen(host);
|
||||
logger.info(`Started frontend on socket ${host}`);
|
||||
} else {
|
||||
this.server.listen(port, host);
|
||||
logger.info(`Started frontend on port ${host}:${port}`);
|
||||
}
|
||||
|
||||
this.wss = new WebSocket.Server({noServer: true, path: posix.join(this.baseUrl, "api")});
|
||||
}
|
||||
|
||||
this.wss.on("connection", this.onWebSocketConnection);
|
||||
|
||||
if (hasSSL(sslKey, "ssl_key") && hasSSL(sslCert, "ssl_cert")) {
|
||||
const serverOptions = {key: readFileSync(sslKey), cert: readFileSync(sslCert)};
|
||||
this.server = createSecureServer(serverOptions, this.onRequest);
|
||||
} else {
|
||||
this.server = createServer(this.onRequest);
|
||||
}
|
||||
|
||||
this.server.on("upgrade", this.onUpgrade);
|
||||
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessageOrEntityState);
|
||||
this.eventBus.onPublishEntityState(this, this.onMQTTPublishMessageOrEntityState);
|
||||
|
||||
if (!host) {
|
||||
this.server.listen(port);
|
||||
logger.info(`Started frontend on port ${port}`);
|
||||
} else if (host.startsWith("/")) {
|
||||
this.server.listen(host);
|
||||
logger.info(`Started frontend on socket ${host}`);
|
||||
} else {
|
||||
this.server.listen(port, host);
|
||||
logger.info(`Started frontend on port ${host}:${port}`);
|
||||
}
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
@@ -117,34 +151,7 @@ export class Frontend extends Extension {
|
||||
this.wss.close();
|
||||
}
|
||||
|
||||
await new Promise((resolve) => this.server?.close(resolve));
|
||||
}
|
||||
|
||||
@bind private onRequest(request: IncomingMessage, response: ServerResponse): void {
|
||||
const fin = finalhandler(request, response);
|
||||
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
|
||||
const newUrl = posix.relative(this.baseUrl, request.url!);
|
||||
|
||||
// The request url is not within the frontend base url, so the relative path starts with '..'
|
||||
if (newUrl.startsWith(".")) {
|
||||
fin();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach originalUrl so that static-server can perform a redirect to '/' when serving the root directory.
|
||||
// This is necessary for the browser to resolve relative assets paths correctly.
|
||||
request.originalUrl = request.url;
|
||||
request.url = `/${newUrl}`;
|
||||
request.path = request.url;
|
||||
|
||||
if (newUrl.startsWith("device_icons/")) {
|
||||
request.path = request.path.replace("device_icons/", "");
|
||||
request.url = request.url.replace("/device_icons", "");
|
||||
this.deviceIconsFileServer(request, response, fin);
|
||||
} else {
|
||||
this.fileServer(request, response, fin);
|
||||
}
|
||||
await new Promise((resolve) => (this.server ? this.server.close(resolve) : resolve(undefined)));
|
||||
}
|
||||
|
||||
@bind private onUpgrade(request: IncomingMessage, socket: Socket, head: Buffer): void {
|
||||
|
||||
@@ -11,8 +11,6 @@ import * as settings from "../util/settings";
|
||||
import utils, {isLightExpose} from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
|
||||
|
||||
const STATE_PROPERTIES: Readonly<Record<string, (value: string, exposes: zhc.Expose[]) => boolean>> = {
|
||||
state: () => true,
|
||||
brightness: (_value, exposes) => exposes.some((e) => isLightExpose(e) && e.features.some((f) => f.name === "brightness")),
|
||||
@@ -38,6 +36,7 @@ interface ParsedMQTTMessage {
|
||||
}
|
||||
|
||||
export default class Groups extends Extension {
|
||||
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
|
||||
private lastOptimisticState: {[s: string]: KeyValue} = {};
|
||||
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
@@ -185,7 +184,7 @@ export default class Groups extends Extension {
|
||||
private parseMQTTMessage(
|
||||
data: eventdata.MQTTMessage,
|
||||
): [raw: KeyValue | undefined, parsed: ParsedMQTTMessage | undefined, error: string | undefined] {
|
||||
const topicRegexMatch = data.topic.match(TOPIC_REGEX);
|
||||
const topicRegexMatch = data.topic.match(this.#topicRegex);
|
||||
|
||||
if (topicRegexMatch) {
|
||||
const type = topicRegexMatch[1] as "remove" | "add" | "remove_all";
|
||||
@@ -273,7 +272,7 @@ export default class Groups extends Extension {
|
||||
try {
|
||||
if (type === "add") {
|
||||
assert(resolvedGroup, "`resolvedGroup` is missing");
|
||||
logger.info(`Adding '${resolvedDevice.name}' to '${resolvedGroup.name}'`);
|
||||
logger.info(`Adding endpoint '${resolvedEndpoint.ID}' of device '${resolvedDevice.name}' to group '${resolvedGroup.name}'`);
|
||||
await resolvedEndpoint.addToGroup(resolvedGroup.zh);
|
||||
changedGroups.push(resolvedGroup);
|
||||
// biome-ignore lint/style/noNonNullAssertion: valid from resolved asserts
|
||||
@@ -281,7 +280,7 @@ export default class Groups extends Extension {
|
||||
await this.publishResponse<"bridge/response/group/members/add">(parsed.type, raw, respPayload);
|
||||
} else if (type === "remove") {
|
||||
assert(resolvedGroup, "`resolvedGroup` is missing");
|
||||
logger.info(`Removing '${resolvedDevice.name}' from '${resolvedGroup.name}'`);
|
||||
logger.info(`Removing endpoint '${resolvedEndpoint.ID}' of device '${resolvedDevice.name}' from group '${resolvedGroup.name}'`);
|
||||
await resolvedEndpoint.removeFromGroup(resolvedGroup.zh);
|
||||
changedGroups.push(resolvedGroup);
|
||||
// biome-ignore lint/style/noNonNullAssertion: valid from resolved asserts
|
||||
@@ -289,7 +288,7 @@ export default class Groups extends Extension {
|
||||
await this.publishResponse<"bridge/response/group/members/remove">(parsed.type, raw, respPayload);
|
||||
} else {
|
||||
// remove_all
|
||||
logger.info(`Removing '${resolvedDevice.name}' from all groups`);
|
||||
logger.info(`Removing endpoint '${resolvedEndpoint.ID}' of device '${resolvedDevice.name}' from all groups`);
|
||||
|
||||
for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEndpoint))) {
|
||||
changedGroups.push(group);
|
||||
@@ -301,7 +300,7 @@ export default class Groups extends Extension {
|
||||
await this.publishResponse<"bridge/response/group/members/remove_all">(parsed.type, raw, respPayload);
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to ${type} from group (${(e as Error).message})`;
|
||||
const errorMsg = `Failed to ${type} ${type === "add" ? "to" : "from"} group (${(e as Error).message})`;
|
||||
await this.publishResponse(parsed.type, raw, {}, errorMsg);
|
||||
// biome-ignore lint/style/noNonNullAssertion: always Error
|
||||
logger.debug((e as Error).stack!);
|
||||
|
||||
@@ -33,13 +33,14 @@ export default class Health extends Extension {
|
||||
async #checkHealth(): Promise<void> {
|
||||
const sysMemTotalKb = os.totalmem() / 1024;
|
||||
const sysMemFreeKb = os.freemem() / 1024;
|
||||
const sysMemUsedKb = sysMemTotalKb - sysMemFreeKb;
|
||||
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),
|
||||
memory_used_mb: round2(sysMemUsedKb / 1024),
|
||||
memory_percent: round4((sysMemUsedKb / sysMemTotalKb) * 100.0),
|
||||
},
|
||||
process: {
|
||||
uptime_sec: Math.floor(process.uptime()),
|
||||
|
||||
@@ -13,7 +13,7 @@ const SUPPORTED_FORMATS = ["raw", "graphviz", "plantuml"];
|
||||
* This extension creates a network map
|
||||
*/
|
||||
export default class NetworkMap extends Extension {
|
||||
private topic = `${settings.get().mqtt.base_topic}/bridge/request/networkmap`;
|
||||
#topic = `${settings.get().mqtt.base_topic}/bridge/request/networkmap`;
|
||||
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
override async start(): Promise<void> {
|
||||
@@ -21,7 +21,7 @@ export default class NetworkMap extends Extension {
|
||||
}
|
||||
|
||||
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
if (data.topic === this.topic) {
|
||||
if (data.topic === this.#topic) {
|
||||
const message = utils.parseJSON(data.message, data.message) as Zigbee2MQTTAPI["bridge/request/networkmap"];
|
||||
|
||||
try {
|
||||
|
||||
@@ -25,12 +25,11 @@ export interface UpdatePayload {
|
||||
};
|
||||
}
|
||||
|
||||
const topicRegex = new RegExp(
|
||||
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
|
||||
"i",
|
||||
);
|
||||
|
||||
export default class OTAUpdate extends Extension {
|
||||
#topicRegex = new RegExp(
|
||||
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
|
||||
"i",
|
||||
);
|
||||
private inProgress = new Set<string>();
|
||||
private lastChecked = new Map<string, number>();
|
||||
private scheduledUpgrades = new Set<string>();
|
||||
@@ -252,7 +251,7 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
const topicMatch = data.topic.match(topicRegex);
|
||||
const topicMatch = data.topic.match(this.#topicRegex);
|
||||
|
||||
if (!topicMatch) {
|
||||
return;
|
||||
|
||||
@@ -9,12 +9,12 @@ import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
// TODO: get rid of this, use class member
|
||||
let topicGetSetRegex: RegExp;
|
||||
// Used by `publish.test.js` to reload regex when changing `mqtt.base_topic`.
|
||||
// Used by `publish.test.ts` to reload regex when changing `mqtt.base_topic`.
|
||||
export const loadTopicGetSetRegex = (): void => {
|
||||
topicGetSetRegex = new RegExp(`^${settings.get().mqtt.base_topic}/(?!bridge)(.+?)/(get|set)(?:/(.+))?$`);
|
||||
};
|
||||
loadTopicGetSetRegex();
|
||||
|
||||
const STATE_VALUES: ReadonlyArray<string> = ["on", "off", "toggle", "open", "close", "stop", "lock", "unlock"];
|
||||
const SCENE_CONVERTER_KEYS: ReadonlyArray<string> = ["scene_store", "scene_add", "scene_remove", "scene_remove_all", "scene_rename"];
|
||||
@@ -29,6 +29,7 @@ interface ParsedTopic {
|
||||
export default class Publish extends Extension {
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
override async start(): Promise<void> {
|
||||
loadTopicGetSetRegex();
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
}
|
||||
|
||||
|
||||
+16
-24
@@ -76,43 +76,33 @@ export default class Device {
|
||||
}
|
||||
|
||||
endpoint(key?: string | number): zh.Endpoint | undefined {
|
||||
let endpoint: zh.Endpoint | undefined;
|
||||
|
||||
if (!key) {
|
||||
key = "default";
|
||||
} else if (!Number.isNaN(Number(key))) {
|
||||
return this.zh.getEndpoint(Number(key));
|
||||
}
|
||||
|
||||
if (!Number.isNaN(Number(key))) {
|
||||
endpoint = this.zh.getEndpoint(Number(key));
|
||||
} else if (this.definition?.endpoint) {
|
||||
const ID = this.definition?.endpoint?.(this.zh)[key];
|
||||
if (this.definition?.endpoint) {
|
||||
const ID = this.definition.endpoint(this.zh)[key];
|
||||
|
||||
if (ID) {
|
||||
endpoint = this.zh.getEndpoint(ID);
|
||||
} else if (key === "default") {
|
||||
endpoint = this.zh.endpoints[0];
|
||||
} else {
|
||||
return undefined;
|
||||
return this.zh.getEndpoint(ID);
|
||||
}
|
||||
} else {
|
||||
if (key !== "default") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
endpoint = this.zh.endpoints[0];
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
return key === "default" ? this.zh.endpoints[0] : undefined;
|
||||
}
|
||||
|
||||
endpointName(endpoint: zh.Endpoint): string | undefined {
|
||||
let epName: string | undefined;
|
||||
|
||||
if (this.definition?.endpoint) {
|
||||
const mapping = this.definition?.endpoint(this.zh);
|
||||
for (const [name, id] of Object.entries(mapping)) {
|
||||
if (id === endpoint.ID) {
|
||||
const mapping = this.definition.endpoint(this.zh);
|
||||
|
||||
for (const name in mapping) {
|
||||
if (mapping[name] === endpoint.ID) {
|
||||
epName = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,9 +114,11 @@ export default class Device {
|
||||
getEndpointNames(): string[] {
|
||||
const names: string[] = [];
|
||||
|
||||
for (const name in this.definition?.endpoint?.(this.zh) ?? {}) {
|
||||
if (name !== "default") {
|
||||
names.push(name);
|
||||
if (this.definition?.endpoint) {
|
||||
for (const name in this.definition.endpoint(this.zh)) {
|
||||
if (name !== "default") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import * as settings from "../util/settings";
|
||||
import {DEFAULT_BIND_GROUP_ID} from "../util/utils";
|
||||
|
||||
export default class Group {
|
||||
public zh: zh.Group;
|
||||
@@ -23,6 +23,12 @@ export default class Group {
|
||||
this.resolveDevice = resolveDevice;
|
||||
}
|
||||
|
||||
ensureInSettings(): void {
|
||||
if (this.ID !== DEFAULT_BIND_GROUP_ID && !settings.getGroup(this.ID)) {
|
||||
settings.addGroup(this.name, this.ID.toString());
|
||||
}
|
||||
}
|
||||
|
||||
hasMember(device: Device): boolean {
|
||||
return !!device.zh.endpoints.find((e) => this.zh.members.includes(e));
|
||||
}
|
||||
|
||||
+5
-6
@@ -171,7 +171,7 @@ export default class Mqtt {
|
||||
|
||||
const stateData: Zigbee2MQTTAPI["bridge/state"] = {state: "online"};
|
||||
|
||||
await this.publish("bridge/state", JSON.stringify(stateData), {clientOptions: {retain: true}});
|
||||
await this.publish("bridge/state", JSON.stringify(stateData), {clientOptions: {retain: true, qos: 1}});
|
||||
await this.subscribe(`${settings.get().mqtt.base_topic}/#`);
|
||||
}
|
||||
|
||||
@@ -222,19 +222,18 @@ export default class Mqtt {
|
||||
logger.error("Not connected to MQTT server!");
|
||||
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!finalOptions.skipLog) {
|
||||
logger.info(() => `MQTT publish: topic '${topic}', payload '${payload}'`, NS);
|
||||
}
|
||||
|
||||
let clientOptions: IClientPublishOptions = finalOptions.clientOptions;
|
||||
if (settings.get().mqtt.force_disable_retain) {
|
||||
clientOptions = {...finalOptions.clientOptions, retain: false};
|
||||
}
|
||||
|
||||
if (!finalOptions.skipLog) {
|
||||
logger.info(() => `MQTT publish: topic '${topic}', payload '${payload}'`, NS);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.publishAsync(topic, payload, clientOptions);
|
||||
} catch (error) {
|
||||
|
||||
@@ -151,6 +151,7 @@ export interface Zigbee2MQTTSettings {
|
||||
ssl_cert?: string;
|
||||
ssl_key?: string;
|
||||
notification_filter?: string[];
|
||||
disable_ui_serving?: boolean;
|
||||
};
|
||||
devices: {[s: string]: Zigbee2MQTTDeviceOptions};
|
||||
groups: {[s: string]: Omit<Zigbee2MQTTGroupOptions, "ID">};
|
||||
@@ -196,6 +197,7 @@ export interface Zigbee2MQTTScene {
|
||||
}
|
||||
|
||||
export interface Zigbee2MQTTDeviceEndpoint {
|
||||
name?: string;
|
||||
bindings: Zigbee2MQTTDeviceEndpointBinding[];
|
||||
configured_reportings: Zigbee2MQTTDeviceEndpointConfiguredReporting[];
|
||||
clusters: {input: string[]; output: string[]};
|
||||
@@ -218,6 +220,7 @@ export interface Zigbee2MQTTDeviceEndpointConfiguredReporting {
|
||||
}
|
||||
|
||||
export interface Zigbee2MQTTDeviceDefinition {
|
||||
source: "native" | "generated" | "external";
|
||||
model: string;
|
||||
vendor: string;
|
||||
description: string;
|
||||
|
||||
Vendored
-7
@@ -12,10 +12,3 @@ declare module "http" {
|
||||
path?: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "express-static-gzip" {
|
||||
import type {IncomingMessage, ServerResponse} from "node:http";
|
||||
|
||||
export type RequestHandler = (req: IncomingMessage, res: ServerResponse, finalhandler: (err: unknown) => void) => void;
|
||||
export default function expressStaticGzip(root: string, options?: Record<string, unknown>): RequestHandler;
|
||||
}
|
||||
|
||||
+49
-23
@@ -1,11 +1,10 @@
|
||||
import {existsSync, mkdirSync} from "node:fs";
|
||||
import {createServer} from "node:http";
|
||||
import {parse} from "node:querystring";
|
||||
|
||||
import {findAllDevices} from "zigbee-herdsman/dist/adapter/adapterDiscovery";
|
||||
|
||||
import data from "./data";
|
||||
import * as settings from "./settings";
|
||||
import {YAMLFileException} from "./yaml";
|
||||
|
||||
type OnboardSettings = {
|
||||
mqtt_base_topic?: string;
|
||||
@@ -518,6 +517,27 @@ async function startFailureServer(errors: string): Promise<void> {
|
||||
await new Promise((resolve) => server?.close(resolve));
|
||||
}
|
||||
|
||||
async function onSettingsErrors(errors: string[]): Promise<void> {
|
||||
let pErrors = "";
|
||||
|
||||
console.error("\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
console.error(" READ THIS CAREFULLY\n");
|
||||
console.error("Refusing to start because configuration is not valid, found the following errors:");
|
||||
|
||||
for (const error of errors) {
|
||||
console.error(`- ${error}`);
|
||||
|
||||
pErrors += `<p>- ${escapeHtml(error)}</p>`;
|
||||
}
|
||||
|
||||
console.error("\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration");
|
||||
console.error("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n");
|
||||
|
||||
if (!process.env.Z2M_ONBOARD_NO_SERVER && !process.env.Z2M_ONBOARD_NO_FAILURE_PAGE) {
|
||||
await startFailureServer(pErrors);
|
||||
}
|
||||
}
|
||||
|
||||
export async function onboard(): Promise<boolean> {
|
||||
if (!existsSync(data.getPath())) {
|
||||
mkdirSync(data.getPath(), {recursive: true});
|
||||
@@ -525,15 +545,38 @@ export async function onboard(): Promise<boolean> {
|
||||
|
||||
const confExists = existsSync(data.joinPath("configuration.yaml"));
|
||||
|
||||
if (!confExists) {
|
||||
settings.writeMinimalDefaults();
|
||||
} else {
|
||||
if (confExists) {
|
||||
// initial caching, ensure file is valid yaml first
|
||||
try {
|
||||
settings.getPersistedSettings();
|
||||
} catch (error) {
|
||||
await onSettingsErrors(
|
||||
error instanceof YAMLFileException
|
||||
? [`Your configuration file: '${error.file}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`]
|
||||
: [`${error}`],
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// migrate first
|
||||
const {migrateIfNecessary} = await import("./settingsMigration.js");
|
||||
|
||||
migrateIfNecessary();
|
||||
|
||||
// make sure existing settings are valid before applying envs
|
||||
const errors = settings.validateNonRequired();
|
||||
|
||||
if (errors.length > 0) {
|
||||
await onSettingsErrors(errors);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// trigger initial writing of `ZIGBEE2MQTT_CONFIG_*` ENVs
|
||||
settings.write();
|
||||
} else {
|
||||
settings.writeMinimalDefaults();
|
||||
}
|
||||
|
||||
// use `configuration.yaml` file to detect "brand new install"
|
||||
@@ -553,24 +596,7 @@ export async function onboard(): Promise<boolean> {
|
||||
const errors = settings.validate();
|
||||
|
||||
if (errors.length > 0) {
|
||||
let pErrors = "";
|
||||
|
||||
console.error("\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
console.error(" READ THIS CAREFULLY\n");
|
||||
console.error("Refusing to start because configuration is not valid, found the following errors:");
|
||||
|
||||
for (const error of errors) {
|
||||
console.error(`- ${error}`);
|
||||
|
||||
pErrors += `<p>- ${escapeHtml(error)}</p>`;
|
||||
}
|
||||
|
||||
console.error("\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration");
|
||||
console.error("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n");
|
||||
|
||||
if (!process.env.Z2M_ONBOARD_NO_SERVER && !process.env.Z2M_ONBOARD_NO_FAILURE_PAGE) {
|
||||
await startFailureServer(pErrors);
|
||||
}
|
||||
await onSettingsErrors(errors);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -248,8 +248,8 @@
|
||||
"type": "number",
|
||||
"title": "Baudrate",
|
||||
"requiresRestart": true,
|
||||
"description": "Baud rate speed for serial port, this can be anything firmware support but default is 115200 for Z-Stack and EZSP, 38400 for Deconz, however note that some EZSP firmware need 57600",
|
||||
"examples": [38400, 57600, 115200]
|
||||
"description": "Baud rate speed for the serial port. This must match what the firmware on your adapter supports (most commonly 115200).",
|
||||
"examples": [115200, 921600, 460800, 230400, 57600, 38400]
|
||||
},
|
||||
"rtscts": {
|
||||
"type": "boolean",
|
||||
@@ -291,13 +291,16 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enddevice": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#fff8ce"
|
||||
},
|
||||
"coordinator": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#e04e5d"
|
||||
},
|
||||
"router": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#4ea3e0"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -305,13 +308,16 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enddevice": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#000000"
|
||||
},
|
||||
"coordinator": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#ffffff"
|
||||
},
|
||||
"router": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#ffffff"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -319,10 +325,12 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#009900"
|
||||
},
|
||||
"inactive": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "#994444"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,6 +454,12 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"disable_ui_serving": {
|
||||
"type": ["boolean", "null"],
|
||||
"title": "Disable UI serving",
|
||||
"description": "If true, the frontend UI is not served, only the WebSocket is maintained by Zigbee2MQTT (you are required to serve a standalone UI yourself as needed).",
|
||||
"requiresRestart": true
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@@ -509,7 +523,8 @@
|
||||
"enum": ["console", "file", "syslog"]
|
||||
},
|
||||
"title": "Log output",
|
||||
"description": "Output location of the log, leave empty to suppress logging"
|
||||
"description": "Output location of the log, leave empty to suppress logging",
|
||||
"default": ["console", "file"]
|
||||
},
|
||||
"log_directory": {
|
||||
"type": "string",
|
||||
@@ -524,7 +539,7 @@
|
||||
"requiresRestart": true,
|
||||
"description": "Log file name, can also contain timestamp",
|
||||
"examples": ["zigbee2mqtt_%TIMESTAMP%.log"],
|
||||
"default": "log.txt"
|
||||
"default": "log.log"
|
||||
},
|
||||
"log_level": {
|
||||
"type": "string",
|
||||
@@ -653,7 +668,8 @@
|
||||
],
|
||||
"title": "Pan ID",
|
||||
"requiresRestart": true,
|
||||
"description": "ZigBee pan ID, changing requires re-pairing all devices!"
|
||||
"description": "ZigBee pan ID, changing requires re-pairing all devices!",
|
||||
"default": 6754
|
||||
},
|
||||
"ext_pan_id": {
|
||||
"oneOf": [
|
||||
@@ -671,7 +687,8 @@
|
||||
],
|
||||
"title": "Ext Pan ID",
|
||||
"requiresRestart": true,
|
||||
"description": "Zigbee extended pan ID, changing requires re-pairing all devices!"
|
||||
"description": "Zigbee extended pan ID, changing requires re-pairing all devices!",
|
||||
"default": [221, 221, 221, 221, 221, 221, 221, 221]
|
||||
},
|
||||
"channel": {
|
||||
"type": "number",
|
||||
@@ -689,6 +706,7 @@
|
||||
"type": ["number", "null"],
|
||||
"minimum": 1,
|
||||
"maximum": 64,
|
||||
"default": null,
|
||||
"description": "Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)"
|
||||
},
|
||||
"adapter_delay": {
|
||||
@@ -697,6 +715,7 @@
|
||||
"title": "Adapter delay",
|
||||
"minimum": 0,
|
||||
"maximum": 1000,
|
||||
"default": null,
|
||||
"description": "Adapter delay"
|
||||
},
|
||||
"cache_state": {
|
||||
@@ -746,14 +765,16 @@
|
||||
],
|
||||
"title": "Network key",
|
||||
"requiresRestart": true,
|
||||
"description": "Network encryption key, changing requires re-pairing all devices!"
|
||||
"description": "Network encryption key, changing requires re-pairing all devices!",
|
||||
"default": [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13]
|
||||
},
|
||||
"timestamp_format": {
|
||||
"type": "string",
|
||||
"title": "Timestamp format",
|
||||
"requiresRestart": true,
|
||||
"description": "Log timestamp format",
|
||||
"examples": ["YYYY-MM-DD HH:mm:ss"]
|
||||
"default": "YYYY-MM-DD HH:mm:ss",
|
||||
"examples": ["YYYY-MM-DD HH:mm:ss.SSS"]
|
||||
},
|
||||
"transmit_power": {
|
||||
"type": ["number", "null"],
|
||||
@@ -767,7 +788,8 @@
|
||||
"type": "string",
|
||||
"enum": ["attribute_and_json", "attribute", "json"],
|
||||
"title": "MQTT output type",
|
||||
"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)"
|
||||
"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)",
|
||||
"default": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -821,7 +843,8 @@
|
||||
"description": "Sets the MQTT Message Expiry in seconds, Make sure to set mqtt.version to 5"
|
||||
},
|
||||
"qos": {
|
||||
"type": "number",
|
||||
"type": ["number"],
|
||||
"enum": [0, 1, 2],
|
||||
"title": "QoS",
|
||||
"description": "QoS level for MQTT messages of this device"
|
||||
},
|
||||
@@ -911,13 +934,16 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"qos": {
|
||||
"type": "number"
|
||||
"type": ["number"],
|
||||
"enum": [0, 1, 2],
|
||||
"title": "QoS",
|
||||
"description": "QoS level for MQTT messages of this group"
|
||||
},
|
||||
"off_state": {
|
||||
"type": ["string"],
|
||||
"enum": ["all_members_off", "last_member_state"],
|
||||
"title": "Group off state",
|
||||
"default": "auto",
|
||||
"default": "all_members_off",
|
||||
"requiresRestart": true,
|
||||
"description": "Control when to publish state OFF or CLOSE for a group. 'all_members_off': only publish state OFF/CLOSE when all group members are in state OFF/CLOSE, 'last_member_state': publish state OFF whenever one of its members changes to OFF"
|
||||
},
|
||||
|
||||
+22
-17
@@ -7,7 +7,7 @@ import objectAssignDeep from "object-assign-deep";
|
||||
import data from "./data";
|
||||
import schemaJson from "./settings.schema.json";
|
||||
import utils from "./utils";
|
||||
import yaml, {YAMLFileException} from "./yaml";
|
||||
import yaml from "./yaml";
|
||||
|
||||
export {schemaJson};
|
||||
// When updating also update:
|
||||
@@ -52,6 +52,9 @@ export const defaults = {
|
||||
force_disable_retain: false,
|
||||
// 1MB = roughly 3.5KB per device * 300 devices for `/bridge/devices`
|
||||
maximum_packet_size: 1048576,
|
||||
keepalive: 60,
|
||||
reject_unauthorized: true,
|
||||
version: 4,
|
||||
},
|
||||
serial: {
|
||||
disable_led: false,
|
||||
@@ -257,15 +260,7 @@ export function write(): void {
|
||||
}
|
||||
|
||||
export function validate(): string[] {
|
||||
try {
|
||||
getPersistedSettings();
|
||||
} catch (error) {
|
||||
if (error instanceof YAMLFileException) {
|
||||
return [`Your YAML file: '${error.file}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`];
|
||||
}
|
||||
|
||||
return [`${error}`];
|
||||
}
|
||||
getPersistedSettings();
|
||||
|
||||
if (!ajvSetting(_settings)) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: When `ajvSetting()` return false it always has `errors`
|
||||
@@ -295,9 +290,6 @@ export function validate(): string[] {
|
||||
if ("icon" in e && e.icon && !e.icon.startsWith("http://") && !e.icon.startsWith("https://") && !e.icon.startsWith("device_icons/")) {
|
||||
errors.push(`Device icon of '${e.friendly_name}' should start with 'device_icons/', got '${e.icon}'`);
|
||||
}
|
||||
if (e.qos != null && ![0, 1, 2].includes(e.qos)) {
|
||||
errors.push(`QOS for '${e.friendly_name}' not valid, should be 0, 1 or 2 got ${e.qos}`);
|
||||
}
|
||||
};
|
||||
|
||||
const settingsWithDefaults = get();
|
||||
@@ -321,6 +313,19 @@ export function validate(): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateNonRequired(): string[] {
|
||||
getPersistedSettings();
|
||||
|
||||
if (!ajvSetting(_settings)) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: When `ajvSetting()` return false it always has `errors`
|
||||
const errors = ajvSetting.errors!.filter((e) => e.keyword !== "required");
|
||||
|
||||
return errors.map((v) => `${v.instancePath.substring(1)} ${v.message}`);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function read(): Partial<Settings> {
|
||||
const s = yaml.read(CONFIG_FILE_PATH) as Partial<Settings>;
|
||||
|
||||
@@ -478,12 +483,12 @@ export function apply(settings: Record<string, unknown>, throwOnError = true): b
|
||||
const newSettings = objectAssignDeep.noMutate(_settings, settings);
|
||||
|
||||
utils.removeNullPropertiesFromObject(newSettings, NULLABLE_SETTINGS);
|
||||
ajvSetting(newSettings);
|
||||
|
||||
if (throwOnError) {
|
||||
const errors = ajvSetting.errors?.filter((e) => e.keyword !== "required");
|
||||
if (!ajvSetting(newSettings) && throwOnError) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: When `ajvSetting()` return false it always has `errors`
|
||||
const errors = ajvSetting.errors!.filter((e) => e.keyword !== "required");
|
||||
|
||||
if (errors?.length) {
|
||||
if (errors.length) {
|
||||
const error = errors[0];
|
||||
throw new Error(`${error.instancePath.substring(1)} ${error.message}`);
|
||||
}
|
||||
|
||||
+14
-5
@@ -12,6 +12,8 @@ import data from "./data";
|
||||
|
||||
const BASE64_IMAGE_REGEX = /data:image\/(?<extension>.+);base64,(?<data>.+)/;
|
||||
|
||||
export const DEFAULT_BIND_GROUP_ID = 901;
|
||||
|
||||
function pad(num: number): string {
|
||||
const norm = Math.floor(Math.abs(num));
|
||||
return (norm < 10 ? "0" : "") + norm;
|
||||
@@ -200,14 +202,21 @@ function containsControlCharacter(str: string): boolean {
|
||||
|
||||
function getAllFiles(path_: string): string[] {
|
||||
const result = [];
|
||||
for (let item of fs.readdirSync(path_)) {
|
||||
item = path.join(path_, item);
|
||||
if (fs.lstatSync(item).isFile()) {
|
||||
result.push(item);
|
||||
|
||||
for (const item of fs.readdirSync(path_, {withFileTypes: true})) {
|
||||
if (item.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileName = path.join(path_, item.name);
|
||||
|
||||
if (fs.lstatSync(fileName).isFile()) {
|
||||
result.push(fileName);
|
||||
} else {
|
||||
result.push(...getAllFiles(item));
|
||||
result.push(...getAllFiles(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export class YAMLFileException extends YAMLException {
|
||||
function read(file: string): KeyValue {
|
||||
try {
|
||||
const result = yaml.load(fs.readFileSync(file, "utf8"));
|
||||
assert(result instanceof Object);
|
||||
assert(result instanceof Object, `The content of ${file} is expected to be an object`);
|
||||
return result as KeyValue;
|
||||
} catch (error) {
|
||||
if (error instanceof YAMLException) {
|
||||
|
||||
+13
-6
@@ -15,8 +15,7 @@ import utils from "./util/utils";
|
||||
const entityIDRegex = /^(.+?)(?:\/([^/]+))?$/;
|
||||
|
||||
export default class Zigbee {
|
||||
// @ts-expect-error initialized in start
|
||||
private herdsman: Controller;
|
||||
private herdsman!: Controller;
|
||||
private eventBus: EventBus;
|
||||
private groupLookup = new Map<number /* group ID */, Group>();
|
||||
private deviceLookup = new Map<string /* IEEE address */, Device>();
|
||||
@@ -266,12 +265,20 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
private resolveGroup(groupID: number): Group | undefined {
|
||||
const group = this.herdsman.getGroupByID(Number(groupID));
|
||||
if (group && !this.groupLookup.has(groupID)) {
|
||||
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
|
||||
if (!this.groupLookup.has(groupID)) {
|
||||
const group = this.herdsman.getGroupByID(groupID);
|
||||
|
||||
if (group) {
|
||||
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
|
||||
}
|
||||
}
|
||||
|
||||
return this.groupLookup.get(groupID);
|
||||
const group = this.groupLookup.get(groupID);
|
||||
|
||||
if (group) {
|
||||
group.ensureInSettings();
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
resolveEntity(key: string | number | zh.Device): Device | Group | undefined {
|
||||
|
||||
+8
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "2.5.1",
|
||||
"version": "2.6.0",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"types": "dist/types/api.d.ts",
|
||||
@@ -31,6 +31,7 @@
|
||||
"test": "vitest run --config ./test/vitest.config.mts",
|
||||
"test:coverage": "vitest run --config ./test/vitest.config.mts --coverage",
|
||||
"test:watch": "vitest watch --config ./test/vitest.config.mts",
|
||||
"bench": "vitest bench --run --config ./test/vitest.config.mts",
|
||||
"prepack": "pnpm run clean && pnpm run build",
|
||||
"clean": "rimraf coverage dist tsconfig.tsbuildinfo"
|
||||
},
|
||||
@@ -62,13 +63,14 @@
|
||||
"winston-syslog": "^2.7.1",
|
||||
"winston-transport": "^4.9.0",
|
||||
"ws": "^8.18.1",
|
||||
"zigbee-herdsman": "4.3.1",
|
||||
"zigbee-herdsman-converters": "23.69.0",
|
||||
"zigbee2mqtt-frontend": "0.9.16",
|
||||
"zigbee2mqtt-windfront": "^1.2.1"
|
||||
"zigbee-herdsman": "5.0.4",
|
||||
"zigbee-herdsman-converters": "24.11.0",
|
||||
"zigbee2mqtt-frontend": "0.9.19",
|
||||
"zigbee2mqtt-windfront": "^1.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.0.5",
|
||||
"@codspeed/vitest-plugin": "^4.0.1",
|
||||
"@types/finalhandler": "^1.2.3",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
@@ -99,4 +101,4 @@
|
||||
"optionalDependencies": {
|
||||
"unix-dgram": "^2.0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+365
-102
@@ -5,7 +5,7 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
zigbee-herdsman: 4.3.1
|
||||
zigbee-herdsman: 5.0.4
|
||||
|
||||
importers:
|
||||
|
||||
@@ -46,7 +46,7 @@ importers:
|
||||
version: 2.30.1
|
||||
mqtt:
|
||||
specifier: ^5.11.0
|
||||
version: 5.13.1
|
||||
version: 5.13.3
|
||||
object-assign-deep:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
@@ -75,21 +75,24 @@ importers:
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.3
|
||||
zigbee-herdsman:
|
||||
specifier: 4.3.1
|
||||
version: 4.3.1
|
||||
specifier: 5.0.4
|
||||
version: 5.0.4
|
||||
zigbee-herdsman-converters:
|
||||
specifier: 23.69.0
|
||||
version: 23.69.0
|
||||
specifier: 24.11.0
|
||||
version: 24.11.0
|
||||
zigbee2mqtt-frontend:
|
||||
specifier: 0.9.16
|
||||
version: 0.9.16
|
||||
specifier: 0.9.19
|
||||
version: 0.9.19
|
||||
zigbee2mqtt-windfront:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
specifier: ^1.5.2
|
||||
version: 1.5.2
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.6
|
||||
version: 2.1.2
|
||||
'@codspeed/vitest-plugin':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(vite@6.3.5(@types/node@24.0.15))(vitest@3.2.4(@types/node@24.0.15))
|
||||
'@types/finalhandler':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.4
|
||||
@@ -101,7 +104,7 @@ importers:
|
||||
version: 4.0.9
|
||||
'@types/node':
|
||||
specifier: ^24.0.3
|
||||
version: 24.0.8
|
||||
version: 24.0.15
|
||||
'@types/object-assign-deep':
|
||||
specifier: ^0.4.3
|
||||
version: 0.4.3
|
||||
@@ -116,7 +119,7 @@ importers:
|
||||
version: 8.18.1
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.1.1
|
||||
version: 3.2.4(vitest@3.2.4(@types/node@24.0.8))
|
||||
version: 3.2.4(vitest@3.2.4(@types/node@24.0.15))
|
||||
tmp:
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
@@ -125,7 +128,7 @@ importers:
|
||||
version: 5.8.3
|
||||
vitest:
|
||||
specifier: ^3.1.1
|
||||
version: 3.2.4(@types/node@24.0.8)
|
||||
version: 3.2.4(@types/node@24.0.15)
|
||||
optionalDependencies:
|
||||
unix-dgram:
|
||||
specifier: ^2.0.6
|
||||
@@ -150,8 +153,8 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.27.4':
|
||||
resolution: {integrity: sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==}
|
||||
'@babel/runtime@7.27.6':
|
||||
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.27.6':
|
||||
@@ -162,59 +165,68 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@biomejs/biome@2.0.6':
|
||||
resolution: {integrity: sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==}
|
||||
'@biomejs/biome@2.1.2':
|
||||
resolution: {integrity: sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.0.6':
|
||||
resolution: {integrity: sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==}
|
||||
'@biomejs/cli-darwin-arm64@2.1.2':
|
||||
resolution: {integrity: sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.0.6':
|
||||
resolution: {integrity: sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==}
|
||||
'@biomejs/cli-darwin-x64@2.1.2':
|
||||
resolution: {integrity: sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.0.6':
|
||||
resolution: {integrity: sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==}
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.2':
|
||||
resolution: {integrity: sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.0.6':
|
||||
resolution: {integrity: sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==}
|
||||
'@biomejs/cli-linux-arm64@2.1.2':
|
||||
resolution: {integrity: sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.0.6':
|
||||
resolution: {integrity: sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==}
|
||||
'@biomejs/cli-linux-x64-musl@2.1.2':
|
||||
resolution: {integrity: sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64@2.0.6':
|
||||
resolution: {integrity: sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==}
|
||||
'@biomejs/cli-linux-x64@2.1.2':
|
||||
resolution: {integrity: sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.0.6':
|
||||
resolution: {integrity: sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==}
|
||||
'@biomejs/cli-win32-arm64@2.1.2':
|
||||
resolution: {integrity: sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@2.0.6':
|
||||
resolution: {integrity: sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==}
|
||||
'@biomejs/cli-win32-x64@2.1.2':
|
||||
resolution: {integrity: sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@codspeed/core@4.0.1':
|
||||
resolution: {integrity: sha512-fJ53arfgtzCDZa8DuGJhpTZ3Ll9A1uW5nQ2jSJnfO4Hl5MRD2cP8P4vPvIUAGbdbjwCxR1jat6cW8OloMJkJXw==}
|
||||
|
||||
'@codspeed/vitest-plugin@4.0.1':
|
||||
resolution: {integrity: sha512-aqmrPJzX9cD50UWDsOyih5L5WcEYlNQg3u84sJJ9ZuuLApA51w+LGxk6Xbyb8LJF9n/CwM94HKHV/qArfnvDoQ==}
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
|
||||
vitest: '>=1.2.2'
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
@@ -549,8 +561,8 @@ packages:
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/node@24.0.8':
|
||||
resolution: {integrity: sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==}
|
||||
'@types/node@24.0.15':
|
||||
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==}
|
||||
|
||||
'@types/object-assign-deep@0.4.3':
|
||||
resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
|
||||
@@ -644,6 +656,12 @@ packages:
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
axios@1.10.0:
|
||||
resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -682,6 +700,10 @@ packages:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
chai@5.2.0:
|
||||
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -712,6 +734,10 @@ packages:
|
||||
colorspace@1.1.4:
|
||||
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
commist@3.2.0:
|
||||
resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==}
|
||||
|
||||
@@ -752,6 +778,10 @@ packages:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -760,6 +790,10 @@ packages:
|
||||
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
@@ -779,9 +813,25 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-module-lexer@1.7.0:
|
||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.25.5:
|
||||
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -840,13 +890,30 @@ packages:
|
||||
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
find-up@6.3.0:
|
||||
resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
fn.name@1.1.0:
|
||||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||
|
||||
follow-redirects@1.15.9:
|
||||
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.3:
|
||||
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -856,6 +923,17 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
hasBin: true
|
||||
@@ -869,10 +947,26 @@ packages:
|
||||
resolution: {integrity: sha512-mTCC51QFadK75MvAhrL5nPVIP291NjML1guo10Sa7Yj04tJU4V++Vgm780NIddg9etQD9D8FM67hFGqM8EE2HQ==}
|
||||
engines: {node: '>= 0.2.5'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
@@ -971,6 +1065,10 @@ packages:
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
locate-path@7.2.0:
|
||||
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
logform@2.7.0:
|
||||
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -995,10 +1093,22 @@ packages:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@3.0.1:
|
||||
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1028,8 +1138,8 @@ packages:
|
||||
mqtt-packet@9.0.2:
|
||||
resolution: {integrity: sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==}
|
||||
|
||||
mqtt@5.13.1:
|
||||
resolution: {integrity: sha512-g+4G+ma0UeL3Pgu1y1si2NHb4VLIEUCtF789WrG99lLG0XZyoab2EJoy58YgGSg/1yFdthDBH0+4llsZZD/vug==}
|
||||
mqtt@5.13.3:
|
||||
resolution: {integrity: sha512-91x03kh1+vBBA51OMNbEw2fymXfaUjpHkC0NcMckg9Vf6ee/GrM/HXfE8XeeziHQpJL8adr+9ThTbN5v/WmrRA==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1070,6 +1180,14 @@ packages:
|
||||
one-time@1.0.0:
|
||||
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
||||
|
||||
p-limit@4.0.0:
|
||||
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
p-locate@6.0.0:
|
||||
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
@@ -1080,6 +1198,10 @@ packages:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-exists@5.0.0:
|
||||
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1117,6 +1239,9 @@ packages:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1206,8 +1331,8 @@ packages:
|
||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
socks@2.8.4:
|
||||
resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==}
|
||||
socks@2.8.6:
|
||||
resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
@@ -1465,22 +1590,26 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
zigbee-herdsman-converters@23.69.0:
|
||||
resolution: {integrity: sha512-JHppwhLJEf8P4cSa3T0bF0nDS3Kwk2Bk1ZKtH3ec/1jUFwUGY+L9OFoz1Q0JF3L/lIR2E5ioehl5TgGG61YjVg==}
|
||||
yocto-queue@1.2.1:
|
||||
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
zigbee-herdsman@4.3.1:
|
||||
resolution: {integrity: sha512-oFUJZr6Ms/buPaOExoVR37JMG7ty68l+AKUCRezkfhL2kkj7HHzdtFpHll/4W2miRTIokvtsKLG/4FOyJCuH4g==}
|
||||
zigbee-herdsman-converters@24.11.0:
|
||||
resolution: {integrity: sha512-95qnZCZjScc/ET3zO/nkzis3QbfPRYg2IMCjoeOwxNCvRczVHEP9ptj1Fj1vK0s1kgTpyB1gSdYAfeYyxa+zcw==}
|
||||
|
||||
zigbee-on-host@0.1.12:
|
||||
resolution: {integrity: sha512-ZhYaDIZpQlgSKzcrfoJgQ85UYk26nzRILFBF2n9GUpiyBFrKOQ5zSqLCQW8XroiD9ndFPDiiKzunmsty20GyfQ==}
|
||||
engines: {node: '>=20.17.0'}
|
||||
zigbee-herdsman@5.0.4:
|
||||
resolution: {integrity: sha512-WeopbhVlIUyQZm2lm6hh+R35FJ9WZbJ8bKoQiQXoS9Myu3oJRrQFQPPqCnDSzG1djfPparVXTetgadImm2Ytgg==}
|
||||
|
||||
zigbee2mqtt-frontend@0.9.16:
|
||||
resolution: {integrity: sha512-z+f/kYi7GEnybr2DRbjPybF1uEDbPovZFpjLHf40/bWvYVF+NSNOEzHVGlSaCf7vnMD2CqAZOWa7Lg4I69hQZg==}
|
||||
zigbee-on-host@0.1.13:
|
||||
resolution: {integrity: sha512-mL7s9ic7J85YI7wOG1/QlbRYE3haFzIHqgnM2+xLmjj8CazjhmGjop9TCfuY2JXyDI9ss7bNCLxuI9n6M+TXug==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
||||
zigbee2mqtt-frontend@0.9.19:
|
||||
resolution: {integrity: sha512-HOlfiY9xn2+u18TN/HXoXlfuGS5662TkveJeu6q8mVHvViyn9SVH3WUJG9sRbcnbqGHtIruBiUit1U8azxIgzA==}
|
||||
engines: {node: '>=20.11'}
|
||||
|
||||
zigbee2mqtt-windfront@1.2.1:
|
||||
resolution: {integrity: sha512-RQ8o5NUdoA3mYe/s0LMJgKjFfEpdkLteOE55r3+sAxIhE1NrpidjzYrVRqhwtWg2VI9tp2neoRhUi7T2/A2yaA==}
|
||||
zigbee2mqtt-windfront@1.5.2:
|
||||
resolution: {integrity: sha512-YzXY82dX2y+TywZ1QhpzRThoqwWMYIn/LMxohOfXO3qDzv+2uG9NUzQYAt6B9yaTinnZej5CP5nTTbIvOLVb4g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
||||
snapshots:
|
||||
@@ -1498,7 +1627,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.27.6
|
||||
|
||||
'@babel/runtime@7.27.4': {}
|
||||
'@babel/runtime@7.27.6': {}
|
||||
|
||||
'@babel/types@7.27.6':
|
||||
dependencies:
|
||||
@@ -1507,41 +1636,58 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@biomejs/biome@2.0.6':
|
||||
'@biomejs/biome@2.1.2':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 2.0.6
|
||||
'@biomejs/cli-darwin-x64': 2.0.6
|
||||
'@biomejs/cli-linux-arm64': 2.0.6
|
||||
'@biomejs/cli-linux-arm64-musl': 2.0.6
|
||||
'@biomejs/cli-linux-x64': 2.0.6
|
||||
'@biomejs/cli-linux-x64-musl': 2.0.6
|
||||
'@biomejs/cli-win32-arm64': 2.0.6
|
||||
'@biomejs/cli-win32-x64': 2.0.6
|
||||
'@biomejs/cli-darwin-arm64': 2.1.2
|
||||
'@biomejs/cli-darwin-x64': 2.1.2
|
||||
'@biomejs/cli-linux-arm64': 2.1.2
|
||||
'@biomejs/cli-linux-arm64-musl': 2.1.2
|
||||
'@biomejs/cli-linux-x64': 2.1.2
|
||||
'@biomejs/cli-linux-x64-musl': 2.1.2
|
||||
'@biomejs/cli-win32-arm64': 2.1.2
|
||||
'@biomejs/cli-win32-x64': 2.1.2
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.0.6':
|
||||
'@biomejs/cli-darwin-arm64@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.0.6':
|
||||
'@biomejs/cli-darwin-x64@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.0.6':
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.0.6':
|
||||
'@biomejs/cli-linux-arm64@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.0.6':
|
||||
'@biomejs/cli-linux-x64-musl@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@2.0.6':
|
||||
'@biomejs/cli-linux-x64@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.0.6':
|
||||
'@biomejs/cli-win32-arm64@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@2.0.6':
|
||||
'@biomejs/cli-win32-x64@2.1.2':
|
||||
optional: true
|
||||
|
||||
'@codspeed/core@4.0.1':
|
||||
dependencies:
|
||||
axios: 1.10.0
|
||||
find-up: 6.3.0
|
||||
form-data: 4.0.3
|
||||
node-gyp-build: 4.8.4
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@codspeed/vitest-plugin@4.0.1(vite@6.3.5(@types/node@24.0.15))(vitest@3.2.4(@types/node@24.0.15))':
|
||||
dependencies:
|
||||
'@codspeed/core': 4.0.1
|
||||
vite: 6.3.5(@types/node@24.0.15)
|
||||
vitest: 3.2.4(@types/node@24.0.15)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
'@dabh/diagnostics@2.0.3':
|
||||
@@ -1753,7 +1899,7 @@ snapshots:
|
||||
|
||||
'@types/finalhandler@1.2.4':
|
||||
dependencies:
|
||||
'@types/node': 24.0.8
|
||||
'@types/node': 24.0.15
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
@@ -1763,7 +1909,7 @@ snapshots:
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/node@24.0.8':
|
||||
'@types/node@24.0.15':
|
||||
dependencies:
|
||||
undici-types: 7.8.0
|
||||
|
||||
@@ -1771,26 +1917,26 @@ snapshots:
|
||||
|
||||
'@types/readable-stream@4.0.21':
|
||||
dependencies:
|
||||
'@types/node': 24.0.8
|
||||
'@types/node': 24.0.15
|
||||
|
||||
'@types/send@0.17.5':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 24.0.8
|
||||
'@types/node': 24.0.15
|
||||
|
||||
'@types/serve-static@1.15.8':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 24.0.8
|
||||
'@types/node': 24.0.15
|
||||
'@types/send': 0.17.5
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 24.0.8
|
||||
'@types/node': 24.0.15
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.0.8))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.0.15))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
@@ -1805,7 +1951,7 @@ snapshots:
|
||||
std-env: 3.9.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/node@24.0.8)
|
||||
vitest: 3.2.4(@types/node@24.0.15)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -1817,13 +1963,13 @@ snapshots:
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.8))':
|
||||
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.15))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
vite: 6.3.5(@types/node@24.0.8)
|
||||
vite: 6.3.5(@types/node@24.0.15)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -1884,6 +2030,16 @@ snapshots:
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.10.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9
|
||||
form-data: 4.0.3
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
@@ -1926,6 +2082,11 @@ snapshots:
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
chai@5.2.0:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
@@ -1963,6 +2124,10 @@ snapshots:
|
||||
color: 3.2.1
|
||||
text-hex: 1.0.0
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
commist@3.2.0: {}
|
||||
|
||||
concat-stream@2.0.0:
|
||||
@@ -1992,12 +2157,20 @@ snapshots:
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dns-packet@5.6.1:
|
||||
dependencies:
|
||||
'@leichtgewicht/ip-codec': 2.0.5
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
@@ -2010,8 +2183,23 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
esbuild@0.25.5:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.5
|
||||
@@ -2066,7 +2254,7 @@ snapshots:
|
||||
|
||||
fast-unique-numbers@8.0.13:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
tslib: 2.8.1
|
||||
|
||||
fast-uri@3.0.6: {}
|
||||
@@ -2091,18 +2279,53 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
find-up@6.3.0:
|
||||
dependencies:
|
||||
locate-path: 7.2.0
|
||||
path-exists: 5.0.0
|
||||
|
||||
fn.name@1.1.0: {}
|
||||
|
||||
follow-redirects@1.15.9: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.3:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
glob@10.4.5:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
@@ -2123,8 +2346,20 @@ snapshots:
|
||||
|
||||
glossy@0.1.7: {}
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
@@ -2222,6 +2457,10 @@ snapshots:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
locate-path@7.2.0:
|
||||
dependencies:
|
||||
p-locate: 6.0.0
|
||||
|
||||
logform@2.7.0:
|
||||
dependencies:
|
||||
'@colors/colors': 1.6.0
|
||||
@@ -2251,8 +2490,16 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mime-types@3.0.1:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
@@ -2281,8 +2528,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mqtt@5.13.1:
|
||||
mqtt@5.13.3:
|
||||
dependencies:
|
||||
'@types/readable-stream': 4.0.21
|
||||
'@types/ws': 8.18.1
|
||||
commist: 3.2.0
|
||||
concat-stream: 2.0.0
|
||||
debug: 4.4.1
|
||||
@@ -2293,7 +2542,7 @@ snapshots:
|
||||
number-allocator: 1.0.14
|
||||
readable-stream: 4.7.0
|
||||
rfdc: 1.4.1
|
||||
socks: 2.8.4
|
||||
socks: 2.8.6
|
||||
split2: 4.2.0
|
||||
worker-timers: 7.1.8
|
||||
ws: 8.18.3
|
||||
@@ -2335,12 +2584,22 @@ snapshots:
|
||||
dependencies:
|
||||
fn.name: 1.1.0
|
||||
|
||||
p-limit@4.0.0:
|
||||
dependencies:
|
||||
yocto-queue: 1.2.1
|
||||
|
||||
p-locate@6.0.0:
|
||||
dependencies:
|
||||
p-limit: 4.0.0
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-exists@5.0.0: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
@@ -2371,6 +2630,8 @@ snapshots:
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
@@ -2489,7 +2750,7 @@ snapshots:
|
||||
|
||||
smart-buffer@4.2.0: {}
|
||||
|
||||
socks@2.8.4:
|
||||
socks@2.8.6:
|
||||
dependencies:
|
||||
ip-address: 9.0.5
|
||||
smart-buffer: 4.2.0
|
||||
@@ -2600,13 +2861,13 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite-node@3.2.4(@types/node@24.0.8):
|
||||
vite-node@3.2.4(@types/node@24.0.15):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.1
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 6.3.5(@types/node@24.0.8)
|
||||
vite: 6.3.5(@types/node@24.0.15)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -2621,7 +2882,7 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite@6.3.5(@types/node@24.0.8):
|
||||
vite@6.3.5(@types/node@24.0.15):
|
||||
dependencies:
|
||||
esbuild: 0.25.5
|
||||
fdir: 6.4.6(picomatch@4.0.2)
|
||||
@@ -2630,14 +2891,14 @@ snapshots:
|
||||
rollup: 4.44.0
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 24.0.8
|
||||
'@types/node': 24.0.15
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitest@3.2.4(@types/node@24.0.8):
|
||||
vitest@3.2.4(@types/node@24.0.15):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.8))
|
||||
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.15))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -2655,11 +2916,11 @@ snapshots:
|
||||
tinyglobby: 0.2.14
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 6.3.5(@types/node@24.0.8)
|
||||
vite-node: 3.2.4(@types/node@24.0.8)
|
||||
vite: 6.3.5(@types/node@24.0.15)
|
||||
vite-node: 3.2.4(@types/node@24.0.15)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.0.8
|
||||
'@types/node': 24.0.15
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
@@ -2714,19 +2975,19 @@ snapshots:
|
||||
|
||||
worker-timers-broker@6.1.8:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
fast-unique-numbers: 8.0.13
|
||||
tslib: 2.8.1
|
||||
worker-timers-worker: 7.0.71
|
||||
|
||||
worker-timers-worker@7.0.71:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
tslib: 2.8.1
|
||||
|
||||
worker-timers@7.1.8:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
tslib: 2.8.1
|
||||
worker-timers-broker: 6.1.8
|
||||
worker-timers-worker: 7.0.71
|
||||
@@ -2745,16 +3006,18 @@ snapshots:
|
||||
|
||||
ws@8.18.3: {}
|
||||
|
||||
zigbee-herdsman-converters@23.69.0:
|
||||
yocto-queue@1.2.1: {}
|
||||
|
||||
zigbee-herdsman-converters@24.11.0:
|
||||
dependencies:
|
||||
buffer-crc32: 1.0.0
|
||||
iconv-lite: 0.6.3
|
||||
semver: 7.7.2
|
||||
zigbee-herdsman: 4.3.1
|
||||
zigbee-herdsman: 5.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
zigbee-herdsman@4.3.1:
|
||||
zigbee-herdsman@5.0.4:
|
||||
dependencies:
|
||||
'@serialport/bindings-cpp': 13.0.1
|
||||
'@serialport/parser-delimiter': 13.0.0
|
||||
@@ -2764,12 +3027,12 @@ snapshots:
|
||||
fast-deep-equal: 3.1.3
|
||||
mixin-deep: 2.0.1
|
||||
slip: 1.0.2
|
||||
zigbee-on-host: 0.1.12
|
||||
zigbee-on-host: 0.1.13
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
zigbee-on-host@0.1.12: {}
|
||||
zigbee-on-host@0.1.13: {}
|
||||
|
||||
zigbee2mqtt-frontend@0.9.16: {}
|
||||
zigbee2mqtt-frontend@0.9.19: {}
|
||||
|
||||
zigbee2mqtt-windfront@1.2.1: {}
|
||||
zigbee2mqtt-windfront@1.5.2: {}
|
||||
|
||||
@@ -0,0 +1,672 @@
|
||||
import {existsSync, mkdirSync} from "node:fs";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, bench, describe} from "vitest";
|
||||
import {Zcl, Zdo, ZSpec} from "zigbee-herdsman";
|
||||
import type Adapter from "zigbee-herdsman/dist/adapter/adapter";
|
||||
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
|
||||
import {Device, InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
import {Endpoint} from "zigbee-herdsman/dist/controller/model/endpoint";
|
||||
import Entity from "zigbee-herdsman/dist/controller/model/entity";
|
||||
import {Group} from "zigbee-herdsman/dist/controller/model/group";
|
||||
import type {DeviceType} from "zigbee-herdsman/dist/controller/tstype";
|
||||
import {Foundation} from "zigbee-herdsman/dist/zspec/zcl/definition/foundation";
|
||||
import type {RequestToResponseMap} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
|
||||
import data from "../lib/util/data";
|
||||
|
||||
process.env.ZIGBEE2MQTT_DATA = "data-bench";
|
||||
data._testReload();
|
||||
|
||||
if (!existsSync(data.getPath())) {
|
||||
mkdirSync(data.getPath(), {recursive: true});
|
||||
}
|
||||
|
||||
const createEndpoint = (id: number, ieeeAddr: string, networkAddress: number) => {
|
||||
const ep = Endpoint.create(
|
||||
id,
|
||||
id === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID,
|
||||
id === ZSpec.GP_ENDPOINT ? 0x66 : 0x65,
|
||||
id === ZSpec.GP_ENDPOINT ? [Zcl.Clusters.greenPower.ID] : [Zcl.Clusters.genBasic.ID, Zcl.Clusters.genOnOff.ID],
|
||||
id === ZSpec.GP_ENDPOINT ? [Zcl.Clusters.greenPower.ID] : [Zcl.Clusters.genBasic.ID],
|
||||
networkAddress,
|
||||
ieeeAddr,
|
||||
);
|
||||
ep.save = () => {};
|
||||
|
||||
return ep;
|
||||
};
|
||||
// `Device.create` requires `Database`, bypass it by using the private constructor directly
|
||||
const createDevice = (
|
||||
dbId: number,
|
||||
type: DeviceType,
|
||||
ieeeAddr: string,
|
||||
networkAddress: number,
|
||||
manufacturerID: number | undefined,
|
||||
manufacturerName: string | undefined,
|
||||
powerSource: string | undefined,
|
||||
modelID: string | undefined,
|
||||
): Device => {
|
||||
const haEp = createEndpoint(ZSpec.HA_ENDPOINT, ieeeAddr, networkAddress);
|
||||
const ep2 = createEndpoint(2, ieeeAddr, networkAddress);
|
||||
const endpoints = [haEp, ep2];
|
||||
|
||||
if (type === "Coordinator" || type === "Router") {
|
||||
const gpEp = createEndpoint(ZSpec.GP_ENDPOINT, ieeeAddr, networkAddress);
|
||||
|
||||
endpoints.push(gpEp);
|
||||
}
|
||||
|
||||
// @ts-expect-error mocking private
|
||||
const device = new Device(
|
||||
dbId,
|
||||
type,
|
||||
ieeeAddr,
|
||||
networkAddress,
|
||||
manufacturerID,
|
||||
endpoints,
|
||||
manufacturerName,
|
||||
powerSource,
|
||||
modelID,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
InterviewState.Successful,
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// in-memory only
|
||||
device.save = () => {};
|
||||
|
||||
return device;
|
||||
};
|
||||
// `Group.create` requires `Database`, bypass it by using the private constructor directly
|
||||
const createGroup = (dbId: number, id: number): Group => {
|
||||
// @ts-expect-error mocking private
|
||||
const group = new Group(dbId, id, [], {});
|
||||
|
||||
// in-memory only
|
||||
group.save = () => {};
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const COORD_IEEE = "0x0101010101010101";
|
||||
const EXT_PAN_ID = [0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd];
|
||||
const NETWORK_PARAMS = {
|
||||
panID: 0x1a62,
|
||||
extendedPanID: `0x${Buffer.from(EXT_PAN_ID).toString("hex")}`,
|
||||
channel: 11,
|
||||
nwkUpdateID: 1,
|
||||
};
|
||||
const NETWORK_KEY = [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13];
|
||||
const ZH_DEVICES: Device[] = [];
|
||||
const ZH_GROUPS: Group[] = [];
|
||||
const MANY_DEVICES = 100;
|
||||
|
||||
const initDevices = () => {
|
||||
ZH_DEVICES.splice(0, ZH_DEVICES.length);
|
||||
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
0,
|
||||
"Coordinator",
|
||||
COORD_IEEE,
|
||||
ZSpec.COORDINATOR_ADDRESS,
|
||||
Zcl.ManufacturerCode.SILICON_LABORATORIES,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(1, "Router", "0xf1f1f1f1f1f1f1f1", 0x0001, Zcl.ManufacturerCode.INNR_LIGHTING_BV, "Innr", "Mains (single phase)", "AE 262"),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(2, "EndDevice", "0xe2e2e2e2e2e2e2e2", 0x0002, Zcl.ManufacturerCode.TUYA_GLOBAL_INC, "_TYZB01_kvwjujy9", "Battery", "TS0222"),
|
||||
);
|
||||
ZH_DEVICES.push(createDevice(3, "GreenPower", "0x00000000015d3d3d", 0x0003, undefined, undefined, undefined, "GreenPower_7"));
|
||||
// these have configure, without setTimeout, they hammer really badly (# of fn calls), so, only one of each
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
4,
|
||||
"Router",
|
||||
"0xd3d3d3d3d3d3d3d3",
|
||||
0x0004,
|
||||
Zcl.ManufacturerCode.LEDVANCE_GMBH,
|
||||
"LEDVANCE",
|
||||
"Mains (single phase)",
|
||||
"PLUG OUTDOOR EU T",
|
||||
),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(5, "Router", "0xc4c4c4c4c4c4c4c4", 0x0005, Zcl.ManufacturerCode.INOVELLI, "Inovelli", "Mains (single phase)", "VZM35-SN"),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
6,
|
||||
"Router",
|
||||
"0xb5b5b5b5b5b5b5b5",
|
||||
0x0006,
|
||||
Zcl.ManufacturerCode.SILICON_LABORATORIES,
|
||||
"SMLIGHT",
|
||||
"Mains (single phase)",
|
||||
"SLZB-06Mg24",
|
||||
),
|
||||
);
|
||||
ZH_DEVICES.push(
|
||||
createDevice(
|
||||
7,
|
||||
"Router",
|
||||
"0xa6a6a6a6a6a6a6a6",
|
||||
0x0007,
|
||||
Zcl.ManufacturerCode.TUYA_GLOBAL_INC,
|
||||
"_TZE200_p0gzbqct",
|
||||
"Mains (single phase)",
|
||||
"TS0601",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const initGroups = () => {
|
||||
ZH_GROUPS.splice(0, ZH_GROUPS.length);
|
||||
|
||||
ZH_GROUPS.push(createGroup(0, 1));
|
||||
};
|
||||
|
||||
const addManyDevices = () => {
|
||||
for (let i = 0; i < MANY_DEVICES; i++) {
|
||||
const ieee = `0xf1f1f1f1f1f1f1${i.toString(16).padStart(2, "0")}`;
|
||||
|
||||
// device without `configure` (too many calls otherwise)
|
||||
ZH_DEVICES.push(createDevice(1, "Router", ieee, 0x0008 + i, Zcl.ManufacturerCode.INNR_LIGHTING_BV, "Innr", "Mains (single phase)", "AE 262"));
|
||||
}
|
||||
};
|
||||
|
||||
const getMidDeviceIeee = () =>
|
||||
`0xf1f1f1f1f1f1f1${Math.floor(MANY_DEVICES / 2)
|
||||
.toString(16)
|
||||
.padStart(2, "0")}`;
|
||||
|
||||
Device.byIeeeAddr = (ieeeAddr, _includeDeleted) => ZH_DEVICES.find((device) => device.ieeeAddr === ieeeAddr);
|
||||
Device.byType = (type) => ZH_DEVICES.filter((device) => device.type === type);
|
||||
|
||||
const adapter = {
|
||||
sendZclFrameToEndpoint: async (
|
||||
_ieeeAddr: string,
|
||||
networkAddress: number,
|
||||
endpoint: number,
|
||||
zclFrame: Zcl.Frame,
|
||||
_timeout: number,
|
||||
disableResponse: boolean,
|
||||
_disableRecovery: boolean,
|
||||
sourceEndpoint?: number,
|
||||
): Promise<ZclPayload | undefined> => {
|
||||
const payload: {[key: string]: unknown}[] = [];
|
||||
|
||||
if (!disableResponse) {
|
||||
if (zclFrame.header.isGlobal) {
|
||||
switch (zclFrame.command.ID) {
|
||||
case Foundation.read.ID: {
|
||||
for (const attr of zclFrame.payload) {
|
||||
const attribute = zclFrame.cluster.getAttribute(attr.attrId);
|
||||
|
||||
if (attribute && attribute.type !== Zcl.DataType.NO_DATA && attribute.type < Zcl.DataType.OCTET_STR) {
|
||||
payload.push({
|
||||
attrId: attr.attrId,
|
||||
dataType: attribute.type,
|
||||
attrData: 1,
|
||||
status: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const messageContents = Zcl.Frame.create(
|
||||
0,
|
||||
Zcl.Direction.SERVER_TO_CLIENT,
|
||||
true,
|
||||
undefined,
|
||||
10,
|
||||
Foundation.readRsp.ID,
|
||||
zclFrame.cluster.ID,
|
||||
payload,
|
||||
{},
|
||||
).toBuffer();
|
||||
|
||||
return await Promise.resolve({
|
||||
clusterID: zclFrame.cluster.ID,
|
||||
header: Zcl.Header.fromBuffer(messageContents),
|
||||
address: networkAddress,
|
||||
data: messageContents,
|
||||
endpoint: sourceEndpoint ?? 1,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
wasBroadcast: false,
|
||||
destinationEndpoint: endpoint,
|
||||
});
|
||||
}
|
||||
case Foundation.configReport.ID: {
|
||||
for (const item of zclFrame.payload) {
|
||||
payload.push({attrId: item.attrId, status: 0, direction: 1});
|
||||
}
|
||||
|
||||
const messageContents = Zcl.Frame.create(
|
||||
0,
|
||||
Zcl.Direction.SERVER_TO_CLIENT,
|
||||
true,
|
||||
undefined,
|
||||
10,
|
||||
Foundation.configReportRsp.ID,
|
||||
zclFrame.cluster.ID,
|
||||
payload,
|
||||
{},
|
||||
).toBuffer();
|
||||
|
||||
return await Promise.resolve({
|
||||
clusterID: zclFrame.cluster.ID,
|
||||
header: Zcl.Header.fromBuffer(messageContents),
|
||||
address: networkAddress,
|
||||
data: messageContents,
|
||||
endpoint: sourceEndpoint ?? 1,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
wasBroadcast: false,
|
||||
destinationEndpoint: endpoint,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.resolve(undefined);
|
||||
},
|
||||
sendZclFrameToGroup: async (_groupID: number, _zclFrame: Zcl.Frame, _sourceEndpoint?: number): Promise<void> => await Promise.resolve(),
|
||||
sendZdo: async (
|
||||
_ieeeAddress: string,
|
||||
_networkAddress: number,
|
||||
clusterId: Zdo.ClusterId,
|
||||
_payload: Buffer,
|
||||
_disableResponse: boolean,
|
||||
): Promise<RequestToResponseMap[keyof RequestToResponseMap] | undefined> => {
|
||||
switch (clusterId) {
|
||||
case Zdo.ClusterId.BIND_REQUEST:
|
||||
case Zdo.ClusterId.UNBIND_REQUEST: {
|
||||
return await Promise.resolve([Zdo.Status.SUCCESS, undefined]);
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.resolve(undefined);
|
||||
},
|
||||
};
|
||||
|
||||
Entity.injectAdapter(adapter as Adapter);
|
||||
|
||||
// use plain type to avoid early import that otherwise messes with data path
|
||||
let controller: import("../lib/controller.js").Controller;
|
||||
const origSetImmediate = global.setImmediate;
|
||||
const origSetTimeout = global.setTimeout;
|
||||
|
||||
const mockGlobalThis = () => {
|
||||
const setImmediateProms: (void | Promise<void>)[] = [];
|
||||
const setTimeoutProms: (void | Promise<void>)[] = [];
|
||||
|
||||
// @ts-expect-error mock
|
||||
globalThis.setImmediate = (callback: () => void) => {
|
||||
setImmediateProms.push(callback());
|
||||
};
|
||||
// @ts-expect-error mock
|
||||
globalThis.setTimeout = (callback: () => void) => {
|
||||
setTimeoutProms.push(callback());
|
||||
};
|
||||
|
||||
return {setImmediateProms, setTimeoutProms};
|
||||
};
|
||||
|
||||
const unmockGlobalThis = () => {
|
||||
globalThis.setImmediate = origSetImmediate;
|
||||
globalThis.setTimeout = origSetTimeout;
|
||||
};
|
||||
|
||||
const settle = async (mockedGlobal: ReturnType<typeof mockGlobalThis>) => {
|
||||
await Promise.allSettled(mockedGlobal.setImmediateProms);
|
||||
await Promise.allSettled(mockedGlobal.setTimeoutProms);
|
||||
await new Promise((resolve) => origSetImmediate(resolve));
|
||||
};
|
||||
|
||||
const initSettings = async (pathValuePairs?: [string[], string | number | boolean][]) => {
|
||||
const settings = await import("../lib/util/settings.js");
|
||||
|
||||
settings.writeMinimalDefaults();
|
||||
// disable logging, too much influence on perf
|
||||
settings.set(["advanced", "log_level"], "error");
|
||||
settings.set(["advanced", "log_output"], []);
|
||||
settings.set(["advanced", "pan_id"], NETWORK_PARAMS.panID);
|
||||
settings.set(["advanced", "ext_pan_id"], EXT_PAN_ID);
|
||||
settings.set(["advanced", "network_key"], NETWORK_KEY);
|
||||
|
||||
if (pathValuePairs) {
|
||||
for (const [path, value] of pathValuePairs) {
|
||||
settings.set(path, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initController = async () => {
|
||||
const {Controller} = await import("../lib/controller.js");
|
||||
|
||||
controller = new Controller(
|
||||
async () => {},
|
||||
async () => {},
|
||||
);
|
||||
|
||||
controller.zigbee.start = async () => {
|
||||
await controller.zigbee.resolveDevicesDefinitions();
|
||||
|
||||
return "resumed";
|
||||
};
|
||||
controller.zigbee.stop = async () => {
|
||||
await Promise.resolve();
|
||||
};
|
||||
// @ts-expect-error mocking private
|
||||
controller.zigbee.herdsman = {
|
||||
isStopping: () => false,
|
||||
getCoordinatorVersion: async () =>
|
||||
Promise.resolve({
|
||||
type: "Dummy",
|
||||
meta: {revision: "9.9.9"},
|
||||
}),
|
||||
getNetworkParameters: async () => Promise.resolve({...NETWORK_PARAMS}),
|
||||
getPermitJoin: () => false,
|
||||
getPermitJoinEnd: () => undefined,
|
||||
getDeviceByIeeeAddr: (ieeeAddr) => ZH_DEVICES.find((device) => device.ieeeAddr === ieeeAddr),
|
||||
getGroupByID: (id) => ZH_GROUPS.find((group) => group.groupID === id),
|
||||
getDevicesByType: (type) => ZH_DEVICES.filter((device) => device.type === type),
|
||||
getDeviceByNetworkAddress: (networkAddress) => ZH_DEVICES.find((device) => device.networkAddress === networkAddress),
|
||||
*getDevicesIterator(predicate) {
|
||||
for (const device of ZH_DEVICES) {
|
||||
if (!predicate || predicate(device)) {
|
||||
yield device;
|
||||
}
|
||||
}
|
||||
},
|
||||
*getGroupsIterator(predicate) {
|
||||
for (const group of ZH_GROUPS) {
|
||||
if (!predicate || predicate(group)) {
|
||||
yield group;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// all dummies, can trigger `controller.mqtt.onMessage(topic, message)` as needed
|
||||
// @ts-expect-error mocking private
|
||||
controller.mqtt.client = {
|
||||
options: {
|
||||
protocolVersion: 5,
|
||||
protocol: "mqtt",
|
||||
host: "localhost",
|
||||
port: 1883,
|
||||
},
|
||||
queue: [],
|
||||
reconnecting: false,
|
||||
disconnecting: false,
|
||||
disconnected: false,
|
||||
endAsync: async () => {},
|
||||
// @ts-expect-error Z2M does not make use of return
|
||||
publishAsync: async () => {},
|
||||
};
|
||||
controller.mqtt.connect = async () => {
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.onConnect();
|
||||
};
|
||||
controller.mqtt.subscribe = async () => {};
|
||||
controller.mqtt.unsubscribe = async () => {};
|
||||
|
||||
// will be in-memory only
|
||||
controller.state.start = () => {};
|
||||
controller.state.stop = () => {};
|
||||
};
|
||||
|
||||
describe("Controller with dummy zigbee/mqtt", () => {
|
||||
describe("defaults start & stop", () => {
|
||||
beforeEach(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings();
|
||||
await initController();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[defaults] start & stop controller",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
await settle(mockedGlobal);
|
||||
|
||||
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
|
||||
throw new Error("Invalid");
|
||||
}
|
||||
|
||||
await controller.stop();
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("HA start & stop", () => {
|
||||
beforeEach(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings([[["homeassistant", "enabled"], true]]);
|
||||
await initController();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[HA] start & stop controller",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
|
||||
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
|
||||
throw new Error("Invalid");
|
||||
}
|
||||
|
||||
await controller.stop();
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("defaults runtime", () => {
|
||||
beforeAll(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings();
|
||||
await initController();
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
await settle(mockedGlobal);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.stop();
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[defaults] receive device message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitDeviceMessage({
|
||||
type: "attributeReport",
|
||||
device: controller.zigbee.resolveEntity("0xf1f1f1f1f1f1f1f1"),
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
cluster: "genOnOff",
|
||||
data: {onOff: 1},
|
||||
meta: {},
|
||||
});
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[defaults] receive MQTT message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[defaults] add group member",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitMQTTMessage({
|
||||
topic: "zigbee2mqtt/bridge/request/group/members/add",
|
||||
message: stringify({
|
||||
device: "0xf1f1f1f1f1f1f1f1",
|
||||
group: `${ZH_GROUPS[0].groupID}`,
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
}),
|
||||
});
|
||||
|
||||
await settle(mockedGlobal);
|
||||
|
||||
if (ZH_GROUPS[0].members.length !== 1) {
|
||||
throw new Error("Invalid state");
|
||||
}
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("defaults/stress runtime", () => {
|
||||
beforeAll(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
addManyDevices();
|
||||
await initSettings();
|
||||
await initController();
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
await settle(mockedGlobal);
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.stop();
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
// this is mostly just to confirm the number of devices does not influence the processing (much)
|
||||
bench(
|
||||
"[defaults/stress] receive device message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitDeviceMessage({
|
||||
type: "attributeReport",
|
||||
device: controller.zigbee.resolveEntity(getMidDeviceIeee()),
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
cluster: "genOnOff",
|
||||
data: {onOff: 1},
|
||||
meta: {},
|
||||
});
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
|
||||
describe("HA runtime", () => {
|
||||
beforeAll(async () => {
|
||||
initDevices();
|
||||
initGroups();
|
||||
await initSettings([[["homeassistant", "enabled"], true]]);
|
||||
await initController();
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
await controller.start();
|
||||
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.stop();
|
||||
unmockGlobalThis();
|
||||
});
|
||||
|
||||
bench(
|
||||
"[HA] receive device message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.eventBus.emitDeviceMessage({
|
||||
type: "attributeReport",
|
||||
device: controller.zigbee.resolveEntity("0xf1f1f1f1f1f1f1f1"),
|
||||
endpoint: ZSpec.HA_ENDPOINT,
|
||||
linkquality: 200,
|
||||
groupID: 0,
|
||||
cluster: "genOnOff",
|
||||
data: {onOff: 1},
|
||||
meta: {},
|
||||
});
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[HA] receive MQTT message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
|
||||
bench(
|
||||
"[HA] receive MQTT discovery message",
|
||||
async () => {
|
||||
const mockedGlobal = mockGlobalThis();
|
||||
|
||||
controller.mqtt.onMessage(
|
||||
"homeassistant/sensor/0xe2e2e2e2e2e2e2e2/update/config",
|
||||
Buffer.from(stringify({availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}]}), "utf8"),
|
||||
);
|
||||
await settle(mockedGlobal);
|
||||
},
|
||||
{throws: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
+9
-20
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "./mocks/data";
|
||||
import {mockLogger} from "./mocks/logger";
|
||||
import {
|
||||
@@ -55,7 +56,6 @@ describe("Controller", () => {
|
||||
let mockExit: Mock;
|
||||
|
||||
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDevice)! as Device;
|
||||
};
|
||||
|
||||
@@ -112,6 +112,8 @@ describe("Controller", () => {
|
||||
expect(mockMQTTConnectAsync).toHaveBeenCalledWith("mqtt://localhost", {
|
||||
will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: "zigbee2mqtt/bridge/state", qos: 1},
|
||||
properties: {maximumPacketSize: 1048576},
|
||||
keepalive: 60,
|
||||
protocolVersion: 4,
|
||||
});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bulb",
|
||||
@@ -293,11 +295,9 @@ describe("Controller", () => {
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.publish("z2m/#/status", "empty");
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(`Topic 'z2m/#/status' includes wildcard characters, skipping publish.`);
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.publish("z2m/+/status", "empty");
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(`Topic 'z2m/+/status' includes wildcard characters, skipping publish.`);
|
||||
@@ -410,14 +410,15 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("Start controller adapter disconnects", async () => {
|
||||
mockZHController.stop.mockRejectedValueOnce("failed");
|
||||
// Fail to stop extension exit code 1 should not override adapter disconnect exit code 2
|
||||
vi.spyOn(Array.from(controller.extensions)[0], "stop").mockRejectedValueOnce(new Error("failed"));
|
||||
await controller.start();
|
||||
await mockZHEvents.adapterDisconnected();
|
||||
await flushPromises();
|
||||
expect(mockMQTTEndAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.stop).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1, false);
|
||||
expect(mockExit).toHaveBeenCalledWith(2, false);
|
||||
});
|
||||
|
||||
it("does not throw when extension fails to stop on controller stop", async () => {
|
||||
@@ -500,7 +501,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("Handle mqtt message", async () => {
|
||||
// @ts-expect-error private
|
||||
const spyEventbusEmitMQTTMessage = vi.spyOn(controller.eventBus, "emitMQTTMessage").mockImplementation(vi.fn());
|
||||
|
||||
await controller.start();
|
||||
@@ -511,7 +511,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("Skip MQTT messages on topic we published to", async () => {
|
||||
// @ts-expect-error private
|
||||
const spyEventbusEmitMQTTMessage = vi.spyOn(controller.eventBus, "emitMQTTMessage").mockImplementation(vi.fn());
|
||||
|
||||
await controller.start();
|
||||
@@ -519,7 +518,6 @@ describe("Controller", () => {
|
||||
await mockMQTTEvents.message("zigbee2mqtt/skip-this-topic", "skipped");
|
||||
expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: "zigbee2mqtt/skip-this-topic", message: "skipped"});
|
||||
mockLogger.debug.mockClear();
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.publish("skip-this-topic", "", {});
|
||||
await mockMQTTEvents.message("zigbee2mqtt/skip-this-topic", "skipped");
|
||||
expect(mockLogger.debug).toHaveBeenCalledTimes(0);
|
||||
@@ -841,13 +839,11 @@ describe("Controller", () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
const device = getZ2MDevice("bulb");
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON"});
|
||||
|
||||
await controller.publishEntityState(device, {state: "ON", brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 200, color_temp: 370, state: "ON"});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {qos: 0, retain: true});
|
||||
@@ -867,13 +863,11 @@ describe("Controller", () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
const device = getZ2MDevice("bulb");
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON"});
|
||||
|
||||
await controller.publishEntityState(device, {state: "ON", brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 200, color_temp: 370, state: "ON"});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {qos: 0, retain: true});
|
||||
@@ -1045,6 +1039,8 @@ describe("Controller", () => {
|
||||
expect(mockMQTTConnectAsync).toHaveBeenCalledTimes(1);
|
||||
const expected = {
|
||||
will: {payload: Buffer.from('{"state":"offline"}'), retain: false, topic: "zigbee2mqtt/bridge/state", qos: 1},
|
||||
keepalive: 60,
|
||||
protocolVersion: 4,
|
||||
properties: {maximumPacketSize: 1048576},
|
||||
};
|
||||
expect(mockMQTTConnectAsync).toHaveBeenCalledWith("mqtt://localhost", expected);
|
||||
@@ -1054,10 +1050,7 @@ describe("Controller", () => {
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
|
||||
const retainedMessages = Object.keys(
|
||||
// @ts-expect-error private
|
||||
controller.mqtt.retainedMessages,
|
||||
).length;
|
||||
const retainedMessages = Object.keys(controller.mqtt.retainedMessages).length;
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
await vi.advanceTimersByTimeAsync(2500); // before any startup configure triggers
|
||||
@@ -1078,7 +1071,6 @@ describe("Controller", () => {
|
||||
|
||||
it("Should prevent any message being published with retain flag when force_disable_retain is set", async () => {
|
||||
settings.set(["mqtt", "force_disable_retain"], true);
|
||||
// @ts-expect-error private
|
||||
await controller.mqtt.connect();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
@@ -1136,7 +1128,6 @@ describe("Controller", () => {
|
||||
it("Should remove state of removed device when stopped", async () => {
|
||||
await controller.start();
|
||||
const device = getZ2MDevice("bulb");
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: "ON"});
|
||||
device.zh.isDeleted = true;
|
||||
await controller.stop();
|
||||
@@ -1145,7 +1136,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("EventBus should handle sync errors", async () => {
|
||||
// @ts-expect-error private
|
||||
const eventbus = controller.eventBus;
|
||||
const callback = vi.fn().mockImplementation(() => {
|
||||
throw new Error("Whoops!");
|
||||
@@ -1158,7 +1148,6 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("EventBus should handle async errors", async () => {
|
||||
// @ts-expect-error private
|
||||
const eventbus = controller.eventBus;
|
||||
const callback = vi.fn().mockRejectedValue(new Error("Whoops!"));
|
||||
eventbus.onStateChange({constructor: {name: "Test"}}, callback);
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import path from "node:path";
|
||||
|
||||
import tmp from "tmp";
|
||||
|
||||
import {describe, expect, it} from "vitest";
|
||||
import data from "../lib/util/data";
|
||||
|
||||
describe("Data", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -281,9 +282,7 @@ describe("Extension: Availability", () => {
|
||||
});
|
||||
|
||||
it("Should retrieve device state when it reconnects", async () => {
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "OFF"});
|
||||
|
||||
const endpoint = devices.bulb_color.getEndpoint(1);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockDebounce} from "../mocks/debounce";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
@@ -10,6 +11,7 @@ import stringify from "json-stable-stringify-without-jsonify";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Bind from "../../lib/extension/bind";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
|
||||
const mocksClear = [
|
||||
mockDebounce,
|
||||
@@ -648,9 +650,9 @@ describe("Extension: Bind", () => {
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/unbind", stringify({from: "remote", to: target}));
|
||||
await flushPromises();
|
||||
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", DEFAULT_BIND_GROUP_ID);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", DEFAULT_BIND_GROUP_ID);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", DEFAULT_BIND_GROUP_ID);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/unbind",
|
||||
stringify({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockJSZipFile, mockJSZipGenerateAsync} from "../mocks/jszip";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
@@ -8,13 +9,14 @@ import {CUSTOM_CLUSTERS, devices, groups, mockController as mockZHController, ev
|
||||
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs";
|
||||
import {platform} from "node:os";
|
||||
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";
|
||||
import utils from "../../lib/util/utils";
|
||||
import utils, {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
|
||||
returnDevices.push(devices.coordinator.ieeeAddr);
|
||||
returnDevices.push(devices.bulb.ieeeAddr);
|
||||
@@ -309,6 +311,9 @@ describe("Extension: Bridge", () => {
|
||||
force_disable_retain: false,
|
||||
include_device_information: false,
|
||||
maximum_packet_size: 1048576,
|
||||
keepalive: 60,
|
||||
reject_unauthorized: true,
|
||||
version: 4,
|
||||
server: "mqtt://localhost",
|
||||
},
|
||||
ota: {
|
||||
@@ -347,7 +352,7 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
it("Should publish devices on startup", async () => {
|
||||
await resetExtension();
|
||||
// console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/devices')[1]);
|
||||
// console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "zigbee2mqtt/bridge/devices")[1]);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/devices",
|
||||
stringify([
|
||||
@@ -365,6 +370,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm",
|
||||
exposes: [
|
||||
{
|
||||
@@ -622,6 +628,7 @@ describe("Extension: Bridge", () => {
|
||||
{
|
||||
date_code: "2019.09",
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "Hue Go",
|
||||
exposes: [
|
||||
{
|
||||
@@ -817,6 +824,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "Hue dimmer switch",
|
||||
exposes: [
|
||||
{
|
||||
@@ -920,6 +928,7 @@ describe("Extension: Bridge", () => {
|
||||
disabled: false,
|
||||
endpoints: {
|
||||
"1": {
|
||||
name: "ep1",
|
||||
bindings: [
|
||||
{cluster: "genLevelCtrl", target: {endpoint: 1, ieee_address: "0x000b57fffec6a5b3", type: "endpoint"}},
|
||||
{cluster: "genOnOff", target: {endpoint: 1, ieee_address: "0x000b57fffec6a5b3", type: "endpoint"}},
|
||||
@@ -931,7 +940,13 @@ describe("Extension: Bridge", () => {
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
"2": {bindings: [], clusters: {input: ["genBasic"], output: ["genOta", "genOnOff"]}, configured_reportings: [], scenes: []},
|
||||
"2": {
|
||||
name: "ep2",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic"], output: ["genOta", "genOnOff"]},
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
},
|
||||
friendly_name: "remote",
|
||||
ieee_address: "0x0017880104e45517",
|
||||
@@ -946,6 +961,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "generated",
|
||||
description: "Automatically generated definition",
|
||||
exposes: [
|
||||
{
|
||||
@@ -1040,6 +1056,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "Wireless mini switch",
|
||||
exposes: [
|
||||
{
|
||||
@@ -1115,6 +1132,7 @@ describe("Extension: Bridge", () => {
|
||||
name: "device_temperature_calibration",
|
||||
property: "device_temperature_calibration",
|
||||
type: "numeric",
|
||||
value_step: 0.1,
|
||||
},
|
||||
],
|
||||
supports_ota: false,
|
||||
@@ -1150,6 +1168,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "Temperature and humidity sensor",
|
||||
exposes: [
|
||||
{
|
||||
@@ -1223,6 +1242,7 @@ describe("Extension: Bridge", () => {
|
||||
name: "temperature_calibration",
|
||||
property: "temperature_calibration",
|
||||
type: "numeric",
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -1242,6 +1262,7 @@ describe("Extension: Bridge", () => {
|
||||
name: "humidity_calibration",
|
||||
property: "humidity_calibration",
|
||||
type: "numeric",
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -1261,6 +1282,7 @@ describe("Extension: Bridge", () => {
|
||||
name: "pressure_calibration",
|
||||
property: "pressure_calibration",
|
||||
type: "numeric",
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -1292,6 +1314,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "Mi smart plug",
|
||||
exposes: [
|
||||
{
|
||||
@@ -1371,6 +1394,7 @@ describe("Extension: Bridge", () => {
|
||||
name: "power_calibration",
|
||||
property: "power_calibration",
|
||||
type: "numeric",
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -1390,6 +1414,7 @@ describe("Extension: Bridge", () => {
|
||||
name: "energy_calibration",
|
||||
property: "energy_calibration",
|
||||
type: "numeric",
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -1409,6 +1434,7 @@ describe("Extension: Bridge", () => {
|
||||
name: "device_temperature_calibration",
|
||||
property: "device_temperature_calibration",
|
||||
type: "numeric",
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -1439,6 +1465,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "zigfred plus smart in-wall switch",
|
||||
exposes: [
|
||||
{
|
||||
@@ -1884,42 +1911,49 @@ describe("Extension: Bridge", () => {
|
||||
disabled: false,
|
||||
endpoints: {
|
||||
"10": {
|
||||
name: "l5",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
"11": {
|
||||
name: "l6",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic", "genScenes", "closuresWindowCovering"], output: []},
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
"12": {
|
||||
name: "l7",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic", "genScenes", "closuresWindowCovering"], output: []},
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
"5": {
|
||||
name: "l1",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl", "lightingColorCtrl"], output: []},
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
"7": {
|
||||
name: "l2",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
"8": {
|
||||
name: "l3",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
|
||||
configured_reportings: [],
|
||||
scenes: [],
|
||||
},
|
||||
"9": {
|
||||
name: "l4",
|
||||
bindings: [],
|
||||
clusters: {input: ["genBasic", "genScenes", "genOnOff", "genLevelCtrl"], output: []},
|
||||
configured_reportings: [],
|
||||
@@ -1940,6 +1974,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm",
|
||||
exposes: [
|
||||
{
|
||||
@@ -2285,7 +2320,7 @@ describe("Extension: Bridge", () => {
|
||||
scenes: [],
|
||||
},
|
||||
{friendly_name: "gledopto_group", id: 21, members: [{endpoint: 15, ieee_address: "0x0017880104e45724"}], scenes: []},
|
||||
{friendly_name: "default_bind_group", id: 901, members: [], scenes: []},
|
||||
{friendly_name: "default_bind_group", id: DEFAULT_BIND_GROUP_ID, members: [], scenes: []},
|
||||
{
|
||||
friendly_name: "ha_discovery_group",
|
||||
id: 9,
|
||||
@@ -2374,6 +2409,7 @@ describe("Extension: Bridge", () => {
|
||||
stringify({
|
||||
data: {
|
||||
definition: {
|
||||
source: "native",
|
||||
description: "TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm",
|
||||
exposes: [
|
||||
{
|
||||
@@ -2609,6 +2645,7 @@ describe("Extension: Bridge", () => {
|
||||
stringify({
|
||||
data: {
|
||||
definition: {
|
||||
source: "generated",
|
||||
description: "Automatically generated definition",
|
||||
exposes: [
|
||||
{
|
||||
@@ -3144,7 +3181,6 @@ describe("Extension: Bridge", () => {
|
||||
});
|
||||
|
||||
it("Should allow interviewing a device by ieeeAddr", async () => {
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb)!;
|
||||
assert("resolveDefinition" in device);
|
||||
device.resolveDefinition = vi.fn();
|
||||
@@ -3806,6 +3842,11 @@ describe("Extension: Bridge", () => {
|
||||
fs.writeFileSync(path.join(data.mockDir, "log", "log.log"), "test123");
|
||||
fs.mkdirSync(path.join(data.mockDir, "ext_converters", "123"));
|
||||
fs.writeFileSync(path.join(data.mockDir, "ext_converters", "123", "myfile.js"), "test123");
|
||||
fs.symlinkSync(
|
||||
path.join(data.mockDir, "ext_converters"),
|
||||
path.join(data.mockDir, "ext_converters_sym"),
|
||||
platform() === "win32" ? "junction" : "dir",
|
||||
);
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/backup", "");
|
||||
await flushPromises();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -120,11 +121,9 @@ describe("Extension: Configure", () => {
|
||||
|
||||
it("Should reconfigure reporting on reconfigure event", async () => {
|
||||
expectBulbConfigured();
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb)!;
|
||||
mockClear(device.zh);
|
||||
expectBulbNotConfigured();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitReconfigure({device});
|
||||
await flushPromises();
|
||||
expectBulbConfigured();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTEndAsync, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -49,7 +50,6 @@ describe("Extension: ExternalConverters", () => {
|
||||
};
|
||||
|
||||
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDevice)! as Device;
|
||||
};
|
||||
|
||||
@@ -72,7 +72,6 @@ describe("Extension: ExternalConverters", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
zhc.removeExternalDefinitions(); // remove all external converters
|
||||
// @ts-expect-error private - clear cached
|
||||
await controller.zigbee.resolveDevicesDefinitions(true);
|
||||
for (const mock of mocksClear) mock.mockClear();
|
||||
data.writeDefaultConfiguration();
|
||||
@@ -348,6 +347,7 @@ describe("Extension: ExternalConverters", () => {
|
||||
);
|
||||
expect(fs.existsSync(filepath)).toStrictEqual(false);
|
||||
expect(fs.existsSync(path.join(mockBasePath, "invalid.mjs.invalid"))).toStrictEqual(true);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Parse failure: Expected ';', '}' or <eof>"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTEndAsync, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -486,4 +487,17 @@ describe("Extension: Frontend", () => {
|
||||
|
||||
await vi.waitFor(() => controller.getExtension("Frontend") === undefined);
|
||||
});
|
||||
|
||||
it("disables serving", async () => {
|
||||
settings.set(["frontend", "disable_ui_serving"], true);
|
||||
controller = new Controller(vi.fn(), vi.fn());
|
||||
await controller.start();
|
||||
|
||||
expect(mockHTTP.listen).toHaveBeenCalledTimes(0);
|
||||
mockWS.clients.push(mockWSClient);
|
||||
await controller.stop();
|
||||
expect(mockWSClient.terminate).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTP.close).toHaveBeenCalledTimes(0);
|
||||
expect(mockWS.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -44,7 +45,6 @@ describe("Extension: Groups", () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
groups.gledopto_group.command.mockClear();
|
||||
zhcGlobalStore.clear();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
});
|
||||
|
||||
@@ -459,7 +459,6 @@ describe("Extension: Groups", () => {
|
||||
await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
|
||||
await flushPromises();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
|
||||
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
|
||||
@@ -506,7 +505,7 @@ describe("Extension: Groups", () => {
|
||||
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/group/members/add",
|
||||
stringify({data: {}, status: "error", error: "Failed to add from group (timeout)"}),
|
||||
stringify({data: {}, status: "error", error: "Failed to add to group (timeout)"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
@@ -591,6 +590,26 @@ describe("Extension: Groups", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("Remove from group via MQTT fails", async () => {
|
||||
const device = devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
const group = groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
endpoint.removeFromGroup.mockImplementationOnce(() => {
|
||||
throw new Error("timeout");
|
||||
});
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1", device: "bulb_color"}));
|
||||
await flushPromises();
|
||||
expect(group.members.length).toStrictEqual(1);
|
||||
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/group/members/remove",
|
||||
stringify({data: {}, status: "error", error: "Failed to remove from group (timeout)"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("Remove from group via MQTT keeping device reporting", async () => {
|
||||
const device = devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, test, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync, mockMQTTSubscribeAsync, mockMQTTUnsubscribeAsync} from "../mocks/mqtt";
|
||||
@@ -55,7 +56,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
};
|
||||
|
||||
const getZ2MEntity = (zhDeviceOrGroup: string | number | ZhDevice): Device | Group => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDeviceOrGroup)!;
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
|
||||
for (const baseDefinition of await getZhcBaseDefinitions()) {
|
||||
const d = zhc.prepareDefinition(baseDefinition);
|
||||
const exposes = typeof d.exposes === "function" ? d.exposes(undefined, undefined) : d.exposes;
|
||||
const exposes = typeof d.exposes === "function" ? d.exposes({isDummyDevice: true}, {}) : d.exposes;
|
||||
const device = {
|
||||
definition: d,
|
||||
isDevice: (): boolean => true,
|
||||
@@ -1179,9 +1179,9 @@ describe("Extension: HomeAssistant", () => {
|
||||
command_topic: "zigbee2mqtt/0xa4c138018cf95021/left/set",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0xa4c138018cf95021"],
|
||||
manufacturer: "Lonsonho",
|
||||
model: "Dual curtain/blind module",
|
||||
model_id: "TS130F_dual",
|
||||
manufacturer: "Girier",
|
||||
model: "Dual smart curtain switch",
|
||||
model_id: "TS130F_GIRIER_DUAL",
|
||||
name: "0xa4c138018cf95021",
|
||||
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
@@ -1209,9 +1209,9 @@ describe("Extension: HomeAssistant", () => {
|
||||
command_topic: "zigbee2mqtt/0xa4c138018cf95021/right/set",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0xa4c138018cf95021"],
|
||||
manufacturer: "Lonsonho",
|
||||
model: "Dual curtain/blind module",
|
||||
model_id: "TS130F_dual",
|
||||
manufacturer: "Girier",
|
||||
model: "Dual smart curtain switch",
|
||||
model_id: "TS130F_GIRIER_DUAL",
|
||||
name: "0xa4c138018cf95021",
|
||||
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
@@ -1230,6 +1230,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
value_template: '{% if "moving" in value_json and value_json.moving %} {{ value_json.moving }} {% else %} STOP {% endif %}',
|
||||
};
|
||||
|
||||
console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "homeassistant/cover/0xa4c138018cf95021/cover_left/config"));
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xa4c138018cf95021/cover_left/config", stringify(payload_left), {
|
||||
retain: true,
|
||||
qos: 1,
|
||||
@@ -1464,7 +1466,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
assert("ieeeAddr" in device);
|
||||
resetDiscoveryPayloads(device.ieeeAddr);
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitEntityOptionsChanged({entity: device, from: {}, to: {test: 123}});
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(`homeassistant/light/${device.ID}/light/config`, expect.any(String), expect.any(Object));
|
||||
@@ -2384,7 +2385,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
resetDiscoveryPayloads(device.ieeeAddr);
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitScenesChanged({entity: device});
|
||||
await flushPromises();
|
||||
|
||||
@@ -2421,7 +2421,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
resetDiscoveryPayloads("9");
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitScenesChanged({entity: group});
|
||||
await flushPromises();
|
||||
|
||||
@@ -2483,7 +2482,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
availability_mode: "all",
|
||||
};
|
||||
|
||||
// @ts-expect-error private
|
||||
controller.eventBus.emitMQTTMessage({
|
||||
topic: topic,
|
||||
message: stringify(payload),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -24,7 +25,6 @@ describe("Extension: OnEvent", () => {
|
||||
let deviceOnEventSpy: MockInstance<DefinitionOnEvent>;
|
||||
|
||||
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
|
||||
// @ts-expect-error private
|
||||
return controller.zigbee.resolveEntity(zhDevice)! as Device;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -67,7 +68,6 @@ describe("Extension: OTAUpdate", () => {
|
||||
devices.bulb.mockClear();
|
||||
updateSpy.mockClear();
|
||||
isUpdateAvailableSpy.mockClear();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
});
|
||||
|
||||
@@ -747,12 +747,9 @@ describe("Extension: OTAUpdate", () => {
|
||||
});
|
||||
|
||||
it("clear update state on startup", async () => {
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {update: {progress: 100, remaining: 10, state: "updating"}});
|
||||
await resetExtension();
|
||||
// @ts-expect-error private
|
||||
expect(controller.state.get(device)).toStrictEqual({update: {state: "available"}});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -41,7 +42,6 @@ describe("Extension: Publish", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
data.writeDefaultConfiguration();
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
settings.reRead();
|
||||
loadTopicGetSetRegex();
|
||||
@@ -1256,11 +1256,8 @@ describe("Extension: Publish", () => {
|
||||
|
||||
it("Home Assistant: should not set state when color temperature is also set and device is already on", async () => {
|
||||
settings.set(["homeassistant"], {enabled: true});
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!;
|
||||
// @ts-expect-error private
|
||||
controller.state.remove(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "ON"});
|
||||
const endpoint = device.zh.getEndpoint(1)!;
|
||||
const payload = {state: "ON", color_temp: 100};
|
||||
@@ -1275,11 +1272,8 @@ describe("Extension: Publish", () => {
|
||||
|
||||
it("Home Assistant: should set state when color temperature is also set and device is off", async () => {
|
||||
settings.set(["homeassistant"], {enabled: true});
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!;
|
||||
// @ts-expect-error private
|
||||
controller.state.remove(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "OFF"});
|
||||
const endpoint = device.zh.getEndpoint(1)!;
|
||||
const payload = {state: "ON", color_temp: 100};
|
||||
@@ -1298,11 +1292,8 @@ describe("Extension: Publish", () => {
|
||||
|
||||
it("Home Assistant: should not set state when color is also set", async () => {
|
||||
settings.set(["homeassistant"], {enabled: true});
|
||||
// @ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!;
|
||||
// @ts-expect-error private
|
||||
controller.state.remove(devices.bulb_color.ieeeAddr);
|
||||
// @ts-expect-error private
|
||||
controller.state.set(device, {state: "ON"});
|
||||
const endpoint = device.zh.getEndpoint(1)!;
|
||||
const payload = {state: "ON", color: {x: 0.41, y: 0.25}};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -24,7 +25,6 @@ describe("Extension: Receive", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error private
|
||||
controller.state.clear();
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
@@ -339,7 +339,6 @@ describe("Extension: Receive", () => {
|
||||
type: "attributeReport",
|
||||
linkquality: 10,
|
||||
});
|
||||
// @ts-expect-error private
|
||||
const realDevice = controller.zigbee.resolveEntity(device);
|
||||
|
||||
// Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {AdapterTypes} from "zigbee-herdsman";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
import type {EventHandler} from "./utils";
|
||||
|
||||
type ZHConfiguredReporting = {
|
||||
@@ -505,7 +505,7 @@ export const groups = {
|
||||
thermostat_group: new Group(12, [TS0601_thermostat.endpoints[0]]),
|
||||
group_with_switch: new Group(14, [ZNCZ02LM.endpoints[0], bulb_2.endpoints[0]]),
|
||||
gledopto_group: new Group(21, [GLEDOPTO_2ID.endpoints[3]]),
|
||||
default_bind_group: new Group(901, []),
|
||||
default_bind_group: new Group(DEFAULT_BIND_GROUP_ID, []),
|
||||
ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[2]]),
|
||||
hue_twilight_group: new Group(19, [hue_twilight.endpoints[1]]),
|
||||
};
|
||||
|
||||
+92
-3
@@ -1,8 +1,9 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import {rmSync} from "node:fs";
|
||||
|
||||
import {rmSync, writeFileSync} from "node:fs";
|
||||
import {join} from "node:path";
|
||||
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";
|
||||
@@ -752,7 +753,13 @@ describe("Onboarding", () => {
|
||||
});
|
||||
|
||||
it("handles validation failure", async () => {
|
||||
settings.set(["serial", "adapter"], "emberz");
|
||||
const reReadSpy = vi.spyOn(settings, "reRead");
|
||||
|
||||
// set after onboarding server is done to reach bottom code path
|
||||
reReadSpy.mockImplementationOnce(() => {
|
||||
settings.set(["serial", "adapter"], "emberz");
|
||||
settings.reRead();
|
||||
});
|
||||
|
||||
let p;
|
||||
const getHtml = await new Promise<string>((resolve, reject) => {
|
||||
@@ -769,6 +776,88 @@ describe("Onboarding", () => {
|
||||
|
||||
await expect(p).resolves.toStrictEqual(false);
|
||||
expect(getHtml).toContain("adapter must be equal to one of the allowed values");
|
||||
|
||||
reReadSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("handles non-required validation failure before applying envs", async () => {
|
||||
settings.set(["serial"], "/dev/ttyUSB0");
|
||||
|
||||
let p;
|
||||
const getHtml = await new Promise<string>((resolve, reject) => {
|
||||
mockHttpOnListen.mockImplementationOnce(async () => {
|
||||
try {
|
||||
resolve(await runFailure());
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
p = onboard();
|
||||
});
|
||||
|
||||
await expect(p).resolves.toStrictEqual(false);
|
||||
expect(getHtml).toContain("serial must be object");
|
||||
});
|
||||
|
||||
it("handles invalid yaml file", async () => {
|
||||
settings.testing.clear();
|
||||
|
||||
const configFile = join(data.mockDir, "configuration.yaml");
|
||||
|
||||
writeFileSync(
|
||||
configFile,
|
||||
`
|
||||
good: 9
|
||||
\t wrong
|
||||
`,
|
||||
);
|
||||
|
||||
let p;
|
||||
const getHtml = await new Promise<string>((resolve, reject) => {
|
||||
mockHttpOnListen.mockImplementationOnce(async () => {
|
||||
try {
|
||||
resolve(await runFailure());
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
p = onboard();
|
||||
});
|
||||
|
||||
await expect(p).resolves.toStrictEqual(false);
|
||||
expect(getHtml).toContain("Your configuration file");
|
||||
expect(getHtml).toContain("is invalid");
|
||||
|
||||
data.removeConfiguration();
|
||||
});
|
||||
|
||||
it("handles error while loading yaml file", async () => {
|
||||
settings.testing.clear();
|
||||
|
||||
const configFile = join(data.mockDir, "configuration.yaml");
|
||||
|
||||
writeFileSync(configFile, "badfile");
|
||||
|
||||
let p;
|
||||
const getHtml = await new Promise<string>((resolve, reject) => {
|
||||
mockHttpOnListen.mockImplementationOnce(async () => {
|
||||
try {
|
||||
resolve(await runFailure());
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
p = onboard();
|
||||
});
|
||||
|
||||
await expect(p).resolves.toStrictEqual(false);
|
||||
expect(getHtml).toContain("AssertionError");
|
||||
expect(getHtml).toContain("expected to be an object");
|
||||
|
||||
data.removeConfiguration();
|
||||
});
|
||||
|
||||
it("handles creating data path", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import {mockLogger} from "./mocks/logger";
|
||||
|
||||
import {initSdNotify} from "../lib/util/sd-notify";
|
||||
|
||||
+48
-21
@@ -1,8 +1,8 @@
|
||||
// side-effect ensures using mock paths
|
||||
import {beforeEach, describe, expect, it} from "vitest";
|
||||
import "./mocks/data";
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import objectAssignDeep from "object-assign-deep";
|
||||
|
||||
@@ -165,6 +165,43 @@ describe("Settings", () => {
|
||||
writeAndCheck();
|
||||
});
|
||||
|
||||
it("Should apply Home Assistant environment variables", () => {
|
||||
// should be kept in sync with envs in https://github.com/zigbee2mqtt/hassio-zigbee2mqtt/blob/master/common/rootfs/docker-entrypoint.sh
|
||||
process.env.ZIGBEE2MQTT_CONFIG_FRONTEND_ENABLED = "true";
|
||||
process.env.ZIGBEE2MQTT_CONFIG_FRONTEND_PORT = "8099";
|
||||
process.env.ZIGBEE2MQTT_CONFIG_HOMEASSISTANT_ENABLED = "true";
|
||||
process.env.ZIGBEE2MQTT_CONFIG_SERIAL_PORT =
|
||||
"/dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_48be7a7468d8ed11bfea786ff2c613ac-if00-port0";
|
||||
process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER = "mqtt://core-mosquitto:1883";
|
||||
process.env.ZIGBEE2MQTT_CONFIG_MQTT_USER = "addons";
|
||||
process.env.ZIGBEE2MQTT_CONFIG_MQTT_PASSWORD = "7RbR4NBSskS8TKG4ugzdRilXw6TPQ4NKFs259j2DxeultOFtl5JwciNZFd6feT5o";
|
||||
|
||||
write(configurationFile, {});
|
||||
|
||||
// @ts-expect-error workaround
|
||||
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
|
||||
expected.frontend.enabled = true;
|
||||
expected.frontend.port = 8099;
|
||||
expected.homeassistant.enabled = true;
|
||||
expected.serial.port = "/dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_48be7a7468d8ed11bfea786ff2c613ac-if00-port0";
|
||||
expected.mqtt.server = "mqtt://core-mosquitto:1883";
|
||||
expected.mqtt.user = "addons";
|
||||
expected.mqtt.password = "7RbR4NBSskS8TKG4ugzdRilXw6TPQ4NKFs259j2DxeultOFtl5JwciNZFd6feT5o";
|
||||
expected.devices = {};
|
||||
expected.groups = {};
|
||||
|
||||
const writeAndCheck = (): void => {
|
||||
expect(settings.write()); // trigger writing of ENVs
|
||||
expect(settings.validate()).toStrictEqual([]);
|
||||
expect(settings.get()).toStrictEqual(expected);
|
||||
};
|
||||
|
||||
// Write trice to ensure there are no side effects.
|
||||
writeAndCheck();
|
||||
writeAndCheck();
|
||||
writeAndCheck();
|
||||
});
|
||||
|
||||
it("Should write environment variables as overrides to configuration.yaml, not in the ref file", () => {
|
||||
write(secretFile, {password: "password-in-secret-file"}, false);
|
||||
write(configurationFile, {mqtt: {password: "!secret password", server: "server"}});
|
||||
@@ -275,6 +312,9 @@ describe("Settings", () => {
|
||||
base_topic: "zigbee2mqtt",
|
||||
include_device_information: false,
|
||||
maximum_packet_size: 1048576,
|
||||
keepalive: 60,
|
||||
reject_unauthorized: true,
|
||||
version: 4,
|
||||
force_disable_retain: false,
|
||||
password: "mysecretpassword",
|
||||
server: "my.mqtt.server",
|
||||
@@ -320,6 +360,9 @@ describe("Settings", () => {
|
||||
base_topic: "zigbee2mqtt",
|
||||
include_device_information: false,
|
||||
maximum_packet_size: 1048576,
|
||||
keepalive: 60,
|
||||
reject_unauthorized: true,
|
||||
version: 4,
|
||||
force_disable_retain: false,
|
||||
password: "mysecretpassword",
|
||||
server: "my.mqtt.server",
|
||||
@@ -638,6 +681,9 @@ describe("Settings", () => {
|
||||
base_topic: "zigbee2mqtt",
|
||||
include_device_information: false,
|
||||
maximum_packet_size: 1048576,
|
||||
keepalive: 60,
|
||||
reject_unauthorized: true,
|
||||
version: 4,
|
||||
force_disable_retain: false,
|
||||
server: "my.mqtt.server",
|
||||
user: "myusername",
|
||||
@@ -801,25 +847,6 @@ describe("Settings", () => {
|
||||
expect(settings.get().blocklist).toStrictEqual(["0x123", "0x1234"]);
|
||||
});
|
||||
|
||||
it("Should throw error when yaml file is invalid", () => {
|
||||
fs.writeFileSync(
|
||||
configurationFile,
|
||||
`
|
||||
good: 9
|
||||
\t wrong
|
||||
`,
|
||||
);
|
||||
|
||||
settings.testing.clear();
|
||||
const error = `Your YAML file: '${configurationFile}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it("Should throw error when yaml file does not exist", () => {
|
||||
settings.testing.clear();
|
||||
expect(settings.validate()[0]).toContain("ENOENT: no such file or directory, open ");
|
||||
});
|
||||
|
||||
it("Configuration shouldnt be valid when invalid QOS value is used", () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
@@ -828,7 +855,7 @@ describe("Settings", () => {
|
||||
|
||||
settings.reRead();
|
||||
|
||||
const error = `QOS for 'myname' not valid, should be 0, 1 or 2 got 3`;
|
||||
const error = "devices/0x0017880104e45519/qos must be equal to one of the allowed values";
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import {existsSync, readFileSync, rmSync} from "node:fs";
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "../tsconfig",
|
||||
"include": ["./**/*", "vitest.config.mts"],
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"rootDir": "..",
|
||||
"noEmit": true
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {exec} from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {describe, expect, it, vi} from "vitest";
|
||||
import utils, {assertString} from "../lib/util/utils";
|
||||
|
||||
// keep the implementations, just spy
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import codspeedPlugin from "@codspeed/vitest-plugin";
|
||||
import {defineConfig} from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [codspeedPlugin()],
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(_log: string, _type: "stdout" | "stderr"): boolean | undefined {
|
||||
|
||||
Reference in New Issue
Block a user