├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ └── config.yml ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── ci.yml │ ├── npmpublish.yml │ └── release-drafter.yml ├── .gitignore ├── .nvmrc ├── API_SCHEMA.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── example_config.js ├── package-lock.json ├── package.json ├── src ├── bin │ ├── client.ts │ └── server.ts ├── lib │ ├── broadcast_node │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ ├── command.ts │ ├── common.ts │ ├── config_manager │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ ├── const.ts │ ├── controller │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ ├── driver │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ ├── endpoint │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ ├── error.ts │ ├── forward.ts │ ├── inclusion_user_callbacks.ts │ ├── incoming_message.ts │ ├── incoming_message_base.ts │ ├── index.ts │ ├── instance.ts │ ├── logging.ts │ ├── message_handler.ts │ ├── multicast_group │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ ├── node │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ ├── outgoing_message.ts │ ├── server.ts │ ├── state.ts │ ├── utils │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts │ └── zniffer │ │ ├── command.ts │ │ ├── incoming_message.ts │ │ ├── message_handler.ts │ │ └── outgoing_message.ts ├── mock │ └── index.ts ├── test │ └── integration.ts └── util │ ├── logger.ts │ ├── parse-args.ts │ └── stringify.ts └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/typescript-node 3 | { 4 | "name": "Z-Wave JS Server", 5 | "image": "mcr.microsoft.com/vscode/devcontainers/typescript-node:dev-18", 6 | "context": "..", 7 | "postCreateCommand": "npm install", 8 | "forwardPorts": [3000], 9 | "remoteUser": "node", 10 | "extensions": [ 11 | "dbaeumer.vscode-eslint", 12 | "github.vscode-pull-request-github", 13 | "esbenp.prettier-vscode" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Report a bug with Z-Wave JS 4 | url: https://github.com/home-assistant/core/issues 5 | about: Please report issues with Z-Wave JS in the Home Assistant core repository unless a developer told you otherwise. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml file with 2 | # minimum configuration for two package managers 3 | 4 | version: 2 5 | updates: 6 | # Enable version updates for npm 7 | - package-ecosystem: "npm" 8 | # Look for `package.json` and `lock` files in the `root` directory 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | # Keep GitHub Actions up to date 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: "⬆️ Dependencies" 3 | collapse-after: 1 4 | labels: 5 | - "dependencies" 6 | template: | 7 | ## What's Changed 8 | 9 | $CHANGES 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js 20 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | cache: "npm" 23 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | cache: "npm" 19 | - run: npm ci 20 | - run: npm test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | registry-url: https://registry.npmjs.org/ 31 | cache: "npm" 32 | - run: npm ci 33 | 34 | - name: Determine dist-tag 35 | id: dist_tag 36 | uses: actions/github-script@v7 37 | with: 38 | result-encoding: string 39 | script: | 40 | const semver = require("semver"); 41 | const version = require(`${process.env.GITHUB_WORKSPACE}/package.json`).version; 42 | const parsed = semver.parse(version); 43 | return parsed.prerelease.length ? "--tag next" : ""; 44 | 45 | - name: Publish to NPM 46 | env: 47 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 48 | TAG: ${{ steps.dist_tag.outputs.result }} 49 | run: npm publish $TAG 50 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v6 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.json 3 | /dist/ 4 | /dist-esm/ 5 | /dist-cjs/ 6 | .vscode/* 7 | .DS_Store 8 | *.tgz -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /API_SCHEMA.md: -------------------------------------------------------------------------------- 1 | # API Schema 2 | 3 | This document describes the changes that are introduced with each schema version. 4 | 5 | ## Schema 0 6 | 7 | Base schema. 8 | 9 | ## Schema 1 10 | 11 | - Device classes were previously exposed as their `string` representation. They are now exposed with both their `string` and `integer` representation. 12 | - Command classes at the node level were previously exposed as their `string` representation. They are now exposed with both their `string` and `integer` representation. 13 | 14 | ## Schema 2 15 | 16 | - `Buffer` values were previously exposed with a `ValueType` of `string`. They are now exposed with a `ValueType` of `Buffer` 17 | 18 | # Schema 3 19 | 20 | - Renamed `controller.removeNodeFromAllAssocations` to `controller.removeNodeFromAllAssociations` to fix a typo 21 | - Numeric loglevels are converted to the corresponding string loglevel internally. driver.getLogConfig always returns the string loglevel regardless. 22 | - `isFrequentListening` was changed to have the type `FLiRS = false | "250ms" | "1000ms"` (previously `boolean`) to indicate the wakeup frequency. 23 | - `maxBaudRate` was renamed to `maxDataRate`, the type `Baudrate` was renamed to `DataRate` 24 | - The property `supportedDataRates` was added to provide an array of supported data rates 25 | - The `version` property was renamed to `protocolVersion` and had its type changed from `number` to the enum `ProtocolVersion` (the underlying values are still the same). 26 | - The `isBeaming` property was renamed to `supportsBeaming` to better show its intent. 27 | - The `supportsSecurity` property was split off from the `isSecure` property because they have a different meaning. 28 | - The old `nodeType` and `roleType` properties were renamed to `zwavePlusNodeType` and `zwavePlusRoleType` to clarify that they refer to Z-Wave+. 29 | - The node `notification` event was reworked and decoupled from the Notification CC. The event callback now indicates which CC raised the event and its arguments are moved into a single object parameter. 30 | - Moved the `deviceClass` property from `ZWaveNode` to its base class `Endpoint` and consider the endpoint's device class where necessary 31 | 32 | # Schema 4 33 | 34 | - Node `interviewStage` property was changed from type `number` to type `string` 35 | 36 | # Schema 5 37 | 38 | - Added `deviceDatabaseUrl` property to Node 39 | - Removed `neighbors` property from Node. Use `controller.get_node_neighbors` instead. 40 | 41 | --- 42 | 43 | > Missing schemas (6 - 32) will be added later 44 | 45 | --- 46 | 47 | # Schema 33 48 | 49 | - Fixed `node.set_raw_config_parameter_value` command to match Z-Wave JS types 50 | - Added `endpoint.set_raw_config_parameter_value` command 51 | - Added `driver.update_options` command 52 | 53 | # Schema 34 54 | 55 | - Added `rebuildRoutesProgress` to controller state dump 56 | - Listen for clients using IPv6 in addition to IPv4 which was already supported 57 | 58 | # Schema 35 59 | 60 | - Adds Z-Wave Long Range support 61 | - Added `supportsLongRange` to controller state dump 62 | 63 | # Schema 36 64 | 65 | - Added `maxLongRangePowerlevel`, `longRangeChannel`, and `supportsLongRangeAutoChannelSelection` to controller state dump 66 | - Added commands for controller methods `getMaxLongRangePowerlevel`, `setMaxLongRangePowerlevel`, `getLongRangeChannel`, and `setLongRangeChannel` 67 | - Removed deprecated `mandatoryControlledCCs` and `mandatorySupportedCCs` properties from device class dump 68 | - Added commands for `node.createDump` and `driver.sendTestFrame` 69 | 70 | # Schema 37 71 | 72 | - Added command for `checkAssocation` controller method 73 | - Updated payload for `inclusion started` controller event 74 | 75 | # Schema 38 76 | 77 | - Added controller `inclusion state changed` event 78 | - Added `config_manager` commands 79 | - Added `zniffer` commands 80 | 81 | # Schema 39 82 | 83 | - Added support for both overloads of `node.manuallyIdleNotificationValue` 84 | - Added `node.get_raw_config_parameter_value` and `endpoint.get_raw_config_parameter_value` commands 85 | 86 | # Schema 40 87 | 88 | - Added `endpoint.try_get_node` command 89 | - Added `controller.cancelSecureBootstrapS2` command 90 | 91 | # Schema 41 92 | 93 | - Changed `source` of the `firmware update progress` and `firmware update finished` events from `controller` to `driver` 94 | - Added `driver.firmware_update_otw` and `driver.is_otw_firmware_update_in_progress` commands 95 | - Added `node.get_supported_notification_events` command 96 | 97 | # Schema 42 98 | 99 | - Added `sdkVersion` property to `NodeState` 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default [ 18 | ...compat.extends("prettier"), 19 | { 20 | files: ["**/*.ts"], 21 | plugins: { 22 | "@typescript-eslint": typescriptEslint, 23 | }, 24 | 25 | languageOptions: { 26 | globals: { 27 | ...globals.node, 28 | }, 29 | 30 | parser: tsParser, 31 | ecmaVersion: 11, 32 | sourceType: "module", 33 | }, 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /example_config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | logConfig: { 3 | filename: "/var/log/zwave/zwave", 4 | forceConsole: true, 5 | logToFile: true, 6 | level: "info", 7 | }, 8 | 9 | storage: { 10 | cacheDir: "/opt/zwave_js_server/data", 11 | deviceConfigPriorityDir: "/opt/zwave_js_server/data/config", 12 | }, 13 | 14 | // Generated with: "< /dev/urandom tr -dc A-F0-9 | head -c32 ;echo" 15 | securityKeys: { 16 | S0_Legacy: Buffer.from("", "hex"), 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zwave-js/server", 3 | "version": "3.0.2", 4 | "description": "Full access to zwave-js driver through Websockets", 5 | "homepage": "https://github.com/zwave-js/zwave-js-server#readme", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/zwave-js/zwave-js-server.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/zwave-js/zwave-js-server/issues" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "type": "module", 17 | "module": "dist-esm/lib/index.js", 18 | "main": "dist-cjs/lib/index.js", 19 | "types": "dist-cjs/lib/index.d.ts", 20 | "bin": { 21 | "zwave-server": "dist-esm/bin/server.js", 22 | "zwave-client": "dist-esm/bin/client.js" 23 | }, 24 | "exports": { 25 | ".": { 26 | "import": "./dist-esm/lib/index.js", 27 | "require": "./dist-cjs/lib/index.js" 28 | }, 29 | "./package.json": "./package.json" 30 | }, 31 | "files": [ 32 | "dist-esm", 33 | "dist-cjs" 34 | ], 35 | "scripts": { 36 | "lint": "eslint", 37 | "lint:fix": "eslint --fix && prettier -w .", 38 | "test": "prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", 39 | "build": "tsc -p .", 40 | "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", 41 | "prepare": "npm run build", 42 | "prepublishOnly": "rm -rf dist-* && npm run build" 43 | }, 44 | "author": "", 45 | "license": "Apache-2.0", 46 | "dependencies": { 47 | "@homebridge/ciao": "^1.1.7", 48 | "minimist": "^1.2.8", 49 | "ws": "^8.18.0" 50 | }, 51 | "peerDependencies": { 52 | "zwave-js": "^15.3.0" 53 | }, 54 | "devDependencies": { 55 | "@alcalzone/esm2cjs": "^1.4.0", 56 | "@eslint/eslintrc": "^3.1.0", 57 | "@eslint/js": "^9.14.0", 58 | "@tsconfig/node20": "^20.1.4", 59 | "@types/minimist": "^1.2.2", 60 | "@types/node": "^22.5.0", 61 | "@types/triple-beam": "^1.3.2", 62 | "@types/ws": "^8.5.13", 63 | "@types/yargs": "^17.0.24", 64 | "@typescript-eslint/eslint-plugin": "^8.14.0", 65 | "@typescript-eslint/parser": "^8.14.0", 66 | "alcalzone-shared": "^5.0.0", 67 | "eslint": "^9.14.0", 68 | "eslint-config-prettier": "^9.1.0", 69 | "globals": "^15.9.0", 70 | "husky": "^4.3.8", 71 | "lint-staged": "^15.0.2", 72 | "prettier": "^3.0.0", 73 | "semver": "^7.5.4", 74 | "tsx": "^4.19.2", 75 | "typescript": "^5.3.3", 76 | "zwave-js": "^15.3.0" 77 | }, 78 | "engines": { 79 | "node": ">= 20" 80 | }, 81 | "husky": { 82 | "hooks": { 83 | "pre-commit": "lint-staged" 84 | } 85 | }, 86 | "lint-staged": { 87 | "*.{ts,js,json,css,md}": [ 88 | "prettier --write" 89 | ] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/bin/client.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import dns from "node:dns"; 3 | import ws from "ws"; 4 | import { maxSchemaVersion } from "../lib/const.js"; 5 | import { OutgoingMessage, ResultTypes } from "../lib/outgoing_message.js"; 6 | import { parseArgs } from "../util/parse-args.js"; 7 | 8 | dns.setDefaultResultOrder("ipv4first"); 9 | 10 | interface Args { 11 | _: Array; 12 | dump: boolean; 13 | node: string; 14 | schemaVersion: string; 15 | } 16 | 17 | const args = parseArgs(["_", "dump", "node", "schemaVersion"]); 18 | const schemaVersion = args.schemaVersion 19 | ? Number(args.schemaVersion) 20 | : maxSchemaVersion; 21 | const url = args._[0] || "ws://localhost:3000"; 22 | const filterNode = args.node ? Number(args.node) : undefined; 23 | 24 | if ( 25 | isNaN(schemaVersion) || 26 | schemaVersion > maxSchemaVersion || 27 | schemaVersion < 0 28 | ) { 29 | console.log("Schema version must be between 0 and ", maxSchemaVersion); 30 | process.exit(); 31 | } 32 | 33 | if (!args.dump) { 34 | console.info("Connecting to", url); 35 | } 36 | 37 | const socket = new ws(url); 38 | 39 | socket.on("open", function open() { 40 | socket.send( 41 | JSON.stringify({ 42 | messageId: "api-schema-id", 43 | command: "set_api_schema", 44 | schemaVersion: schemaVersion, 45 | }), 46 | ); 47 | socket.send( 48 | JSON.stringify({ 49 | messageId: "start-listening-result", 50 | command: "start_listening", 51 | }), 52 | ); 53 | }); 54 | 55 | socket.on("message", (data) => { 56 | const msg = JSON.parse(data.toString()) as OutgoingMessage; 57 | 58 | if (filterNode) { 59 | if (msg.type !== "result" && msg.type !== "event") { 60 | return; 61 | } 62 | 63 | if ( 64 | msg.type === "result" && 65 | msg.messageId === "start-listening-result" && 66 | msg.success 67 | ) { 68 | const state = (msg.result as ResultTypes["start_listening"]).state; 69 | 70 | const nodes = state.nodes.filter((node) => node.nodeId === filterNode); 71 | 72 | if (nodes.length !== 1) { 73 | console.error("Unable to find node", filterNode); 74 | process.exit(1); 75 | } 76 | 77 | state.nodes = nodes; 78 | } else if (msg.type === "event" && msg.event.nodeId !== filterNode) { 79 | return; 80 | } 81 | } 82 | 83 | if (args.dump) { 84 | console.log(JSON.stringify(msg)); 85 | } else { 86 | console.dir(msg); 87 | } 88 | }); 89 | 90 | let closing = false; 91 | const handleShutdown = () => { 92 | // Pressing ctrl+c twice. 93 | if (closing) { 94 | process.exit(); 95 | } 96 | 97 | // Close gracefully 98 | closing = true; 99 | if (!args.dump) { 100 | console.log("Shutting down"); 101 | } 102 | socket.close(); 103 | process.exit(); 104 | }; 105 | process.on("SIGINT", handleShutdown); 106 | process.on("SIGTERM", handleShutdown); 107 | -------------------------------------------------------------------------------- /src/bin/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { resolve } from "path"; 3 | import { 4 | Driver, 5 | PartialZWaveOptions, 6 | ZWaveError, 7 | ZWaveErrorCodes, 8 | driverPresets, 9 | } from "zwave-js"; 10 | import { ZwavejsServer } from "../lib/server.js"; 11 | import { createMockDriver } from "../mock/index.js"; 12 | import { parseArgs } from "../util/parse-args.js"; 13 | import { createRequire } from "node:module"; 14 | const require = createRequire(import.meta.url); 15 | 16 | const normalizeKey = (key: Buffer | string, keyName: string): Buffer => { 17 | if (Buffer.isBuffer(key)) return key; 18 | if (key.length === 32) return Buffer.from(key, "hex"); 19 | // Convert from OpenZWave format 20 | key = key.toLowerCase(); 21 | if (key.includes("0x")) 22 | return Buffer.from(key.replace(/0x/g, "").replace(/, /g, ""), "hex"); 23 | throw new Error(`Invalid key format for ${keyName} option`); 24 | }; 25 | 26 | interface Args { 27 | _: Array; 28 | config?: string; 29 | "mock-driver": boolean; 30 | port?: number; 31 | host?: string; 32 | "disable-dns-sd": boolean; 33 | } 34 | 35 | (async () => { 36 | const args = parseArgs([ 37 | "_", 38 | "config", 39 | "mock-driver", 40 | "port", 41 | "host", 42 | "disable-dns-sd", 43 | ]); 44 | 45 | if (args.port) { 46 | if (typeof args["port"] !== "number") { 47 | throw new Error("port must be a valid integer"); 48 | } 49 | } 50 | 51 | if (args["mock-driver"]) { 52 | args._.push("mock-serial-port"); 53 | } 54 | 55 | if (args._.length < 1) { 56 | console.error("Error: Missing path to serial port"); 57 | return; 58 | } 59 | 60 | const serialPort = args._[0]; 61 | 62 | let configPath = args.config; 63 | if (configPath && configPath.substring(0, 1) !== "/") { 64 | configPath = resolve(process.cwd(), configPath); 65 | } 66 | 67 | let options; 68 | let presetNames: string[] | string | undefined; 69 | 70 | if (configPath) { 71 | try { 72 | // Pull presets out of options so we can pass them to the driver 73 | ({ presets: presetNames, ...options } = require(configPath)); 74 | // If both securityKeys.S0_Legacy and networkKey are defined, throw an error. 75 | if (options.securityKeys?.S0_Legacy && options.networkKey) { 76 | throw new Error( 77 | "Both `networkKey` and `securityKeys.S0_Legacy` options are present in the " + 78 | "config. Remove `networkKey`.", 79 | ); 80 | } 81 | const securityKeyNames = [ 82 | "S0_Legacy", 83 | "S2_AccessControl", 84 | "S2_Authenticated", 85 | "S2_Unauthenticated", 86 | ]; 87 | // We prefer the securityKeys option over the networkKey one 88 | if (options.securityKeys) { 89 | for (const key of securityKeyNames) { 90 | if (key in options.securityKeys) { 91 | options.securityKeys[key] = normalizeKey( 92 | options.securityKeys[key], 93 | `securityKeys.${key}`, 94 | ); 95 | } 96 | } 97 | } 98 | // If we get here, securityKeys.S0_Legacy is not defined, so we can safely use networkKey 99 | // make sure that networkKey is passed as buffer and accept both zwave2mqtt format and ozw format 100 | if (options.networkKey) { 101 | if (!options.securityKeys) options.securityKeys = {}; 102 | options.securityKeys.S0_Legacy = normalizeKey( 103 | options.networkKey, 104 | "networkKey", 105 | ); 106 | console.warn( 107 | "The `networkKey` option is deprecated in favor of `securityKeys` option. To eliminate " + 108 | "this warning, move your networkKey into the securityKeys.S0_Legacy option. Refer to " + 109 | "the Z-Wave JS docs for more information", 110 | ); 111 | delete options.networkKey; 112 | } else if (!options.securityKeys?.S0_Legacy) 113 | throw new Error("Error: `securityKeys.S0_Legacy` key is missing."); 114 | 115 | // Support long range keys 116 | const securityKeysLongRangeNames = [ 117 | "S2_AccessControl", 118 | "S2_Authenticated", 119 | ]; 120 | if (options.securityKeysLongRange) { 121 | for (const key of securityKeysLongRangeNames) { 122 | if (key in options.securityKeysLongRange) { 123 | options.securityKeysLongRange[key] = normalizeKey( 124 | options.securityKeysLongRange[key], 125 | `securityKeysLongRange.${key}`, 126 | ); 127 | } 128 | } 129 | } 130 | } catch (err) { 131 | console.error(`Error: failed loading config file ${configPath}`); 132 | console.error(err); 133 | return; 134 | } 135 | } 136 | 137 | if (!options) { 138 | options = { emitValueUpdateAfterSetValue: true }; 139 | } else if (!("emitValueUpdateAfterSetValue" in options)) { 140 | options["emitValueUpdateAfterSetValue"] = true; 141 | } else if (!options["emitValueUpdateAfterSetValue"]) { 142 | console.warn( 143 | "Because `emitValueUpdateAfterSetValue` is set to false, multi-client setups will not work " + 144 | "as expected. In particular, clients will not see value updates that are initiated by " + 145 | "another client.", 146 | ); 147 | } 148 | 149 | // Normalize the presets 150 | let presets: PartialZWaveOptions[] | undefined; 151 | if (presetNames !== undefined) { 152 | if (typeof presetNames === "string") { 153 | presetNames = [presetNames]; 154 | } else if ( 155 | !Array.isArray(presetNames) || 156 | !presetNames.every((p) => typeof p === "string") 157 | ) { 158 | throw new Error( 159 | "presets must be an array of strings or a string if provided", 160 | ); 161 | } 162 | presets = presetNames 163 | .map< 164 | PartialZWaveOptions | undefined 165 | >((name) => (driverPresets as any)[name]) 166 | .filter((preset): preset is PartialZWaveOptions => preset !== undefined); 167 | } 168 | const driver = args["mock-driver"] 169 | ? createMockDriver() 170 | : new Driver(serialPort, options, ...(presets ?? [])); 171 | 172 | driver.on("error", (e) => { 173 | console.error("Error in driver", e); 174 | // Driver_Failed cannot be recovered by zwave-js so we shut down 175 | if (e instanceof ZWaveError && e.code === ZWaveErrorCodes.Driver_Failed) { 176 | handleShutdown(1); 177 | } 178 | }); 179 | 180 | let server: ZwavejsServer; 181 | 182 | driver.once("driver ready", async () => { 183 | server = new ZwavejsServer(driver, { 184 | port: args.port, 185 | host: args.host, 186 | enableDNSServiceDiscovery: !args["disable-dns-sd"], 187 | }); 188 | await server.start(true); 189 | }); 190 | 191 | await driver.start(); 192 | 193 | let closing = false; 194 | 195 | const handleShutdown = async (exitCode = 0) => { 196 | // Pressing ctrl+c twice. 197 | if (closing) { 198 | process.exit(exitCode); 199 | } 200 | 201 | // Close gracefully 202 | closing = true; 203 | console.log("Shutting down"); 204 | if (server) { 205 | await server.destroy(); 206 | } 207 | if (driver) { 208 | await driver.destroy(); 209 | } 210 | process.exit(exitCode); 211 | }; 212 | 213 | process.on("SIGINT", () => handleShutdown(0)); 214 | process.on("SIGTERM", () => handleShutdown(0)); 215 | })().catch((err) => { 216 | console.error("Unable to start driver", err); 217 | process.exit(1); 218 | }); 219 | -------------------------------------------------------------------------------- /src/lib/broadcast_node/command.ts: -------------------------------------------------------------------------------- 1 | export enum BroadcastNodeCommand { 2 | setValue = "broadcast_node.set_value", 3 | getEndpointCount = "broadcast_node.get_endpoint_count", 4 | supportsCC = "broadcast_node.supports_cc", 5 | getCCVersion = "broadcast_node.get_cc_version", 6 | invokeCCAPI = "broadcast_node.invoke_cc_api", 7 | supportsCCAPI = "broadcast_node.supports_cc_api", 8 | getDefinedValueIDs = "multicast_group.get_defined_value_ids", 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/broadcast_node/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { CommandClasses, ValueID } from "@zwave-js/core"; 2 | import { SetValueAPIOptions } from "zwave-js"; 3 | import { IncomingCommandBase } from "../incoming_message_base.js"; 4 | import { BroadcastNodeCommand } from "./command.js"; 5 | 6 | export interface IncomingCommandBroadcastNodeBase extends IncomingCommandBase {} 7 | 8 | export interface IncomingCommandBroadcastNodeSetValue 9 | extends IncomingCommandBroadcastNodeBase { 10 | command: BroadcastNodeCommand.setValue; 11 | valueId: ValueID; 12 | value: unknown; 13 | options?: SetValueAPIOptions; 14 | } 15 | 16 | export interface IncomingCommandBroadcastNodeGetEndpointCount 17 | extends IncomingCommandBroadcastNodeBase { 18 | command: BroadcastNodeCommand.getEndpointCount; 19 | } 20 | 21 | export interface IncomingCommandBroadcastNodeSupportsCC 22 | extends IncomingCommandBroadcastNodeBase { 23 | command: BroadcastNodeCommand.supportsCC; 24 | index: number; 25 | commandClass: CommandClasses; 26 | } 27 | 28 | export interface IncomingCommandBroadcastNodeGetCCVersion 29 | extends IncomingCommandBroadcastNodeBase { 30 | command: BroadcastNodeCommand.getCCVersion; 31 | index: number; 32 | commandClass: CommandClasses; 33 | } 34 | 35 | export interface IncomingCommandBroadcastNodeInvokeCCAPI 36 | extends IncomingCommandBroadcastNodeBase { 37 | command: BroadcastNodeCommand.invokeCCAPI; 38 | index?: number; 39 | commandClass: CommandClasses; 40 | methodName: string; 41 | args: unknown[]; 42 | } 43 | 44 | export interface IncomingCommandBroadcastNodeSupportsCCAPI 45 | extends IncomingCommandBroadcastNodeBase { 46 | command: BroadcastNodeCommand.supportsCCAPI; 47 | index?: number; 48 | commandClass: CommandClasses; 49 | } 50 | 51 | export interface IncomingCommandBroadcastNodeGetDefinedValueIDs 52 | extends IncomingCommandBroadcastNodeBase { 53 | command: BroadcastNodeCommand.getDefinedValueIDs; 54 | } 55 | 56 | export type IncomingMessageBroadcastNode = 57 | | IncomingCommandBroadcastNodeSetValue 58 | | IncomingCommandBroadcastNodeGetEndpointCount 59 | | IncomingCommandBroadcastNodeSupportsCC 60 | | IncomingCommandBroadcastNodeGetCCVersion 61 | | IncomingCommandBroadcastNodeInvokeCCAPI 62 | | IncomingCommandBroadcastNodeSupportsCCAPI 63 | | IncomingCommandBroadcastNodeGetDefinedValueIDs; 64 | -------------------------------------------------------------------------------- /src/lib/broadcast_node/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { Driver, VirtualEndpoint, VirtualNode } from "zwave-js"; 2 | import { UnknownCommandError, VirtualEndpointNotFoundError } from "../error.js"; 3 | import { BroadcastNodeCommand } from "./command.js"; 4 | import { IncomingMessageBroadcastNode } from "./incoming_message.js"; 5 | import { BroadcastNodeResultTypes } from "./outgoing_message.js"; 6 | import { Client } from "../server.js"; 7 | import { setValueOutgoingMessage } from "../common.js"; 8 | import { MessageHandler } from "../message_handler.js"; 9 | 10 | export class BroadcastNodeMessageHandler implements MessageHandler { 11 | constructor( 12 | private driver: Driver, 13 | private client: Client, 14 | ) {} 15 | 16 | async handle( 17 | message: IncomingMessageBroadcastNode, 18 | ): Promise { 19 | const { command } = message; 20 | 21 | const virtualNode = this.driver.controller.getBroadcastNode(); 22 | 23 | switch (message.command) { 24 | case BroadcastNodeCommand.setValue: { 25 | const result = await virtualNode.setValue( 26 | message.valueId, 27 | message.value, 28 | message.options, 29 | ); 30 | return setValueOutgoingMessage(result, this.client.schemaVersion); 31 | } 32 | case BroadcastNodeCommand.getEndpointCount: { 33 | const count = virtualNode.getEndpointCount(); 34 | return { count }; 35 | } 36 | case BroadcastNodeCommand.supportsCC: { 37 | const supported = getVirtualEndpoint( 38 | virtualNode, 39 | message.index, 40 | ).supportsCC(message.commandClass); 41 | return { supported }; 42 | } 43 | case BroadcastNodeCommand.getCCVersion: { 44 | const version = getVirtualEndpoint( 45 | virtualNode, 46 | message.index, 47 | ).getCCVersion(message.commandClass); 48 | return { version }; 49 | } 50 | case BroadcastNodeCommand.invokeCCAPI: { 51 | const response = getVirtualEndpoint( 52 | virtualNode, 53 | message.index, 54 | ).invokeCCAPI( 55 | message.commandClass, 56 | message.methodName, 57 | ...message.args, 58 | ); 59 | return { response }; 60 | } 61 | case BroadcastNodeCommand.supportsCCAPI: { 62 | const supported = getVirtualEndpoint( 63 | virtualNode, 64 | message.index, 65 | ).supportsCCAPI(message.commandClass); 66 | return { supported }; 67 | } 68 | case BroadcastNodeCommand.getDefinedValueIDs: { 69 | const valueIDs = virtualNode.getDefinedValueIDs(); 70 | return { valueIDs }; 71 | } 72 | default: { 73 | throw new UnknownCommandError(command); 74 | } 75 | } 76 | } 77 | } 78 | 79 | function getVirtualEndpoint( 80 | virtualNode: VirtualNode, 81 | index?: number, 82 | ): VirtualEndpoint { 83 | if (!index) return virtualNode; 84 | const virtualEndpoint = virtualNode.getEndpoint(index); 85 | if (!virtualEndpoint) { 86 | throw new VirtualEndpointNotFoundError(index, undefined, true); 87 | } 88 | return virtualEndpoint; 89 | } 90 | -------------------------------------------------------------------------------- /src/lib/broadcast_node/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { VirtualValueID } from "zwave-js"; 2 | import { BroadcastNodeCommand } from "./command.js"; 3 | import { SetValueResultType } from "../common.js"; 4 | 5 | export interface BroadcastNodeResultTypes { 6 | [BroadcastNodeCommand.setValue]: SetValueResultType; 7 | [BroadcastNodeCommand.getEndpointCount]: { count: number }; 8 | [BroadcastNodeCommand.supportsCC]: { supported: boolean }; 9 | [BroadcastNodeCommand.getCCVersion]: { version: number }; 10 | [BroadcastNodeCommand.invokeCCAPI]: { response: unknown }; 11 | [BroadcastNodeCommand.supportsCCAPI]: { supported: boolean }; 12 | [BroadcastNodeCommand.getDefinedValueIDs]: { valueIDs: VirtualValueID[] }; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/command.ts: -------------------------------------------------------------------------------- 1 | export { BroadcastNodeCommand } from "./broadcast_node/command.js"; 2 | export { ConfigManagerCommand } from "./config_manager/command.js"; 3 | export { ControllerCommand } from "./controller/command.js"; 4 | export { DriverCommand } from "./driver/command.js"; 5 | export { EndpointCommand } from "./endpoint/command.js"; 6 | export { MulticastGroupCommand } from "./multicast_group/command.js"; 7 | export { NodeCommand } from "./node/command.js"; 8 | export { UtilsCommand } from "./utils/command.js"; 9 | 10 | export enum ServerCommand { 11 | startListening = "start_listening", 12 | updateLogConfig = "update_log_config", 13 | getLogConfig = "get_log_config", 14 | setApiSchema = "set_api_schema", 15 | initialize = "initialize", 16 | startListeningLogs = "start_listening_logs", 17 | stopListeningLogs = "stop_listening_logs", 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OTWFirmwareUpdateResult, 3 | Endpoint, 4 | FirmwareUpdateResult, 5 | ConfigValue, 6 | SetValueResult, 7 | SetValueStatus, 8 | ZWaveNode, 9 | } from "zwave-js"; 10 | import type { ConfigurationCCAPISetOptions } from "@zwave-js/cc"; 11 | import { SupervisionResult, MaybeNotKnown } from "@zwave-js/core"; 12 | import { 13 | IncomingCommandNodeGetRawConfigParameterValue, 14 | IncomingCommandNodeSetRawConfigParameterValue, 15 | } from "./node/incoming_message.js"; 16 | import { 17 | IncomingCommandEndpointGetRawConfigParameterValue, 18 | IncomingCommandEndpointSetRawConfigParameterValue, 19 | } from "./endpoint/incoming_message.js"; 20 | import { InvalidParamsPassedToCommandError } from "./error.js"; 21 | 22 | export type SetValueResultType = 23 | | { result: SetValueResult } // schemaVersion >= 29 24 | | { success: boolean }; // schemaVersion < 29 25 | 26 | export function setValueOutgoingMessage( 27 | result: SetValueResult, 28 | schemaVersion: number, 29 | ): SetValueResultType { 30 | if (schemaVersion < 29) { 31 | return { 32 | success: [ 33 | SetValueStatus.Working, 34 | SetValueStatus.Success, 35 | SetValueStatus.SuccessUnsupervised, 36 | ].includes(result.status), 37 | }; 38 | } 39 | return { result }; 40 | } 41 | 42 | export type FirmwareUpdateResultType = 43 | | { result: OTWFirmwareUpdateResult | FirmwareUpdateResult } // schemaVersion >= 29 44 | | { success: boolean }; // schemaVersion < 29 45 | 46 | // Schema version >= 41, driver command only 47 | export type OTWFirmwareUpdateResultType = { result: OTWFirmwareUpdateResult }; 48 | 49 | export function firmwareUpdateOutgoingMessage< 50 | T extends OTWFirmwareUpdateResult | FirmwareUpdateResult, 51 | >(result: T, schemaVersion: number): { result: T } | { success: boolean } { 52 | if (schemaVersion < 29) { 53 | return { 54 | success: result.success, 55 | }; 56 | } 57 | return { result }; 58 | } 59 | 60 | export async function setRawConfigParameterValue( 61 | message: 62 | | IncomingCommandNodeSetRawConfigParameterValue 63 | | IncomingCommandEndpointSetRawConfigParameterValue, 64 | nodeOrEndpoint: ZWaveNode | Endpoint, 65 | ): Promise<{ result?: SupervisionResult }> { 66 | if ( 67 | (message.valueSize !== undefined && message.valueFormat === undefined) || 68 | (message.valueSize === undefined && message.valueFormat !== undefined) 69 | ) { 70 | throw new InvalidParamsPassedToCommandError( 71 | "valueFormat and valueSize must be used in combination or not at all", 72 | ); 73 | } 74 | if (message.valueSize !== undefined && message.bitMask !== undefined) { 75 | throw new InvalidParamsPassedToCommandError( 76 | "bitMask cannot be used in combination with valueFormat and valueSize", 77 | ); 78 | } 79 | let options: ConfigurationCCAPISetOptions; 80 | if (message.bitMask !== undefined) { 81 | options = { 82 | parameter: message.parameter, 83 | bitMask: message.bitMask, 84 | value: message.value, 85 | }; 86 | } else { 87 | options = { 88 | parameter: message.parameter, 89 | valueFormat: message.valueFormat, 90 | valueSize: message.valueSize, 91 | value: message.value, 92 | }; 93 | } 94 | const result = await nodeOrEndpoint.commandClasses.Configuration.set(options); 95 | return { result }; 96 | } 97 | 98 | export async function getRawConfigParameterValue( 99 | message: 100 | | IncomingCommandNodeGetRawConfigParameterValue 101 | | IncomingCommandEndpointGetRawConfigParameterValue, 102 | nodeOrEndpoint: ZWaveNode | Endpoint, 103 | ): Promise<{ value: MaybeNotKnown }> { 104 | const value = await nodeOrEndpoint.commandClasses.Configuration.get( 105 | message.parameter, 106 | { 107 | valueBitMask: message.bitMask, 108 | }, 109 | ); 110 | return { value }; 111 | } 112 | -------------------------------------------------------------------------------- /src/lib/config_manager/command.ts: -------------------------------------------------------------------------------- 1 | export enum ConfigManagerCommand { 2 | loadManufacturers = "config_manager.load_manufacturers", 3 | lookupManufacturer = "config_manager.lookup_manufacturer", 4 | loadDeviceIndex = "config_manager.load_device_index", 5 | getIndex = "config_manager.get_index", 6 | loadFulltextDeviceIndex = "config_manager.load_fulltext_device_index", 7 | getFulltextIndex = "config_manager.get_fulltext_index", 8 | lookupDevice = "config_manager.lookup_device", 9 | lookupDevicePreserveConditions = "config_manager.lookup_device_preserve_conditions", 10 | manufacturers = "config_manager.manufacturers", 11 | loadAll = "config_manager.load_all", 12 | configVersion = "config_manager.config_version", 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/config_manager/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { IncomingCommandBase } from "../incoming_message_base.js"; 2 | import { ConfigManagerCommand } from "./command.js"; 3 | 4 | export interface IncomingCommandConfigManagerBase extends IncomingCommandBase {} 5 | 6 | export interface IncomingCommandConfigManagerLookupDevice 7 | extends IncomingCommandConfigManagerBase { 8 | command: ConfigManagerCommand.lookupDevice; 9 | manufacturerId: number; 10 | productType: number; 11 | productId: number; 12 | firmwareVersion?: string; 13 | } 14 | 15 | export interface IncomingCommandConfigManagerLoadManufacturers 16 | extends IncomingCommandConfigManagerBase { 17 | command: ConfigManagerCommand.loadManufacturers; 18 | } 19 | 20 | export interface IncomingCommandConfigManagerLookupManufacturer 21 | extends IncomingCommandConfigManagerBase { 22 | command: ConfigManagerCommand.lookupManufacturer; 23 | manufacturerId: number; 24 | } 25 | export interface IncomingCommandConfigManagerLoadDeviceIndex 26 | extends IncomingCommandConfigManagerBase { 27 | command: ConfigManagerCommand.loadDeviceIndex; 28 | } 29 | export interface IncomingCommandConfigManagerGetIndex 30 | extends IncomingCommandConfigManagerBase { 31 | command: ConfigManagerCommand.getIndex; 32 | } 33 | export interface IncomingCommandConfigManagerLoadFulltextDeviceIndex 34 | extends IncomingCommandConfigManagerBase { 35 | command: ConfigManagerCommand.loadFulltextDeviceIndex; 36 | } 37 | export interface IncomingCommandConfigManagerGetFulltextIndex 38 | extends IncomingCommandConfigManagerBase { 39 | command: ConfigManagerCommand.getFulltextIndex; 40 | } 41 | export interface IncomingCommandConfigManagerLookupDevicePreserveConditions 42 | extends IncomingCommandConfigManagerBase { 43 | command: ConfigManagerCommand.lookupDevicePreserveConditions; 44 | manufacturerId: number; 45 | productType: number; 46 | productId: number; 47 | firmwareVersion?: string; 48 | } 49 | export interface IncomingCommandConfigManagerManufacturers 50 | extends IncomingCommandConfigManagerBase { 51 | command: ConfigManagerCommand.manufacturers; 52 | } 53 | export interface IncomingCommandConfigManagerLoadAll 54 | extends IncomingCommandConfigManagerBase { 55 | command: ConfigManagerCommand.loadAll; 56 | } 57 | export interface IncomingCommandConfigManagerConfigVersion 58 | extends IncomingCommandConfigManagerBase { 59 | command: ConfigManagerCommand.configVersion; 60 | } 61 | export type IncomingMessageConfigManager = 62 | | IncomingCommandConfigManagerLookupDevice 63 | | IncomingCommandConfigManagerLoadManufacturers 64 | | IncomingCommandConfigManagerLookupManufacturer 65 | | IncomingCommandConfigManagerLoadDeviceIndex 66 | | IncomingCommandConfigManagerGetIndex 67 | | IncomingCommandConfigManagerLoadFulltextDeviceIndex 68 | | IncomingCommandConfigManagerGetFulltextIndex 69 | | IncomingCommandConfigManagerLookupDevicePreserveConditions 70 | | IncomingCommandConfigManagerManufacturers 71 | | IncomingCommandConfigManagerLoadAll 72 | | IncomingCommandConfigManagerConfigVersion; 73 | -------------------------------------------------------------------------------- /src/lib/config_manager/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { UnknownCommandError } from "../error.js"; 2 | import { ConfigManagerCommand } from "./command.js"; 3 | import { IncomingMessageConfigManager } from "./incoming_message.js"; 4 | import { ConfigManagerResultTypes } from "./outgoing_message.js"; 5 | import { MessageHandler } from "../message_handler.js"; 6 | import { Driver } from "zwave-js"; 7 | 8 | export class ConfigManagerMessageHandler implements MessageHandler { 9 | constructor(private driver: Driver) {} 10 | 11 | async handle( 12 | message: IncomingMessageConfigManager, 13 | ): Promise { 14 | const { command } = message; 15 | 16 | switch (message.command) { 17 | case ConfigManagerCommand.lookupDevice: { 18 | const config = await this.driver.configManager.lookupDevice( 19 | message.manufacturerId, 20 | message.productType, 21 | message.productId, 22 | message.firmwareVersion, 23 | ); 24 | return { config }; 25 | } 26 | case ConfigManagerCommand.loadManufacturers: { 27 | await this.driver.configManager.loadManufacturers(); 28 | return {}; 29 | } 30 | case ConfigManagerCommand.lookupManufacturer: { 31 | const name = this.driver.configManager.lookupManufacturer( 32 | message.manufacturerId, 33 | ); 34 | return { name }; 35 | } 36 | case ConfigManagerCommand.loadDeviceIndex: { 37 | await this.driver.configManager.loadDeviceIndex(); 38 | return {}; 39 | } 40 | case ConfigManagerCommand.getIndex: { 41 | const index = this.driver.configManager.getIndex(); 42 | return { index }; 43 | } 44 | case ConfigManagerCommand.loadFulltextDeviceIndex: { 45 | await this.driver.configManager.loadFulltextDeviceIndex(); 46 | return {}; 47 | } 48 | case ConfigManagerCommand.getFulltextIndex: { 49 | const index = this.driver.configManager.getFulltextIndex(); 50 | return { index }; 51 | } 52 | case ConfigManagerCommand.lookupDevicePreserveConditions: { 53 | const config = 54 | await this.driver.configManager.lookupDevicePreserveConditions( 55 | message.manufacturerId, 56 | message.productType, 57 | message.productId, 58 | message.firmwareVersion, 59 | ); 60 | return { config }; 61 | } 62 | case ConfigManagerCommand.manufacturers: { 63 | const manufacturers = this.driver.configManager.manufacturers; 64 | return { manufacturers }; 65 | } 66 | case ConfigManagerCommand.loadAll: { 67 | await this.driver.configManager.loadAll(); 68 | return {}; 69 | } 70 | case ConfigManagerCommand.configVersion: { 71 | return { configVersion: this.driver.configManager.configVersion }; 72 | } 73 | default: { 74 | throw new UnknownCommandError(command); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/lib/config_manager/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConditionalDeviceConfig, 3 | DeviceConfig, 4 | DeviceConfigIndex, 5 | FulltextDeviceConfigIndex, 6 | ManufacturersMap, 7 | } from "@zwave-js/config"; 8 | import { ConfigManagerCommand } from "./command.js"; 9 | 10 | export interface ConfigManagerResultTypes { 11 | [ConfigManagerCommand.lookupDevice]: { 12 | config?: DeviceConfig; 13 | }; 14 | [ConfigManagerCommand.loadManufacturers]: Record; 15 | [ConfigManagerCommand.lookupManufacturer]: { name?: string }; 16 | [ConfigManagerCommand.loadDeviceIndex]: Record; 17 | [ConfigManagerCommand.getIndex]: { index?: DeviceConfigIndex }; 18 | [ConfigManagerCommand.loadFulltextDeviceIndex]: Record; 19 | [ConfigManagerCommand.getFulltextIndex]: { 20 | index?: FulltextDeviceConfigIndex; 21 | }; 22 | [ConfigManagerCommand.lookupDevicePreserveConditions]: { 23 | config?: ConditionalDeviceConfig; 24 | }; 25 | [ConfigManagerCommand.manufacturers]: { manufacturers: ManufacturersMap }; 26 | [ConfigManagerCommand.loadAll]: Record; 27 | [ConfigManagerCommand.configVersion]: { configVersion: string }; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/const.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | const require = createRequire(import.meta.url); 3 | export const version = require("../../package.json").version; 4 | 5 | // minimal schema version the server supports 6 | export const minSchemaVersion = 0; 7 | 8 | // maximal/current schema version the server supports 9 | export const maxSchemaVersion = 42; 10 | 11 | export const applicationName = "zwave-js-server"; 12 | export const dnssdServiceType = applicationName; 13 | -------------------------------------------------------------------------------- /src/lib/controller/command.ts: -------------------------------------------------------------------------------- 1 | export enum ControllerCommand { 2 | beginInclusion = "controller.begin_inclusion", 3 | stopInclusion = "controller.stop_inclusion", 4 | cancelSecureBootstrapS2 = "controller.cancel_secure_bootstrap_s2", 5 | beginExclusion = "controller.begin_exclusion", 6 | stopExclusion = "controller.stop_exclusion", 7 | removeFailedNode = "controller.remove_failed_node", 8 | replaceFailedNode = "controller.replace_failed_node", 9 | // Schema version <= 31 10 | healNode = "controller.heal_node", 11 | // Schema version >= 32 12 | rebuildNodeRoutes = "controller.rebuild_node_routes", 13 | // Schema version <= 31 14 | beginHealingNetwork = "controller.begin_healing_network", 15 | // Schema version >= 32 16 | beginRebuildingRoutes = "controller.begin_rebuilding_routes", 17 | // Schema version <= 31 18 | stopHealingNetwork = "controller.stop_healing_network", 19 | // Schema version >= 32 20 | stopRebuildingRoutes = "controller.stop_rebuilding_routes", 21 | isFailedNode = "controller.is_failed_node", 22 | getAssociationGroups = "controller.get_association_groups", 23 | getAssociations = "controller.get_associations", 24 | checkAssociation = "controller.check_association", 25 | isAssociationAllowed = "controller.is_association_allowed", 26 | addAssociations = "controller.add_associations", 27 | removeAssociations = "controller.remove_associations", 28 | // Schema version < 3 29 | removeNodeFromAllAssocations = "controller.remove_node_from_all_assocations", 30 | // Schema version > 2 31 | removeNodeFromAllAssociations = "controller.remove_node_from_all_associations", 32 | getNodeNeighbors = "controller.get_node_neighbors", 33 | grantSecurityClasses = "controller.grant_security_classes", 34 | validateDSKAndEnterPIN = "controller.validate_dsk_and_enter_pin", 35 | provisionSmartStartNode = "controller.provision_smart_start_node", 36 | unprovisionSmartStartNode = "controller.unprovision_smart_start_node", 37 | getProvisioningEntry = "controller.get_provisioning_entry", 38 | getProvisioningEntries = "controller.get_provisioning_entries", 39 | supportsFeature = "controller.supports_feature", 40 | backupNVMRaw = "controller.backup_nvm_raw", 41 | restoreNVM = "controller.restore_nvm", 42 | setRFRegion = "controller.set_rf_region", 43 | getRFRegion = "controller.get_rf_region", 44 | setPowerlevel = "controller.set_powerlevel", 45 | getPowerlevel = "controller.get_powerlevel", 46 | getState = "controller.get_state", 47 | getKnownLifelineRoutes = "controller.get_known_lifeline_routes", 48 | getAnyFirmwareUpdateProgress = "controller.get_any_firmware_update_progress", 49 | isAnyOTAFirmwareUpdateInProgress = "controller.is_any_ota_firmware_update_in_progress", 50 | getAvailableFirmwareUpdates = "controller.get_available_firmware_updates", 51 | beginOTAFirmwareUpdate = "controller.begin_ota_firmware_update", 52 | firmwareUpdateOTA = "controller.firmware_update_ota", 53 | // Schema version 41+: use corresponding driver command instead 54 | firmwareUpdateOTW = "controller.firmware_update_otw", 55 | // Schema version 41+: use corresponding driver command instead 56 | isFirmwareUpdateInProgress = "controller.is_firmware_update_in_progress", 57 | setMaxLongRangePowerlevel = "controller.set_max_long_range_powerlevel", 58 | getMaxLongRangePowerlevel = "controller.get_max_long_range_powerlevel", 59 | setLongRangeChannel = "controller.set_long_range_channel", 60 | getLongRangeChannel = "controller.get_long_range_channel", 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/controller/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AssociationAddress, 3 | ExclusionOptions, 4 | ExclusionStrategy, 5 | FirmwareFileFormat, 6 | FirmwareUpdateFileInfo, 7 | FirmwareUpdateInfo, 8 | InclusionGrant, 9 | InclusionOptions, 10 | KEXFailType, 11 | MigrateNVMOptions, 12 | PlannedProvisioningEntry, 13 | RebuildRoutesOptions, 14 | ReplaceNodeOptions, 15 | RFRegion, 16 | ZWaveFeature, 17 | } from "zwave-js"; 18 | import type { QRProvisioningInformation } from "@zwave-js/core"; 19 | import { IncomingCommandBase } from "../incoming_message_base.js"; 20 | import { ControllerCommand } from "./command.js"; 21 | 22 | export interface IncomingCommandControllerBase extends IncomingCommandBase {} 23 | 24 | // Schema >= 8 25 | export interface IncomingCommandControllerBeginInclusion 26 | extends IncomingCommandControllerBase { 27 | command: ControllerCommand.beginInclusion; 28 | options: InclusionOptions; 29 | } 30 | 31 | // Schema <= 7 32 | export interface IncomingCommandControllerBeginInclusionLegacy 33 | extends IncomingCommandControllerBase { 34 | command: ControllerCommand.beginInclusion; 35 | includeNonSecure?: boolean; 36 | } 37 | 38 | export interface IncomingCommandControllerStopInclusion 39 | extends IncomingCommandControllerBase { 40 | command: ControllerCommand.stopInclusion; 41 | } 42 | 43 | export interface IncomingCommandControllerCancelSecureBootstrapS2 44 | extends IncomingCommandControllerBase { 45 | command: ControllerCommand.cancelSecureBootstrapS2; 46 | reason: KEXFailType; 47 | } 48 | 49 | export interface IncomingCommandControllerBeginExclusion // schema >=29 50 | extends IncomingCommandControllerBase { 51 | command: ControllerCommand.beginExclusion; 52 | options?: ExclusionOptions; 53 | } 54 | 55 | export interface IncomingCommandControllerBeginExclusionLegacy // schema < 29 56 | extends IncomingCommandControllerBase { 57 | command: ControllerCommand.beginExclusion; 58 | unprovision?: boolean | "inactive"; 59 | strategy?: ExclusionStrategy; 60 | } 61 | 62 | export interface IncomingCommandControllerStopExclusion 63 | extends IncomingCommandControllerBase { 64 | command: ControllerCommand.stopExclusion; 65 | } 66 | 67 | export interface IncomingCommandControllerRemoveFailedNode 68 | extends IncomingCommandControllerBase { 69 | command: ControllerCommand.removeFailedNode; 70 | nodeId: number; 71 | } 72 | 73 | // Schema >= 8 74 | export interface IncomingCommandControllerReplaceFailedNode 75 | extends IncomingCommandControllerBase { 76 | command: ControllerCommand.replaceFailedNode; 77 | nodeId: number; 78 | options: ReplaceNodeOptions; 79 | } 80 | 81 | // Schema <= 7 82 | export interface IncomingCommandControllerReplaceFailedNodeLegacy 83 | extends IncomingCommandControllerBase { 84 | command: ControllerCommand.replaceFailedNode; 85 | nodeId: number; 86 | includeNonSecure?: boolean; 87 | } 88 | 89 | // Schema <= 31 90 | export interface IncomingCommandControllerHealNode 91 | extends IncomingCommandControllerBase { 92 | command: ControllerCommand.healNode; 93 | nodeId: number; 94 | } 95 | 96 | // Schema >= 32 97 | export interface IncomingCommandControllerRebuildNodeRoutes 98 | extends IncomingCommandControllerBase { 99 | command: ControllerCommand.rebuildNodeRoutes; 100 | nodeId: number; 101 | } 102 | 103 | // Schema <= 31 104 | export interface IncomingCommandControllerBeginHealingNetwork 105 | extends IncomingCommandControllerBase { 106 | command: ControllerCommand.beginHealingNetwork; 107 | } 108 | 109 | // Schema >= 32 110 | export interface IncomingCommandControllerBeginRebuildingRoutes 111 | extends IncomingCommandControllerBase { 112 | command: ControllerCommand.beginRebuildingRoutes; 113 | options?: RebuildRoutesOptions; 114 | } 115 | 116 | // Schema <= 31 117 | export interface IncomingCommandControllerStopHealingNetwork 118 | extends IncomingCommandControllerBase { 119 | command: ControllerCommand.stopHealingNetwork; 120 | } 121 | 122 | // Schema >= 32 123 | export interface IncomingCommandControllerStopRebuildingRoutes 124 | extends IncomingCommandControllerBase { 125 | command: ControllerCommand.stopRebuildingRoutes; 126 | } 127 | 128 | export interface IncomingCommandControllerIsFailedNode 129 | extends IncomingCommandControllerBase { 130 | command: ControllerCommand.isFailedNode; 131 | nodeId: number; 132 | } 133 | 134 | export interface IncomingCommandControllerGetAssociationGroups 135 | extends IncomingCommandControllerBase { 136 | command: ControllerCommand.getAssociationGroups; 137 | nodeId: number; 138 | endpoint?: number; 139 | } 140 | 141 | export interface IncomingCommandControllerGetAssociations 142 | extends IncomingCommandControllerBase { 143 | command: ControllerCommand.getAssociations; 144 | nodeId: number; 145 | endpoint?: number; 146 | } 147 | 148 | export interface IncomingCommandControllerCheckAssociation 149 | extends IncomingCommandControllerBase { 150 | command: ControllerCommand.checkAssociation; 151 | nodeId: number; 152 | group: number; 153 | association: AssociationAddress; 154 | endpoint?: number; 155 | } 156 | 157 | export interface IncomingCommandControllerIsAssociationAllowed 158 | extends IncomingCommandControllerBase { 159 | command: ControllerCommand.isAssociationAllowed; 160 | nodeId: number; 161 | group: number; 162 | association: AssociationAddress; 163 | endpoint?: number; 164 | } 165 | 166 | export interface IncomingCommandControllerAddAssociations 167 | extends IncomingCommandControllerBase { 168 | command: ControllerCommand.addAssociations; 169 | nodeId: number; 170 | group: number; 171 | associations: AssociationAddress[]; 172 | endpoint?: number; 173 | } 174 | 175 | export interface IncomingCommandControllerRemoveAssociations 176 | extends IncomingCommandControllerBase { 177 | command: ControllerCommand.removeAssociations; 178 | nodeId: number; 179 | group: number; 180 | associations: AssociationAddress[]; 181 | endpoint?: number; 182 | } 183 | export interface IncomingCommandControllerRemoveNodeFromAllAssociations 184 | extends IncomingCommandControllerBase { 185 | command: 186 | | ControllerCommand.removeNodeFromAllAssociations 187 | | ControllerCommand.removeNodeFromAllAssocations; 188 | nodeId: number; 189 | } 190 | 191 | export interface IncomingCommandControllerGetNodeNeighbors 192 | extends IncomingCommandControllerBase { 193 | command: ControllerCommand.getNodeNeighbors; 194 | nodeId: number; 195 | } 196 | 197 | export interface IncomingCommandControllerGrantSecurityClasses 198 | extends IncomingCommandControllerBase { 199 | command: ControllerCommand.grantSecurityClasses; 200 | inclusionGrant: InclusionGrant; 201 | } 202 | 203 | export interface IncomingCommandControllerValidateDSKAndEnterPIN 204 | extends IncomingCommandControllerBase { 205 | command: ControllerCommand.validateDSKAndEnterPIN; 206 | pin: string; 207 | } 208 | 209 | export interface IncomingCommandControllerProvisionSmartStartNode 210 | extends IncomingCommandControllerBase { 211 | command: ControllerCommand.provisionSmartStartNode; 212 | entry: PlannedProvisioningEntry | string | QRProvisioningInformation; 213 | } 214 | 215 | export interface IncomingCommandControllerUnprovisionSmartStartNode 216 | extends IncomingCommandControllerBase { 217 | command: ControllerCommand.unprovisionSmartStartNode; 218 | dskOrNodeId: string | number; 219 | } 220 | 221 | export interface IncomingCommandControllerGetProvisioningEntry 222 | extends IncomingCommandControllerBase { 223 | command: ControllerCommand.getProvisioningEntry; 224 | // schema version < 17 225 | dsk?: string; 226 | // schema version > 16 227 | dskOrNodeId?: string | number; 228 | } 229 | 230 | export interface IncomingCommandControllerGetProvisioningEntries 231 | extends IncomingCommandControllerBase { 232 | command: ControllerCommand.getProvisioningEntries; 233 | } 234 | 235 | export interface IncomingCommandControllerSupportsFeature 236 | extends IncomingCommandControllerBase { 237 | command: ControllerCommand.supportsFeature; 238 | feature: ZWaveFeature; 239 | } 240 | 241 | export interface IncomingCommandControllerBackupNVMRaw 242 | extends IncomingCommandControllerBase { 243 | command: ControllerCommand.backupNVMRaw; 244 | } 245 | 246 | export interface IncomingCommandControllerRestoreNVM 247 | extends IncomingCommandControllerBase { 248 | command: ControllerCommand.restoreNVM; 249 | nvmData: string; 250 | migrateOptions?: MigrateNVMOptions; 251 | } 252 | 253 | export interface IncomingCommandControllerSetRFRegion 254 | extends IncomingCommandControllerBase { 255 | command: ControllerCommand.setRFRegion; 256 | region: RFRegion; 257 | } 258 | 259 | export interface IncomingCommandControllerGetRFRegion 260 | extends IncomingCommandControllerBase { 261 | command: ControllerCommand.getRFRegion; 262 | } 263 | 264 | export interface IncomingCommandControllerSetPowerlevel 265 | extends IncomingCommandControllerBase { 266 | command: ControllerCommand.setPowerlevel; 267 | powerlevel: number; 268 | measured0dBm: number; 269 | } 270 | 271 | export interface IncomingCommandControllerGetPowerlevel 272 | extends IncomingCommandControllerBase { 273 | command: ControllerCommand.getPowerlevel; 274 | } 275 | export interface IncomingCommandControllerGetState 276 | extends IncomingCommandControllerBase { 277 | command: ControllerCommand.getState; 278 | } 279 | 280 | export interface IncomingCommandControllerGetKnownLifelineRoutes 281 | extends IncomingCommandControllerBase { 282 | command: ControllerCommand.getKnownLifelineRoutes; 283 | } 284 | 285 | export interface IncomingCommandControllerIsAnyOTAFirmwareUpdateInProgress 286 | extends IncomingCommandControllerBase { 287 | command: 288 | | ControllerCommand.isAnyOTAFirmwareUpdateInProgress 289 | | ControllerCommand.getAnyFirmwareUpdateProgress; 290 | } 291 | 292 | export interface IncomingCommandControllerGetAvailableFirmwareUpdates 293 | extends IncomingCommandControllerBase { 294 | command: ControllerCommand.getAvailableFirmwareUpdates; 295 | nodeId: number; 296 | apiKey?: string; 297 | includePrereleases?: boolean; 298 | } 299 | 300 | // Schema <= 23 - no longer supported due to a breaking change upstream 301 | export interface IncomingCommandControllerBeginOTAFirmwareUpdate 302 | extends IncomingCommandControllerBase { 303 | command: ControllerCommand.beginOTAFirmwareUpdate; 304 | nodeId: number; 305 | update: FirmwareUpdateFileInfo; 306 | } 307 | 308 | // Schema > 23 309 | export interface IncomingCommandControllerFirmwareUpdateOTA 310 | extends IncomingCommandControllerBase { 311 | command: ControllerCommand.firmwareUpdateOTA; 312 | nodeId: number; 313 | updates?: FirmwareUpdateFileInfo[]; 314 | updateInfo?: FirmwareUpdateInfo; 315 | } 316 | 317 | export interface IncomingCommandControllerFirmwareUpdateOTW 318 | extends IncomingCommandControllerBase { 319 | command: ControllerCommand.firmwareUpdateOTW; 320 | filename: string; 321 | file: string; // use base64 encoding for the file 322 | fileFormat?: FirmwareFileFormat; 323 | } 324 | 325 | export interface IncomingCommandIsFirmwareUpdateInProgress 326 | extends IncomingCommandControllerBase { 327 | command: ControllerCommand.isFirmwareUpdateInProgress; 328 | } 329 | 330 | export interface IncomingCommandControllerSetMaxLongRangePowerlevel 331 | extends IncomingCommandControllerBase { 332 | command: ControllerCommand.setMaxLongRangePowerlevel; 333 | limit: number; 334 | } 335 | 336 | export interface IncomingCommandControllerGetMaxLongRangePowerlevel 337 | extends IncomingCommandControllerBase { 338 | command: ControllerCommand.getMaxLongRangePowerlevel; 339 | } 340 | 341 | export interface IncomingCommandControllerSetLongRangeChannel 342 | extends IncomingCommandControllerBase { 343 | command: ControllerCommand.setLongRangeChannel; 344 | channel: number; 345 | } 346 | 347 | export interface IncomingCommandControllerGetLongRangeChannel 348 | extends IncomingCommandControllerBase { 349 | command: ControllerCommand.getLongRangeChannel; 350 | } 351 | 352 | export type IncomingMessageController = 353 | | IncomingCommandControllerBeginInclusion 354 | | IncomingCommandControllerBeginInclusionLegacy 355 | | IncomingCommandControllerStopInclusion 356 | | IncomingCommandControllerCancelSecureBootstrapS2 357 | | IncomingCommandControllerBeginExclusion 358 | | IncomingCommandControllerBeginExclusionLegacy 359 | | IncomingCommandControllerStopExclusion 360 | | IncomingCommandControllerRemoveFailedNode 361 | | IncomingCommandControllerReplaceFailedNode 362 | | IncomingCommandControllerReplaceFailedNodeLegacy 363 | | IncomingCommandControllerHealNode 364 | | IncomingCommandControllerRebuildNodeRoutes 365 | | IncomingCommandControllerBeginHealingNetwork 366 | | IncomingCommandControllerBeginRebuildingRoutes 367 | | IncomingCommandControllerStopHealingNetwork 368 | | IncomingCommandControllerStopRebuildingRoutes 369 | | IncomingCommandControllerIsFailedNode 370 | | IncomingCommandControllerGetAssociationGroups 371 | | IncomingCommandControllerGetAssociations 372 | | IncomingCommandControllerCheckAssociation 373 | | IncomingCommandControllerIsAssociationAllowed 374 | | IncomingCommandControllerAddAssociations 375 | | IncomingCommandControllerRemoveAssociations 376 | | IncomingCommandControllerRemoveNodeFromAllAssociations 377 | | IncomingCommandControllerGetNodeNeighbors 378 | | IncomingCommandControllerGrantSecurityClasses 379 | | IncomingCommandControllerValidateDSKAndEnterPIN 380 | | IncomingCommandControllerProvisionSmartStartNode 381 | | IncomingCommandControllerUnprovisionSmartStartNode 382 | | IncomingCommandControllerGetProvisioningEntry 383 | | IncomingCommandControllerGetProvisioningEntries 384 | | IncomingCommandControllerSupportsFeature 385 | | IncomingCommandControllerBackupNVMRaw 386 | | IncomingCommandControllerRestoreNVM 387 | | IncomingCommandControllerSetRFRegion 388 | | IncomingCommandControllerGetRFRegion 389 | | IncomingCommandControllerSetPowerlevel 390 | | IncomingCommandControllerGetPowerlevel 391 | | IncomingCommandControllerGetState 392 | | IncomingCommandControllerGetKnownLifelineRoutes 393 | | IncomingCommandControllerIsAnyOTAFirmwareUpdateInProgress 394 | | IncomingCommandControllerGetAvailableFirmwareUpdates 395 | | IncomingCommandControllerBeginOTAFirmwareUpdate 396 | | IncomingCommandControllerFirmwareUpdateOTA 397 | | IncomingCommandControllerFirmwareUpdateOTW 398 | | IncomingCommandIsFirmwareUpdateInProgress 399 | | IncomingCommandControllerSetMaxLongRangePowerlevel 400 | | IncomingCommandControllerGetMaxLongRangePowerlevel 401 | | IncomingCommandControllerSetLongRangeChannel 402 | | IncomingCommandControllerGetLongRangeChannel; 403 | -------------------------------------------------------------------------------- /src/lib/controller/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseQRCodeString, 3 | Driver, 4 | InclusionOptions, 5 | InclusionStrategy, 6 | ReplaceNodeOptions, 7 | extractFirmware, 8 | guessFirmwareFileFormat, 9 | ExclusionStrategy, 10 | ExclusionOptions, 11 | AssociationCheckResult, 12 | } from "zwave-js"; 13 | import { 14 | InclusionAlreadyInProgressError, 15 | InclusionPhaseNotInProgressError, 16 | InvalidParamsPassedToCommandError, 17 | NoLongerSupportedError, 18 | UnknownCommandError, 19 | } from "../error.js"; 20 | import { Client, ClientsController } from "../server.js"; 21 | import { ControllerCommand } from "./command.js"; 22 | import { 23 | IncomingCommandControllerBeginExclusion, 24 | IncomingCommandControllerBeginExclusionLegacy, 25 | IncomingCommandControllerBeginInclusion, 26 | IncomingCommandControllerBeginInclusionLegacy, 27 | IncomingCommandControllerReplaceFailedNode, 28 | IncomingCommandControllerReplaceFailedNodeLegacy, 29 | IncomingMessageController, 30 | } from "./incoming_message.js"; 31 | import { ControllerResultTypes } from "./outgoing_message.js"; 32 | import { firmwareUpdateOutgoingMessage } from "../common.js"; 33 | import { inclusionUserCallbacks } from "../inclusion_user_callbacks.js"; 34 | import { MessageHandler } from "../message_handler.js"; 35 | import { dumpController } from "../state.js"; 36 | 37 | export class ControllerMessageHandler implements MessageHandler { 38 | constructor( 39 | private clientsController: ClientsController, 40 | private driver: Driver, 41 | private client: Client, 42 | ) {} 43 | 44 | async handle( 45 | message: IncomingMessageController, 46 | ): Promise { 47 | const { command } = message; 48 | 49 | switch (message.command) { 50 | case ControllerCommand.beginInclusion: { 51 | if ( 52 | this.clientsController.grantSecurityClassesPromise || 53 | this.clientsController.validateDSKAndEnterPinPromise 54 | ) 55 | throw new InclusionAlreadyInProgressError(); 56 | const success = await this.driver.controller.beginInclusion( 57 | await processInclusionOptions( 58 | this.clientsController, 59 | this.client, 60 | message, 61 | ), 62 | ); 63 | return { success }; 64 | } 65 | case ControllerCommand.grantSecurityClasses: { 66 | if (!this.clientsController.grantSecurityClassesPromise) 67 | throw new InclusionPhaseNotInProgressError( 68 | "grantSecurityClassesPromise", 69 | ); 70 | this.clientsController.grantSecurityClassesPromise.resolve( 71 | message.inclusionGrant, 72 | ); 73 | return {}; 74 | } 75 | case ControllerCommand.validateDSKAndEnterPIN: { 76 | if (!this.clientsController.validateDSKAndEnterPinPromise) 77 | throw new InclusionPhaseNotInProgressError( 78 | "validateDSKAndEnterPinPromise", 79 | ); 80 | this.clientsController.validateDSKAndEnterPinPromise.resolve( 81 | message.pin, 82 | ); 83 | return {}; 84 | } 85 | case ControllerCommand.provisionSmartStartNode: { 86 | if (typeof message.entry === "string") { 87 | this.driver.controller.provisionSmartStartNode( 88 | await parseQRCodeString(message.entry), 89 | ); 90 | } else { 91 | this.driver.controller.provisionSmartStartNode(message.entry); 92 | } 93 | return {}; 94 | } 95 | case ControllerCommand.unprovisionSmartStartNode: { 96 | this.driver.controller.unprovisionSmartStartNode(message.dskOrNodeId); 97 | return {}; 98 | } 99 | case ControllerCommand.getProvisioningEntry: { 100 | const dskOrNodeId = message.dskOrNodeId || message.dsk; 101 | if (!dskOrNodeId) { 102 | throw new InvalidParamsPassedToCommandError( 103 | "Must include one of dsk or dskOrNodeId in call to getProvisioningEntry", 104 | ); 105 | } 106 | const entry = this.driver.controller.getProvisioningEntry(dskOrNodeId); 107 | return { entry }; 108 | } 109 | case ControllerCommand.getProvisioningEntries: { 110 | const entries = this.driver.controller.getProvisioningEntries(); 111 | return { entries }; 112 | } 113 | case ControllerCommand.stopInclusion: { 114 | const success = await this.driver.controller.stopInclusion(); 115 | return { success }; 116 | } 117 | case ControllerCommand.cancelSecureBootstrapS2: { 118 | this.driver.controller.cancelSecureBootstrapS2(message.reason); 119 | return {}; 120 | } 121 | case ControllerCommand.beginExclusion: { 122 | const success = await this.driver.controller.beginExclusion( 123 | processExclusionOptions(message), 124 | ); 125 | return { success }; 126 | } 127 | case ControllerCommand.stopExclusion: { 128 | const success = await this.driver.controller.stopExclusion(); 129 | return { success }; 130 | } 131 | case ControllerCommand.removeFailedNode: { 132 | await this.driver.controller.removeFailedNode(message.nodeId); 133 | return {}; 134 | } 135 | case ControllerCommand.replaceFailedNode: { 136 | const success = await this.driver.controller.replaceFailedNode( 137 | message.nodeId, 138 | (await processInclusionOptions( 139 | this.clientsController, 140 | this.client, 141 | message, 142 | )) as ReplaceNodeOptions, 143 | ); 144 | return { success }; 145 | } 146 | // Schema <= 31 147 | case ControllerCommand.healNode: 148 | case ControllerCommand.rebuildNodeRoutes: { 149 | const success = await this.driver.controller.rebuildNodeRoutes( 150 | message.nodeId, 151 | ); 152 | return { success }; 153 | } 154 | // Schema <= 31 155 | case ControllerCommand.beginHealingNetwork: { 156 | const success = this.driver.controller.beginRebuildingRoutes(); 157 | return { success }; 158 | } 159 | // Schema >= 32 160 | case ControllerCommand.beginRebuildingRoutes: { 161 | const success = this.driver.controller.beginRebuildingRoutes( 162 | message.options!, 163 | ); 164 | return { success }; 165 | } 166 | // Schema <= 31 167 | case ControllerCommand.stopHealingNetwork: 168 | // Schema >= 32 169 | case ControllerCommand.stopRebuildingRoutes: { 170 | const success = this.driver.controller.stopRebuildingRoutes(); 171 | return { success }; 172 | } 173 | case ControllerCommand.isFailedNode: { 174 | const failed = await this.driver.controller.isFailedNode( 175 | message.nodeId, 176 | ); 177 | return { failed }; 178 | } 179 | case ControllerCommand.getAssociationGroups: { 180 | const groups: ControllerResultTypes[ControllerCommand.getAssociationGroups]["groups"] = 181 | {}; 182 | this.driver.controller 183 | .getAssociationGroups({ 184 | nodeId: message.nodeId, 185 | endpoint: message.endpoint, 186 | }) 187 | .forEach((value, key) => (groups[key] = value)); 188 | return { groups }; 189 | } 190 | case ControllerCommand.getAssociations: { 191 | const associations: ControllerResultTypes[ControllerCommand.getAssociations]["associations"] = 192 | {}; 193 | this.driver.controller 194 | .getAssociations({ 195 | nodeId: message.nodeId, 196 | endpoint: message.endpoint, 197 | }) 198 | .forEach((value, key) => (associations[key] = value)); 199 | return { associations }; 200 | } 201 | case ControllerCommand.checkAssociation: { 202 | const result = this.driver.controller.checkAssociation( 203 | { nodeId: message.nodeId, endpoint: message.endpoint }, 204 | message.group, 205 | message.association, 206 | ); 207 | return { result }; 208 | } 209 | case ControllerCommand.isAssociationAllowed: { 210 | const result = this.driver.controller.checkAssociation( 211 | { nodeId: message.nodeId, endpoint: message.endpoint }, 212 | message.group, 213 | message.association, 214 | ); 215 | return { allowed: result === AssociationCheckResult.OK }; 216 | } 217 | case ControllerCommand.addAssociations: { 218 | await this.driver.controller.addAssociations( 219 | { nodeId: message.nodeId, endpoint: message.endpoint }, 220 | message.group, 221 | message.associations, 222 | ); 223 | return {}; 224 | } 225 | case ControllerCommand.removeAssociations: { 226 | await this.driver.controller.removeAssociations( 227 | { nodeId: message.nodeId, endpoint: message.endpoint }, 228 | message.group, 229 | message.associations, 230 | ); 231 | return {}; 232 | } 233 | case ControllerCommand.removeNodeFromAllAssocations: 234 | case ControllerCommand.removeNodeFromAllAssociations: { 235 | await this.driver.controller.removeNodeFromAllAssociations( 236 | message.nodeId, 237 | ); 238 | return {}; 239 | } 240 | case ControllerCommand.getNodeNeighbors: { 241 | const neighbors = await this.driver.controller.getNodeNeighbors( 242 | message.nodeId, 243 | ); 244 | return { neighbors }; 245 | } 246 | case ControllerCommand.supportsFeature: { 247 | const supported = this.driver.controller.supportsFeature( 248 | message.feature, 249 | ); 250 | return { supported }; 251 | } 252 | case ControllerCommand.backupNVMRaw: { 253 | const nvmDataRaw = await this.driver.controller.backupNVMRaw( 254 | (bytesRead: number, total: number) => { 255 | this.clientsController.clients.forEach((client) => 256 | this.client.sendEvent({ 257 | source: "controller", 258 | event: "nvm backup progress", 259 | bytesRead, 260 | total, 261 | }), 262 | ); 263 | }, 264 | ); 265 | return { nvmData: Buffer.from(nvmDataRaw.buffer).toString("base64") }; 266 | } 267 | case ControllerCommand.restoreNVM: { 268 | const nvmData = Buffer.from(message.nvmData, "base64"); 269 | await this.driver.controller.restoreNVM( 270 | nvmData, 271 | (bytesRead: number, total: number) => { 272 | this.clientsController.clients.forEach((client) => 273 | this.client.sendEvent({ 274 | source: "controller", 275 | event: "nvm convert progress", 276 | bytesRead, 277 | total, 278 | }), 279 | ); 280 | }, 281 | (bytesWritten: number, total: number) => { 282 | this.clientsController.clients.forEach((client) => 283 | this.client.sendEvent({ 284 | source: "controller", 285 | event: "nvm restore progress", 286 | bytesWritten, 287 | total, 288 | }), 289 | ); 290 | }, 291 | message.migrateOptions, 292 | ); 293 | return {}; 294 | } 295 | case ControllerCommand.setRFRegion: { 296 | const success = await this.driver.controller.setRFRegion( 297 | message.region, 298 | ); 299 | return { success }; 300 | } 301 | case ControllerCommand.getRFRegion: { 302 | const region = await this.driver.controller.getRFRegion(); 303 | return { region }; 304 | } 305 | case ControllerCommand.setPowerlevel: { 306 | const success = await this.driver.controller.setPowerlevel( 307 | message.powerlevel, 308 | message.measured0dBm, 309 | ); 310 | return { success }; 311 | } 312 | case ControllerCommand.getPowerlevel: { 313 | return await this.driver.controller.getPowerlevel(); 314 | } 315 | case ControllerCommand.getState: { 316 | const state = dumpController(this.driver, this.client.schemaVersion); 317 | return { state }; 318 | } 319 | case ControllerCommand.getKnownLifelineRoutes: { 320 | const routes = this.driver.controller.getKnownLifelineRoutes(); 321 | return { routes }; 322 | } 323 | case ControllerCommand.getAnyFirmwareUpdateProgress: 324 | case ControllerCommand.isAnyOTAFirmwareUpdateInProgress: { 325 | return { 326 | progress: this.driver.controller.isAnyOTAFirmwareUpdateInProgress(), 327 | }; 328 | } 329 | case ControllerCommand.getAvailableFirmwareUpdates: { 330 | return { 331 | updates: await this.driver.controller.getAvailableFirmwareUpdates( 332 | message.nodeId, 333 | { 334 | apiKey: message.apiKey, 335 | additionalUserAgentComponents: 336 | this.client.additionalUserAgentComponents, 337 | includePrereleases: message.includePrereleases, 338 | }, 339 | ), 340 | }; 341 | } 342 | case ControllerCommand.beginOTAFirmwareUpdate: { 343 | throw new NoLongerSupportedError( 344 | ControllerCommand.beginOTAFirmwareUpdate + 345 | " is a legacy command that is no longer supported.", 346 | ); 347 | } 348 | case ControllerCommand.firmwareUpdateOTA: { 349 | if (message.updates !== undefined) { 350 | throw new NoLongerSupportedError( 351 | ControllerCommand.firmwareUpdateOTA + 352 | " no longer accepts the `updates` parameter and expects `updateInfo` instead.", 353 | ); 354 | } 355 | if (message.updateInfo === undefined) { 356 | throw new InvalidParamsPassedToCommandError( 357 | "Missing required parameter `updateInfo`", 358 | ); 359 | } 360 | const result = await this.driver.controller.firmwareUpdateOTA( 361 | message.nodeId, 362 | message.updateInfo, 363 | ); 364 | return firmwareUpdateOutgoingMessage(result, this.client.schemaVersion); 365 | } 366 | case ControllerCommand.firmwareUpdateOTW: { 367 | const file = Buffer.from(message.file, "base64"); 368 | const { data } = await extractFirmware( 369 | file, 370 | message.fileFormat ?? guessFirmwareFileFormat(message.filename, file), 371 | ); 372 | const result = await this.driver.firmwareUpdateOTW(data); 373 | return firmwareUpdateOutgoingMessage(result, this.client.schemaVersion); 374 | } 375 | case ControllerCommand.isFirmwareUpdateInProgress: { 376 | const progress = this.driver.isOTWFirmwareUpdateInProgress(); 377 | return { progress }; 378 | } 379 | case ControllerCommand.setMaxLongRangePowerlevel: { 380 | const success = await this.driver.controller.setMaxLongRangePowerlevel( 381 | message.limit, 382 | ); 383 | return { success }; 384 | } 385 | case ControllerCommand.getMaxLongRangePowerlevel: { 386 | const limit = await this.driver.controller.getMaxLongRangePowerlevel(); 387 | return { 388 | limit, 389 | }; 390 | } 391 | case ControllerCommand.setLongRangeChannel: { 392 | const success = await this.driver.controller.setLongRangeChannel( 393 | message.channel, 394 | ); 395 | return { success }; 396 | } 397 | case ControllerCommand.getLongRangeChannel: { 398 | const response = await this.driver.controller.getLongRangeChannel(); 399 | return response; 400 | } 401 | default: { 402 | throw new UnknownCommandError(command); 403 | } 404 | } 405 | } 406 | } 407 | 408 | function processExclusionOptions( 409 | message: 410 | | IncomingCommandControllerBeginExclusion 411 | | IncomingCommandControllerBeginExclusionLegacy, 412 | ): ExclusionOptions | undefined { 413 | if ("options" in message) { 414 | return message.options; 415 | } else if ("unprovision" in message) { 416 | if (typeof message.unprovision === "boolean") { 417 | return { 418 | strategy: message.unprovision 419 | ? ExclusionStrategy.Unprovision 420 | : ExclusionStrategy.ExcludeOnly, 421 | }; 422 | } else if (message.unprovision === "inactive") { 423 | return { 424 | strategy: ExclusionStrategy.DisableProvisioningEntry, 425 | }; 426 | } 427 | } else if ("strategy" in message && message.strategy !== undefined) { 428 | return { strategy: message.strategy }; 429 | } 430 | } 431 | 432 | async function processInclusionOptions( 433 | clientsController: ClientsController, 434 | client: Client, 435 | message: 436 | | IncomingCommandControllerBeginInclusion 437 | | IncomingCommandControllerBeginInclusionLegacy 438 | | IncomingCommandControllerReplaceFailedNode 439 | | IncomingCommandControllerReplaceFailedNodeLegacy, 440 | ): Promise { 441 | // Schema 8+ inclusion handling 442 | if ("options" in message) { 443 | const options = message.options; 444 | if ( 445 | options.strategy === InclusionStrategy.Default || 446 | options.strategy === InclusionStrategy.Security_S2 447 | ) { 448 | // When using Security_S2 inclusion, the user can either provide the provisioning details ahead 449 | // of time or go through a standard inclusion process and let the this.driver/client prompt them 450 | // for provisioning details based on information received from the device. We have to handle 451 | // each scenario separately. 452 | if ("provisioning" in options) { 453 | // There are three input options when providing provisioning details ahead of time: 454 | // PlannedProvisioningEntry, QRProvisioningInformation, or a QR code string which the server 455 | // will automatically parse into a QRProvisioningInformation object before proceeding with the 456 | // inclusion process 457 | if (typeof options.provisioning === "string") { 458 | options.provisioning = await parseQRCodeString(options.provisioning); 459 | } 460 | } else { 461 | // @ts-expect-error 462 | options.userCallbacks = inclusionUserCallbacks( 463 | clientsController, 464 | client, 465 | ); 466 | } 467 | } 468 | return options; 469 | } 470 | // Schema <=7 inclusion handling (backwards compatibility logic) 471 | if ("includeNonSecure" in message && message.includeNonSecure) 472 | return { 473 | strategy: InclusionStrategy.Insecure, 474 | }; 475 | return { 476 | strategy: InclusionStrategy.Security_S0, 477 | }; 478 | } 479 | -------------------------------------------------------------------------------- /src/lib/controller/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AssociationAddress, 3 | AssociationCheckResult, 4 | AssociationGroup, 5 | FirmwareUpdateInfo, 6 | LifelineRoutes, 7 | RFRegion, 8 | SmartStartProvisioningEntry, 9 | } from "zwave-js"; 10 | import { LongRangeChannel } from "@zwave-js/core"; 11 | import { ControllerCommand } from "./command.js"; 12 | import { FirmwareUpdateResultType } from "../common.js"; 13 | import { ControllerState } from "../state.js"; 14 | 15 | export interface ControllerResultTypes { 16 | [ControllerCommand.beginInclusion]: { success: boolean }; 17 | [ControllerCommand.stopInclusion]: { success: boolean }; 18 | [ControllerCommand.cancelSecureBootstrapS2]: Record; 19 | [ControllerCommand.beginExclusion]: { success: boolean }; 20 | [ControllerCommand.stopExclusion]: { success: boolean }; 21 | [ControllerCommand.removeFailedNode]: Record; 22 | [ControllerCommand.replaceFailedNode]: { success: boolean }; 23 | [ControllerCommand.healNode]: { success: boolean }; 24 | [ControllerCommand.rebuildNodeRoutes]: { success: boolean }; 25 | [ControllerCommand.beginHealingNetwork]: { success: boolean }; 26 | [ControllerCommand.beginRebuildingRoutes]: { success: boolean }; 27 | [ControllerCommand.stopHealingNetwork]: { success: boolean }; 28 | [ControllerCommand.stopRebuildingRoutes]: { success: boolean }; 29 | [ControllerCommand.isFailedNode]: { failed: boolean }; 30 | [ControllerCommand.getAssociationGroups]: { 31 | groups: Record; 32 | }; 33 | [ControllerCommand.getAssociations]: { 34 | associations: Record; 35 | }; 36 | [ControllerCommand.checkAssociation]: { result: AssociationCheckResult }; 37 | [ControllerCommand.isAssociationAllowed]: { allowed: boolean }; 38 | [ControllerCommand.addAssociations]: Record; 39 | [ControllerCommand.removeAssociations]: Record; 40 | // Schema version < 3 41 | [ControllerCommand.removeNodeFromAllAssocations]: Record; 42 | // Schema version > 2 43 | [ControllerCommand.removeNodeFromAllAssociations]: Record; 44 | [ControllerCommand.getNodeNeighbors]: { neighbors: readonly number[] }; 45 | [ControllerCommand.grantSecurityClasses]: Record; 46 | [ControllerCommand.validateDSKAndEnterPIN]: Record; 47 | [ControllerCommand.provisionSmartStartNode]: Record; 48 | [ControllerCommand.unprovisionSmartStartNode]: Record; 49 | [ControllerCommand.getProvisioningEntry]: { 50 | entry: SmartStartProvisioningEntry | undefined; 51 | }; 52 | [ControllerCommand.getProvisioningEntries]: { 53 | entries: SmartStartProvisioningEntry[]; 54 | }; 55 | [ControllerCommand.supportsFeature]: { supported: boolean | undefined }; 56 | [ControllerCommand.backupNVMRaw]: { nvmData: string }; 57 | [ControllerCommand.restoreNVM]: Record; 58 | [ControllerCommand.setRFRegion]: { success: boolean }; 59 | [ControllerCommand.getRFRegion]: { region: RFRegion }; 60 | [ControllerCommand.setPowerlevel]: { success: boolean }; 61 | [ControllerCommand.getPowerlevel]: { 62 | powerlevel: number; 63 | measured0dBm: number; 64 | }; 65 | [ControllerCommand.getState]: { state: ControllerState }; 66 | [ControllerCommand.getKnownLifelineRoutes]: { 67 | routes: ReadonlyMap; 68 | }; 69 | [ControllerCommand.getAnyFirmwareUpdateProgress]: { 70 | progress: boolean; 71 | }; 72 | [ControllerCommand.isAnyOTAFirmwareUpdateInProgress]: { 73 | progress: boolean; 74 | }; 75 | [ControllerCommand.getAvailableFirmwareUpdates]: { 76 | updates: FirmwareUpdateInfo[]; 77 | }; 78 | [ControllerCommand.beginOTAFirmwareUpdate]: Record; 79 | [ControllerCommand.firmwareUpdateOTA]: FirmwareUpdateResultType; 80 | [ControllerCommand.firmwareUpdateOTW]: FirmwareUpdateResultType; 81 | [ControllerCommand.isFirmwareUpdateInProgress]: { progress: boolean }; 82 | [ControllerCommand.setMaxLongRangePowerlevel]: { success: boolean }; 83 | [ControllerCommand.getMaxLongRangePowerlevel]: { limit: number }; 84 | [ControllerCommand.setLongRangeChannel]: { success: boolean }; 85 | [ControllerCommand.getLongRangeChannel]: { 86 | channel: LongRangeChannel; 87 | supportsAutoChannelSelection: boolean; 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/lib/driver/command.ts: -------------------------------------------------------------------------------- 1 | export enum DriverCommand { 2 | getConfig = "driver.get_config", 3 | updateLogConfig = "driver.update_log_config", 4 | getLogConfig = "driver.get_log_config", 5 | enableStatistics = "driver.enable_statistics", 6 | disableStatistics = "driver.disable_statistics", 7 | isStatisticsEnabled = "driver.is_statistics_enabled", 8 | startListeningLogs = "driver.start_listening_logs", 9 | stopListeningLogs = "driver.stop_listening_logs", 10 | checkForConfigUpdates = "driver.check_for_config_updates", 11 | installConfigUpdate = "driver.install_config_update", 12 | setPreferredScales = "driver.set_preferred_scales", 13 | enableErrorReporting = "driver.enable_error_reporting", 14 | softReset = "driver.soft_reset", 15 | trySoftReset = "driver.try_soft_reset", 16 | hardReset = "driver.hard_reset", 17 | shutdown = "driver.shutdown", 18 | updateOptions = "driver.update_options", 19 | sendTestFrame = "driver.send_test_frame", 20 | // Schema version >= 41: 21 | firmwareUpdateOTW = "driver.firmware_update_otw", 22 | // Schema version >= 41: 23 | isOTWFirmwareUpdateInProgress = "driver.is_otw_firmware_update_in_progress", 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/driver/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { FirmwareFileFormat, LogConfig } from "@zwave-js/core"; 2 | import { DriverCommand } from "./command.js"; 3 | import { IncomingCommandBase } from "../incoming_message_base.js"; 4 | import { EditableZWaveOptions, Powerlevel, ZWaveOptions } from "zwave-js"; 5 | import { LogContexts } from "../logging.js"; 6 | 7 | interface IncomingCommandGetConfig extends IncomingCommandBase { 8 | command: DriverCommand.getConfig; 9 | } 10 | 11 | interface IncomingCommandUpdateLogConfig extends IncomingCommandBase { 12 | command: DriverCommand.updateLogConfig; 13 | config: Partial; 14 | } 15 | 16 | interface IncomingCommandGetLogConfig extends IncomingCommandBase { 17 | command: DriverCommand.getLogConfig; 18 | } 19 | 20 | interface IncomingCommandEnableStatistics extends IncomingCommandBase { 21 | command: DriverCommand.enableStatistics; 22 | applicationName: string; 23 | applicationVersion: string; 24 | } 25 | 26 | interface IncomingCommandDisableStatistics extends IncomingCommandBase { 27 | command: DriverCommand.disableStatistics; 28 | } 29 | 30 | interface IncomingCommandIsStatisticsEnabled extends IncomingCommandBase { 31 | command: DriverCommand.isStatisticsEnabled; 32 | } 33 | 34 | interface IncomingCommandStartListeningLogs extends IncomingCommandBase { 35 | command: DriverCommand.startListeningLogs; 36 | filter?: Partial; 37 | } 38 | 39 | interface IncomingCommandStopListeningLogs extends IncomingCommandBase { 40 | command: DriverCommand.stopListeningLogs; 41 | } 42 | 43 | interface IncomingCommandCheckForConfigUpdates extends IncomingCommandBase { 44 | command: DriverCommand.checkForConfigUpdates; 45 | } 46 | 47 | interface IncomingCommandInstallConfigUpdate extends IncomingCommandBase { 48 | command: DriverCommand.installConfigUpdate; 49 | } 50 | 51 | interface IncomingCommandSetPreferredScales extends IncomingCommandBase { 52 | command: DriverCommand.setPreferredScales; 53 | scales: ZWaveOptions["preferences"]["scales"]; 54 | } 55 | 56 | interface IncomingCommandEnableErrorReporting extends IncomingCommandBase { 57 | command: DriverCommand.enableErrorReporting; 58 | } 59 | 60 | interface IncomingCommandSoftReset extends IncomingCommandBase { 61 | command: DriverCommand.softReset; 62 | } 63 | 64 | interface IncomingCommandTrySoftReset extends IncomingCommandBase { 65 | command: DriverCommand.trySoftReset; 66 | } 67 | 68 | interface IncomingCommandHardReset extends IncomingCommandBase { 69 | command: DriverCommand.hardReset; 70 | } 71 | 72 | interface IncomingCommandShutdown extends IncomingCommandBase { 73 | command: DriverCommand.shutdown; 74 | } 75 | 76 | interface IncomingCommandUpdateOptions extends IncomingCommandBase { 77 | command: DriverCommand.updateOptions; 78 | options: EditableZWaveOptions; 79 | } 80 | 81 | interface IncomingCommandSendTestFrame extends IncomingCommandBase { 82 | command: DriverCommand.sendTestFrame; 83 | nodeId: number; 84 | powerlevel: Powerlevel; 85 | } 86 | 87 | export interface IncomingCommandFirmwareUpdateOTW extends IncomingCommandBase { 88 | command: DriverCommand.firmwareUpdateOTW; 89 | filename: string; 90 | file: string; // use base64 encoding for the file 91 | fileFormat?: FirmwareFileFormat; 92 | } 93 | 94 | export interface IncomingCommandIsOTWFirmwareUpdateInProgress 95 | extends IncomingCommandBase { 96 | command: DriverCommand.isOTWFirmwareUpdateInProgress; 97 | } 98 | 99 | export type IncomingMessageDriver = 100 | | IncomingCommandGetConfig 101 | | IncomingCommandUpdateLogConfig 102 | | IncomingCommandGetLogConfig 103 | | IncomingCommandDisableStatistics 104 | | IncomingCommandEnableStatistics 105 | | IncomingCommandIsStatisticsEnabled 106 | | IncomingCommandStartListeningLogs 107 | | IncomingCommandStopListeningLogs 108 | | IncomingCommandCheckForConfigUpdates 109 | | IncomingCommandInstallConfigUpdate 110 | | IncomingCommandSetPreferredScales 111 | | IncomingCommandEnableErrorReporting 112 | | IncomingCommandSoftReset 113 | | IncomingCommandTrySoftReset 114 | | IncomingCommandHardReset 115 | | IncomingCommandShutdown 116 | | IncomingCommandUpdateOptions 117 | | IncomingCommandSendTestFrame 118 | | IncomingCommandFirmwareUpdateOTW 119 | | IncomingCommandIsOTWFirmwareUpdateInProgress; 120 | -------------------------------------------------------------------------------- /src/lib/driver/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { Driver, extractFirmware, guessFirmwareFileFormat } from "zwave-js"; 2 | import { UnknownCommandError } from "../error.js"; 3 | import { 4 | Client, 5 | ClientsController, 6 | Logger, 7 | ZwavejsServerRemoteController, 8 | } from "../server.js"; 9 | import { DriverCommand } from "./command.js"; 10 | import { IncomingMessageDriver } from "./incoming_message.js"; 11 | import { DriverResultTypes } from "./outgoing_message.js"; 12 | import { dumpDriver, dumpLogConfig } from "../state.js"; 13 | import { MessageHandler } from "../message_handler.js"; 14 | import { firmwareUpdateOutgoingMessage } from "../common.js"; 15 | 16 | export class DriverMessageHandler implements MessageHandler { 17 | constructor( 18 | private remoteController: ZwavejsServerRemoteController, 19 | private clientsController: ClientsController, 20 | private logger: Logger, 21 | private driver: Driver, 22 | private client: Client, 23 | ) {} 24 | 25 | async handle( 26 | message: IncomingMessageDriver, 27 | ): Promise { 28 | const { command } = message; 29 | switch (message.command) { 30 | case DriverCommand.getConfig: { 31 | const config = dumpDriver(this.driver, this.client.schemaVersion); 32 | return { config }; 33 | } 34 | case DriverCommand.disableStatistics: { 35 | this.driver.disableStatistics(); 36 | return {}; 37 | } 38 | case DriverCommand.enableStatistics: { 39 | this.driver.enableStatistics({ 40 | applicationName: message.applicationName, 41 | applicationVersion: message.applicationVersion, 42 | }); 43 | return {}; 44 | } 45 | case DriverCommand.getLogConfig: { 46 | const config = dumpLogConfig(this.driver, this.client.schemaVersion); 47 | return { config }; 48 | } 49 | case DriverCommand.updateLogConfig: { 50 | this.driver.updateLogConfig(message.config); 51 | // If the logging event forwarder is enabled, we need to restart 52 | // it so that it picks up the new config. 53 | this.clientsController.restartLoggingEventForwarderIfNeeded(); 54 | this.clientsController.clients.forEach((cl) => { 55 | cl.sendEvent({ 56 | source: "driver", 57 | event: "log config updated", 58 | config: dumpLogConfig(this.driver, cl.schemaVersion), 59 | }); 60 | }); 61 | return {}; 62 | } 63 | case DriverCommand.isStatisticsEnabled: { 64 | const statisticsEnabled = this.driver.statisticsEnabled; 65 | return { statisticsEnabled }; 66 | } 67 | case DriverCommand.startListeningLogs: { 68 | this.client.receiveLogs = true; 69 | this.clientsController.configureLoggingEventForwarder(message.filter); 70 | return {}; 71 | } 72 | case DriverCommand.stopListeningLogs: { 73 | this.client.receiveLogs = false; 74 | this.clientsController.cleanupLoggingEventForwarder(); 75 | return {}; 76 | } 77 | case DriverCommand.checkForConfigUpdates: { 78 | const installedVersion = this.driver.configVersion; 79 | const newVersion = await this.driver.checkForConfigUpdates(); 80 | const updateAvailable = newVersion !== undefined; 81 | return { installedVersion, updateAvailable, newVersion }; 82 | } 83 | case DriverCommand.installConfigUpdate: { 84 | const success = await this.driver.installConfigUpdate(); 85 | return { success }; 86 | } 87 | case DriverCommand.setPreferredScales: { 88 | this.driver.setPreferredScales(message.scales); 89 | return {}; 90 | } 91 | case DriverCommand.enableErrorReporting: { 92 | // This capability no longer exists but we keep the command here for backwards 93 | // compatibility. 94 | this.logger.warn( 95 | "Z-Wave JS no longer supports enabling error reporting. If you are using " + 96 | "an application that integrates with Z-Wave JS and you receive this " + 97 | "error, you may need to update the application.", 98 | ); 99 | return {}; 100 | } 101 | case DriverCommand.softReset: { 102 | await this.driver.softReset(); 103 | return {}; 104 | } 105 | case DriverCommand.trySoftReset: { 106 | await this.driver.trySoftReset(); 107 | return {}; 108 | } 109 | case DriverCommand.hardReset: { 110 | setTimeout(() => this.remoteController.hardResetController(), 1); 111 | return {}; 112 | } 113 | case DriverCommand.shutdown: { 114 | const success = await this.driver.shutdown(); 115 | return { success }; 116 | } 117 | case DriverCommand.updateOptions: { 118 | this.driver.updateOptions(message.options); 119 | return {}; 120 | } 121 | case DriverCommand.sendTestFrame: { 122 | const status = await this.driver.sendTestFrame( 123 | message.nodeId, 124 | message.powerlevel, 125 | ); 126 | return { status }; 127 | } 128 | case DriverCommand.firmwareUpdateOTW: { 129 | const file = Buffer.from(message.file, "base64"); 130 | const { data } = await extractFirmware( 131 | file, 132 | message.fileFormat ?? guessFirmwareFileFormat(message.filename, file), 133 | ); 134 | const result = await this.driver.firmwareUpdateOTW(data); 135 | return { result }; 136 | } 137 | case DriverCommand.isOTWFirmwareUpdateInProgress: { 138 | const progress = this.driver.isOTWFirmwareUpdateInProgress(); 139 | return { progress }; 140 | } 141 | default: { 142 | throw new UnknownCommandError(command); 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/lib/driver/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { LogConfig, TransmitStatus } from "@zwave-js/core"; 2 | import { DriverState } from "../state.js"; 3 | import { DriverCommand } from "./command.js"; 4 | import { OTWFirmwareUpdateResultType } from "../common.js"; 5 | 6 | export interface DriverResultTypes { 7 | [DriverCommand.getConfig]: { config: DriverState }; 8 | [DriverCommand.updateLogConfig]: Record; 9 | [DriverCommand.getLogConfig]: { config: Partial }; 10 | [DriverCommand.disableStatistics]: Record; 11 | [DriverCommand.enableStatistics]: Record; 12 | [DriverCommand.isStatisticsEnabled]: { statisticsEnabled: boolean }; 13 | [DriverCommand.startListeningLogs]: Record; 14 | [DriverCommand.stopListeningLogs]: Record; 15 | [DriverCommand.checkForConfigUpdates]: { 16 | installedVersion: string; 17 | updateAvailable: boolean; 18 | newVersion?: string; 19 | }; 20 | [DriverCommand.installConfigUpdate]: { success: boolean }; 21 | [DriverCommand.setPreferredScales]: Record; 22 | [DriverCommand.enableErrorReporting]: Record; 23 | [DriverCommand.softReset]: Record; 24 | [DriverCommand.trySoftReset]: Record; 25 | [DriverCommand.hardReset]: Record; 26 | [DriverCommand.shutdown]: { success: boolean }; 27 | [DriverCommand.updateOptions]: Record; 28 | [DriverCommand.sendTestFrame]: { status?: TransmitStatus }; 29 | [DriverCommand.firmwareUpdateOTW]: OTWFirmwareUpdateResultType; 30 | [DriverCommand.isOTWFirmwareUpdateInProgress]: { progress: boolean }; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/endpoint/command.ts: -------------------------------------------------------------------------------- 1 | export enum EndpointCommand { 2 | invokeCCAPI = "endpoint.invoke_cc_api", 3 | supportsCCAPI = "endpoint.supports_cc_api", 4 | supportsCC = "endpoint.supports_cc", 5 | controlsCC = "endpoint.controls_cc", 6 | isCCSecure = "endpoint.is_cc_secure", 7 | getCCVersion = "endpoint.get_cc_version", 8 | getNodeUnsafe = "endpoint.get_node_unsafe", 9 | tryGetNode = "endpoint.try_get_node", 10 | setRawConfigParameterValue = "endpoint.set_raw_config_parameter_value", 11 | getRawConfigParameterValue = "endpoint.get_raw_config_parameter_value", 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/endpoint/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { CommandClasses, ConfigValue, ConfigValueFormat } from "@zwave-js/core"; 2 | import { IncomingCommandBase } from "../incoming_message_base.js"; 3 | import { EndpointCommand } from "./command.js"; 4 | 5 | export interface IncomingCommandEndpointBase extends IncomingCommandBase { 6 | nodeId: number; 7 | endpoint?: number; 8 | } 9 | 10 | export interface IncomingCommandEndpointInvokeCCAPI 11 | extends IncomingCommandEndpointBase { 12 | command: EndpointCommand.invokeCCAPI; 13 | commandClass: CommandClasses; 14 | methodName: string; 15 | args: unknown[]; 16 | } 17 | 18 | export interface IncomingCommandEndpointSupportsCCAPI 19 | extends IncomingCommandEndpointBase { 20 | command: EndpointCommand.supportsCCAPI; 21 | commandClass: CommandClasses; 22 | } 23 | 24 | export interface IncomingCommandEndpointSupportsCC 25 | extends IncomingCommandEndpointBase { 26 | command: EndpointCommand.supportsCC; 27 | commandClass: CommandClasses; 28 | } 29 | 30 | export interface IncomingCommandEndpointControlsCC 31 | extends IncomingCommandEndpointBase { 32 | command: EndpointCommand.controlsCC; 33 | commandClass: CommandClasses; 34 | } 35 | 36 | export interface IncomingCommandEndpointIsCCSecure 37 | extends IncomingCommandEndpointBase { 38 | command: EndpointCommand.isCCSecure; 39 | commandClass: CommandClasses; 40 | } 41 | 42 | export interface IncomingCommandEndpointGetCCVersion 43 | extends IncomingCommandEndpointBase { 44 | command: EndpointCommand.getCCVersion; 45 | commandClass: CommandClasses; 46 | } 47 | 48 | export interface IncomingCommandEndpointGetNodeUnsafe 49 | extends IncomingCommandEndpointBase { 50 | command: EndpointCommand.getNodeUnsafe; 51 | } 52 | 53 | export interface IncomingCommandEndpointTryGetNode 54 | extends IncomingCommandEndpointBase { 55 | command: EndpointCommand.tryGetNode; 56 | } 57 | 58 | export interface IncomingCommandEndpointSetRawConfigParameterValue 59 | extends IncomingCommandEndpointBase { 60 | command: EndpointCommand.setRawConfigParameterValue; 61 | parameter: number; 62 | bitMask?: number; 63 | value: ConfigValue; 64 | valueSize?: 1 | 2 | 4; // valueSize and valueFormat should be used together. 65 | valueFormat?: ConfigValueFormat; 66 | } 67 | 68 | export interface IncomingCommandEndpointGetRawConfigParameterValue 69 | extends IncomingCommandEndpointBase { 70 | command: EndpointCommand.getRawConfigParameterValue; 71 | parameter: number; 72 | bitMask?: number; 73 | } 74 | 75 | export type IncomingMessageEndpoint = 76 | | IncomingCommandEndpointInvokeCCAPI 77 | | IncomingCommandEndpointSupportsCCAPI 78 | | IncomingCommandEndpointSupportsCC 79 | | IncomingCommandEndpointControlsCC 80 | | IncomingCommandEndpointIsCCSecure 81 | | IncomingCommandEndpointGetCCVersion 82 | | IncomingCommandEndpointGetNodeUnsafe 83 | | IncomingCommandEndpointTryGetNode 84 | | IncomingCommandEndpointSetRawConfigParameterValue 85 | | IncomingCommandEndpointGetRawConfigParameterValue; 86 | -------------------------------------------------------------------------------- /src/lib/endpoint/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from "zwave-js"; 2 | import { 3 | EndpointNotFoundError, 4 | NodeNotFoundError, 5 | UnknownCommandError, 6 | } from "../error.js"; 7 | import { Client } from "../server.js"; 8 | import { dumpNode } from "../state.js"; 9 | import { EndpointCommand } from "./command.js"; 10 | import { IncomingMessageEndpoint } from "./incoming_message.js"; 11 | import { EndpointResultTypes } from "./outgoing_message.js"; 12 | import { 13 | getRawConfigParameterValue, 14 | setRawConfigParameterValue, 15 | } from "../common.js"; 16 | import { MessageHandler } from "../message_handler.js"; 17 | 18 | const isBufferObject = (obj: any): boolean => { 19 | return ( 20 | obj instanceof Object && 21 | Object.keys(obj).length === 2 && 22 | "type" in obj && 23 | obj.type === "Buffer" && 24 | "data" in obj && 25 | Array.isArray(obj.data) 26 | ); 27 | }; 28 | 29 | const deserializeBufferInArray = (array: Array): Array => { 30 | // Iterate over all items in array and deserialize any Buffer objects 31 | for (var idx = 0; idx < array.length; idx++) { 32 | const value = array[idx]; 33 | if (isBufferObject(value)) { 34 | array[idx] = Buffer.from(value.data); 35 | } 36 | } 37 | return array; 38 | }; 39 | 40 | export class EndpointMessageHandler implements MessageHandler { 41 | constructor( 42 | private driver: Driver, 43 | private client: Client, 44 | ) {} 45 | 46 | async handle( 47 | message: IncomingMessageEndpoint, 48 | ): Promise { 49 | const { nodeId, command } = message; 50 | let endpoint; 51 | 52 | const node = this.driver.controller.nodes.get(nodeId); 53 | if (!node) { 54 | throw new NodeNotFoundError(nodeId); 55 | } 56 | 57 | if (message.endpoint) { 58 | endpoint = node.getEndpoint(message.endpoint); 59 | if (!endpoint) { 60 | throw new EndpointNotFoundError(nodeId, message.endpoint); 61 | } 62 | } else { 63 | endpoint = node; 64 | } 65 | 66 | switch (message.command) { 67 | case EndpointCommand.invokeCCAPI: { 68 | const response = await endpoint.invokeCCAPI( 69 | message.commandClass, 70 | message.methodName, 71 | ...deserializeBufferInArray(message.args), 72 | ); 73 | return { response }; 74 | } 75 | case EndpointCommand.supportsCCAPI: { 76 | const supported = endpoint.supportsCCAPI(message.commandClass); 77 | return { supported }; 78 | } 79 | case EndpointCommand.supportsCC: { 80 | const supported = endpoint.supportsCC(message.commandClass); 81 | return { supported }; 82 | } 83 | case EndpointCommand.controlsCC: { 84 | const controlled = endpoint.controlsCC(message.commandClass); 85 | return { controlled }; 86 | } 87 | case EndpointCommand.isCCSecure: { 88 | const secure = endpoint.isCCSecure(message.commandClass); 89 | return { secure }; 90 | } 91 | case EndpointCommand.getCCVersion: { 92 | const version = endpoint.getCCVersion(message.commandClass); 93 | return { version }; 94 | } 95 | case EndpointCommand.getNodeUnsafe: 96 | case EndpointCommand.tryGetNode: { 97 | const node = endpoint.tryGetNode(); 98 | return { 99 | node: 100 | node === undefined 101 | ? node 102 | : dumpNode(node, this.client.schemaVersion), 103 | }; 104 | } 105 | case EndpointCommand.setRawConfigParameterValue: { 106 | return setRawConfigParameterValue(message, endpoint); 107 | } 108 | case EndpointCommand.getRawConfigParameterValue: { 109 | return getRawConfigParameterValue(message, endpoint); 110 | } 111 | default: { 112 | throw new UnknownCommandError(command); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/lib/endpoint/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { SupervisionResult, MaybeNotKnown, ConfigValue } from "@zwave-js/core"; 2 | import { EndpointCommand } from "./command.js"; 3 | import { NodeState } from "../state.js"; 4 | 5 | export interface EndpointResultTypes { 6 | [EndpointCommand.invokeCCAPI]: { response: unknown }; 7 | [EndpointCommand.supportsCCAPI]: { supported: boolean }; 8 | [EndpointCommand.supportsCC]: { supported: boolean }; 9 | [EndpointCommand.controlsCC]: { controlled: boolean }; 10 | [EndpointCommand.isCCSecure]: { secure: boolean }; 11 | [EndpointCommand.getCCVersion]: { version: number }; 12 | [EndpointCommand.getNodeUnsafe]: { node: NodeState | undefined }; 13 | [EndpointCommand.tryGetNode]: { node: NodeState | undefined }; 14 | [EndpointCommand.setRawConfigParameterValue]: { result?: SupervisionResult }; 15 | [EndpointCommand.getRawConfigParameterValue]: { 16 | value: MaybeNotKnown; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/error.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCode { 2 | unknownError = "unknown_error", 3 | unknownCommand = "unknown_command", 4 | nodeNotFound = "node_not_found", 5 | endpointNotFound = "endpoint_not_found", 6 | virtualEndpointNotFound = "virtual_endpoint_not_found", 7 | schemaIncompatible = "schema_incompatible", 8 | zwaveError = "zwave_error", 9 | inclusionPhaseNotInProgress = "inclusion_phase_not_in_progress", 10 | inclusionAlreadyInProgress = "inclusion_already_in_progress", 11 | invalidParamsPassedToCommand = "invalid_params_passed_to_command", 12 | noLongerSupported = "no_longer_supported", 13 | } 14 | 15 | export class BaseError extends Error { 16 | // @ts-ignore 17 | errorCode: ErrorCode; 18 | 19 | constructor(message?: string) { 20 | super(message); 21 | // We need to set the prototype explicitly 22 | Object.setPrototypeOf(this, BaseError.prototype); 23 | Object.getPrototypeOf(this).name = "BaseError"; 24 | } 25 | } 26 | 27 | export class UnknownError extends BaseError { 28 | errorCode = ErrorCode.unknownError; 29 | 30 | constructor(public error: Error) { 31 | super(); 32 | // We need to set the prototype explicitly 33 | Object.setPrototypeOf(this, UnknownError.prototype); 34 | Object.getPrototypeOf(this).name = "UnknownError"; 35 | } 36 | } 37 | 38 | export class UnknownCommandError extends BaseError { 39 | errorCode = ErrorCode.unknownCommand; 40 | 41 | constructor(public command: string) { 42 | super(); 43 | // We need to set the prototype explicitly 44 | Object.setPrototypeOf(this, UnknownCommandError.prototype); 45 | Object.getPrototypeOf(this).name = "UnknownCommandError"; 46 | } 47 | } 48 | 49 | export class NodeNotFoundError extends BaseError { 50 | errorCode = ErrorCode.nodeNotFound; 51 | 52 | constructor(public nodeId: number) { 53 | super(); 54 | // We need to set the prototype explicitly 55 | Object.setPrototypeOf(this, NodeNotFoundError.prototype); 56 | Object.getPrototypeOf(this).name = "NodeNotFoundError"; 57 | } 58 | } 59 | 60 | export class SchemaIncompatibleError extends BaseError { 61 | errorCode = ErrorCode.schemaIncompatible; 62 | 63 | constructor(public schemaId: number) { 64 | super(); 65 | // We need to set the prototype explicitly 66 | Object.setPrototypeOf(this, SchemaIncompatibleError.prototype); 67 | Object.getPrototypeOf(this).name = "SchemaIncompatibleError"; 68 | } 69 | } 70 | 71 | export class VirtualEndpointNotFoundError extends BaseError { 72 | errorCode = ErrorCode.virtualEndpointNotFound; 73 | 74 | constructor( 75 | public index: number, 76 | public nodeIDs?: number[], 77 | public broadcast?: boolean, 78 | ) { 79 | super(); 80 | // We need to set the prototype explicitly 81 | Object.setPrototypeOf(this, VirtualEndpointNotFoundError.prototype); 82 | Object.getPrototypeOf(this).name = "VirtualEndpointNotFoundError"; 83 | } 84 | } 85 | 86 | export class EndpointNotFoundError extends BaseError { 87 | errorCode = ErrorCode.endpointNotFound; 88 | 89 | constructor( 90 | public nodeId: number, 91 | public index: number, 92 | ) { 93 | super(); 94 | // We need to set the prototype explicitly 95 | Object.setPrototypeOf(this, EndpointNotFoundError.prototype); 96 | Object.getPrototypeOf(this).name = "EndpointNotFoundError"; 97 | } 98 | } 99 | 100 | export class InclusionPhaseNotInProgressError extends BaseError { 101 | errorCode = ErrorCode.inclusionPhaseNotInProgress; 102 | 103 | constructor(public phase: string) { 104 | super(); 105 | // We need to set the prototype explicitly 106 | Object.setPrototypeOf(this, InclusionPhaseNotInProgressError.prototype); 107 | Object.getPrototypeOf(this).name = "InclusionPhaseNotInProgressError"; 108 | } 109 | } 110 | 111 | export class InclusionAlreadyInProgressError extends BaseError { 112 | errorCode = ErrorCode.inclusionAlreadyInProgress; 113 | 114 | constructor() { 115 | super(); 116 | // We need to set the prototype explicitly 117 | Object.setPrototypeOf(this, InclusionAlreadyInProgressError.prototype); 118 | Object.getPrototypeOf(this).name = "InclusionAlreadyInProgressError"; 119 | } 120 | } 121 | 122 | export class InvalidParamsPassedToCommandError extends BaseError { 123 | errorCode = ErrorCode.invalidParamsPassedToCommand; 124 | 125 | constructor(message: string) { 126 | super(message); 127 | // We need to set the prototype explicitly 128 | Object.setPrototypeOf(this, InvalidParamsPassedToCommandError.prototype); 129 | Object.getPrototypeOf(this).name = "InvalidParamsPassedToCommandError"; 130 | } 131 | } 132 | 133 | export class NoLongerSupportedError extends BaseError { 134 | errorCode = ErrorCode.noLongerSupported; 135 | 136 | constructor(message: string) { 137 | super( 138 | message + 139 | " If you are using an application that integrates with Z-Wave JS and you receive this error, you may need to update the application.", 140 | ); 141 | // We need to set the prototype explicitly 142 | Object.setPrototypeOf(this, NoLongerSupportedError.prototype); 143 | Object.getPrototypeOf(this).name = "NoLongerSupportedError"; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/lib/forward.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ControllerEvents, 3 | Endpoint, 4 | FirmwareUpdateProgress, 5 | FirmwareUpdateResult, 6 | InclusionStrategy, 7 | NodeStatistics, 8 | NodeStatus, 9 | RemoveNodeReason, 10 | ZWaveNode, 11 | ZWaveNodeEvents, 12 | ZWaveNodeMetadataUpdatedArgs, 13 | } from "zwave-js"; 14 | import { CommandClasses, ConfigurationMetadata } from "@zwave-js/core"; 15 | import { OutgoingEvent } from "./outgoing_message.js"; 16 | import { 17 | dumpConfigurationMetadata, 18 | dumpFoundNode, 19 | dumpMetadata, 20 | dumpNode, 21 | } from "./state.js"; 22 | import { Client, ClientsController } from "./server.js"; 23 | import { NodeNotFoundError } from "./error.js"; 24 | 25 | export class EventForwarder { 26 | /** 27 | * Only load this once the driver is ready. 28 | * 29 | * @param clientsController 30 | */ 31 | constructor(private clientsController: ClientsController) {} 32 | 33 | start() { 34 | // Bind events for the controller and all existing nodes 35 | this.setupControllerAndNodes(); 36 | 37 | // Bind to driver events 38 | this.clientsController.driver.on("driver ready", () => { 39 | // Re-bind events for the controller and nodes after the "driver ready" event, 40 | // which implies that the old controller and node instances are no longer valid. 41 | this.setupControllerAndNodes(); 42 | 43 | // forward event to all connected clients, respecting schemaVersion it supports 44 | this.clientsController.clients.forEach((client) => { 45 | if (client.schemaVersion >= 40) { 46 | this.sendEvent(client, { 47 | source: "driver", 48 | event: "driver ready", 49 | }); 50 | } 51 | }); 52 | }); 53 | 54 | this.clientsController.driver.on("firmware update progress", (progress) => { 55 | // forward event to all connected clients, respecting schemaVersion it supports 56 | this.clientsController.clients.forEach((client) => { 57 | this.sendEvent(client, { 58 | source: client.schemaVersion >= 41 ? "driver" : "controller", 59 | event: "firmware update progress", 60 | progress, 61 | }); 62 | }); 63 | }); 64 | 65 | this.clientsController.driver.on("firmware update finished", (result) => { 66 | // forward event to all connected clients, respecting schemaVersion it supports 67 | this.clientsController.clients.forEach((client) => { 68 | this.sendEvent(client, { 69 | source: client.schemaVersion >= 41 ? "driver" : "controller", 70 | event: "firmware update finished", 71 | result, 72 | }); 73 | }); 74 | }); 75 | } 76 | 77 | forwardEvent(data: OutgoingEvent, minSchemaVersion?: number) { 78 | // Forward event to all clients 79 | this.clientsController.clients.forEach((client) => 80 | this.sendEvent(client, data, minSchemaVersion), 81 | ); 82 | } 83 | 84 | sendEvent(client: Client, data: OutgoingEvent, minSchemaVersion?: number) { 85 | // Send event to connected client only 86 | if ( 87 | client.receiveEvents && 88 | client.isConnected && 89 | client.schemaVersion >= (minSchemaVersion ?? 0) 90 | ) { 91 | client.sendEvent(data); 92 | } 93 | } 94 | 95 | setupControllerAndNodes() { 96 | // Bind events for all existing nodes 97 | this.clientsController.driver.controller.nodes.forEach((node) => 98 | this.setupNode(node), 99 | ); 100 | 101 | // Bind to all controller events 102 | // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/controller/Controller.ts#L112 103 | 104 | this.clientsController.driver.controller.on( 105 | "node added", 106 | (node, result) => { 107 | // forward event to all connected clients, respecting schemaVersion it supports 108 | this.clientsController.clients.forEach((client) => 109 | this.sendEvent(client, { 110 | source: "controller", 111 | event: "node added", 112 | node: dumpNode(node, client.schemaVersion), 113 | result, 114 | }), 115 | ); 116 | this.setupNode(node); 117 | }, 118 | ); 119 | 120 | this.clientsController.driver.controller.on("node found", (node) => { 121 | // forward event to all connected clients, respecting schemaVersion it supports 122 | this.clientsController.clients 123 | .filter((client) => client.schemaVersion > 18) 124 | .forEach((client) => 125 | this.sendEvent(client, { 126 | source: "controller", 127 | event: "node found", 128 | node: dumpFoundNode(node, client.schemaVersion), 129 | }), 130 | ); 131 | }); 132 | 133 | this.clientsController.driver.controller.on( 134 | "inclusion state changed", 135 | (state) => { 136 | // forward event to all connected clients, respecting schemaVersion it supports 137 | this.clientsController.clients 138 | .filter((client) => client.schemaVersion > 37) 139 | .forEach((client) => 140 | this.sendEvent(client, { 141 | source: "controller", 142 | event: "inclusion state changed", 143 | state, 144 | }), 145 | ); 146 | }, 147 | ); 148 | 149 | { 150 | const events: ControllerEvents[] = [ 151 | "inclusion failed", 152 | "exclusion failed", 153 | "exclusion started", 154 | "inclusion stopped", 155 | "exclusion stopped", 156 | ]; 157 | for (const event of events) { 158 | this.clientsController.driver.controller.on(event, () => 159 | this.forwardEvent({ 160 | source: "controller", 161 | event, 162 | }), 163 | ); 164 | } 165 | } 166 | 167 | this.clientsController.driver.controller.on( 168 | "inclusion started", 169 | (strategy) => { 170 | // forward event to all connected clients, respecting schemaVersion it supports 171 | this.clientsController.clients.forEach((client) => { 172 | if (client.schemaVersion >= 37) { 173 | this.sendEvent(client, { 174 | source: "controller", 175 | event: "inclusion started", 176 | strategy, 177 | }); 178 | } else { 179 | this.sendEvent(client, { 180 | source: "controller", 181 | event: "inclusion started", 182 | secure: strategy !== InclusionStrategy.Insecure, 183 | }); 184 | } 185 | }); 186 | }, 187 | ); 188 | 189 | this.clientsController.driver.controller.on( 190 | "node removed", 191 | (node, reason) => 192 | // forward event to all connected clients, respecting schemaVersion it supports 193 | this.clientsController.clients.forEach((client) => { 194 | if (client.schemaVersion < 29) { 195 | this.sendEvent(client, { 196 | source: "controller", 197 | event: "node removed", 198 | node: dumpNode(node, client.schemaVersion), 199 | replaced: [ 200 | RemoveNodeReason.Replaced, 201 | RemoveNodeReason.ProxyReplaced, 202 | ].includes(reason), 203 | }); 204 | } else { 205 | this.sendEvent(client, { 206 | source: "controller", 207 | event: "node removed", 208 | node: dumpNode(node, client.schemaVersion), 209 | reason, 210 | }); 211 | } 212 | }), 213 | ); 214 | 215 | this.clientsController.driver.controller.on( 216 | "rebuild routes progress", 217 | (progress) => { 218 | this.clientsController.clients.forEach((client) => { 219 | if (client.schemaVersion <= 31) { 220 | client.sendEvent({ 221 | source: "controller", 222 | event: "heal network progress", 223 | progress: Object.fromEntries(progress), 224 | }); 225 | } else { 226 | client.sendEvent({ 227 | source: "controller", 228 | event: "rebuild routes progress", 229 | progress: Object.fromEntries(progress), 230 | }); 231 | } 232 | }); 233 | }, 234 | ); 235 | 236 | this.clientsController.driver.controller.on("status changed", (status) => 237 | this.forwardEvent( 238 | { 239 | source: "controller", 240 | event: "status changed", 241 | status, 242 | }, 243 | 31, 244 | ), 245 | ); 246 | 247 | this.clientsController.driver.controller.on( 248 | "rebuild routes done", 249 | (result) => { 250 | this.clientsController.clients.forEach((client) => { 251 | if (client.schemaVersion <= 31) { 252 | client.sendEvent({ 253 | source: "controller", 254 | event: "heal network done", 255 | result: Object.fromEntries(result), 256 | }); 257 | } else { 258 | client.sendEvent({ 259 | source: "controller", 260 | event: "rebuild routes done", 261 | result: Object.fromEntries(result), 262 | }); 263 | } 264 | }); 265 | }, 266 | ); 267 | 268 | this.clientsController.driver.controller.on( 269 | "statistics updated", 270 | (statistics) => 271 | this.forwardEvent({ 272 | source: "controller", 273 | event: "statistics updated", 274 | statistics, 275 | }), 276 | ); 277 | 278 | this.clientsController.driver.controller.on("identify", (triggeringNode) => 279 | this.forwardEvent( 280 | { 281 | source: "controller", 282 | event: "identify", 283 | nodeId: triggeringNode.nodeId, 284 | }, 285 | 31, 286 | ), 287 | ); 288 | } 289 | 290 | setupNode(node: ZWaveNode) { 291 | // Bind to all node events 292 | // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/node/Types.ts#L84-L103 293 | const notifyNode = (node: ZWaveNode, event: string, extra = {}) => 294 | this.forwardEvent({ 295 | source: "node", 296 | event, 297 | nodeId: node.nodeId, 298 | ...extra, 299 | }); 300 | 301 | node.on("ready", (changedNode: ZWaveNode) => { 302 | // Dump full node state on ready event 303 | this.clientsController.clients.forEach((client) => 304 | this.sendEvent(client, { 305 | source: "node", 306 | event: "ready", 307 | nodeId: changedNode.nodeId, 308 | nodeState: dumpNode(changedNode, client.schemaVersion), 309 | }), 310 | ); 311 | }); 312 | 313 | { 314 | const events: ZWaveNodeEvents[] = ["wake up", "sleep", "dead", "alive"]; 315 | for (const event of events) { 316 | node.on(event, (changedNode: ZWaveNode, oldStatus: NodeStatus) => 317 | notifyNode(changedNode, event, { oldStatus }), 318 | ); 319 | } 320 | } 321 | 322 | { 323 | const events: ZWaveNodeEvents[] = [ 324 | "interview completed", 325 | "interview started", 326 | "interview failed", 327 | ]; 328 | for (const event of events) { 329 | node.on(event, (changedNode: ZWaveNode, args: any) => { 330 | notifyNode(changedNode, event, { args }); 331 | }); 332 | } 333 | } 334 | 335 | node.on( 336 | "interview stage completed", 337 | (changedNode: ZWaveNode, stageName: string) => { 338 | notifyNode(changedNode, "interview stage completed", { stageName }); 339 | }, 340 | ); 341 | 342 | { 343 | const events: ZWaveNodeEvents[] = [ 344 | "value updated", 345 | "value removed", 346 | "value added", 347 | "value notification", 348 | ]; 349 | for (const event of events) { 350 | node.on(event, (changedNode: ZWaveNode, args: any) => { 351 | // only forward value events for ready nodes 352 | if (!changedNode.ready) return; 353 | notifyNode(changedNode, event, { args }); 354 | }); 355 | } 356 | } 357 | 358 | node.on( 359 | "metadata updated", 360 | (changedNode: ZWaveNode, oldArgs: ZWaveNodeMetadataUpdatedArgs) => { 361 | // only forward value events for ready nodes 362 | if (!changedNode.ready) return; 363 | this.clientsController.clients.forEach((client) => { 364 | // Copy arguments for each client so transforms don't impact all clients 365 | const args = { ...oldArgs }; 366 | if (args.metadata != undefined) { 367 | if (args.commandClass === CommandClasses.Configuration) { 368 | args.metadata = dumpConfigurationMetadata( 369 | args.metadata as ConfigurationMetadata, 370 | client.schemaVersion, 371 | ); 372 | } else { 373 | args.metadata = dumpMetadata(args.metadata, client.schemaVersion); 374 | } 375 | } 376 | this.sendEvent(client, { 377 | source: "node", 378 | event: "metadata updated", 379 | nodeId: changedNode.nodeId, 380 | args, 381 | }); 382 | }); 383 | }, 384 | ); 385 | 386 | node.on( 387 | "notification", 388 | (endpoint: Endpoint, ccId: CommandClasses, args: any) => { 389 | // only forward value events for ready nodes 390 | const changedNode = endpoint.tryGetNode(); 391 | if (!changedNode) { 392 | throw new NodeNotFoundError(endpoint.nodeId); 393 | } 394 | if (!changedNode.ready) return; 395 | this.clientsController.clients.forEach((client) => { 396 | // Only send notification events from the Notification CC for schema version < 3 397 | if (client.schemaVersion < 3 && ccId == CommandClasses.Notification) { 398 | let eventData: OutgoingEvent = { 399 | source: "node", 400 | event: "notification", 401 | nodeId: changedNode.nodeId, 402 | notificationLabel: args.eventLabel, 403 | }; 404 | if ("parameters" in args) { 405 | eventData["parameters"] = args.parameters; 406 | } 407 | this.sendEvent(client, eventData); 408 | } else if (client.schemaVersion >= 3) { 409 | if (client.schemaVersion < 21) { 410 | if ( 411 | [ 412 | CommandClasses["Multilevel Switch"], 413 | CommandClasses["Entry Control"], 414 | ].includes(ccId) 415 | ) { 416 | delete args.eventTypeLabel; 417 | } 418 | if (ccId == CommandClasses["Entry Control"]) { 419 | delete args.dataTypeLabel; 420 | } 421 | } 422 | if (client.schemaVersion <= 31) { 423 | this.sendEvent(client, { 424 | source: "node", 425 | event: "notification", 426 | nodeId: changedNode.nodeId, 427 | ccId, 428 | args, 429 | }); 430 | } else { 431 | this.sendEvent(client, { 432 | source: "node", 433 | event: "notification", 434 | nodeId: endpoint.nodeId, 435 | endpointIndex: endpoint.index, 436 | ccId, 437 | args, 438 | }); 439 | } 440 | } 441 | }); 442 | }, 443 | ); 444 | 445 | node.on( 446 | "firmware update progress", 447 | (changedNode: ZWaveNode, progress: FirmwareUpdateProgress) => { 448 | // only forward value events for ready nodes 449 | if (!changedNode.ready) return; 450 | this.clientsController.clients.forEach((client) => { 451 | if (client.schemaVersion <= 23) { 452 | this.sendEvent(client, { 453 | source: "node", 454 | event: "firmware update progress", 455 | nodeId: changedNode.nodeId, 456 | sentFragments: progress.sentFragments, 457 | totalFragments: progress.totalFragments, 458 | }); 459 | } else { 460 | this.sendEvent(client, { 461 | source: "node", 462 | event: "firmware update progress", 463 | nodeId: changedNode.nodeId, 464 | progress, 465 | }); 466 | } 467 | }); 468 | }, 469 | ); 470 | 471 | node.on( 472 | "firmware update finished", 473 | (changedNode: ZWaveNode, result: FirmwareUpdateResult) => { 474 | // only forward value events for ready nodes 475 | if (!changedNode.ready) return; 476 | this.clientsController.clients.forEach((client) => { 477 | if (client.schemaVersion <= 23) { 478 | this.sendEvent(client, { 479 | source: "node", 480 | event: "firmware update finished", 481 | nodeId: changedNode.nodeId, 482 | status: result.status, 483 | waitTime: result.waitTime as any, 484 | }); 485 | } else { 486 | this.sendEvent(client, { 487 | source: "node", 488 | event: "firmware update finished", 489 | nodeId: changedNode.nodeId, 490 | result, 491 | }); 492 | } 493 | }); 494 | }, 495 | ); 496 | 497 | node.on( 498 | "statistics updated", 499 | (changedNode: ZWaveNode, statistics: NodeStatistics) => { 500 | notifyNode(changedNode, "statistics updated", { statistics }); 501 | }, 502 | ); 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /src/lib/inclusion_user_callbacks.ts: -------------------------------------------------------------------------------- 1 | import { InclusionUserCallbacks } from "zwave-js"; 2 | import { createDeferredPromise } from "alcalzone-shared/deferred-promise"; 3 | import { Client, ClientsController } from "./server.js"; 4 | 5 | export const inclusionUserCallbacks = ( 6 | clientsController: ClientsController, 7 | client?: Client, 8 | ): InclusionUserCallbacks => { 9 | return { 10 | grantSecurityClasses: (requested) => { 11 | clientsController.grantSecurityClassesPromise = createDeferredPromise(); 12 | clientsController.grantSecurityClassesPromise.catch(() => {}); 13 | clientsController.grantSecurityClassesPromise.finally(() => { 14 | if (clientsController.grantSecurityClassesPromise !== undefined) { 15 | delete clientsController.grantSecurityClassesPromise; 16 | } 17 | }); 18 | if (client !== undefined) { 19 | client.sendEvent({ 20 | source: "controller", 21 | event: "grant security classes", 22 | requested: requested, 23 | }); 24 | } else { 25 | clientsController.clients.forEach((client) => { 26 | if (client.isConnected && client.receiveEvents) { 27 | client.sendEvent({ 28 | source: "controller", 29 | event: "grant security classes", 30 | requested: requested, 31 | }); 32 | } 33 | }); 34 | } 35 | 36 | return clientsController.grantSecurityClassesPromise; 37 | }, 38 | validateDSKAndEnterPIN: (dsk) => { 39 | clientsController.validateDSKAndEnterPinPromise = createDeferredPromise(); 40 | clientsController.validateDSKAndEnterPinPromise.catch(() => {}); 41 | clientsController.validateDSKAndEnterPinPromise.finally(() => { 42 | if (clientsController.validateDSKAndEnterPinPromise != undefined) { 43 | delete clientsController.validateDSKAndEnterPinPromise; 44 | } 45 | }); 46 | if (client !== undefined) { 47 | client.sendEvent({ 48 | source: "controller", 49 | event: "validate dsk and enter pin", 50 | dsk, 51 | }); 52 | } else { 53 | clientsController.clients.forEach((client) => { 54 | if (client.isConnected && client.receiveEvents) { 55 | client.sendEvent({ 56 | source: "controller", 57 | event: "validate dsk and enter pin", 58 | dsk, 59 | }); 60 | } 61 | }); 62 | } 63 | return clientsController.validateDSKAndEnterPinPromise; 64 | }, 65 | abort: () => { 66 | delete clientsController.grantSecurityClassesPromise; 67 | delete clientsController.validateDSKAndEnterPinPromise; 68 | if (client !== undefined) { 69 | client.sendEvent({ 70 | source: "controller", 71 | event: "inclusion aborted", 72 | }); 73 | } else { 74 | clientsController.clients.forEach((client) => { 75 | if (client.isConnected && client.receiveEvents) { 76 | client.sendEvent({ 77 | source: "controller", 78 | event: "inclusion aborted", 79 | }); 80 | } 81 | }); 82 | } 83 | }, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/lib/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { LogConfig } from "@zwave-js/core"; 2 | import { IncomingMessageController } from "./controller/incoming_message.js"; 3 | import { ServerCommand } from "./command.js"; 4 | import { IncomingCommandBase } from "./incoming_message_base.js"; 5 | import { IncomingMessageNode } from "./node/incoming_message.js"; 6 | import { IncomingMessageDriver } from "./driver/incoming_message.js"; 7 | import { IncomingMessageBroadcastNode } from "./broadcast_node/incoming_message.js"; 8 | import { IncomingMessageMulticastGroup } from "./multicast_group/incoming_message.js"; 9 | import { IncomingMessageEndpoint } from "./endpoint/incoming_message.js"; 10 | import { IncomingMessageUtils } from "./utils/incoming_message.js"; 11 | import { IncomingMessageConfigManager } from "./config_manager/incoming_message.js"; 12 | import { LogContexts } from "./logging.js"; 13 | import { IncomingMessageZniffer } from "./zniffer/incoming_message.js"; 14 | 15 | interface IncomingCommandStartListening extends IncomingCommandBase { 16 | command: ServerCommand.startListening; 17 | } 18 | 19 | interface IncomingCommandUpdateLogConfig extends IncomingCommandBase { 20 | command: ServerCommand.updateLogConfig; 21 | config: Partial; 22 | } 23 | 24 | interface IncomingCommandGetLogConfig extends IncomingCommandBase { 25 | command: ServerCommand.getLogConfig; 26 | } 27 | 28 | interface IncomingCommandSetApiSchema extends IncomingCommandBase { 29 | command: ServerCommand.setApiSchema; 30 | schemaVersion: number; 31 | } 32 | 33 | interface IncomingCommandInitialize extends IncomingCommandBase { 34 | command: ServerCommand.initialize; 35 | schemaVersion: number; 36 | additionalUserAgentComponents?: Record; 37 | } 38 | 39 | interface IncomingCommandStartListeningLogs extends IncomingCommandBase { 40 | command: ServerCommand.startListeningLogs; 41 | filter?: Partial; 42 | } 43 | 44 | interface IncomingCommandStopListeningLogs extends IncomingCommandBase { 45 | command: ServerCommand.stopListeningLogs; 46 | } 47 | 48 | export type IncomingMessage = 49 | | IncomingCommandStartListening 50 | | IncomingCommandUpdateLogConfig 51 | | IncomingCommandGetLogConfig 52 | | IncomingCommandSetApiSchema 53 | | IncomingMessageNode 54 | | IncomingMessageController 55 | | IncomingMessageDriver 56 | | IncomingMessageMulticastGroup 57 | | IncomingMessageBroadcastNode 58 | | IncomingMessageEndpoint 59 | | IncomingMessageUtils 60 | | IncomingMessageZniffer 61 | | IncomingMessageConfigManager 62 | | IncomingCommandInitialize 63 | | IncomingCommandStartListeningLogs 64 | | IncomingCommandStopListeningLogs; 65 | -------------------------------------------------------------------------------- /src/lib/incoming_message_base.ts: -------------------------------------------------------------------------------- 1 | export interface IncomingCommandBase { 2 | messageId: string; 3 | command: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./command.js"; 2 | export * from "./state.js"; 3 | export { ZwavejsServer } from "./server.js"; 4 | export { version as serverVersion } from "./const.js"; 5 | -------------------------------------------------------------------------------- /src/lib/instance.ts: -------------------------------------------------------------------------------- 1 | export enum Instance { 2 | broadcast_node = "broadcast_node", 3 | config_manager = "config_manager", 4 | controller = "controller", 5 | driver = "driver", 6 | endpoint = "endpoint", 7 | multicast_group = "multicast_group", 8 | node = "node", 9 | utils = "utils", 10 | zniffer = "zniffer", 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/logging.ts: -------------------------------------------------------------------------------- 1 | import Transport from "winston-transport"; 2 | import { MESSAGE as messageSymbol } from "triple-beam"; 3 | import { ConfigLogContext } from "@zwave-js/config"; 4 | import { NodeLogContext } from "@zwave-js/core"; 5 | import { createDefaultTransportFormat } from "@zwave-js/core/bindings/log/node"; 6 | import type { ZWaveLogInfo } from "@zwave-js/core"; 7 | import { SerialLogContext } from "@zwave-js/serial"; 8 | import { ClientsController, Logger } from "./server.js"; 9 | import { ControllerLogContext, DriverLogContext, Driver } from "zwave-js"; 10 | 11 | export type LogContexts = 12 | | ConfigLogContext 13 | | ControllerLogContext 14 | | DriverLogContext 15 | | NodeLogContext 16 | | SerialLogContext; 17 | 18 | export class LoggingEventForwarder { 19 | /** 20 | * Only load this once the driver is ready. 21 | * 22 | * @param clients 23 | * @param driver 24 | */ 25 | private serverTransport?: WebSocketLogTransport; 26 | 27 | constructor( 28 | private clients: ClientsController, 29 | private driver: Driver, 30 | private logger: Logger, 31 | ) {} 32 | 33 | public get started(): boolean { 34 | return this.serverTransport !== undefined; 35 | } 36 | 37 | start(filter?: Partial) { 38 | var { transports, level } = this.driver.getLogConfig(); 39 | // Set the log level before attaching the transport 40 | this.logger.info("Starting logging event forwarder at " + level + " level"); 41 | this.serverTransport = new WebSocketLogTransport( 42 | level as string, 43 | this.clients, 44 | filter, 45 | ); 46 | transports = transports || []; 47 | transports.push(this.serverTransport); 48 | this.driver.updateLogConfig({ transports }); 49 | } 50 | 51 | stop() { 52 | this.logger.info("Stopping logging event forwarder"); 53 | const transports = this.driver 54 | .getLogConfig() 55 | .transports.filter((transport) => transport !== this.serverTransport); 56 | this.driver.updateLogConfig({ transports }); 57 | delete this.serverTransport; 58 | } 59 | 60 | restartIfNeeded() { 61 | var { level } = this.driver.getLogConfig(); 62 | if (this.started && this.serverTransport?.level != level) { 63 | this.stop(); 64 | this.start(); 65 | } 66 | } 67 | } 68 | 69 | class WebSocketLogTransport extends Transport { 70 | public constructor( 71 | level: string, 72 | private clients: ClientsController, 73 | private filter?: Partial, 74 | ) { 75 | super({ 76 | format: createDefaultTransportFormat(false, false), 77 | level, 78 | }); 79 | } 80 | 81 | public log(info: ZWaveLogInfo, next: () => void): any { 82 | const context: { [key: string]: any } = info.context; 83 | // If there is no filter or if all key/value pairs match from filter, forward 84 | // the message to the client 85 | if ( 86 | !this.filter || 87 | Object.entries(this.filter).every( 88 | ([key, value]) => key in context && context[key] === value, 89 | ) 90 | ) { 91 | // Forward logs on to clients that are currently 92 | // receiving logs 93 | this.clients.clients 94 | .filter((cl) => cl.receiveLogs && cl.isConnected) 95 | .forEach((client) => 96 | client.sendEvent({ 97 | source: "driver", 98 | event: "logging", 99 | formattedMessage: info[messageSymbol] as string, 100 | ...info, 101 | }), 102 | ); 103 | } 104 | next(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/lib/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from "./incoming_message.js"; 2 | import { ResultTypes } from "./outgoing_message.js"; 3 | 4 | export interface MessageHandler { 5 | handle(message: IncomingMessage): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/multicast_group/command.ts: -------------------------------------------------------------------------------- 1 | export enum MulticastGroupCommand { 2 | setValue = "multicast_group.set_value", 3 | getEndpointCount = "multicast_group.get_endpoint_count", 4 | supportsCC = "multicast_group.supports_cc", 5 | getCCVersion = "multicast_group.get_cc_version", 6 | invokeCCAPI = "multicast_group.invoke_cc_api", 7 | supportsCCAPI = "multicast_group.supports_cc_api", 8 | getDefinedValueIDs = "multicast_group.get_defined_value_ids", 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/multicast_group/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { CommandClasses, ValueID } from "@zwave-js/core"; 2 | import { SetValueAPIOptions } from "zwave-js"; 3 | import { IncomingCommandBase } from "../incoming_message_base.js"; 4 | import { MulticastGroupCommand } from "./command.js"; 5 | 6 | export interface IncomingCommandMulticastGroupBase extends IncomingCommandBase { 7 | nodeIDs: number[]; 8 | } 9 | 10 | export interface IncomingCommandMulticastGroupSetValue 11 | extends IncomingCommandMulticastGroupBase { 12 | command: MulticastGroupCommand.setValue; 13 | valueId: ValueID; 14 | value: unknown; 15 | options?: SetValueAPIOptions; 16 | } 17 | 18 | export interface IncomingCommandMulticastGroupGetEndpointCount 19 | extends IncomingCommandMulticastGroupBase { 20 | command: MulticastGroupCommand.getEndpointCount; 21 | } 22 | 23 | export interface IncomingCommandMulticastGroupSupportsCC 24 | extends IncomingCommandMulticastGroupBase { 25 | command: MulticastGroupCommand.supportsCC; 26 | index: number; 27 | commandClass: CommandClasses; 28 | } 29 | 30 | export interface IncomingCommandMulticastGroupGetCCVersion 31 | extends IncomingCommandMulticastGroupBase { 32 | command: MulticastGroupCommand.getCCVersion; 33 | index: number; 34 | commandClass: CommandClasses; 35 | } 36 | 37 | export interface IncomingCommandMulticastGroupInvokeCCAPI 38 | extends IncomingCommandMulticastGroupBase { 39 | command: MulticastGroupCommand.invokeCCAPI; 40 | index?: number; 41 | commandClass: CommandClasses; 42 | methodName: string; 43 | args: unknown[]; 44 | } 45 | 46 | export interface IncomingCommandMulticastGroupSupportsCCAPI 47 | extends IncomingCommandMulticastGroupBase { 48 | command: MulticastGroupCommand.supportsCCAPI; 49 | index?: number; 50 | commandClass: CommandClasses; 51 | } 52 | 53 | export interface IncomingCommandBroadcastNodeGetDefinedValueIDs 54 | extends IncomingCommandMulticastGroupBase { 55 | command: MulticastGroupCommand.getDefinedValueIDs; 56 | } 57 | 58 | export type IncomingMessageMulticastGroup = 59 | | IncomingCommandMulticastGroupSetValue 60 | | IncomingCommandMulticastGroupGetEndpointCount 61 | | IncomingCommandMulticastGroupSupportsCC 62 | | IncomingCommandMulticastGroupGetCCVersion 63 | | IncomingCommandMulticastGroupInvokeCCAPI 64 | | IncomingCommandMulticastGroupSupportsCCAPI 65 | | IncomingCommandBroadcastNodeGetDefinedValueIDs; 66 | -------------------------------------------------------------------------------- /src/lib/multicast_group/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { Driver, VirtualEndpoint, VirtualNode } from "zwave-js"; 2 | import { UnknownCommandError, VirtualEndpointNotFoundError } from "../error.js"; 3 | import { MulticastGroupCommand } from "./command.js"; 4 | import { IncomingMessageMulticastGroup } from "./incoming_message.js"; 5 | import { MulticastGroupResultTypes } from "./outgoing_message.js"; 6 | import { Client } from "../server.js"; 7 | import { setValueOutgoingMessage } from "../common.js"; 8 | import { MessageHandler } from "../message_handler.js"; 9 | 10 | export class MulticastGroupMessageHandler implements MessageHandler { 11 | constructor( 12 | private driver: Driver, 13 | private client: Client, 14 | ) {} 15 | 16 | async handle( 17 | message: IncomingMessageMulticastGroup, 18 | ): Promise { 19 | const { command } = message; 20 | 21 | const virtualNode = this.driver.controller.getMulticastGroup( 22 | message.nodeIDs, 23 | ); 24 | 25 | switch (message.command) { 26 | case MulticastGroupCommand.setValue: { 27 | const result = await virtualNode.setValue( 28 | message.valueId, 29 | message.value, 30 | message.options, 31 | ); 32 | return setValueOutgoingMessage(result, this.client.schemaVersion); 33 | } 34 | case MulticastGroupCommand.getEndpointCount: { 35 | const count = virtualNode.getEndpointCount(); 36 | return { count }; 37 | } 38 | case MulticastGroupCommand.supportsCC: { 39 | const supported = getVirtualEndpoint( 40 | virtualNode, 41 | message.nodeIDs, 42 | message.index, 43 | ).supportsCC(message.commandClass); 44 | return { supported }; 45 | } 46 | case MulticastGroupCommand.getCCVersion: { 47 | const version = getVirtualEndpoint( 48 | virtualNode, 49 | message.nodeIDs, 50 | message.index, 51 | ).getCCVersion(message.commandClass); 52 | return { version }; 53 | } 54 | case MulticastGroupCommand.invokeCCAPI: { 55 | const response = getVirtualEndpoint( 56 | virtualNode, 57 | message.nodeIDs, 58 | message.index, 59 | ).invokeCCAPI( 60 | message.commandClass, 61 | message.methodName, 62 | ...message.args, 63 | ); 64 | return { response }; 65 | } 66 | case MulticastGroupCommand.supportsCCAPI: { 67 | const supported = getVirtualEndpoint( 68 | virtualNode, 69 | message.nodeIDs, 70 | message.index, 71 | ).supportsCCAPI(message.commandClass); 72 | return { supported }; 73 | } 74 | case MulticastGroupCommand.getDefinedValueIDs: { 75 | const valueIDs = virtualNode.getDefinedValueIDs(); 76 | return { valueIDs }; 77 | } 78 | default: { 79 | throw new UnknownCommandError(command); 80 | } 81 | } 82 | } 83 | } 84 | 85 | function getVirtualEndpoint( 86 | virtualNode: VirtualNode, 87 | nodeIDs: number[], 88 | index?: number, 89 | ): VirtualEndpoint { 90 | if (!index) return virtualNode; 91 | const virtualEndpoint = virtualNode.getEndpoint(index); 92 | if (!virtualEndpoint) { 93 | throw new VirtualEndpointNotFoundError(index, nodeIDs, undefined); 94 | } 95 | return virtualEndpoint; 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/multicast_group/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { VirtualValueID } from "zwave-js"; 2 | import { MulticastGroupCommand } from "./command.js"; 3 | import { SetValueResultType } from "../common.js"; 4 | 5 | export interface MulticastGroupResultTypes { 6 | [MulticastGroupCommand.setValue]: SetValueResultType; 7 | [MulticastGroupCommand.getEndpointCount]: { count: number }; 8 | [MulticastGroupCommand.supportsCC]: { supported: boolean }; 9 | [MulticastGroupCommand.getCCVersion]: { version: number }; 10 | [MulticastGroupCommand.invokeCCAPI]: { response: unknown }; 11 | [MulticastGroupCommand.supportsCCAPI]: { supported: boolean }; 12 | [MulticastGroupCommand.getDefinedValueIDs]: { valueIDs: VirtualValueID[] }; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/node/command.ts: -------------------------------------------------------------------------------- 1 | export enum NodeCommand { 2 | setValue = "node.set_value", 3 | refreshInfo = "node.refresh_info", 4 | getDefinedValueIDs = "node.get_defined_value_ids", 5 | getValueMetadata = "node.get_value_metadata", 6 | beginFirmwareUpdate = "node.begin_firmware_update", 7 | updateFirmware = "node.update_firmware", 8 | abortFirmwareUpdate = "node.abort_firmware_update", 9 | pollValue = "node.poll_value", 10 | setRawConfigParameterValue = "node.set_raw_config_parameter_value", 11 | getRawConfigParameterValue = "node.get_raw_config_parameter_value", 12 | refreshValues = "node.refresh_values", 13 | refreshCCValues = "node.refresh_cc_values", 14 | ping = "node.ping", 15 | getFirmwareUpdateCapabilities = "node.get_firmware_update_capabilities", 16 | getFirmwareUpdateCapabilitiesCached = "node.get_firmware_update_capabilities_cached", 17 | hasSecurityClass = "node.has_security_class", 18 | getHighestSecurityClass = "node.get_highest_security_class", 19 | testPowerlevel = "node.test_powerlevel", 20 | checkLifelineHealth = "node.check_lifeline_health", 21 | checkRouteHealth = "node.check_route_health", 22 | getValue = "node.get_value", 23 | getEndpointCount = "node.get_endpoint_count", 24 | interviewCC = "node.interview_cc", 25 | getState = "node.get_state", 26 | setName = "node.set_name", 27 | setLocation = "node.set_location", 28 | setKeepAwake = "node.set_keep_awake", 29 | getFirmwareUpdateProgress = "node.get_firmware_update_progress", 30 | isFirmwareUpdateInProgress = "node.is_firmware_update_in_progress", 31 | waitForWakeup = "node.wait_for_wakeup", 32 | interview = "node.interview", 33 | getValueTimestamp = "node.get_value_timestamp", 34 | manuallyIdleNotificationValue = "node.manually_idle_notification_value", 35 | setDateAndTime = "node.set_date_and_time", 36 | getDateAndTime = "node.get_date_and_time", 37 | isHealthCheckInProgress = "node.is_health_check_in_progress", 38 | abortHealthCheck = "node.abort_health_check", 39 | setDefaultVolume = "node.set_default_volume", 40 | setDefaultTransitionDuration = "node.set_default_transition_duration", 41 | hasDeviceConfigChanged = "node.has_device_config_changed", 42 | createDump = "node.create_dump", 43 | getSupportedNotificationEvents = "node.get_supported_notification_events", 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/node/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Powerlevel, 3 | RefreshInfoOptions, 4 | SetValueAPIOptions, 5 | ValueID, 6 | } from "zwave-js"; 7 | import { 8 | CommandClasses, 9 | ConfigValue, 10 | ConfigValueFormat, 11 | FirmwareFileFormat, 12 | SecurityClass, 13 | } from "@zwave-js/core"; 14 | import { IncomingCommandBase } from "../incoming_message_base.js"; 15 | import { NodeCommand } from "./command.js"; 16 | 17 | export interface IncomingCommandNodeBase extends IncomingCommandBase { 18 | nodeId: number; 19 | } 20 | 21 | export interface IncomingCommandNodeSetValue extends IncomingCommandNodeBase { 22 | command: NodeCommand.setValue; 23 | valueId: ValueID; 24 | value: unknown; 25 | options?: SetValueAPIOptions; 26 | } 27 | 28 | export interface IncomingCommandNodeRefreshInfo 29 | extends IncomingCommandNodeBase { 30 | command: NodeCommand.refreshInfo; 31 | options?: RefreshInfoOptions; 32 | } 33 | 34 | export interface IncomingCommandNodeGetDefinedValueIDs 35 | extends IncomingCommandNodeBase { 36 | command: NodeCommand.getDefinedValueIDs; 37 | } 38 | 39 | export interface IncomingCommandNodeGetValueMetadata 40 | extends IncomingCommandNodeBase { 41 | command: NodeCommand.getValueMetadata; 42 | valueId: ValueID; 43 | } 44 | 45 | // Schema <= 23 46 | export interface IncomingCommandNodeBeginFirmwareUpdate 47 | extends IncomingCommandNodeBase { 48 | command: NodeCommand.beginFirmwareUpdate; 49 | firmwareFilename: string; 50 | firmwareFile: string; // use base64 encoding for the file 51 | firmwareFileFormat?: FirmwareFileFormat; 52 | target?: number; 53 | } 54 | 55 | // Schema > 23 56 | export interface IncomingCommandNodeUpdateFirmware 57 | extends IncomingCommandNodeBase { 58 | command: NodeCommand.updateFirmware; 59 | updates: { 60 | filename: string; 61 | file: string; // use base64 encoding for the file 62 | fileFormat?: FirmwareFileFormat; 63 | firmwareTarget?: number; 64 | }[]; 65 | } 66 | 67 | export interface IncomingCommandNodeAbortFirmwareUpdate 68 | extends IncomingCommandNodeBase { 69 | command: NodeCommand.abortFirmwareUpdate; 70 | } 71 | 72 | export interface IncomingCommandGetFirmwareUpdateCapabilities 73 | extends IncomingCommandNodeBase { 74 | command: NodeCommand.getFirmwareUpdateCapabilities; 75 | } 76 | 77 | export interface IncomingCommandGetFirmwareUpdateCapabilitiesCached 78 | extends IncomingCommandNodeBase { 79 | command: NodeCommand.getFirmwareUpdateCapabilitiesCached; 80 | } 81 | 82 | export interface IncomingCommandNodePollValue extends IncomingCommandNodeBase { 83 | command: NodeCommand.pollValue; 84 | valueId: ValueID; 85 | } 86 | 87 | export interface IncomingCommandNodeSetRawConfigParameterValue 88 | extends IncomingCommandNodeBase { 89 | command: NodeCommand.setRawConfigParameterValue; 90 | parameter: number; 91 | bitMask?: number; 92 | value: ConfigValue; 93 | valueSize?: 1 | 2 | 4; // valueSize and valueFormat should be used together. 94 | valueFormat?: ConfigValueFormat; 95 | } 96 | 97 | export interface IncomingCommandNodeGetRawConfigParameterValue 98 | extends IncomingCommandNodeBase { 99 | command: NodeCommand.getRawConfigParameterValue; 100 | parameter: number; 101 | bitMask?: number; 102 | } 103 | 104 | export interface IncomingCommandNodeRefreshValues 105 | extends IncomingCommandNodeBase { 106 | command: NodeCommand.refreshValues; 107 | } 108 | 109 | export interface IncomingCommandNodeRefreshCCValues 110 | extends IncomingCommandNodeBase { 111 | command: NodeCommand.refreshCCValues; 112 | commandClass: CommandClasses; 113 | } 114 | 115 | export interface IncomingCommandNodePing extends IncomingCommandNodeBase { 116 | command: NodeCommand.ping; 117 | } 118 | 119 | export interface IncomingCommandHasSecurityClass 120 | extends IncomingCommandNodeBase { 121 | command: NodeCommand.hasSecurityClass; 122 | securityClass: SecurityClass; 123 | } 124 | 125 | export interface IncomingCommandGetHighestSecurityClass 126 | extends IncomingCommandNodeBase { 127 | command: NodeCommand.getHighestSecurityClass; 128 | } 129 | 130 | export interface IncomingCommandTestPowerlevel extends IncomingCommandNodeBase { 131 | command: NodeCommand.testPowerlevel; 132 | testNodeId: number; 133 | powerlevel: Powerlevel; 134 | testFrameCount: number; 135 | } 136 | 137 | export interface IncomingCommandCheckLifelineHealth 138 | extends IncomingCommandNodeBase { 139 | command: NodeCommand.checkLifelineHealth; 140 | rounds?: number; 141 | } 142 | 143 | export interface IncomingCommandCheckRouteHealth 144 | extends IncomingCommandNodeBase { 145 | command: NodeCommand.checkRouteHealth; 146 | targetNodeId: number; 147 | rounds?: number; 148 | } 149 | 150 | export interface IncomingCommandGetValue extends IncomingCommandNodeBase { 151 | command: NodeCommand.getValue; 152 | valueId: ValueID; 153 | } 154 | 155 | export interface IncomingCommandGetEndpointCount 156 | extends IncomingCommandNodeBase { 157 | command: NodeCommand.getEndpointCount; 158 | } 159 | 160 | export interface IncomingCommandInterviewCC extends IncomingCommandNodeBase { 161 | command: NodeCommand.interviewCC; 162 | commandClass: CommandClasses; 163 | } 164 | 165 | export interface IncomingCommandGetState extends IncomingCommandNodeBase { 166 | command: NodeCommand.getState; 167 | } 168 | 169 | export interface IncomingCommandSetName extends IncomingCommandNodeBase { 170 | command: NodeCommand.setName; 171 | name: string; 172 | updateCC?: boolean; 173 | } 174 | 175 | export interface IncomingCommandSetLocation extends IncomingCommandNodeBase { 176 | command: NodeCommand.setLocation; 177 | location: string; 178 | updateCC?: boolean; 179 | } 180 | 181 | export interface IncomingCommandSetKeepAwake extends IncomingCommandNodeBase { 182 | command: NodeCommand.setKeepAwake; 183 | keepAwake: boolean; 184 | } 185 | 186 | export interface IncomingCommandIsFirmwareUpdateInProgress 187 | extends IncomingCommandNodeBase { 188 | command: 189 | | NodeCommand.isFirmwareUpdateInProgress 190 | | NodeCommand.getFirmwareUpdateProgress; 191 | } 192 | 193 | export interface IncomingCommandWaitForWakeup extends IncomingCommandNodeBase { 194 | command: NodeCommand.waitForWakeup; 195 | } 196 | 197 | export interface IncomingCommandInterview extends IncomingCommandNodeBase { 198 | command: NodeCommand.interview; 199 | } 200 | 201 | export interface IncomingCommandNodeGetValueTimestamp 202 | extends IncomingCommandNodeBase { 203 | command: NodeCommand.getValueTimestamp; 204 | valueId: ValueID; 205 | } 206 | 207 | export interface IncomingCommandNodeManuallyIdleNotificationValueMethod1 208 | extends IncomingCommandNodeBase { 209 | command: NodeCommand.manuallyIdleNotificationValue; 210 | valueId: ValueID; 211 | } 212 | 213 | export interface IncomingCommandNodeManuallyIdleNotificationValueMethod2 214 | extends IncomingCommandNodeBase { 215 | command: NodeCommand.manuallyIdleNotificationValue; 216 | notificationType: number; 217 | prevValue: number; 218 | endpointIndex?: number; 219 | } 220 | 221 | export interface IncomingCommandNodeSetDateAndTime 222 | extends IncomingCommandNodeBase { 223 | command: NodeCommand.setDateAndTime; 224 | date?: string; // use ISO 8601 date string format 225 | } 226 | 227 | export interface IncomingCommandNodeGetDateAndTime 228 | extends IncomingCommandNodeBase { 229 | command: NodeCommand.getDateAndTime; 230 | } 231 | 232 | export interface IncomingCommandNodeIsHealthCheckInProgress 233 | extends IncomingCommandNodeBase { 234 | command: NodeCommand.isHealthCheckInProgress; 235 | } 236 | 237 | export interface IncomingCommandNodeAbortHealthCheck 238 | extends IncomingCommandNodeBase { 239 | command: NodeCommand.abortHealthCheck; 240 | } 241 | 242 | export interface IncomingCommandNodeSetDefaultVolume 243 | extends IncomingCommandNodeBase { 244 | command: NodeCommand.setDefaultVolume; 245 | defaultVolume?: number; 246 | } 247 | 248 | export interface IncomingCommandNodeSetDefaultTransitionDuration 249 | extends IncomingCommandNodeBase { 250 | command: NodeCommand.setDefaultTransitionDuration; 251 | defaultTransitionDuration?: string; // Will be converted to a Duration object 252 | } 253 | 254 | export interface IncomingCommandNodeHasDeviceConfigChanged 255 | extends IncomingCommandNodeBase { 256 | command: NodeCommand.hasDeviceConfigChanged; 257 | } 258 | 259 | export interface IncomingCommandNodeCreateDump extends IncomingCommandNodeBase { 260 | command: NodeCommand.createDump; 261 | } 262 | 263 | export interface IncomingCommandNodeGetSupportedNotificationEvents 264 | extends IncomingCommandNodeBase { 265 | command: NodeCommand.getSupportedNotificationEvents; 266 | } 267 | 268 | export type IncomingMessageNode = 269 | | IncomingCommandNodeSetValue 270 | | IncomingCommandNodeRefreshInfo 271 | | IncomingCommandNodeGetDefinedValueIDs 272 | | IncomingCommandNodeGetValueMetadata 273 | | IncomingCommandNodeBeginFirmwareUpdate 274 | | IncomingCommandNodeUpdateFirmware 275 | | IncomingCommandNodeAbortFirmwareUpdate 276 | | IncomingCommandGetFirmwareUpdateCapabilities 277 | | IncomingCommandGetFirmwareUpdateCapabilitiesCached 278 | | IncomingCommandNodePollValue 279 | | IncomingCommandNodeSetRawConfigParameterValue 280 | | IncomingCommandNodeGetRawConfigParameterValue 281 | | IncomingCommandNodeRefreshValues 282 | | IncomingCommandNodeRefreshCCValues 283 | | IncomingCommandNodePing 284 | | IncomingCommandHasSecurityClass 285 | | IncomingCommandGetHighestSecurityClass 286 | | IncomingCommandTestPowerlevel 287 | | IncomingCommandCheckLifelineHealth 288 | | IncomingCommandCheckRouteHealth 289 | | IncomingCommandGetValue 290 | | IncomingCommandGetEndpointCount 291 | | IncomingCommandInterviewCC 292 | | IncomingCommandGetState 293 | | IncomingCommandSetName 294 | | IncomingCommandSetLocation 295 | | IncomingCommandSetKeepAwake 296 | | IncomingCommandIsFirmwareUpdateInProgress 297 | | IncomingCommandWaitForWakeup 298 | | IncomingCommandInterview 299 | | IncomingCommandNodeGetValueTimestamp 300 | | IncomingCommandNodeManuallyIdleNotificationValueMethod1 301 | | IncomingCommandNodeManuallyIdleNotificationValueMethod2 302 | | IncomingCommandNodeSetDateAndTime 303 | | IncomingCommandNodeGetDateAndTime 304 | | IncomingCommandNodeIsHealthCheckInProgress 305 | | IncomingCommandNodeAbortHealthCheck 306 | | IncomingCommandNodeSetDefaultVolume 307 | | IncomingCommandNodeSetDefaultTransitionDuration 308 | | IncomingCommandNodeHasDeviceConfigChanged 309 | | IncomingCommandNodeCreateDump 310 | | IncomingCommandNodeGetSupportedNotificationEvents; 311 | -------------------------------------------------------------------------------- /src/lib/node/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Driver, 3 | LifelineHealthCheckResult, 4 | RouteHealthCheckResult, 5 | } from "zwave-js"; 6 | import { 7 | CommandClasses, 8 | ConfigurationMetadata, 9 | extractFirmware, 10 | Firmware, 11 | guessFirmwareFileFormat, 12 | } from "@zwave-js/core"; 13 | import { NodeNotFoundError, UnknownCommandError } from "../error.js"; 14 | import { Client, ClientsController } from "../server.js"; 15 | import { dumpConfigurationMetadata, dumpMetadata, dumpNode } from "../state.js"; 16 | import { NodeCommand } from "./command.js"; 17 | import { IncomingMessageNode } from "./incoming_message.js"; 18 | import { NodeResultTypes } from "./outgoing_message.js"; 19 | import { 20 | firmwareUpdateOutgoingMessage, 21 | getRawConfigParameterValue, 22 | setRawConfigParameterValue, 23 | setValueOutgoingMessage, 24 | } from "../common.js"; 25 | import { OutgoingEvent } from "../outgoing_message.js"; 26 | import { MessageHandler } from "../message_handler.js"; 27 | 28 | export class NodeMessageHandler implements MessageHandler { 29 | constructor( 30 | private clientsController: ClientsController, 31 | private driver: Driver, 32 | private client: Client, 33 | ) {} 34 | 35 | public async handle( 36 | message: IncomingMessageNode, 37 | ): Promise { 38 | const { nodeId, command } = message; 39 | 40 | const node = this.driver.controller.nodes.get(nodeId); 41 | if (!node) { 42 | throw new NodeNotFoundError(nodeId); 43 | } 44 | 45 | switch (message.command) { 46 | case NodeCommand.setValue: { 47 | const result = await node.setValue( 48 | message.valueId, 49 | message.value, 50 | message.options, 51 | ); 52 | return setValueOutgoingMessage(result, this.client.schemaVersion); 53 | } 54 | case NodeCommand.refreshInfo: { 55 | await node.refreshInfo(message.options); 56 | return {}; 57 | } 58 | case NodeCommand.getDefinedValueIDs: { 59 | const valueIds = node.getDefinedValueIDs(); 60 | return { valueIds }; 61 | } 62 | case NodeCommand.getValueMetadata: { 63 | if (message.valueId.commandClass == CommandClasses.Configuration) { 64 | return dumpConfigurationMetadata( 65 | node.getValueMetadata(message.valueId) as ConfigurationMetadata, 66 | this.client.schemaVersion, 67 | ); 68 | } 69 | 70 | return dumpMetadata( 71 | node.getValueMetadata(message.valueId), 72 | this.client.schemaVersion, 73 | ); 74 | } 75 | case NodeCommand.beginFirmwareUpdate: { 76 | const firmwareFile = Buffer.from(message.firmwareFile, "base64"); 77 | let firmware = await extractFirmware( 78 | firmwareFile, 79 | message.firmwareFileFormat ?? 80 | guessFirmwareFileFormat(message.firmwareFilename, firmwareFile), 81 | ); 82 | // Defer to the target provided in the messaage when available 83 | firmware.firmwareTarget = message.target ?? firmware.firmwareTarget; 84 | const result = await node.updateFirmware([firmware]); 85 | return firmwareUpdateOutgoingMessage(result, this.client.schemaVersion); 86 | } 87 | case NodeCommand.updateFirmware: { 88 | const updates: Firmware[] = []; 89 | for (const update of message.updates) { 90 | const file = Buffer.from(update.file, "base64"); 91 | let firmware = await extractFirmware( 92 | file, 93 | update.fileFormat ?? guessFirmwareFileFormat(update.filename, file), 94 | ); 95 | // Defer to the target provided in the messaage when available 96 | firmware.firmwareTarget = 97 | update.firmwareTarget ?? firmware.firmwareTarget; 98 | updates.push(firmware); 99 | } 100 | const result = await node.updateFirmware(updates); 101 | return firmwareUpdateOutgoingMessage(result, this.client.schemaVersion); 102 | } 103 | case NodeCommand.abortFirmwareUpdate: { 104 | await node.abortFirmwareUpdate(); 105 | return {}; 106 | } 107 | case NodeCommand.getFirmwareUpdateCapabilities: { 108 | const capabilities = await node.getFirmwareUpdateCapabilities(); 109 | return { capabilities }; 110 | } 111 | case NodeCommand.getFirmwareUpdateCapabilitiesCached: { 112 | const capabilities = node.getFirmwareUpdateCapabilitiesCached(); 113 | return { capabilities }; 114 | } 115 | case NodeCommand.pollValue: { 116 | const value = await node.pollValue(message.valueId); 117 | return { value }; 118 | } 119 | case NodeCommand.setRawConfigParameterValue: { 120 | return setRawConfigParameterValue(message, node); 121 | } 122 | case NodeCommand.getRawConfigParameterValue: { 123 | return getRawConfigParameterValue(message, node); 124 | } 125 | case NodeCommand.refreshValues: { 126 | await node.refreshValues(); 127 | return {}; 128 | } 129 | case NodeCommand.refreshCCValues: { 130 | await node.refreshCCValues(message.commandClass); 131 | return {}; 132 | } 133 | case NodeCommand.ping: { 134 | const responded = await node.ping(); 135 | return { responded }; 136 | } 137 | case NodeCommand.hasSecurityClass: { 138 | const hasSecurityClass = node.hasSecurityClass(message.securityClass); 139 | return { hasSecurityClass }; 140 | } 141 | case NodeCommand.getHighestSecurityClass: { 142 | const highestSecurityClass = node.getHighestSecurityClass(); 143 | return { highestSecurityClass }; 144 | } 145 | case NodeCommand.testPowerlevel: { 146 | const framesAcked = await node.testPowerlevel( 147 | message.testNodeId, 148 | message.powerlevel, 149 | message.testFrameCount, 150 | (acknowledged: number, total: number) => { 151 | this.clientsController.clients.forEach((client) => 152 | client.sendEvent({ 153 | source: "node", 154 | event: "test powerlevel progress", 155 | nodeId: message.nodeId, 156 | acknowledged, 157 | total, 158 | }), 159 | ); 160 | }, 161 | ); 162 | return { framesAcked }; 163 | } 164 | case NodeCommand.checkLifelineHealth: { 165 | const summary = await node.checkLifelineHealth( 166 | message.rounds, 167 | ( 168 | round: number, 169 | totalRounds: number, 170 | lastRating: number, 171 | lastResult: LifelineHealthCheckResult, 172 | ) => { 173 | const returnEvent0: OutgoingEvent = { 174 | source: "node", 175 | event: "check lifeline health progress", 176 | nodeId: message.nodeId, 177 | round, 178 | totalRounds, 179 | lastRating, 180 | }; 181 | const returnEvent31 = { ...returnEvent0, lastResult }; 182 | this.clientsController.clients.forEach((client) => { 183 | client.sendEvent( 184 | client.schemaVersion >= 31 ? returnEvent31 : returnEvent0, 185 | ); 186 | }); 187 | }, 188 | ); 189 | return { summary }; 190 | } 191 | case NodeCommand.checkRouteHealth: { 192 | const summary = await node.checkRouteHealth( 193 | message.targetNodeId, 194 | message.rounds, 195 | ( 196 | round: number, 197 | totalRounds: number, 198 | lastRating: number, 199 | lastResult: RouteHealthCheckResult, 200 | ) => { 201 | const returnEvent0: OutgoingEvent = { 202 | source: "node", 203 | event: "check route health progress", 204 | nodeId: message.nodeId, 205 | round, 206 | totalRounds, 207 | lastRating, 208 | }; 209 | const returnEvent31 = { ...returnEvent0, lastResult }; 210 | this.clientsController.clients.forEach((client) => { 211 | client.sendEvent( 212 | client.schemaVersion >= 31 ? returnEvent31 : returnEvent0, 213 | ); 214 | }); 215 | }, 216 | ); 217 | return { summary }; 218 | } 219 | case NodeCommand.getValue: { 220 | const value = node.getValue(message.valueId); 221 | return { value }; 222 | } 223 | case NodeCommand.getEndpointCount: { 224 | const count = node.getEndpointCount(); 225 | return { count }; 226 | } 227 | case NodeCommand.interviewCC: { 228 | await node.interviewCC(message.commandClass); 229 | return {}; 230 | } 231 | case NodeCommand.getState: { 232 | const state = dumpNode(node, this.client.schemaVersion); 233 | return { state }; 234 | } 235 | case NodeCommand.setKeepAwake: { 236 | node.keepAwake = message.keepAwake; 237 | return {}; 238 | } 239 | case NodeCommand.setLocation: { 240 | node.location = message.location; 241 | if ( 242 | (message.updateCC ?? true) && 243 | node.supportsCC(CommandClasses["Node Naming and Location"]) 244 | ) { 245 | await node.commandClasses["Node Naming and Location"].setLocation( 246 | message.location, 247 | ); 248 | } 249 | return {}; 250 | } 251 | case NodeCommand.setName: { 252 | node.name = message.name; 253 | if ( 254 | (message.updateCC ?? true) && 255 | node.supportsCC(CommandClasses["Node Naming and Location"]) 256 | ) { 257 | await node.commandClasses["Node Naming and Location"].setName( 258 | message.name, 259 | ); 260 | } 261 | return {}; 262 | } 263 | case NodeCommand.getFirmwareUpdateProgress: 264 | case NodeCommand.isFirmwareUpdateInProgress: { 265 | const progress = node.isFirmwareUpdateInProgress(); 266 | return { progress }; 267 | } 268 | case NodeCommand.waitForWakeup: { 269 | await node.waitForWakeup(); 270 | return {}; 271 | } 272 | case NodeCommand.interview: { 273 | await node.interview(); 274 | return {}; 275 | } 276 | case NodeCommand.getValueTimestamp: { 277 | const timestamp = node.getValueTimestamp(message.valueId); 278 | return { timestamp }; 279 | } 280 | case NodeCommand.manuallyIdleNotificationValue: { 281 | if ("valueId" in message) { 282 | node.manuallyIdleNotificationValue(message.valueId); 283 | } else { 284 | node.manuallyIdleNotificationValue( 285 | message.notificationType, 286 | message.prevValue, 287 | message.endpointIndex, 288 | ); 289 | } 290 | return {}; 291 | } 292 | case NodeCommand.setDateAndTime: { 293 | const success = await node.setDateAndTime( 294 | message.date === undefined ? undefined : new Date(message.date), 295 | ); 296 | return { success }; 297 | } 298 | case NodeCommand.getDateAndTime: { 299 | const dateAndTime = await node.getDateAndTime(); 300 | return { dateAndTime }; 301 | } 302 | case NodeCommand.isHealthCheckInProgress: { 303 | const progress = node.isHealthCheckInProgress(); 304 | return { progress }; 305 | } 306 | case NodeCommand.abortHealthCheck: { 307 | await node.abortHealthCheck(); 308 | return {}; 309 | } 310 | case NodeCommand.setDefaultVolume: { 311 | node.defaultVolume = message.defaultVolume; 312 | return {}; 313 | } 314 | case NodeCommand.setDefaultTransitionDuration: { 315 | node.defaultTransitionDuration = message.defaultTransitionDuration; 316 | return {}; 317 | } 318 | case NodeCommand.hasDeviceConfigChanged: { 319 | const changed = node.hasDeviceConfigChanged(); 320 | return { changed }; 321 | } 322 | case NodeCommand.createDump: { 323 | const dump = node.createDump(); 324 | return { dump }; 325 | } 326 | default: { 327 | throw new UnknownCommandError(command); 328 | } 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/lib/node/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DateAndTime, 3 | FirmwareUpdateCapabilities, 4 | LifelineHealthCheckSummary, 5 | RouteHealthCheckSummary, 6 | TranslatedValueID, 7 | ValueMetadata, 8 | ConfigValue, 9 | ZWaveNotificationCapability, 10 | } from "zwave-js"; 11 | import { 12 | MaybeNotKnown, 13 | SecurityClass, 14 | SupervisionResult, 15 | } from "@zwave-js/core"; 16 | import { NodeCommand } from "./command.js"; 17 | import { NodeState } from "../state.js"; 18 | import { FirmwareUpdateResultType, SetValueResultType } from "../common.js"; 19 | 20 | export interface NodeResultTypes { 21 | [NodeCommand.setValue]: SetValueResultType; 22 | [NodeCommand.refreshInfo]: Record; 23 | [NodeCommand.getDefinedValueIDs]: { valueIds: TranslatedValueID[] }; 24 | [NodeCommand.getValueMetadata]: ValueMetadata; 25 | [NodeCommand.beginFirmwareUpdate]: FirmwareUpdateResultType; 26 | [NodeCommand.updateFirmware]: FirmwareUpdateResultType; 27 | [NodeCommand.abortFirmwareUpdate]: Record; 28 | [NodeCommand.getFirmwareUpdateCapabilities]: { 29 | capabilities: FirmwareUpdateCapabilities; 30 | }; 31 | [NodeCommand.getFirmwareUpdateCapabilitiesCached]: { 32 | capabilities: FirmwareUpdateCapabilities; 33 | }; 34 | [NodeCommand.pollValue]: { value?: any }; 35 | [NodeCommand.setRawConfigParameterValue]: { result?: SupervisionResult }; 36 | [NodeCommand.getRawConfigParameterValue]: { 37 | value: MaybeNotKnown; 38 | }; 39 | [NodeCommand.refreshValues]: Record; 40 | [NodeCommand.refreshCCValues]: Record; 41 | [NodeCommand.ping]: { responded: boolean }; 42 | [NodeCommand.hasSecurityClass]: { hasSecurityClass: MaybeNotKnown }; 43 | [NodeCommand.getHighestSecurityClass]: { 44 | highestSecurityClass: MaybeNotKnown; 45 | }; 46 | [NodeCommand.testPowerlevel]: { framesAcked: number }; 47 | [NodeCommand.checkLifelineHealth]: { summary: LifelineHealthCheckSummary }; 48 | [NodeCommand.checkRouteHealth]: { summary: RouteHealthCheckSummary }; 49 | [NodeCommand.getValue]: { value?: any }; 50 | [NodeCommand.getEndpointCount]: { count: number }; 51 | [NodeCommand.interviewCC]: Record; 52 | [NodeCommand.getState]: { state: NodeState }; 53 | [NodeCommand.setName]: Record; 54 | [NodeCommand.setLocation]: Record; 55 | [NodeCommand.setKeepAwake]: Record; 56 | [NodeCommand.getFirmwareUpdateProgress]: { progress: boolean }; 57 | [NodeCommand.isFirmwareUpdateInProgress]: { progress: boolean }; 58 | [NodeCommand.waitForWakeup]: Record; 59 | [NodeCommand.interview]: Record; 60 | [NodeCommand.getValueTimestamp]: { timestamp?: number }; 61 | [NodeCommand.manuallyIdleNotificationValue]: Record; 62 | [NodeCommand.setDateAndTime]: { success: boolean }; 63 | [NodeCommand.getDateAndTime]: { dateAndTime: DateAndTime }; 64 | [NodeCommand.isHealthCheckInProgress]: { progress: boolean }; 65 | [NodeCommand.abortHealthCheck]: Record; 66 | [NodeCommand.setDefaultVolume]: Record; 67 | [NodeCommand.setDefaultTransitionDuration]: Record; 68 | [NodeCommand.hasDeviceConfigChanged]: { changed: MaybeNotKnown }; 69 | [NodeCommand.createDump]: { dump: object }; // TODO: Fix type 70 | [NodeCommand.getSupportedNotificationEvents]: { 71 | events: ZWaveNotificationCapability[]; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { LogConfig, ZWaveErrorCodes } from "@zwave-js/core"; 2 | import type { ZwaveState } from "./state.js"; 3 | import { NodeResultTypes } from "./node/outgoing_message.js"; 4 | import { ControllerResultTypes } from "./controller/outgoing_message.js"; 5 | import { ServerCommand } from "./command.js"; 6 | import { DriverResultTypes } from "./driver/outgoing_message.js"; 7 | import { ErrorCode } from "./error.js"; 8 | import { BroadcastNodeResultTypes } from "./broadcast_node/outgoing_message.js"; 9 | import { MulticastGroupResultTypes } from "./multicast_group/outgoing_message.js"; 10 | import { EndpointResultTypes } from "./endpoint/outgoing_message.js"; 11 | import { UtilsResultTypes } from "./utils/outgoing_message.js"; 12 | import { ConfigManagerResultTypes } from "./config_manager/outgoing_message.js"; 13 | import { ZnifferResultTypes } from "./zniffer/outgoing_message.js"; 14 | 15 | // https://github.com/microsoft/TypeScript/issues/1897#issuecomment-822032151 16 | export type JSONValue = 17 | | string 18 | | number 19 | | boolean 20 | | null 21 | | JSONValue[] 22 | | { [key: string]: JSONValue } 23 | | {}; 24 | 25 | export interface OutgoingEvent { 26 | source: "controller" | "node" | "driver" | "zniffer"; 27 | event: string; 28 | [key: string]: JSONValue; 29 | } 30 | 31 | interface OutgoingVersionMessage { 32 | type: "version"; 33 | driverVersion: string; 34 | serverVersion: string; 35 | homeId: number | undefined; 36 | minSchemaVersion: number; 37 | maxSchemaVersion: number; 38 | } 39 | 40 | interface OutgoingEventMessage { 41 | type: "event"; 42 | event: OutgoingEvent; 43 | } 44 | 45 | interface OutgoingResultMessageError { 46 | type: "result"; 47 | messageId: string; 48 | success: false; 49 | errorCode: Omit; 50 | message?: string; 51 | args: JSONValue; 52 | } 53 | 54 | interface OutgoingResultMessageZWaveError { 55 | type: "result"; 56 | messageId: string; 57 | success: false; 58 | errorCode: ErrorCode.zwaveError; 59 | zwaveErrorCode: ZWaveErrorCodes; 60 | zwaveErrorCodeName?: string; 61 | zwaveErrorMessage: string; 62 | } 63 | 64 | export interface ServerResultTypes { 65 | [ServerCommand.startListening]: { state: ZwaveState }; 66 | [ServerCommand.updateLogConfig]: Record; 67 | [ServerCommand.getLogConfig]: { config: Partial }; 68 | [ServerCommand.initialize]: Record; 69 | [ServerCommand.setApiSchema]: Record; 70 | } 71 | 72 | export type ResultTypes = ServerResultTypes & 73 | NodeResultTypes & 74 | ControllerResultTypes & 75 | DriverResultTypes & 76 | MulticastGroupResultTypes & 77 | BroadcastNodeResultTypes & 78 | EndpointResultTypes & 79 | UtilsResultTypes & 80 | ZnifferResultTypes & 81 | ConfigManagerResultTypes; 82 | 83 | export interface OutgoingResultMessageSuccess { 84 | type: "result"; 85 | messageId: string; 86 | success: true; 87 | result: ResultTypes[keyof ResultTypes]; 88 | } 89 | 90 | export type OutgoingMessage = 91 | | OutgoingVersionMessage 92 | | OutgoingEventMessage 93 | | OutgoingResultMessageSuccess 94 | | OutgoingResultMessageError 95 | | OutgoingResultMessageZWaveError; 96 | -------------------------------------------------------------------------------- /src/lib/utils/command.ts: -------------------------------------------------------------------------------- 1 | export enum UtilsCommand { 2 | parseQRCodeString = "utils.parse_qr_code_string", 3 | tryParseDSKFromQRCodeString = "utils.try_parse_dsk_from_qr_code_string", 4 | num2hex = "utils.num2hex", // While made available in the server due to its availability within the driver, this functionality works best when implemented locally 5 | formatId = "utils.format_id", // While made available in the server due to its availability within the driver, this functionality works best when implemented locally 6 | buffer2hex = "utils.buffer2hex", // While made available in the server due to its availability within the driver, this functionality works best when implemented locally 7 | getEnumMemberName = "utils.get_enum_member_name", // While made available in the server due to its availability within the driver, this functionality works best when implemented locally 8 | rssiToString = "utils.rssi_to_string", 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/utils/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { RSSI } from "zwave-js"; 2 | import { IncomingCommandBase } from "../incoming_message_base.js"; 3 | import { UtilsCommand } from "./command.js"; 4 | 5 | export interface IncomingCommandUtilsBase extends IncomingCommandBase {} 6 | 7 | export interface IncomingCommandUtilsParseQRCodeString 8 | extends IncomingCommandUtilsBase { 9 | command: UtilsCommand.parseQRCodeString; 10 | qr: string; 11 | } 12 | 13 | export interface IncomingCommandUtilsTryParseDSKFromQRCodeString 14 | extends IncomingCommandUtilsBase { 15 | command: UtilsCommand.tryParseDSKFromQRCodeString; 16 | qr: string; 17 | } 18 | 19 | export interface IncomingCommandUtilsNum2hex extends IncomingCommandUtilsBase { 20 | command: UtilsCommand.num2hex; 21 | val?: number | null; 22 | uppercase: boolean; 23 | } 24 | 25 | export interface IncomingCommandUtilsFormatId extends IncomingCommandUtilsBase { 26 | command: UtilsCommand.formatId; 27 | id: number | string; 28 | } 29 | 30 | export interface IncomingCommandUtilsBuffer2hex 31 | extends IncomingCommandUtilsBase { 32 | command: UtilsCommand.buffer2hex; 33 | buffer: Buffer; 34 | uppercase: boolean; 35 | } 36 | 37 | export interface IncomingCommandUtilsGetEnumMemberName 38 | extends IncomingCommandUtilsBase { 39 | command: UtilsCommand.getEnumMemberName; 40 | enumeration: unknown; 41 | value: number; 42 | } 43 | 44 | export interface IncomingCommandUtilsRssiToString 45 | extends IncomingCommandUtilsBase { 46 | command: UtilsCommand.rssiToString; 47 | rssi: RSSI; 48 | } 49 | 50 | export type IncomingMessageUtils = 51 | | IncomingCommandUtilsParseQRCodeString 52 | | IncomingCommandUtilsTryParseDSKFromQRCodeString 53 | | IncomingCommandUtilsNum2hex 54 | | IncomingCommandUtilsFormatId 55 | | IncomingCommandUtilsBuffer2hex 56 | | IncomingCommandUtilsGetEnumMemberName 57 | | IncomingCommandUtilsRssiToString; 58 | -------------------------------------------------------------------------------- /src/lib/utils/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffer2hex, 3 | formatId, 4 | getEnumMemberName, 5 | num2hex, 6 | rssiToString, 7 | } from "zwave-js"; 8 | import { parseQRCodeString, tryParseDSKFromQRCodeString } from "@zwave-js/core"; 9 | import { UnknownCommandError } from "../error.js"; 10 | import { UtilsCommand } from "./command.js"; 11 | import { IncomingMessageUtils } from "./incoming_message.js"; 12 | import { UtilsResultTypes } from "./outgoing_message.js"; 13 | import { MessageHandler } from "../message_handler.js"; 14 | 15 | export class UtilsMessageHandler implements MessageHandler { 16 | async handle( 17 | message: IncomingMessageUtils, 18 | ): Promise { 19 | const { command } = message; 20 | 21 | switch (message.command) { 22 | case UtilsCommand.parseQRCodeString: { 23 | const qrProvisioningInformation = await parseQRCodeString(message.qr); 24 | return { qrProvisioningInformation }; 25 | } 26 | case UtilsCommand.tryParseDSKFromQRCodeString: { 27 | const dsk = tryParseDSKFromQRCodeString(message.qr); 28 | return { dsk }; 29 | } 30 | case UtilsCommand.num2hex: { 31 | const hex = num2hex(message.val, message.uppercase); 32 | return { hex }; 33 | } 34 | case UtilsCommand.formatId: { 35 | const id = formatId(message.id); 36 | return { id }; 37 | } 38 | case UtilsCommand.buffer2hex: { 39 | const hex = buffer2hex(message.buffer, message.uppercase); 40 | return { hex }; 41 | } 42 | case UtilsCommand.getEnumMemberName: { 43 | const name = getEnumMemberName(message.enumeration, message.value); 44 | return { name }; 45 | } 46 | case UtilsCommand.rssiToString: { 47 | const rssi = rssiToString(message.rssi); 48 | return { rssi }; 49 | } 50 | default: { 51 | throw new UnknownCommandError(command); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/utils/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { QRProvisioningInformation } from "@zwave-js/core"; 2 | import { UtilsCommand } from "./command.js"; 3 | 4 | export interface UtilsResultTypes { 5 | [UtilsCommand.parseQRCodeString]: { 6 | qrProvisioningInformation: QRProvisioningInformation; 7 | }; 8 | [UtilsCommand.tryParseDSKFromQRCodeString]: { 9 | dsk?: string; 10 | }; 11 | [UtilsCommand.num2hex]: { hex: string }; 12 | [UtilsCommand.formatId]: { id: string }; 13 | [UtilsCommand.buffer2hex]: { hex: string }; 14 | [UtilsCommand.getEnumMemberName]: { name: string }; 15 | [UtilsCommand.rssiToString]: { rssi: string }; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/zniffer/command.ts: -------------------------------------------------------------------------------- 1 | export enum ZnifferCommand { 2 | init = "zniffer.init", 3 | start = "zniffer.start", 4 | clearCapturedFrames = "zniffer.clear_captured_frames", 5 | getCaptureAsZLFBuffer = "zniffer.get_capture_as_zlf_buffer", 6 | capturedFrames = "zniffer.captured_frames", 7 | stop = "zniffer.stop", 8 | destroy = "zniffer.destroy", 9 | supportedFrequencies = "zniffer.supported_frequencies", 10 | currentFrequency = "zniffer.current_frequency", 11 | setFrequency = "zniffer.set_frequency", 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/zniffer/incoming_message.ts: -------------------------------------------------------------------------------- 1 | import { ZnifferOptions } from "zwave-js"; 2 | import { IncomingCommandBase } from "../incoming_message_base.js"; 3 | import { ZnifferCommand } from "./command.js"; 4 | 5 | export interface IncomingCommandZnifferBase extends IncomingCommandBase {} 6 | 7 | export interface IncomingCommandZnifferInit extends IncomingCommandZnifferBase { 8 | command: ZnifferCommand.init; 9 | devicePath: string; 10 | options: ZnifferOptions; 11 | } 12 | 13 | export interface IncomingCommandZnifferStart 14 | extends IncomingCommandZnifferBase { 15 | command: ZnifferCommand.start; 16 | } 17 | 18 | export interface IncomingCommandZnifferClearCapturedFrames 19 | extends IncomingCommandZnifferBase { 20 | command: ZnifferCommand.clearCapturedFrames; 21 | } 22 | 23 | export interface IncomingCommandZnifferGetCaptureAsZLFBuffer 24 | extends IncomingCommandZnifferBase { 25 | command: ZnifferCommand.getCaptureAsZLFBuffer; 26 | } 27 | 28 | export interface IncomingCommandZnifferStop extends IncomingCommandZnifferBase { 29 | command: ZnifferCommand.stop; 30 | } 31 | 32 | export interface IncomingCommandZnifferDestroy 33 | extends IncomingCommandZnifferBase { 34 | command: ZnifferCommand.destroy; 35 | } 36 | 37 | export interface IncomingCommandZnifferCapturedFrames 38 | extends IncomingCommandZnifferBase { 39 | command: ZnifferCommand.capturedFrames; 40 | } 41 | 42 | export interface IncomingCommandZnifferSupportedFrequencies 43 | extends IncomingCommandZnifferBase { 44 | command: ZnifferCommand.supportedFrequencies; 45 | } 46 | 47 | export interface IncomingCommandZnifferCurrentFrequency 48 | extends IncomingCommandZnifferBase { 49 | command: ZnifferCommand.currentFrequency; 50 | } 51 | 52 | export interface IncomingCommandZnifferSetFrequency 53 | extends IncomingCommandZnifferBase { 54 | command: ZnifferCommand.setFrequency; 55 | frequency: number; 56 | } 57 | 58 | export type IncomingMessageZniffer = 59 | | IncomingCommandZnifferClearCapturedFrames 60 | | IncomingCommandZnifferGetCaptureAsZLFBuffer 61 | | IncomingCommandZnifferInit 62 | | IncomingCommandZnifferStart 63 | | IncomingCommandZnifferStop 64 | | IncomingCommandZnifferCapturedFrames 65 | | IncomingCommandZnifferSupportedFrequencies 66 | | IncomingCommandZnifferCurrentFrequency 67 | | IncomingCommandZnifferSetFrequency 68 | | IncomingCommandZnifferDestroy; 69 | -------------------------------------------------------------------------------- /src/lib/zniffer/message_handler.ts: -------------------------------------------------------------------------------- 1 | import { Driver, Zniffer } from "zwave-js"; 2 | import { UnknownCommandError } from "../error.js"; 3 | import { Client, ClientsController } from "../server.js"; 4 | import { ZnifferCommand } from "./command.js"; 5 | import { IncomingMessageZniffer } from "./incoming_message.js"; 6 | import { ZnifferResultTypes } from "./outgoing_message.js"; 7 | import { OutgoingEvent } from "../outgoing_message.js"; 8 | import { MessageHandler } from "../message_handler.js"; 9 | 10 | export class ZnifferMessageHandler implements MessageHandler { 11 | private zniffer?: Zniffer; 12 | 13 | constructor( 14 | private driver: Driver, 15 | private clientsController: ClientsController, 16 | ) {} 17 | 18 | forwardEvent(data: OutgoingEvent, minSchemaVersion: number = 38) { 19 | // Forward event to all clients 20 | this.clientsController.clients.forEach((client) => 21 | this.sendEvent(client, data, minSchemaVersion), 22 | ); 23 | } 24 | 25 | sendEvent(client: Client, data: OutgoingEvent, minSchemaVersion?: number) { 26 | // Send event to connected client only 27 | if ( 28 | client.receiveEvents && 29 | client.isConnected && 30 | client.schemaVersion >= (minSchemaVersion ?? 0) 31 | ) { 32 | client.sendEvent(data); 33 | } 34 | } 35 | 36 | async handle( 37 | message: IncomingMessageZniffer, 38 | ): Promise { 39 | const { command } = message; 40 | 41 | if (message.command != ZnifferCommand.init && !this.zniffer) { 42 | throw new Error("Zniffer is not running"); 43 | } 44 | switch (message.command) { 45 | case ZnifferCommand.init: { 46 | if (this.zniffer) { 47 | throw new Error("Zniffer is already running"); 48 | } 49 | if (message.options.logConfig === undefined) { 50 | message.options.logConfig = this.driver.options.logConfig; 51 | } 52 | if (message.options.securityKeys === undefined) { 53 | message.options.securityKeys = this.driver.options.securityKeys; 54 | } 55 | if (message.options.securityKeysLongRange === undefined) { 56 | message.options.securityKeysLongRange = 57 | this.driver.options.securityKeysLongRange; 58 | } 59 | this.zniffer = new Zniffer(message.devicePath, message.options); 60 | this.zniffer 61 | .on("ready", () => 62 | this.forwardEvent({ 63 | source: "zniffer", 64 | event: "ready", 65 | }), 66 | ) 67 | .on("corrupted frame", (corruptedFrame, rawDate) => 68 | this.forwardEvent({ 69 | source: "zniffer", 70 | event: "corrupted frame", 71 | corruptedFrame, 72 | rawDate, 73 | }), 74 | ) 75 | .on("frame", (frame, rawData) => 76 | this.forwardEvent({ 77 | source: "zniffer", 78 | event: "frame", 79 | frame, 80 | rawData, 81 | }), 82 | ) 83 | .on("error", (error) => 84 | this.forwardEvent({ 85 | source: "zniffer", 86 | event: "error", 87 | error, 88 | }), 89 | ); 90 | await this.zniffer.init(); 91 | return {}; 92 | } 93 | case ZnifferCommand.start: { 94 | await this.zniffer?.start(); 95 | return {}; 96 | } 97 | case ZnifferCommand.clearCapturedFrames: { 98 | this.zniffer?.clearCapturedFrames(); 99 | return {}; 100 | } 101 | case ZnifferCommand.getCaptureAsZLFBuffer: { 102 | return { 103 | capture: Buffer.from(this.zniffer!.getCaptureAsZLFBuffer().buffer), 104 | }; 105 | } 106 | case ZnifferCommand.stop: { 107 | await this.zniffer?.stop(); 108 | return {}; 109 | } 110 | case ZnifferCommand.destroy: { 111 | this.zniffer?.removeAllListeners(); 112 | await this.zniffer?.destroy(); 113 | this.zniffer = undefined; 114 | return {}; 115 | } 116 | case ZnifferCommand.capturedFrames: { 117 | return { capturedFrames: this.zniffer!.capturedFrames }; 118 | } 119 | case ZnifferCommand.supportedFrequencies: { 120 | return { frequencies: this.zniffer!.supportedFrequencies }; 121 | } 122 | case ZnifferCommand.currentFrequency: { 123 | return { frequency: this.zniffer!.currentFrequency }; 124 | } 125 | case ZnifferCommand.setFrequency: { 126 | await this.zniffer?.setFrequency(message.frequency); 127 | return {}; 128 | } 129 | default: { 130 | throw new UnknownCommandError(command); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/lib/zniffer/outgoing_message.ts: -------------------------------------------------------------------------------- 1 | import { ZnifferCommand } from "./command.js"; 2 | 3 | export interface ZnifferResultTypes { 4 | [ZnifferCommand.init]: Record; 5 | [ZnifferCommand.start]: Record; 6 | [ZnifferCommand.clearCapturedFrames]: Record; 7 | [ZnifferCommand.getCaptureAsZLFBuffer]: { capture: Buffer }; 8 | [ZnifferCommand.capturedFrames]: { capturedFrames: any[] }; 9 | [ZnifferCommand.stop]: Record; 10 | [ZnifferCommand.destroy]: Record; 11 | [ZnifferCommand.supportedFrequencies]: { 12 | frequencies: ReadonlyMap; 13 | }; 14 | [ZnifferCommand.currentFrequency]: { frequency?: number }; 15 | [ZnifferCommand.setFrequency]: Record; 16 | } 17 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | import type { Driver, ZWaveOptions } from "zwave-js"; 2 | import { EventEmitter } from "events"; 3 | import { LogConfig } from "@zwave-js/core"; 4 | 5 | class MockController extends EventEmitter { 6 | homeId = 1; 7 | nodes = new Map(); 8 | } 9 | 10 | class MockDriver extends EventEmitter { 11 | public controller = new MockController(); 12 | 13 | public ready = true; 14 | 15 | public statisticsEnabled = true; 16 | 17 | async start() { 18 | this.emit("driver ready"); 19 | } 20 | 21 | public getLogConfig(): Partial { 22 | return { 23 | enabled: true, 24 | level: "debug", 25 | transports: [], 26 | }; 27 | } 28 | 29 | public updateLogConfig(config: Partial) {} 30 | 31 | public updateUserAgent( 32 | additionalUserAgentComponents?: Record | null, 33 | ) {} 34 | 35 | public updateOptions(options: Partial) {} 36 | 37 | async destroy() {} 38 | } 39 | 40 | export const createMockDriver = () => new MockDriver() as unknown as Driver; 41 | -------------------------------------------------------------------------------- /src/test/integration.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import dns from "node:dns"; 3 | import ws from "ws"; 4 | import { libVersion } from "zwave-js"; 5 | import { ZwavejsServer } from "../lib/server.js"; 6 | import { createMockDriver } from "../mock/index.js"; 7 | import { minSchemaVersion, maxSchemaVersion } from "../lib/const.js"; 8 | import { createRequire } from "node:module"; 9 | const require = createRequire(import.meta.url); 10 | 11 | dns.setDefaultResultOrder("ipv4first"); 12 | 13 | const PORT = 45001; 14 | 15 | const createNextMessage = (socket: ws) => { 16 | let waitingListener: ((msg: unknown) => void) | undefined; 17 | const pendingMessages: unknown[] = []; 18 | 19 | socket.on("message", (data: string) => { 20 | const msg = JSON.parse(data); 21 | if (!waitingListener) { 22 | pendingMessages.push(msg); 23 | return; 24 | } 25 | const listener = waitingListener; 26 | waitingListener = undefined; 27 | listener(msg); 28 | }); 29 | 30 | return () => { 31 | if (pendingMessages.length) { 32 | return pendingMessages.splice(0, 1)[0]; 33 | } 34 | return new Promise((resolve) => { 35 | waitingListener = resolve; 36 | }); 37 | }; 38 | }; 39 | 40 | const runTest = async () => { 41 | const server = new ZwavejsServer(createMockDriver(), { port: PORT }); 42 | await server.start(true); 43 | let socket: ws | undefined = undefined; 44 | 45 | try { 46 | socket = new ws(`ws://localhost:${PORT}`); 47 | const nextMessage = createNextMessage(socket); 48 | await new Promise((resolve) => socket!.once("open", resolve)); 49 | 50 | assert.deepEqual(await nextMessage(), { 51 | driverVersion: libVersion, 52 | homeId: 1, 53 | serverVersion: require("../../package.json").version, 54 | minSchemaVersion: minSchemaVersion, 55 | maxSchemaVersion: maxSchemaVersion, 56 | type: "version", 57 | }); 58 | 59 | socket.send( 60 | JSON.stringify({ 61 | command: "initialize", 62 | messageId: "initialize", 63 | schemaVersion: maxSchemaVersion, 64 | }), 65 | ); 66 | 67 | assert.deepEqual(await nextMessage(), { 68 | type: "result", 69 | success: true, 70 | messageId: "initialize", 71 | result: {}, 72 | }); 73 | 74 | socket.send( 75 | JSON.stringify({ 76 | messageId: "my-msg-id!", 77 | command: "start_listening", 78 | }), 79 | ); 80 | 81 | assert.deepEqual(await nextMessage(), { 82 | type: "result", 83 | success: true, 84 | messageId: "my-msg-id!", 85 | result: { 86 | state: { 87 | driver: { 88 | logConfig: { enabled: true, level: "debug" }, 89 | statisticsEnabled: true, 90 | }, 91 | controller: { homeId: 1 }, 92 | nodes: [], 93 | }, 94 | }, 95 | }); 96 | 97 | console.log("Integration tests passed :)"); 98 | } finally { 99 | if (socket) { 100 | socket.close(); 101 | } 102 | await server.destroy(); 103 | } 104 | }; 105 | 106 | runTest(); 107 | -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { configs } from "triple-beam"; 2 | 3 | const loglevels = configs.npm.levels; 4 | 5 | export function numberFromLogLevel( 6 | logLevel: string | undefined, 7 | ): number | undefined { 8 | if (logLevel == undefined) return; 9 | for (const [level, value] of Object.entries(loglevels)) { 10 | if (level === logLevel) return value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/util/parse-args.ts: -------------------------------------------------------------------------------- 1 | import minimist from "minimist"; 2 | 3 | export const parseArgs = (expectedKeys: string[]): T => { 4 | const args = minimist(process.argv.slice(2)); 5 | 6 | const extraKeys = Object.keys(args).filter( 7 | (key) => !expectedKeys.includes(key), 8 | ); 9 | 10 | if (extraKeys.length > 0) { 11 | console.error(`Error: Got unexpected keys ${extraKeys.join(", ")}`); 12 | process.exit(1); 13 | } 14 | 15 | return args as unknown as T; 16 | }; 17 | -------------------------------------------------------------------------------- /src/util/stringify.ts: -------------------------------------------------------------------------------- 1 | import { isUint8Array } from "node:util/types"; 2 | 3 | export function stringifyReplacer(key: string, value: any): any { 4 | // Ensure that Uint8Arrays are serialized as if they were Buffers 5 | // to keep the API backwards compatible 6 | if (isUint8Array(value)) { 7 | return Buffer.from(value).toJSON(); 8 | } 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "dist-esm", 6 | "rootDir": "src", 7 | "resolveJsonModule": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------