├── .github ├── ISSUE_TEMPLATE │ └── config.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release-please.yml │ ├── stale.yml │ └── typedoc.yaml ├── .gitignore ├── .release-please-manifest.json ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── biome.json ├── examples └── join-and-log.js ├── package.json ├── pnpm-lock.yaml ├── release-please-config.json ├── src ├── adapter │ ├── adapter.ts │ ├── adapterDiscovery.ts │ ├── const.ts │ ├── deconz │ │ ├── adapter │ │ │ └── deconzAdapter.ts │ │ ├── driver │ │ │ ├── constants.ts │ │ │ ├── driver.ts │ │ │ ├── frame.ts │ │ │ ├── frameParser.ts │ │ │ ├── parser.ts │ │ │ └── writer.ts │ │ └── types.d.ts │ ├── ember │ │ ├── adapter │ │ │ ├── emberAdapter.ts │ │ │ ├── endpoints.ts │ │ │ ├── oneWaitress.ts │ │ │ └── tokensManager.ts │ │ ├── consts.ts │ │ ├── enums.ts │ │ ├── ezsp │ │ │ ├── buffalo.ts │ │ │ ├── consts.ts │ │ │ ├── enums.ts │ │ │ └── ezsp.ts │ │ ├── ezspError.ts │ │ ├── types.ts │ │ ├── uart │ │ │ ├── ash.ts │ │ │ ├── consts.ts │ │ │ ├── enums.ts │ │ │ ├── parser.ts │ │ │ ├── queues.ts │ │ │ └── writer.ts │ │ └── utils │ │ │ ├── initters.ts │ │ │ └── math.ts │ ├── events.ts │ ├── ezsp │ │ ├── adapter │ │ │ ├── backup.ts │ │ │ └── ezspAdapter.ts │ │ └── driver │ │ │ ├── commands.ts │ │ │ ├── consts.ts │ │ │ ├── driver.ts │ │ │ ├── ezsp.ts │ │ │ ├── frame.ts │ │ │ ├── index.ts │ │ │ ├── multicast.ts │ │ │ ├── parser.ts │ │ │ ├── types │ │ │ ├── basic.ts │ │ │ ├── index.ts │ │ │ ├── named.ts │ │ │ └── struct.ts │ │ │ ├── uart.ts │ │ │ ├── utils │ │ │ ├── crc16ccitt.ts │ │ │ └── index.ts │ │ │ └── writer.ts │ ├── index.ts │ ├── serialPort.ts │ ├── socketPortUtils.ts │ ├── tstype.ts │ ├── z-stack │ │ ├── adapter │ │ │ ├── adapter-backup.ts │ │ │ ├── adapter-nv-memory.ts │ │ │ ├── endpoints.ts │ │ │ ├── manager.ts │ │ │ ├── tstype.ts │ │ │ └── zStackAdapter.ts │ │ ├── constants │ │ │ ├── af.ts │ │ │ ├── common.ts │ │ │ ├── dbg.ts │ │ │ ├── index.ts │ │ │ ├── mac.ts │ │ │ ├── sapi.ts │ │ │ ├── sys.ts │ │ │ ├── util.ts │ │ │ ├── utils.ts │ │ │ └── zdo.ts │ │ ├── models │ │ │ └── startup-options.ts │ │ ├── structs │ │ │ ├── entries │ │ │ │ ├── address-manager-entry.ts │ │ │ │ ├── address-manager-table.ts │ │ │ │ ├── aps-link-key-data-entry.ts │ │ │ │ ├── aps-link-key-data-table.ts │ │ │ │ ├── aps-tc-link-key-entry.ts │ │ │ │ ├── aps-tc-link-key-table.ts │ │ │ │ ├── channel-list.ts │ │ │ │ ├── has-configured.ts │ │ │ │ ├── index.ts │ │ │ │ ├── nib.ts │ │ │ │ ├── nwk-key-descriptor.ts │ │ │ │ ├── nwk-key.ts │ │ │ │ ├── nwk-pan-id.ts │ │ │ │ ├── nwk-sec-material-descriptor-entry.ts │ │ │ │ ├── nwk-sec-material-descriptor-table.ts │ │ │ │ ├── security-manager-entry.ts │ │ │ │ └── security-manager-table.ts │ │ │ ├── index.ts │ │ │ ├── serializable-memory-object.ts │ │ │ ├── struct.ts │ │ │ └── table.ts │ │ ├── unpi │ │ │ ├── constants.ts │ │ │ ├── frame.ts │ │ │ ├── index.ts │ │ │ ├── parser.ts │ │ │ └── writer.ts │ │ ├── utils │ │ │ ├── channel-list.ts │ │ │ ├── index.ts │ │ │ └── network-options.ts │ │ └── znp │ │ │ ├── buffaloZnp.ts │ │ │ ├── definition.ts │ │ │ ├── index.ts │ │ │ ├── parameterType.ts │ │ │ ├── tstype.ts │ │ │ ├── utils.ts │ │ │ ├── znp.ts │ │ │ └── zpiObject.ts │ ├── zboss │ │ ├── adapter │ │ │ └── zbossAdapter.ts │ │ ├── commands.ts │ │ ├── consts.ts │ │ ├── driver.ts │ │ ├── enums.ts │ │ ├── frame.ts │ │ ├── reader.ts │ │ ├── types.ts │ │ ├── uart.ts │ │ ├── utils.ts │ │ └── writer.ts │ ├── zigate │ │ ├── adapter │ │ │ ├── patchZdoBuffaloBE.ts │ │ │ └── zigateAdapter.ts │ │ └── driver │ │ │ ├── LICENSE │ │ │ ├── buffaloZiGate.ts │ │ │ ├── commandType.ts │ │ │ ├── constants.ts │ │ │ ├── frame.ts │ │ │ ├── messageType.ts │ │ │ ├── parameterType.ts │ │ │ ├── ziGateObject.ts │ │ │ └── zigate.ts │ └── zoh │ │ └── adapter │ │ ├── utils.ts │ │ └── zohAdapter.ts ├── buffalo │ ├── buffalo.ts │ └── index.ts ├── controller │ ├── controller.ts │ ├── database.ts │ ├── events.ts │ ├── greenPower.ts │ ├── helpers │ │ ├── index.ts │ │ ├── request.ts │ │ ├── requestQueue.ts │ │ ├── zclFrameConverter.ts │ │ └── zclTransactionSequenceNumber.ts │ ├── index.ts │ ├── model │ │ ├── device.ts │ │ ├── endpoint.ts │ │ ├── entity.ts │ │ ├── group.ts │ │ └── index.ts │ ├── touchlink.ts │ └── tstype.ts ├── index.ts ├── models │ ├── backup-storage-legacy.ts │ ├── backup-storage-unified.ts │ ├── backup.ts │ ├── index.ts │ └── network-options.ts ├── utils │ ├── backup.ts │ ├── index.ts │ ├── logger.ts │ ├── patchBigIntSerialization.ts │ ├── queue.ts │ ├── types.d.ts │ ├── utils.ts │ ├── wait.ts │ └── waitress.ts └── zspec │ ├── consts.ts │ ├── enums.ts │ ├── index.ts │ ├── tstypes.ts │ ├── utils.ts │ ├── zcl │ ├── buffaloZcl.ts │ ├── definition │ │ ├── cluster.ts │ │ ├── consts.ts │ │ ├── enums.ts │ │ ├── foundation.ts │ │ ├── manufacturerCode.ts │ │ ├── status.ts │ │ └── tstype.ts │ ├── index.ts │ ├── utils.ts │ ├── zclFrame.ts │ ├── zclHeader.ts │ └── zclStatusError.ts │ └── zdo │ ├── buffaloZdo.ts │ ├── definition │ ├── clusters.ts │ ├── consts.ts │ ├── enums.ts │ ├── status.ts │ └── tstypes.ts │ ├── index.ts │ ├── utils.ts │ └── zdoStatusError.ts ├── test ├── adapter │ ├── adapter.test.ts │ ├── ember │ │ ├── ash.test.ts │ │ ├── consts.ts │ │ ├── emberAdapter.test.ts │ │ ├── ezsp.test.ts │ │ ├── ezspBuffalo.test.ts │ │ ├── ezspError.test.ts │ │ └── math.test.ts │ ├── ezsp │ │ ├── frame.test.ts │ │ └── uart.test.ts │ ├── z-stack │ │ ├── adapter.test.ts │ │ ├── constants.test.ts │ │ ├── structs.test.ts │ │ ├── unpi.test.ts │ │ └── znp.test.ts │ ├── zboss │ │ └── fixZdoResponse.test.ts │ ├── zigate │ │ ├── patchZdoBuffaloBE.test.ts │ │ └── zdo.test.ts │ └── zoh │ │ ├── utils.test.ts │ │ └── zohAdapter.test.ts ├── buffalo.test.ts ├── controller.test.ts ├── greenpower.test.ts ├── mockAdapters.ts ├── mockDevices.ts ├── testUtils.ts ├── tsconfig.json ├── utils.test.ts ├── utils │ └── math.ts ├── vitest.config.mts ├── zcl.test.ts └── zspec │ ├── utils.test.ts │ ├── zcl │ ├── buffalo.test.ts │ ├── frame.test.ts │ └── utils.test.ts │ └── zdo │ ├── buffalo.test.ts │ └── utils.test.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Zigbee2MQTT issue tracker 4 | url: https://github.com/Koenkk/zigbee2mqtt/issues/new/choose 5 | about: Preferably create an issue in the Zigbee2MQTT issue tracker. Only click on "Open a blank issue" below if you are conviced it really belongs in this issue tracker. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | groups: 8 | minor-patch: 9 | applies-to: version-updates 10 | update-types: 11 | - minor 12 | - patch 13 | commit-message: 14 | prefix: fix(ignore) 15 | - package-ecosystem: github-actions 16 | directory: / 17 | schedule: 18 | interval: weekly -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | jobs: 10 | ci: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 24 20 | registry-url: https://registry.npmjs.org/ 21 | cache: pnpm 22 | - name: Install dependencies 23 | run: pnpm i --frozen-lockfile 24 | - name: Check 25 | run: pnpm run check 26 | - name: Build 27 | run: pnpm run build 28 | - name: Test 29 | run: pnpm run test:coverage -- --testTimeout=10000 30 | - name: Publish new release 31 | if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' 32 | run: | 33 | pnpm publish --no-git-checks 34 | PACKAGE=$(node -p "require('./package.json').name") 35 | VERSION=$(node -p "require('./package.json').version") 36 | until [ $(pnpm view $PACKAGE --json | jq --arg version "$VERSION" -r '.versions[] | select (. == $version)') ]; 37 | do 38 | echo "Waiting for publish to complete" 39 | sleep 5s 40 | done 41 | curl -XPOST -H "Authorization: token ${{ secrets.GH_TOKEN }}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/koenkk/zigbee2mqtt/dispatches --data "{\"event_type\": \"update_dep\", \"client_payload\": { \"version\": \"$VERSION\", \"package\": \"$PACKAGE\"}}" 42 | env: 43 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN }} 44 | NPM_CONFIG_PROVENANCE: true 45 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: googleapis/release-please-action@v4 17 | with: 18 | token: ${{ secrets.GH_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/stale@v9 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' 16 | stale-pr-message: 'This pull request is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' 17 | days-before-stale: 60 18 | days-before-close: 7 19 | exempt-issue-labels: dont-stale 20 | operations-per-run: 500 21 | -------------------------------------------------------------------------------- /.github/workflows/typedoc.yaml: -------------------------------------------------------------------------------- 1 | name: Publish typedoc on Github Pages 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 24 20 | registry-url: https://registry.npmjs.org/ 21 | cache: pnpm 22 | 23 | - run: pnpm install --frozen-lockfile 24 | - run: pnpm add -g typedoc 25 | 26 | - name: Generate docs 27 | run: typedoc --gitRevision "$(git describe --tag --abbrev=0)" --tsconfig tsconfig.json --excludePrivate --excludeProtected --excludeExternals --entryPointStrategy expand ./src --sourceLinkTemplate "https://github.com/Koenkk/zigbee-herdsman/blob/{gitRevision}/{path}#L{line}" -out typedoc 28 | 29 | - uses: actions/upload-pages-artifact@v3 30 | with: 31 | name: github-pages 32 | # typedoc "out" path 33 | path: ./typedoc 34 | 35 | deploy: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | permissions: 39 | pages: write # to deploy to Pages 40 | id-token: write # to verify the deployment originates from an appropriate source 41 | environment: 42 | name: github-pages 43 | url: ${{ steps.deployment.outputs.page_url }} 44 | steps: 45 | - name: Deploy to GitHub Pages 46 | id: deployment 47 | uses: actions/deploy-pages@v4 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | .idea 5 | temp/ 6 | typedoc/ 7 | .remote-sync.json 8 | tsconfig.tsbuildinfo 9 | 10 | # MacOS indexing files 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "4.1.0" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome", "vitest.explorer"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "notebook.defaultFormatter": "biomejs.biome", 4 | "editor.tabSize": 4, 5 | "editor.insertSpaces": true, 6 | "files.defaultLanguage": "typescript", 7 | "files.eol": "\n", 8 | "[typescript]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jack Wu , Simen Li , Hedy Wang and Koen Kanters 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zigbee-herdsman 2 | 3 | ![npm](https://img.shields.io/npm/v/zigbee-herdsman) 4 | 5 | zigbee-herdsman is an open source Zigbee gateway solution with Node.js JavaScript runtime back-end. 6 | 7 | It was originally forked from zigbee-shepherd with the goal to refactor it to improve maintainability. 8 | 9 | # API Documentation 10 | 11 | For automatically generated API reference documentation, see: https://koenkk.github.io/zigbee-herdsman. 12 | 13 | # Changelog 14 | 15 | ## 0.14.0 breaking changes 16 | 17 | - `sendWhenActive` has been replaced with `sendWhen: 'active'` 18 | 19 | ## 0.13.0 breaking changes 20 | 21 | - `controller.touchlinkFactoryReset` has been renamed to `controller.touchlinkFactoryResetFirst()` 22 | 23 | ## 0.12.0 breaking changes 24 | 25 | - options.network.extenedPanID -> options.network.extendedPanID (typo fix) 26 | 27 | ## 0.11.0 breaking changes 28 | 29 | - endpoint.bind[].cluster will now return cluster object instead of cluster number 30 | 31 | ## 0.10.0 breaking changes 32 | 33 | - controller.start() renamed `resetted` start result to `reset` 34 | 35 | ## 0.9.0 breaking changes 36 | 37 | - Removed controller.softReset() -> use controller.reset('soft') now 38 | - Removed group.get('groupID') -> use group.groupID now 39 | 40 | ## 0.8.0 breaking changes 41 | 42 | - Removed device.getEndpoints() -> use device.endpoints now 43 | - Removed device/endpoint.set() -> directly set properties now (e.g. device.modelID = 'newmodelid') 44 | - Removed device/endpoint.get() -> directly get properties now (e.g. device.modelID) 45 | - Removed group.getMembers() -> use group.members now 46 | - Removed endpoint.deviceIeeeAddress -> use endpoint.getDevice().ieeeAddr 47 | 48 | # Related projects 49 | 50 | ## Zigbee2MQTT 51 | 52 | [Zigbee2MQTT](https://github.com/Koenkk/zigbee2mqtt) is a Zigbee to MQTT gateway. It bridges events and allows you to control your Zigbee devices via MQTT. Allows you to use your Zigbee devices without the vendors or propritary and closed sources bridges or gateways. Zigbee2MQTT also keeps track of the state of the system and the capabilities of connected devices. It uses zigbee-herdsman and [zigbee-herdsman-converters](https://github.com/Koenkk/zigbee-herdsman-converters) as modules to handle low-level core Zigbee communication. 53 | 54 | ## ioBroker 55 | 56 | [ioBroker](https://github.com/ioBroker) is an home automation integration platform that is focused on Building Automation, Smart Metering, Ambient Assisted Living, Process Automation, Visualization and Data Logging. It uses zigbee-herdsman for its Zigbee integration. 57 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "formatter": { 9 | "indentStyle": "space", 10 | "indentWidth": 4, 11 | "lineWidth": 150, 12 | "bracketSpacing": false 13 | }, 14 | "files": { 15 | "ignore": ["package.json"] 16 | }, 17 | "linter": { 18 | "ignore": ["src/adapter/ezsp/driver/types/*"], 19 | "rules": { 20 | "correctness": { 21 | "noUnusedImports": "error", 22 | "noUnusedVariables": { 23 | "level": "warn", 24 | "fix": "none" 25 | } 26 | }, 27 | "style": { 28 | "noParameterAssign": "off", 29 | "useThrowNewError": "error", 30 | "useThrowOnlyError": "error", 31 | "useNamingConvention": { 32 | "level": "error", 33 | "options": { 34 | "strictCase": false, 35 | "conventions": [ 36 | { 37 | "selector": { 38 | "kind": "objectLiteralProperty" 39 | }, 40 | "formats": ["snake_case", "camelCase", "CONSTANT_CASE", "PascalCase"] 41 | }, 42 | { 43 | "selector": { 44 | "kind": "const" 45 | }, 46 | "formats": ["snake_case", "camelCase", "CONSTANT_CASE", "PascalCase"] 47 | }, 48 | { 49 | "selector": { 50 | "kind": "typeProperty" 51 | }, 52 | "formats": ["snake_case", "camelCase", "CONSTANT_CASE", "PascalCase"] 53 | }, 54 | { 55 | "selector": { 56 | "kind": "enumMember" 57 | }, 58 | "formats": ["CONSTANT_CASE", "PascalCase"] 59 | } 60 | ] 61 | } 62 | } 63 | }, 64 | "performance": { 65 | "noDelete": "off" 66 | }, 67 | "suspicious": { 68 | "noConstEnum": "off", 69 | "useAwait": "error" 70 | } 71 | } 72 | }, 73 | "overrides": [ 74 | { 75 | "include": ["src/adapter/ezsp/driver/types/*", "test/**"], 76 | "linter": { 77 | "rules": { 78 | "style": { 79 | "noNonNullAssertion": "off", 80 | "useNamingConvention": "off" 81 | }, 82 | "suspicious": { 83 | "noImplicitAnyLet": "off" 84 | } 85 | } 86 | } 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /examples/join-and-log.js: -------------------------------------------------------------------------------- 1 | const {Controller} = require("zigbee-herdsman"); 2 | 3 | const SERIAL = "/dev/ttyACM0"; 4 | const DB = "./devices.db"; 5 | 6 | const coordinator = new Controller({ 7 | serialPort: {path: SERIAL}, 8 | databasePath: DB, 9 | }); 10 | 11 | coordinator.on("message", (msg) => { 12 | console.log(msg); 13 | }); 14 | 15 | coordinator 16 | .start() 17 | .then(() => { 18 | console.log("started with device", SERIAL); 19 | return coordinator.permitJoin(true, null, 600); 20 | }) 21 | .catch((err) => { 22 | console.error(err); 23 | process.exit(1); 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Koen Kanters", 4 | "email": "koenkanters94@gmail.com" 5 | }, 6 | "bugs": { 7 | "url": "https://github.com/koenkk/zigbee-herdsman/issues" 8 | }, 9 | "packageManager": "pnpm@10.8.0", 10 | "contributors": [ 11 | { 12 | "name": "Koen Kanters", 13 | "email": "koenkanters94@gmail.com" 14 | }, 15 | { 16 | "name": "Hedy Wang", 17 | "email": "hedywings@gmail.com" 18 | }, 19 | { 20 | "name": "Simen Li", 21 | "email": "simenkid@gmail.com" 22 | }, 23 | { 24 | "name": "Jack Wu", 25 | "email": "jackchased@gmail.com" 26 | } 27 | ], 28 | "dependencies": { 29 | "@serialport/bindings-cpp": "^13.0.1", 30 | "@serialport/parser-delimiter": "^13.0.0", 31 | "@serialport/stream": "^13.0.0", 32 | "bonjour-service": "^1.3.0", 33 | "debounce": "^2.2.0", 34 | "fast-deep-equal": "^3.1.3", 35 | "mixin-deep": "^2.0.1", 36 | "slip": "^1.0.2", 37 | "zigbee-on-host": "^0.1.12" 38 | }, 39 | "deprecated": false, 40 | "description": "An open source ZigBee gateway solution with node.js.", 41 | "devDependencies": { 42 | "@biomejs/biome": "^1.9.4", 43 | "@serialport/binding-mock": "^10.2.2", 44 | "@types/debounce": "^1.2.4", 45 | "@types/node": "^22.14.1", 46 | "@vitest/coverage-v8": "^3.1.1", 47 | "rimraf": "^6.0.1", 48 | "typescript": "^5.8.3", 49 | "vitest": "^3.1.1" 50 | }, 51 | "homepage": "https://github.com/Koenkk/zigbee-herdsman", 52 | "keywords": [ 53 | "zigbee", 54 | "zstack", 55 | "emberznet", 56 | "deconz", 57 | "zigate" 58 | ], 59 | "license": "MIT", 60 | "main": "dist/index.js", 61 | "types": "dist/index.d.ts", 62 | "name": "zigbee-herdsman", 63 | "repository": { 64 | "type": "git", 65 | "url": "git+https://github.com/Koenkk/zigbee-herdsman.git" 66 | }, 67 | "scripts": { 68 | "build": "tsc", 69 | "build:watch": "tsc -w", 70 | "test": "vitest run --config ./test/vitest.config.mts", 71 | "test:coverage": "vitest run --config ./test/vitest.config.mts --coverage", 72 | "test:watch": "vitest watch --config ./test/vitest.config.mts", 73 | "check": "biome check", 74 | "check:w": "biome check --write", 75 | "clean": "rimraf temp coverage dist tsconfig.tsbuildinfo", 76 | "prepack": "pnpm run clean && pnpm run build" 77 | }, 78 | "version": "4.1.0", 79 | "pnpm": { 80 | "onlyBuiltDependencies": [ 81 | "@biomejs/biome", 82 | "@serialport/bindings-cpp", 83 | "esbuild" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "node", 5 | "include-component-in-tag": false 6 | } 7 | }, 8 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 9 | } 10 | -------------------------------------------------------------------------------- /src/adapter/const.ts: -------------------------------------------------------------------------------- 1 | import * as Zcl from "../zspec/zcl"; 2 | 3 | /** 4 | * Workaround for devices that require a specific manufacturer code to be reported by coordinator while interviewing... 5 | * - Lumi/Aqara devices do not work properly otherwise (missing features): https://github.com/Koenkk/zigbee2mqtt/issues/9274 6 | */ 7 | export const WORKAROUND_JOIN_MANUF_IEEE_PREFIX_TO_CODE: {[ieeePrefix: string]: Zcl.ManufacturerCode} = { 8 | // NOTE: Lumi has a new prefix registered since 2021, in case they start using that one with new devices, it might need to be added here too... 9 | // "0x18c23c" https://maclookup.app/vendors/lumi-united-technology-co-ltd 10 | "0x54ef44": Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, 11 | "0x04cf8c": Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, 12 | }; 13 | -------------------------------------------------------------------------------- /src/adapter/deconz/driver/constants.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import type {GenericZdoResponse} from "../../../zspec/zdo/definition/tstypes"; 4 | 5 | const PARAM = { 6 | Network: { 7 | NET_OFFLINE: 0x00, 8 | NET_JOINING: 0x01, 9 | NET_CONNECTED: 0x02, 10 | NET_LEAVING: 0x03, 11 | MAC: 0x01, 12 | PAN_ID: 0x05, 13 | NWK_ADDRESS: 0x07, 14 | EXT_PAN_ID: 0x08, 15 | CHANNEL_MASK: 0x0a, 16 | APS_EXT_PAN_ID: 0x0b, 17 | NETWORK_KEY: 0x18, 18 | LINK_KEY: 0x19, 19 | CHANNEL: 0x1c, 20 | PERMIT_JOIN: 0x21, 21 | WATCHDOG_TTL: 0x26, 22 | }, 23 | STK: { 24 | Endpoint: 0x13, 25 | }, 26 | FrameType: { 27 | ReadDeviceState: 0x07, 28 | ReadParameter: 0x0a, 29 | WriteParameter: 0x0b, 30 | ReadFirmwareVersion: 0x0d, 31 | DeviceStateChanged: 0x0e, 32 | GreenPowerDataInd: 0x19, 33 | }, 34 | APS: { 35 | DATA_CONFIRM: 0x04, 36 | DATA_REQUEST: 0x12, 37 | DATA_INDICATION: 0x17, 38 | }, 39 | NetworkState: { 40 | NET_OFFLINE: 0x00, 41 | NET_JOINING: 0x01, 42 | NET_CONNECTED: 0x02, 43 | NET_LEAVING: 0x03, 44 | CHANGE_NETWORK_STATE: 0x08, 45 | }, 46 | addressMode: { 47 | GROUP_ADDR: 0x01, 48 | NWK_ADDR: 0x02, 49 | IEEE_ADDR: 0x03, 50 | }, 51 | txRadius: { 52 | DEFAULT_RADIUS: 30, 53 | UNLIMITED: 0, 54 | }, 55 | }; 56 | 57 | interface Request { 58 | commandId: number; 59 | networkState?: number; 60 | parameterId?: number; 61 | parameter?: ParameterT; 62 | request?: ApsDataRequest; 63 | seqNumber: number; 64 | // biome-ignore lint/suspicious/noExplicitAny: API 65 | resolve: (value: any) => void; 66 | reject: (value: Error) => void; 67 | ts?: number; 68 | } 69 | 70 | interface WaitForDataRequest { 71 | addr: number | string; 72 | profileId: number; 73 | clusterId: number; 74 | transactionSequenceNumber?: number; 75 | resolve: (value: ReceivedDataResponse | PromiseLike) => void; 76 | reject: (value: Error) => void; 77 | ts?: number; 78 | timeout?: number; 79 | } 80 | 81 | interface ReceivedDataResponse { 82 | commandId: number; 83 | seqNr: number; 84 | status: number; 85 | frameLength: number; 86 | payloadLength: number; 87 | deviceState: number; 88 | destAddrMode: number; 89 | destAddr16?: number; 90 | destAddr64?: string; 91 | destEndpoint: number; 92 | srcAddrMode: number; 93 | srcAddr16?: number; 94 | srcAddr64?: string; 95 | srcEndpoint: number; 96 | profileId: number; 97 | clusterId: number; 98 | asduLength: number; 99 | asduPayload: Buffer; 100 | lqi: number; 101 | rssi: number; 102 | zdo?: GenericZdoResponse; 103 | } 104 | 105 | interface GpDataInd { 106 | rspId: number; 107 | seqNr: number; 108 | id: number; 109 | options: number; 110 | srcId: number; 111 | frameCounter: number; 112 | commandId: number; 113 | commandFrameSize: number; 114 | commandFrame: Buffer; 115 | } 116 | 117 | interface DataStateResponse { 118 | commandId: number; 119 | seqNr: number; 120 | status: number; 121 | frameLength: number; 122 | payloadLength: number; 123 | deviceState: number; 124 | requestId: number; 125 | destAddrMode: number; 126 | destAddr16?: number; 127 | destAddr64?: string; 128 | destEndpoint?: number; 129 | srcEndpoint: number; 130 | confirmStatus: number; 131 | } 132 | 133 | interface ApsDataRequest { 134 | requestId: number; 135 | destAddrMode: number; 136 | destAddr16?: number; 137 | destAddr64?: string; //number[]; 138 | destEndpoint?: number; 139 | profileId: number; 140 | clusterId: number; 141 | srcEndpoint: number; 142 | asduLength: number; 143 | asduPayload: Buffer; 144 | txOptions: number; 145 | radius: number; 146 | timeout?: number; // seconds 147 | } 148 | 149 | type ParamMac = string; 150 | type ParamPanId = number; 151 | type ParamExtPanId = string; 152 | type ParamNwkAddr = number; 153 | type ParamChannel = number; 154 | type ParamChannelMask = number; 155 | type ParamPermitJoin = number; 156 | type ParamNetworkKey = string; 157 | 158 | type Command = ParamMac | ParamPanId | ParamNwkAddr | ParamExtPanId | ParamChannel | ParamChannelMask | ParamPermitJoin | ParamNetworkKey; 159 | type ParameterT = number | number[]; 160 | 161 | export type { 162 | Request, 163 | WaitForDataRequest, 164 | ApsDataRequest, 165 | ReceivedDataResponse, 166 | DataStateResponse, 167 | ParameterT, 168 | Command, 169 | ParamMac, 170 | ParamPanId, 171 | ParamNwkAddr, 172 | ParamExtPanId, 173 | ParamChannel, 174 | ParamChannelMask, 175 | ParamPermitJoin, 176 | ParamNetworkKey, 177 | GpDataInd, 178 | }; 179 | 180 | export default {PARAM}; 181 | -------------------------------------------------------------------------------- /src/adapter/deconz/driver/frame.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | class Frame { 3 | public toBuffer(): Buffer { 4 | return Buffer.alloc(0); 5 | } 6 | public static fromBuffer(_buffer: Buffer): Frame { 7 | return new Frame(); 8 | } 9 | } 10 | 11 | export default Frame; 12 | -------------------------------------------------------------------------------- /src/adapter/deconz/driver/parser.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {Transform, type TransformCallback} from "node:stream"; 4 | 5 | import slip from "slip"; 6 | 7 | import {logger} from "../../../utils/logger"; 8 | 9 | const NS = "zh:deconz:driver:parser"; 10 | 11 | class Parser extends Transform { 12 | private decoder: slip.Decoder; 13 | 14 | public constructor() { 15 | super(); 16 | 17 | this.onMessage = this.onMessage.bind(this); 18 | this.onError = this.onError.bind(this); 19 | 20 | this.decoder = new slip.Decoder({ 21 | onMessage: this.onMessage, 22 | onError: this.onError, 23 | maxMessageSize: 1000000, 24 | bufferSize: 2048, 25 | }); 26 | } 27 | 28 | private onMessage(message: Uint8Array): void { 29 | //logger.debug(`message received: ${message}`, NS); 30 | this.emit("parsed", message); 31 | } 32 | 33 | private onError(_: Uint8Array, error: string): void { 34 | logger.debug(`<-- error '${error}'`, NS); 35 | } 36 | 37 | public override _transform(chunk: Buffer, _: string, cb: TransformCallback): void { 38 | //logger.debug(`<-- [${[...chunk]}]`, NS); 39 | this.decoder.decode(chunk); 40 | //logger.debug(`<-- [${[...chunk]}]`, NS); 41 | cb(); 42 | } 43 | } 44 | 45 | export default Parser; 46 | -------------------------------------------------------------------------------- /src/adapter/deconz/driver/writer.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import * as stream from "node:stream"; 4 | 5 | import slip from "slip"; 6 | 7 | import {logger} from "../../../utils/logger"; 8 | import type Frame from "./frame"; 9 | 10 | const NS = "zh:deconz:driver:writer"; 11 | 12 | class Writer extends stream.Readable { 13 | public writeFrame(frame: Frame): void { 14 | const buffer = slip.encode(frame.toBuffer()); 15 | logger.debug(`--> frame [${[...buffer]}]`, NS); 16 | this.push(buffer); 17 | } 18 | 19 | public override _read(): void {} 20 | } 21 | 22 | export default Writer; 23 | -------------------------------------------------------------------------------- /src/adapter/deconz/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "slip" { 2 | export function encode(data: ArrayLike | Buffer, options?: object): Uint8Array; 3 | export class Decoder { 4 | constructor(options: { 5 | maxMessageSize: number; 6 | bufferSize: number; 7 | onMessage: (msg: Uint8Array) => void; 8 | onError: (msgBuffer: Uint8Array, errorMsg: string) => void; 9 | }); 10 | 11 | decode(data: ArrayLike | Buffer): Uint8Array; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/adapter/ember/adapter/endpoints.ts: -------------------------------------------------------------------------------- 1 | import {GP_ENDPOINT, GP_PROFILE_ID, HA_PROFILE_ID} from "../../../zspec/consts"; 2 | import {Clusters} from "../../../zspec/zcl/definition/cluster"; 3 | import type {ClusterId, EmberMulticastId, ProfileId} from "../types"; 4 | 5 | type FixedEndpointInfo = { 6 | /** Actual Zigbee endpoint number. uint8_t */ 7 | endpoint: number; 8 | /** Profile ID of the device on this endpoint. */ 9 | profileId: ProfileId; 10 | /** Device ID of the device on this endpoint. uint16_t*/ 11 | deviceId: number; 12 | /** Version of the device. uint8_t */ 13 | deviceVersion: number; 14 | /** List of server clusters. */ 15 | inClusterList: readonly ClusterId[]; 16 | /** List of client clusters. */ 17 | outClusterList: readonly ClusterId[]; 18 | /** Network index for this endpoint. uint8_t */ 19 | networkIndex: number; 20 | /** Multicast group IDs to register in the multicast table */ 21 | multicastIds: readonly EmberMulticastId[]; 22 | }; 23 | 24 | /** 25 | * List of endpoints to register. 26 | * 27 | * Index 0 is used as default and expected to be the primary network. 28 | */ 29 | export const FIXED_ENDPOINTS: readonly FixedEndpointInfo[] = [ 30 | { 31 | // primary network 32 | endpoint: 1, 33 | profileId: HA_PROFILE_ID, 34 | deviceId: 0x65, // ? 35 | deviceVersion: 1, 36 | inClusterList: [ 37 | Clusters.genBasic.ID, // 0x0000,// Basic 38 | Clusters.genIdentify.ID, // 0x0003,// Identify 39 | Clusters.genOnOff.ID, // 0x0006,// On/off 40 | Clusters.genLevelCtrl.ID, // 0x0008,// Level Control 41 | Clusters.genTime.ID, // 0x000A,// Time 42 | Clusters.genOta.ID, // 0x0019,// Over the Air Bootloading 43 | // Cluster.genPowerProfile.ID,// 0x001A,// Power Profile XXX: missing ZCL cluster def in Z2M? 44 | Clusters.lightingColorCtrl.ID, // 0x0300,// Color Control 45 | ], 46 | outClusterList: [ 47 | Clusters.genBasic.ID, // 0x0000,// Basic 48 | Clusters.genIdentify.ID, // 0x0003,// Identify 49 | Clusters.genGroups.ID, // 0x0004,// Groups 50 | Clusters.genScenes.ID, // 0x0005,// Scenes 51 | Clusters.genOnOff.ID, // 0x0006,// On/off 52 | Clusters.genLevelCtrl.ID, // 0x0008,// Level Control 53 | Clusters.genPollCtrl.ID, // 0x0020,// Poll Control 54 | Clusters.lightingColorCtrl.ID, // 0x0300,// Color Control 55 | Clusters.msIlluminanceMeasurement.ID, // 0x0400,// Illuminance Measurement 56 | Clusters.msTemperatureMeasurement.ID, // 0x0402,// Temperature Measurement 57 | Clusters.msRelativeHumidity.ID, // 0x0405,// Relative Humidity Measurement 58 | Clusters.msOccupancySensing.ID, // 0x0406,// Occupancy Sensing 59 | Clusters.ssIasZone.ID, // 0x0500,// IAS Zone 60 | Clusters.seMetering.ID, // 0x0702,// Simple Metering 61 | Clusters.haMeterIdentification.ID, // 0x0B01,// Meter Identification 62 | Clusters.haApplianceStatistics.ID, // 0x0B03,// Appliance Statistics 63 | Clusters.haElectricalMeasurement.ID, // 0x0B04,// Electrical Measurement 64 | Clusters.touchlink.ID, // 0x1000, // touchlink 65 | ], 66 | networkIndex: 0x00, 67 | // - Cluster spec 3.7.2.4.1: group identifier 0x0000 is reserved for the global scene used by the OnOff cluster. 68 | // - 901: defaultBindGroup 69 | multicastIds: [0, 901], 70 | }, 71 | { 72 | // green power 73 | endpoint: GP_ENDPOINT, 74 | profileId: GP_PROFILE_ID, 75 | deviceId: 0x66, 76 | deviceVersion: 1, 77 | inClusterList: [ 78 | Clusters.greenPower.ID, // 0x0021,// Green Power 79 | ], 80 | outClusterList: [ 81 | Clusters.greenPower.ID, // 0x0021,// Green Power 82 | ], 83 | networkIndex: 0x00, 84 | multicastIds: [0x0b84], 85 | }, 86 | ]; 87 | -------------------------------------------------------------------------------- /src/adapter/ember/ezspError.ts: -------------------------------------------------------------------------------- 1 | import {EzspStatus} from "./enums"; 2 | 3 | export class EzspError extends Error { 4 | public code: EzspStatus; 5 | 6 | constructor(code: EzspStatus) { 7 | super(EzspStatus[code]); 8 | this.code = code; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/adapter/ember/uart/consts.ts: -------------------------------------------------------------------------------- 1 | import {EZSP_MAX_FRAME_LENGTH, EZSP_MIN_FRAME_LENGTH} from "../ezsp/consts"; 2 | 3 | /** 4 | * Define the size of the receive buffer pool on the EZSP host. 5 | * 6 | * The number of receive buffers does not need to be greater than the number of packet buffers available on the NCP, 7 | * because this in turn is the maximum number of callbacks that could be received between commands. 8 | * In reality a value of 20 is a generous allocation. 9 | */ 10 | export const EZSP_HOST_RX_POOL_SIZE = 32; 11 | /** 12 | * The number of transmit buffers must be set to the number of receive buffers 13 | * -- to hold the immediate ACKs sent for each callabck frame received -- 14 | * plus 3 buffers for the retransmit queue and one each for an automatic ACK 15 | * (due to data flow control) and a command. 16 | */ 17 | export const TX_POOL_BUFFERS = EZSP_HOST_RX_POOL_SIZE + 5; 18 | 19 | /** protocol version */ 20 | export const ASH_VERSION = 2; 21 | 22 | /** 23 | * Timeouts before link is judged down. 24 | * 25 | * Consecutive ACK timeouts (minus 1) needed to enter the ERROR state. 26 | * 27 | * Is 3 in ash-ncp.h 28 | */ 29 | export const ASH_MAX_TIMEOUTS = 6; 30 | /** max time in msecs for ncp to wake */ 31 | export const ASH_MAX_WAKE_TIME = 150; 32 | 33 | /** 34 | * Define the units used by the Not Ready timer as 2**n msecs 35 | * log2 of msecs per NR timer unit 36 | */ 37 | export const ASH_NR_TIMER_BIT = 4; 38 | 39 | /** Control byte mask for DATA frame */ 40 | export const ASH_DFRAME_MASK = 0x80; 41 | /** Control byte mask for short frames (ACK/NAK) */ 42 | export const ASH_SHFRAME_MASK = 0xe0; 43 | 44 | /** Acknowledge frame number */ 45 | export const ASH_ACKNUM_MASK = 0x07; 46 | export const ASH_ACKNUM_BIT = 0; 47 | /** Retransmitted frame flag */ 48 | export const ASH_RFLAG_MASK = 0x08; 49 | export const ASH_RFLAG_BIT = 3; 50 | /** Receiver not ready flag */ 51 | export const ASH_NFLAG_MASK = 0x08; 52 | export const ASH_NFLAG_BIT = 3; 53 | /** Reserved for future use */ 54 | export const ASH_PFLAG_MASK = 0x10; 55 | export const ASH_PFLAG_BIT = 4; 56 | /** DATA frame number */ 57 | export const ASH_FRMNUM_MASK = 0x70; 58 | export const ASH_FRMNUM_BIT = 4; 59 | 60 | /** 61 | * The wake byte special function applies only when in between frames, 62 | * so it does not need to be escaped within a frame. 63 | * (also means NCP data pending) 64 | */ 65 | export const ASH_WAKE = 0xff; /*!< */ 66 | 67 | /** Constant used in byte-stuffing (XOR mask used in byte stuffing) */ 68 | export const ASH_FLIP = 0x20; 69 | 70 | // Field and frame lengths, excluding flag byte and any byte stuffing overhead 71 | // All limits are inclusive 72 | export const ASH_MIN_DATA_FIELD_LEN = EZSP_MIN_FRAME_LENGTH; 73 | export const ASH_MAX_DATA_FIELD_LEN = EZSP_MAX_FRAME_LENGTH; 74 | /** with control */ 75 | export const ASH_MIN_DATA_FRAME_LEN = ASH_MIN_DATA_FIELD_LEN + 1; 76 | /** control plus data field, but not CRC */ 77 | export const ASH_MIN_FRAME_LEN = 1; 78 | export const ASH_MAX_FRAME_LEN = ASH_MAX_DATA_FIELD_LEN + 1; 79 | export const ASH_CRC_LEN = 2; 80 | export const ASH_MIN_FRAME_WITH_CRC_LEN = ASH_MIN_FRAME_LEN + ASH_CRC_LEN; 81 | export const ASH_MAX_FRAME_WITH_CRC_LEN = ASH_MAX_FRAME_LEN + ASH_CRC_LEN; 82 | 83 | // Lengths for each frame type: includes control and data field (if any), excludes the CRC and flag bytes 84 | /** ash frame len data min */ 85 | export const ASH_FRAME_LEN_DATA_MIN = ASH_MIN_DATA_FIELD_LEN + 1; 86 | /** [control] */ 87 | export const ASH_FRAME_LEN_ACK = 1; 88 | /** [control] */ 89 | export const ASH_FRAME_LEN_NAK = 1; 90 | /** [control] */ 91 | export const ASH_FRAME_LEN_RST = 1; 92 | /** [control, version, reset reason] */ 93 | export const ASH_FRAME_LEN_RSTACK = 3; 94 | /** [control, version, error] */ 95 | export const ASH_FRAME_LEN_ERROR = 3; 96 | 97 | // Define lengths of short frames - includes control byte and data field 98 | /** longest non-data frame sent */ 99 | export const SH_TX_BUFFER_LEN = 2; 100 | /** longest non-data frame received */ 101 | export const SH_RX_BUFFER_LEN = 3; 102 | 103 | // Define constants for the LFSR in randomizeBuffer() 104 | /** polynomial */ 105 | export const LFSR_POLY = 0xb8; 106 | /** initial value (seed) */ 107 | export const LFSR_SEED = 0x42; 108 | 109 | export const VALID_BAUDRATES = [600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800]; 110 | -------------------------------------------------------------------------------- /src/adapter/ember/uart/parser.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {Transform, type TransformCallback, type TransformOptions} from "node:stream"; 4 | 5 | // import {logger} from '../../../utils/logger'; 6 | import {AshReservedByte} from "./enums"; 7 | 8 | // const NS = 'zh:ember:uart:ash:parser'; 9 | 10 | export class AshParser extends Transform { 11 | private buffer: Buffer; 12 | 13 | public constructor(opts?: TransformOptions) { 14 | super(opts); 15 | 16 | this.buffer = Buffer.alloc(0); 17 | } 18 | 19 | override _transform(chunk: Buffer, _encoding: BufferEncoding, cb: TransformCallback): void { 20 | let data = Buffer.concat([this.buffer, chunk]); 21 | let position: number; 22 | 23 | // biome-ignore lint/suspicious/noAssignInExpressions: shorter 24 | while ((position = data.indexOf(AshReservedByte.FLAG)) !== -1) { 25 | // emit the frame via 'data' event 26 | const frame = data.subarray(0, position + 1); 27 | 28 | // expensive and very verbose, enable locally only if necessary 29 | // logger.debug(`<<<< [FRAME raw=${frame.toString('hex')}]`, NS); 30 | this.push(frame); 31 | 32 | // remove the frame from internal buffer (set below) 33 | data = data.subarray(position + 1); 34 | } 35 | 36 | this.buffer = data; 37 | 38 | cb(); 39 | } 40 | 41 | override _flush(cb: TransformCallback): void { 42 | this.push(this.buffer); 43 | 44 | this.buffer = Buffer.alloc(0); 45 | 46 | cb(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/adapter/ember/uart/writer.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {Readable, type ReadableOptions} from "node:stream"; 4 | 5 | // import {logger} from '../../../utils/logger'; 6 | 7 | // const NS = 'zh:ember:uart:ash:writer'; 8 | 9 | export class AshWriter extends Readable { 10 | private bytesToWrite: number[]; 11 | 12 | constructor(opts?: ReadableOptions) { 13 | super(opts); 14 | 15 | this.bytesToWrite = []; 16 | } 17 | 18 | private writeBytes(): void { 19 | const buffer = Buffer.from(this.bytesToWrite); 20 | this.bytesToWrite = []; 21 | 22 | // expensive and very verbose, enable locally only if necessary 23 | // logger.debug(`>>>> [FRAME raw=${buffer.toString('hex')}]`, NS); 24 | 25 | // this.push(buffer); 26 | this.emit("data", buffer); 27 | } 28 | 29 | public writeByte(byte: number): void { 30 | this.bytesToWrite.push(byte); 31 | } 32 | 33 | public writeAvailable(): boolean { 34 | if (this.readableLength < this.readableHighWaterMark) { 35 | return true; 36 | } 37 | 38 | this.writeFlush(); 39 | 40 | return false; 41 | } 42 | 43 | /** 44 | * If there is anything to send, send to the port. 45 | */ 46 | public writeFlush(): void { 47 | if (this.bytesToWrite.length) { 48 | this.writeBytes(); 49 | } 50 | } 51 | 52 | public override _read(): void {} 53 | } 54 | -------------------------------------------------------------------------------- /src/adapter/ember/utils/initters.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import * as ZSpec from "../../../zspec"; 4 | import type {NetworkCache} from "../adapter/emberAdapter"; 5 | import {INVALID_RADIO_CHANNEL, ZB_PSA_ALG} from "../consts"; 6 | import {EmberJoinMethod, SecManDerivedKeyType, SecManFlag, SecManKeyType} from "../enums"; 7 | import {EMBER_AES_HASH_BLOCK_SIZE} from "../ezsp/consts"; 8 | import type {EmberAesMmoHashContext, SecManContext} from "../types"; 9 | 10 | /** 11 | * Initialize a network cache index with proper "invalid" values. 12 | * @returns 13 | */ 14 | export const initNetworkCache = (): NetworkCache => { 15 | return { 16 | eui64: ZSpec.BLANK_EUI64, 17 | parameters: { 18 | extendedPanId: ZSpec.BLANK_EXTENDED_PAN_ID.slice(), // copy 19 | panId: ZSpec.INVALID_PAN_ID, 20 | radioTxPower: 0, 21 | radioChannel: INVALID_RADIO_CHANNEL, 22 | joinMethod: EmberJoinMethod.MAC_ASSOCIATION, 23 | nwkManagerId: ZSpec.NULL_NODE_ID, 24 | nwkUpdateId: 0, 25 | channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, 26 | }, 27 | }; 28 | }; 29 | 30 | /** 31 | * This routine will initialize a Security Manager context correctly for use in subsequent function calls. 32 | * @returns 33 | */ 34 | export const initSecurityManagerContext = (): SecManContext => { 35 | return { 36 | coreKeyType: SecManKeyType.NONE, 37 | keyIndex: 0, 38 | derivedType: SecManDerivedKeyType.NONE, 39 | eui64: "0x0000000000000000", 40 | multiNetworkIndex: 0, 41 | flags: SecManFlag.NONE, 42 | psaKeyAlgPermission: ZB_PSA_ALG, // unused for classic key storage 43 | }; 44 | }; 45 | 46 | /** 47 | * This routine clears the passed context so that a new hash calculation 48 | * can be performed. 49 | * 50 | * @returns context A pointer to the location of hash context to clear. 51 | */ 52 | export const aesMmoHashInit = (): EmberAesMmoHashContext => { 53 | // MEMSET(context, 0, sizeof(EmberAesMmoHashContext)); 54 | return { 55 | result: Buffer.alloc(EMBER_AES_HASH_BLOCK_SIZE), // uint8_t[EMBER_AES_HASH_BLOCK_SIZE] 56 | length: 0x00000000, // uint32_t 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/adapter/ember/utils/math.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | //-------------------------------------------------------------- 3 | // Define macros for handling 3-bit frame numbers modulo 8 4 | 5 | /** mask to frame number modulus */ 6 | export const mod8 = (n: number): number => n & 7; 7 | /** increment in frame number modulus */ 8 | export const inc8 = (n: number): number => mod8(n + 1); 9 | /** Return true if n is within the range lo through hi, computed (mod 8) */ 10 | export const withinRange = (lo: number, n: number, hi: number): boolean => mod8(n - lo) <= mod8(hi - lo); 11 | 12 | //-------------------------------------------------------------- 13 | // CRC 14 | 15 | /** 16 | * Calculates 16-bit cyclic redundancy code (CITT CRC 16). 17 | * 18 | * Applies the standard CITT CRC 16 polynomial to a 19 | * single byte. It should support being called first with an initial 20 | * value, then repeatedly until all data is processed. 21 | * 22 | * @param newByte The new byte to be run through CRC. 23 | * @param prevResult The previous CRC result. 24 | * @returns The new CRC result. 25 | */ 26 | export const halCommonCrc16 = (newByte: number, prevResult: number): number => { 27 | /* 28 | * 16bit CRC notes: 29 | * "CRC-CCITT" 30 | * poly is g(X) = X^16 + X^12 + X^5 + 1 (0x1021) 31 | * used in the FPGA (green boards and 15.4) 32 | * initial remainder should be 0xFFFF 33 | */ 34 | prevResult = ((prevResult >> 8) & 0xffff) | ((prevResult << 8) & 0xffff); 35 | prevResult ^= newByte; 36 | prevResult ^= (prevResult & 0xff) >> 4; 37 | prevResult ^= (((prevResult << 8) & 0xffff) << 4) & 0xffff; 38 | 39 | prevResult ^= (((prevResult & 0xff) << 5) & 0xff) | (((((prevResult & 0xff) >> 3) & 0xffff) << 8) & 0xffff); 40 | 41 | return prevResult; 42 | }; 43 | 44 | //-------------------------------------------------------------- 45 | // Byte manipulation 46 | 47 | /** Returns the low bits of the 8-bit value 'n' as uint8_t. */ 48 | export const lowBits = (n: number): number => n & 0xf; 49 | /** Returns the high bits of the 8-bit value 'n' as uint8_t. */ 50 | export const highBits = (n: number): number => lowBits(n >> 4) & 0xf; 51 | /** Returns the low byte of the 16-bit value 'n' as uint8_t. */ 52 | export const lowByte = (n: number): number => n & 0xff; 53 | /** Returns the high byte of the 16-bit value 'n' as uint8_t. */ 54 | export const highByte = (n: number): number => lowByte(n >> 8) & 0xff; 55 | /** Returns the value built from the two uint8_t values high and low. */ 56 | export const highLowToInt = (high: number, low: number): number => ((high & 0xffff) << 8) + (low & 0xffff & 0xff); 57 | /** Useful to reference a single bit of a byte. */ 58 | export const bit = (x: number): number => 1 << x; 59 | /** Useful to reference a single bit of an uint32_t type. */ 60 | export const bit32 = (x: number): number => 1 << x; 61 | /** Returns both the low and high bytes (in that order) of the same 16-bit value 'n' as uint8_t. */ 62 | export const lowHighBytes = (n: number): [number, highByte: number] => [lowByte(n), highByte(n)]; 63 | /** Returns both the low and high bits (in that order) of the same 8-bit value 'n' as uint8_t. */ 64 | export const lowHighBits = (n: number): [number, highBits: number] => [lowBits(n), highBits(n)]; 65 | 66 | /** 67 | * Get byte as an 8-bit string (`n` assumed of proper range). 68 | * @param n 69 | * @returns 70 | */ 71 | export const byteToBits = (n: number): string => { 72 | return (n >>> 0).toString(2).padStart(8, "0"); 73 | }; 74 | -------------------------------------------------------------------------------- /src/adapter/events.ts: -------------------------------------------------------------------------------- 1 | import type {Header as ZclHeader} from "../zspec/zcl"; 2 | 3 | export type DeviceJoinedPayload = { 4 | networkAddress: number; 5 | ieeeAddr: string; 6 | }; 7 | 8 | export type DeviceLeavePayload = {networkAddress?: number; ieeeAddr: string} | {networkAddress: number; ieeeAddr?: string}; 9 | 10 | export interface ZclPayload { 11 | clusterID: number; 12 | address: number | string; 13 | header: ZclHeader | undefined; 14 | // This buffer contains the whole Zcl.Frame (including the ZclHeader) 15 | data: Buffer; 16 | endpoint: number; 17 | linkquality: number; 18 | groupID: number; 19 | wasBroadcast: boolean; 20 | destinationEndpoint: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/adapter/ezsp/adapter/backup.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import * as fs from "node:fs"; 4 | 5 | import type * as Models from "../../../models"; 6 | import {BackupUtils} from "../../../utils"; 7 | import {logger} from "../../../utils/logger"; 8 | import {uint32MaskToChannels} from "../../../zspec/utils"; 9 | import type {Driver} from "../driver"; 10 | import { 11 | type EmberKeyData, 12 | type EmberKeyStruct, 13 | EmberKeyType, 14 | type EmberNetworkParameters, 15 | type EmberSecurityManagerNetworkKeyInfo, 16 | } from "../driver/types"; 17 | 18 | const NS = "zh:ezsp:backup"; 19 | 20 | export class EZSPAdapterBackup { 21 | private driver: Driver; 22 | private defaultPath: string; 23 | 24 | public constructor(driver: Driver, path: string) { 25 | this.driver = driver; 26 | this.defaultPath = path; 27 | } 28 | 29 | public async createBackup(): Promise { 30 | logger.debug("creating backup", NS); 31 | const version: number = await this.driver.ezsp.version(); 32 | const linkResult = await this.driver.getKey(EmberKeyType.TRUST_CENTER_LINK_KEY); 33 | const netParams = await this.driver.ezsp.execCommand("getNetworkParameters"); 34 | const networkParams: EmberNetworkParameters = netParams.parameters; 35 | const netResult = await this.driver.getKey(EmberKeyType.CURRENT_NETWORK_KEY); 36 | let tclKey: Buffer; 37 | let netKey: Buffer; 38 | let netKeySequenceNumber = 0; 39 | let netKeyFrameCounter = 0; 40 | 41 | if (version < 13) { 42 | tclKey = Buffer.from((linkResult.keyStruct as EmberKeyStruct).key.contents); 43 | netKey = Buffer.from((netResult.keyStruct as EmberKeyStruct).key.contents); 44 | netKeySequenceNumber = (netResult.keyStruct as EmberKeyStruct).sequenceNumber; 45 | netKeyFrameCounter = (netResult.keyStruct as EmberKeyStruct).outgoingFrameCounter; 46 | } else { 47 | tclKey = Buffer.from((linkResult.keyData as EmberKeyData).contents); 48 | netKey = Buffer.from((netResult.keyData as EmberKeyData).contents); 49 | // get rest of info from second cmd in EZSP 13+ 50 | const netKeyInfoResult = await this.driver.getNetworkKeyInfo(); 51 | const networkKeyInfo: EmberSecurityManagerNetworkKeyInfo = netKeyInfoResult.networkKeyInfo; 52 | netKeySequenceNumber = networkKeyInfo.networkKeySequenceNumber; 53 | netKeyFrameCounter = networkKeyInfo.networkKeyFrameCounter; 54 | } 55 | 56 | const ieee = (await this.driver.ezsp.execCommand("getEui64")).eui64; 57 | /* return backup structure */ 58 | return { 59 | ezsp: { 60 | version: version, 61 | hashed_tclk: tclKey, 62 | }, 63 | networkOptions: { 64 | panId: networkParams.panId, 65 | extendedPanId: Buffer.from(networkParams.extendedPanId), 66 | channelList: uint32MaskToChannels(networkParams.channels), 67 | networkKey: netKey, 68 | networkKeyDistribute: true, 69 | }, 70 | logicalChannel: networkParams.radioChannel, 71 | networkKeyInfo: { 72 | sequenceNumber: netKeySequenceNumber, 73 | frameCounter: netKeyFrameCounter, 74 | }, 75 | securityLevel: 5, 76 | networkUpdateId: networkParams.nwkUpdateId, 77 | coordinatorIeeeAddress: ieee, 78 | devices: [], 79 | }; 80 | } 81 | 82 | /** 83 | * Loads currently stored backup and returns it in internal backup model. 84 | */ 85 | public getStoredBackup(): Models.Backup | undefined { 86 | try { 87 | fs.accessSync(this.defaultPath); 88 | } catch { 89 | return undefined; 90 | } 91 | let data: Models.UnifiedBackupStorage; 92 | try { 93 | data = JSON.parse(fs.readFileSync(this.defaultPath).toString()); 94 | } catch (error) { 95 | throw new Error(`Coordinator backup is corrupted (${(error as Error).stack})`); 96 | } 97 | if (data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) { 98 | if (data.metadata?.version !== 1) { 99 | throw new Error(`Unsupported open coordinator backup version (version=${data.metadata?.version})`); 100 | } 101 | if (!data.metadata.internal?.ezspVersion) { 102 | throw new Error("This open coordinator backup format not for EZSP adapter"); 103 | } 104 | return BackupUtils.fromUnifiedBackup(data); 105 | } 106 | 107 | throw new Error("Unknown backup format"); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/consts.ts: -------------------------------------------------------------------------------- 1 | export const FLAG = 0x7e; // Marks end of frame 2 | export const ESCAPE = 0x7d; // Indicates that the following byte is escaped 3 | export const CANCEL = 0x1a; // Terminates a frame in progress 4 | export const XON = 0x11; // Resume transmission 5 | export const XOFF = 0x13; // Stop transmission 6 | export const SUBSTITUTE = 0x18; // Replaces a byte received with a low-level communication error 7 | export const STUFF = 0x20; 8 | export const RANDOMIZE_START = 0x42; 9 | export const RANDOMIZE_SEQ = 0xb8; 10 | 11 | export const RESERVED = [FLAG, ESCAPE, XON, XOFF, SUBSTITUTE, CANCEL]; 12 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/frame.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {RANDOMIZE_SEQ, RANDOMIZE_START} from "./consts"; 4 | import crc16ccitt from "./utils/crc16ccitt"; 5 | 6 | export enum FrameType { 7 | UNKNOWN = 0, 8 | ERROR = 1, 9 | DATA = 2, 10 | ACK = 3, 11 | NAK = 4, 12 | RST = 5, 13 | RSTACK = 6, 14 | } 15 | 16 | /** 17 | * Basic class to handle uart-level frames 18 | * https://www.silabs.com/documents/public/user-guides/ug101-uart-gateway-protocol-reference.pdf 19 | */ 20 | export class Frame { 21 | /** 22 | * Type of the Frame as determined by its control byte. 23 | */ 24 | public readonly type: FrameType; 25 | public readonly buffer: Buffer; 26 | 27 | public constructor(buffer: Buffer) { 28 | this.buffer = buffer; 29 | 30 | const ctrlByte = this.buffer[0]; 31 | 32 | if ((ctrlByte & 0x80) === 0) { 33 | this.type = FrameType.DATA; 34 | } else if ((ctrlByte & 0xe0) === 0x80) { 35 | this.type = FrameType.ACK; 36 | } else if ((ctrlByte & 0xe0) === 0xa0) { 37 | this.type = FrameType.NAK; 38 | } else if (ctrlByte === 0xc0) { 39 | this.type = FrameType.RST; 40 | } else if (ctrlByte === 0xc1) { 41 | this.type = FrameType.RSTACK; 42 | } else if (ctrlByte === 0xc2) { 43 | this.type = FrameType.ERROR; 44 | } else { 45 | this.type = FrameType.UNKNOWN; 46 | } 47 | } 48 | 49 | get control(): number { 50 | return this.buffer[0]; 51 | } 52 | 53 | public static fromBuffer(buffer: Buffer): Frame { 54 | return new Frame(buffer); 55 | } 56 | 57 | /** 58 | * XOR s with a pseudo-random sequence for transmission. 59 | * Used only in data frames. 60 | */ 61 | public static makeRandomizedBuffer(buffer: Buffer): Buffer { 62 | let rand = RANDOMIZE_START; 63 | const out = Buffer.alloc(buffer.length); 64 | let outIdx = 0; 65 | 66 | for (const c of buffer) { 67 | out.writeUInt8(c ^ rand, outIdx++); 68 | 69 | if (rand % 2) { 70 | rand = (rand >> 1) ^ RANDOMIZE_SEQ; 71 | } else { 72 | rand = rand >> 1; 73 | } 74 | } 75 | 76 | return out; 77 | } 78 | 79 | /** 80 | * Throws on CRC error. 81 | */ 82 | public checkCRC(): void { 83 | const crc = crc16ccitt(this.buffer.subarray(0, -3), 65535); 84 | const crcArr = Buffer.from([crc >> 8, crc % 256]); 85 | const subArr = this.buffer.subarray(-3, -1); 86 | 87 | if (!subArr.equals(crcArr)) { 88 | throw new Error(`<-- CRC error: ${this.toString()}|${subArr.toString("hex")}|${crcArr.toString("hex")}`); 89 | } 90 | } 91 | 92 | /** 93 | * 94 | * @returns Buffer to hex string 95 | */ 96 | public toString(): string { 97 | return this.buffer.toString("hex"); 98 | } 99 | } 100 | 101 | export default Frame; 102 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/index.ts: -------------------------------------------------------------------------------- 1 | import {Driver, EmberIncomingMessage} from "./driver"; 2 | import {Ezsp} from "./ezsp"; 3 | 4 | export {Ezsp, Driver, EmberIncomingMessage}; 5 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/multicast.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {logger} from "../../../utils/logger"; 4 | import type {Driver} from "./driver"; 5 | import {EzspConfigId} from "./types"; 6 | import {EmberStatus} from "./types/named"; 7 | import {EmberMulticastTableEntry} from "./types/struct"; 8 | 9 | const NS = "zh:ezsp:cast"; 10 | 11 | export class Multicast { 12 | tableSize = 16; 13 | private driver: Driver; 14 | private _multicast: Record; 15 | private _available: number[]; 16 | 17 | constructor(driver: Driver) { 18 | this.driver = driver; 19 | this._multicast = {}; 20 | this._available = []; 21 | } 22 | 23 | private async _initialize(): Promise { 24 | const size = await this.driver.ezsp.getConfigurationValue(EzspConfigId.CONFIG_MULTICAST_TABLE_SIZE); 25 | for (let i = 0; i < size; i++) { 26 | const entry = await this.driver.ezsp.getMulticastTableEntry(i); 27 | logger.debug(`MulticastTableEntry[${i}] = ${entry}`, NS); 28 | if (entry.endpoint !== 0) { 29 | this._multicast[entry.multicastId] = [entry, i]; 30 | } else { 31 | this._available.push(i); 32 | } 33 | } 34 | } 35 | 36 | async startup(enpoints: {id: number; member_of: number[]}[]): Promise { 37 | await this._initialize(); 38 | for (const ep of enpoints) { 39 | if (!ep.id) continue; 40 | for (const group_id of ep.member_of) { 41 | await this.subscribe(group_id, ep.id); 42 | } 43 | } 44 | } 45 | 46 | public async subscribe(groupId: number, endpoint: number): Promise { 47 | if (this._multicast[groupId] !== undefined) { 48 | logger.debug(`${groupId} is already subscribed`, NS); 49 | return EmberStatus.SUCCESS; 50 | } 51 | 52 | try { 53 | const idx = this._available.pop(); 54 | 55 | if (idx === undefined) { 56 | throw new Error("No available"); 57 | } 58 | 59 | const entry: EmberMulticastTableEntry = new EmberMulticastTableEntry(); 60 | entry.endpoint = endpoint; 61 | entry.multicastId = groupId; 62 | entry.networkIndex = 0; 63 | const status = await this.driver.ezsp.setMulticastTableEntry(idx, entry); 64 | if (status !== EmberStatus.SUCCESS) { 65 | logger.error(`Set MulticastTableEntry #${idx} for ${entry.multicastId} multicast id: ${status}`, NS); 66 | this._available.push(idx); 67 | return status; 68 | } 69 | 70 | this._multicast[entry.multicastId] = [entry, idx]; 71 | logger.debug(`Set MulticastTableEntry #${idx} for ${entry.multicastId} multicast id: ${status}`, NS); 72 | return status; 73 | } catch (error) { 74 | logger.error(`No more available slots MulticastId subscription (${(error as Error).stack})`, NS); 75 | return EmberStatus.INDEX_OUT_OF_RANGE; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/parser.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import * as stream from "node:stream"; 4 | 5 | import {logger} from "../../../utils/logger"; 6 | import * as consts from "./consts"; 7 | import Frame from "./frame"; 8 | 9 | const NS = "zh:ezsp:uart"; 10 | 11 | export class Parser extends stream.Transform { 12 | private tail: Buffer[]; 13 | 14 | public constructor() { 15 | super(); 16 | 17 | this.tail = []; 18 | } 19 | 20 | public override _transform(chunk: Buffer, _: string, cb: () => void): void { 21 | if (chunk.indexOf(consts.CANCEL) >= 0) { 22 | this.reset(); 23 | chunk = chunk.subarray(chunk.lastIndexOf(consts.CANCEL) + 1); 24 | } 25 | 26 | if (chunk.indexOf(consts.SUBSTITUTE) >= 0) { 27 | this.reset(); 28 | chunk = chunk.subarray(chunk.indexOf(consts.FLAG) + 1); 29 | } 30 | 31 | logger.debug(`<-- [${chunk.toString("hex")}]`, NS); 32 | 33 | let delimiterPlace = chunk.indexOf(consts.FLAG); 34 | 35 | while (delimiterPlace >= 0) { 36 | const buffer = chunk.subarray(0, delimiterPlace + 1); 37 | const frameBuffer = Buffer.from([...this.unstuff(Buffer.concat([...this.tail, buffer]))]); 38 | this.reset(); 39 | 40 | try { 41 | const frame = Frame.fromBuffer(frameBuffer); 42 | 43 | if (frame) { 44 | this.emit("parsed", frame); 45 | } 46 | } catch (error) { 47 | logger.debug(`<-- error ${error}`, NS); 48 | } 49 | 50 | chunk = chunk.subarray(delimiterPlace + 1); 51 | delimiterPlace = chunk.indexOf(consts.FLAG); 52 | } 53 | 54 | this.tail.push(chunk); 55 | cb(); 56 | } 57 | 58 | private *unstuff(buffer: Buffer): Generator { 59 | /* Unstuff (unescape) a buffer after receipt */ 60 | let escaped = false; 61 | for (const byte of buffer) { 62 | if (escaped) { 63 | yield byte ^ consts.STUFF; 64 | escaped = false; 65 | } else { 66 | if (byte === consts.ESCAPE) { 67 | escaped = true; 68 | } else if (byte === consts.XOFF || byte === consts.XON) { 69 | // skip 70 | } else { 71 | yield byte; 72 | } 73 | } 74 | } 75 | } 76 | 77 | public reset(): void { 78 | // clear tail 79 | this.tail.length = 0; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/utils/crc16ccitt.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import type {Buffer} from "node:buffer"; 4 | 5 | type CalcFn = (buf: Buffer | number[], previous: number) => number; 6 | 7 | function defineCrc(model: string, calc: CalcFn): CalcFn { 8 | const fn = (buf: Buffer | number[], previous: number): number => calc(buf, previous) >>> 0; 9 | fn.signed = calc; 10 | fn.unsigned = fn; 11 | fn.model = model; 12 | 13 | return fn; 14 | } 15 | 16 | // Generated by `./pycrc.py --algorithm=table-driven --model=ccitt --generate=c` 17 | const TABLE: number[] = [ 18 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 19 | 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 20 | 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 21 | 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 22 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 23 | 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 24 | 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 25 | 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 26 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 27 | 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 28 | 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 29 | 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 30 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 31 | 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 32 | 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 33 | ]; 34 | 35 | const crc16ccitt = defineCrc("ccitt", (buf: Buffer | number[], previous: number): number => { 36 | let crc = ~~previous; 37 | for (const byte of buf) { 38 | crc = (TABLE[((crc >> 8) ^ byte) & 0xff] ^ (crc << 8)) & 0xffff; 39 | } 40 | 41 | return crc; 42 | }); 43 | 44 | export default crc16ccitt; 45 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {randomBytes} from "node:crypto"; 4 | 5 | import {EmberEUI64, EmberInitialSecurityBitmask} from "../types/named"; 6 | import {EmberInitialSecurityState, EmberKeyData} from "../types/struct"; 7 | import crc16ccitt from "./crc16ccitt"; 8 | 9 | if (!Symbol.asyncIterator) { 10 | // biome-ignore lint/suspicious/noExplicitAny: API 11 | (Symbol).asyncIterator = Symbol.for("Symbol.asyncIterator"); 12 | } 13 | 14 | function emberSecurity(networkKey: Buffer): EmberInitialSecurityState { 15 | const isc: EmberInitialSecurityState = new EmberInitialSecurityState(); 16 | isc.bitmask = 17 | EmberInitialSecurityBitmask.HAVE_PRECONFIGURED_KEY | 18 | EmberInitialSecurityBitmask.TRUST_CENTER_GLOBAL_LINK_KEY | 19 | EmberInitialSecurityBitmask.HAVE_NETWORK_KEY | 20 | //EmberInitialSecurityBitmask.PRECONFIGURED_NETWORK_KEY_MODE | 21 | EmberInitialSecurityBitmask.REQUIRE_ENCRYPTED_KEY | 22 | EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY; 23 | isc.preconfiguredKey = new EmberKeyData(); 24 | isc.preconfiguredKey.contents = randomBytes(16); 25 | isc.networkKey = new EmberKeyData(); 26 | isc.networkKey.contents = networkKey; 27 | isc.networkKeySequenceNumber = 0; 28 | isc.preconfiguredTrustCenterEui64 = new EmberEUI64([0, 0, 0, 0, 0, 0, 0, 0]); 29 | return isc; 30 | } 31 | 32 | export {crc16ccitt, emberSecurity}; 33 | -------------------------------------------------------------------------------- /src/adapter/ezsp/driver/writer.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import * as stream from "node:stream"; 4 | 5 | import {logger} from "../../../utils/logger"; 6 | import * as consts from "./consts"; 7 | import {crc16ccitt} from "./utils"; 8 | 9 | const NS = "zh:ezsp:uart"; 10 | 11 | export class Writer extends stream.Readable { 12 | public writeBuffer(buffer: Buffer): void { 13 | logger.debug(`--> [${buffer.toString("hex")}]`, NS); 14 | this.push(buffer); 15 | } 16 | 17 | public override _read(): void {} 18 | 19 | public sendACK(ackNum: number): void { 20 | /* Construct a acknowledgement frame */ 21 | const ackFrame = this.makeFrame(0b10000000 | ackNum); 22 | this.writeBuffer(ackFrame); 23 | } 24 | 25 | public sendNAK(ackNum: number): void { 26 | /* Construct a negative acknowledgement frame */ 27 | const nakFrame = this.makeFrame(0b10100000 | ackNum); 28 | this.writeBuffer(nakFrame); 29 | } 30 | 31 | public sendReset(): void { 32 | /* Construct a reset frame */ 33 | const rstFrame = Buffer.concat([Buffer.from([consts.CANCEL]), this.makeFrame(0xc0)]); 34 | this.writeBuffer(rstFrame); 35 | } 36 | 37 | public sendData(data: Buffer, seq: number, rxmit: number, ackSeq: number): void { 38 | /* Construct a data frame */ 39 | const control = (seq << 4) | (rxmit << 3) | ackSeq; 40 | const dataFrame = this.makeFrame(control, data); 41 | this.writeBuffer(dataFrame); 42 | } 43 | 44 | private *stuff(buffer: number[]): Generator { 45 | /* Byte stuff (escape) a string for transmission */ 46 | for (const byte of buffer) { 47 | if (consts.RESERVED.includes(byte)) { 48 | yield consts.ESCAPE; 49 | yield byte ^ consts.STUFF; 50 | } else { 51 | yield byte; 52 | } 53 | } 54 | } 55 | 56 | private makeFrame(control: number, data?: Buffer): Buffer { 57 | /* Construct a frame */ 58 | const frm = [control, ...(data || [])]; 59 | const crc = crc16ccitt(frm, 65535); 60 | frm.push(crc >> 8); 61 | frm.push(crc % 256); 62 | return Buffer.from([...this.stuff(frm), consts.FLAG]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export {Adapter} from "./adapter"; 2 | export * as Events from "./events"; 3 | export * as TsType from "./tstype"; 4 | -------------------------------------------------------------------------------- /src/adapter/serialPort.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {type AutoDetectTypes, type OpenOptionsFromBinding, type SetOptions, autoDetect} from "@serialport/bindings-cpp"; 4 | // This file was copied from https://github.com/serialport/node-serialport/blob/master/packages/serialport/lib/serialport.ts. 5 | import {type ErrorCallback, type OpenOptions, SerialPortStream, type StreamOptions} from "@serialport/stream"; 6 | 7 | const DetectedBinding = autoDetect(); 8 | 9 | export type SerialPortOpenOptions = Omit, "binding"> & OpenOptionsFromBinding; 10 | 11 | export class SerialPort extends SerialPortStream { 12 | static list = DetectedBinding.list; 13 | static readonly binding = DetectedBinding; 14 | 15 | constructor(options: SerialPortOpenOptions, openCallback?: ErrorCallback) { 16 | const opts: OpenOptions = { 17 | binding: DetectedBinding as T, 18 | ...options, 19 | }; 20 | super(opts, openCallback); 21 | } 22 | 23 | public async asyncOpen(): Promise { 24 | return await new Promise((resolve, reject): void => { 25 | this.open((err) => (err ? reject(err) : resolve())); 26 | }); 27 | } 28 | 29 | public async asyncClose(): Promise { 30 | return await new Promise((resolve, reject): void => { 31 | this.close((err) => (err ? reject(err) : resolve())); 32 | }); 33 | } 34 | 35 | public async asyncFlush(): Promise { 36 | return await new Promise((resolve, reject): void => { 37 | this.flush((err) => (err ? reject(err) : resolve())); 38 | }); 39 | } 40 | 41 | public async asyncFlushAndClose(): Promise { 42 | await this.asyncFlush(); 43 | await this.asyncClose(); 44 | } 45 | 46 | public async asyncGet(): Promise<{cts: boolean; dsr: boolean; dcd: boolean}> { 47 | return await new Promise((resolve, reject): void => { 48 | // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` 49 | this.get((err, options?) => (err ? reject(err) : resolve(options!))); 50 | }); 51 | } 52 | 53 | public async asyncSet(options: SetOptions): Promise { 54 | return await new Promise((resolve, reject): void => { 55 | this.set(options, (err) => (err ? reject(err) : resolve())); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/adapter/socketPortUtils.ts: -------------------------------------------------------------------------------- 1 | export function isTcpPath(path: string): boolean { 2 | // tcp path must be: 3 | // tcp://: 4 | const regex = /^(?:tcp:\/\/)[\w.-]+[:][\d]+$/gm; 5 | return regex.test(path); 6 | } 7 | 8 | export function parseTcpPath(path: string): {host: string; port: number} { 9 | const str = path.replace("tcp://", ""); 10 | return { 11 | host: str.substring(0, str.indexOf(":")), 12 | port: Number(str.substring(str.indexOf(":") + 1)), 13 | }; 14 | } 15 | 16 | export default {isTcpPath, parseTcpPath}; 17 | -------------------------------------------------------------------------------- /src/adapter/tstype.ts: -------------------------------------------------------------------------------- 1 | export type Adapter = "deconz" | "ember" | "zstack" | "zboss" | "zigate" | "ezsp" | "zoh"; 2 | export type DiscoverableUsbAdapter = "deconz" | "ember" | "zstack" | "zboss" | "zigate"; 3 | 4 | export type UsbAdapterFingerprint = { 5 | vendorId: string; 6 | productId: string; 7 | manufacturer?: string; 8 | pathRegex: string; 9 | }; 10 | 11 | export interface NetworkOptions { 12 | panID: number; 13 | extendedPanID?: number[]; 14 | channelList: number[]; 15 | networkKey?: number[]; 16 | networkKeyDistribute?: boolean; 17 | } 18 | 19 | export interface SerialPortOptions { 20 | baudRate?: number; 21 | rtscts?: boolean; 22 | path?: string; 23 | adapter?: Adapter; 24 | } 25 | 26 | export interface AdapterOptions { 27 | concurrent?: number; 28 | delay?: number; 29 | disableLED: boolean; 30 | transmitPower?: number; 31 | forceStartWithInconsistentAdapterConfiguration?: boolean; 32 | } 33 | 34 | export interface CoordinatorVersion { 35 | type: string; 36 | meta: {[s: string]: number | string}; 37 | } 38 | 39 | export type DeviceType = "Coordinator" | "EndDevice" | "Router" | "Unknown"; 40 | 41 | export type StartResult = "resumed" | "reset" | "restored"; 42 | 43 | export interface LQINeighbor { 44 | ieeeAddr: string; 45 | networkAddress: number; 46 | linkquality: number; 47 | relationship: number; 48 | depth: number; 49 | } 50 | 51 | export interface Lqi { 52 | neighbors: LQINeighbor[]; 53 | } 54 | 55 | export interface RoutingTableEntry { 56 | destinationAddress: number; 57 | status: string; 58 | nextHop: number; 59 | } 60 | 61 | export interface RoutingTable { 62 | table: RoutingTableEntry[]; 63 | } 64 | 65 | export interface Backup { 66 | adapterType: "zStack"; 67 | time: string; 68 | meta: {[s: string]: number}; 69 | // biome-ignore lint/suspicious/noExplicitAny: API 70 | data: any; 71 | } 72 | 73 | export interface NetworkParameters { 74 | panID: number; 75 | extendedPanID: string; // `0x${string}` same as IEEE address 76 | channel: number; 77 | nwkUpdateID: number; 78 | } 79 | -------------------------------------------------------------------------------- /src/adapter/z-stack/adapter/endpoints.ts: -------------------------------------------------------------------------------- 1 | import {Clusters} from "../../../zspec/zcl/definition/cluster"; 2 | import * as Constants from "../constants"; 3 | 4 | const EndpointDefaults: { 5 | appdeviceid: number; 6 | appdevver: number; 7 | appnuminclusters: number; 8 | appinclusterlist: number[]; 9 | appnumoutclusters: number; 10 | appoutclusterlist: number[]; 11 | latencyreq: number; 12 | } = { 13 | appdeviceid: 0x0005, 14 | appdevver: 0, 15 | appnuminclusters: 0, 16 | appinclusterlist: [], 17 | appnumoutclusters: 0, 18 | appoutclusterlist: [], 19 | latencyreq: Constants.AF.networkLatencyReq.NO_LATENCY_REQS, 20 | }; 21 | 22 | export const Endpoints = [ 23 | {...EndpointDefaults, endpoint: 1, appprofid: 0x0104}, 24 | {...EndpointDefaults, endpoint: 2, appprofid: 0x0101}, 25 | // Required for https://github.com/Koenkk/zigbee-herdsman-converters/commit/d0fb06c2429171f327950484ea3dec80864637cc 26 | {...EndpointDefaults, endpoint: 3, appprofid: 0x0104}, 27 | {...EndpointDefaults, endpoint: 4, appprofid: 0x0107}, 28 | {...EndpointDefaults, endpoint: 5, appprofid: 0x0108}, 29 | {...EndpointDefaults, endpoint: 6, appprofid: 0x0109}, 30 | {...EndpointDefaults, endpoint: 8, appprofid: 0x0104}, 31 | {...EndpointDefaults, endpoint: 10, appprofid: 0x0104}, 32 | { 33 | ...EndpointDefaults, 34 | endpoint: 11, 35 | appprofid: 0x0104, 36 | appdeviceid: 0x0400, 37 | appnumoutclusters: 2, 38 | appoutclusterlist: [Clusters.ssIasZone.ID, Clusters.ssIasWd.ID], 39 | appnuminclusters: 2, 40 | // genTime required for https://github.com/Koenkk/zigbee2mqtt/issues/10816 41 | appinclusterlist: [Clusters.ssIasAce.ID, Clusters.genTime.ID], 42 | }, 43 | // TERNCY: https://github.com/Koenkk/zigbee-herdsman/issues/82 44 | {...EndpointDefaults, endpoint: 0x6e, appprofid: 0x0104}, 45 | {...EndpointDefaults, endpoint: 12, appprofid: 0xc05e}, 46 | { 47 | ...EndpointDefaults, 48 | endpoint: 13, 49 | appprofid: 0x0104, 50 | appnuminclusters: 1, 51 | appinclusterlist: [Clusters.genOta.ID], 52 | }, 53 | // Insta/Jung/Gira: OTA fallback EP (since it's buggy in firmware 10023202 when it tries to find a matching EP for 54 | // OTA - it queries for ZLL profile, but then contacts with HA profile) 55 | {...EndpointDefaults, endpoint: 47, appprofid: 0x0104}, 56 | {...EndpointDefaults, endpoint: 242, appprofid: 0xa1e0}, 57 | ]; 58 | -------------------------------------------------------------------------------- /src/adapter/z-stack/adapter/tstype.ts: -------------------------------------------------------------------------------- 1 | enum ZnpVersion { 2 | ZStack12 = 0, 3 | ZStack3x0 = 1, 4 | ZStack30x = 2, 5 | } 6 | export {ZnpVersion}; 7 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/af.ts: -------------------------------------------------------------------------------- 1 | const BEACON_MAX_DEPTH = 0x0f; 2 | const DEF_NWK_RADIUS = 2 * BEACON_MAX_DEPTH; 3 | 4 | const AF = { 5 | interpanCtl: { 6 | CTL: 0, 7 | SET: 1, 8 | REG: 2, 9 | CHK: 3, 10 | }, 11 | networkLatencyReq: { 12 | NO_LATENCY_REQS: 0, 13 | FAST_BEACONS: 1, 14 | SLOW_BEACONS: 2, 15 | }, 16 | options: { 17 | PREPROCESS: 4, 18 | LIMIT_CONCENTRATOR: 8, 19 | ACK_REQUEST: 16, 20 | DISCV_ROUTE: 32, 21 | EN_SECURITY: 64, 22 | SKIP_ROUTING: 128, 23 | }, 24 | DEFAULT_RADIUS: DEF_NWK_RADIUS, 25 | }; 26 | 27 | export default AF; 28 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/dbg.ts: -------------------------------------------------------------------------------- 1 | const DBG = { 2 | debugThreshold: { 3 | CRITICAL: 1, 4 | ERROR: 2, 5 | INFORMATION: 3, 6 | TRACE: 4, 7 | }, 8 | componentId: { 9 | OSAL: 0, 10 | MTEL: 1, 11 | MTSPCI: 2, 12 | NWK: 3, 13 | NWKIF: 4, 14 | MACCB: 5, 15 | MAC: 6, 16 | APP: 7, 17 | TEST: 8, 18 | RTG: 9, 19 | DATA: 11, 20 | }, 21 | }; 22 | 23 | export default DBG; 24 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/index.ts: -------------------------------------------------------------------------------- 1 | import AF from "./af"; 2 | import * as common from "./common"; 3 | import DBG from "./dbg"; 4 | import MAC from "./mac"; 5 | import SAPI from "./sapi"; 6 | import SYS from "./sys"; 7 | import UTIL from "./util"; 8 | import * as Utils from "./utils"; 9 | import ZDO from "./zdo"; 10 | 11 | export {AF, common as COMMON, DBG, MAC, SAPI, SYS, UTIL, ZDO, Utils}; 12 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/mac.ts: -------------------------------------------------------------------------------- 1 | const MAC = { 2 | assocStatus: { 3 | SUCCESSFUL_ASSOCIATION: 0, 4 | PAN_AT_CAPACITY: 1, 5 | PAN_ACCESS_DENIED: 2, 6 | }, 7 | channelPage: { 8 | PAGE_0: 0, 9 | PAGE_1: 1, 10 | PAGE_2: 2, 11 | }, 12 | txOpt: { 13 | UNDEFINED: 0, 14 | ACK_TRANS: 1, 15 | GTS_TRANS: 2, 16 | IND_TRANS: 4, 17 | SEC_ENABLED_TRANS: 8, 18 | NO_RE_TRANS: 16, 19 | NO_CONFIRM_TRANS: 32, 20 | USE_PIB_VALUE: 64, 21 | USE_POWER_CHANNEL_VALUES: 128, 22 | }, 23 | commReason: { 24 | ASSOCIATE_RSP: 0, 25 | ORPHAN_RSP: 1, 26 | RX_SECURE: 2, 27 | }, 28 | disassocReason: { 29 | RESERVED: 0, 30 | COOR_WISHES_DEV_LEAVE: 1, 31 | DEV_WISHES_LEAVE: 2, 32 | }, 33 | keyIdMode: { 34 | MODE_NONE_OR_IMPLICIT: 0, 35 | MODE_1: 1, 36 | MODE_4: 2, 37 | MODE_8: 3, 38 | }, 39 | beaconOrder: { 40 | ORDER_NO_BEACONS: 15, 41 | ORDER_4_MINUTES: 14, 42 | ORDER_2_MINUTES: 13, 43 | ORDER_1_MINUTE: 12, 44 | ORDER_31_SECONDS: 11, 45 | ORDER_15_SECONDS: 10, 46 | ORDER_7_5_SECONDS: 9, 47 | ORDER_4_SECONDS: 8, 48 | ORDER_2_SECONDS: 7, 49 | ORDER_1_SECOND: 6, 50 | ORDER_480_MSEC: 5, 51 | ORDER_240_MSEC: 4, 52 | ORDER_120_MSEC: 3, 53 | ORDER_60_MSEC: 2, 54 | ORDER_30_MSEC: 1, 55 | ORDER_15_MSEC: 0, 56 | }, 57 | scanType: { 58 | ENERGY_DETECT: 0, 59 | ACTIVE: 1, 60 | PASSIVE: 2, 61 | ORPHAN: 3, 62 | ENHANCED: 5, 63 | }, 64 | frontEndMode: { 65 | PA_LNA_OFF: 0, 66 | PA_LNA_ON: 1, 67 | }, 68 | pidAttr: { 69 | ACK_WAIT_DURATION: 64, 70 | ASSOCIATION_PERMIT: 65, 71 | AUTO_REQUEST: 66, 72 | BATT_LIFE_EXT: 67, 73 | BATT_LIFE_EXT_PERIODS: 68, 74 | BEACON_PAYLOAD: 69, 75 | BEACON_PAYLOAD_LENGTH: 70, 76 | BEACON_ORDER: 71, 77 | BEACON_TX_TIME: 72, 78 | BSN: 73, 79 | COORD_EXTENDED_ADDRESS: 74, 80 | COORD_SHORT_ADDRESS: 75, 81 | DSN: 76, 82 | GTS_PERMIT: 77, 83 | MAX_CSMA_BACKOFFS: 78, 84 | MIN_BE: 79, 85 | PAN_ID: 80, 86 | PROMISCUOUS_MODE: 81, 87 | RX_ON_WHEN_IDLE: 82, 88 | SHORT_ADDRESS: 83, 89 | SUPERFRAME_ORDER: 84, 90 | TRANSACTION_PERSISTENCE_TIME: 85, 91 | ASSOCIATED_PAN_COORD: 86, 92 | MAX_BE: 87, 93 | MAX_FRAME_TOTAL_WAIT_TIME: 88, 94 | MAX_FRAME_RETRIES: 89, 95 | RESPONSE_WAIT_TIME: 90, 96 | SYNC_SYMBOL_OFFSET: 91, 97 | TIMESTAMP_SUPPORTED: 92, 98 | SECURITY_ENABLED: 93, 99 | KEY_TABLE: 113, 100 | KEY_TABLE_ENTRIES: 114, 101 | DEVICE_TABLE: 115, 102 | DEVICE_TABLE_ENTRIES: 116, 103 | SECURITY_LEVEL_TABLE: 117, 104 | SECURITY_LEVEL_TABLE_ENTRIES: 118, 105 | FRAME_COUNTER: 119, 106 | AUTO_REQUEST_SECURITY_LEVEL: 120, 107 | AUTO_REQUEST_KEY_ID_MODE: 121, 108 | AUTO_REQUEST_KEY_SOURCE: 122, 109 | AUTO_REQUEST_KEY_INDEX: 123, 110 | DEFAULT_KEY_SOURCE: 124, 111 | PAN_COORD_EXTENDED_ADDRESS: 125, 112 | PAN_COORD_SHORT_ADDRESS: 126, 113 | KEY_ID_LOOKUP_ENTRY: 208, 114 | KEY_DEVICE_ENTRY: 209, 115 | KEY_USAGE_ENTRY: 210, 116 | KEY_ENTRY: 211, 117 | DEVICE_ENTRY: 212, 118 | SECURITY_LEVEL_ENTRY: 213, 119 | PHY_TRANSMIT_POWER: 224, 120 | LOGICAL_CHANNEL: 225, 121 | EXTENDED_ADDRESS: 226, 122 | ALT_BE: 227, 123 | DEVICE_BEACON_ORDER: 228, 124 | PHY_TRANSMIT_POWER_SIGNED: 229, 125 | }, 126 | }; 127 | 128 | export default MAC; 129 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/sapi.ts: -------------------------------------------------------------------------------- 1 | const SAPI = { 2 | zbDeviceInfo: { 3 | DEV_STATE: 0, 4 | IEEE_ADDR: 1, 5 | SHORT_ADDR: 2, 6 | PARENT_SHORT_ADDR: 3, 7 | PARENT_IEEE_ADDR: 4, 8 | CHANNEL: 5, 9 | PAN_ID: 6, 10 | EXT_PAN_ID: 7, 11 | }, 12 | bindAction: { 13 | REMOVE_BIND: 0, 14 | CREATE_BIND: 1, 15 | }, 16 | searchType: { 17 | ZB_IEEE_SEARCH: 1, 18 | }, 19 | txOptAck: { 20 | NONE: 0, 21 | END_TO_END_ACK: 1, 22 | }, 23 | }; 24 | 25 | export default SAPI; 26 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/sys.ts: -------------------------------------------------------------------------------- 1 | const SYS = { 2 | resetType: { 3 | HARD: 0, 4 | SOFT: 1, 5 | }, 6 | capabilities: { 7 | SYS: 1, 8 | MAC: 2, 9 | NWK: 4, 10 | AF: 8, 11 | ZDO: 16, 12 | SAPI: 32, 13 | UTIL: 64, 14 | DEBUG: 128, 15 | APP: 256, 16 | ZOAD: 4096, 17 | }, 18 | osalTimerEvent: { 19 | EVENT_0: 0, 20 | EVENT_1: 1, 21 | EVENT_2: 2, 22 | EVENT_3: 3, 23 | }, 24 | adcChannels: { 25 | AIN0: 0, 26 | AIN1: 1, 27 | AIN2: 2, 28 | AIN3: 3, 29 | AIN4: 4, 30 | AIN5: 5, 31 | AIN6: 6, 32 | AIN7: 7, 33 | TEMP_SENSOR: 14, 34 | VOLT_READ: 15, 35 | }, 36 | adcResolution: { 37 | BIT_8: 0, 38 | BIT_10: 1, 39 | BIT_12: 2, 40 | BIT_14: 3, 41 | }, 42 | gpioOperation: { 43 | SET_DIRECTION: 0, 44 | SET_INPUT_MODE: 1, 45 | SET: 2, 46 | CLEAR: 3, 47 | TOGGLE: 4, 48 | READ: 5, 49 | }, 50 | sysStkTune: { 51 | TX_PWR: 0, 52 | RX_ON_IDLE: 1, 53 | }, 54 | resetReason: { 55 | POWER_UP: 0, 56 | EXTERNAL: 1, 57 | WATCH_DOG: 2, 58 | }, 59 | nvItemInitStatus: { 60 | ALREADY_EXISTS: 0, 61 | SUCCESS: 9, 62 | FAILED: 10, 63 | }, 64 | nvItemDeleteStatus: { 65 | SUCCESS: 0, 66 | NOT_EXISTS: 9, 67 | FAILED: 10, 68 | BAD_LENGTH: 12, 69 | }, 70 | }; 71 | 72 | export default SYS; 73 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/util.ts: -------------------------------------------------------------------------------- 1 | const UTIL = { 2 | getNvStatus: { 3 | SUCCESS: 0, 4 | GET_IEEE_ADDR_FAIL: 1, 5 | GET_SCAN_CHANNEL_FAIL: 2, 6 | GET_PAN_ID_FAIL: 4, 7 | GET_SECURITY_LEVEL_FAIL: 8, 8 | GET_PRECONFIG_KEY_FAIL: 16, 9 | }, 10 | subsystemId: { 11 | SYS: 256, 12 | MAC: 512, 13 | NWK: 768, 14 | AF: 1024, 15 | ZDO: 1280, 16 | SAPI: 1536, 17 | UTIL: 1792, 18 | DBG: 2048, 19 | APP: 2304, 20 | ALL_SUBSYSTEM: 65535, 21 | }, 22 | deviceType: { 23 | NONE: 0, 24 | COORDINATOR: 1, 25 | ROUTER: 2, 26 | END_DEVICE: 4, 27 | }, 28 | keyEvent: { 29 | KEY_1: 0, 30 | KEY_2: 1, 31 | KEY_3: 2, 32 | KEY_4: 3, 33 | KEY_5: 4, 34 | KEY_6: 5, 35 | KEY_7: 6, 36 | KEY_8: 7, 37 | }, 38 | keyValue: { 39 | KEY_1: 1, 40 | KEY_2: 2, 41 | KEY_3: 4, 42 | KEY_4: 8, 43 | KEY_5: 16, 44 | KEY_6: 32, 45 | KEY_7: 64, 46 | KEY_8: 128, 47 | }, 48 | ledMode: { 49 | OFF: 0, 50 | ON: 1, 51 | BLINK: 2, 52 | FLASH: 3, 53 | TOGGLE: 4, 54 | }, 55 | ledNum: { 56 | LED_1: 1, 57 | LED_2: 2, 58 | LED_3: 3, 59 | LED_4: 4, 60 | ALL_LEDS: 255, 61 | }, 62 | subsAction: { 63 | UNSUBSCRIBE: 0, 64 | SUBSCRIBE: 1, 65 | }, 66 | ackPendingOption: { 67 | ACK_DISABLE: 0, 68 | ACK_ENABLE: 1, 69 | }, 70 | nodeRelation: { 71 | PARENT: 0, 72 | CHILD_RFD: 1, 73 | CHILD_RFD_RX_IDLE: 2, 74 | CHILD_FFD: 3, 75 | CHILD_FFD_RX_IDLE: 4, 76 | NEIGHBOR: 5, 77 | OTHER: 6, 78 | NOTUSED: 255, 79 | }, 80 | }; 81 | 82 | export default UTIL; 83 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/utils.ts: -------------------------------------------------------------------------------- 1 | import {ZnpCommandStatus} from "./common"; 2 | 3 | function getChannelMask(channels: number[]): number[] { 4 | const value = channels.reduce((mask, channel) => mask | (1 << channel), 0); 5 | 6 | return [value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff]; 7 | } 8 | 9 | function statusDescription(code: ZnpCommandStatus): string { 10 | const hex = `0x${code.toString(16).padStart(2, "0")}`; 11 | return `(${hex}: ${ZnpCommandStatus[code] || "UNKNOWN"})`; 12 | } 13 | 14 | export {getChannelMask, statusDescription}; 15 | -------------------------------------------------------------------------------- /src/adapter/z-stack/constants/zdo.ts: -------------------------------------------------------------------------------- 1 | const ZDO = { 2 | status: { 3 | SUCCESS: 0, 4 | INVALID_REQTYPE: 128, 5 | DEVICE_NOT_FOUND: 129, 6 | INVALID_EP: 130, 7 | NOT_ACTIVE: 131, 8 | NOT_SUPPORTED: 132, 9 | TIMEOUT: 133, 10 | NO_MATCH: 134, 11 | NO_ENTRY: 136, 12 | NO_DESCRIPTOR: 137, 13 | INSUFFICIENT_SPACE: 138, 14 | NOT_PERMITTED: 139, 15 | TABLE_FULL: 140, 16 | NOT_AUTHORIZED: 141, 17 | BINDING_TABLE_FULL: 142, 18 | }, 19 | initDev: { 20 | RESTORED_NETWORK_STATE: 0, 21 | NEW_NETWORK_STATE: 1, 22 | LEAVE_NOT_STARTED: 2, 23 | }, 24 | serverCapability: { 25 | NOT_SUPPORTED: 0, 26 | PRIM_TRUST_CENTER: 1, 27 | BKUP_TRUST_CENTER: 2, 28 | PRIM_BIND_TABLE: 4, 29 | BKUP_BIND_TABLE: 8, 30 | PRIM_DISC_TABLE: 16, 31 | BKUP_DISC_TABLE: 32, 32 | NETWORK_MANAGER: 64, 33 | }, 34 | appDevVer: { 35 | VER_100: 0, 36 | RESERVE01: 1, 37 | RESERVE02: 2, 38 | RESERVE03: 3, 39 | RESERVE04: 4, 40 | RESERVE05: 5, 41 | RESERVE06: 6, 42 | RESERVE07: 7, 43 | RESERVE08: 8, 44 | RESERVE09: 9, 45 | RESERVE10: 10, 46 | RESERVE11: 11, 47 | RESERVE12: 12, 48 | RESERVE13: 13, 49 | RESERVE14: 14, 50 | RESERVE15: 15, 51 | }, 52 | stackProfileId: { 53 | NETWORK_SPECIFIC: 0, 54 | HOME_CONTROLS: 1, 55 | ZIGBEEPRO_PROFILE: 2, 56 | GENERIC_STAR: 3, 57 | GENERIC_TREE: 4, 58 | }, 59 | deviceLogicalType: { 60 | COORDINATOR: 0, 61 | ROUTER: 1, 62 | ENDDEVICE: 2, 63 | COMPLEX_DESC_AVAIL: 4, 64 | USER_DESC_AVAIL: 8, 65 | RESERVED1: 16, 66 | RESERVED2: 32, 67 | RESERVED3: 64, 68 | RESERVED4: 128, 69 | }, 70 | addrReqType: { 71 | SINGLE: 0, 72 | EXTENDED: 1, 73 | }, 74 | leaveAndRemoveChild: { 75 | NONE: 0, 76 | LEAVE_REMOVE_CHILDREN: 1, 77 | }, 78 | leaveIndRequest: { 79 | INDICATION: 0, 80 | REQUEST: 1, 81 | }, 82 | leaveIndRemove: { 83 | NONE: 0, 84 | REMOVE_CHILDREN: 1, 85 | }, 86 | leaveIndRejoin: { 87 | NONE: 0, 88 | REJOIN: 1, 89 | }, 90 | descCapability: { 91 | EXT_LIST_NOT_SUPPORTED: 0, 92 | EXT_ACTIVE_EP_LIST_AVAIL: 1, 93 | EXT_SIMPLE_DESC_LIST_AVAIL: 2, 94 | RESERVED1: 4, 95 | RESERVED2: 8, 96 | RESERVED3: 16, 97 | RESERVED4: 32, 98 | RESERVED5: 64, 99 | RESERVED6: 128, 100 | }, 101 | }; 102 | 103 | export default ZDO; 104 | -------------------------------------------------------------------------------- /src/adapter/z-stack/models/startup-options.ts: -------------------------------------------------------------------------------- 1 | import type {TsType} from "../../"; 2 | import type {ZnpVersion} from "../adapter/tstype"; 3 | 4 | /** 5 | * Startup options structure is used by `zStackAdapter` to pass configuration to adapter manager. 6 | */ 7 | export interface StartupOptions { 8 | version: ZnpVersion; 9 | networkOptions: TsType.NetworkOptions; 10 | greenPowerGroup: number; 11 | backupPath: string; 12 | adapterOptions: TsType.AdapterOptions; 13 | } 14 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/address-manager-entry.ts: -------------------------------------------------------------------------------- 1 | import {Struct} from "../struct"; 2 | 3 | /** 4 | * Address manager entry flags present in `user` field. 5 | * 6 | * *Definition from Z-Stack 3.0.2 `ADdrMgr.h`* 7 | */ 8 | export enum AddressManagerUser { 9 | /* ADDRMGR_USER_DEFAULT */ 10 | Default = 0x00, 11 | 12 | /* ADDRMGR_USER_ASSOC */ 13 | Assoc = 0x01, 14 | 15 | /* ADDRMGR_USER_SECURITY */ 16 | Security = 0x02, 17 | 18 | /* ADDRMGR_USER_BINDING */ 19 | Binding = 0x04, 20 | 21 | /* ADDRMGR_USER_PRIVATE1 */ 22 | Private1 = 0x08, 23 | } 24 | 25 | const emptyAddress1 = Buffer.alloc(8, 0x00); 26 | const emptyAddress2 = Buffer.alloc(8, 0xff); 27 | 28 | /** 29 | * Creates an address manager entry. 30 | * 31 | * *Definition from Z-Stack 3.0.2 `AddrMgr.h`* 32 | * *The `uint16` index field is not physically present.* 33 | * 34 | * @param data Data to initialize structure with. 35 | */ 36 | export const addressManagerEntry = (data?: Buffer) => { 37 | return Struct.new() 38 | .member("uint8", "user") 39 | .member("uint16", "nwkAddr") 40 | .member("uint8array-reversed", "extAddr", 8) 41 | .method("isSet", Boolean.prototype, (e) => e.user !== 0x00 && !e.extAddr.equals(emptyAddress1) && !e.extAddr.equals(emptyAddress2)) 42 | .padding(0xff) 43 | .build(data); 44 | }; 45 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/address-manager-table.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import type {StructMemoryAlignment} from "../struct"; 4 | import {Table} from "../table"; 5 | import {addressManagerEntry} from "./address-manager-entry"; 6 | 7 | /** 8 | * Creates an address manager inline table present within Z-Stack NV memory. 9 | * 10 | * @param data Data to initialize table with. 11 | * @param alignment Memory alignment of initialization data. 12 | */ 13 | export const addressManagerTable = (dataOrCapacity?: Buffer | Buffer[] | number, alignment: StructMemoryAlignment = "unaligned") => { 14 | const table = Table.new>() 15 | .struct(addressManagerEntry) 16 | .occupancy((e) => e.isSet() as boolean); 17 | assert(dataOrCapacity !== undefined, "dataOrCapacity cannot be undefined"); 18 | return typeof dataOrCapacity === "number" ? table.build(dataOrCapacity) : table.build(dataOrCapacity, alignment); 19 | }; 20 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/aps-link-key-data-entry.ts: -------------------------------------------------------------------------------- 1 | import {Struct} from "../struct"; 2 | 3 | /** 4 | * Creates a APS Link Key Data Entry. 5 | * 6 | * *Definition from Z-Stack 3.0.2 `APSMEDE.h`* 7 | * 8 | * @param data Data to initialize structure with. 9 | */ 10 | export const apsLinkKeyDataEntry = (data?: Buffer) => { 11 | return Struct.new().member("uint8array", "key", 16).member("uint32", "txFrmCntr").member("uint32", "rxFrmCntr").build(data); 12 | }; 13 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/aps-link-key-data-table.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import type {StructMemoryAlignment} from "../struct"; 4 | import {Table} from "../table"; 5 | import {apsLinkKeyDataEntry} from "./aps-link-key-data-entry"; 6 | 7 | const emptyKey = Buffer.alloc(16, 0x00); 8 | 9 | /** 10 | * Creates an APS link key data table. 11 | * 12 | * @param data Data to initialize table with. 13 | * @param alignment Memory alignment of initialization data. 14 | */ 15 | export const apsLinkKeyDataTable = (dataOrCapacity?: Buffer | Buffer[] | number, alignment: StructMemoryAlignment = "unaligned") => { 16 | const table = Table.new>() 17 | .struct(apsLinkKeyDataEntry) 18 | .occupancy((e) => !e.key.equals(emptyKey)); 19 | assert(dataOrCapacity !== undefined, "dataOrCapacity cannot be undefined"); 20 | return typeof dataOrCapacity === "number" ? table.build(dataOrCapacity) : table.build(dataOrCapacity, alignment); 21 | }; 22 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/aps-tc-link-key-entry.ts: -------------------------------------------------------------------------------- 1 | import {Struct} from "../struct"; 2 | 3 | /** 4 | * Creates a APS ME Trust Center Link Key NV Entry struct. 5 | * 6 | * *Definition from Z-Stack 3.0.2 `APSMEDE.h`* 7 | * 8 | * @param data Data to initialize structure with. 9 | */ 10 | export const apsTcLinkKeyEntry = (data?: Buffer) => { 11 | return Struct.new() 12 | .member("uint32", "txFrmCntr") 13 | .member("uint32", "rxFrmCntr") 14 | .member("uint8array-reversed", "extAddr", 8) 15 | .member("uint8", "keyAttributes") 16 | .member("uint8", "keyType") 17 | .member("uint8", "SeedShift_IcIndex") 18 | .build(data); 19 | }; 20 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/aps-tc-link-key-table.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import type {StructMemoryAlignment} from "../struct"; 4 | import {Table} from "../table"; 5 | import {apsTcLinkKeyEntry} from "./aps-tc-link-key-entry"; 6 | 7 | const emptyAddress = Buffer.alloc(8, 0x00); 8 | 9 | /** 10 | * Creates an APS trust center link key data table. 11 | * 12 | * @param data Data to initialize table with. 13 | * @param alignment Memory alignment of initialization data. 14 | */ 15 | export const apsTcLinkKeyTable = (dataOrCapacity?: Buffer | Buffer[] | number, alignment: StructMemoryAlignment = "unaligned") => { 16 | const table = Table.new>() 17 | .struct(apsTcLinkKeyEntry) 18 | .occupancy((e) => !e.extAddr.equals(emptyAddress)); 19 | assert(dataOrCapacity !== undefined, "dataOrCapacity cannot be undefined"); 20 | return typeof dataOrCapacity === "number" ? table.build(dataOrCapacity) : table.build(dataOrCapacity, alignment); 21 | }; 22 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/channel-list.ts: -------------------------------------------------------------------------------- 1 | import {Struct} from "../struct"; 2 | 3 | /** 4 | * Creates a channel list struct. 5 | * 6 | * @param data Data to initialize structure with. 7 | */ 8 | export const channelList = (data?: Buffer) => Struct.new().member("uint32", "channelList").build(data); 9 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/has-configured.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import {Struct} from "../struct"; 4 | 5 | /** 6 | * Creates a zigbee-herdsman `hasConfigured` struct. 7 | * 8 | * @param data Data to initialize structure with. 9 | */ 10 | export const hasConfigured = (data?: Buffer | Buffer[]) => { 11 | assert(!Array.isArray(data)); 12 | return Struct.new() 13 | .member("uint8", "hasConfigured") 14 | .method("isConfigured", Boolean.prototype, (struct) => struct.hasConfigured === 0x55) 15 | .build(data); 16 | }; 17 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./channel-list"; 2 | export * from "./has-configured"; 3 | export * from "./nib"; 4 | export * from "./nwk-key-descriptor"; 5 | export * from "./nwk-key"; 6 | export * from "./nwk-pan-id"; 7 | export * from "./nwk-sec-material-descriptor-entry"; 8 | export * from "./nwk-sec-material-descriptor-table"; 9 | export * from "./aps-tc-link-key-entry"; 10 | export * from "./aps-tc-link-key-table"; 11 | export * from "./aps-link-key-data-entry"; 12 | export * from "./aps-link-key-data-table"; 13 | export * from "./address-manager-entry"; 14 | export * from "./address-manager-table"; 15 | export * from "./security-manager-entry"; 16 | export * from "./security-manager-table"; 17 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/nib.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import {Struct} from "../struct"; 4 | import {nwkKeyDescriptor} from "./nwk-key-descriptor"; 5 | 6 | /** 7 | * Creates a NIB (Network Information Base) struct. 8 | * 9 | * *Definition from Z-Stack 3.0.2 `nwk.h`* 10 | * 11 | * @param data Data to initialize structure with. 12 | */ 13 | export const nib = (data?: Buffer | Buffer[]) => { 14 | assert(!Array.isArray(data)); 15 | return Struct.new() 16 | .member("uint8", "SequenceNum") 17 | .member("uint8", "PassiveAckTimeout") 18 | .member("uint8", "MaxBroadcastRetries") 19 | .member("uint8", "MaxChildren") 20 | .member("uint8", "MaxDepth") 21 | .member("uint8", "MaxRouters") 22 | .member("uint8", "dummyNeighborTable") 23 | .member("uint8", "BroadcastDeliveryTime") 24 | .member("uint8", "ReportConstantCost") 25 | .member("uint8", "RouteDiscRetries") 26 | .member("uint8", "dummyRoutingTable") 27 | .member("uint8", "SecureAllFrames") 28 | .member("uint8", "SecurityLevel") 29 | .member("uint8", "SymLink") 30 | .member("uint8", "CapabilityFlags") 31 | .member("uint16", "TransactionPersistenceTime") 32 | .member("uint8", "nwkProtocolVersion") 33 | .member("uint8", "RouteDiscoveryTime") 34 | .member("uint8", "RouteExpiryTime") 35 | .member("uint16", "nwkDevAddress") 36 | .member("uint8", "nwkLogicalChannel") 37 | .member("uint16", "nwkCoordAddress") 38 | .member("uint8array-reversed", "nwkCoordExtAddress", 8) 39 | .member("uint16", "nwkPanId") 40 | .member("uint8", "nwkState") 41 | .member("uint32", "channelList") 42 | .member("uint8", "beaconOrder") 43 | .member("uint8", "superFrameOrder") 44 | .member("uint8", "scanDuration") 45 | .member("uint8", "battLifeExt") 46 | .member("uint32", "allocatedRouterAddresses") 47 | .member("uint32", "allocatedEndDeviceAddresses") 48 | .member("uint8", "nodeDepth") 49 | .member("uint8array-reversed", "extendedPANID", 8) 50 | .member("uint8", "nwkKeyLoaded") 51 | .member("struct", "spare1", nwkKeyDescriptor) 52 | .member("struct", "spare2", nwkKeyDescriptor) 53 | .member("uint8", "spare3") 54 | .member("uint8", "spare4") 55 | .member("uint8", "nwkLinkStatusPeriod") 56 | .member("uint8", "nwkRouterAgeLimit") 57 | .member("uint8", "nwkUseMultiCast") 58 | .member("uint8", "nwkIsConcentrator") 59 | .member("uint8", "nwkConcentratorDiscoveryTime") 60 | .member("uint8", "nwkConcentratorRadius") 61 | .member("uint8", "nwkAllFresh") 62 | .member("uint16", "nwkManagerAddr") 63 | .member("uint16", "nwkTotalTransmissions") 64 | .member("uint8", "nwkUpdateId") 65 | .build(data); 66 | }; 67 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/nwk-key-descriptor.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import {Struct} from "../struct"; 4 | 5 | /** 6 | * Creates a Security Service Provider (SSP) Network Descriptor struct. 7 | * 8 | * *Definition from Z-Stack 3.0.2 `ssp.h`* 9 | * 10 | * @param data Data to initialize structure with. 11 | */ 12 | export const nwkKeyDescriptor = (data?: Buffer | Buffer[]) => { 13 | assert(!Array.isArray(data)); 14 | return Struct.new().member("uint8", "keySeqNum").member("uint8array", "key", 16).build(data); 15 | }; 16 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/nwk-key.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import {Struct} from "../struct"; 4 | 5 | /** 6 | * Creates a network key struct. 7 | * 8 | * @param data Data to initialize structure with. 9 | */ 10 | export const nwkKey = (data?: Buffer | Buffer[]) => { 11 | assert(!Array.isArray(data)); 12 | return Struct.new().member("uint8array", "key", 16).build(data); 13 | }; 14 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/nwk-pan-id.ts: -------------------------------------------------------------------------------- 1 | import {Struct} from "../struct"; 2 | 3 | /** 4 | * Creates a network PAN ID struct. 5 | * 6 | * @param data Data to initialize structure with. 7 | */ 8 | export const nwkPanId = (data?: Buffer) => Struct.new().member("uint16", "panId").build(data); 9 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/nwk-sec-material-descriptor-entry.ts: -------------------------------------------------------------------------------- 1 | import {Struct} from "../struct"; 2 | 3 | const emptyExtendedPanId = Buffer.alloc(8, 0x00); 4 | 5 | /** 6 | * Create a ZigBee Device Security Manager Security Material struct. This structure stores a frame counter 7 | * associated with a particular Extended PAN ID used by device. Used in NV in table format: 8 | * - `ZCD_NV_EX_NWK_SEC_MATERIAL_TABLE` - extended table (SimpleLink Z-Stack 3.x.0) 9 | * - `ZCD_NV_LEGACY_NWK_SEC_MATERIAL_TABLE_START` through `ZCD_NV_LEGACY_NWK_SEC_MATERIAL_TABLE_END` (Z-Stack 3.0.x) 10 | * 11 | * *Definition from Z-Stack 3.0.2 `ZDSecMgr.h`* 12 | * 13 | * @param data Data to initialize structure with. 14 | */ 15 | export const nwkSecMaterialDescriptorEntry = (data?: Buffer) => 16 | Struct.new() 17 | .member("uint32", "FrameCounter") 18 | .member("uint8array-reversed", "extendedPanID", 8) 19 | .method("isSet", Boolean.prototype, (struct) => !struct.extendedPanID.equals(emptyExtendedPanId)) 20 | .build(data); 21 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/nwk-sec-material-descriptor-table.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import type {StructMemoryAlignment} from "../struct"; 4 | import {Table} from "../table"; 5 | import {nwkSecMaterialDescriptorEntry} from "./nwk-sec-material-descriptor-entry"; 6 | 7 | /** 8 | * Creates a network security material table. 9 | * 10 | * @param data Data to initialize table with. 11 | * @param alignment Memory alignment of initialization data. 12 | */ 13 | export const nwkSecMaterialDescriptorTable = (dataOrCapacity?: Buffer | Buffer[] | number, alignment: StructMemoryAlignment = "unaligned") => { 14 | const table = Table.new>() 15 | .struct(nwkSecMaterialDescriptorEntry) 16 | .occupancy((e) => e.isSet() as boolean); 17 | assert(dataOrCapacity !== undefined, "dataOrCapacity cannot be undefined"); 18 | return typeof dataOrCapacity === "number" ? table.build(dataOrCapacity) : table.build(dataOrCapacity, alignment); 19 | }; 20 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/security-manager-entry.ts: -------------------------------------------------------------------------------- 1 | import {Struct} from "../struct"; 2 | 3 | /** 4 | * Security manager authentication options. 5 | * 6 | * *Definition from Z-Stack 3.0.2 `ZDSecMgr.h.h`* 7 | */ 8 | export enum SecurityManagerAuthenticationOption { 9 | /* ZDSecMgr_Not_Authenticated */ 10 | Default = 0x00, 11 | 12 | /* ZDSecMgr_Authenticated_CBCK */ 13 | AuthenticatedCBCK = 0x01, 14 | 15 | /* ZDSecMgr_Authenticated_EA */ 16 | AuthenticatedEA = 0x02, 17 | } 18 | 19 | /** 20 | * Creates a security manager entry. 21 | * 22 | * *Definition from Z-Stack 3.0.2 `ZDSecMgr.c`* 23 | * 24 | * @param data Data to initialize structure with. 25 | */ 26 | export const securityManagerEntry = (data?: Buffer) => { 27 | return Struct.new() 28 | .member("uint16", "ami") 29 | .member("uint16", "keyNvId") 30 | .member("uint8", "authenticationOption") 31 | .default(Buffer.from("feff000000", "hex")) 32 | .build(data); 33 | }; 34 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/entries/security-manager-table.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | 3 | import type {StructMemoryAlignment} from "../struct"; 4 | import {Table} from "../table"; 5 | import {SecurityManagerAuthenticationOption, securityManagerEntry} from "./security-manager-entry"; 6 | 7 | /** 8 | * Creates a security manager inline table present within Z-Stack NV memory. 9 | * 10 | * @param data Data to initialize table with. 11 | * @param alignment Memory alignment of initialization data. 12 | */ 13 | export const securityManagerTable = (dataOrCapacity?: Buffer | Buffer[] | number, alignment: StructMemoryAlignment = "unaligned") => { 14 | const table = Table.new>() 15 | .struct(securityManagerEntry) 16 | .occupancy( 17 | (e) => ![0xfffe, 0xffff].includes(e.ami) && !(e.ami === 0x0000 && e.authenticationOption === SecurityManagerAuthenticationOption.Default), 18 | ) 19 | .inlineHeader(); 20 | assert(dataOrCapacity !== undefined, "dataOrCapacity cannot be undefined"); 21 | return typeof dataOrCapacity === "number" ? table.build(dataOrCapacity) : table.build(dataOrCapacity, alignment); 22 | }; 23 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./struct"; 2 | export * from "./table"; 3 | export * from "./serializable-memory-object"; 4 | export * from "./entries"; 5 | -------------------------------------------------------------------------------- /src/adapter/z-stack/structs/serializable-memory-object.ts: -------------------------------------------------------------------------------- 1 | import type {StructMemoryAlignment} from "./struct"; 2 | 3 | /** 4 | * Interface for serializable items to be written to NV. Objects implementing this interface 5 | * are structs and tables. 6 | */ 7 | export interface SerializableMemoryObject { 8 | serialize(alignment?: StructMemoryAlignment): Buffer; 9 | } 10 | 11 | /** 12 | * Signature for factory returning a memory struct or a table. 13 | */ 14 | export type MemoryObjectFactory = (data?: Buffer | Buffer[], alignment?: StructMemoryAlignment) => T; 15 | -------------------------------------------------------------------------------- /src/adapter/z-stack/unpi/constants.ts: -------------------------------------------------------------------------------- 1 | enum Type { 2 | POLL = 0, 3 | SREQ = 1, 4 | AREQ = 2, 5 | SRSP = 3, 6 | } 7 | 8 | enum Subsystem { 9 | RESERVED = 0, 10 | SYS = 1, 11 | MAC = 2, 12 | NWK = 3, 13 | AF = 4, 14 | ZDO = 5, 15 | SAPI = 6, 16 | UTIL = 7, 17 | DEBUG = 8, 18 | APP = 9, 19 | APP_CNF = 15, 20 | GREENPOWER = 21, 21 | } 22 | 23 | const DataStart = 4; 24 | const SOF = 0xfe; 25 | 26 | const PositionDataLength = 1; 27 | const PositionCmd0 = 2; 28 | const PositionCmd1 = 3; 29 | 30 | const MinMessageLength = 5; 31 | const MaxDataSize = 250; 32 | 33 | export {Type, Subsystem, DataStart, SOF, PositionDataLength, MinMessageLength, PositionCmd0, PositionCmd1, MaxDataSize}; 34 | -------------------------------------------------------------------------------- /src/adapter/z-stack/unpi/frame.ts: -------------------------------------------------------------------------------- 1 | import {DataStart, PositionCmd0, PositionCmd1, SOF, type Subsystem, type Type} from "./constants"; 2 | 3 | export class Frame { 4 | public readonly type: Type; 5 | public readonly subsystem: Subsystem; 6 | public readonly commandID: number; 7 | public readonly data: Buffer; 8 | 9 | public readonly length?: number; 10 | public readonly fcs?: number; 11 | 12 | public constructor(type: Type, subsystem: Subsystem, commandID: number, data: Buffer, length?: number, fcs?: number) { 13 | this.type = type; 14 | this.subsystem = subsystem; 15 | this.commandID = commandID; 16 | this.data = data; 17 | this.length = length; 18 | this.fcs = fcs; 19 | } 20 | 21 | public toBuffer(): Buffer { 22 | const length = this.data.length; 23 | const cmd0 = ((this.type << 5) & 0xe0) | (this.subsystem & 0x1f); 24 | 25 | let payload = Buffer.from([SOF, length, cmd0, this.commandID]); 26 | payload = Buffer.concat([payload, this.data]); 27 | const fcs = Frame.calculateChecksum(payload.slice(1, payload.length)); 28 | 29 | return Buffer.concat([payload, Buffer.from([fcs])]); 30 | } 31 | 32 | public static fromBuffer(length: number, fcsPosition: number, buffer: Buffer): Frame { 33 | const subsystem: Subsystem = buffer.readUInt8(PositionCmd0) & 0x1f; 34 | const type: Type = (buffer.readUInt8(PositionCmd0) & 0xe0) >> 5; 35 | const commandID = buffer.readUInt8(PositionCmd1); 36 | const data = buffer.subarray(DataStart, fcsPosition); 37 | const fcs = buffer.readUInt8(fcsPosition); 38 | 39 | // Validate the checksum to see if we fully received the message 40 | const checksum = Frame.calculateChecksum(buffer.subarray(1, fcsPosition)); 41 | 42 | if (checksum === fcs) { 43 | return new Frame(type, subsystem, commandID, data, length, fcs); 44 | } 45 | 46 | throw new Error("Invalid checksum"); 47 | } 48 | 49 | private static calculateChecksum(values: Buffer): number { 50 | let checksum = 0; 51 | 52 | for (const value of values) { 53 | checksum ^= value; 54 | } 55 | 56 | return checksum; 57 | } 58 | 59 | public toString(): string { 60 | return `${this.length} - ${this.type} - ${this.subsystem} - ${this.commandID} - [${[...this.data]}] - ${this.fcs}`; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/adapter/z-stack/unpi/index.ts: -------------------------------------------------------------------------------- 1 | export * as Constants from "./constants"; 2 | export {Frame} from "./frame"; 3 | export {Parser} from "./parser"; 4 | export {Writer} from "./writer"; 5 | -------------------------------------------------------------------------------- /src/adapter/z-stack/unpi/parser.ts: -------------------------------------------------------------------------------- 1 | import * as stream from "node:stream"; 2 | 3 | import {logger} from "../../../utils/logger"; 4 | import {DataStart, MinMessageLength, PositionDataLength, SOF} from "./constants"; 5 | import {Frame} from "./frame"; 6 | 7 | const NS = "zh:zstack:unpi:parser"; 8 | 9 | export class Parser extends stream.Transform { 10 | private buffer: Buffer; 11 | 12 | public constructor() { 13 | super(); 14 | this.buffer = Buffer.from([]); 15 | } 16 | 17 | public override _transform(chunk: Buffer, _: string, cb: () => void): void { 18 | logger.debug(`<-- [${[...chunk]}]`, NS); 19 | this.buffer = Buffer.concat([this.buffer, chunk]); 20 | this.parseNext(); 21 | cb(); 22 | } 23 | 24 | private parseNext(): void { 25 | logger.debug(`--- parseNext [${[...this.buffer]}]`, NS); 26 | 27 | if (this.buffer.length !== 0 && this.buffer.readUInt8(0) !== SOF) { 28 | // Buffer doesn't start with SOF, skip till SOF. 29 | const index = this.buffer.indexOf(SOF); 30 | if (index !== -1) { 31 | this.buffer = this.buffer.slice(index, this.buffer.length); 32 | } 33 | } 34 | 35 | if (this.buffer.length >= MinMessageLength && this.buffer.readUInt8(0) === SOF) { 36 | const dataLength = this.buffer[PositionDataLength]; 37 | const fcsPosition = DataStart + dataLength; 38 | const frameLength = fcsPosition + 1; 39 | 40 | if (this.buffer.length >= frameLength) { 41 | const frameBuffer = this.buffer.slice(0, frameLength); 42 | 43 | try { 44 | const frame = Frame.fromBuffer(dataLength, fcsPosition, frameBuffer); 45 | logger.debug(`--> parsed ${frame}`, NS); 46 | this.emit("parsed", frame); 47 | } catch (error) { 48 | logger.debug(`--> error ${error}`, NS); 49 | } 50 | 51 | this.buffer = this.buffer.slice(frameLength, this.buffer.length); 52 | this.parseNext(); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/adapter/z-stack/unpi/writer.ts: -------------------------------------------------------------------------------- 1 | import * as stream from "node:stream"; 2 | 3 | import {logger} from "../../../utils/logger"; 4 | import type {Frame} from "./frame"; 5 | 6 | const NS = "zh:zstack:unpi:writer"; 7 | 8 | export class Writer extends stream.Readable { 9 | public writeFrame(frame: Frame): void { 10 | const buffer = frame.toBuffer(); 11 | logger.debug(`--> frame [${[...buffer]}]`, NS); 12 | this.push(buffer); 13 | } 14 | 15 | public writeBuffer(buffer: Buffer): void { 16 | logger.debug(`--> buffer [${[...buffer]}]`, NS); 17 | this.push(buffer); 18 | } 19 | 20 | public override _read(): void {} 21 | } 22 | -------------------------------------------------------------------------------- /src/adapter/z-stack/utils/channel-list.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts packed `uint32` channel list to array of channel numbers. 3 | * 4 | * @param packedList Packed channel list value. 5 | */ 6 | export const unpackChannelList = (packedList: number): number[] => { 7 | return Array(26 - 11 + 1) 8 | .fill(0) 9 | .map((_, i) => 11 + i) 10 | .filter((c) => ((1 << c) & packedList) > 0); 11 | }; 12 | 13 | /** 14 | * Converts array of channel numbers to packed `uint32` structure represented as number. 15 | * Supported channel range is 11 - 29. 16 | * 17 | * @param channelList List of channels to be packed. 18 | */ 19 | export const packChannelList = (channelList: number[]): number => { 20 | const invalidChannel = channelList.find((c) => c < 11 || c > 26); 21 | /* v8 ignore start */ 22 | if (invalidChannel !== undefined) { 23 | throw new Error(`Cannot pack channel list - unsupported channel ${invalidChannel}`); 24 | } 25 | /* v8 ignore stop */ 26 | return channelList.reduce((a, c) => a + (1 << c), 0); 27 | }; 28 | 29 | /** 30 | * Compares two channel lists. Either number arrays or packed `uint32` numbers may be provided. 31 | * 32 | * @param list1 First list to compare. 33 | * @param list2 Second list to compare. 34 | */ 35 | export const compareChannelLists = (list1: number | number[], list2: number | number[]): boolean => { 36 | /* v8 ignore next */ 37 | list1 = Array.isArray(list1) ? packChannelList(list1) : list1; 38 | list2 = Array.isArray(list2) ? packChannelList(list2) : list2; 39 | return list1 === list2; 40 | }; 41 | -------------------------------------------------------------------------------- /src/adapter/z-stack/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./channel-list"; 2 | export * from "./network-options"; 3 | -------------------------------------------------------------------------------- /src/adapter/z-stack/utils/network-options.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import type * as Models from "../../../models"; 4 | import {compareChannelLists} from "./channel-list"; 5 | 6 | /** 7 | * Checks if two network options models match. 8 | * 9 | * @param opts1 First network options struct to check. 10 | * @param opts2 Second network options struct to check. 11 | */ 12 | export const compareNetworkOptions = ( 13 | opts1: Models.NetworkOptions, 14 | opts2: Models.NetworkOptions, 15 | lenientExtendedPanIdMatching?: boolean, 16 | ): boolean => { 17 | return Boolean( 18 | opts1.panId === opts2.panId && 19 | (opts1.extendedPanId.equals(opts2.extendedPanId) || 20 | (lenientExtendedPanIdMatching && (opts1.hasDefaultExtendedPanId || opts2.hasDefaultExtendedPanId)) || 21 | (lenientExtendedPanIdMatching && opts1.extendedPanId.equals(Buffer.from(opts2.extendedPanId).reverse()))) && 22 | opts1.networkKey.equals(opts2.networkKey) && 23 | compareChannelLists(opts1.channelList, opts2.channelList) && 24 | opts1.networkKeyDistribute === opts2.networkKeyDistribute, 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/adapter/z-stack/znp/index.ts: -------------------------------------------------------------------------------- 1 | export {Znp} from "./znp"; 2 | export {ZpiObject} from "./zpiObject"; 3 | -------------------------------------------------------------------------------- /src/adapter/z-stack/znp/parameterType.ts: -------------------------------------------------------------------------------- 1 | enum ParameterType { 2 | UINT8 = 0, 3 | UINT16 = 1, 4 | UINT32 = 2, 5 | IEEEADDR = 3, 6 | 7 | BUFFER = 4, 8 | BUFFER8 = 5, 9 | BUFFER16 = 6, 10 | BUFFER18 = 7, 11 | BUFFER32 = 8, 12 | BUFFER42 = 9, 13 | BUFFER100 = 10, 14 | 15 | LIST_UINT8 = 11, 16 | LIST_UINT16 = 12, 17 | LIST_NETWORK = 16, 18 | 19 | INT8 = 18, 20 | } 21 | 22 | export default ParameterType; 23 | -------------------------------------------------------------------------------- /src/adapter/z-stack/znp/tstype.ts: -------------------------------------------------------------------------------- 1 | import type {ClusterId as ZdoClusterId} from "../../../zspec/zdo"; 2 | import type {Type as CommandType} from "../unpi/constants"; 3 | import type ParameterType from "./parameterType"; 4 | 5 | export type MtType = number | number[] | string | Buffer | {[s: string]: number | string}[]; 6 | 7 | export interface MtParameter { 8 | name: string; 9 | parameterType: ParameterType; 10 | } 11 | 12 | interface MtCmdBase { 13 | name: string; 14 | ID: number; 15 | type: number; 16 | request: MtParameter[]; 17 | response: MtParameter[]; 18 | zdoClusterId: ZdoClusterId; 19 | } 20 | 21 | interface MtCmdAreq extends Omit { 22 | type: CommandType.AREQ; 23 | } 24 | 25 | interface MtCmdSreq extends Omit { 26 | type: CommandType.SREQ; 27 | } 28 | 29 | export interface MtCmdAreqZdo extends Omit { 30 | type: CommandType.AREQ; 31 | } 32 | 33 | export interface MtCmdSreqZdo extends Omit { 34 | type: CommandType.SREQ; 35 | } 36 | 37 | export type MtCmd = MtCmdAreq | MtCmdSreq | MtCmdAreqZdo | MtCmdSreqZdo; 38 | // biome-ignore lint/suspicious/noExplicitAny: API 39 | export type ZpiObjectPayload = {[s: string]: any}; 40 | 41 | export interface BuffaloZnpOptions { 42 | length?: number; 43 | startIndex?: number; 44 | } 45 | -------------------------------------------------------------------------------- /src/adapter/z-stack/znp/utils.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "../unpi/constants"; 2 | import type {MtCmd, MtCmdAreqZdo, MtCmdSreqZdo} from "./tstype"; 3 | 4 | export function isMtCmdAreqZdo(cmd: MtCmd): cmd is MtCmdAreqZdo { 5 | return cmd.type === Type.AREQ && "zdoClusterId" in cmd; 6 | } 7 | 8 | export function isMtCmdSreqZdo(cmd: MtCmd): cmd is MtCmdSreqZdo { 9 | return cmd.type === Type.SREQ && "zdoClusterId" in cmd; 10 | } 11 | -------------------------------------------------------------------------------- /src/adapter/zboss/consts.ts: -------------------------------------------------------------------------------- 1 | export const END = 0xc0; 2 | export const ESCAPE = 0xdb; 3 | export const ESCEND = 0xdc; 4 | export const ESCESC = 0xdd; 5 | 6 | export const SIGNATURE = 0xdead; 7 | export const ZBOSS_NCP_API_HL = 0x06; 8 | export const ZBOSS_FLAG_FIRST_FRAGMENT = 0x40; 9 | export const ZBOSS_FLAG_LAST_FRAGMENT = 0x80; 10 | -------------------------------------------------------------------------------- /src/adapter/zboss/reader.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {Transform, type TransformCallback, type TransformOptions} from "node:stream"; 4 | 5 | import {logger} from "../../utils/logger"; 6 | import {SIGNATURE} from "./consts"; 7 | 8 | const NS = "zh:zboss:read"; 9 | 10 | export class ZBOSSReader extends Transform { 11 | private buffer: Buffer; 12 | 13 | public constructor(opts?: TransformOptions) { 14 | super(opts); 15 | 16 | this.buffer = Buffer.alloc(0); 17 | } 18 | 19 | override _transform(chunk: Buffer, _encoding: BufferEncoding, cb: TransformCallback): void { 20 | let data = Buffer.concat([this.buffer, chunk]); 21 | let position: number; 22 | 23 | logger.debug(`<<< DATA [${chunk.toString("hex")}]`, NS); 24 | // SIGNATURE - start of package 25 | // biome-ignore lint/suspicious/noAssignInExpressions: shorter 26 | while ((position = data.indexOf(SIGNATURE)) !== -1) { 27 | // need for read length 28 | if (data.length > position + 3) { 29 | const len = data.readUInt16LE(position + 1); 30 | if (data.length >= position + 1 + len) { 31 | const frame = data.subarray(position + 1, position + 1 + len); 32 | logger.debug(`<<< FRAME [${frame.toString("hex")}]`, NS); 33 | // emit the frame via 'data' event 34 | this.push(frame); 35 | 36 | // if position not 1 - try to convert buffer before position to text - chip console output 37 | if (position > 1) { 38 | logger.debug(`<<< CONSOLE:\n\r${data.subarray(0, position - 1).toString()}`, NS); 39 | } 40 | // remove the frame from internal buffer (set below) 41 | data = data.subarray(position + 1 + len); 42 | if (data.length) logger.debug(`<<< TAIL [${data.toString("hex")}]`, NS); 43 | } else { 44 | logger.debug(`<<< Not enough data. Length=${data.length}, frame length=${len}. Waiting`, NS); 45 | break; 46 | } 47 | } else { 48 | logger.debug(`<<< Not enough data. Length=${data.length}. Waiting`, NS); 49 | break; 50 | } 51 | } 52 | 53 | this.buffer = data; 54 | 55 | cb(); 56 | } 57 | 58 | override _flush(cb: TransformCallback): void { 59 | this.push(this.buffer); 60 | 61 | this.buffer = Buffer.alloc(0); 62 | 63 | cb(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/adapter/zboss/types.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koenkk/zigbee-herdsman/949a08c7ffd1685030e8a713c40970c17cee88a3/src/adapter/zboss/types.ts -------------------------------------------------------------------------------- /src/adapter/zboss/utils.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | const crc16Table = [ 4 | 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 5 | 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 6 | 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 7 | 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 8 | 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 9 | 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 10 | 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 11 | 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 12 | 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 13 | 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 14 | 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 15 | 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 16 | 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 17 | 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 18 | 0x3de3, 0x2c6a, 0x1ef1, 0x0f78, 19 | ]; 20 | 21 | /** 22 | * width=16 poly=0x1021 init=0x0000 refin=true refout=true xorout=0x0000 check=0x2189 residue=0x0000 name="CRC-16/KERMIT" 23 | */ 24 | export function crc16(data: Buffer): number { 25 | let crc = 0x0000; 26 | 27 | for (const byte of data) { 28 | crc = crc16Table[(crc ^ byte) & 0xff] ^ ((crc >> 8) & 0xff); 29 | } 30 | 31 | return crc ^ (0x0 & 0xffff); 32 | } 33 | 34 | const crc8Table = [ 35 | 0xea, 0xd4, 0x96, 0xa8, 0x12, 0x2c, 0x6e, 0x50, 0x7f, 0x41, 0x03, 0x3d, 0x87, 0xb9, 0xfb, 0xc5, 0xa5, 0x9b, 0xd9, 0xe7, 0x5d, 0x63, 0x21, 0x1f, 36 | 0x30, 0x0e, 0x4c, 0x72, 0xc8, 0xf6, 0xb4, 0x8a, 0x74, 0x4a, 0x08, 0x36, 0x8c, 0xb2, 0xf0, 0xce, 0xe1, 0xdf, 0x9d, 0xa3, 0x19, 0x27, 0x65, 0x5b, 37 | 0x3b, 0x05, 0x47, 0x79, 0xc3, 0xfd, 0xbf, 0x81, 0xae, 0x90, 0xd2, 0xec, 0x56, 0x68, 0x2a, 0x14, 0xb3, 0x8d, 0xcf, 0xf1, 0x4b, 0x75, 0x37, 0x09, 38 | 0x26, 0x18, 0x5a, 0x64, 0xde, 0xe0, 0xa2, 0x9c, 0xfc, 0xc2, 0x80, 0xbe, 0x04, 0x3a, 0x78, 0x46, 0x69, 0x57, 0x15, 0x2b, 0x91, 0xaf, 0xed, 0xd3, 39 | 0x2d, 0x13, 0x51, 0x6f, 0xd5, 0xeb, 0xa9, 0x97, 0xb8, 0x86, 0xc4, 0xfa, 0x40, 0x7e, 0x3c, 0x02, 0x62, 0x5c, 0x1e, 0x20, 0x9a, 0xa4, 0xe6, 0xd8, 40 | 0xf7, 0xc9, 0x8b, 0xb5, 0x0f, 0x31, 0x73, 0x4d, 0x58, 0x66, 0x24, 0x1a, 0xa0, 0x9e, 0xdc, 0xe2, 0xcd, 0xf3, 0xb1, 0x8f, 0x35, 0x0b, 0x49, 0x77, 41 | 0x17, 0x29, 0x6b, 0x55, 0xef, 0xd1, 0x93, 0xad, 0x82, 0xbc, 0xfe, 0xc0, 0x7a, 0x44, 0x06, 0x38, 0xc6, 0xf8, 0xba, 0x84, 0x3e, 0x00, 0x42, 0x7c, 42 | 0x53, 0x6d, 0x2f, 0x11, 0xab, 0x95, 0xd7, 0xe9, 0x89, 0xb7, 0xf5, 0xcb, 0x71, 0x4f, 0x0d, 0x33, 0x1c, 0x22, 0x60, 0x5e, 0xe4, 0xda, 0x98, 0xa6, 43 | 0x01, 0x3f, 0x7d, 0x43, 0xf9, 0xc7, 0x85, 0xbb, 0x94, 0xaa, 0xe8, 0xd6, 0x6c, 0x52, 0x10, 0x2e, 0x4e, 0x70, 0x32, 0x0c, 0xb6, 0x88, 0xca, 0xf4, 44 | 0xdb, 0xe5, 0xa7, 0x99, 0x23, 0x1d, 0x5f, 0x61, 0x9f, 0xa1, 0xe3, 0xdd, 0x67, 0x59, 0x1b, 0x25, 0x0a, 0x34, 0x76, 0x48, 0xf2, 0xcc, 0x8e, 0xb0, 45 | 0xd0, 0xee, 0xac, 0x92, 0x28, 0x16, 0x54, 0x6a, 0x45, 0x7b, 0x39, 0x07, 0xbd, 0x83, 0xc1, 0xff, 46 | ]; 47 | /** 48 | * width=8 poly=0x4d init=0xff refin=true refout=true xorout=0xff check=0xd8 name="CRC-8/KOOP" 49 | */ 50 | export function crc8(data: Buffer): number { 51 | let crc = 0x00; 52 | 53 | for (const byte of data) { 54 | crc = crc8Table[(crc ^ byte) & 0xff]; 55 | } 56 | 57 | return crc; 58 | } 59 | -------------------------------------------------------------------------------- /src/adapter/zboss/writer.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {Readable, type ReadableOptions} from "node:stream"; 4 | 5 | export class ZBOSSWriter extends Readable { 6 | private bytesToWrite: number[]; 7 | 8 | constructor(opts?: ReadableOptions) { 9 | super(opts); 10 | 11 | this.bytesToWrite = []; 12 | } 13 | 14 | private writeBytes(): void { 15 | const buffer = Buffer.from(this.bytesToWrite); 16 | this.bytesToWrite = []; 17 | 18 | // expensive and very verbose, enable locally only if necessary 19 | // logger.debug(`>>>> [FRAME raw=${buffer.toString('hex')}]`, NS); 20 | 21 | // this.push(buffer); 22 | this.emit("data", buffer); 23 | } 24 | 25 | public writeByte(byte: number): void { 26 | this.bytesToWrite.push(byte); 27 | } 28 | 29 | public writeAvailable(): boolean { 30 | if (this.readableLength < this.readableHighWaterMark) { 31 | return true; 32 | } 33 | 34 | this.writeFlush(); 35 | 36 | return false; 37 | } 38 | 39 | /** 40 | * If there is anything to send, send to the port. 41 | */ 42 | public writeFlush(): void { 43 | if (this.bytesToWrite.length) { 44 | this.writeBytes(); 45 | } 46 | } 47 | 48 | public override _read(): void {} 49 | } 50 | -------------------------------------------------------------------------------- /src/adapter/zigate/adapter/patchZdoBuffaloBE.ts: -------------------------------------------------------------------------------- 1 | import {BuffaloZdo} from "../../../zspec/zdo/buffaloZdo"; 2 | 3 | class ZiGateZdoBuffalo extends BuffaloZdo { 4 | public override writeUInt16(value: number): void { 5 | this.buffer.writeUInt16BE(value, this.position); 6 | this.position += 2; 7 | } 8 | 9 | public override writeUInt32(value: number): void { 10 | this.buffer.writeUInt32BE(value, this.position); 11 | this.position += 4; 12 | } 13 | 14 | public override writeIeeeAddr(value: string /*TODO: EUI64*/): void { 15 | this.writeUInt32(Number.parseInt(value.slice(2, 10), 16)); 16 | this.writeUInt32(Number.parseInt(value.slice(10), 16)); 17 | } 18 | } 19 | 20 | /** 21 | * Patch BuffaloZdo to use Big Endian variants. 22 | */ 23 | export const patchZdoBuffaloBE = (): void => { 24 | BuffaloZdo.prototype.writeUInt16 = ZiGateZdoBuffalo.prototype.writeUInt16; 25 | BuffaloZdo.prototype.writeUInt32 = ZiGateZdoBuffalo.prototype.writeUInt32; 26 | BuffaloZdo.prototype.writeIeeeAddr = ZiGateZdoBuffalo.prototype.writeIeeeAddr; 27 | }; 28 | -------------------------------------------------------------------------------- /src/adapter/zigate/driver/LICENSE: -------------------------------------------------------------------------------- 1 | When writing the adapter, the first tests and code implementation examples were taken from 2 | https://github.com/nouknouk/node-zigate 3 | https://github.com/Neonox31/zigate 4 | 5 | 6 | The zigate frame parsing is mostly inherited from Neonox31/zigate 7 | driver/frame.ts 8 | 9 | Copyright 2017 WEBER Logan 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/adapter/zigate/driver/constants.ts: -------------------------------------------------------------------------------- 1 | import {ClusterId as ZdoClusterId} from "../../../zspec/zdo"; 2 | 3 | export enum AddressMode { 4 | Bound = 0x00, //Use one or more bound nodes/endpoints, with acknowledgements 5 | Group = 0x01, //Use a pre-defined group address, with acknowledgements 6 | Short = 0x02, //Use a 16-bit network address, with acknowledgements 7 | Ieee = 0x03, //Use a 64-bit IEEE/MAC address, with acknowledgements 8 | Broadcast = 0x04, //Perform a broadcast 9 | NoTransmit = 0x05, //Do not transmit 10 | BoundNoAck = 0x06, //Perform a bound transmission, with no acknowledgements 11 | ShortNoAck = 0x07, //Perform a transmission using a 16-bit network address, with no acknowledgements 12 | IeeeNoAck = 0x08, //Perform a transmission using a 64-bit IEEE/MAC address, with no acknowledgements 13 | BoundNonBlocking = 0x09, //Perform a non-blocking bound transmission, with acknowledgements 14 | BoundNonBlockingNoAck = 10, //Perform a non-blocking bound transmission, with no acknowledgements 15 | } 16 | 17 | export enum DeviceType { 18 | Coordinator = 0, 19 | Router = 1, 20 | LegacyRouter = 2, 21 | } 22 | 23 | export enum LogLevel { 24 | EMERG = 0, 25 | ALERT = 1, 26 | "CRIT " = 2, 27 | ERROR = 3, 28 | "WARN " = 4, 29 | "NOT " = 5, 30 | "INFO " = 6, 31 | DEBUG = 7, 32 | } 33 | 34 | export enum Status { 35 | E_SL_MSG_STATUS_SUCCESS = 0, 36 | E_SL_MSG_STATUS_INCORRECT_PARAMETERS = 1, 37 | E_SL_MSG_STATUS_UNHANDLED_COMMAND = 2, 38 | E_SL_MSG_STATUS_BUSY = 3, 39 | E_SL_MSG_STATUS_STACK_ALREADY_STARTED = 4, 40 | } 41 | 42 | export enum ZiGateCommandCode { 43 | GetNetworkState = 0x0009, 44 | RawMode = 0x0002, 45 | SetExtendedPANID = 0x0020, 46 | SetChannelMask = 0x0021, 47 | GetVersion = 0x0010, 48 | Reset = 0x0011, 49 | ErasePersistentData = 0x0012, 50 | RemoveDevice = 0x0026, 51 | RawAPSDataRequest = 0x0530, 52 | GetTimeServer = 0x0017, 53 | SetTimeServer = 0x0016, 54 | PermitJoinStatus = 0x0014, 55 | GetDevicesList = 0x0015, 56 | 57 | StartNetwork = 0x0024, 58 | StartNetworkScan = 0x0025, 59 | SetCertification = 0x0019, 60 | 61 | // ResetFactoryNew = 0x0013, 62 | OnOff = 0x0092, 63 | OnOffTimed = 0x0093, 64 | AttributeDiscovery = 0x0140, 65 | AttributeRead = 0x0100, 66 | AttributeWrite = 0x0110, 67 | DescriptorComplex = 0x0531, 68 | 69 | // zdo 70 | Bind = 0x0030, 71 | UnBind = 0x0031, 72 | NwkAddress = 0x0040, 73 | IEEEAddress = 0x0041, 74 | NodeDescriptor = 0x0042, 75 | SimpleDescriptor = 0x0043, 76 | PowerDescriptor = 0x0044, 77 | ActiveEndpoint = 0x0045, 78 | MatchDescriptor = 0x0046, 79 | // ManagementLeaveRequest = 0x0047, XXX: some non-standard form of LeaveRequest? 80 | PermitJoin = 0x0049, 81 | ManagementNetworkUpdate = 0x004a, 82 | SystemServerDiscovery = 0x004b, 83 | LeaveRequest = 0x004c, 84 | ManagementLQI = 0x004e, 85 | // ManagementRtg = 0x004?, 86 | // ManagementBind = 0x004?, 87 | 88 | SetDeviceType = 0x0023, 89 | LED = 0x0018, 90 | SetTXpower = 0x0806, 91 | SetSecurityStateKey = 0x0022, 92 | AddGroup = 0x0060, 93 | } 94 | 95 | export const ZDO_REQ_CLUSTER_ID_TO_ZIGATE_COMMAND_ID: Readonly>> = { 96 | [ZdoClusterId.NETWORK_ADDRESS_REQUEST]: ZiGateCommandCode.NwkAddress, 97 | [ZdoClusterId.IEEE_ADDRESS_REQUEST]: ZiGateCommandCode.IEEEAddress, 98 | [ZdoClusterId.NODE_DESCRIPTOR_REQUEST]: ZiGateCommandCode.NodeDescriptor, 99 | [ZdoClusterId.POWER_DESCRIPTOR_REQUEST]: ZiGateCommandCode.PowerDescriptor, 100 | [ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST]: ZiGateCommandCode.SimpleDescriptor, 101 | [ZdoClusterId.MATCH_DESCRIPTORS_REQUEST]: ZiGateCommandCode.MatchDescriptor, 102 | [ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST]: ZiGateCommandCode.ActiveEndpoint, 103 | [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST]: ZiGateCommandCode.SystemServerDiscovery, 104 | [ZdoClusterId.BIND_REQUEST]: ZiGateCommandCode.Bind, 105 | [ZdoClusterId.UNBIND_REQUEST]: ZiGateCommandCode.UnBind, 106 | [ZdoClusterId.LQI_TABLE_REQUEST]: ZiGateCommandCode.ManagementLQI, 107 | // [ZdoClusterId.ROUTING_TABLE_REQUEST]: ZiGateCommandCode.ManagementRtg, 108 | // [ZdoClusterId.BINDING_TABLE_REQUEST]: ZiGateCommandCode.ManagementBind, 109 | [ZdoClusterId.LEAVE_REQUEST]: ZiGateCommandCode.LeaveRequest, 110 | [ZdoClusterId.NWK_UPDATE_REQUEST]: ZiGateCommandCode.ManagementNetworkUpdate, 111 | [ZdoClusterId.PERMIT_JOINING_REQUEST]: ZiGateCommandCode.PermitJoin, 112 | }; 113 | 114 | export enum ZiGateMessageCode { 115 | DeviceAnnounce = 0x004d, 116 | Status = 0x8000, 117 | LOG = 0x8001, 118 | DataIndication = 0x8002, 119 | NodeClusterList = 0x8003, 120 | NodeAttributeList = 0x8004, 121 | NodeCommandIDList = 0x8005, 122 | SimpleDescriptorResponse = 0x8043, 123 | NetworkState = 0x8009, 124 | VersionList = 0x8010, 125 | APSDataACK = 0x8011, 126 | APSDataConfirm = 0x8012, 127 | APSDataConfirmFailed = 0x8702, 128 | NetworkJoined = 0x8024, 129 | LeaveIndication = 0x8048, 130 | RouterDiscoveryConfirm = 0x8701, 131 | PermitJoinStatus = 0x8014, 132 | GetTimeServer = 0x8017, 133 | ManagementLQIResponse = 0x804e, 134 | ManagementLeaveResponse = 0x8047, 135 | PDMEvent = 0x8035, 136 | PDMLoaded = 0x0302, 137 | RestartNonFactoryNew = 0x8006, 138 | RestartFactoryNew = 0x8007, 139 | ExtendedStatusCallBack = 0x9999, 140 | AddGroupResponse = 0x8060, 141 | } 142 | // biome-ignore lint/suspicious/noExplicitAny: API 143 | export type ZiGateObjectPayload = any; 144 | 145 | export enum ZPSNwkKeyState { 146 | ZPS_ZDO_NO_NETWORK_KEY = 0, 147 | ZPS_ZDO_PRECONFIGURED_LINK_KEY = 1, 148 | ZPS_ZDO_DISTRIBUTED_LINK_KEY = 2, 149 | ZPS_ZDO_PRECONFIGURED_INSTALLATION_CODE = 3, 150 | } 151 | -------------------------------------------------------------------------------- /src/adapter/zigate/driver/parameterType.ts: -------------------------------------------------------------------------------- 1 | // All multi-byte values are big-endian 2 | enum ParameterType { 3 | UINT8 = 0, 4 | UINT16 = 1, 5 | UINT32 = 2, 6 | IEEEADDR = 3, 7 | 8 | BUFFER = 4, 9 | BUFFER8 = 5, 10 | BUFFER16 = 6, 11 | BUFFER18 = 7, 12 | BUFFER32 = 8, 13 | BUFFER42 = 9, 14 | BUFFER100 = 10, 15 | 16 | LIST_UINT8 = 11, 17 | LIST_UINT16 = 12, 18 | 19 | INT8 = 18, 20 | MACCAPABILITY = 100, 21 | ADDRESS_WITH_TYPE_DEPENDENCY = 101, 22 | RAW = 102, 23 | 24 | // /!\ TODO missing but used in code, IDs assigned for proper compiling, NOT based on spec, needs updating 25 | // /!\ some also don't have proper read/write in BuffaloZiGate 26 | BUFFER_RAW = 247, 27 | MAYBE_UINT8 = 252, 28 | LOG_LEVEL = 253, 29 | STRING = 254, 30 | } 31 | 32 | export default ParameterType; 33 | -------------------------------------------------------------------------------- /src/adapter/zigate/driver/ziGateObject.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | import {logger} from "../../../utils/logger"; 4 | import BuffaloZiGate, {type BuffaloZiGateOptions} from "./buffaloZiGate"; 5 | import {ZiGateCommand, type ZiGateCommandParameter, type ZiGateCommandType} from "./commandType"; 6 | import type {ZiGateCommandCode, ZiGateMessageCode, ZiGateObjectPayload} from "./constants"; 7 | import ZiGateFrame from "./frame"; 8 | import {ZiGateMessage, type ZiGateMessageParameter} from "./messageType"; 9 | import ParameterType from "./parameterType"; 10 | 11 | type ZiGateCode = ZiGateCommandCode | ZiGateMessageCode; 12 | type ZiGateParameter = ZiGateCommandParameter | ZiGateMessageParameter; 13 | 14 | const NS = "zh:zigate:object"; 15 | 16 | const BufferAndListTypes: ParameterType[] = [ 17 | ParameterType.BUFFER, 18 | ParameterType.BUFFER8, 19 | ParameterType.BUFFER16, 20 | ParameterType.BUFFER18, 21 | ParameterType.BUFFER32, 22 | ParameterType.BUFFER42, 23 | ParameterType.BUFFER100, 24 | ParameterType.LIST_UINT16, 25 | ParameterType.LIST_UINT8, 26 | ]; 27 | 28 | class ZiGateObject { 29 | private readonly _code: ZiGateCode; 30 | private readonly _payload: ZiGateObjectPayload; 31 | private readonly _parameters: ZiGateParameter[]; 32 | private readonly _frame?: ZiGateFrame; 33 | 34 | private constructor(code: ZiGateCode, payload: ZiGateObjectPayload, parameters: ZiGateParameter[], frame?: ZiGateFrame) { 35 | this._code = code; 36 | this._payload = payload; 37 | this._parameters = parameters; 38 | this._frame = frame; 39 | } 40 | 41 | get code(): ZiGateCode { 42 | return this._code; 43 | } 44 | 45 | get frame(): ZiGateFrame | undefined { 46 | return this._frame; 47 | } 48 | 49 | get payload(): ZiGateObjectPayload { 50 | return this._payload; 51 | } 52 | 53 | get command(): ZiGateCommandType { 54 | return ZiGateCommand[this._code]; 55 | } 56 | 57 | public static createRequest(commandCode: ZiGateCommandCode, payload: ZiGateObjectPayload): ZiGateObject { 58 | const cmd = ZiGateCommand[commandCode]; 59 | 60 | if (!cmd) { 61 | throw new Error(`Command '${commandCode}' not found`); 62 | } 63 | 64 | return new ZiGateObject(commandCode, payload, cmd.request); 65 | } 66 | 67 | public static fromZiGateFrame(frame: ZiGateFrame): ZiGateObject { 68 | const code = frame.readMsgCode(); 69 | return ZiGateObject.fromBuffer(code, frame.msgPayloadBytes, frame); 70 | } 71 | 72 | public static fromBuffer(code: number, buffer: Buffer, frame: ZiGateFrame): ZiGateObject { 73 | const msg = ZiGateMessage[code]; 74 | 75 | if (!msg) { 76 | throw new Error(`Message '${code.toString(16)}' not found`); 77 | } 78 | 79 | const parameters = msg.response; 80 | if (parameters === undefined) { 81 | throw new Error(`Message '${code.toString(16)}' cannot be a response`); 82 | } 83 | 84 | const payload = ZiGateObject.readParameters(buffer, parameters); 85 | 86 | return new ZiGateObject(code, payload, parameters, frame); 87 | } 88 | 89 | private static readParameters(buffer: Buffer, parameters: ZiGateParameter[]): ZiGateObjectPayload { 90 | const buffalo = new BuffaloZiGate(buffer); 91 | const result: ZiGateObjectPayload = {}; 92 | 93 | for (const parameter of parameters) { 94 | const options: BuffaloZiGateOptions = {}; 95 | 96 | if (BufferAndListTypes.includes(parameter.parameterType)) { 97 | // When reading a buffer, assume that the previous parsed parameter contains 98 | // the length of the buffer 99 | const lengthParameter = parameters[parameters.indexOf(parameter) - 1]; 100 | const length = result[lengthParameter.name]; 101 | 102 | if (typeof length === "number") { 103 | options.length = length; 104 | } 105 | } 106 | 107 | try { 108 | result[parameter.name] = buffalo.read(parameter.parameterType, options); 109 | } catch (error) { 110 | // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` 111 | logger.error((error as Error).stack!, NS); 112 | } 113 | } 114 | 115 | if (buffalo.isMore()) { 116 | const bufferString = buffalo.getBuffer().toString("hex"); 117 | logger.debug( 118 | `Last bytes of data were not parsed \x1b[32m${bufferString.slice(0, buffalo.getPosition() * 2).replace(/../g, "$& ")}` + 119 | `\x1b[31m${bufferString.slice(buffalo.getPosition() * 2).replace(/../g, "$& ")}\x1b[0m `, 120 | NS, 121 | ); 122 | } 123 | 124 | return result; 125 | } 126 | 127 | public toZiGateFrame(): ZiGateFrame { 128 | const buffer = this.createPayloadBuffer(); 129 | const frame = new ZiGateFrame(); 130 | frame.writeMsgCode(this._code as number); 131 | frame.writeMsgPayload(buffer); 132 | return frame; 133 | } 134 | 135 | private createPayloadBuffer(): Buffer { 136 | const buffalo = new BuffaloZiGate(Buffer.alloc(256)); // hardcode @todo 137 | 138 | for (const parameter of this._parameters) { 139 | const value = this._payload[parameter.name]; 140 | buffalo.write(parameter.parameterType, value, {}); 141 | } 142 | return buffalo.getWritten(); 143 | } 144 | } 145 | 146 | export default ZiGateObject; 147 | -------------------------------------------------------------------------------- /src/adapter/zoh/adapter/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param value 64-bit bigint 3 | * @returns 16-length hex string in big-endian 4 | */ 5 | export function bigUInt64ToHexBE(value: bigint): string { 6 | return value.toString(16).padStart(16, "0"); 7 | } 8 | 9 | /** 10 | * @param value 64-bit bigint 11 | * @returns 8-bytelength buffer in little-endian 12 | */ 13 | export function bigUInt64ToBufferLE(value: bigint): Buffer { 14 | const b = Buffer.alloc(8); 15 | b.writeBigUInt64LE(value, 0); 16 | return b; 17 | } 18 | 19 | /** 20 | * @param value 64-bit bigint 21 | * @returns 8-bytelength buffer in big-endian 22 | */ 23 | export function bigUInt64ToBufferBE(value: bigint): Buffer { 24 | const b = Buffer.alloc(8); 25 | b.writeBigUInt64BE(value, 0); 26 | return b; 27 | } 28 | -------------------------------------------------------------------------------- /src/buffalo/index.ts: -------------------------------------------------------------------------------- 1 | export {Buffalo} from "./buffalo"; 2 | -------------------------------------------------------------------------------- /src/controller/database.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | import {logger} from "../utils/logger"; 4 | import type {DatabaseEntry, EntityType} from "./tstype"; 5 | 6 | const NS = "zh:controller:database"; 7 | 8 | export class Database { 9 | private entries: {[id: number]: DatabaseEntry}; 10 | private path: string; 11 | private maxId: number; 12 | 13 | private constructor(entries: {[id: number]: DatabaseEntry}, path: string) { 14 | this.entries = entries; 15 | this.maxId = Math.max(...Object.keys(entries).map((t) => Number(t)), 0); 16 | this.path = path; 17 | } 18 | 19 | public static open(path: string): Database { 20 | const entries: {[id: number]: DatabaseEntry} = {}; 21 | 22 | if (fs.existsSync(path)) { 23 | const file = fs.readFileSync(path, "utf-8"); 24 | 25 | for (const row of file.split("\n")) { 26 | if (!row) { 27 | continue; 28 | } 29 | 30 | try { 31 | const json = JSON.parse(row); 32 | 33 | if (json.id != null) { 34 | entries[json.id] = json; 35 | } 36 | } catch (error) { 37 | logger.error(`Corrupted database line, ignoring. ${error}`, NS); 38 | } 39 | } 40 | } 41 | 42 | return new Database(entries, path); 43 | } 44 | 45 | public *getEntriesIterator(type: EntityType[]): Generator { 46 | for (const id in this.entries) { 47 | const entry = this.entries[id]; 48 | 49 | if (type.includes(entry.type)) { 50 | yield entry; 51 | } 52 | } 53 | } 54 | 55 | public insert(databaseEntry: DatabaseEntry): void { 56 | if (this.entries[databaseEntry.id]) { 57 | throw new Error(`DatabaseEntry with ID '${databaseEntry.id}' already exists`); 58 | } 59 | 60 | this.entries[databaseEntry.id] = databaseEntry; 61 | this.write(); 62 | } 63 | 64 | public update(databaseEntry: DatabaseEntry, write: boolean): void { 65 | if (!this.entries[databaseEntry.id]) { 66 | throw new Error(`DatabaseEntry with ID '${databaseEntry.id}' does not exist`); 67 | } 68 | 69 | this.entries[databaseEntry.id] = databaseEntry; 70 | 71 | if (write) { 72 | this.write(); 73 | } 74 | } 75 | 76 | public remove(id: number): void { 77 | if (!this.entries[id]) { 78 | throw new Error(`DatabaseEntry with ID '${id}' does not exist`); 79 | } 80 | 81 | delete this.entries[id]; 82 | this.write(); 83 | } 84 | 85 | public has(id: number): boolean { 86 | return Boolean(this.entries[id]); 87 | } 88 | 89 | public newID(): number { 90 | this.maxId += 1; 91 | return this.maxId; 92 | } 93 | 94 | public write(): void { 95 | logger.debug(`Writing database to '${this.path}'`, NS); 96 | let lines = ""; 97 | 98 | for (const id in this.entries) { 99 | lines += `${JSON.stringify(this.entries[id])}\n`; 100 | } 101 | 102 | const tmpPath = `${this.path}.tmp`; 103 | 104 | try { 105 | // If there already exsits a database.db.tmp, rename it to database.db.tmp. 106 | const dateTmpPath = `${tmpPath}.${new Date().toISOString().replaceAll(":", "-")}`; 107 | fs.renameSync(tmpPath, dateTmpPath); 108 | 109 | // If we got this far, we succeeded! Warn the user about this 110 | logger.warning(`Found '${tmpPath}' when writing database, indicating past write failure; renamed it to '${dateTmpPath}'`, NS); 111 | } catch { 112 | // Nothing to catch; if the renameSync fails, we ignore that exception 113 | } 114 | 115 | const fd = fs.openSync(tmpPath, "w"); 116 | fs.writeFileSync(fd, lines.slice(0, -1)); // remove last newline, no effect if empty string 117 | // Ensure file is on disk https://github.com/Koenkk/zigbee2mqtt/issues/11759 118 | fs.fsyncSync(fd); 119 | fs.closeSync(fd); 120 | fs.renameSync(tmpPath, this.path); 121 | } 122 | } 123 | 124 | export default Database; 125 | -------------------------------------------------------------------------------- /src/controller/events.ts: -------------------------------------------------------------------------------- 1 | import type {FrameControl} from "../zspec/zcl/definition/tstype"; 2 | import type {Device, Endpoint} from "./model"; 3 | import type {KeyValue} from "./tstype"; 4 | 5 | export interface DeviceJoinedPayload { 6 | device: Device; 7 | } 8 | 9 | export interface DeviceInterviewPayload { 10 | status: "started" | "successful" | "failed"; 11 | device: Device; 12 | } 13 | 14 | export interface DeviceNetworkAddressChangedPayload { 15 | device: Device; 16 | } 17 | 18 | export interface DeviceAnnouncePayload { 19 | device: Device; 20 | } 21 | 22 | export interface DeviceLeavePayload { 23 | ieeeAddr: string; 24 | } 25 | 26 | export interface PermitJoinChangedPayload { 27 | permitted: boolean; 28 | time?: number; 29 | } 30 | 31 | export interface LastSeenChangedPayload { 32 | device: Device; 33 | reason: "deviceAnnounce" | "networkAddress" | "deviceJoined" | "messageEmitted" | "messageNonEmitted"; 34 | } 35 | 36 | export type MessagePayloadType = "attributeReport" | "readResponse" | "raw" | "read" | "write" | `command${string}`; 37 | 38 | export interface MessagePayload { 39 | type: MessagePayloadType; 40 | device: Device; 41 | endpoint: Endpoint; 42 | linkquality: number; 43 | groupID: number; 44 | cluster: string | number; 45 | data: KeyValue | Array; 46 | meta: { 47 | zclTransactionSequenceNumber?: number; 48 | manufacturerCode?: number; 49 | frameControl?: FrameControl; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/controller/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * as ZclFrameConverter from "./zclFrameConverter"; 2 | -------------------------------------------------------------------------------- /src/controller/helpers/request.ts: -------------------------------------------------------------------------------- 1 | import type * as Zcl from "../../zspec/zcl"; 2 | import type {SendPolicy} from "../tstype"; 3 | 4 | // biome-ignore lint/suspicious/noExplicitAny: API 5 | export class Request { 6 | static defaultSendPolicy: {[key: number]: SendPolicy} = { 7 | 0: "keep-payload", // Read Attributes 8 | 1: "immediate", // Read Attributes Response 9 | 2: "keep-command", // Write Attributes 10 | 3: "keep-cmd-undiv", // Write Attributes Undivided 11 | 4: "immediate", // Write Attributes Response 12 | 5: "keep-command", // Write Attributes No Response 13 | 6: "keep-payload", // Configure Reporting 14 | 7: "immediate", // Configure Reporting Response 15 | 8: "keep-payload", // Read Reporting Configuration 16 | 9: "immediate", // Read Reporting Configuration Response 17 | 10: "keep-payload", // Report attributes 18 | 11: "immediate", // Default Response 19 | 12: "keep-payload", // Discover Attributes 20 | 13: "immediate", // Discover Attributes Response 21 | 14: "keep-payload", // Read Attributes Structured 22 | 15: "keep-payload", // Write Attributes Structured 23 | 16: "immediate", // Write Attributes Structured response 24 | 17: "keep-payload", // Discover Commands Received 25 | 18: "immediate", // Discover Commands Received Response 26 | 19: "keep-payload", // Discover Commands Generated 27 | 20: "immediate", // Discover Commands Generated Response 28 | 21: "keep-payload", // Discover Attributes Extended 29 | 22: "immediate", // Discover Attributes Extended Response 30 | }; 31 | 32 | private func: (frame: Zcl.Frame) => Promise; 33 | frame: Zcl.Frame; 34 | expires: number; 35 | sendPolicy: SendPolicy | undefined; 36 | private resolveQueue: Array<(value: Type) => void>; 37 | private rejectQueue: Array<(error: Error) => void>; 38 | private lastError: Error; 39 | 40 | constructor( 41 | func: (frame: Zcl.Frame) => Promise, 42 | frame: Zcl.Frame, 43 | timeout: number, 44 | sendPolicy?: SendPolicy, 45 | lastError?: Error, 46 | resolve?: (value: Type) => void, 47 | reject?: (error: Error) => void, 48 | ) { 49 | this.func = func; 50 | this.frame = frame; 51 | this.expires = timeout + Date.now(); 52 | this.sendPolicy = sendPolicy ?? (!frame.command ? undefined : Request.defaultSendPolicy[frame.command.ID]); 53 | this.resolveQueue = resolve === undefined ? new Array<(value: Type) => void>() : new Array<(value: Type) => void>(resolve); 54 | this.rejectQueue = reject === undefined ? new Array<(error: Error) => void>() : new Array<(error: Error) => void>(reject); 55 | this.lastError = lastError ?? Error("Request rejected before first send"); 56 | } 57 | 58 | moveCallbacks(from: Request): void { 59 | this.resolveQueue = this.resolveQueue.concat(from.resolveQueue); 60 | this.rejectQueue = this.rejectQueue.concat(from.rejectQueue); 61 | from.resolveQueue.length = 0; 62 | from.rejectQueue.length = 0; 63 | } 64 | 65 | addCallbacks(resolve: (value: Type) => void, reject: (error: Error) => void): void { 66 | this.resolveQueue.push(resolve); 67 | this.rejectQueue.push(reject); 68 | } 69 | 70 | reject(error?: Error): void { 71 | for (const el of this.rejectQueue) { 72 | el(error ?? this.lastError); 73 | } 74 | this.rejectQueue.length = 0; 75 | } 76 | 77 | resolve(value: Type): void { 78 | for (const el of this.resolveQueue) { 79 | el(value); 80 | } 81 | this.resolveQueue.length = 0; 82 | } 83 | 84 | async send(): Promise { 85 | try { 86 | return await this.func(this.frame); 87 | } catch (error) { 88 | this.lastError = error as Error; 89 | throw error; 90 | } 91 | } 92 | } 93 | 94 | export default Request; 95 | -------------------------------------------------------------------------------- /src/controller/helpers/zclFrameConverter.ts: -------------------------------------------------------------------------------- 1 | import * as Zcl from "../../zspec/zcl"; 2 | import type {Cluster, CustomClusters} from "../../zspec/zcl/definition/tstype"; 3 | 4 | interface KeyValue { 5 | [s: string]: number | string; 6 | } 7 | 8 | // Legrand devices (e.g. 4129) fail to set the manufacturerSpecific flag and 9 | // manufacturerCode in the frame header, despite using specific attributes. 10 | // This leads to incorrect reported attribute names. 11 | // Remap the attributes using the target device's manufacturer ID 12 | // if the header is lacking the information. 13 | function getCluster(frame: Zcl.Frame, deviceManufacturerID: number | undefined, customClusters: CustomClusters): Cluster { 14 | let cluster = frame.cluster; 15 | if (!frame?.header?.manufacturerCode && frame?.cluster && deviceManufacturerID === Zcl.ManufacturerCode.LEGRAND_GROUP) { 16 | cluster = Zcl.Utils.getCluster(frame.cluster.ID, deviceManufacturerID, customClusters); 17 | } 18 | return cluster; 19 | } 20 | 21 | function attributeKeyValue(frame: Zcl.Frame, deviceManufacturerID: number | undefined, customClusters: CustomClusters): KeyValue { 22 | const payload: KeyValue = {}; 23 | const cluster = getCluster(frame, deviceManufacturerID, customClusters); 24 | 25 | for (const item of frame.payload) { 26 | try { 27 | const attribute = cluster.getAttribute(item.attrId); 28 | payload[attribute.name] = item.attrData; 29 | } catch { 30 | payload[item.attrId] = item.attrData; 31 | } 32 | } 33 | return payload; 34 | } 35 | 36 | function attributeList(frame: Zcl.Frame, deviceManufacturerID: number | undefined, customClusters: CustomClusters): Array { 37 | const payload: Array = []; 38 | const cluster = getCluster(frame, deviceManufacturerID, customClusters); 39 | 40 | for (const item of frame.payload) { 41 | try { 42 | const attribute = cluster.getAttribute(item.attrId); 43 | payload.push(attribute.name); 44 | } catch { 45 | payload.push(item.attrId); 46 | } 47 | } 48 | return payload; 49 | } 50 | 51 | export {attributeKeyValue, attributeList}; 52 | -------------------------------------------------------------------------------- /src/controller/helpers/zclTransactionSequenceNumber.ts: -------------------------------------------------------------------------------- 1 | class ZclTransactionSequenceNumber { 2 | private number = 1; 3 | 4 | get current() { 5 | return this.number; 6 | } 7 | 8 | public next(): number { 9 | this.number++; 10 | 11 | if (this.number > 255) { 12 | this.number = 1; 13 | } 14 | 15 | return this.number; 16 | } 17 | } 18 | 19 | export default new ZclTransactionSequenceNumber(); 20 | -------------------------------------------------------------------------------- /src/controller/index.ts: -------------------------------------------------------------------------------- 1 | import Controller from "./controller"; 2 | 3 | /** 4 | * @internal 5 | */ 6 | export {Controller}; 7 | -------------------------------------------------------------------------------- /src/controller/model/entity.ts: -------------------------------------------------------------------------------- 1 | import events from "node:events"; 2 | 3 | import type {Adapter} from "../../adapter"; 4 | import type Database from "../database"; 5 | 6 | // biome-ignore lint/suspicious/noExplicitAny: API 7 | type EventMap = Record | DefaultEventMap; 8 | type DefaultEventMap = [never]; 9 | 10 | export abstract class Entity = DefaultEventMap> extends events.EventEmitter { 11 | protected static database?: Database; 12 | protected static adapter?: Adapter; 13 | 14 | public static injectDatabase(database: Database): void { 15 | Entity.database = database; 16 | } 17 | 18 | public static injectAdapter(adapter: Adapter): void { 19 | Entity.adapter = adapter; 20 | } 21 | } 22 | 23 | export default Entity; 24 | -------------------------------------------------------------------------------- /src/controller/model/index.ts: -------------------------------------------------------------------------------- 1 | export {Device} from "./device"; 2 | export {Endpoint} from "./endpoint"; 3 | export {Entity} from "./entity"; 4 | export {Group} from "./group"; 5 | -------------------------------------------------------------------------------- /src/controller/tstype.ts: -------------------------------------------------------------------------------- 1 | export interface KeyValue { 2 | // biome-ignore lint/suspicious/noExplicitAny: API 3 | [s: string]: any; 4 | } 5 | 6 | /* Send request policies: 7 | 'bulk': Message must be sent together with other messages in the correct sequence. 8 | No immediate delivery required. 9 | 'queue': Request shall be sent 'as-is' as soon as possible. 10 | Multiple identical requests shall be delivered multiple times. 11 | Not strict ordering required. 12 | 'immediate': Request shall be sent immediately and not be kept for later retries (e.g. response message). 13 | 'keep-payload': Request shall be sent as soon as possible. 14 | If immediate delivery fails, the exact same payload is only sent once, even if there were 15 | multiple requests. 16 | 'keep-command': Request shall be sent as soon as possible. 17 | If immediate delivery fails, only the latest command for each command ID is kept for delivery. 18 | 'keep-cmd-undiv': Request shall be sent as soon as possible. 19 | If immediate delivery fails, only the latest undivided set of commands is sent for each unique 20 | set of command IDs. 21 | */ 22 | export type SendPolicy = "bulk" | "queue" | "immediate" | "keep-payload" | "keep-command" | "keep-cmd-undiv"; 23 | export type DeviceType = "Coordinator" | "Router" | "EndDevice" | "Unknown" | "GreenPower"; 24 | 25 | export type EntityType = DeviceType | "Group"; 26 | 27 | export interface DatabaseEntry { 28 | id: number; 29 | type: EntityType; 30 | // biome-ignore lint/suspicious/noExplicitAny: API 31 | [s: string]: any; 32 | } 33 | 34 | export interface GreenPowerDeviceJoinedPayload { 35 | sourceID: number; 36 | deviceID: number; 37 | networkAddress: number; 38 | securityKey?: Buffer; 39 | } 40 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | 3 | export {Controller} from "./controller/controller"; 4 | export type * as Events from "./controller/events"; 5 | export type * as Types from "./controller/tstype"; 6 | export type * as Models from "./controller/model"; 7 | export type * as AdapterEvents from "./adapter/events"; 8 | export type * as AdapterTypes from "./adapter/tstype"; 9 | export * as Zcl from "./zspec/zcl"; 10 | export * as Zdo from "./zspec/zdo"; 11 | export * as ZSpec from "./zspec"; 12 | export {setLogger} from "./utils/logger"; 13 | -------------------------------------------------------------------------------- /src/models/backup-storage-legacy.ts: -------------------------------------------------------------------------------- 1 | import type {ZnpVersion} from "../adapter/z-stack/adapter/tstype"; 2 | import type {NvSystemIds} from "../adapter/z-stack/constants/common"; 3 | 4 | type LegacyNvItemKey = 5 | | "ZCD_NV_EXTADDR" 6 | | "ZCD_NV_NIB" 7 | | "ZCD_NV_PANID" 8 | | "ZCD_NV_EXTENDED_PAN_ID" 9 | | "ZCD_NV_NWK_ACTIVE_KEY_INFO" 10 | | "ZCD_NV_NWK_ALTERN_KEY_INFO" 11 | | "ZCD_NV_APS_USE_EXT_PANID" 12 | | "ZCD_NV_PRECFGKEY" 13 | | "ZCD_NV_PRECFGKEY_ENABLE" 14 | | "ZCD_NV_CHANLIST" 15 | 16 | /* Z-Stack 3.0.x tables */ 17 | | "ZCD_NV_LEGACY_TCLK_TABLE_START" 18 | | "ZCD_NV_LEGACY_NWK_SEC_MATERIAL_TABLE_START" 19 | 20 | /* SimpleLink Z-Stack 3.x.0 tables */ 21 | | "ZCD_NV_EX_TCLK_TABLE" 22 | | "ZCD_NV_EX_NWK_SEC_MATERIAL_TABLE"; 23 | 24 | /** 25 | * Legacy backup format to allow for backup migration. 26 | */ 27 | export interface LegacyBackupStorage { 28 | adapterType: "zStack"; 29 | time: string; 30 | meta: { 31 | product: ZnpVersion; 32 | }; 33 | data: Record< 34 | LegacyNvItemKey, 35 | { 36 | id: number; 37 | product: ZnpVersion; 38 | offset: number; 39 | osal: boolean; 40 | value: number[]; 41 | len: number; 42 | 43 | /* System ID and Sub ID used in SimpleLink Z-Stack 3.x.0 */ 44 | sysid?: NvSystemIds; 45 | subid?: number; 46 | } 47 | >; 48 | } 49 | -------------------------------------------------------------------------------- /src/models/backup-storage-unified.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Unified configuration storage model based on 3 | * [zigpy/open-coordinator-backup](https://github.com/zigpy/open-coordinator-backup). 4 | * 5 | * This format should allow for seamless migration between adapter types or event vendors. 6 | */ 7 | export interface UnifiedBackupStorage { 8 | metadata: { 9 | format: "zigpy/open-coordinator-backup"; 10 | version: 1; 11 | source: string; 12 | internal: { 13 | /* zigbee-herdsman specific data */ 14 | date: string; 15 | znpVersion?: number; 16 | ezspVersion?: number; 17 | 18 | [key: string]: unknown; 19 | }; 20 | }; 21 | stack_specific?: { 22 | zstack?: { 23 | tclk_seed?: string; 24 | }; 25 | ezsp?: { 26 | hashed_tclk?: string; 27 | }; 28 | }; 29 | coordinator_ieee: string; 30 | pan_id: string; 31 | extended_pan_id: string; 32 | security_level: number; 33 | nwk_update_id: number; 34 | channel: number; 35 | channel_mask: number[]; 36 | network_key: { 37 | key: string; 38 | sequence_number: number; 39 | frame_counter: number; 40 | }; 41 | devices: { 42 | nwk_address: string | null; 43 | ieee_address: string; 44 | is_child: boolean; 45 | link_key: {key: string; rx_counter: number; tx_counter: number} | undefined; 46 | }[]; 47 | } 48 | -------------------------------------------------------------------------------- /src/models/backup.ts: -------------------------------------------------------------------------------- 1 | import type {ZnpVersion} from "../adapter/z-stack/adapter/tstype"; 2 | import type {NetworkOptions} from "./network-options"; 3 | 4 | /** 5 | * Internal representation of stored backup. Contains all essential network information. 6 | * 7 | * Additional `znp` object may contain extra information specific to Z-Stack based coordinators. 8 | */ 9 | export interface Backup { 10 | networkOptions: NetworkOptions; 11 | logicalChannel: number; 12 | networkKeyInfo: { 13 | sequenceNumber: number; 14 | frameCounter: number; 15 | }; 16 | securityLevel: number; 17 | networkUpdateId: number; 18 | coordinatorIeeeAddress: Buffer; 19 | devices: { 20 | networkAddress: number | null; 21 | ieeeAddress: Buffer; 22 | isDirectChild: boolean; 23 | linkKey?: { 24 | key: Buffer; 25 | rxCounter: number; 26 | txCounter: number; 27 | }; 28 | }[]; 29 | znp?: { 30 | version?: ZnpVersion; 31 | trustCenterLinkKeySeed?: Buffer; 32 | }; 33 | ezsp?: { 34 | version?: number; 35 | hashed_tclk?: Buffer; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore start */ 2 | export * from "./backup"; 3 | export * from "./backup-storage-legacy"; 4 | export * from "./backup-storage-unified"; 5 | export * from "./network-options"; 6 | -------------------------------------------------------------------------------- /src/models/network-options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Buffer-oriented structure representing network configuration. 3 | */ 4 | export interface NetworkOptions { 5 | panId: number; 6 | extendedPanId: Buffer; 7 | channelList: number[]; 8 | networkKey: Buffer; 9 | networkKeyDistribute: boolean; 10 | hasDefaultExtendedPanId?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * as BackupUtils from "./backup"; 2 | export {Queue} from "./queue"; 3 | export * as Utils from "./utils"; 4 | export {wait} from "./wait"; 5 | export {Waitress} from "./waitress"; 6 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | debug: (messageOrLambda: string | (() => string), namespace: string) => void; 3 | info: (messageOrLambda: string | (() => string), namespace: string) => void; 4 | warning: (messageOrLambda: string | (() => string), namespace: string) => void; 5 | error: (messageOrLambda: string, namespace: string) => void; 6 | } 7 | 8 | export let logger: Logger = { 9 | debug: (messageOrLambda, namespace) => 10 | console.debug(`[${new Date().toISOString()}] ${namespace}: ${typeof messageOrLambda === "function" ? messageOrLambda() : messageOrLambda}`), 11 | info: (messageOrLambda, namespace) => 12 | console.info(`[${new Date().toISOString()}] ${namespace}: ${typeof messageOrLambda === "function" ? messageOrLambda() : messageOrLambda}`), 13 | warning: (messageOrLambda, namespace) => 14 | console.warn(`[${new Date().toISOString()}] ${namespace}: ${typeof messageOrLambda === "function" ? messageOrLambda() : messageOrLambda}`), 15 | error: (message, namespace) => console.error(`[${new Date().toISOString()}] ${namespace}: ${message}`), 16 | }; 17 | 18 | export function setLogger(l: Logger): void { 19 | logger = l; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/patchBigIntSerialization.ts: -------------------------------------------------------------------------------- 1 | // Patch BigInt serialization which is used in e.g. Zcl payloads. 2 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json 3 | // biome-ignore lint/suspicious/noExplicitAny: API 4 | (BigInt.prototype as any).toJSON = function (): string { 5 | return this.toString(); 6 | }; 7 | 8 | export {}; 9 | -------------------------------------------------------------------------------- /src/utils/queue.ts: -------------------------------------------------------------------------------- 1 | interface Job { 2 | key?: string | number; 3 | running: boolean; 4 | start?: () => void; 5 | } 6 | 7 | export class Queue { 8 | private jobs: Job[]; 9 | private readonly concurrent: number; 10 | 11 | constructor(concurrent = 1) { 12 | this.jobs = []; 13 | this.concurrent = concurrent; 14 | } 15 | 16 | public async execute(func: () => Promise, key?: string | number): Promise { 17 | const job: Job = {key, running: false}; 18 | this.jobs.push(job); 19 | 20 | // Minor optimization/workaround: various tests like the idea that a job that is immediately runnable is run without an event loop spin. 21 | // This also helps with stack traces in some cases, so avoid an `await` if we can help it. 22 | if (this.getNext() !== job) { 23 | await new Promise((resolve): void => { 24 | job.start = (): void => { 25 | job.running = true; 26 | resolve(); 27 | }; 28 | 29 | this.executeNext(); 30 | }); 31 | } else { 32 | job.running = true; 33 | } 34 | 35 | try { 36 | return await func(); 37 | } finally { 38 | this.jobs.splice(this.jobs.indexOf(job), 1); 39 | this.executeNext(); 40 | } 41 | } 42 | 43 | private executeNext(): void { 44 | const job = this.getNext(); 45 | 46 | if (job) { 47 | // if we get here, start is always defined for job 48 | // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` 49 | job.start!(); 50 | } 51 | } 52 | 53 | private getNext(): Job | undefined { 54 | if (this.jobs.filter((j) => j.running).length > this.concurrent - 1) { 55 | return undefined; 56 | } 57 | 58 | for (let i = 0; i < this.jobs.length; i++) { 59 | const job = this.jobs[i]; 60 | 61 | if (!job.running && (!job.key || !this.jobs.find((j) => j.key === job.key && j.running))) { 62 | return job; 63 | } 64 | } 65 | 66 | return undefined; 67 | } 68 | 69 | public clear(): void { 70 | this.jobs = []; 71 | } 72 | 73 | public count(): number { 74 | return this.jobs.length; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "mixin-deep" { 2 | export default function mixinDeep(target: T, ...rest: object[]): T; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function isNumberArray(value: unknown): value is number[] { 2 | return Array.isArray(value) && value.every((item) => typeof item === "number"); 3 | } 4 | 5 | export function isNumberArrayOfLength(value: unknown, length: number): boolean { 6 | return isNumberArray(value) && value.length === length; 7 | } 8 | 9 | export function assertString(input: unknown): asserts input is string { 10 | if (typeof input !== "string") { 11 | throw new Error("Input must be a string!"); 12 | } 13 | } 14 | 15 | export function isObjectEmpty(object: object): boolean { 16 | // much faster than checking `Object.keys(object).length` 17 | // biome-ignore lint/style/useNamingConvention: not working properly 18 | for (const _k in object) return false; 19 | return true; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export function wait(milliseconds: number): Promise { 2 | return new Promise((resolve): void => { 3 | setTimeout((): void => resolve(), milliseconds); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/waitress.ts: -------------------------------------------------------------------------------- 1 | interface Waiter { 2 | ID: number; 3 | resolve: (payload: TPayload) => void; 4 | reject: (error: Error) => void; 5 | timer?: NodeJS.Timeout; 6 | resolved: boolean; 7 | timedout: boolean; 8 | matcher: TMatcher; 9 | } 10 | 11 | type Validator = (payload: TPayload, matcher: TMatcher) => boolean; 12 | type TimeoutFormatter = (matcher: TMatcher, timeout: number) => string; 13 | 14 | export class Waitress { 15 | private waiters: Map>; 16 | private readonly validator: Validator; 17 | private readonly timeoutFormatter: TimeoutFormatter; 18 | private currentID: number; 19 | 20 | public constructor(validator: Validator, timeoutFormatter: TimeoutFormatter) { 21 | this.waiters = new Map(); 22 | this.timeoutFormatter = timeoutFormatter; 23 | this.validator = validator; 24 | this.currentID = 0; 25 | } 26 | 27 | public clear(): void { 28 | for (const [, waiter] of this.waiters) { 29 | clearTimeout(waiter.timer); 30 | } 31 | 32 | this.waiters.clear(); 33 | } 34 | 35 | public resolve(payload: TPayload): boolean { 36 | return this.forEachMatching(payload, (waiter) => waiter.resolve(payload)); 37 | } 38 | 39 | public reject(payload: TPayload, message: string): boolean { 40 | return this.forEachMatching(payload, (waiter) => waiter.reject(new Error(message))); 41 | } 42 | 43 | public remove(id: number): void { 44 | const waiter = this.waiters.get(id); 45 | if (waiter) { 46 | if (!waiter.timedout && waiter.timer) { 47 | clearTimeout(waiter.timer); 48 | } 49 | 50 | this.waiters.delete(id); 51 | } 52 | } 53 | 54 | public waitFor(matcher: TMatcher, timeout: number): {ID: number; start: () => {promise: Promise; ID: number}} { 55 | const ID = this.currentID++; 56 | 57 | const promise: Promise = new Promise((resolve, reject): void => { 58 | const object: Waiter = {matcher, resolve, reject, timedout: false, resolved: false, ID}; 59 | this.waiters.set(ID, object); 60 | }); 61 | 62 | const start = (): {promise: Promise; ID: number} => { 63 | const waiter = this.waiters.get(ID); 64 | if (waiter && !waiter.resolved && !waiter.timer) { 65 | // Capture the stack trace from the caller of start() 66 | const error = new Error(this.timeoutFormatter(matcher, timeout)); 67 | Error.captureStackTrace(error); 68 | waiter.timer = setTimeout((): void => { 69 | waiter.timedout = true; 70 | waiter.reject(error); 71 | }, timeout); 72 | } 73 | 74 | return {promise, ID}; 75 | }; 76 | 77 | return {ID, start}; 78 | } 79 | 80 | private forEachMatching(payload: TPayload, action: (waiter: Waiter) => void): boolean { 81 | let foundMatching = false; 82 | for (const [index, waiter] of this.waiters.entries()) { 83 | if (waiter.timedout) { 84 | this.waiters.delete(index); 85 | } else if (this.validator(payload, waiter.matcher)) { 86 | clearTimeout(waiter.timer); 87 | waiter.resolved = true; 88 | this.waiters.delete(index); 89 | action(waiter); 90 | foundMatching = true; 91 | } 92 | } 93 | return foundMatching; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/zspec/consts.ts: -------------------------------------------------------------------------------- 1 | import type {ExtendedPanId} from "./tstypes"; 2 | 3 | /** Current supported Zigbee revision: https://csa-iot.org/wp-content/uploads/2023/04/05-3474-23-csg-zigbee-specification-compressed.pdf */ 4 | export const ZIGBEE_REVISION = 23; 5 | 6 | /** The network ID of the coordinator in a ZigBee network is 0x0000. */ 7 | export const COORDINATOR_ADDRESS = 0x0000; 8 | 9 | /** Endpoint profile ID for Zigbee 3.0. "Home Automation" */ 10 | export const HA_PROFILE_ID = 0x0104; 11 | /** Endpoint profile ID for Smart Energy */ 12 | export const SE_PROFILE_ID = 0x0109; 13 | /** Endpoint profile ID for Green Power */ 14 | export const GP_PROFILE_ID = 0xa1e0; 15 | /** The touchlink (ZigBee Light Link/ZLL) Profile ID. */ 16 | export const TOUCHLINK_PROFILE_ID = 0xc05e; 17 | /** The profile ID used to address all the public profiles. */ 18 | export const WILDCARD_PROFILE_ID = 0xffff; 19 | 20 | /** The default HA endpoint. */ 21 | export const HA_ENDPOINT = 0x01; 22 | /** The GP endpoint, as defined in the ZigBee spec. */ 23 | export const GP_ENDPOINT = 0xf2; 24 | 25 | export const GP_GROUP_ID = 0x0b84; 26 | 27 | /** The maximum 802.15.4 channel number is 26. */ 28 | export const MAX_802_15_4_CHANNEL_NUMBER = 26; 29 | /** The minimum 2.4GHz 802.15.4 channel number is 11. */ 30 | export const MIN_802_15_4_CHANNEL_NUMBER = 11; 31 | /** There are sixteen 802.15.4 channels. */ 32 | export const NUM_802_15_4_CHANNELS = MAX_802_15_4_CHANNEL_NUMBER - MIN_802_15_4_CHANNEL_NUMBER + 1; 33 | /** A bitmask to scan all 2.4 GHz 802.15.4 channels. */ 34 | export const ALL_802_15_4_CHANNELS_MASK = 0x07fff800; 35 | /** A bitmask of the preferred 2.4 GHz 802.15.4 channels to scan. */ 36 | export const PREFERRED_802_15_4_CHANNELS_MASK = 0x0318c800; 37 | /** List of all Zigbee channels */ 38 | export const ALL_802_15_4_CHANNELS = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]; 39 | /** List of preferred Zigbee channels */ 40 | export const PREFERRED_802_15_4_CHANNELS = [11, 14, 15, 19, 20, 24, 25]; 41 | 42 | /** A blank (also used as "wildcard") EUI64 hex string prefixed with 0x */ 43 | export const BLANK_EUI64 = "0xFFFFFFFFFFFFFFFF"; 44 | /** A blank extended PAN ID. (null/not present) */ 45 | export const BLANK_EXTENDED_PAN_ID: Readonly = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; 46 | 47 | /** An invalid profile ID. This is a reserved profileId. */ 48 | export const INVALID_PROFILE_ID = 0xffff; 49 | /** An invalid cluster ID. */ 50 | export const INVALID_CLUSTER_ID = 0xffff; 51 | /** An invalid PAN ID. */ 52 | export const INVALID_PAN_ID = 0xffff; 53 | 54 | /** A distinguished network ID that will never be assigned to any node. It is used to indicate the absence of a node ID. */ 55 | export const NULL_NODE_ID = 0xffff; 56 | /** A distinguished binding index used to indicate the absence of a binding. */ 57 | export const NULL_BINDING = 0xff; 58 | 59 | /** This key is "ZigBeeAlliance09" */ 60 | export const INTEROPERABILITY_LINK_KEY: readonly number[] = [ 61 | 0x5a, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6c, 0x6c, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x30, 0x39, 62 | ]; 63 | 64 | export const PERMIT_JOIN_FOREVER = 0xff; 65 | export const PERMIT_JOIN_MAX_TIMEOUT = 0xfe; 66 | 67 | /** Size of EUI64 (an IEEE address) in bytes. */ 68 | export const EUI64_SIZE = 8; 69 | /** Size of an PAN identifier in bytes. */ 70 | export const PAN_ID_SIZE = 2; 71 | /** Size of an extended PAN identifier in bytes. */ 72 | export const EXTENDED_PAN_ID_SIZE = 8; 73 | /** Size of an encryption key in bytes. */ 74 | export const DEFAULT_ENCRYPTION_KEY_SIZE = 16; 75 | /** Size of a AES-128-MMO (Matyas-Meyer-Oseas) block in bytes. */ 76 | export const AES_MMO_128_BLOCK_SIZE = 16; 77 | /** 78 | * Valid install code sizes, including `INSTALL_CODE_CRC_SIZE`. 79 | * 80 | * NOTE: 18 is now standard, first for iterations, order after is important (8 before 10)! 81 | */ 82 | export const INSTALL_CODE_SIZES: ReadonlyArray = [18, 8, 10, 14]; 83 | /** Size of the CRC appended to install codes. */ 84 | export const INSTALL_CODE_CRC_SIZE = 2; 85 | -------------------------------------------------------------------------------- /src/zspec/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ZigBee Broadcast Addresses 3 | * 4 | * ZigBee specifies three different broadcast addresses that reach different collections of nodes. 5 | * Broadcasts are normally sent only to routers. 6 | * Broadcasts can also be forwarded to end devices, either all of them or only those that do not sleep. 7 | * Broadcasting to end devices is both significantly more resource-intensive and significantly less reliable than broadcasting to routers. 8 | */ 9 | export enum BroadcastAddress { 10 | // Reserved = 0xfff8, 11 | // Reserved = 0xfff9, 12 | // Reserved = 0xfffa, 13 | /** Low power routers only */ 14 | LOW_POWER_ROUTERS = 0xfffb, 15 | /** All routers and coordinator */ 16 | DEFAULT = 0xfffc, 17 | /** macRxOnWhenIdle = TRUE (all non-sleepy devices) */ 18 | RX_ON_WHEN_IDLE = 0xfffd, 19 | // Reserved = 0xFFFE, 20 | /** All devices in PAN (including sleepy end devices) */ 21 | SLEEPY = 0xffff, 22 | } 23 | -------------------------------------------------------------------------------- /src/zspec/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./consts"; 2 | export * from "./enums"; 3 | export * as Utils from "./utils"; 4 | -------------------------------------------------------------------------------- /src/zspec/tstypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * EUI 64-bit ID (IEEE 802.15.4 long address). uint8[EUI64_SIZE] 3 | * 4 | * NOTE: Expected to contain `0x` prefix 5 | */ 6 | export type Eui64 = `0x${string}`; 7 | /** IEEE 802.15.4 node ID. Also known as short address. uint16 */ 8 | export type NodeId = number; 9 | /** IEEE 802.15.4 PAN ID. uint16 */ 10 | export type PanId = number; 11 | /** PAN 64-bit ID (IEEE 802.15.4 long address). uint8[EXTENDED_PAN_ID_SIZE] */ 12 | export type ExtendedPanId = number[]; 13 | /** 16-bit ZigBee multicast group identifier. uint16 */ 14 | export type MulticastId = number; 15 | /** Refer to the Zigbee application profile ID. uint16 */ 16 | export type ProfileId = number; 17 | /** Refer to the ZCL cluster ID. uint16 */ 18 | export type ClusterId = number; 19 | -------------------------------------------------------------------------------- /src/zspec/zcl/definition/consts.ts: -------------------------------------------------------------------------------- 1 | /** Mapping of power source bits to descriptive string. */ 2 | export const POWER_SOURCES: Readonly<{[s: number]: string}> = { 3 | 0: "Unknown", 4 | 1: "Mains (single phase)", 5 | 2: "Mains (3 phase)", 6 | 3: "Battery", 7 | 4: "DC Source", 8 | 5: "Emergency mains constantly powered", 9 | 6: "Emergency mains and transfer switch", 10 | }; 11 | 12 | /** Mapping of device type to ID */ 13 | export const ENDPOINT_DEVICE_TYPE: Readonly<{[s: string]: number}> = { 14 | ZLLOnOffLight: 0x0000, 15 | ZLLOnOffPluginUnit: 0x0010, 16 | ZLLDimmableLight: 0x0100, 17 | ZLLDimmablePluginUnit: 0x0110, 18 | ZLLColorLight: 0x0200, 19 | ZLLExtendedColorLight: 0x0210, 20 | ZLLColorTemperatureLight: 0x0220, 21 | HAOnOffLight: 0x0100, 22 | HADimmableLight: 0x0101, 23 | HAColorLight: 0x0102, 24 | }; 25 | -------------------------------------------------------------------------------- /src/zspec/zcl/definition/status.ts: -------------------------------------------------------------------------------- 1 | export enum Status { 2 | /** Operation was successful. */ 3 | SUCCESS = 0x00, 4 | /** Operation was not successful. */ 5 | FAILURE = 0x01, 6 | /** The sender of the command does not have authorization to carry out this command. */ 7 | NOT_AUTHORIZED = 0x7e, 8 | RESERVED = 0x7f, 9 | /** 10 | * The command appears to contain the wrong fields, as detected either by the presence of one or more invalid 11 | * field entries or by there being missing fields. 12 | * Command not carried out. Implementer has discretion as to whether to return this error or INVALID_FIELD. 13 | */ 14 | MALFORMED_COMMAND = 0x80, 15 | // UNSUP_CLUSTER_COMMAND = 0x81, DEPRECATED in favor of UNSUP_COMMAND 16 | /** The specified command is not supported on the device. Command not carried out. */ 17 | UNSUP_COMMAND = 0x81, 18 | UNSUP_GENERAL_COMMAND = 0x82, // DEPRECATED in favor of UNSUP_COMMAND 19 | UNSUP_MANUF_CLUSTER_COMMAND = 0x83, // DEPRECATED in favor of UNSUP_COMMAND 20 | UNSUP_MANUF_GENERAL_COMMAND = 0x84, // DEPRECATED in favor of UNSUP_COMMAND 21 | /** At least one field of the command contains an incorrect value, according to the specification the device is implemented to. */ 22 | INVALID_FIELD = 0x85, 23 | /** The specified attribute does not exist on the device. */ 24 | UNSUPPORTED_ATTRIBUTE = 0x86, 25 | /** 26 | * Out of range error or set to a reserved value. Attribute keeps its old value. 27 | * Note that an attribute value may be out of range if an attribute is related to another, 28 | * e.g., with minimum and maximum attributes. See the individual attribute descriptions for specific details. 29 | */ 30 | INVALID_VALUE = 0x87, 31 | /** Attempt to write a read-only attribute. */ 32 | READ_ONLY = 0x88, 33 | /** An operation failed due to an insufficient amount of free space available. */ 34 | INSUFFICIENT_SPACE = 0x89, 35 | DUPLICATE_EXISTS = 0x8a, // DEPRECATED in favor of SUCCESS 36 | /** The requested information (e.g., table entry) could not be found. */ 37 | NOT_FOUND = 0x8b, 38 | /** Periodic reports cannot be issued for this attribute.*/ 39 | UNREPORTABLE_ATTRIBUTE = 0x8c, 40 | /** The data type given for an attribute is incorrect. Command not carried out.*/ 41 | INVALID_DATA_TYPE = 0x8d, 42 | /** The selector for an attribute is incorrect. */ 43 | INVALID_SELECTOR = 0x8e, 44 | WRITE_ONLY = 0x8f, // DEPRECATED in favor of NOT_AUTHORIZED 45 | INCONSISTENT_STARTUP_STATE = 0x90, // DEPRECATED in favor of FAILURE 46 | DEFINED_OUT_OF_BAND = 0x91, // DEPRECATED in favor of FAILURE 47 | RESERVED14 = 0x92, 48 | ACTION_DENIED = 0x93, // DEPRECATED in favor of FAILURE 49 | /** The exchange was aborted due to excessive response time. */ 50 | TIMEOUT = 0x94, 51 | /** Failed case when a client or a server decides to abort the upgrade process. */ 52 | ABORT = 0x95, 53 | /** Invalid OTA upgrade image (ex. failed signature validation or signer information check or CRC check). */ 54 | INVALID_IMAGE = 0x96, 55 | /** Server does not have data block available yet. */ 56 | WAIT_FOR_DATA = 0x97, 57 | /** No OTA upgrade image available for the client. */ 58 | NO_IMAGE_AVAILABLE = 0x98, 59 | /** The client still requires more OTA upgrade image files to successfully upgrade. */ 60 | REQUIRE_MORE_IMAGE = 0x99, 61 | /** The command has been received and is being processed. */ 62 | NOTIFICATION_PENDING = 0x9a, 63 | HARDWARE_FAILURE = 0xc0, // DEPRECATED in favor of FAILURE 64 | SOFTWARE_FAILURE = 0xc1, // DEPRECATED in favor of FAILURE 65 | RESERVED15 = 0xc2, 66 | /** The cluster is not supported. */ 67 | UNSUPPORTED_CLUSTER = 0xc3, 68 | LIMIT_REACHED = 0xc4, // DEPRECATED in favor of SUCCESS 69 | } 70 | -------------------------------------------------------------------------------- /src/zspec/zcl/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./definition/consts"; 2 | export * from "./definition/enums"; 3 | export {Clusters} from "./definition/cluster"; 4 | export {Status} from "./definition/status"; 5 | export {Foundation} from "./definition/foundation"; 6 | export {ManufacturerCode} from "./definition/manufacturerCode"; 7 | export {ZclFrame as Frame} from "./zclFrame"; 8 | export {ZclHeader as Header} from "./zclHeader"; 9 | export {ZclStatusError as StatusError} from "./zclStatusError"; 10 | export * as Utils from "./utils"; 11 | -------------------------------------------------------------------------------- /src/zspec/zcl/zclHeader.ts: -------------------------------------------------------------------------------- 1 | import {logger} from "../../utils/logger"; 2 | import {BuffaloZcl} from "./buffaloZcl"; 3 | import {FrameType} from "./definition/enums"; 4 | import type {FrameControl} from "./definition/tstype"; 5 | 6 | const NS = "zh:zcl:header"; 7 | const HEADER_MINIMAL_LENGTH = 3; 8 | const HEADER_WITH_MANUF_LENGTH = HEADER_MINIMAL_LENGTH + 2; 9 | /** ZCL Header frame control frame type */ 10 | const HEADER_CTRL_FRAME_TYPE_MASK = 0x03; 11 | const HEADER_CTRL_FRAME_TYPE_BIT = 0; 12 | /** ZCL Header frame control manufacturer specific */ 13 | const HEADER_CTRL_MANUF_SPE_MASK = 0x04; 14 | const HEADER_CTRL_MANUF_SPE_BIT = 2; 15 | /** ZCL Header frame control direction */ 16 | const HEADER_CTRL_DIRECTION_MASK = 0x08; 17 | const HEADER_CTRL_DIRECTION_BIT = 3; 18 | /** ZCL Header frame control disable default response */ 19 | const HEADER_CTRL_DISABLE_DEF_RESP_MASK = 0x10; 20 | const HEADER_CTRL_DISABLE_DEF_RESP_BIT = 4; 21 | /** ZCL Header frame control reserved */ 22 | const HEADER_CTRL_RESERVED_MASK = 0xe0; 23 | const HEADER_CTRL_RESERVED_BIT = 5; 24 | 25 | export class ZclHeader { 26 | public readonly frameControl: FrameControl; 27 | public readonly manufacturerCode: number | undefined; 28 | public readonly transactionSequenceNumber: number; 29 | public readonly commandIdentifier: number; 30 | 31 | constructor(frameControl: FrameControl, manufacturerCode: number | undefined, transactionSequenceNumber: number, commandIdentifier: number) { 32 | this.frameControl = frameControl; 33 | this.manufacturerCode = manufacturerCode; 34 | this.transactionSequenceNumber = transactionSequenceNumber; 35 | this.commandIdentifier = commandIdentifier; 36 | } 37 | 38 | /** Returns the amount of bytes used by this header */ 39 | get length(): number { 40 | return this.manufacturerCode === undefined ? HEADER_MINIMAL_LENGTH : HEADER_WITH_MANUF_LENGTH; 41 | } 42 | 43 | get isGlobal(): boolean { 44 | return this.frameControl.frameType === FrameType.GLOBAL; 45 | } 46 | 47 | get isSpecific(): boolean { 48 | return this.frameControl.frameType === FrameType.SPECIFIC; 49 | } 50 | 51 | public write(buffalo: BuffaloZcl): void { 52 | const frameControl = 53 | ((this.frameControl.frameType << HEADER_CTRL_FRAME_TYPE_BIT) & HEADER_CTRL_FRAME_TYPE_MASK) | 54 | (((this.frameControl.manufacturerSpecific ? 1 : 0) << HEADER_CTRL_MANUF_SPE_BIT) & HEADER_CTRL_MANUF_SPE_MASK) | 55 | ((this.frameControl.direction << HEADER_CTRL_DIRECTION_BIT) & HEADER_CTRL_DIRECTION_MASK) | 56 | (((this.frameControl.disableDefaultResponse ? 1 : 0) << HEADER_CTRL_DISABLE_DEF_RESP_BIT) & HEADER_CTRL_DISABLE_DEF_RESP_MASK) | 57 | ((this.frameControl.reservedBits << HEADER_CTRL_RESERVED_BIT) & HEADER_CTRL_RESERVED_MASK); 58 | 59 | buffalo.writeUInt8(frameControl); 60 | 61 | if (this.frameControl.manufacturerSpecific && this.manufacturerCode) { 62 | buffalo.writeUInt16(this.manufacturerCode); 63 | } 64 | 65 | buffalo.writeUInt8(this.transactionSequenceNumber); 66 | buffalo.writeUInt8(this.commandIdentifier); 67 | } 68 | 69 | public static fromBuffer(buffer: Buffer): ZclHeader | undefined { 70 | // Returns `undefined` in case the ZclHeader cannot be parsed. 71 | if (buffer.length < HEADER_MINIMAL_LENGTH) { 72 | logger.debug("ZclHeader is too short.", NS); 73 | return undefined; 74 | } 75 | 76 | const buffalo = new BuffaloZcl(buffer); 77 | const frameControlValue = buffalo.readUInt8(); 78 | const frameControl = { 79 | frameType: (frameControlValue & HEADER_CTRL_FRAME_TYPE_MASK) >> HEADER_CTRL_FRAME_TYPE_BIT, 80 | manufacturerSpecific: (frameControlValue & HEADER_CTRL_MANUF_SPE_MASK) >> HEADER_CTRL_MANUF_SPE_BIT === 1, 81 | direction: (frameControlValue & HEADER_CTRL_DIRECTION_MASK) >> HEADER_CTRL_DIRECTION_BIT, 82 | disableDefaultResponse: (frameControlValue & HEADER_CTRL_DISABLE_DEF_RESP_MASK) >> HEADER_CTRL_DISABLE_DEF_RESP_BIT === 1, 83 | reservedBits: (frameControlValue & HEADER_CTRL_RESERVED_MASK) >> HEADER_CTRL_RESERVED_BIT, 84 | }; 85 | 86 | let manufacturerCode: number | undefined; 87 | 88 | if (frameControl.manufacturerSpecific) { 89 | if (buffer.length < HEADER_WITH_MANUF_LENGTH) { 90 | logger.debug("ZclHeader is too short for control with manufacturer-specific.", NS); 91 | return undefined; 92 | } 93 | 94 | manufacturerCode = buffalo.readUInt16(); 95 | } 96 | 97 | const transactionSequenceNumber = buffalo.readUInt8(); 98 | const commandIdentifier = buffalo.readUInt8(); 99 | 100 | return new ZclHeader(frameControl, manufacturerCode, transactionSequenceNumber, commandIdentifier); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/zspec/zcl/zclStatusError.ts: -------------------------------------------------------------------------------- 1 | import {Status} from "./definition/status"; 2 | 3 | export class ZclStatusError extends Error { 4 | public code: Status; 5 | 6 | constructor(code: Status) { 7 | super(`Status '${Status[code]}'`); 8 | this.code = code; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/zspec/zdo/definition/consts.ts: -------------------------------------------------------------------------------- 1 | /** The endpoint where the ZigBee Device Object (ZDO) resides. */ 2 | export const ZDO_ENDPOINT = 0; 3 | 4 | /** The profile ID used by the ZigBee Device Object (ZDO). */ 5 | export const ZDO_PROFILE_ID = 0x0000; 6 | 7 | /** ZDO messages start with a sequence number. */ 8 | export const ZDO_MESSAGE_OVERHEAD = 1; 9 | 10 | export const MULTICAST_BINDING = 0x01; 11 | export const UNICAST_BINDING = 0x03; 12 | 13 | /** 64-bit challenge value used by CHALLENGE_REQUEST/CHALLENGE_RESPONSE clusters */ 14 | export const CHALLENGE_VALUE_SIZE = 8; 15 | /** The 256-bit Curve 25519 public point. */ 16 | export const CURVE_PUBLIC_POINT_SIZE = 32; 17 | -------------------------------------------------------------------------------- /src/zspec/zdo/definition/enums.ts: -------------------------------------------------------------------------------- 1 | export enum LeaveRequestFlags { 2 | /** Leave and rejoin. */ 3 | AND_REJOIN = 0x80, 4 | /** DEPRECATED */ 5 | // AND_REMOVE_CHILDREN = 0x40, 6 | /** Leave. */ 7 | WITHOUT_REJOIN = 0x00, 8 | } 9 | 10 | export enum JoiningPolicy { 11 | /** Any device is allowed to join. */ 12 | ALL_JOIN = 0x00, 13 | /** Only devices on the mibJoiningIeeeList are allowed to join. */ 14 | IEEELIST_JOIN = 0x01, 15 | /** No device is allowed to join. */ 16 | NO_JOIN = 0x02, 17 | } 18 | 19 | //------------------------------------------------------------------------------------------------- 20 | //-- TLVs 21 | 22 | export enum SelectedKeyNegotiationProtocol { 23 | /** (Zigbee 3.0 Mechanism) */ 24 | RESERVED = 0, 25 | /** SPEKE using Curve25519 with Hash AES-MMO-128 */ 26 | SPEKE_CURVE25519_AESMMO128 = 1, 27 | /** SPEKE using Curve25519 with Hash SHA-256 */ 28 | SPEKE_CURVE25519_SHA256 = 2, 29 | // 3 – 255 Reserved 30 | } 31 | 32 | export enum SelectedPreSharedSecret { 33 | /** Symmetric Authentication Token */ 34 | SYMMETRIC_AUTHENTICATION_TOKEN = 0, 35 | /** Pre-configured link-ley derived from installation code */ 36 | PRECONFIGURED_LINKKEY_DERIVED_FROM_INSTALL_CODE = 1, 37 | /** Variable-length pass code (for PAKE protocols) */ 38 | PAKE_VARIABLE_LENGTH_PASS_CODE = 2, 39 | /** Basic Authorization Key */ 40 | BASIC_AUTHORIZATION_KEY = 3, 41 | /** Administrative Authorization Key */ 42 | ADMIN_AUTHORIZATION_KEY = 4, 43 | // 5 – 254 Reserved, 44 | /** Anonymous Well-Known Secret */ 45 | ANONYMOUS_WELLKNOWN_SECRET = 255, 46 | } 47 | 48 | export enum InitialJoinMethod { 49 | ANONYMOUS = 0x00, 50 | INSTALL_CODE_KEY = 0x01, 51 | WELLKNOWN_PASSPHRASE = 0x02, 52 | INSTALL_CODE_PASSPHRASE = 0x03, 53 | } 54 | 55 | export enum ActiveLinkKeyType { 56 | NOT_UPDATED = 0x00, 57 | KEY_REQUEST_METHOD = 0x01, 58 | UNAUTHENTICATED_KEY_NEGOTIATION = 0x02, 59 | AUTHENTICATED_KEY_NEGOTIATION = 0x03, 60 | APPLICATION_DEFINED_CERTIFICATE_BASED_MUTUAL_AUTHENTICATION = 0x04, 61 | } 62 | 63 | export enum GlobalTLV { 64 | /** Minimum Length 2-byte */ 65 | MANUFACTURER_SPECIFIC = 64, 66 | /** Minimum Length 2-byte */ 67 | SUPPORTED_KEY_NEGOTIATION_METHODS = 65, 68 | /** Minimum Length 4-byte XXX: spec min doesn't make sense, this is one pan id => 2-byte??? */ 69 | PAN_ID_CONFLICT_REPORT = 66, 70 | /** Minimum Length 2-byte */ 71 | NEXT_PAN_ID_CHANGE = 67, 72 | /** Minimum Length 4-byte */ 73 | NEXT_CHANNEL_CHANGE = 68, 74 | /** Minimum Length 16-byte */ 75 | SYMMETRIC_PASSPHRASE = 69, 76 | /** Minimum Length 2-byte */ 77 | ROUTER_INFORMATION = 70, 78 | /** Minimum Length 2-byte */ 79 | FRAGMENTATION_PARAMETERS = 71, 80 | JOINER_ENCAPSULATION = 72, 81 | BEACON_APPENDIX_ENCAPSULATION = 73, 82 | // Reserved = 74, 83 | /** Minimum Length 2-byte XXX: min not in spec??? */ 84 | CONFIGURATION_PARAMETERS = 75, 85 | /** Refer to the Zigbee Direct specification for more details. */ 86 | DEVICE_CAPABILITY_EXTENSION = 76, 87 | // Reserved = 77-255 88 | } 89 | 90 | export enum RoutingTableStatus { 91 | ACTIVE = 0x0, 92 | DISCOVERY_UNDERWAY = 0x1, 93 | DISCOVERY_FAILED = 0x2, 94 | INACTIVE = 0x3, 95 | VALIDATION_UNDERWAY = 0x4, 96 | RESERVED1 = 0x5, 97 | RESERVED2 = 0x6, 98 | RESERVED3 = 0x7, 99 | } 100 | -------------------------------------------------------------------------------- /src/zspec/zdo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./definition/consts"; 2 | export * from "./definition/enums"; 3 | export {ClusterId} from "./definition/clusters"; 4 | export {Status} from "./definition/status"; 5 | export {ZdoStatusError as StatusError} from "./zdoStatusError"; 6 | export {BuffaloZdo as Buffalo} from "./buffaloZdo"; 7 | export * as Utils from "./utils"; 8 | -------------------------------------------------------------------------------- /src/zspec/zdo/utils.ts: -------------------------------------------------------------------------------- 1 | import {ClusterId} from "./definition/clusters"; 2 | import type {MACCapabilityFlags, ServerMask} from "./definition/tstypes"; 3 | 4 | /** 5 | * Get a the response cluster ID corresponding to a request. 6 | * @param requestClusterId 7 | * @returns Response cluster ID or undefined if unknown/invalid 8 | */ 9 | export const getResponseClusterId = (requestClusterId: ClusterId): ClusterId | undefined => { 10 | if (0x8000 < requestClusterId || requestClusterId === ClusterId.END_DEVICE_ANNOUNCE) { 11 | return undefined; 12 | } 13 | 14 | const responseClusterId = requestClusterId + 0x8000; 15 | 16 | if (ClusterId[responseClusterId] === undefined) { 17 | return undefined; 18 | } 19 | 20 | return responseClusterId; 21 | }; 22 | 23 | /** 24 | * Get the values for the bitmap `Mac Capability Flags Field` as per spec. 25 | * Given value is assumed to be a proper 1-byte length. 26 | * @param capabilities 27 | * @returns 28 | */ 29 | export const getMacCapFlags = (capabilities: number): MACCapabilityFlags => { 30 | return { 31 | alternatePANCoordinator: capabilities & 0x01, 32 | deviceType: (capabilities & 0x02) >> 1, 33 | powerSource: (capabilities & 0x04) >> 2, 34 | rxOnWhenIdle: (capabilities & 0x08) >> 3, 35 | reserved1: (capabilities & 0x10) >> 4, 36 | reserved2: (capabilities & 0x20) >> 5, 37 | securityCapability: (capabilities & 0x40) >> 6, 38 | allocateAddress: (capabilities & 0x80) >> 7, 39 | }; 40 | }; 41 | 42 | /** 43 | * Get the values for the bitmap `Server Mask Field` as per spec. 44 | * Given value is assumed to be a proper 2-byte length. 45 | * @param serverMask 46 | * @returns 47 | */ 48 | export const getServerMask = (serverMask: number): ServerMask => { 49 | return { 50 | primaryTrustCenter: serverMask & 0x01, 51 | backupTrustCenter: (serverMask & 0x02) >> 1, 52 | deprecated1: (serverMask & 0x04) >> 2, 53 | deprecated2: (serverMask & 0x08) >> 3, 54 | deprecated3: (serverMask & 0x10) >> 4, 55 | deprecated4: (serverMask & 0x20) >> 5, 56 | networkManager: (serverMask & 0x40) >> 6, 57 | reserved1: (serverMask & 0x80) >> 7, 58 | reserved2: (serverMask & 0x100) >> 8, 59 | stackComplianceRevision: (serverMask & 0xfe00) >> 9, 60 | }; 61 | }; 62 | 63 | export const createServerMask = (serverMask: ServerMask): number => { 64 | return ( 65 | (serverMask.primaryTrustCenter & 0x01) | 66 | ((serverMask.backupTrustCenter << 1) & 0x02) | 67 | ((serverMask.deprecated1 << 2) & 0x04) | 68 | ((serverMask.deprecated2 << 3) & 0x08) | 69 | ((serverMask.deprecated3 << 4) & 0x10) | 70 | ((serverMask.deprecated4 << 5) & 0x20) | 71 | ((serverMask.networkManager << 6) & 0x40) | 72 | ((serverMask.reserved1 << 7) & 0x80) | 73 | ((serverMask.reserved2 << 8) & 0x100) | 74 | ((serverMask.stackComplianceRevision << 9) & 0xfe00) 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/zspec/zdo/zdoStatusError.ts: -------------------------------------------------------------------------------- 1 | import {Status} from "./definition/status"; 2 | 3 | export class ZdoStatusError extends Error { 4 | public code: Status; 5 | 6 | constructor(code: Status) { 7 | super(`Status '${Status[code]}'`); 8 | this.code = code; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/adapter/ember/ezspBuffalo.test.ts: -------------------------------------------------------------------------------- 1 | import {SLStatus} from "../../../src/adapter/ember/enums"; 2 | import {EzspBuffalo} from "../../../src/adapter/ember/ezsp/buffalo"; 3 | import { 4 | EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, 5 | EZSP_FRAME_CONTROL_COMMAND, 6 | EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK, 7 | EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET, 8 | EZSP_FRAME_CONTROL_SLEEP_MODE_MASK, 9 | EZSP_FRAME_ID_INDEX, 10 | EZSP_MAX_FRAME_LENGTH, 11 | EZSP_PARAMETERS_INDEX, 12 | EZSP_SEQUENCE_INDEX, 13 | } from "../../../src/adapter/ember/ezsp/consts"; 14 | import {EzspFrameID} from "../../../src/adapter/ember/ezsp/enums"; 15 | import {lowByte} from "../../../src/adapter/ember/utils/math"; 16 | 17 | describe("Ember EZSP Buffalo", () => { 18 | let buffalo: EzspBuffalo; 19 | 20 | beforeAll(async () => {}); 21 | 22 | afterAll(async () => {}); 23 | 24 | beforeEach(() => { 25 | buffalo = new EzspBuffalo(Buffer.alloc(EZSP_MAX_FRAME_LENGTH)); 26 | }); 27 | 28 | afterEach(() => {}); 29 | 30 | it("Is empty after init", () => { 31 | expect(buffalo.getWritten()).toStrictEqual(Buffer.from([])); 32 | }); 33 | 34 | it("Writes & read at position without altering internal position tracker", () => { 35 | // mock send `version` command logic flow 36 | buffalo.setPosition(EZSP_PARAMETERS_INDEX); 37 | buffalo.setCommandByte(EZSP_FRAME_ID_INDEX, lowByte(EzspFrameID.VERSION)); 38 | buffalo.setCommandByte(EZSP_SEQUENCE_INDEX, 0); 39 | buffalo.setCommandByte( 40 | EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX, 41 | EZSP_FRAME_CONTROL_COMMAND | 42 | (0x00 & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) | 43 | ((0x00 << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK), 44 | ); 45 | buffalo.writeUInt8(12); // desiredProtocolVersion 46 | 47 | expect(buffalo.getWritten()).toStrictEqual(Buffer.from([0x00, 0x00, 0x00, 0x0c])); 48 | 49 | expect(buffalo.getCommandByte(EZSP_FRAME_ID_INDEX)).toStrictEqual(lowByte(EzspFrameID.VERSION)); 50 | expect(buffalo.getCommandByte(EZSP_SEQUENCE_INDEX)).toStrictEqual(0); 51 | expect(buffalo.getCommandByte(EZSP_EXTENDED_FRAME_CONTROL_LB_INDEX)).toStrictEqual( 52 | EZSP_FRAME_CONTROL_COMMAND | 53 | (0x00 & EZSP_FRAME_CONTROL_SLEEP_MODE_MASK) | 54 | ((0x00 << EZSP_FRAME_CONTROL_NETWORK_INDEX_OFFSET) & EZSP_FRAME_CONTROL_NETWORK_INDEX_MASK), 55 | ); 56 | }); 57 | 58 | it("Maps EmberStatus/EzspStatus to SLStatus", () => { 59 | buffalo.setCommandByte(0, 0x00); 60 | buffalo.setCommandByte(1, 0x00); 61 | buffalo.setCommandByte(2, 0x00); 62 | buffalo.setCommandByte(3, 0x00); 63 | // zero always zero 64 | buffalo.setPosition(0); 65 | expect(buffalo.readStatus(0x0d)).toStrictEqual(SLStatus.OK); 66 | buffalo.setPosition(0); 67 | expect(buffalo.readStatus(0x0d, false)).toStrictEqual(SLStatus.OK); 68 | buffalo.setPosition(0); 69 | expect(buffalo.readStatus(0x0e)).toStrictEqual(SLStatus.OK); 70 | buffalo.setPosition(0); 71 | expect(buffalo.readStatus(0x0e, false)).toStrictEqual(SLStatus.OK); 72 | 73 | buffalo.setCommandByte(0, 0x02); 74 | buffalo.setPosition(0); 75 | expect(buffalo.readStatus(0x0d)).toStrictEqual(SLStatus.INVALID_PARAMETER); 76 | buffalo.setPosition(0); 77 | expect(buffalo.readStatus(0x0d, false)).toStrictEqual(SLStatus.ZIGBEE_EZSP_ERROR); 78 | 79 | buffalo.setCommandByte(0, 0x9c); 80 | buffalo.setPosition(0); 81 | expect(buffalo.readStatus(0x0d)).toStrictEqual(SLStatus.ZIGBEE_NETWORK_OPENED); 82 | buffalo.setPosition(0); 83 | expect(buffalo.readStatus(0x0d, false)).toStrictEqual(SLStatus.ZIGBEE_EZSP_ERROR); 84 | 85 | // no mapped value 86 | buffalo.setCommandByte(0, 0x4b); 87 | buffalo.setPosition(0); 88 | expect(buffalo.readStatus(0x0d)).toStrictEqual(SLStatus.BUS_ERROR); 89 | buffalo.setPosition(0); 90 | expect(buffalo.readStatus(0x0d, false)).toStrictEqual(SLStatus.ZIGBEE_EZSP_ERROR); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/adapter/ember/ezspError.test.ts: -------------------------------------------------------------------------------- 1 | import {EzspError} from "../../../src/adapter/ember/ezspError"; 2 | import {EzspStatus} from "../../../src/adapter/ezsp/driver/types"; 3 | 4 | describe("Ezsp Error", () => { 5 | it("Creates error", () => { 6 | const error = new EzspError(EzspStatus.ASH_ACK_TIMEOUT); 7 | 8 | expect(error.message).toStrictEqual("ASH_ACK_TIMEOUT"); 9 | expect(error.code).toStrictEqual(EzspStatus.ASH_ACK_TIMEOUT); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/adapter/ezsp/frame.test.ts: -------------------------------------------------------------------------------- 1 | import {EZSPFrameData} from "../../../src/adapter/ezsp/driver/ezsp"; 2 | 3 | describe("FRAME Parsing", () => { 4 | it("changeSourceRouteHandler", () => { 5 | const frm = EZSPFrameData.createFrame(8, 0x00c4, false, Buffer.from("05e399a000", "hex")); 6 | expect(frm._cls_).toBe("changeSourceRouteHandler"); 7 | expect(frm._id_).toBe(0x00c4); 8 | expect(frm.newChildId).toBe(0xe305); 9 | expect(frm.newParentId).toBe(0xa099); 10 | }); 11 | it("changeSourceRouteHandler", () => { 12 | const frm = EZSPFrameData.createFrame(9, 0x00c4, false, Buffer.from("05e399a000", "hex")); 13 | expect(frm._cls_).toBe("incomingNetworkStatusHandler"); 14 | expect(frm._id_).toBe(0x00c4); 15 | expect(frm.errorCode).toBe(0x05); 16 | expect(frm.target).toBe(0x99e3); 17 | }); 18 | it("incomingNetworkStatusHandler", () => { 19 | const frm = EZSPFrameData.createFrame(9, 0x00c4, false, Buffer.from("0b044e", "hex")); 20 | expect(frm._cls_).toBe("incomingNetworkStatusHandler"); 21 | expect(frm._id_).toBe(0x00c4); 22 | expect(frm.errorCode).toBe(0x0b); 23 | expect(frm.target).toBe(0x4e04); 24 | }); 25 | it("incomingNetworkStatusHandler", () => { 26 | const frm = EZSPFrameData.createFrame(8, 0x00c4, false, Buffer.from("0b044e", "hex")); 27 | expect(frm).toBe(undefined); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/adapter/z-stack/constants.test.ts: -------------------------------------------------------------------------------- 1 | import * as Constants from "../../../src/adapter/z-stack/constants"; 2 | 3 | describe("zstack-constants", () => { 4 | it("Simple check", () => { 5 | expect(Constants.AF.DEFAULT_RADIUS).toBe(30); 6 | }); 7 | 8 | describe("utils", () => { 9 | describe("getChannelMask", () => { 10 | it("Get channel mask 11", () => { 11 | expect(Constants.Utils.getChannelMask([11])).toStrictEqual([0, 8, 0, 0]); 12 | }); 13 | 14 | it("Get channel mask 25", () => { 15 | expect(Constants.Utils.getChannelMask([25])).toStrictEqual([0, 0, 0, 2]); 16 | }); 17 | 18 | it("Get channel mask 11 and 25", () => { 19 | expect(Constants.Utils.getChannelMask([11, 25])).toStrictEqual([0, 8, 0, 2]); 20 | }); 21 | }); 22 | 23 | describe("statusDescription", () => { 24 | it("formats known status", () => { 25 | expect(Constants.Utils.statusDescription(0x10)).toBe("(0x10: MEM_ERROR)"); 26 | }); 27 | it("formats unknown status", () => { 28 | expect(Constants.Utils.statusDescription(0x08)).toBe("(0x08: UNKNOWN)"); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/adapter/zigate/patchZdoBuffaloBE.test.ts: -------------------------------------------------------------------------------- 1 | import * as Zdo from "../../../src/zspec/zdo"; 2 | 3 | describe("ZiGate Patch BuffaloZdo to use BE variants when writing", () => { 4 | let BuffaloZdo: typeof Zdo.Buffalo; 5 | 6 | beforeAll(async () => { 7 | vi.resetModules(); 8 | 9 | const buf = await import("../../../src/zspec/zdo/buffaloZdo.js"); 10 | BuffaloZdo = buf.BuffaloZdo; 11 | const {ZiGateAdapter} = await import("../../../src/adapter/zigate/adapter/zigateAdapter.js"); 12 | // @ts-expect-error bogus, just need to trigger constructor 13 | new ZiGateAdapter({}, {}, "", {}); 14 | }); 15 | 16 | it("writeUInt16", () => { 17 | expect(BuffaloZdo.buildRequest(false, Zdo.ClusterId.IEEE_ADDRESS_REQUEST, 0x1234, false, 0)).toStrictEqual( 18 | Buffer.from([0x12, 0x34, 0x00, 0x00]), 19 | ); 20 | 21 | // ensure regular parsing OK 22 | expect(Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.IEEE_ADDRESS_REQUEST, 0x1234, false, 0)).toStrictEqual( 23 | Buffer.from([0x34, 0x12, 0x00, 0x00]), 24 | ); 25 | }); 26 | 27 | it("writeUInt32", () => { 28 | expect(BuffaloZdo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, undefined, undefined)).toStrictEqual( 29 | Buffer.from([0x00, 0x00, 0x80, 0x00, 0xfe]), 30 | ); 31 | 32 | // ensure regular parsing OK 33 | expect(Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, undefined, undefined)).toStrictEqual( 34 | Buffer.from([0x00, 0x80, 0x00, 0x00, 0xfe]), 35 | ); 36 | }); 37 | 38 | it("readUInt16 + readUInt32 - LE", () => { 39 | expect( 40 | BuffaloZdo.readResponse( 41 | true, 42 | Zdo.ClusterId.NWK_UPDATE_RESPONSE, 43 | Buffer.from([0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x12, 0x34, 0x00, 0x01, 0x01, 0x12]), 44 | ), 45 | ).toStrictEqual( 46 | Zdo.Buffalo.readResponse( 47 | true, 48 | Zdo.ClusterId.NWK_UPDATE_RESPONSE, 49 | Buffer.from([0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x12, 0x34, 0x00, 0x01, 0x01, 0x12]), 50 | ), 51 | ); 52 | }); 53 | 54 | it("writeIeeeAddr", () => { 55 | expect(BuffaloZdo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, "0x1122334455667788", false, 0)).toStrictEqual( 56 | Buffer.from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x00, 0x00]), 57 | ); 58 | 59 | // ensure regular parsing OK 60 | expect(Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, "0x1122334455667788", false, 0)).toStrictEqual( 61 | Buffer.from([0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x00]), 62 | ); 63 | }); 64 | 65 | it("readIeeeAddr - LE", () => { 66 | expect( 67 | BuffaloZdo.readResponse( 68 | true, 69 | Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, 70 | Buffer.from([0x01, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x12, 0x34]), 71 | ), 72 | ).toStrictEqual( 73 | Zdo.Buffalo.readResponse( 74 | true, 75 | Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, 76 | Buffer.from([0x01, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x12, 0x34]), 77 | ), 78 | ); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/adapter/zoh/utils.test.ts: -------------------------------------------------------------------------------- 1 | import {bigUInt64ToBufferBE, bigUInt64ToBufferLE, bigUInt64ToHexBE} from "../../../src/adapter/zoh/adapter/utils"; 2 | 3 | describe("ZoH Utils", () => { 4 | it("handles bigint conversions", () => { 5 | const v10x = "0x90395efffec7fd21"; 6 | const v1Buf = Buffer.from([0x21, 0xfd, 0xc7, 0xfe, 0xff, 0x5e, 0x39, 0x90]); 7 | const v1BigInt = 10392442068718320929n; 8 | 9 | const v20x = "0x9986ffbb4523acef"; 10 | const v2Buf = Buffer.from([0xef, 0xac, 0x23, 0x45, 0xbb, 0xff, 0x86, 0x99]); 11 | const v2BigInt = 11062810714466135279n; 12 | 13 | const v30x = "0x0322334455667788"; 14 | const v3Buf = Buffer.from([0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x03]); 15 | const v3BigInt = 225799299905517448n; 16 | 17 | expect(bigUInt64ToHexBE(v1BigInt)).toStrictEqual(v10x.slice(2)); 18 | expect(bigUInt64ToBufferLE(v1BigInt)).toStrictEqual(v1Buf); 19 | expect(bigUInt64ToBufferBE(v1BigInt)).toStrictEqual(Buffer.from(v1Buf).reverse()); 20 | expect(BigInt(v10x)).toStrictEqual(v1BigInt); 21 | expect(v1Buf.readBigUInt64LE(0)).toStrictEqual(v1BigInt); 22 | 23 | expect(bigUInt64ToHexBE(v2BigInt)).toStrictEqual(v20x.slice(2)); 24 | expect(bigUInt64ToBufferLE(v2BigInt)).toStrictEqual(v2Buf); 25 | expect(bigUInt64ToBufferBE(v2BigInt)).toStrictEqual(Buffer.from(v2Buf).reverse()); 26 | expect(BigInt(v20x)).toStrictEqual(v2BigInt); 27 | expect(v2Buf.readBigUInt64LE(0)).toStrictEqual(v2BigInt); 28 | 29 | expect(bigUInt64ToHexBE(v3BigInt)).toStrictEqual(v30x.slice(2)); 30 | expect(bigUInt64ToBufferLE(v3BigInt)).toStrictEqual(v3Buf); 31 | expect(bigUInt64ToBufferBE(v3BigInt)).toStrictEqual(Buffer.from(v3Buf).reverse()); 32 | expect(BigInt(v30x)).toStrictEqual(v3BigInt); 33 | expect(v3Buf.readBigUInt64LE(0)).toStrictEqual(v3BigInt); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/mockAdapters.ts: -------------------------------------------------------------------------------- 1 | export const DECONZ_CONBEE_II = { 2 | path: "/dev/serial/by-id/usb-dresden_elektronik_ingenieurtechnik_GmbH_ConBee_II_DE2132111-if00", 3 | vendorId: "1cf1", 4 | productId: "0030", 5 | manufacturer: "dresden elektronik ingenieurtechnik GmbH", 6 | }; 7 | export const EMBER_ZBDONGLE_E = { 8 | // may or may not have `V2` (bad metadata in some batches) 9 | path: "/dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20240122184111-if00", 10 | vendorId: "1A86", // uppercased for extra coverage 11 | productId: "55d4", 12 | manufacturer: "ITEAD", 13 | }; 14 | export const EMBER_ZBDONGLE_E_CP = { 15 | // may or may not have `V2` (bad metadata in some batches) 16 | path: "/dev/serial/by-id/usb-Itead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_V2_a6ee897e4d1fef11aa004ad0639e525b-if00-port0", 17 | vendorId: "10c4", 18 | productId: "ea60", 19 | manufacturer: "ITEAD", 20 | }; 21 | // vendorId+productId conflict with all 10c4:ea60 22 | export const EMBER_SKYCONNECT = { 23 | path: "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_3abe54797c91ed118fc3cad13b20a111-if00-port0", 24 | vendorId: "10c4", 25 | productId: "ea60", 26 | manufacturer: "Nabu Casa", 27 | }; 28 | export const ZSTACK_CC2538 = { 29 | path: "/dev/serial/by-id/usb-Texas_Instruments_CC2538_USB_CDC-if00", 30 | vendorId: "0451", 31 | productId: "16C8", // uppercased for extra coverage 32 | manufacturer: "Texas Instruments", 33 | }; 34 | // vendorId+productId conflict with all 10c4:ea60 35 | export const ZSTACK_ZBDONGLE_P = { 36 | path: "/dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_b8b49abd27a6ed11a280eba32981d111-if00-port0", 37 | vendorId: "10c4", 38 | productId: "ea60", 39 | manufacturer: "ITEAD", 40 | }; 41 | // vendorId+productId conflict with all 10c4:ea60 42 | export const ZSTACK_SMLIGHT_SLZB_06P10 = { 43 | path: "/dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-06p10_40df2f3e3977ed11b142f6fafdf7b791-if00-port0", 44 | vendorId: "10c4", 45 | productId: "ea60", 46 | manufacturer: "SMLIGHT", 47 | }; 48 | // vendorId+productId conflict with all 10c4:ea60 49 | export const ZSTACK_SMLIGHT_SLZB_07 = { 50 | path: "/dev/serial/by-id/usb-SMLIGHT_SMLIGHT_SLZB-07_be9faa0786e1ea11bd68dc2d9a583111-if00-port0", 51 | vendorId: "10c4", 52 | productId: "ea60", 53 | manufacturer: "SMLIGHT", 54 | }; 55 | export const ZBOSS_NORDIC = { 56 | path: "/dev/serial/by-id/usb-ZEPHYR_Zigbee_NCP_54ACCFAFA6DADC49-if00", 57 | vendorId: "2fe3", 58 | productId: "0100", 59 | manufacturer: "ZEPHYR", 60 | }; 61 | export const ZIGATE_PLUSV2 = { 62 | path: "/dev/serial/by-id/usb-FTDI_ZiGate_ZIGATE+-if00-port0", 63 | vendorId: "0403", 64 | productId: "6015", 65 | }; 66 | -------------------------------------------------------------------------------- /test/testUtils.ts: -------------------------------------------------------------------------------- 1 | function duplicateArray(amount, value) { 2 | let result = []; 3 | for (let i = 0; i < amount; i++) { 4 | result = result.concat(value); 5 | } 6 | 7 | return result; 8 | } 9 | 10 | const ieeeaAddr1 = { 11 | string: "0xae440112004b1200", 12 | hex: [0x00, 0x12, 0x4b, 0x00, 0x12, 0x01, 0x44, 0xae], 13 | }; 14 | 15 | const ieeeaAddr2 = { 16 | string: "0xaf440112005b1200", 17 | hex: [0x00, 0x12, 0x5b, 0x00, 0x12, 0x01, 0x44, 0xaf], 18 | }; 19 | 20 | export {duplicateArray, ieeeaAddr1, ieeeaAddr2}; 21 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "include": ["./**/*", "vitest.config.mts"], 4 | "compilerOptions": { 5 | "types": ["vitest/globals"], 6 | "rootDir": "..", 7 | "noEmit": true 8 | }, 9 | "references": [{ "path": ".." }] 10 | } 11 | -------------------------------------------------------------------------------- /test/utils/math.ts: -------------------------------------------------------------------------------- 1 | export const uint16To8Array = (n: number): number[] => { 2 | return [n & 0xff, (n >> 8) & 0xff]; 3 | }; 4 | 5 | export const uint32To8Array = (n: number): number[] => { 6 | return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; 7 | }; 8 | 9 | export const uint56To8Array = (n: bigint): number[] => { 10 | return [ 11 | Number(n & 0xffn), 12 | Number(n >> 8n) & 0xff, 13 | Number(n >> 16n) & 0xff, 14 | Number(n >> 24n) & 0xff, 15 | Number(n >> 32n) & 0xff, 16 | Number(n >> 40n) & 0xff, 17 | Number(n >> 48n) & 0xff, 18 | ]; 19 | }; 20 | -------------------------------------------------------------------------------- /test/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | onConsoleLog(_log: string, _type: "stdout" | "stderr"): boolean | undefined { 7 | return false; 8 | }, 9 | coverage: { 10 | enabled: false, 11 | provider: "v8", 12 | include: ["src/**"], 13 | extension: [".ts"], 14 | // exclude: [], 15 | clean: true, 16 | cleanOnRerun: true, 17 | reportsDirectory: "coverage", 18 | reporter: ["text", "html"], 19 | reportOnFailure: false, 20 | thresholds: { 21 | 100: true, 22 | }, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "module": "NodeNext", 5 | "esModuleInterop": true, 6 | "target": "ES2022", 7 | "lib": ["ES2022"], 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "declarationMap": true, 14 | "outDir": "dist", 15 | "baseUrl": ".", 16 | "resolveJsonModule": true, 17 | "incremental": true, 18 | "composite": true, 19 | "rootDir": "src", 20 | "checkJs": true 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.json"], 23 | "exclude": ["src/deprecated"] 24 | } 25 | --------------------------------------------------------------------------------