Compare commits

...
52 Commits
Author SHA1 Message Date
Koen Kanters 110149fa20 chore(dev): release 2.7.0 (#29396) 2025-12-01 21:22:06 +01:00
Koen Kanters 445890a071 fix(ignore): update zigbee-herdsman-converters to 25.80.0 (#29924) 2025-12-01 20:17:02 +00:00
Nerivec 3dce8548ce fix(ignore): bump zigbee2mqtt-windfront to 2.4.2 (#29919) 2025-12-01 16:02:29 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4a020fd8de fix(ignore): bump zigbee2mqtt-windfront from 2.4.0 to 2.4.1 in the minor-patch group (#29905)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-30 15:36:04 +00:00
Koen Kanters d411fc5c69 fix(ignore): update zigbee-herdsman-converters to 25.79.0 (#29903) 2025-11-30 14:06:44 +00:00
burmistrzakandKoen Kanters 631c4f6a57 fix: Home Assistant: expose current_humidity for climate devices exposing humidity (#29842)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-29 13:32:28 +01:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
c02ed09211 fix(ignore): bump the minor-patch group with 2 updates (#29859)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-11-27 20:12:22 +00:00
Koen Kanters 49b4876768 fix(ignore): update zigbee-herdsman-converters to 25.78.0 (#29856) 2025-11-27 19:16:34 +00:00
Koen Kanters 802271ed37 fix(ignore): update zigbee-herdsman to 7.0.1 (#29855) 2025-11-27 19:00:08 +00:00
Nerivec 42fc6e03a7 fix: Reporting payload detection fixes (#29854) 2025-11-27 19:46:31 +01:00
Koen Kanters 35d8a5da7f fix(ignore): update zigbee-herdsman-converters to 25.77.0 (#29838) 2025-11-26 20:00:02 +00:00
Koen Kanters 72d862f4a0 fix(ignore): update zigbee-herdsman-converters to 25.76.0 (#29823) 2025-11-25 19:57:30 +00:00
Nerivec ad0696856a fix(ignore): prop name convention on new API endpoints (#29820) 2025-11-25 20:18:14 +01:00
NerivecandKoen Kanters a61646d584 feat: Add action bridge/request API (#29730)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-24 21:57:48 +01:00
Koen Kanters 854df498bd fix(ignore): update zigbee-herdsman-converters to 25.75.0 (#29811) 2025-11-24 20:54:10 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
e2232e9e91 fix(ignore): bump the minor-patch group with 3 updates (#29804)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-24 21:33:29 +01:00
dependabot[bot] 6775c4f17b chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 21:31:41 +01:00
Nerivec 17520f7cfb fix(ignore): update zigbee2mqtt-windfront to 2.3.1 (#29788) 2025-11-23 22:27:04 +00:00
Koen Kanters 1707bcab88 chore: merge master to dev 2025-11-23 22:05:07 +01:00
Koen Kanters c577c8e5f0 chore: lint issueBot 2025-11-23 22:04:48 +01:00
Koen Kanters 7b8efbe2c4 chore: merge master to dev 2025-11-23 21:59:31 +01:00
Koen Kanters 459b695f60 chore: issueBot: add logging and search in title 2025-11-23 21:59:13 +01:00
NerivecandKoen Kanters f26ade4f93 feat: Add new bind/reporting/map features (#29750)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-23 21:50:11 +01:00
Koenkk 3e2aefbcf8 fix(ignore): update zigbee-herdsman to 7.0.0 2025-11-23 21:50:11 +01:00
Koen Kanters ba77ce7c8d fix(ignore): update zigbee-herdsman-converters to 25.74.0 (#29770) 2025-11-22 20:47:44 +00:00
Alex Freidel 4c21b66079 fix: Add port 8080 EXPOSE to container (#29754) 2025-11-22 13:24:24 +01:00
Koen Kanters be774a4d0d fix(ignore): update zigbee-herdsman-converters to 25.73.0 (#29752) 2025-11-21 19:48:11 +00:00
Koen Kanters 7b7b988960 fix(ignore): update zigbee-herdsman-converters to 25.72.0 (#29735) 2025-11-20 21:00:08 +00:00
Koen Kanters d6bf5174be fix(ignore): update zigbee-herdsman-converters to 25.71.0 (#29716) 2025-11-19 20:57:49 +00:00
Koen Kanters 73225e2436 fix(ignore): update zigbee-herdsman-converters to 25.70.0 (#29684) 2025-11-17 21:08:22 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
afedab9680 chore(deps): bump js-yaml from 4.1.0 to 4.1.1 (#29675)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-11-17 21:58:42 +01:00
Koen Kanters 53b7eaabf0 fix(ignore): update zigbee-herdsman to 6.4.2 (#29683) 2025-11-17 20:49:33 +00:00
Koen Kantersandburmistrzak 16d0e56968 fix(ignore): update zigbee-herdsman-converters to 25.69.0 (#29659)
Co-authored-by: burmistrzak <61958704+burmistrzak@users.noreply.github.com>
2025-11-17 20:11:05 +00:00
dependabot[bot] aea41b5c82 chore(deps): bump js-yaml from 4.1.0 to 4.1.1 (#29649) 2025-11-16 15:28:44 +01:00
Koen Kanters 564e9ab9eb fix(ignore): update zigbee-herdsman-converters to 25.68.0 (#29606) 2025-11-12 19:51:37 +00:00
Koen Kanters 61c6ec6111 fix(ignore): update zigbee-herdsman to 6.4.1 (#29595) 2025-11-11 21:09:25 +00:00
Koen Kanters 4794b3c81f fix(ignore): update zigbee-herdsman-converters to 25.67.0 (#29572) 2025-11-10 17:16:16 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 517b286583 fix(ignore): bump @biomejs/biome from 2.3.3 to 2.3.4 in the minor-patch group (#29571)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 18:09:42 +01:00
Louis Sautier aee173d10a chore: update build command in README (#29562) 2025-11-10 18:03:18 +01:00
Koen Kanters 940fff244c fix(ignore): update zigbee-herdsman-converters to 25.66.0 (#29558) 2025-11-09 20:50:48 +00:00
Koen Kanters 8822aedb5e fix(ignore): update zigbee-herdsman-converters to 25.65.0 (#29523) 2025-11-07 20:42:22 +00:00
Koen Kanters e788c520a9 fix(ignore): update zigbee-herdsman to 6.4.0 (#29522) 2025-11-07 20:37:54 +00:00
Koen Kanters 39c5bc8c04 fix(ignore): update zigbee-herdsman to 6.3.3 (#29500) 2025-11-06 21:53:56 +00:00
Koen Kanters 5c4e43fd10 fix(ignore): update zigbee-herdsman-converters to 25.64.0 (#29499) 2025-11-06 19:23:23 +00:00
Koen Kanters 5a6a9cf4d2 fix(ignore): update zigbee-herdsman-converters to 25.63.0 (#29482) 2025-11-05 20:18:35 +00:00
Koen Kanters ad0e91e61e chore: merge master to dev 2025-11-04 21:22:30 +01:00
Koen Kanters 5797b56871 chore: refactor issue bot 2025-11-04 21:22:09 +01:00
Koen Kanters ea1c5a8f4e fix(ignore): update zigbee-herdsman-converters to 25.62.0 (#29449) 2025-11-04 20:08:45 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8099322cc2 fix(ignore): bump debounce from 2.2.0 to 3.0.0 (#29444)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 22:18:45 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 619b939b14 fix(ignore): bump the minor-patch group with 2 updates (#29443)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 22:18:32 +01:00
Koen Kanters eeed56ef2c fix(ignore): update zigbee-herdsman-converters to 25.60.0 (#29426) 2025-11-02 20:13:40 +00:00
github-actions[bot] e9b4a6221b chore: promote to dev 2025-11-01 21:04:00 +00:00
32 changed files with 1281 additions and 561 deletions
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+38
View File
@@ -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")
+1 -1
View File
@@ -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')
});
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.6.3"
".": "2.7.0"
}
+50
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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",
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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))) {
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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;
-3
View File
@@ -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 {
+2 -2
View File
@@ -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}`);
});
});
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
}
+110 -122
View File
@@ -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: {}
+149
View File
@@ -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
View File
@@ -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 = {
+80
View File
@@ -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
View File
@@ -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"}),
{},
);
});
});
+2 -2
View File
@@ -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,
+118 -55
View File
@@ -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,
+44 -39
View File
@@ -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");
+19 -10
View File
@@ -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) => ({
+2 -2
View File
@@ -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];
};