From 204e0ebcc2302cd90dc8a415c33f7e9949e8e36d Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:15:33 -0500 Subject: [PATCH 1/2] feat: add introspect module with commands endpoint Add a new `introspect` command module that returns the JSON Schema describing all incoming message types. The `introspect.commands` command works before `initialize`, enabling API discovery pre-negotiation. - Create `src/lib/introspect/` module (command, incoming_message, outgoing_message, message_handler) following existing patterns - Register in Instance enum and wire into server.ts - Add `ts-json-schema-generator` for build-time schema generation - Rename interfaces for `$ref` reuse (node, driver, controller, multicast_group incoming messages) - Add introspection section to README with tool recommendations Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + README.md | 27 ++ package-lock.json | 274 ++++++++++++++++++++ package.json | 5 +- src/lib/command.ts | 2 + src/lib/controller/incoming_message.ts | 4 +- src/lib/driver/incoming_message.ts | 112 ++++---- src/lib/incoming_message.ts | 2 + src/lib/instance.ts | 1 + src/lib/introspect/command.ts | 3 + src/lib/introspect/incoming_message.ts | 11 + src/lib/introspect/message_handler.ts | 21 ++ src/lib/introspect/outgoing_message.ts | 5 + src/lib/multicast_group/incoming_message.ts | 4 +- src/lib/node/incoming_message.ts | 68 ++--- src/lib/outgoing_message.ts | 2 + src/lib/server.ts | 2 + 17 files changed, 449 insertions(+), 95 deletions(-) create mode 100644 src/lib/introspect/command.ts create mode 100644 src/lib/introspect/incoming_message.ts create mode 100644 src/lib/introspect/message_handler.ts create mode 100644 src/lib/introspect/outgoing_message.ts diff --git a/.gitignore b/.gitignore index d38be344d..2ef4fe209 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ config.json *.tgz cache .claude +/src/lib/generated/ diff --git a/README.md b/README.md index d07754d62..1f3c83b47 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,33 @@ interface { } ``` +### Introspection + +The server supports introspection commands that allow clients to discover the API at runtime. These commands work **before `initialize`**, so clients can explore the API before negotiating a schema version. + +#### Get command schema + +Returns the full [JSON Schema](https://json-schema.org/) describing all incoming message types. The schema uses `$ref` pointers and a shared `definitions` dictionary for compact representation. + +```ts +interface { + messageId: string; + command: "introspect.commands"; +} +``` + +Returns a JSON Schema (Draft 7) object describing all valid incoming messages. + +**Exploring the schema** + +The returned schema can be explored with any JSON Schema-compatible tool: + +- **[jq](https://jqlang.org/)** — Query the schema from the command line (e.g. `jq '.definitions.IncomingMessageNode'`) +- **[JSON Schema Viewer](https://json-schema.app/)** — Paste the schema into the online viewer for interactive exploration +- **[JSON Crack](https://jsoncrack.com/)** — Visualize the schema as an interactive graph +- **VS Code** — Extensions like [JSON Schema Viewer](https://marketplace.visualstudio.com/items?itemName=jock.svg) provide in-editor navigation +- **[jsonschema](https://python-jsonschema.readthedocs.io/) (Python)** / **[ajv](https://ajv.js.org/) (JS)** — Validate messages programmatically against the schema + ### Driver level commands #### Get the config of the driver diff --git a/package-lock.json b/package-lock.json index 9da8a51c7..5a101535e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "lint-staged": "^16.2.6", "prettier": "^3.6.2", "semver": "^7.5.4", + "ts-json-schema-generator": "^2.4.0", "tsx": "^4.19.2", "typescript": "^5.3.3", "typescript-eslint": "^8.46.1", @@ -891,6 +892,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -2869,6 +2880,36 @@ "dev": true, "license": "MIT" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2919,6 +2960,31 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3109,6 +3175,22 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3488,6 +3570,16 @@ "node": ">= 12.0.0" } }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/mdns-server": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/mdns-server/-/mdns-server-1.0.11.tgz", @@ -3545,6 +3637,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -3586,6 +3688,16 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nrf-intel-hex": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.4.0.tgz", @@ -3717,6 +3829,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3765,6 +3884,23 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4314,6 +4450,39 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-json-schema-generator": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.4.0.tgz", + "integrity": "sha512-HbmNsgs58CfdJq0gpteRTxPXG26zumezOs+SB9tgky6MpqiFgQwieCn2MW70+sxpHouZ/w9LW0V6L4ZQO4y1Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "commander": "^13.1.0", + "glob": "^11.0.1", + "json5": "^2.2.3", + "normalize-path": "^3.0.0", + "safe-stable-stringify": "^2.5.0", + "tslib": "^2.8.1", + "typescript": "^5.8.2" + }, + "bin": { + "ts-json-schema-generator": "bin/ts-json-schema-generator.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ts-json-schema-generator/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5584,6 +5753,12 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true }, + "@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true + }, "@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -6950,6 +7125,24 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "dev": true }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6978,6 +7171,20 @@ "resolve-pkg-maps": "^1.0.0" } }, + "glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "requires": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7108,6 +7315,15 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "requires": { + "@isaacs/cliui": "^9.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7376,6 +7592,12 @@ "triple-beam": "^1.3.0" } }, + "lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true + }, "mdns-server": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/mdns-server/-/mdns-server-1.0.11.tgz", @@ -7415,6 +7637,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true + }, "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -7444,6 +7672,12 @@ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "dev": true }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "nrf-intel-hex": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.4.0.tgz", @@ -7528,6 +7762,12 @@ "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", "dev": true }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7561,6 +7801,16 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7925,6 +8175,30 @@ "dev": true, "requires": {} }, + "ts-json-schema-generator": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.4.0.tgz", + "integrity": "sha512-HbmNsgs58CfdJq0gpteRTxPXG26zumezOs+SB9tgky6MpqiFgQwieCn2MW70+sxpHouZ/w9LW0V6L4ZQO4y1Ug==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.15", + "commander": "^13.1.0", + "glob": "^11.0.1", + "json5": "^2.2.3", + "normalize-path": "^3.0.0", + "safe-stable-stringify": "^2.5.0", + "tslib": "^2.8.1", + "typescript": "^5.8.2" + }, + "dependencies": { + "commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true + } + } + }, "tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index e8f2938a0..2c04a09a9 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ "scripts": { "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", - "test": "prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", + "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", + "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --out src/lib/generated/incoming_message_schema.json", + "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", "prepare": "npm run build", @@ -66,6 +68,7 @@ "lint-staged": "^16.2.6", "prettier": "^3.6.2", "semver": "^7.5.4", + "ts-json-schema-generator": "^2.4.0", "tsx": "^4.19.2", "typescript": "^5.3.3", "typescript-eslint": "^8.46.1", diff --git a/src/lib/command.ts b/src/lib/command.ts index 16008a29e..03ccd6071 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -3,9 +3,11 @@ export { ConfigManagerCommand } from "./config_manager/command.js"; export { ControllerCommand } from "./controller/command.js"; export { DriverCommand } from "./driver/command.js"; export { EndpointCommand } from "./endpoint/command.js"; +export { IntrospectCommand } from "./introspect/command.js"; export { MulticastGroupCommand } from "./multicast_group/command.js"; export { NodeCommand } from "./node/command.js"; export { UtilsCommand } from "./utils/command.js"; +export { ZnifferCommand } from "./zniffer/command.js"; export enum ServerCommand { startListening = "start_listening", diff --git a/src/lib/controller/incoming_message.ts b/src/lib/controller/incoming_message.ts index e64975263..c9c21a620 100644 --- a/src/lib/controller/incoming_message.ts +++ b/src/lib/controller/incoming_message.ts @@ -285,7 +285,7 @@ export interface IncomingCommandControllerFirmwareUpdateOTW extends IncomingComm fileFormat?: FirmwareFileFormat; } -export interface IncomingCommandIsFirmwareUpdateInProgress extends IncomingCommandControllerBase { +export interface IncomingCommandControllerIsFirmwareUpdateInProgress extends IncomingCommandControllerBase { command: ControllerCommand.isFirmwareUpdateInProgress; } @@ -354,7 +354,7 @@ export type IncomingMessageController = | IncomingCommandControllerBeginOTAFirmwareUpdate | IncomingCommandControllerFirmwareUpdateOTA | IncomingCommandControllerFirmwareUpdateOTW - | IncomingCommandIsFirmwareUpdateInProgress + | IncomingCommandControllerIsFirmwareUpdateInProgress | IncomingCommandControllerSetMaxLongRangePowerlevel | IncomingCommandControllerGetMaxLongRangePowerlevel | IncomingCommandControllerSetLongRangeChannel diff --git a/src/lib/driver/incoming_message.ts b/src/lib/driver/incoming_message.ts index 02edeb563..defa13abb 100644 --- a/src/lib/driver/incoming_message.ts +++ b/src/lib/driver/incoming_message.ts @@ -9,87 +9,87 @@ import { } from "zwave-js"; import { LogContexts } from "../logging.js"; -interface IncomingCommandGetConfig extends IncomingCommandBase { +interface IncomingCommandDriverGetConfig extends IncomingCommandBase { command: DriverCommand.getConfig; } -interface IncomingCommandUpdateLogConfig extends IncomingCommandBase { +interface IncomingCommandDriverUpdateLogConfig extends IncomingCommandBase { command: DriverCommand.updateLogConfig; config: Partial; } -interface IncomingCommandGetLogConfig extends IncomingCommandBase { +interface IncomingCommandDriverGetLogConfig extends IncomingCommandBase { command: DriverCommand.getLogConfig; } -interface IncomingCommandEnableStatistics extends IncomingCommandBase { +interface IncomingCommandDriverEnableStatistics extends IncomingCommandBase { command: DriverCommand.enableStatistics; applicationName: string; applicationVersion: string; } -interface IncomingCommandDisableStatistics extends IncomingCommandBase { +interface IncomingCommandDriverDisableStatistics extends IncomingCommandBase { command: DriverCommand.disableStatistics; } -interface IncomingCommandIsStatisticsEnabled extends IncomingCommandBase { +interface IncomingCommandDriverIsStatisticsEnabled extends IncomingCommandBase { command: DriverCommand.isStatisticsEnabled; } -interface IncomingCommandStartListeningLogs extends IncomingCommandBase { +interface IncomingCommandDriverStartListeningLogs extends IncomingCommandBase { command: DriverCommand.startListeningLogs; filter?: Partial; } -interface IncomingCommandStopListeningLogs extends IncomingCommandBase { +interface IncomingCommandDriverStopListeningLogs extends IncomingCommandBase { command: DriverCommand.stopListeningLogs; } -interface IncomingCommandCheckForConfigUpdates extends IncomingCommandBase { +interface IncomingCommandDriverCheckForConfigUpdates extends IncomingCommandBase { command: DriverCommand.checkForConfigUpdates; } -interface IncomingCommandInstallConfigUpdate extends IncomingCommandBase { +interface IncomingCommandDriverInstallConfigUpdate extends IncomingCommandBase { command: DriverCommand.installConfigUpdate; } -interface IncomingCommandSetPreferredScales extends IncomingCommandBase { +interface IncomingCommandDriverSetPreferredScales extends IncomingCommandBase { command: DriverCommand.setPreferredScales; scales: ZWaveOptions["preferences"]["scales"]; } -interface IncomingCommandEnableErrorReporting extends IncomingCommandBase { +interface IncomingCommandDriverEnableErrorReporting extends IncomingCommandBase { command: DriverCommand.enableErrorReporting; } -interface IncomingCommandSoftReset extends IncomingCommandBase { +interface IncomingCommandDriverSoftReset extends IncomingCommandBase { command: DriverCommand.softReset; } -interface IncomingCommandTrySoftReset extends IncomingCommandBase { +interface IncomingCommandDriverTrySoftReset extends IncomingCommandBase { command: DriverCommand.trySoftReset; } -interface IncomingCommandHardReset extends IncomingCommandBase { +interface IncomingCommandDriverHardReset extends IncomingCommandBase { command: DriverCommand.hardReset; } -interface IncomingCommandShutdown extends IncomingCommandBase { +interface IncomingCommandDriverShutdown extends IncomingCommandBase { command: DriverCommand.shutdown; } -interface IncomingCommandUpdateOptions extends IncomingCommandBase { +interface IncomingCommandDriverUpdateOptions extends IncomingCommandBase { command: DriverCommand.updateOptions; options: EditableZWaveOptions; } -interface IncomingCommandSendTestFrame extends IncomingCommandBase { +interface IncomingCommandDriverSendTestFrame extends IncomingCommandBase { command: DriverCommand.sendTestFrame; nodeId: number; powerlevel: Powerlevel; } -export type IncomingCommandFirmwareUpdateOTW = IncomingCommandBase & { +export type IncomingCommandDriverFirmwareUpdateOTW = IncomingCommandBase & { command: DriverCommand.firmwareUpdateOTW; } & ( | { @@ -105,31 +105,31 @@ export type IncomingCommandFirmwareUpdateOTW = IncomingCommandBase & { } ); -export interface IncomingCommandIsOTWFirmwareUpdateInProgress extends IncomingCommandBase { +export interface IncomingCommandDriverIsOTWFirmwareUpdateInProgress extends IncomingCommandBase { command: DriverCommand.isOTWFirmwareUpdateInProgress; } -interface IncomingCommandSoftResetAndRestart extends IncomingCommandBase { +interface IncomingCommandDriverSoftResetAndRestart extends IncomingCommandBase { command: DriverCommand.softResetAndRestart; } -interface IncomingCommandEnterBootloader extends IncomingCommandBase { +interface IncomingCommandDriverEnterBootloader extends IncomingCommandBase { command: DriverCommand.enterBootloader; } -interface IncomingCommandLeaveBootloader extends IncomingCommandBase { +interface IncomingCommandDriverLeaveBootloader extends IncomingCommandBase { command: DriverCommand.leaveBootloader; } // CC version queries -interface IncomingCommandGetSupportedCCVersion extends IncomingCommandBase { +interface IncomingCommandDriverGetSupportedCCVersion extends IncomingCommandBase { command: DriverCommand.getSupportedCCVersion; cc: CommandClasses; nodeId: number; endpointIndex?: number; } -interface IncomingCommandGetSafeCCVersion extends IncomingCommandBase { +interface IncomingCommandDriverGetSafeCCVersion extends IncomingCommandBase { command: DriverCommand.getSafeCCVersion; cc: CommandClasses; nodeId: number; @@ -137,47 +137,47 @@ interface IncomingCommandGetSafeCCVersion extends IncomingCommandBase { } // User agent -interface IncomingCommandUpdateUserAgent extends IncomingCommandBase { +interface IncomingCommandDriverUpdateUserAgent extends IncomingCommandBase { command: DriverCommand.updateUserAgent; components: Record; } // RSSI monitoring -interface IncomingCommandEnableFrequentRSSIMonitoring extends IncomingCommandBase { +interface IncomingCommandDriverEnableFrequentRSSIMonitoring extends IncomingCommandBase { command: DriverCommand.enableFrequentRSSIMonitoring; durationMs: number; } -interface IncomingCommandDisableFrequentRSSIMonitoring extends IncomingCommandBase { +interface IncomingCommandDriverDisableFrequentRSSIMonitoring extends IncomingCommandBase { command: DriverCommand.disableFrequentRSSIMonitoring; } export type IncomingMessageDriver = - | IncomingCommandGetConfig - | IncomingCommandUpdateLogConfig - | IncomingCommandGetLogConfig - | IncomingCommandDisableStatistics - | IncomingCommandEnableStatistics - | IncomingCommandIsStatisticsEnabled - | IncomingCommandStartListeningLogs - | IncomingCommandStopListeningLogs - | IncomingCommandCheckForConfigUpdates - | IncomingCommandInstallConfigUpdate - | IncomingCommandSetPreferredScales - | IncomingCommandEnableErrorReporting - | IncomingCommandSoftReset - | IncomingCommandTrySoftReset - | IncomingCommandHardReset - | IncomingCommandShutdown - | IncomingCommandUpdateOptions - | IncomingCommandSendTestFrame - | IncomingCommandFirmwareUpdateOTW - | IncomingCommandIsOTWFirmwareUpdateInProgress - | IncomingCommandSoftResetAndRestart - | IncomingCommandEnterBootloader - | IncomingCommandLeaveBootloader - | IncomingCommandGetSupportedCCVersion - | IncomingCommandGetSafeCCVersion - | IncomingCommandUpdateUserAgent - | IncomingCommandEnableFrequentRSSIMonitoring - | IncomingCommandDisableFrequentRSSIMonitoring; + | IncomingCommandDriverGetConfig + | IncomingCommandDriverUpdateLogConfig + | IncomingCommandDriverGetLogConfig + | IncomingCommandDriverDisableStatistics + | IncomingCommandDriverEnableStatistics + | IncomingCommandDriverIsStatisticsEnabled + | IncomingCommandDriverStartListeningLogs + | IncomingCommandDriverStopListeningLogs + | IncomingCommandDriverCheckForConfigUpdates + | IncomingCommandDriverInstallConfigUpdate + | IncomingCommandDriverSetPreferredScales + | IncomingCommandDriverEnableErrorReporting + | IncomingCommandDriverSoftReset + | IncomingCommandDriverTrySoftReset + | IncomingCommandDriverHardReset + | IncomingCommandDriverShutdown + | IncomingCommandDriverUpdateOptions + | IncomingCommandDriverSendTestFrame + | IncomingCommandDriverFirmwareUpdateOTW + | IncomingCommandDriverIsOTWFirmwareUpdateInProgress + | IncomingCommandDriverSoftResetAndRestart + | IncomingCommandDriverEnterBootloader + | IncomingCommandDriverLeaveBootloader + | IncomingCommandDriverGetSupportedCCVersion + | IncomingCommandDriverGetSafeCCVersion + | IncomingCommandDriverUpdateUserAgent + | IncomingCommandDriverEnableFrequentRSSIMonitoring + | IncomingCommandDriverDisableFrequentRSSIMonitoring; diff --git a/src/lib/incoming_message.ts b/src/lib/incoming_message.ts index d492f4b6a..ca2e4adb1 100644 --- a/src/lib/incoming_message.ts +++ b/src/lib/incoming_message.ts @@ -10,6 +10,7 @@ import { IncomingMessageEndpoint } from "./endpoint/incoming_message.js"; import { IncomingMessageUtils } from "./utils/incoming_message.js"; import { IncomingMessageConfigManager } from "./config_manager/incoming_message.js"; import { LogContexts } from "./logging.js"; +import { IncomingMessageIntrospect } from "./introspect/incoming_message.js"; import { IncomingMessageZniffer } from "./zniffer/incoming_message.js"; interface IncomingCommandStartListening extends IncomingCommandBase { @@ -59,6 +60,7 @@ export type IncomingMessage = | IncomingMessageUtils | IncomingMessageZniffer | IncomingMessageConfigManager + | IncomingMessageIntrospect | IncomingCommandInitialize | IncomingCommandStartListeningLogs | IncomingCommandStopListeningLogs; diff --git a/src/lib/instance.ts b/src/lib/instance.ts index 51aecb33e..dfa764ab2 100644 --- a/src/lib/instance.ts +++ b/src/lib/instance.ts @@ -4,6 +4,7 @@ export enum Instance { controller = "controller", driver = "driver", endpoint = "endpoint", + introspect = "introspect", multicast_group = "multicast_group", node = "node", utils = "utils", diff --git a/src/lib/introspect/command.ts b/src/lib/introspect/command.ts new file mode 100644 index 000000000..32ca90b99 --- /dev/null +++ b/src/lib/introspect/command.ts @@ -0,0 +1,3 @@ +export enum IntrospectCommand { + commands = "introspect.commands", +} diff --git a/src/lib/introspect/incoming_message.ts b/src/lib/introspect/incoming_message.ts new file mode 100644 index 000000000..a83bbc805 --- /dev/null +++ b/src/lib/introspect/incoming_message.ts @@ -0,0 +1,11 @@ +import { IncomingCommandBase } from "../incoming_message_base.js"; +import { IntrospectCommand } from "./command.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface IncomingCommandIntrospectBase extends IncomingCommandBase {} + +export interface IncomingCommandIntrospectCommands extends IncomingCommandIntrospectBase { + command: IntrospectCommand.commands; +} + +export type IncomingMessageIntrospect = IncomingCommandIntrospectCommands; diff --git a/src/lib/introspect/message_handler.ts b/src/lib/introspect/message_handler.ts new file mode 100644 index 000000000..f2dfba710 --- /dev/null +++ b/src/lib/introspect/message_handler.ts @@ -0,0 +1,21 @@ +import { UnknownCommandError } from "../error.js"; +import incomingMessageSchema from "../generated/incoming_message_schema.json" with { type: "json" }; +import { MessageHandler } from "../message_handler.js"; +import { IntrospectCommand } from "./command.js"; +import { IncomingMessageIntrospect } from "./incoming_message.js"; +import { IntrospectResultTypes } from "./outgoing_message.js"; + +export class IntrospectMessageHandler implements MessageHandler { + async handle( + message: IncomingMessageIntrospect, + ): Promise { + const { command } = message; + + switch (message.command) { + case IntrospectCommand.commands: + return incomingMessageSchema; + default: + throw new UnknownCommandError(command); + } + } +} diff --git a/src/lib/introspect/outgoing_message.ts b/src/lib/introspect/outgoing_message.ts new file mode 100644 index 000000000..096c02301 --- /dev/null +++ b/src/lib/introspect/outgoing_message.ts @@ -0,0 +1,5 @@ +import { IntrospectCommand } from "./command.js"; + +export interface IntrospectResultTypes { + [IntrospectCommand.commands]: Record; +} diff --git a/src/lib/multicast_group/incoming_message.ts b/src/lib/multicast_group/incoming_message.ts index 269c85044..8bcb47e95 100644 --- a/src/lib/multicast_group/incoming_message.ts +++ b/src/lib/multicast_group/incoming_message.ts @@ -44,7 +44,7 @@ export interface IncomingCommandMulticastGroupSupportsCCAPI extends IncomingComm commandClass: CommandClasses; } -export interface IncomingCommandBroadcastNodeGetDefinedValueIDs extends IncomingCommandMulticastGroupBase { +export interface IncomingCommandMulticastGroupGetDefinedValueIDs extends IncomingCommandMulticastGroupBase { command: MulticastGroupCommand.getDefinedValueIDs; } @@ -55,4 +55,4 @@ export type IncomingMessageMulticastGroup = | IncomingCommandMulticastGroupGetCCVersion | IncomingCommandMulticastGroupInvokeCCAPI | IncomingCommandMulticastGroupSupportsCCAPI - | IncomingCommandBroadcastNodeGetDefinedValueIDs; + | IncomingCommandMulticastGroupGetDefinedValueIDs; diff --git a/src/lib/node/incoming_message.ts b/src/lib/node/incoming_message.ts index 025f5c311..d00a523e2 100644 --- a/src/lib/node/incoming_message.ts +++ b/src/lib/node/incoming_message.ts @@ -63,11 +63,11 @@ export interface IncomingCommandNodeAbortFirmwareUpdate extends IncomingCommandN command: NodeCommand.abortFirmwareUpdate; } -export interface IncomingCommandGetFirmwareUpdateCapabilities extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetFirmwareUpdateCapabilities extends IncomingCommandNodeBase { command: NodeCommand.getFirmwareUpdateCapabilities; } -export interface IncomingCommandGetFirmwareUpdateCapabilitiesCached extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetFirmwareUpdateCapabilitiesCached extends IncomingCommandNodeBase { command: NodeCommand.getFirmwareUpdateCapabilitiesCached; } @@ -104,79 +104,79 @@ export interface IncomingCommandNodePing extends IncomingCommandNodeBase { command: NodeCommand.ping; } -export interface IncomingCommandHasSecurityClass extends IncomingCommandNodeBase { +export interface IncomingCommandNodeHasSecurityClass extends IncomingCommandNodeBase { command: NodeCommand.hasSecurityClass; securityClass: SecurityClass; } -export interface IncomingCommandGetHighestSecurityClass extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetHighestSecurityClass extends IncomingCommandNodeBase { command: NodeCommand.getHighestSecurityClass; } -export interface IncomingCommandTestPowerlevel extends IncomingCommandNodeBase { +export interface IncomingCommandNodeTestPowerlevel extends IncomingCommandNodeBase { command: NodeCommand.testPowerlevel; testNodeId: number; powerlevel: Powerlevel; testFrameCount: number; } -export interface IncomingCommandCheckLifelineHealth extends IncomingCommandNodeBase { +export interface IncomingCommandNodeCheckLifelineHealth extends IncomingCommandNodeBase { command: NodeCommand.checkLifelineHealth; rounds?: number; } -export interface IncomingCommandCheckRouteHealth extends IncomingCommandNodeBase { +export interface IncomingCommandNodeCheckRouteHealth extends IncomingCommandNodeBase { command: NodeCommand.checkRouteHealth; targetNodeId: number; rounds?: number; } -export interface IncomingCommandGetValue extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetValue extends IncomingCommandNodeBase { command: NodeCommand.getValue; valueId: ValueID; } -export interface IncomingCommandGetEndpointCount extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetEndpointCount extends IncomingCommandNodeBase { command: NodeCommand.getEndpointCount; } -export interface IncomingCommandInterviewCC extends IncomingCommandNodeBase { +export interface IncomingCommandNodeInterviewCC extends IncomingCommandNodeBase { command: NodeCommand.interviewCC; commandClass: CommandClasses; } -export interface IncomingCommandGetState extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetState extends IncomingCommandNodeBase { command: NodeCommand.getState; } -export interface IncomingCommandSetName extends IncomingCommandNodeBase { +export interface IncomingCommandNodeSetName extends IncomingCommandNodeBase { command: NodeCommand.setName; name: string; updateCC?: boolean; } -export interface IncomingCommandSetLocation extends IncomingCommandNodeBase { +export interface IncomingCommandNodeSetLocation extends IncomingCommandNodeBase { command: NodeCommand.setLocation; location: string; updateCC?: boolean; } -export interface IncomingCommandSetKeepAwake extends IncomingCommandNodeBase { +export interface IncomingCommandNodeSetKeepAwake extends IncomingCommandNodeBase { command: NodeCommand.setKeepAwake; keepAwake: boolean; } -export interface IncomingCommandIsFirmwareUpdateInProgress extends IncomingCommandNodeBase { +export interface IncomingCommandNodeIsFirmwareUpdateInProgress extends IncomingCommandNodeBase { command: | NodeCommand.isFirmwareUpdateInProgress | NodeCommand.getFirmwareUpdateProgress; } -export interface IncomingCommandWaitForWakeup extends IncomingCommandNodeBase { +export interface IncomingCommandNodeWaitForWakeup extends IncomingCommandNodeBase { command: NodeCommand.waitForWakeup; } -export interface IncomingCommandInterview extends IncomingCommandNodeBase { +export interface IncomingCommandNodeInterview extends IncomingCommandNodeBase { command: NodeCommand.interview; } @@ -244,29 +244,29 @@ export type IncomingMessageNode = | IncomingCommandNodeBeginFirmwareUpdate | IncomingCommandNodeUpdateFirmware | IncomingCommandNodeAbortFirmwareUpdate - | IncomingCommandGetFirmwareUpdateCapabilities - | IncomingCommandGetFirmwareUpdateCapabilitiesCached + | IncomingCommandNodeGetFirmwareUpdateCapabilities + | IncomingCommandNodeGetFirmwareUpdateCapabilitiesCached | IncomingCommandNodePollValue | IncomingCommandNodeSetRawConfigParameterValue | IncomingCommandNodeGetRawConfigParameterValue | IncomingCommandNodeRefreshValues | IncomingCommandNodeRefreshCCValues | IncomingCommandNodePing - | IncomingCommandHasSecurityClass - | IncomingCommandGetHighestSecurityClass - | IncomingCommandTestPowerlevel - | IncomingCommandCheckLifelineHealth - | IncomingCommandCheckRouteHealth - | IncomingCommandGetValue - | IncomingCommandGetEndpointCount - | IncomingCommandInterviewCC - | IncomingCommandGetState - | IncomingCommandSetName - | IncomingCommandSetLocation - | IncomingCommandSetKeepAwake - | IncomingCommandIsFirmwareUpdateInProgress - | IncomingCommandWaitForWakeup - | IncomingCommandInterview + | IncomingCommandNodeHasSecurityClass + | IncomingCommandNodeGetHighestSecurityClass + | IncomingCommandNodeTestPowerlevel + | IncomingCommandNodeCheckLifelineHealth + | IncomingCommandNodeCheckRouteHealth + | IncomingCommandNodeGetValue + | IncomingCommandNodeGetEndpointCount + | IncomingCommandNodeInterviewCC + | IncomingCommandNodeGetState + | IncomingCommandNodeSetName + | IncomingCommandNodeSetLocation + | IncomingCommandNodeSetKeepAwake + | IncomingCommandNodeIsFirmwareUpdateInProgress + | IncomingCommandNodeWaitForWakeup + | IncomingCommandNodeInterview | IncomingCommandNodeGetValueTimestamp | IncomingCommandNodeManuallyIdleNotificationValueMethod1 | IncomingCommandNodeManuallyIdleNotificationValueMethod2 diff --git a/src/lib/outgoing_message.ts b/src/lib/outgoing_message.ts index 13c24748d..37358bc28 100644 --- a/src/lib/outgoing_message.ts +++ b/src/lib/outgoing_message.ts @@ -10,6 +10,7 @@ import { MulticastGroupResultTypes } from "./multicast_group/outgoing_message.js import { EndpointResultTypes } from "./endpoint/outgoing_message.js"; import { UtilsResultTypes } from "./utils/outgoing_message.js"; import { ConfigManagerResultTypes } from "./config_manager/outgoing_message.js"; +import { IntrospectResultTypes } from "./introspect/outgoing_message.js"; import { ZnifferResultTypes } from "./zniffer/outgoing_message.js"; // https://github.com/microsoft/TypeScript/issues/1897#issuecomment-822032151 @@ -77,6 +78,7 @@ export type ResultTypes = ServerResultTypes & BroadcastNodeResultTypes & EndpointResultTypes & UtilsResultTypes & + IntrospectResultTypes & ZnifferResultTypes & ConfigManagerResultTypes; diff --git a/src/lib/server.ts b/src/lib/server.ts index 1645a36d5..fddc9c91e 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -38,6 +38,7 @@ import { import { Instance } from "./instance.js"; import { ServerCommand } from "./command.js"; import { DriverMessageHandler } from "./driver/message_handler.js"; +import { IntrospectMessageHandler } from "./introspect/message_handler.js"; import { LogContexts, LoggingEventForwarder } from "./logging.js"; import { BroadcastNodeMessageHandler } from "./broadcast_node/message_handler.js"; import { MulticastGroupMessageHandler } from "./multicast_group/message_handler.js"; @@ -113,6 +114,7 @@ export class Client { this, ), [Instance.endpoint]: new EndpointMessageHandler(this.driver, this), + [Instance.introspect]: new IntrospectMessageHandler(), [Instance.utils]: new UtilsMessageHandler(), [Instance.zniffer]: new ZnifferMessageHandler(driver, clientsController), }; From 1fd7e837eef266a48e3d0073ef323a93e61b1ea2 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:24:51 -0500 Subject: [PATCH 2/2] feat: add introspect.states command Add a new `introspect.states` command that returns JSON Schema for the state dump types used in the `start_listening` response. The schema is organized by category (driver, controller, endpoint, node) and schema version number. - Add `state_schemas.ts` with `StateSchemas` interface mapping all versioned state types for JSON Schema generation - Use JSON-serializable replacements for controller schemas 34+ to avoid `ReadonlyMap` (not representable in JSON Schema) - Add `generate:state-schema` npm script chained into `generate:schema` - Wire `introspect.states` into command enum, incoming/outgoing message types, and message handler Co-Authored-By: Claude Opus 4.6 --- package.json | 5 +- src/lib/introspect/command.ts | 1 + src/lib/introspect/incoming_message.ts | 9 ++- src/lib/introspect/message_handler.ts | 3 + src/lib/introspect/outgoing_message.ts | 1 + src/lib/introspect/state_schemas.ts | 105 +++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/lib/introspect/state_schemas.ts diff --git a/package.json b/package.json index 2c04a09a9..d90c2f502 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,13 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --out src/lib/generated/incoming_message_schema.json", + "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --out src/lib/generated/incoming_message_schema.json && npm run generate:state-schema", "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", "prepare": "npm run build", - "prepublishOnly": "rm -rf dist-* && npm run build" + "prepublishOnly": "rm -rf dist-* && npm run build", + "generate:state-schema": "ts-json-schema-generator --path src/lib/introspect/state_schemas.ts --type StateSchemas --tsconfig tsconfig.json --additional-properties --out src/lib/generated/state_schema.json" }, "author": "", "license": "Apache-2.0", diff --git a/src/lib/introspect/command.ts b/src/lib/introspect/command.ts index 32ca90b99..bba7c5a1a 100644 --- a/src/lib/introspect/command.ts +++ b/src/lib/introspect/command.ts @@ -1,3 +1,4 @@ export enum IntrospectCommand { commands = "introspect.commands", + states = "introspect.states", } diff --git a/src/lib/introspect/incoming_message.ts b/src/lib/introspect/incoming_message.ts index a83bbc805..d53c7001d 100644 --- a/src/lib/introspect/incoming_message.ts +++ b/src/lib/introspect/incoming_message.ts @@ -8,4 +8,11 @@ export interface IncomingCommandIntrospectCommands extends IncomingCommandIntros command: IntrospectCommand.commands; } -export type IncomingMessageIntrospect = IncomingCommandIntrospectCommands; +export interface IncomingCommandIntrospectStates extends IncomingCommandIntrospectBase { + command: IntrospectCommand.states; + schemaVersion?: number | "all"; +} + +export type IncomingMessageIntrospect = + | IncomingCommandIntrospectCommands + | IncomingCommandIntrospectStates; diff --git a/src/lib/introspect/message_handler.ts b/src/lib/introspect/message_handler.ts index f2dfba710..35c42bac8 100644 --- a/src/lib/introspect/message_handler.ts +++ b/src/lib/introspect/message_handler.ts @@ -1,5 +1,6 @@ import { UnknownCommandError } from "../error.js"; import incomingMessageSchema from "../generated/incoming_message_schema.json" with { type: "json" }; +import stateSchema from "../generated/state_schema.json" with { type: "json" }; import { MessageHandler } from "../message_handler.js"; import { IntrospectCommand } from "./command.js"; import { IncomingMessageIntrospect } from "./incoming_message.js"; @@ -14,6 +15,8 @@ export class IntrospectMessageHandler implements MessageHandler { switch (message.command) { case IntrospectCommand.commands: return incomingMessageSchema; + case IntrospectCommand.states: + return stateSchema; default: throw new UnknownCommandError(command); } diff --git a/src/lib/introspect/outgoing_message.ts b/src/lib/introspect/outgoing_message.ts index 096c02301..e5ffd636a 100644 --- a/src/lib/introspect/outgoing_message.ts +++ b/src/lib/introspect/outgoing_message.ts @@ -2,4 +2,5 @@ import { IntrospectCommand } from "./command.js"; export interface IntrospectResultTypes { [IntrospectCommand.commands]: Record; + [IntrospectCommand.states]: Record; } diff --git a/src/lib/introspect/state_schemas.ts b/src/lib/introspect/state_schemas.ts new file mode 100644 index 000000000..c6713ea30 --- /dev/null +++ b/src/lib/introspect/state_schemas.ts @@ -0,0 +1,105 @@ +import type { RebuildRoutesStatus } from "zwave-js"; +import type { + LongRangeChannel, + MaybeNotKnown, + UnknownZWaveChipType, + ZWaveApiVersion, +} from "@zwave-js/core"; +import type { + ControllerStateSchema0, + ControllerStateSchema16, + ControllerStateSchema22, + ControllerStateSchema25, + ControllerStateSchema31, + ControllerStateSchema32, + DriverStateSchema0, + DriverStateSchema47, + EndpointStateSchema0, + EndpointStateSchema3, + EndpointStateSchema15, + EndpointStateSchema26, + NodeStateSchema0, + NodeStateSchema1, + NodeStateSchema3, + NodeStateSchema4, + NodeStateSchema5, + NodeStateSchema7, + NodeStateSchema10, + NodeStateSchema14, + NodeStateSchema29, + NodeStateSchema30, + NodeStateSchema31, + NodeStateSchema35, + NodeStateSchema42, + NodeStateSchema47, +} from "../state.js"; + +/** + * JSON-serializable versions of ControllerStateSchema34+. + * + * The original types use `ReadonlyMap` which + * cannot be represented in JSON Schema. We replace it with + * `Record` to match the actual JSON wire format. + */ +type ControllerStateSchema34Json = ControllerStateSchema32 & { + rebuildRoutesProgress?: Record; +}; + +type ControllerStateSchema35Json = ControllerStateSchema34Json & { + supportsLongRange: MaybeNotKnown; +}; + +type ControllerStateSchema36Json = ControllerStateSchema35Json & { + maxLongRangePowerlevel: MaybeNotKnown; + longRangeChannel: MaybeNotKnown; + supportsLongRangeAutoChannelSelection: MaybeNotKnown; +}; + +type ControllerStateSchema47Json = ControllerStateSchema36Json & { + isSIS: MaybeNotKnown; + maxPayloadSize: MaybeNotKnown; + maxPayloadSizeLR: MaybeNotKnown; + zwaveApiVersion: MaybeNotKnown; + zwaveChipType: MaybeNotKnown; +}; + +export interface StateSchemas { + driver: { + 0: DriverStateSchema0; + 47: DriverStateSchema47; + }; + controller: { + 0: ControllerStateSchema0; + 16: ControllerStateSchema16; + 22: ControllerStateSchema22; + 25: ControllerStateSchema25; + 31: ControllerStateSchema31; + 32: ControllerStateSchema32; + 34: ControllerStateSchema34Json; + 35: ControllerStateSchema35Json; + 36: ControllerStateSchema36Json; + 47: ControllerStateSchema47Json; + }; + endpoint: { + 0: EndpointStateSchema0; + 3: EndpointStateSchema3; + 15: EndpointStateSchema15; + 26: EndpointStateSchema26; + }; + node: { + 0: NodeStateSchema0; + 1: NodeStateSchema1; + 3: NodeStateSchema3; + 4: NodeStateSchema4; + 5: NodeStateSchema5; + 7: NodeStateSchema7; + 10: NodeStateSchema10; + 14: NodeStateSchema14; + 29: NodeStateSchema29; + 30: NodeStateSchema30; + 31: NodeStateSchema31; + 35: NodeStateSchema35; + 42: NodeStateSchema42; + 47: NodeStateSchema47; + }; +}