mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
110149fa20 | ||
|
|
445890a071 | ||
|
|
3dce8548ce | ||
|
|
4a020fd8de | ||
|
|
d411fc5c69 | ||
|
|
631c4f6a57 | ||
|
|
c02ed09211 | ||
|
|
49b4876768 | ||
|
|
802271ed37 | ||
|
|
42fc6e03a7 | ||
|
|
35d8a5da7f | ||
|
|
72d862f4a0 | ||
|
|
ad0696856a | ||
|
|
a61646d584 | ||
|
|
854df498bd | ||
|
|
e2232e9e91 | ||
|
|
6775c4f17b | ||
|
|
17520f7cfb | ||
|
|
1707bcab88 | ||
|
|
c577c8e5f0 | ||
|
|
7b8efbe2c4 | ||
|
|
459b695f60 | ||
|
|
f26ade4f93 | ||
|
|
3e2aefbcf8 | ||
|
|
ba77ce7c8d | ||
|
|
4c21b66079 | ||
|
|
be774a4d0d | ||
|
|
7b7b988960 | ||
|
|
d6bf5174be | ||
|
|
73225e2436 | ||
|
|
afedab9680 | ||
|
|
53b7eaabf0 | ||
|
|
16d0e56968 | ||
|
|
aea41b5c82 | ||
|
|
564e9ab9eb | ||
|
|
61c6ec6111 | ||
|
|
4794b3c81f | ||
|
|
517b286583 | ||
|
|
aee173d10a | ||
|
|
940fff244c | ||
|
|
8822aedb5e | ||
|
|
e788c520a9 | ||
|
|
39c5bc8c04 | ||
|
|
5c4e43fd10 | ||
|
|
5a6a9cf4d2 | ||
|
|
ad0e91e61e | ||
|
|
5797b56871 | ||
|
|
ea1c5a8f4e | ||
|
|
8099322cc2 | ||
|
|
619b939b14 | ||
|
|
eeed56ef2c | ||
|
|
e9b4a6221b |
@@ -22,13 +22,13 @@ jobs:
|
||||
actions: read # for Nerivec/action-ci-bench
|
||||
pull-requests: write # for Nerivec/action-ci-bench
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
with:
|
||||
# Required for `release: merge dev -> master and promote dev`
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Dependency review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Issue Bot
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.issue.labels.*.name, 'new device support') ||
|
||||
contains(github.event.issue.labels.*.name, 'external converter') ||
|
||||
startsWith(github.event.issue.title, '[New device support]') ||
|
||||
startsWith(github.event.issue.title, '[External Converter]')
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
path: z2m
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: Koenkk/zigbee-herdsman-converters
|
||||
ref: master
|
||||
fetch-depth: 1
|
||||
path: zhc
|
||||
|
||||
- name: Comment on new device support/external converter issue
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const {newDeviceSupport} = await import("${{ github.workspace }}/z2m/scripts/issueBot.mjs")
|
||||
await newDeviceSupport(github, core, context, "${{ github.workspace }}/zhc")
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
merge-master-to-dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: devmasx/merge-branch@master
|
||||
with:
|
||||
type: now
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
name: New Device Support/External Converter Issue Bot
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Comment on new device support/external converter issue
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const newDeviceSupport = issue.title && issue.title.startsWith("[New device support]");
|
||||
const externalConverter = issue.title && issue.title.startsWith("[External Converter]");
|
||||
|
||||
// Only proceed for new device support or external converter issues
|
||||
if (!newDeviceSupport && !externalConverter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide previous bot comments
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
});
|
||||
|
||||
for (const comment of comments.data) {
|
||||
if (comment.user.type === 'Bot' && comment.user.login === 'github-actions[bot]') {
|
||||
await github.graphql(`
|
||||
mutation {
|
||||
minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// Checkout the repo and grep for the manufacturer name
|
||||
const execSync = require('child_process').execSync;
|
||||
execSync(`git clone https://github.com/Koenkk/zigbee-herdsman-converters.git zhc`, {stdio: 'ignore'});
|
||||
|
||||
// Check if Tuya manufacturer name is already supported.
|
||||
const tuyaManufacturerNameRe = /['"](_T\w+_(\w+))['"]/g;
|
||||
const tuyaManufacturerNames = Array.from(issue.body.matchAll(tuyaManufacturerNameRe), m => [m[1], m[2]]);
|
||||
if (tuyaManufacturerNames.length > 0) {
|
||||
for (const [fullName, partialName] of tuyaManufacturerNames) {
|
||||
const fullMatch = (() => { try { return execSync(`grep -r --include="*.ts" "${fullName}" zhc`, {encoding: 'utf8'}); } catch { return undefined; } })();
|
||||
if (fullMatch) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: [
|
||||
`👋 Hi there! The Tuya device with manufacturer name \`${fullName}\` is already supported in the latest dev branch.`,
|
||||
"See this [guide](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) on how to update, after updating you can remove your external converter.",
|
||||
"",
|
||||
"In case you created the external converter with the goal to extend or fix an issue with the out-of-the-box support, please submit a pull request.",
|
||||
"For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).",
|
||||
"If you need help with the process, feel free to ask here and we'll be happy to assist."
|
||||
].join('\n')
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const partialMatch = (() => { try { return execSync(`grep -r --include="*.ts" "${partialName}" zhc`, {encoding: 'utf8'}); } catch { return undefined; } })();
|
||||
if (partialMatch) {
|
||||
const candidates = Array.from(partialMatch.matchAll(tuyaManufacturerNameRe), m => m[1]);
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: [
|
||||
`👋 Hi there! A similar Tuya device of which the manufacturer name also ends with \`_${partialName}\` is already supported.`,
|
||||
"This means the device can probably be easily be supported by re-using the existing converter.",
|
||||
"",
|
||||
"I found the following matches: " + candidates.map((c) => `\`${c}\``).join(', '),
|
||||
`Try to stop Z2M, change all occurrences of \`${fullName}\` in the \`data/database.db\` to one of the matches above and start Z2M.`,
|
||||
"Let us know if it works so we can support this device out-of-the-box!",
|
||||
].join('\n')
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if zigbee model is already supported.
|
||||
const zigbeeModelRe = /zigbeeModel: \[['"](.+)['"]\]/g;
|
||||
const zigbeeModels = Array.from(issue.body.matchAll(zigbeeModelRe), m => m[1]);
|
||||
if (zigbeeModels.length > 0) {
|
||||
for (const zigbeeModel of zigbeeModels) {
|
||||
const fullMatch = (() => { try { return execSync(`grep -r --include="*.ts" '"${zigbeeModel}"' zhc`, {encoding: 'utf8'}); } catch { return undefined; } })();
|
||||
if (fullMatch) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: [
|
||||
`👋 Hi there! The device with zigbee model \`${zigbeeModel}\` is already supported in the latest dev branch.`,
|
||||
"See this [guide](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) on how to update, after updating you can remove your external converter.",
|
||||
"",
|
||||
"In case you created the external converter with the goal to extend or fix an issue with the out-of-the-box support, please submit a pull request.",
|
||||
"For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).",
|
||||
"If you need help with the process, feel free to ask here and we'll be happy to assist."
|
||||
].join('\n')
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a request to pull request comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: [
|
||||
"🙏 Thank you for creating this issue and sharing your external converter!",
|
||||
"",
|
||||
"In case all features work, please submit a pull request on this repository so the device can be supported out-of-the-box with the next release.",
|
||||
"For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).",
|
||||
"",
|
||||
"If **NOT** all features work, please follow the [How To Support new devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html)",
|
||||
tuyaManufacturerNames.length > 0 ? "Since this is a Tuya also consider [How To Support new Tuya devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html)" : "",
|
||||
"",
|
||||
"If you need help with the process, feel free to ask here and we'll be happy to assist."
|
||||
].join('\n')
|
||||
});
|
||||
@@ -31,11 +31,11 @@ jobs:
|
||||
token: ${{secrets.GH_TOKEN}}
|
||||
|
||||
# Checkout repos
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: koenkk/zigbee2mqtt
|
||||
path: ./z2m
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: koenkk/zigbee2mqtt
|
||||
path: ./z2m-master
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: dev
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.6.3"
|
||||
".": "2.7.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
## [2.7.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.6.3...2.7.0) (2025-12-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `action` bridge/request API ([#29730](https://github.com/Koenkk/zigbee2mqtt/issues/29730)) ([a61646d](https://github.com/Koenkk/zigbee2mqtt/commit/a61646d584c0a0de46abb3545cd964f65d84312d))
|
||||
* Add new bind/reporting/map features ([#29750](https://github.com/Koenkk/zigbee2mqtt/issues/29750)) ([f26ade4](https://github.com/Koenkk/zigbee2mqtt/commit/f26ade4f938350d10ed22a32a2e30365f7514a72))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add port 8080 `EXPOSE` to container ([#29754](https://github.com/Koenkk/zigbee2mqtt/issues/29754)) ([4c21b66](https://github.com/Koenkk/zigbee2mqtt/commit/4c21b660794ee3b85c9a9a6d01b0df5ec8bce639))
|
||||
* Home Assistant: expose `current_humidity` for `climate` devices exposing `humidity` ([#29842](https://github.com/Koenkk/zigbee2mqtt/issues/29842)) ([631c4f6](https://github.com/Koenkk/zigbee2mqtt/commit/631c4f6a5729888d65a1125e0cca8f9ba2ff0e59))
|
||||
* **ignore:** bump @biomejs/biome from 2.3.3 to 2.3.4 in the minor-patch group ([#29571](https://github.com/Koenkk/zigbee2mqtt/issues/29571)) ([517b286](https://github.com/Koenkk/zigbee2mqtt/commit/517b2865835a7c21b4c5ce68018fd30ae45d1a3f))
|
||||
* **ignore:** bump debounce from 2.2.0 to 3.0.0 ([#29444](https://github.com/Koenkk/zigbee2mqtt/issues/29444)) ([8099322](https://github.com/Koenkk/zigbee2mqtt/commit/8099322cc2e93e5c1faf355d41485f2c673e0cea))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#29443](https://github.com/Koenkk/zigbee2mqtt/issues/29443)) ([619b939](https://github.com/Koenkk/zigbee2mqtt/commit/619b939b148394bd6a083c17cc5cf254c8eb3aea))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#29859](https://github.com/Koenkk/zigbee2mqtt/issues/29859)) ([c02ed09](https://github.com/Koenkk/zigbee2mqtt/commit/c02ed092116c7454220c0611bff5d078e12a8ecc))
|
||||
* **ignore:** bump the minor-patch group with 3 updates ([#29804](https://github.com/Koenkk/zigbee2mqtt/issues/29804)) ([e2232e9](https://github.com/Koenkk/zigbee2mqtt/commit/e2232e9e9173ea9ec55152ac33bc0ee0141471bb))
|
||||
* **ignore:** bump zigbee2mqtt-windfront from 2.4.0 to 2.4.1 in the minor-patch group ([#29905](https://github.com/Koenkk/zigbee2mqtt/issues/29905)) ([4a020fd](https://github.com/Koenkk/zigbee2mqtt/commit/4a020fd8de2dab2973b859c34b934416a15e82a5))
|
||||
* **ignore:** bump zigbee2mqtt-windfront to 2.4.2 ([#29919](https://github.com/Koenkk/zigbee2mqtt/issues/29919)) ([3dce854](https://github.com/Koenkk/zigbee2mqtt/commit/3dce8548ceee111f49a5b3460fdb2cbb4f47da62))
|
||||
* **ignore:** prop name convention on new API endpoints ([#29820](https://github.com/Koenkk/zigbee2mqtt/issues/29820)) ([ad06968](https://github.com/Koenkk/zigbee2mqtt/commit/ad0696856a9a63fc74b9c2115f08d7fd97850545))
|
||||
* **ignore:** update zigbee-herdsman to 6.3.3 ([#29500](https://github.com/Koenkk/zigbee2mqtt/issues/29500)) ([39c5bc8](https://github.com/Koenkk/zigbee2mqtt/commit/39c5bc8c0446a7ccdffe6f82ae9e7f31f60f20ba))
|
||||
* **ignore:** update zigbee-herdsman to 6.4.0 ([#29522](https://github.com/Koenkk/zigbee2mqtt/issues/29522)) ([e788c52](https://github.com/Koenkk/zigbee2mqtt/commit/e788c520a9268498daf0f88d7bedb289546d226e))
|
||||
* **ignore:** update zigbee-herdsman to 6.4.1 ([#29595](https://github.com/Koenkk/zigbee2mqtt/issues/29595)) ([61c6ec6](https://github.com/Koenkk/zigbee2mqtt/commit/61c6ec611174529e4c041a56146ba8405cfe9ed8))
|
||||
* **ignore:** update zigbee-herdsman to 6.4.2 ([#29683](https://github.com/Koenkk/zigbee2mqtt/issues/29683)) ([53b7eaa](https://github.com/Koenkk/zigbee2mqtt/commit/53b7eaabf0647c8d1edb0de24b89776991af576c))
|
||||
* **ignore:** update zigbee-herdsman to 7.0.0 ([3e2aefb](https://github.com/Koenkk/zigbee2mqtt/commit/3e2aefbcf84f43d45c1d6ca33397c8bb76648822))
|
||||
* **ignore:** update zigbee-herdsman to 7.0.1 ([#29855](https://github.com/Koenkk/zigbee2mqtt/issues/29855)) ([802271e](https://github.com/Koenkk/zigbee2mqtt/commit/802271ed37d404944d315f83f3b623ba9e424bb9))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.60.0 ([#29426](https://github.com/Koenkk/zigbee2mqtt/issues/29426)) ([eeed56e](https://github.com/Koenkk/zigbee2mqtt/commit/eeed56ef2c01dfe67e7a0cd7cafd35ef1876f5b4))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.62.0 ([#29449](https://github.com/Koenkk/zigbee2mqtt/issues/29449)) ([ea1c5a8](https://github.com/Koenkk/zigbee2mqtt/commit/ea1c5a8f4e8537a2f8c59988bb712f62a0adffd7))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.63.0 ([#29482](https://github.com/Koenkk/zigbee2mqtt/issues/29482)) ([5a6a9cf](https://github.com/Koenkk/zigbee2mqtt/commit/5a6a9cf4d2de803a10cd38bc19f3ec256eff5929))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.64.0 ([#29499](https://github.com/Koenkk/zigbee2mqtt/issues/29499)) ([5c4e43f](https://github.com/Koenkk/zigbee2mqtt/commit/5c4e43fd109321ddcfad3a67b60423f0197c51a6))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.65.0 ([#29523](https://github.com/Koenkk/zigbee2mqtt/issues/29523)) ([8822aed](https://github.com/Koenkk/zigbee2mqtt/commit/8822aedb5e254f5be5f6cda0cfe18fc6f69c84d6))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.66.0 ([#29558](https://github.com/Koenkk/zigbee2mqtt/issues/29558)) ([940fff2](https://github.com/Koenkk/zigbee2mqtt/commit/940fff244c39da04d0de5d7e4076359dc6245aed))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.67.0 ([#29572](https://github.com/Koenkk/zigbee2mqtt/issues/29572)) ([4794b3c](https://github.com/Koenkk/zigbee2mqtt/commit/4794b3c81f9e38660f8549995b7f3ef8b4cd4318))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.68.0 ([#29606](https://github.com/Koenkk/zigbee2mqtt/issues/29606)) ([564e9ab](https://github.com/Koenkk/zigbee2mqtt/commit/564e9ab9eb49c3517913917af8b23d4758c6b6c7))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.69.0 ([#29659](https://github.com/Koenkk/zigbee2mqtt/issues/29659)) ([16d0e56](https://github.com/Koenkk/zigbee2mqtt/commit/16d0e5696859e79be4701154dd35f7780e47acae))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.70.0 ([#29684](https://github.com/Koenkk/zigbee2mqtt/issues/29684)) ([73225e2](https://github.com/Koenkk/zigbee2mqtt/commit/73225e2436bb56ff4ed47d787e8a1793fd10998d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.71.0 ([#29716](https://github.com/Koenkk/zigbee2mqtt/issues/29716)) ([d6bf517](https://github.com/Koenkk/zigbee2mqtt/commit/d6bf5174be905128432e90d54b3c43c2a68e75ac))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.72.0 ([#29735](https://github.com/Koenkk/zigbee2mqtt/issues/29735)) ([7b7b988](https://github.com/Koenkk/zigbee2mqtt/commit/7b7b9889607a6e48001ae3cda5950386c24f5fd6))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.73.0 ([#29752](https://github.com/Koenkk/zigbee2mqtt/issues/29752)) ([be774a4](https://github.com/Koenkk/zigbee2mqtt/commit/be774a4d0d70122ced0f6ca79f49c060a7d2d2cb))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.74.0 ([#29770](https://github.com/Koenkk/zigbee2mqtt/issues/29770)) ([ba77ce7](https://github.com/Koenkk/zigbee2mqtt/commit/ba77ce7c8df0b29845a090c9defb4ce8aaf0f924))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.75.0 ([#29811](https://github.com/Koenkk/zigbee2mqtt/issues/29811)) ([854df49](https://github.com/Koenkk/zigbee2mqtt/commit/854df498bd3c8999393b683052dfba6b906ba669))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.76.0 ([#29823](https://github.com/Koenkk/zigbee2mqtt/issues/29823)) ([72d862f](https://github.com/Koenkk/zigbee2mqtt/commit/72d862f4a080bfcd91bf99d4c13387e719dfdb8e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.77.0 ([#29838](https://github.com/Koenkk/zigbee2mqtt/issues/29838)) ([35d8a5d](https://github.com/Koenkk/zigbee2mqtt/commit/35d8a5da7f1d817727f8b029da1992901f5b4115))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.78.0 ([#29856](https://github.com/Koenkk/zigbee2mqtt/issues/29856)) ([49b4876](https://github.com/Koenkk/zigbee2mqtt/commit/49b4876768c5d15fcff1ad0c9c2d02dda96543d0))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.79.0 ([#29903](https://github.com/Koenkk/zigbee2mqtt/issues/29903)) ([d411fc5](https://github.com/Koenkk/zigbee2mqtt/commit/d411fc5c693300863a19c2798f0a7a05cfc7e55f))
|
||||
* **ignore:** update zigbee-herdsman-converters to 25.80.0 ([#29924](https://github.com/Koenkk/zigbee2mqtt/issues/29924)) ([445890a](https://github.com/Koenkk/zigbee2mqtt/commit/445890a0710ba7f6b101b651bf379f10390f9e7e))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 2.3.1 ([#29788](https://github.com/Koenkk/zigbee2mqtt/issues/29788)) ([17520f7](https://github.com/Koenkk/zigbee2mqtt/commit/17520f7cfb69b577ebd986b86da80931a15a39db))
|
||||
* Reporting payload detection fixes ([#29854](https://github.com/Koenkk/zigbee2mqtt/issues/29854)) ([42fc6e0](https://github.com/Koenkk/zigbee2mqtt/commit/42fc6e03a7a3092502e3c3d1defbcb3ed6438230))
|
||||
|
||||
## [2.6.3](https://github.com/Koenkk/zigbee2mqtt/compare/2.6.2...2.6.3) (2025-11-01)
|
||||
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec
|
||||
|
||||
### Developing
|
||||
|
||||
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.
|
||||
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.2.5/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
||||
@@ -52,5 +52,8 @@ RUN mkdir /app/data
|
||||
ARG COMMIT
|
||||
RUN echo "$COMMIT" > dist/.hash
|
||||
|
||||
# Expose the default frontend port (see lib/util/settings.ts defaults.frontend.port)
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD [ "/sbin/tini", "--", "node", "index.js"]
|
||||
|
||||
+33
-5
@@ -184,7 +184,7 @@ interface ParsedMQTTMessage {
|
||||
}
|
||||
|
||||
export default class Bind extends Extension {
|
||||
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
|
||||
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind|binds/clear)`);
|
||||
private pollDebouncers: {[s: string]: () => void} = {};
|
||||
|
||||
// biome-ignore lint/suspicious/useAwait: API
|
||||
@@ -276,6 +276,36 @@ export default class Bind extends Extension {
|
||||
}
|
||||
|
||||
@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
||||
if (data.topic.endsWith("binds/clear")) {
|
||||
const message = JSON.parse(data.message) as Zigbee2MQTTAPI["bridge/request/device/binds/clear"];
|
||||
|
||||
if (typeof message !== "object" || typeof message.target !== "string") {
|
||||
await this.publishResponse("binds/clear", message, {}, "Invalid payload");
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.zigbee.resolveEntity(message.target);
|
||||
|
||||
if (!(target instanceof Device)) {
|
||||
await this.publishResponse("binds/clear", message, {}, "Invalid target");
|
||||
return;
|
||||
}
|
||||
|
||||
// this list is raw (not resolved) to allow clearing any specific target (not only currently known)
|
||||
const eui64List = message.ieee_list ?? ["0xffffffffffffffff"];
|
||||
|
||||
await target.zh.clearAllBindings(eui64List);
|
||||
|
||||
const responseData: Zigbee2MQTTAPI["bridge/response/device/binds/clear"] = {
|
||||
target: message.target,
|
||||
ieee_list: eui64List,
|
||||
};
|
||||
|
||||
await this.publishResponse("binds/clear", message, responseData);
|
||||
this.eventBus.emitDevicesChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
const [raw, parsed, error] = this.parseMQTTMessage(data);
|
||||
|
||||
if (!raw || !parsed) {
|
||||
@@ -389,7 +419,7 @@ export default class Bind extends Extension {
|
||||
}
|
||||
|
||||
private async publishResponse<T extends Zigbee2MQTTResponseEndpoints>(
|
||||
type: ParsedMQTTMessage["type"],
|
||||
type: ParsedMQTTMessage["type"] | "binds/clear",
|
||||
request: KeyValue,
|
||||
data: Zigbee2MQTTAPI[T],
|
||||
error?: string,
|
||||
@@ -488,9 +518,7 @@ export default class Bind extends Extension {
|
||||
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
for (const endpoint of device.zh.endpoints) {
|
||||
for (const bind of endpoint.binds) {
|
||||
allBinds.push(bind);
|
||||
}
|
||||
allBinds.push(...endpoint.binds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+67
-6
@@ -31,7 +31,10 @@ export default class Bridge extends Extension {
|
||||
private logTransport!: winston.transport;
|
||||
private requestLookup: {[key: string]: (message: KeyValue | string) => Promise<Zigbee2MQTTResponse<Zigbee2MQTTResponseEndpoints>>} = {
|
||||
"device/options": this.deviceOptions,
|
||||
"device/configure_reporting": this.deviceConfigureReporting,
|
||||
/** @deprecated 3.0 */
|
||||
"device/configure_reporting": this.deviceReportingConfigure,
|
||||
"device/reporting/configure": this.deviceReportingConfigure,
|
||||
"device/reporting/read": this.deviceReportingRead,
|
||||
"device/remove": this.deviceRemove,
|
||||
"device/interview": this.deviceInterview,
|
||||
"device/generate_external_definition": this.deviceGenerateExternalDefinition,
|
||||
@@ -50,6 +53,7 @@ export default class Bridge extends Extension {
|
||||
health_check: this.healthCheck,
|
||||
coordinator_check: this.coordinatorCheck,
|
||||
options: this.bridgeOptions,
|
||||
action: this.action,
|
||||
};
|
||||
|
||||
override async start(): Promise<void> {
|
||||
@@ -466,16 +470,16 @@ export default class Bridge extends Extension {
|
||||
return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired});
|
||||
}
|
||||
|
||||
@bind async deviceConfigureReporting(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/configure_reporting">> {
|
||||
@bind async deviceReportingConfigure(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/reporting/configure">> {
|
||||
if (
|
||||
typeof message !== "object" ||
|
||||
message.id === undefined ||
|
||||
message.endpoint === undefined ||
|
||||
message.cluster === undefined ||
|
||||
message.maximum_report_interval === undefined ||
|
||||
message.minimum_report_interval === undefined ||
|
||||
message.reportable_change === undefined ||
|
||||
message.attribute === undefined
|
||||
message.attribute === undefined ||
|
||||
typeof message.maximum_report_interval !== "number" ||
|
||||
typeof message.minimum_report_interval !== "number" ||
|
||||
(message.reportable_change !== undefined && typeof message.reportable_change !== "number")
|
||||
) {
|
||||
throw new Error("Invalid payload");
|
||||
}
|
||||
@@ -518,6 +522,46 @@ export default class Bridge extends Extension {
|
||||
});
|
||||
}
|
||||
|
||||
@bind async deviceReportingRead(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/reporting/read">> {
|
||||
if (
|
||||
typeof message !== "object" ||
|
||||
message.id === undefined ||
|
||||
message.endpoint === undefined ||
|
||||
message.cluster === undefined ||
|
||||
message.configs === undefined
|
||||
) {
|
||||
throw new Error("Invalid payload");
|
||||
}
|
||||
|
||||
const device = this.getEntity("device", message.id);
|
||||
const endpoint = device.endpoint(message.endpoint);
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error(`Device '${device.ID}' does not have endpoint '${message.endpoint}'`);
|
||||
}
|
||||
|
||||
const response = await endpoint.readReportingConfig(
|
||||
message.cluster,
|
||||
message.configs,
|
||||
message.manufacturer_code ? {manufacturerCode: message.manufacturer_code} : {},
|
||||
);
|
||||
|
||||
await this.publishDevices();
|
||||
|
||||
const responseData: Zigbee2MQTTAPI["bridge/response/device/reporting/read"] = {
|
||||
id: message.id,
|
||||
endpoint: message.endpoint,
|
||||
cluster: message.cluster,
|
||||
configs: response,
|
||||
};
|
||||
|
||||
if (message.manufacturer_code) {
|
||||
responseData.manufacturer_code = message.manufacturer_code;
|
||||
}
|
||||
|
||||
return utils.getResponse(message, responseData);
|
||||
}
|
||||
|
||||
@bind async deviceInterview(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/interview">> {
|
||||
if (typeof message !== "object" || message.id === undefined) {
|
||||
throw new Error("Invalid payload");
|
||||
@@ -554,6 +598,22 @@ export default class Bridge extends Extension {
|
||||
return utils.getResponse(message, {id: message.id, source});
|
||||
}
|
||||
|
||||
@bind async action(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/action">> {
|
||||
if (typeof message !== "object" || !message.action) {
|
||||
throw new Error("Invalid payload");
|
||||
}
|
||||
|
||||
const action = zhc.ACTIONS[message.action];
|
||||
|
||||
if (action === undefined) {
|
||||
throw new Error("Invalid action");
|
||||
}
|
||||
|
||||
const response = await action(this.zigbee.zhController, message.params ?? {});
|
||||
|
||||
return utils.getResponse(message, response);
|
||||
}
|
||||
|
||||
async renameEntity<T extends "device" | "group">(
|
||||
entityType: T,
|
||||
message: string | KeyValue,
|
||||
@@ -812,6 +872,7 @@ export default class Bridge extends Extension {
|
||||
const data: Zigbee2MQTTAPI["bridge/definitions"] = {
|
||||
clusters: Zcl.Clusters,
|
||||
custom_clusters: {},
|
||||
actions: Object.keys(zhc.ACTIONS),
|
||||
};
|
||||
|
||||
for (const device of this.zigbee.devicesIterator((d) => !utils.objectIsEmpty(d.customClusters))) {
|
||||
|
||||
@@ -752,6 +752,12 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
|
||||
const currentHumidity = allExposes?.filter(isNumericExpose).find((e) => e.name === "humidity" && e.access === ACCESS_STATE);
|
||||
if (currentHumidity) {
|
||||
discoveryEntry.discovery_payload.current_humidity_template = `{{ value_json.${currentHumidity.property} }}`;
|
||||
discoveryEntry.discovery_payload.current_humidity_topic = true;
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
break;
|
||||
}
|
||||
|
||||
+27
-21
@@ -1,7 +1,8 @@
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
|
||||
import type {LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
@@ -185,8 +186,8 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
async networkScan(includeRoutes: boolean): Promise<Zigbee2MQTTNetworkMap> {
|
||||
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
|
||||
const lqis = new Map<Device, zh.LQI>();
|
||||
const routingTables = new Map<Device, zh.RoutingTable>();
|
||||
const lqis = new Map<Device, LQITableEntry[]>();
|
||||
const routingTables = new Map<Device, RoutingTableEntry[]>();
|
||||
const failed = new Map<Device, string[]>();
|
||||
const requestWithRetry = async <T>(request: () => Promise<T>): Promise<T> => {
|
||||
try {
|
||||
@@ -210,7 +211,7 @@ export default class NetworkMap extends Extension {
|
||||
await utils.sleep(1); // sleep 1 second between each scan to reduce stress on network.
|
||||
|
||||
try {
|
||||
const result = await requestWithRetry<zh.LQI>(async () => await device.zh.lqi());
|
||||
const result = await requestWithRetry<LQITableEntry[]>(async () => await device.zh.lqi());
|
||||
lqis.set(device, result);
|
||||
logger.debug(`LQI succeeded for '${device.name}'`);
|
||||
} catch (error) {
|
||||
@@ -222,7 +223,7 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
if (includeRoutes) {
|
||||
try {
|
||||
const result = await requestWithRetry<zh.RoutingTable>(async () => await device.zh.routingTable());
|
||||
const result = await requestWithRetry<RoutingTableEntry[]>(async () => await device.zh.routingTable());
|
||||
routingTables.set(device, result);
|
||||
logger.debug(`Routing table succeeded for '${device.name}'`);
|
||||
} catch (error) {
|
||||
@@ -274,42 +275,47 @@ export default class NetworkMap extends Extension {
|
||||
}
|
||||
|
||||
// Add links
|
||||
for (const [device, lqi] of lqis) {
|
||||
for (const neighbor of lqi.neighbors) {
|
||||
for (const [device, table] of lqis) {
|
||||
for (const neighbor of table) {
|
||||
if (neighbor.relationship > 3) {
|
||||
// Relationship is not active, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
let neighborEui64 = neighbor.eui64;
|
||||
|
||||
// Some Xiaomi devices return 0x00 as the neighbor ieeeAddr (obviously not correct).
|
||||
// Determine the correct ieeeAddr based on the networkAddress.
|
||||
if (neighbor.ieeeAddr === "0x0000000000000000") {
|
||||
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
|
||||
if (neighborEui64 === "0x0000000000000000") {
|
||||
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.nwkAddress);
|
||||
|
||||
if (neighborDevice) {
|
||||
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
|
||||
neighborEui64 = neighborDevice.ieeeAddr as Eui64;
|
||||
}
|
||||
}
|
||||
|
||||
const link: Zigbee2MQTTNetworkMap["links"][number] = {
|
||||
source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress},
|
||||
source: {ieeeAddr: neighborEui64, networkAddress: neighbor.nwkAddress},
|
||||
target: {ieeeAddr: device.ieeeAddr, networkAddress: device.zh.networkAddress},
|
||||
linkquality: neighbor.linkquality,
|
||||
depth: neighbor.depth,
|
||||
routes: [],
|
||||
// DEPRECATED:
|
||||
sourceIeeeAddr: neighbor.ieeeAddr,
|
||||
targetIeeeAddr: device.ieeeAddr,
|
||||
sourceNwkAddr: neighbor.networkAddress,
|
||||
lqi: neighbor.linkquality,
|
||||
deviceType: neighbor.deviceType,
|
||||
rxOnWhenIdle: neighbor.rxOnWhenIdle,
|
||||
relationship: neighbor.relationship,
|
||||
permitJoining: neighbor.permitJoining,
|
||||
depth: neighbor.depth,
|
||||
lqi: neighbor.lqi,
|
||||
routes: [],
|
||||
// below are @deprecated
|
||||
sourceIeeeAddr: neighborEui64,
|
||||
targetIeeeAddr: device.ieeeAddr,
|
||||
sourceNwkAddr: neighbor.nwkAddress,
|
||||
linkquality: neighbor.lqi,
|
||||
};
|
||||
|
||||
const routingTable = routingTables.get(device);
|
||||
|
||||
if (routingTable) {
|
||||
for (const entry of routingTable.table) {
|
||||
if (entry.status === "ACTIVE" && entry.nextHop === neighbor.networkAddress) {
|
||||
for (const entry of routingTable) {
|
||||
if (entry.nextHopAddress === neighbor.nwkAddress) {
|
||||
link.routes.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
+61
-18
@@ -1,5 +1,8 @@
|
||||
import type * as zigbeeHerdsman from "zigbee-herdsman/dist";
|
||||
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
|
||||
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
|
||||
import type {ClusterDefinition, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
import type {GenericZdoResponse, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
|
||||
import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters";
|
||||
import type {Base} from "zigbee-herdsman-converters/lib/exposes";
|
||||
|
||||
@@ -278,18 +281,21 @@ export interface Zigbee2MQTTNetworkMap {
|
||||
links: {
|
||||
source: {ieeeAddr: string; networkAddress: number};
|
||||
target: {ieeeAddr: string; networkAddress: number};
|
||||
linkquality: number;
|
||||
depth: number;
|
||||
routes: {
|
||||
destinationAddress: number;
|
||||
status: string;
|
||||
nextHop: number;
|
||||
}[];
|
||||
sourceIeeeAddr: string;
|
||||
targetIeeeAddr: string;
|
||||
sourceNwkAddr: number;
|
||||
lqi: number;
|
||||
deviceType: number;
|
||||
rxOnWhenIdle: number;
|
||||
relationship: number;
|
||||
permitJoining: number;
|
||||
depth: number;
|
||||
lqi: number;
|
||||
routes: RoutingTableEntry[];
|
||||
/** @deprecated 3.0 */
|
||||
linkquality: number;
|
||||
/** @deprecated 3.0 */
|
||||
sourceIeeeAddr: string;
|
||||
/** @deprecated 3.0 */
|
||||
targetIeeeAddr: string;
|
||||
/** @deprecated 3.0 */
|
||||
sourceNwkAddr: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -310,6 +316,7 @@ export interface Zigbee2MQTTAPI {
|
||||
"bridge/definitions": {
|
||||
clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>;
|
||||
custom_clusters: Record<string, CustomClusters>;
|
||||
actions: string[];
|
||||
};
|
||||
|
||||
"bridge/event":
|
||||
@@ -558,6 +565,16 @@ export interface Zigbee2MQTTAPI {
|
||||
failed: string[];
|
||||
};
|
||||
|
||||
"bridge/request/device/binds/clear": {
|
||||
target: string;
|
||||
ieee_list?: Eui64[];
|
||||
};
|
||||
|
||||
"bridge/response/device/binds/clear": {
|
||||
target: string;
|
||||
ieee_list?: Eui64[];
|
||||
};
|
||||
|
||||
"bridge/request/device/configure":
|
||||
| {
|
||||
id: string | number;
|
||||
@@ -686,25 +703,41 @@ export interface Zigbee2MQTTAPI {
|
||||
homeassistant_rename: boolean;
|
||||
};
|
||||
|
||||
"bridge/request/device/configure_reporting": {
|
||||
"bridge/request/device/reporting/configure": {
|
||||
id: string;
|
||||
endpoint: string | number;
|
||||
cluster: string | number;
|
||||
attribute: string | number | {ID: number; type: number};
|
||||
minimum_report_interval: number;
|
||||
maximum_report_interval: number;
|
||||
reportable_change: number;
|
||||
reportable_change?: number;
|
||||
option: Record<string, unknown>;
|
||||
};
|
||||
|
||||
"bridge/response/device/configure_reporting": {
|
||||
"bridge/response/device/reporting/configure": {
|
||||
id: string;
|
||||
endpoint: string | number;
|
||||
cluster: string | number;
|
||||
attribute: string | number | {ID: number; type: number};
|
||||
minimum_report_interval: number;
|
||||
maximum_report_interval: number;
|
||||
reportable_change: number;
|
||||
reportable_change?: number;
|
||||
};
|
||||
|
||||
"bridge/request/device/reporting/read": {
|
||||
id: string;
|
||||
endpoint: string | number;
|
||||
cluster: string | number;
|
||||
configs: {direction?: number; attribute: string | number | {ID: number; type: number}}[];
|
||||
manufacturer_code?: number;
|
||||
};
|
||||
|
||||
"bridge/response/device/reporting/read": {
|
||||
id: string;
|
||||
endpoint: string | number;
|
||||
cluster: string | number;
|
||||
configs: zigbeeHerdsman.Zcl.ClustersTypes.TFoundation["readReportConfigRsp"];
|
||||
manufacturer_code?: number;
|
||||
};
|
||||
|
||||
"bridge/request/group/remove": {
|
||||
@@ -821,6 +854,10 @@ export interface Zigbee2MQTTAPI {
|
||||
channel: number;
|
||||
};
|
||||
|
||||
"bridge/request/action": {action: string; params?: Record<string, unknown>};
|
||||
|
||||
"bridge/response/action": GenericZdoResponse | ZclPayload | undefined;
|
||||
|
||||
/**
|
||||
* entity state response
|
||||
*/
|
||||
@@ -883,6 +920,7 @@ export type Zigbee2MQTTRequestEndpoints =
|
||||
| "bridge/request/options"
|
||||
| "bridge/request/device/bind"
|
||||
| "bridge/request/device/unbind"
|
||||
| "bridge/request/device/binds/clear"
|
||||
| "bridge/request/device/configure"
|
||||
| "bridge/request/device/remove"
|
||||
| "bridge/request/device/ota_update/check"
|
||||
@@ -896,7 +934,8 @@ export type Zigbee2MQTTRequestEndpoints =
|
||||
| "bridge/request/device/generate_external_definition"
|
||||
| "bridge/request/device/options"
|
||||
| "bridge/request/device/rename"
|
||||
| "bridge/request/device/configure_reporting"
|
||||
| "bridge/request/device/reporting/configure"
|
||||
| "bridge/request/device/reporting/read"
|
||||
| "bridge/request/group/remove"
|
||||
| "bridge/request/group/add"
|
||||
| "bridge/request/group/rename"
|
||||
@@ -907,6 +946,7 @@ export type Zigbee2MQTTRequestEndpoints =
|
||||
| "bridge/request/touchlink/factory_reset"
|
||||
| "bridge/request/touchlink/scan"
|
||||
| "bridge/request/touchlink/identify"
|
||||
| "bridge/request/action"
|
||||
| "{friendlyNameOrId}/set"
|
||||
| "{friendlyNameOrId}/set/{attribute}"
|
||||
| "{friendlyNameOrId}/{endpoint}/set"
|
||||
@@ -931,6 +971,7 @@ export type Zigbee2MQTTResponseEndpoints =
|
||||
| "bridge/response/options"
|
||||
| "bridge/response/device/bind"
|
||||
| "bridge/response/device/unbind"
|
||||
| "bridge/response/device/binds/clear"
|
||||
| "bridge/response/device/configure"
|
||||
| "bridge/response/device/remove"
|
||||
| "bridge/response/device/ota_update/check"
|
||||
@@ -941,7 +982,8 @@ export type Zigbee2MQTTResponseEndpoints =
|
||||
| "bridge/response/device/generate_external_definition"
|
||||
| "bridge/response/device/options"
|
||||
| "bridge/response/device/rename"
|
||||
| "bridge/response/device/configure_reporting"
|
||||
| "bridge/response/device/reporting/configure"
|
||||
| "bridge/response/device/reporting/read"
|
||||
| "bridge/response/group/remove"
|
||||
| "bridge/response/group/add"
|
||||
| "bridge/response/group/rename"
|
||||
@@ -951,7 +993,8 @@ export type Zigbee2MQTTResponseEndpoints =
|
||||
| "bridge/response/group/members/remove_all"
|
||||
| "bridge/response/touchlink/factory_reset"
|
||||
| "bridge/response/touchlink/scan"
|
||||
| "bridge/response/touchlink/identify";
|
||||
| "bridge/response/touchlink/identify"
|
||||
| "bridge/response/action";
|
||||
|
||||
export type Zigbee2MQTTRequest<T extends Zigbee2MQTTRequestEndpoints> = {
|
||||
transaction?: string;
|
||||
|
||||
Vendored
-3
@@ -36,9 +36,6 @@ declare global {
|
||||
type Endpoint = ZHModels.Endpoint;
|
||||
type Device = ZHModels.Device;
|
||||
type Group = ZHModels.Group;
|
||||
// biome-ignore lint/style/useNamingConvention: API
|
||||
type LQI = ZHAdapterTypes.Lqi;
|
||||
type RoutingTable = ZHAdapterTypes.RoutingTable;
|
||||
type CoordinatorVersion = ZHAdapterTypes.CoordinatorVersion;
|
||||
type NetworkParameters = ZHAdapterTypes.NetworkParameters;
|
||||
interface Bind {
|
||||
|
||||
@@ -488,7 +488,7 @@ async function startOnboardingServer(): Promise<boolean> {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(Number.parseInt(serverUrl.port), serverUrl.hostname, () => {
|
||||
server.listen(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, () => {
|
||||
console.log(`Onboarding page is available at ${serverUrl.href}`);
|
||||
});
|
||||
});
|
||||
@@ -515,7 +515,7 @@ async function startFailureServer(errors: string): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(Number.parseInt(serverUrl.port), serverUrl.hostname, () => {
|
||||
server.listen(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, () => {
|
||||
console.error(`Failure page is available at ${serverUrl.href}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -607,7 +607,7 @@ export function addGroup(name: string, id?: string): GroupOptions {
|
||||
id = "1";
|
||||
|
||||
while (settings.groups[id]) {
|
||||
id = (Number.parseInt(id) + 1).toString();
|
||||
id = (Number.parseInt(id, 10) + 1).toString();
|
||||
}
|
||||
} else {
|
||||
// ensure provided ID is not in use
|
||||
|
||||
+41
-38
@@ -4,7 +4,6 @@ import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Events as ZHEvents} from "zigbee-herdsman";
|
||||
import {Controller} from "zigbee-herdsman";
|
||||
import type {StartResult} from "zigbee-herdsman/dist/adapter/tstype";
|
||||
|
||||
import Device from "./model/device";
|
||||
import Group from "./model/group";
|
||||
import data from "./util/data";
|
||||
@@ -15,7 +14,7 @@ import utils from "./util/utils";
|
||||
const entityIDRegex = /^(.+?)(?:\/([^/]+))?$/;
|
||||
|
||||
export default class Zigbee {
|
||||
private herdsman!: Controller;
|
||||
#herdsman!: Controller;
|
||||
private eventBus: EventBus;
|
||||
private groupLookup = new Map<number /* group ID */, Group>();
|
||||
private deviceLookup = new Map<string /* IEEE address */, Device>();
|
||||
@@ -25,6 +24,10 @@ export default class Zigbee {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
get zhController() {
|
||||
return this.#herdsman;
|
||||
}
|
||||
|
||||
async start(): Promise<StartResult> {
|
||||
const infoHerdsman = await utils.getDependencyVersion("zigbee-herdsman");
|
||||
logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`);
|
||||
@@ -63,37 +66,37 @@ export default class Zigbee {
|
||||
|
||||
let startResult: StartResult;
|
||||
try {
|
||||
this.herdsman = new Controller(herdsmanSettings);
|
||||
startResult = await this.herdsman.start();
|
||||
this.#herdsman = new Controller(herdsmanSettings);
|
||||
startResult = await this.#herdsman.start();
|
||||
} catch (error) {
|
||||
logger.error("Error while starting zigbee-herdsman");
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.coordinatorIeeeAddr = this.herdsman.getDevicesByType("Coordinator")[0].ieeeAddr;
|
||||
this.coordinatorIeeeAddr = this.#herdsman.getDevicesByType("Coordinator")[0].ieeeAddr;
|
||||
await this.resolveDevicesDefinitions();
|
||||
|
||||
this.herdsman.on("adapterDisconnected", () => this.eventBus.emitAdapterDisconnected());
|
||||
this.herdsman.on("lastSeenChanged", (data: ZHEvents.LastSeenChangedPayload) => {
|
||||
this.#herdsman.on("adapterDisconnected", () => this.eventBus.emitAdapterDisconnected());
|
||||
this.#herdsman.on("lastSeenChanged", (data: ZHEvents.LastSeenChangedPayload) => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
this.eventBus.emitLastSeenChanged({device: this.resolveDevice(data.device.ieeeAddr)!, reason: data.reason});
|
||||
});
|
||||
this.herdsman.on("permitJoinChanged", (data: ZHEvents.PermitJoinChangedPayload) => {
|
||||
this.#herdsman.on("permitJoinChanged", (data: ZHEvents.PermitJoinChangedPayload) => {
|
||||
this.eventBus.emitPermitJoinChanged(data);
|
||||
});
|
||||
this.herdsman.on("deviceNetworkAddressChanged", (data: ZHEvents.DeviceNetworkAddressChangedPayload) => {
|
||||
this.#herdsman.on("deviceNetworkAddressChanged", (data: ZHEvents.DeviceNetworkAddressChangedPayload) => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
const device = this.resolveDevice(data.device.ieeeAddr)!;
|
||||
logger.debug(`Device '${device.name}' changed network address`);
|
||||
this.eventBus.emitDeviceNetworkAddressChanged({device});
|
||||
});
|
||||
this.herdsman.on("deviceAnnounce", (data: ZHEvents.DeviceAnnouncePayload) => {
|
||||
this.#herdsman.on("deviceAnnounce", (data: ZHEvents.DeviceAnnouncePayload) => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
const device = this.resolveDevice(data.device.ieeeAddr)!;
|
||||
logger.debug(`Device '${device.name}' announced itself`);
|
||||
this.eventBus.emitDeviceAnnounce({device});
|
||||
});
|
||||
this.herdsman.on("deviceInterview", async (data: ZHEvents.DeviceInterviewPayload) => {
|
||||
this.#herdsman.on("deviceInterview", async (data: ZHEvents.DeviceInterviewPayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
/* v8 ignore next */ if (!device) return; // Prevent potential race
|
||||
await device.resolveDefinition();
|
||||
@@ -101,19 +104,19 @@ export default class Zigbee {
|
||||
this.logDeviceInterview(d);
|
||||
this.eventBus.emitDeviceInterview(d);
|
||||
});
|
||||
this.herdsman.on("deviceJoined", async (data: ZHEvents.DeviceJoinedPayload) => {
|
||||
this.#herdsman.on("deviceJoined", async (data: ZHEvents.DeviceJoinedPayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
/* v8 ignore next */ if (!device) return; // Prevent potential race
|
||||
await device.resolveDefinition();
|
||||
logger.info(`Device '${device.name}' joined`);
|
||||
this.eventBus.emitDeviceJoined({device});
|
||||
});
|
||||
this.herdsman.on("deviceLeave", (data: ZHEvents.DeviceLeavePayload) => {
|
||||
this.#herdsman.on("deviceLeave", (data: ZHEvents.DeviceLeavePayload) => {
|
||||
const name = settings.getDevice(data.ieeeAddr)?.friendly_name || data.ieeeAddr;
|
||||
logger.warning(`Device '${name}' left the network`);
|
||||
this.eventBus.emitDeviceLeave({ieeeAddr: data.ieeeAddr, name, device: this.deviceLookup.get(data.ieeeAddr)});
|
||||
});
|
||||
this.herdsman.on("message", async (data: ZHEvents.MessagePayload) => {
|
||||
this.#herdsman.on("message", async (data: ZHEvents.MessagePayload) => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
const device = this.resolveDevice(data.device.ieeeAddr)!;
|
||||
await device.resolveDefinition();
|
||||
@@ -129,7 +132,7 @@ export default class Zigbee {
|
||||
|
||||
logger.info(`zigbee-herdsman started (${startResult})`);
|
||||
logger.info(`Coordinator firmware version: '${stringify(await this.getCoordinatorVersion())}'`);
|
||||
logger.debug(`Zigbee network parameters: ${stringify(await this.herdsman.getNetworkParameters())}`);
|
||||
logger.debug(`Zigbee network parameters: ${stringify(await this.#herdsman.getNetworkParameters())}`);
|
||||
|
||||
for (const device of this.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
// If a passlist is used, all other device will be removed from the network.
|
||||
@@ -198,39 +201,39 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
async getCoordinatorVersion(): Promise<zh.CoordinatorVersion> {
|
||||
return await this.herdsman.getCoordinatorVersion();
|
||||
return await this.#herdsman.getCoordinatorVersion();
|
||||
}
|
||||
|
||||
isStopping(): boolean {
|
||||
return this.herdsman.isStopping();
|
||||
return this.#herdsman.isStopping();
|
||||
}
|
||||
|
||||
async backup(): Promise<void> {
|
||||
return await this.herdsman.backup();
|
||||
return await this.#herdsman.backup();
|
||||
}
|
||||
|
||||
async coordinatorCheck(): Promise<{missingRouters: Device[]}> {
|
||||
const check = await this.herdsman.coordinatorCheck();
|
||||
const check = await this.#herdsman.coordinatorCheck();
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
return {missingRouters: check.missingRouters.map((d) => this.resolveDevice(d.ieeeAddr)!)};
|
||||
}
|
||||
|
||||
async getNetworkParameters(): Promise<zh.NetworkParameters> {
|
||||
return await this.herdsman.getNetworkParameters();
|
||||
return await this.#herdsman.getNetworkParameters();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
logger.info("Stopping zigbee-herdsman...");
|
||||
await this.herdsman.stop();
|
||||
await this.#herdsman.stop();
|
||||
logger.info("Stopped zigbee-herdsman");
|
||||
}
|
||||
|
||||
getPermitJoin(): boolean {
|
||||
return this.herdsman.getPermitJoin();
|
||||
return this.#herdsman.getPermitJoin();
|
||||
}
|
||||
|
||||
getPermitJoinEnd(): number | undefined {
|
||||
return this.herdsman.getPermitJoinEnd();
|
||||
return this.#herdsman.getPermitJoinEnd();
|
||||
}
|
||||
|
||||
async permitJoin(time: number, device?: Device): Promise<void> {
|
||||
@@ -240,7 +243,7 @@ export default class Zigbee {
|
||||
logger.info("Zigbee: disabling joining new devices.");
|
||||
}
|
||||
|
||||
await this.herdsman.permitJoin(time, device?.zh);
|
||||
await this.#herdsman.permitJoin(time, device?.zh);
|
||||
}
|
||||
|
||||
async resolveDevicesDefinitions(ignoreCache = false): Promise<void> {
|
||||
@@ -251,7 +254,7 @@ export default class Zigbee {
|
||||
|
||||
@bind private resolveDevice(ieeeAddr: string): Device | undefined {
|
||||
if (!this.deviceLookup.has(ieeeAddr)) {
|
||||
const device = this.herdsman.getDeviceByIeeeAddr(ieeeAddr);
|
||||
const device = this.#herdsman.getDeviceByIeeeAddr(ieeeAddr);
|
||||
if (device) {
|
||||
this.deviceLookup.set(ieeeAddr, new Device(device));
|
||||
}
|
||||
@@ -266,7 +269,7 @@ export default class Zigbee {
|
||||
|
||||
private resolveGroup(groupID: number): Group | undefined {
|
||||
if (!this.groupLookup.has(groupID)) {
|
||||
const group = this.herdsman.getGroupByID(groupID);
|
||||
const group = this.#herdsman.getGroupByID(groupID);
|
||||
|
||||
if (group) {
|
||||
this.groupLookup.set(groupID, new Group(group, this.resolveDevice));
|
||||
@@ -339,33 +342,33 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
firstCoordinatorEndpoint(): zh.Endpoint {
|
||||
return this.herdsman.getDevicesByType("Coordinator")[0].endpoints[0];
|
||||
return this.#herdsman.getDevicesByType("Coordinator")[0].endpoints[0];
|
||||
}
|
||||
|
||||
*devicesAndGroupsIterator(
|
||||
devicePredicate?: (value: zh.Device) => boolean,
|
||||
groupPredicate?: (value: zh.Group) => boolean,
|
||||
): Generator<Device | Group> {
|
||||
for (const device of this.herdsman.getDevicesIterator(devicePredicate)) {
|
||||
for (const device of this.#herdsman.getDevicesIterator(devicePredicate)) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
yield this.resolveDevice(device.ieeeAddr)!;
|
||||
}
|
||||
|
||||
for (const group of this.herdsman.getGroupsIterator(groupPredicate)) {
|
||||
for (const group of this.#herdsman.getGroupsIterator(groupPredicate)) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
yield this.resolveGroup(group.groupID)!;
|
||||
}
|
||||
}
|
||||
|
||||
*groupsIterator(predicate?: (value: zh.Group) => boolean): Generator<Group> {
|
||||
for (const group of this.herdsman.getGroupsIterator(predicate)) {
|
||||
for (const group of this.#herdsman.getGroupsIterator(predicate)) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
yield this.resolveGroup(group.groupID)!;
|
||||
}
|
||||
}
|
||||
|
||||
*devicesIterator(predicate?: (value: zh.Device) => boolean): Generator<Device> {
|
||||
for (const device of this.herdsman.getDevicesIterator(predicate)) {
|
||||
for (const device of this.#herdsman.getDevicesIterator(predicate)) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: assumed valid
|
||||
yield this.resolveDevice(device.ieeeAddr)!;
|
||||
}
|
||||
@@ -399,33 +402,33 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
async touchlinkFactoryResetFirst(): Promise<boolean> {
|
||||
return await this.herdsman.touchlinkFactoryResetFirst();
|
||||
return await this.#herdsman.touchlink.factoryResetFirst();
|
||||
}
|
||||
|
||||
async touchlinkFactoryReset(ieeeAddr: string, channel: number): Promise<boolean> {
|
||||
return await this.herdsman.touchlinkFactoryReset(ieeeAddr, channel);
|
||||
return await this.#herdsman.touchlink.factoryReset(ieeeAddr, channel);
|
||||
}
|
||||
|
||||
async addInstallCode(installCode: string): Promise<void> {
|
||||
await this.herdsman.addInstallCode(installCode);
|
||||
await this.#herdsman.addInstallCode(installCode);
|
||||
}
|
||||
|
||||
async touchlinkIdentify(ieeeAddr: string, channel: number): Promise<void> {
|
||||
await this.herdsman.touchlinkIdentify(ieeeAddr, channel);
|
||||
await this.#herdsman.touchlink.identify(ieeeAddr, channel);
|
||||
}
|
||||
|
||||
async touchlinkScan(): Promise<{ieeeAddr: string; channel: number}[]> {
|
||||
return await this.herdsman.touchlinkScan();
|
||||
return await this.#herdsman.touchlink.scan();
|
||||
}
|
||||
|
||||
createGroup(id: number): Group {
|
||||
this.herdsman.createGroup(id);
|
||||
this.#herdsman.createGroup(id);
|
||||
// biome-ignore lint/style/noNonNullAssertion: just created
|
||||
return this.resolveGroup(id)!;
|
||||
}
|
||||
|
||||
deviceByNetworkAddress(networkAddress: number): Device | undefined {
|
||||
const device = this.herdsman.getDeviceByNetworkAddress(networkAddress);
|
||||
const device = this.#herdsman.getDeviceByNetworkAddress(networkAddress);
|
||||
return device && this.resolveDevice(device.ieeeAddr);
|
||||
}
|
||||
|
||||
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "2.6.3",
|
||||
"version": "2.7.0",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"types": "dist/types/api.d.ts",
|
||||
@@ -44,7 +44,7 @@
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1",
|
||||
"bind-decorator": "^1.0.11",
|
||||
"debounce": "^2.2.0",
|
||||
"debounce": "^3.0.0",
|
||||
"express-static-gzip": "^3.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"finalhandler": "^2.1.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"mqtt": "^5.14.1",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^6.1.0",
|
||||
"rimraf": "^6.1.2",
|
||||
"semver": "^7.7.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"throttleit": "^2.1.0",
|
||||
@@ -62,17 +62,17 @@
|
||||
"winston-syslog": "^2.7.1",
|
||||
"winston-transport": "^4.9.0",
|
||||
"ws": "^8.18.1",
|
||||
"zigbee-herdsman": "6.3.2",
|
||||
"zigbee-herdsman-converters": "25.59.0",
|
||||
"zigbee-herdsman": "7.0.1",
|
||||
"zigbee-herdsman-converters": "25.80.0",
|
||||
"zigbee2mqtt-frontend": "0.9.21",
|
||||
"zigbee2mqtt-windfront": "2.2.3"
|
||||
"zigbee2mqtt-windfront": "2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.2",
|
||||
"@biomejs/biome": "^2.3.8",
|
||||
"@types/finalhandler": "^1.2.3",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/object-assign-deep": "^0.4.3",
|
||||
"@types/readable-stream": "4.0.22",
|
||||
"@types/serve-static": "^2.2.0",
|
||||
@@ -99,4 +99,4 @@
|
||||
"optionalDependencies": {
|
||||
"unix-dgram": "^2.0.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+110
-122
@@ -5,7 +5,7 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
zigbee-herdsman: 6.3.2
|
||||
zigbee-herdsman: 7.0.1
|
||||
|
||||
importers:
|
||||
|
||||
@@ -18,8 +18,8 @@ importers:
|
||||
specifier: ^1.0.11
|
||||
version: 1.0.11
|
||||
debounce:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
express-static-gzip:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
@@ -34,7 +34,7 @@ importers:
|
||||
version: 3.33.1
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
version: 4.1.1
|
||||
json-stable-stringify-without-jsonify:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
@@ -48,8 +48,8 @@ importers:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
rimraf:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
semver:
|
||||
specifier: ^7.7.3
|
||||
version: 7.7.3
|
||||
@@ -72,21 +72,21 @@ importers:
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.3
|
||||
zigbee-herdsman:
|
||||
specifier: 6.3.2
|
||||
version: 6.3.2
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1
|
||||
zigbee-herdsman-converters:
|
||||
specifier: 25.59.0
|
||||
version: 25.59.0
|
||||
specifier: 25.80.0
|
||||
version: 25.80.0
|
||||
zigbee2mqtt-frontend:
|
||||
specifier: 0.9.21
|
||||
version: 0.9.21
|
||||
zigbee2mqtt-windfront:
|
||||
specifier: 2.2.3
|
||||
version: 2.2.3
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
specifier: ^2.3.8
|
||||
version: 2.3.8
|
||||
'@types/finalhandler':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.4
|
||||
@@ -97,8 +97,8 @@ importers:
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
'@types/node':
|
||||
specifier: ^24.9.2
|
||||
version: 24.9.2
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.1
|
||||
'@types/object-assign-deep':
|
||||
specifier: ^0.4.3
|
||||
version: 0.4.3
|
||||
@@ -113,7 +113,7 @@ importers:
|
||||
version: 8.18.1
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.1.1
|
||||
version: 3.2.4(vitest@3.2.4(@types/node@24.9.2))
|
||||
version: 3.2.4(vitest@3.2.4(@types/node@24.10.1))
|
||||
tmp:
|
||||
specifier: ^0.2.5
|
||||
version: 0.2.5
|
||||
@@ -122,7 +122,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.1.1
|
||||
version: 3.2.4(@types/node@24.9.2)
|
||||
version: 3.2.4(@types/node@24.10.1)
|
||||
optionalDependencies:
|
||||
unix-dgram:
|
||||
specifier: ^2.0.7
|
||||
@@ -159,55 +159,55 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@biomejs/biome@2.3.2':
|
||||
resolution: {integrity: sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==}
|
||||
'@biomejs/biome@2.3.8':
|
||||
resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.3.2':
|
||||
resolution: {integrity: sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==}
|
||||
'@biomejs/cli-darwin-arm64@2.3.8':
|
||||
resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.3.2':
|
||||
resolution: {integrity: sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==}
|
||||
'@biomejs/cli-darwin-x64@2.3.8':
|
||||
resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.2':
|
||||
resolution: {integrity: sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==}
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.8':
|
||||
resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.3.2':
|
||||
resolution: {integrity: sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==}
|
||||
'@biomejs/cli-linux-arm64@2.3.8':
|
||||
resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.3.2':
|
||||
resolution: {integrity: sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==}
|
||||
'@biomejs/cli-linux-x64-musl@2.3.8':
|
||||
resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64@2.3.2':
|
||||
resolution: {integrity: sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==}
|
||||
'@biomejs/cli-linux-x64@2.3.8':
|
||||
resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.3.2':
|
||||
resolution: {integrity: sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==}
|
||||
'@biomejs/cli-win32-arm64@2.3.8':
|
||||
resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@2.3.2':
|
||||
resolution: {integrity: sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==}
|
||||
'@biomejs/cli-win32-x64@2.3.8':
|
||||
resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -557,8 +557,8 @@ packages:
|
||||
'@types/js-yaml@4.0.9':
|
||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||
|
||||
'@types/node@24.9.2':
|
||||
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
|
||||
'@types/node@24.10.1':
|
||||
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
||||
|
||||
'@types/object-assign-deep@0.4.3':
|
||||
resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
|
||||
@@ -728,9 +728,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
debounce@2.2.0:
|
||||
resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==}
|
||||
engines: {node: '>=18'}
|
||||
debounce@3.0.0:
|
||||
resolution: {integrity: sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
debug@4.4.0:
|
||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
||||
@@ -867,14 +867,13 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
glob@11.0.3:
|
||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||
glob@13.0.0:
|
||||
resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glossy@0.1.7:
|
||||
resolution: {integrity: sha512-mTCC51QFadK75MvAhrL5nPVIP291NjML1guo10Sa7Yj04tJU4V++Vgm780NIddg9etQD9D8FM67hFGqM8EE2HQ==}
|
||||
@@ -947,18 +946,14 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
js-sdsl@4.3.0:
|
||||
resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==}
|
||||
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
@@ -1090,8 +1085,8 @@ packages:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.0:
|
||||
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
pathe@2.0.3:
|
||||
@@ -1141,8 +1136,8 @@ packages:
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rimraf@6.1.0:
|
||||
resolution: {integrity: sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==}
|
||||
rimraf@6.1.2:
|
||||
resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
@@ -1468,23 +1463,23 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
zigbee-herdsman-converters@25.59.0:
|
||||
resolution: {integrity: sha512-Pw0L88hLQgv1vxv3uTFKbPDaMd75zuNWIcM0Y8+F0uKDZt8K8djEiBK74RlWYmHcrvBfzeRM61Jw/991/TShcw==}
|
||||
zigbee-herdsman-converters@25.80.0:
|
||||
resolution: {integrity: sha512-dJSN/Uo3MkGeSB11Afh1BwIRi64+ig5dxOElGUH7Ac1jbDMLSTx8WSTA52d8VcpXUneM4P5pagPlb/7WjFXMKg==}
|
||||
engines: {node: '>=20.15.0'}
|
||||
|
||||
zigbee-herdsman@6.3.2:
|
||||
resolution: {integrity: sha512-35qPU1KoADQrgIol/pWulVKRj8jFpuogtPWjisHSlss3dvzlj5yg5wBdw9/qpcDxNShtKMtj1K28io5KMmRsAw==}
|
||||
zigbee-herdsman@7.0.1:
|
||||
resolution: {integrity: sha512-XHfngXqc0G9cUN/YXivbhdNKX2NapxQ38F+ZWboF0VGJ6ZkiMNpGTO9Wnq6DpveBKHpcYJFlMbenDF5IxEW4qQ==}
|
||||
|
||||
zigbee-on-host@0.1.13:
|
||||
resolution: {integrity: sha512-mL7s9ic7J85YI7wOG1/QlbRYE3haFzIHqgnM2+xLmjj8CazjhmGjop9TCfuY2JXyDI9ss7bNCLxuI9n6M+TXug==}
|
||||
zigbee-on-host@0.2.2:
|
||||
resolution: {integrity: sha512-GixjVXTWDaLAN2650vuafv+KUY0JbwsCg5n8dsarFvCREkQw1B5DbQjbMs2Gz1+aRto2QtKfU7tkdDEa5JtIUg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
||||
zigbee2mqtt-frontend@0.9.21:
|
||||
resolution: {integrity: sha512-ClHYlG7g0v/tnIsD6kycNCi1ZEKIbQuU7lGJR3WqYcDmuqEMVcHDGbRiRwYRQcZGMxkqCf7guxNsXiKnC10+kg==}
|
||||
engines: {node: '>=20.11'}
|
||||
|
||||
zigbee2mqtt-windfront@2.2.3:
|
||||
resolution: {integrity: sha512-DCTmYqTEr5VLHvrtOGym/aKWgU+JolcEiGk5B9C8Hmugb5owwVLyJhdBl2GzIme6/28H5pb+BdxIJcEz0verUg==}
|
||||
zigbee2mqtt-windfront@2.4.2:
|
||||
resolution: {integrity: sha512-Tp77ZQ0yaQzeLOpgfWnkcKrMddjwnWwFHQxmtssbTVHvJUFpTQgB2Pbezj6ji5Wlr1xa8m8/J6S5dQms1TpQOg==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
snapshots:
|
||||
@@ -1511,39 +1506,39 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@biomejs/biome@2.3.2':
|
||||
'@biomejs/biome@2.3.8':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 2.3.2
|
||||
'@biomejs/cli-darwin-x64': 2.3.2
|
||||
'@biomejs/cli-linux-arm64': 2.3.2
|
||||
'@biomejs/cli-linux-arm64-musl': 2.3.2
|
||||
'@biomejs/cli-linux-x64': 2.3.2
|
||||
'@biomejs/cli-linux-x64-musl': 2.3.2
|
||||
'@biomejs/cli-win32-arm64': 2.3.2
|
||||
'@biomejs/cli-win32-x64': 2.3.2
|
||||
'@biomejs/cli-darwin-arm64': 2.3.8
|
||||
'@biomejs/cli-darwin-x64': 2.3.8
|
||||
'@biomejs/cli-linux-arm64': 2.3.8
|
||||
'@biomejs/cli-linux-arm64-musl': 2.3.8
|
||||
'@biomejs/cli-linux-x64': 2.3.8
|
||||
'@biomejs/cli-linux-x64-musl': 2.3.8
|
||||
'@biomejs/cli-win32-arm64': 2.3.8
|
||||
'@biomejs/cli-win32-x64': 2.3.8
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.3.2':
|
||||
'@biomejs/cli-darwin-arm64@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.3.2':
|
||||
'@biomejs/cli-darwin-x64@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.2':
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.3.2':
|
||||
'@biomejs/cli-linux-arm64@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.3.2':
|
||||
'@biomejs/cli-linux-x64-musl@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@2.3.2':
|
||||
'@biomejs/cli-linux-x64@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.3.2':
|
||||
'@biomejs/cli-win32-arm64@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@2.3.2':
|
||||
'@biomejs/cli-win32-x64@2.3.8':
|
||||
optional: true
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
@@ -1770,7 +1765,7 @@ snapshots:
|
||||
|
||||
'@types/finalhandler@1.2.4':
|
||||
dependencies:
|
||||
'@types/node': 24.9.2
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
@@ -1778,7 +1773,7 @@ snapshots:
|
||||
|
||||
'@types/js-yaml@4.0.9': {}
|
||||
|
||||
'@types/node@24.9.2':
|
||||
'@types/node@24.10.1':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
@@ -1786,20 +1781,20 @@ snapshots:
|
||||
|
||||
'@types/readable-stream@4.0.22':
|
||||
dependencies:
|
||||
'@types/node': 24.9.2
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 24.9.2
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 24.9.2
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.9.2))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
@@ -1814,7 +1809,7 @@ snapshots:
|
||||
std-env: 3.9.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/node@24.9.2)
|
||||
vitest: 3.2.4(@types/node@24.10.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -1826,13 +1821,13 @@ snapshots:
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.9.2))':
|
||||
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.10.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
vite: 6.3.5(@types/node@24.9.2)
|
||||
vite: 6.3.5(@types/node@24.10.1)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -1984,7 +1979,7 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
debounce@2.2.0: {}
|
||||
debounce@3.0.0: {}
|
||||
|
||||
debug@4.4.0:
|
||||
dependencies:
|
||||
@@ -2111,7 +2106,7 @@ snapshots:
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
glob@10.4.5:
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
@@ -2120,14 +2115,11 @@ snapshots:
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@11.0.3:
|
||||
glob@13.0.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.1.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 2.0.0
|
||||
path-scurry: 2.0.1
|
||||
|
||||
glossy@0.1.7: {}
|
||||
|
||||
@@ -2194,15 +2186,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
|
||||
js-sdsl@4.3.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
@@ -2349,7 +2337,7 @@ snapshots:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-scurry@2.0.0:
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
@@ -2402,9 +2390,9 @@ snapshots:
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rimraf@6.1.0:
|
||||
rimraf@6.1.2:
|
||||
dependencies:
|
||||
glob: 11.0.3
|
||||
glob: 13.0.0
|
||||
package-json-from-dist: 1.0.1
|
||||
|
||||
rollup@4.44.0:
|
||||
@@ -2549,7 +2537,7 @@ snapshots:
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 10.4.5
|
||||
glob: 10.5.0
|
||||
minimatch: 9.0.5
|
||||
|
||||
text-hex@1.0.0: {}
|
||||
@@ -2601,13 +2589,13 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite-node@3.2.4(@types/node@24.9.2):
|
||||
vite-node@3.2.4(@types/node@24.10.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 6.3.5(@types/node@24.9.2)
|
||||
vite: 6.3.5(@types/node@24.10.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -2622,7 +2610,7 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite@6.3.5(@types/node@24.9.2):
|
||||
vite@6.3.5(@types/node@24.10.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.5
|
||||
fdir: 6.4.6(picomatch@4.0.2)
|
||||
@@ -2631,14 +2619,14 @@ snapshots:
|
||||
rollup: 4.44.0
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 24.9.2
|
||||
'@types/node': 24.10.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitest@3.2.4(@types/node@24.9.2):
|
||||
vitest@3.2.4(@types/node@24.10.1):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.9.2))
|
||||
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.10.1))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -2656,11 +2644,11 @@ snapshots:
|
||||
tinyglobby: 0.2.14
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 6.3.5(@types/node@24.9.2)
|
||||
vite-node: 3.2.4(@types/node@24.9.2)
|
||||
vite: 6.3.5(@types/node@24.10.1)
|
||||
vite-node: 3.2.4(@types/node@24.10.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.9.2
|
||||
'@types/node': 24.10.1
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
@@ -2754,31 +2742,31 @@ snapshots:
|
||||
|
||||
ws@8.18.3: {}
|
||||
|
||||
zigbee-herdsman-converters@25.59.0:
|
||||
zigbee-herdsman-converters@25.80.0:
|
||||
dependencies:
|
||||
iconv-lite: 0.7.0
|
||||
semver: 7.7.3
|
||||
zigbee-herdsman: 6.3.2
|
||||
zigbee-herdsman: 7.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
zigbee-herdsman@6.3.2:
|
||||
zigbee-herdsman@7.0.1:
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.4.1
|
||||
'@serialport/bindings-cpp': 13.0.1
|
||||
'@serialport/parser-delimiter': 13.0.0
|
||||
'@serialport/stream': 13.0.0
|
||||
bonjour-service: 1.3.0
|
||||
debounce: 2.2.0
|
||||
debounce: 3.0.0
|
||||
fast-deep-equal: 3.1.3
|
||||
mixin-deep: 2.0.1
|
||||
slip: 1.0.2
|
||||
zigbee-on-host: 0.1.13
|
||||
zigbee-on-host: 0.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
zigbee-on-host@0.1.13: {}
|
||||
zigbee-on-host@0.2.2: {}
|
||||
|
||||
zigbee2mqtt-frontend@0.9.21: {}
|
||||
|
||||
zigbee2mqtt-windfront@2.2.3: {}
|
||||
zigbee2mqtt-windfront@2.4.2: {}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import {execSync} from "node:child_process";
|
||||
|
||||
export async function newDeviceSupport(github, _core, context, zhcDir) {
|
||||
const issue = context.payload.issue;
|
||||
// Hide previous bot comments
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
});
|
||||
|
||||
for (const comment of comments.data) {
|
||||
if (comment.user.type === "Bot" && comment.user.login === "github-actions[bot]") {
|
||||
await github.graphql(`mutation {
|
||||
minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) {
|
||||
clientMutationId
|
||||
}
|
||||
}`);
|
||||
}
|
||||
}
|
||||
|
||||
const titleAndBody = `${issue.title}\n\n${issue.body ?? ""}`;
|
||||
|
||||
// Check if Tuya manufacturer name is already supported.
|
||||
const tuyaManufacturerNameRe = /['"](_T\w+_(\w+))['"]/g;
|
||||
const tuyaManufacturerNames = Array.from(titleAndBody.matchAll(tuyaManufacturerNameRe), (m) => [m[1], m[2]]);
|
||||
console.log("Found tuyaManufacturerNames", tuyaManufacturerNames);
|
||||
if (tuyaManufacturerNames.length > 0) {
|
||||
for (const [fullName, partialName] of tuyaManufacturerNames) {
|
||||
const fullMatch = (() => {
|
||||
try {
|
||||
return execSync(`grep -r --include="*.ts" "${fullName}" "${zhcDir}"`, {encoding: "utf8"});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(`Checking full match for '${fullName}', result: '${fullMatch}'`);
|
||||
if (fullMatch) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `👋 Hi there! The Tuya device with manufacturer name \`${fullName}\` is already supported in the latest dev branch.
|
||||
See this [guide](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) on how to update, after updating you can remove your external converter.
|
||||
|
||||
In case you created the external converter with the goal to extend or fix an issue with the out-of-the-box support, please submit a pull request.
|
||||
For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).
|
||||
If you need help with the process, feel free to ask here and we'll be happy to assist.`,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const partialMatch = (() => {
|
||||
try {
|
||||
return execSync(`grep -r --include="*.ts" "${partialName}" "${zhcDir}"`, {encoding: "utf8"});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(`Checking partial match for '${partialName}', result: '${partialMatch}'`);
|
||||
if (partialMatch) {
|
||||
const candidates = Array.from(partialMatch.matchAll(tuyaManufacturerNameRe), (m) => m[1]);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `👋 Hi there! A similar Tuya device of which the manufacturer name also ends with \`_${partialName}\` is already supported.
|
||||
This means the device can probably be easily be supported by re-using the existing converter.
|
||||
|
||||
I found the following matches: ${candidates.map((c) => `\`${c}\``).join(", ")}
|
||||
Try to stop Z2M, change all occurrences of \`${fullName}\` in the \`data/database.db\` to one of the matches above and start Z2M.
|
||||
|
||||
Let us know if it works so we can support this device out-of-the-box!`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if zigbee model is already supported.
|
||||
const zigbeeModelRe = /zigbeeModel: \[['"](.+)['"]\]/g;
|
||||
const zigbeeModels = Array.from(titleAndBody.matchAll(zigbeeModelRe), (m) => m[1]);
|
||||
|
||||
if (zigbeeModels.length > 0) {
|
||||
for (const zigbeeModel of zigbeeModels) {
|
||||
const fullMatch = (() => {
|
||||
try {
|
||||
return execSync(`grep -r --include="*.ts" '"${zigbeeModel}"' "${zhcDir}"`, {encoding: "utf8"});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
if (fullMatch) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `👋 Hi there! The device with zigbee model \`${zigbeeModel}\` is already supported in the latest dev branch.
|
||||
See this [guide](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) on how to update, after updating you can remove your external converter.
|
||||
|
||||
In case you created the external converter with the goal to extend or fix an issue with the out-of-the-box support, please submit a pull request.
|
||||
For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).
|
||||
|
||||
If you need help with the process, feel free to ask here and we'll be happy to assist.`,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a request to pull request comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `🙏 Thank you for creating this issue and sharing your external converter!
|
||||
|
||||
In case all features work, please submit a pull request on this repository so the device can be supported out-of-the-box with the next release.
|
||||
For instructions on how to create a pull request see the [docs](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html#_4-create-a-pull-request).
|
||||
|
||||
If **NOT** all features work, please follow the [How To Support new devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
|
||||
${
|
||||
tuyaManufacturerNames.length > 0
|
||||
? "Since this is a Tuya also consider [How To Support new Tuya devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html)."
|
||||
: ""
|
||||
}
|
||||
|
||||
If you need help with the process, feel free to ask here and we'll be happy to assist.`,
|
||||
});
|
||||
}
|
||||
+44
-41
@@ -1,7 +1,7 @@
|
||||
import {existsSync, mkdirSync} from "node:fs";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import {bench, describe} from "vitest";
|
||||
import {Zcl, Zdo, ZSpec} from "zigbee-herdsman";
|
||||
import {bench, describe, vi} from "vitest";
|
||||
import {type Controller, 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";
|
||||
@@ -14,6 +14,48 @@ import type {RequestToResponseMap} from "zigbee-herdsman/dist/zspec/zdo/definiti
|
||||
import data from "../lib/util/data";
|
||||
import {BENCH_OPTIONS} from "./benchOptions";
|
||||
|
||||
vi.doMock("zigbee-herdsman", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("zigbee-herdsman")>();
|
||||
class MockHerdsman {
|
||||
on: Controller["on"] = vi.fn();
|
||||
start: Controller["start"] = async () => "resumed" as const;
|
||||
stop: Controller["stop"] = async () => {};
|
||||
isStopping: Controller["isStopping"] = () => false;
|
||||
getCoordinatorVersion: Controller["getCoordinatorVersion"] = async () =>
|
||||
Promise.resolve({
|
||||
type: "Dummy",
|
||||
meta: {revision: "9.9.9"},
|
||||
});
|
||||
getNetworkParameters: Controller["getNetworkParameters"] = async () => Promise.resolve({...NETWORK_PARAMS});
|
||||
getPermitJoin: Controller["getPermitJoin"] = () => false;
|
||||
getPermitJoinEnd: Controller["getPermitJoinEnd"] = () => undefined;
|
||||
getDeviceByIeeeAddr: Controller["getDeviceByIeeeAddr"] = (ieeeAddr) => ZH_DEVICES.find((device) => device.ieeeAddr === ieeeAddr);
|
||||
getGroupByID: Controller["getGroupByID"] = (id) => ZH_GROUPS.find((group) => group.groupID === id);
|
||||
getDevicesByType: Controller["getDevicesByType"] = (type) => ZH_DEVICES.filter((device) => device.type === type);
|
||||
getDeviceByNetworkAddress: Controller["getDeviceByNetworkAddress"] = (networkAddress) =>
|
||||
ZH_DEVICES.find((device) => device.networkAddress === networkAddress);
|
||||
*getDevicesIterator(predicate: ((device: Device) => boolean) | undefined) {
|
||||
for (const device of ZH_DEVICES) {
|
||||
if (!predicate || predicate(device)) {
|
||||
yield device;
|
||||
}
|
||||
}
|
||||
}
|
||||
*getGroupsIterator(predicate: ((group: Group) => boolean) | undefined) {
|
||||
for (const group of ZH_GROUPS) {
|
||||
if (!predicate || predicate(group)) {
|
||||
yield group;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Controller: MockHerdsman,
|
||||
};
|
||||
});
|
||||
|
||||
process.env.ZIGBEE2MQTT_DATA = "data-bench";
|
||||
data._testReload();
|
||||
|
||||
@@ -365,45 +407,6 @@ const initController = 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 = {
|
||||
|
||||
@@ -795,4 +795,84 @@ describe("Extension: Bind", () => {
|
||||
// Should only call Hue bulb, not e.g. tradfri
|
||||
expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("clears all bindings", async () => {
|
||||
const device = devices.remote;
|
||||
|
||||
device.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({transaction: "1234", target: "remote"}));
|
||||
await flushPromises();
|
||||
|
||||
expect(device.clearAllBindings).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/binds/clear",
|
||||
stringify({
|
||||
transaction: "1234",
|
||||
data: {
|
||||
target: "remote",
|
||||
ieee_list: ["0xffffffffffffffff"],
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
|
||||
});
|
||||
|
||||
it("clears targeted bindings", async () => {
|
||||
const device = devices.remote;
|
||||
const target = devices.bulb_color;
|
||||
|
||||
device.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/binds/clear",
|
||||
stringify({transaction: "1234", target: "remote", ieee_list: [target.ieeeAddr]}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(device.clearAllBindings).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/binds/clear",
|
||||
stringify({
|
||||
transaction: "1234",
|
||||
data: {
|
||||
target: "remote",
|
||||
ieee_list: [target.ieeeAddr],
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
|
||||
});
|
||||
|
||||
it("throw on invalid clears bindings payload", async () => {
|
||||
const device = devices.remote;
|
||||
|
||||
device.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({targetz: "remote"}));
|
||||
await flushPromises();
|
||||
|
||||
expect(device.clearAllBindings).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/binds/clear",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("throw on invalid clears bindings target", async () => {
|
||||
const device = devices.remote;
|
||||
|
||||
device.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({target: "remotez"}));
|
||||
await flushPromises();
|
||||
|
||||
expect(device.clearAllBindings).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/binds/clear",
|
||||
stringify({data: {}, status: "error", error: "Invalid target"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+364
-28
@@ -1,5 +1,6 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import {Zdo} from "zigbee-herdsman";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockJSZipFile, mockJSZipGenerateAsync} from "../mocks/jszip";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
@@ -17,6 +18,7 @@ import {Controller} from "../../lib/controller";
|
||||
import Bridge from "../../lib/extension/bridge";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
import utils, {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
|
||||
returnDevices.push(devices.coordinator.ieeeAddr);
|
||||
returnDevices.push(devices.bulb.ieeeAddr);
|
||||
@@ -545,6 +547,7 @@ describe("Extension: Bridge", () => {
|
||||
property: "transition",
|
||||
type: "numeric",
|
||||
value_min: 0,
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -771,6 +774,7 @@ describe("Extension: Bridge", () => {
|
||||
property: "transition",
|
||||
type: "numeric",
|
||||
value_min: 0,
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -1883,6 +1887,7 @@ describe("Extension: Bridge", () => {
|
||||
property: "transition",
|
||||
type: "numeric",
|
||||
value_min: 0,
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -2149,6 +2154,7 @@ describe("Extension: Bridge", () => {
|
||||
property: "transition",
|
||||
type: "numeric",
|
||||
value_min: 0,
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -2583,6 +2589,7 @@ describe("Extension: Bridge", () => {
|
||||
property: "transition",
|
||||
type: "numeric",
|
||||
value_min: 0,
|
||||
value_step: 0.1,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
@@ -3532,11 +3539,11 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
it("Should allow to touchlink factory reset (succeeds)", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockZHController.touchlinkFactoryResetFirst.mockClear();
|
||||
mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(true);
|
||||
mockZHController.touchlink.factoryResetFirst.mockClear();
|
||||
mockZHController.touchlink.factoryResetFirst.mockReturnValueOnce(true);
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", "");
|
||||
await flushPromises();
|
||||
expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.touchlink.factoryResetFirst).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/touchlink/factory_reset",
|
||||
stringify({data: {}, status: "ok"}),
|
||||
@@ -3546,12 +3553,12 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
it("Should allow to touchlink factory reset specific device", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockZHController.touchlinkFactoryReset.mockClear();
|
||||
mockZHController.touchlinkFactoryReset.mockReturnValueOnce(true);
|
||||
mockZHController.touchlink.factoryReset.mockClear();
|
||||
mockZHController.touchlink.factoryReset.mockReturnValueOnce(true);
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", stringify({ieee_address: "0x1239", channel: 12}));
|
||||
await flushPromises();
|
||||
expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledWith("0x1239", 12);
|
||||
expect(mockZHController.touchlink.factoryReset).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.touchlink.factoryReset).toHaveBeenCalledWith("0x1239", 12);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/touchlink/factory_reset",
|
||||
stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}),
|
||||
@@ -3602,11 +3609,11 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
it("Should allow to touchlink identify specific device", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockZHController.touchlinkIdentify.mockClear();
|
||||
mockZHController.touchlink.identify.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/identify", stringify({ieee_address: "0x1239", channel: 12}));
|
||||
await flushPromises();
|
||||
expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.touchlinkIdentify).toHaveBeenCalledWith("0x1239", 12);
|
||||
expect(mockZHController.touchlink.identify).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.touchlink.identify).toHaveBeenCalledWith("0x1239", 12);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/touchlink/identify",
|
||||
stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}),
|
||||
@@ -3616,10 +3623,10 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
it("Touchlink identify fails when payload is invalid", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockZHController.touchlinkIdentify.mockClear();
|
||||
mockZHController.touchlink.identify.mockClear();
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/identify", stringify({ieee_address: "0x1239"}));
|
||||
await flushPromises();
|
||||
expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(0);
|
||||
expect(mockZHController.touchlink.identify).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/touchlink/identify",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
@@ -3629,11 +3636,11 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
it("Should allow to touchlink factory reset (fails)", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockZHController.touchlinkFactoryResetFirst.mockClear();
|
||||
mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(false);
|
||||
mockZHController.touchlink.factoryResetFirst.mockClear();
|
||||
mockZHController.touchlink.factoryResetFirst.mockReturnValueOnce(false);
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", "");
|
||||
await flushPromises();
|
||||
expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.touchlink.factoryResetFirst).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/touchlink/factory_reset",
|
||||
stringify({data: {}, status: "error", error: "Failed to factory reset device through Touchlink"}),
|
||||
@@ -3643,14 +3650,14 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
it("Should allow to touchlink scan", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockZHController.touchlinkScan.mockClear();
|
||||
mockZHController.touchlinkScan.mockReturnValueOnce([
|
||||
mockZHController.touchlink.scan.mockClear();
|
||||
mockZHController.touchlink.scan.mockReturnValueOnce([
|
||||
{ieeeAddr: "0x123", channel: 12},
|
||||
{ieeeAddr: "0x124", channel: 24},
|
||||
]);
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/scan", "");
|
||||
await flushPromises();
|
||||
expect(mockZHController.touchlinkScan).toHaveBeenCalledTimes(1);
|
||||
expect(mockZHController.touchlink.scan).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/touchlink/scan",
|
||||
stringify({
|
||||
@@ -3673,7 +3680,7 @@ describe("Extension: Bridge", () => {
|
||||
endpoint.configureReporting.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/request/device/reporting/configure",
|
||||
stringify({
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: 1,
|
||||
@@ -3694,7 +3701,7 @@ describe("Extension: Bridge", () => {
|
||||
undefined,
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/response/device/reporting/configure",
|
||||
stringify({
|
||||
data: {
|
||||
id: "0x000b57fffec6a5b2",
|
||||
@@ -3719,7 +3726,7 @@ describe("Extension: Bridge", () => {
|
||||
endpoint.configureReporting.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/request/device/reporting/configure",
|
||||
stringify({
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: "1",
|
||||
@@ -3740,7 +3747,7 @@ describe("Extension: Bridge", () => {
|
||||
undefined,
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/response/device/reporting/configure",
|
||||
stringify({
|
||||
data: {
|
||||
id: "0x000b57fffec6a5b2",
|
||||
@@ -3764,7 +3771,7 @@ describe("Extension: Bridge", () => {
|
||||
endpoint.configureReporting.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/request/device/reporting/configure",
|
||||
stringify({
|
||||
id: "bulb",
|
||||
// endpoint: '1',
|
||||
@@ -3778,7 +3785,7 @@ describe("Extension: Bridge", () => {
|
||||
await flushPromises();
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/response/device/reporting/configure",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
{},
|
||||
);
|
||||
@@ -3790,7 +3797,7 @@ describe("Extension: Bridge", () => {
|
||||
endpoint.configureReporting.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/request/device/reporting/configure",
|
||||
stringify({
|
||||
id: "non_existing_device",
|
||||
endpoint: "1",
|
||||
@@ -3804,7 +3811,7 @@ describe("Extension: Bridge", () => {
|
||||
await flushPromises();
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/response/device/reporting/configure",
|
||||
stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}),
|
||||
{},
|
||||
);
|
||||
@@ -3816,7 +3823,7 @@ describe("Extension: Bridge", () => {
|
||||
endpoint.configureReporting.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/request/device/reporting/configure",
|
||||
stringify({
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: "non_existing_endpoint",
|
||||
@@ -3830,7 +3837,243 @@ describe("Extension: Bridge", () => {
|
||||
await flushPromises();
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure_reporting",
|
||||
"zigbee2mqtt/bridge/response/device/reporting/configure",
|
||||
stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("Should allow to read reporting config with endpoint as number", async () => {
|
||||
const device = devices.bulb;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
endpoint.bind.mockClear();
|
||||
endpoint.readReportingConfig.mockClear();
|
||||
endpoint.readReportingConfig.mockResolvedValueOnce([
|
||||
{
|
||||
status: Zcl.Status.SUCCESS,
|
||||
direction: Zcl.Direction.CLIENT_TO_SERVER,
|
||||
attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
|
||||
dataType: Zcl.DataType.UINT8,
|
||||
minRepIntval: 10,
|
||||
maxRepIntval: 60,
|
||||
repChange: 2,
|
||||
},
|
||||
]);
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/reporting/read",
|
||||
stringify({
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: 1,
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [{attribute: "currentLevel"}],
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/reporting/read",
|
||||
stringify({
|
||||
data: {
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: 1,
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [
|
||||
{
|
||||
status: Zcl.Status.SUCCESS,
|
||||
direction: Zcl.Direction.CLIENT_TO_SERVER,
|
||||
attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
|
||||
dataType: Zcl.DataType.UINT8,
|
||||
minRepIntval: 10,
|
||||
maxRepIntval: 60,
|
||||
repChange: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
|
||||
});
|
||||
|
||||
it("Should allow to read reporting config with endpoint as string", async () => {
|
||||
const device = devices.bulb;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
endpoint.bind.mockClear();
|
||||
endpoint.readReportingConfig.mockClear();
|
||||
endpoint.readReportingConfig.mockResolvedValueOnce([
|
||||
{
|
||||
status: Zcl.Status.SUCCESS,
|
||||
direction: Zcl.Direction.CLIENT_TO_SERVER,
|
||||
attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
|
||||
dataType: Zcl.DataType.UINT8,
|
||||
minRepIntval: 10,
|
||||
maxRepIntval: 60,
|
||||
repChange: 2,
|
||||
},
|
||||
]);
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/reporting/read",
|
||||
stringify({
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: "1",
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [{attribute: "currentLevel"}],
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/reporting/read",
|
||||
stringify({
|
||||
data: {
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: "1",
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [
|
||||
{
|
||||
status: Zcl.Status.SUCCESS,
|
||||
direction: Zcl.Direction.CLIENT_TO_SERVER,
|
||||
attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
|
||||
dataType: Zcl.DataType.UINT8,
|
||||
minRepIntval: 10,
|
||||
maxRepIntval: 60,
|
||||
repChange: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
|
||||
});
|
||||
|
||||
it("Should allow to read reporting config with manufacturer code", async () => {
|
||||
const device = devices.bulb;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
endpoint.bind.mockClear();
|
||||
endpoint.readReportingConfig.mockClear();
|
||||
endpoint.readReportingConfig.mockResolvedValueOnce([
|
||||
{
|
||||
status: Zcl.Status.SUCCESS,
|
||||
direction: Zcl.Direction.CLIENT_TO_SERVER,
|
||||
attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
|
||||
dataType: Zcl.DataType.UINT8,
|
||||
minRepIntval: 10,
|
||||
maxRepIntval: 60,
|
||||
repChange: 2,
|
||||
},
|
||||
]);
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/reporting/read",
|
||||
stringify({
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: 1,
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [{attribute: "currentLevel"}],
|
||||
manufacturer_code: 0x1234,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {manufacturerCode: 0x1234});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/reporting/read",
|
||||
stringify({
|
||||
data: {
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: 1,
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [
|
||||
{
|
||||
status: Zcl.Status.SUCCESS,
|
||||
direction: Zcl.Direction.CLIENT_TO_SERVER,
|
||||
attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID,
|
||||
dataType: Zcl.DataType.UINT8,
|
||||
minRepIntval: 10,
|
||||
maxRepIntval: 60,
|
||||
repChange: 2,
|
||||
},
|
||||
],
|
||||
manufacturer_code: 0x1234,
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
|
||||
});
|
||||
|
||||
it("Should throw error when read reporting config is called with malformed payload", async () => {
|
||||
const device = devices.bulb;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
endpoint.readReportingConfig.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/reporting/read",
|
||||
stringify({
|
||||
id: "bulb",
|
||||
// endpoint: '1',
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [{attribute: "currentLevel"}],
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/reporting/read",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("Should throw error when read reporting config is called for non-existing device", async () => {
|
||||
const device = devices.bulb;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
endpoint.readReportingConfig.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/reporting/read",
|
||||
stringify({
|
||||
id: "non_existing_device",
|
||||
endpoint: "1",
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [{attribute: "currentLevel"}],
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/reporting/read",
|
||||
stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("Should throw error when read reporting config is called for non-existing endpoint", async () => {
|
||||
const device = devices.bulb;
|
||||
const endpoint = device.getEndpoint(1)!;
|
||||
endpoint.readReportingConfig.mockClear();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/device/reporting/read",
|
||||
stringify({
|
||||
id: "0x000b57fffec6a5b2",
|
||||
endpoint: "non_existing_endpoint",
|
||||
cluster: "genLevelCtrl",
|
||||
configs: [{attribute: "currentLevel"}],
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/reporting/read",
|
||||
stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}),
|
||||
{},
|
||||
);
|
||||
@@ -4106,4 +4349,97 @@ describe("Extension: Bridge", () => {
|
||||
});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/device/configure", expect.any(String), {});
|
||||
});
|
||||
|
||||
it("triggers ZHC action by name with params", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/action",
|
||||
JSON.stringify({
|
||||
action: "raw",
|
||||
params: {
|
||||
profileId: Zdo.ZDO_PROFILE_ID,
|
||||
ieeeAddress: "0xf1f2f3f4f5f6f7f8",
|
||||
networkAddress: 0x1234,
|
||||
clusterKey: Zdo.ClusterId.NETWORK_ADDRESS_REQUEST,
|
||||
zdoParams: ["0xa1a2a3a4a5a6a7a8", false, 0],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/action",
|
||||
JSON.stringify({data: [0x00, {assocDevList: [], eui64: "", nwkAddress: 0x1234, startIndex: 0}], status: "ok"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("triggers ZHC action by name without params", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/action",
|
||||
JSON.stringify({
|
||||
action: "raw",
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
// we're mocking the response, so it's always this as long as the action & payload are valid
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/action",
|
||||
JSON.stringify({data: [0x00, {assocDevList: [], eui64: "", nwkAddress: 0x1234, startIndex: 0}], status: "ok"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("throws on invalid action payload", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/action",
|
||||
JSON.stringify({
|
||||
params: {
|
||||
profileId: Zdo.ZDO_PROFILE_ID,
|
||||
ieeeAddress: "0xf1f2f3f4f5f6f7f8",
|
||||
networkAddress: 0x1234,
|
||||
clusterKey: Zdo.ClusterId.NETWORK_ADDRESS_REQUEST,
|
||||
zdoParams: ["0xa1a2a3a4a5a6a7a8", false, 0],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/action",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("throws on invalid action", async () => {
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
|
||||
mockMQTTEvents.message(
|
||||
"zigbee2mqtt/bridge/request/action",
|
||||
JSON.stringify({
|
||||
action: "DOES_NOT_EXIST",
|
||||
params: {
|
||||
profileId: Zdo.ZDO_PROFILE_ID,
|
||||
ieeeAddress: "0xf1f2f3f4f5f6f7f8",
|
||||
networkAddress: 0x1234,
|
||||
clusterKey: Zdo.ClusterId.NETWORK_ADDRESS_REQUEST,
|
||||
zdoParams: ["0xa1a2a3a4a5a6a7a8", false, 0],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/action",
|
||||
stringify({data: {}, status: "error", error: "Invalid action"}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1117,10 +1117,10 @@ describe("Extension: HomeAssistant", () => {
|
||||
},
|
||||
max_temp: "30",
|
||||
min_temp: "5",
|
||||
mode_command_template: `{% set values = { 'auto':'schedule', 'heat':'manual', 'off':'pause' } %}{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`,
|
||||
mode_command_template: `{% set values = { 'auto':'schedule','heat':'manual','off':'pause'} %}{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}`,
|
||||
mode_command_topic: "zigbee2mqtt/bosch_radiator/set",
|
||||
mode_state_template:
|
||||
"{% set values = { 'schedule':'auto', 'manual':'heat', 'pause':'off' } %}{% set value = value_json.operating_mode %}{{ values[value] if value in values.keys() else 'off' }}",
|
||||
"{% set values = {'schedule':'auto','manual':'heat','pause':'off'} %}{% set value = value_json.operating_mode %}{{ values[value] if value in values.keys() else 'off' }}",
|
||||
mode_state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
modes: ["off", "heat", "auto"],
|
||||
name: null,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -25,6 +26,22 @@ returnDevices.push(
|
||||
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
|
||||
|
||||
const LQI_BASE_RSP = {
|
||||
extendedPanId: [],
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
reserved1: 0,
|
||||
permitJoining: 0,
|
||||
reserved2: 0,
|
||||
};
|
||||
|
||||
const ROUTING_BASE_RSP = {
|
||||
memoryConstrained: 0,
|
||||
manyToOne: 0,
|
||||
routeRecordRequired: 0,
|
||||
reserved1: 0,
|
||||
};
|
||||
|
||||
describe("Extension: NetworkMap", () => {
|
||||
let controller: Controller;
|
||||
|
||||
@@ -39,58 +56,62 @@ describe("Extension: NetworkMap", () => {
|
||||
* | -> CC2530_ROUTER -> WXKG02LM_rev1
|
||||
*
|
||||
*/
|
||||
devices.coordinator.lqi.mockResolvedValueOnce({
|
||||
neighbors: [
|
||||
{
|
||||
ieeeAddr: devices.bulb_color.ieeeAddr,
|
||||
networkAddress: devices.bulb_color.networkAddress,
|
||||
relationship: 2,
|
||||
depth: 1,
|
||||
linkquality: 120,
|
||||
},
|
||||
{ieeeAddr: devices.bulb.ieeeAddr, networkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
|
||||
{
|
||||
ieeeAddr: devices.external_converter_device.ieeeAddr,
|
||||
networkAddress: devices.external_converter_device.networkAddress,
|
||||
relationship: 2,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
},
|
||||
],
|
||||
});
|
||||
devices.coordinator.routingTable.mockResolvedValueOnce({
|
||||
table: [{destinationAddress: devices.CC2530_ROUTER.networkAddress, status: "ACTIVE", nextHop: devices.bulb.networkAddress}],
|
||||
});
|
||||
devices.bulb.lqi.mockResolvedValueOnce({
|
||||
neighbors: [
|
||||
{
|
||||
ieeeAddr: devices.bulb_color.ieeeAddr,
|
||||
networkAddress: devices.bulb_color.networkAddress,
|
||||
relationship: 1,
|
||||
depth: 2,
|
||||
linkquality: 110,
|
||||
},
|
||||
{
|
||||
ieeeAddr: devices.CC2530_ROUTER.ieeeAddr,
|
||||
networkAddress: devices.CC2530_ROUTER.networkAddress,
|
||||
relationship: 1,
|
||||
depth: 2,
|
||||
linkquality: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
devices.CC2530_ROUTER.lqi.mockResolvedValueOnce({
|
||||
neighbors: [
|
||||
{ieeeAddr: "0x0000000000000000", networkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
|
||||
{
|
||||
ieeeAddr: devices.bulb_color.ieeeAddr,
|
||||
networkAddress: devices.bulb_color.networkAddress,
|
||||
relationship: 4,
|
||||
depth: 2,
|
||||
linkquality: 130,
|
||||
},
|
||||
],
|
||||
});
|
||||
devices.coordinator.lqi.mockResolvedValueOnce([
|
||||
{
|
||||
eui64: devices.bulb_color.ieeeAddr as Eui64,
|
||||
nwkAddress: devices.bulb_color.networkAddress,
|
||||
relationship: 2,
|
||||
depth: 1,
|
||||
lqi: 120,
|
||||
...LQI_BASE_RSP,
|
||||
},
|
||||
{eui64: devices.bulb.ieeeAddr as Eui64, nwkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, lqi: 92, ...LQI_BASE_RSP},
|
||||
{
|
||||
eui64: devices.external_converter_device.ieeeAddr as Eui64,
|
||||
nwkAddress: devices.external_converter_device.networkAddress,
|
||||
relationship: 2,
|
||||
depth: 1,
|
||||
lqi: 92,
|
||||
...LQI_BASE_RSP,
|
||||
},
|
||||
]);
|
||||
devices.coordinator.routingTable.mockResolvedValueOnce([
|
||||
{
|
||||
destinationAddress: devices.CC2530_ROUTER.networkAddress,
|
||||
status: "ACTIVE",
|
||||
nextHopAddress: devices.bulb.networkAddress,
|
||||
...ROUTING_BASE_RSP,
|
||||
},
|
||||
]);
|
||||
devices.bulb.lqi.mockResolvedValueOnce([
|
||||
{
|
||||
eui64: devices.bulb_color.ieeeAddr as Eui64,
|
||||
nwkAddress: devices.bulb_color.networkAddress,
|
||||
relationship: 1,
|
||||
depth: 2,
|
||||
lqi: 110,
|
||||
...LQI_BASE_RSP,
|
||||
},
|
||||
{
|
||||
eui64: devices.CC2530_ROUTER.ieeeAddr as Eui64,
|
||||
nwkAddress: devices.CC2530_ROUTER.networkAddress,
|
||||
relationship: 1,
|
||||
depth: 2,
|
||||
lqi: 100,
|
||||
...LQI_BASE_RSP,
|
||||
},
|
||||
]);
|
||||
devices.CC2530_ROUTER.lqi.mockResolvedValueOnce([
|
||||
{eui64: "0x0000000000000000", nwkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, lqi: 130, ...LQI_BASE_RSP},
|
||||
{
|
||||
eui64: devices.bulb_color.ieeeAddr as Eui64,
|
||||
nwkAddress: devices.bulb_color.networkAddress,
|
||||
relationship: 4,
|
||||
depth: 2,
|
||||
lqi: 130,
|
||||
...LQI_BASE_RSP,
|
||||
},
|
||||
]);
|
||||
devices.unsupported_router.lqi.mockRejectedValueOnce("failed").mockRejectedValueOnce("failed");
|
||||
devices.unsupported_router.routingTable.mockRejectedValueOnce("failed").mockRejectedValueOnce("failed");
|
||||
};
|
||||
@@ -143,6 +164,9 @@ describe("Extension: NetworkMap", () => {
|
||||
value: {
|
||||
links: [
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 120,
|
||||
lqi: 120,
|
||||
@@ -155,11 +179,14 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
relationship: 2,
|
||||
routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}],
|
||||
routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}],
|
||||
source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369},
|
||||
sourceIeeeAddr: "0x000b57fffec6a5b2",
|
||||
sourceNwkAddr: 40369,
|
||||
@@ -167,6 +194,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
@@ -179,6 +209,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 2,
|
||||
linkquality: 110,
|
||||
lqi: 110,
|
||||
@@ -191,6 +224,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x000b57fffec6a5b2",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 2,
|
||||
linkquality: 100,
|
||||
lqi: 100,
|
||||
@@ -203,6 +239,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x000b57fffec6a5b2",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 2,
|
||||
linkquality: 130,
|
||||
lqi: 130,
|
||||
@@ -492,6 +531,9 @@ describe("Extension: NetworkMap", () => {
|
||||
value: {
|
||||
links: [
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 120,
|
||||
lqi: 120,
|
||||
@@ -504,11 +546,14 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
relationship: 2,
|
||||
routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}],
|
||||
routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}],
|
||||
source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369},
|
||||
sourceIeeeAddr: "0x000b57fffec6a5b2",
|
||||
sourceNwkAddr: 40369,
|
||||
@@ -516,6 +561,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
@@ -528,6 +576,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 2,
|
||||
linkquality: 130,
|
||||
lqi: 130,
|
||||
@@ -649,6 +700,9 @@ describe("Extension: NetworkMap", () => {
|
||||
value: {
|
||||
links: [
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 120,
|
||||
lqi: 120,
|
||||
@@ -661,11 +715,14 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
relationship: 2,
|
||||
routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}],
|
||||
routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}],
|
||||
source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369},
|
||||
sourceIeeeAddr: "0x000b57fffec6a5b2",
|
||||
sourceNwkAddr: 40369,
|
||||
@@ -673,6 +730,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
lqi: 92,
|
||||
@@ -685,6 +745,9 @@ describe("Extension: NetworkMap", () => {
|
||||
targetIeeeAddr: "0x00124b00120144ae",
|
||||
},
|
||||
{
|
||||
deviceType: 0,
|
||||
rxOnWhenIdle: 0,
|
||||
permitJoining: 0,
|
||||
depth: 2,
|
||||
linkquality: 130,
|
||||
lqi: 130,
|
||||
|
||||
@@ -276,50 +276,55 @@ describe("Extension: OTAUpdate", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["check", "check/downgrade", "update", "update/downgrade", "schedule", "schedule/downgrade", "unschedule"])(
|
||||
"refuses to %s when already in progress",
|
||||
async (type) => {
|
||||
if (type.includes("schedule") || type.includes("check")) {
|
||||
isUpdateAvailableSpy.mockImplementationOnce(async () => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
available: false,
|
||||
currentFileVersion: 1,
|
||||
otaFileVersion: 1,
|
||||
}),
|
||||
99999,
|
||||
);
|
||||
});
|
||||
it.each([
|
||||
"check",
|
||||
"check/downgrade",
|
||||
"update",
|
||||
"update/downgrade",
|
||||
"schedule",
|
||||
"schedule/downgrade",
|
||||
"unschedule",
|
||||
])("refuses to %s when already in progress", async (type) => {
|
||||
if (type.includes("schedule") || type.includes("check")) {
|
||||
isUpdateAvailableSpy.mockImplementationOnce(async () => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
available: false,
|
||||
currentFileVersion: 1,
|
||||
otaFileVersion: 1,
|
||||
}),
|
||||
99999,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
updateSpy.mockImplementationOnce(async () => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(1), 99999);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
updateSpy.mockImplementationOnce(async () => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(1), 99999);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mockMQTTEvents.message(`zigbee2mqtt/bridge/request/device/ota_update/${type.includes("schedule") ? "check" : type}`, "bulb");
|
||||
await flushPromises();
|
||||
mockMQTTEvents.message(`zigbee2mqtt/bridge/request/device/ota_update/${type}`, "bulb");
|
||||
await flushPromises();
|
||||
mockMQTTEvents.message(`zigbee2mqtt/bridge/request/device/ota_update/${type.includes("schedule") ? "check" : type}`, "bulb");
|
||||
await flushPromises();
|
||||
mockMQTTEvents.message(`zigbee2mqtt/bridge/request/device/ota_update/${type}`, "bulb");
|
||||
await flushPromises();
|
||||
|
||||
if (type.includes("schedule") || type.includes("check")) {
|
||||
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
|
||||
} else {
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
if (type.includes("schedule") || type.includes("check")) {
|
||||
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
|
||||
} else {
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
`zigbee2mqtt/bridge/response/device/ota_update/${type.replace("/downgrade", "")}`,
|
||||
stringify({data: {}, status: "error", error: `Update or check for update already in progress for 'bulb'`}),
|
||||
{},
|
||||
);
|
||||
},
|
||||
);
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
`zigbee2mqtt/bridge/response/device/ota_update/${type.replace("/downgrade", "")}`,
|
||||
stringify({data: {}, status: "error", error: `Update or check for update already in progress for 'bulb'`}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not crash when read modelID before/after OTA update fails", async () => {
|
||||
devices.bulb.endpoints[0].read.mockRejectedValueOnce("Failed from").mockRejectedValueOnce("Failed to");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assert from "node:assert";
|
||||
import type {Mock} from "vitest";
|
||||
import {type Mock, vi} from "vitest";
|
||||
import type {AdapterTypes} from "zigbee-herdsman";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
import type {BindingTableEntry, LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
|
||||
import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils";
|
||||
import type {EventHandler} from "./utils";
|
||||
|
||||
@@ -104,6 +104,7 @@ export class Endpoint {
|
||||
unbind: Mock;
|
||||
save: Mock;
|
||||
configureReporting: Mock;
|
||||
readReportingConfig: Mock;
|
||||
meta: Record<string, unknown>;
|
||||
binds: ZHBind[];
|
||||
profileID: number | undefined;
|
||||
@@ -138,6 +139,7 @@ export class Endpoint {
|
||||
this.unbind = vi.fn();
|
||||
this.save = vi.fn();
|
||||
this.configureReporting = vi.fn();
|
||||
this.readReportingConfig = vi.fn();
|
||||
this.meta = meta;
|
||||
this.binds = binds;
|
||||
this.profileID = profileID;
|
||||
@@ -218,6 +220,7 @@ export class Endpoint {
|
||||
this.unbind.mockClear();
|
||||
this.save.mockClear();
|
||||
this.configureReporting.mockClear();
|
||||
this.readReportingConfig.mockClear();
|
||||
this.addToGroup.mockClear();
|
||||
this.removeFromGroup.mockClear();
|
||||
this.getClusterAttributeValue.mockClear();
|
||||
@@ -247,8 +250,10 @@ export class Device {
|
||||
lastSeen: number | undefined;
|
||||
isDeleted: boolean;
|
||||
linkquality?: number;
|
||||
lqi: Mock;
|
||||
routingTable: Mock;
|
||||
lqi: Mock<() => Promise<LQITableEntry[]>>;
|
||||
routingTable: Mock<() => Promise<RoutingTableEntry[]>>;
|
||||
bindingTable: Mock<() => Promise<BindingTableEntry[]>>;
|
||||
clearAllBindings: Mock<() => Promise<void>>;
|
||||
|
||||
constructor(
|
||||
type: string,
|
||||
@@ -285,8 +290,10 @@ export class Device {
|
||||
this.manufacturerName = manufacturerName;
|
||||
this.lastSeen = 1000;
|
||||
this.isDeleted = false;
|
||||
this.lqi = vi.fn(() => ({neighbors: []}));
|
||||
this.routingTable = vi.fn(() => ({table: []}));
|
||||
this.lqi = vi.fn(() => Promise.resolve([] as LQITableEntry[]));
|
||||
this.routingTable = vi.fn(() => Promise.resolve([] as RoutingTableEntry[]));
|
||||
this.bindingTable = vi.fn(() => Promise.resolve([] as BindingTableEntry[]));
|
||||
this.clearAllBindings = vi.fn(() => {});
|
||||
}
|
||||
|
||||
getEndpoint(ID: number): Endpoint | undefined {
|
||||
@@ -302,6 +309,8 @@ export class Device {
|
||||
this.save.mockClear();
|
||||
this.lqi.mockClear();
|
||||
this.routingTable.mockClear();
|
||||
this.bindingTable.mockClear();
|
||||
this.clearAllBindings.mockClear();
|
||||
this.meta = {};
|
||||
|
||||
for (const ep of this.endpoints) {
|
||||
@@ -1136,10 +1145,7 @@ export const mockController = {
|
||||
},
|
||||
start: vi.fn((): Promise<AdapterTypes.StartResult> => Promise.resolve("reset")),
|
||||
stop: vi.fn(),
|
||||
touchlinkIdentify: vi.fn(),
|
||||
touchlinkScan: vi.fn(),
|
||||
touchlinkFactoryReset: vi.fn(),
|
||||
touchlinkFactoryResetFirst: vi.fn(),
|
||||
touchlink: {identify: vi.fn(), scan: vi.fn(), factoryReset: vi.fn(), factoryResetFirst: vi.fn()},
|
||||
addInstallCode: vi.fn(),
|
||||
permitJoin: vi.fn(),
|
||||
getPermitJoin: vi.fn((): boolean => false),
|
||||
@@ -1195,6 +1201,9 @@ export const mockController = {
|
||||
groups[`group_${groupID}` as keyof typeof groups] = group;
|
||||
return group;
|
||||
}),
|
||||
sendRaw: vi.fn(async (/*rawPayload: RawPayload, customClusters: CustomClusters = {}*/) =>
|
||||
Promise.resolve([0x00, {nwkAddress: 0x1234, eui64: "", startIndex: 0, assocDevList: []}]),
|
||||
),
|
||||
};
|
||||
|
||||
vi.mock("zigbee-herdsman", async (importOriginal) => ({
|
||||
|
||||
@@ -293,7 +293,7 @@ describe("Onboarding", () => {
|
||||
}
|
||||
|
||||
const serverUrl = new URL(process.env.Z2M_ONBOARD_URL ?? "http://0.0.0.0:8080");
|
||||
expect(mockHttpListen).toHaveBeenCalledWith(Number.parseInt(serverUrl.port), serverUrl.hostname, expect.any(Function));
|
||||
expect(mockHttpListen).toHaveBeenCalledWith(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, expect.any(Function));
|
||||
|
||||
return [resEnd.mock.calls[0][0], resEnd.mock.calls[1][0]];
|
||||
};
|
||||
@@ -351,7 +351,7 @@ describe("Onboarding", () => {
|
||||
expect(resEnd).toHaveBeenCalledTimes(2);
|
||||
|
||||
const serverUrl = new URL(process.env.Z2M_ONBOARD_URL ?? "http://0.0.0.0:8080");
|
||||
expect(mockHttpListen).toHaveBeenCalledWith(Number.parseInt(serverUrl.port), serverUrl.hostname, expect.any(Function));
|
||||
expect(mockHttpListen).toHaveBeenCalledWith(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, expect.any(Function));
|
||||
|
||||
return resEnd.mock.calls[0][0];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user