diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9b86897b6b..ce53c4296b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ +## 12.0.4 (2023-10-09) +### Bugfixes +* Normalize result of `Controller.getAvailableFirmwareUpdates` to always include `channel` field (#6359) +* Fixed a crash that could happen while logging dropped sensor readings (#6379) +* Increased the range and default of the `response` timeout to accomodate slower 500 series controllers (#6378) + +### Config file changes +* Treat Basic Set as events for TKB TZ35S/D and TZ55S/D (#6381) +* Add Zooz ZAC38 Range Extender (#6136) +* Corrected the label of the notification event `0x0a` to be `Emergency Alarm` (#6368) + +## 12.0.3 (2023-10-05) +The `v12` release was supposed to increase reliability of Z-Wave JS, primarily by detecting situations where the controller was unable to transmit due to excessive RF noise or being unresponsive and automatically taking the necessary steps to recover. + +Instead, it uncovered bugs and erratical behavior in the 500 series firmwares, which triggered the automatic recovery in situations where it was not necessary. In the worst case, this would cause Z-Wave JS to end up in an infinite loop or restart over and over. + +This patch should fix and/or work around most (if not all) of these issues. Really sorry for the inconvenience! + +### Bugfixes +* Fixed an infinite loop caused by assuming the controller was temporarily unable to transmit when when sending a command results in the transmit status `Fail` (#6361) +* Added a workaround to avoid a restart loop caused by 500 series controllers replying with invalid commands when assigning routes back to the controller (SUC) failed (#6370, #6372) +* Automatically recovering an unresponsive controller by restarting it or Z-Wave JS in case of a missing callback is now only done for `SendData` commands. Previously some commands which were expecting a specific command to be received from a node could also trigger this, even if that command was not technically a command callback. (#6373) +* Fixed an issue where rebuilding routes would throw an error because of calling the wrong method internally (#6362) + ## 12.0.2 (2023-09-29) ### Bugfixes * The workaround from `v12.0.0` for the `7.19.x` SDK bug was not working correctly when the command that caused the controller to get stuck could be retried. This has now been fixed. (#6343) @@ -27,6 +51,7 @@ Home Assistant users who manage `zwave-js-server` themselves, **must** install t * `zwave-js-server` **1.32.0** ### Breaking changes ยท [Migration guide](https://zwave-js.github.io/node-zwave-js/#/getting-started/migrating-to-v12) +* Removed auto-disabling of soft-reset capability. If Z-Wave JS is no longer able to communicate with the controller after updating, please read [this issue](https://github.com/zwave-js/node-zwave-js/issues/6341) (#6256) * Remove support for Node.js 14 and 16 (#6245) * Subpath exports are now exposed using the `exports` field in `package.json` instead of `typesVersions` (#5839) * The `"notification"` event now includes a reference to the endpoint that sent the notification (#6083) @@ -43,7 +68,6 @@ Home Assistant users who manage `zwave-js-server` themselves, **must** install t ### Bugfixes * A bug in the `7.19.x` SDK has surfaced where the controller gets stuck in the middle of a transmission. Previously this would go unnoticed because the failed commands would cause the nodes to be marked dead until the controller finally recovered. Since `v11.12.0` however, Z-Wave JS would consider the controller jammed and retry the last command indefinitely. This situation is now detected and Z-Wave JS attempts to recover by soft-resetting the controller when this happens. (#6296) -* Removed auto-disabling of soft-reset capability (#6256) * Default to RF protection state `Unprotected` if not given for `Protection CC` V2+ (#6257) ### Config file changes diff --git a/docs/api/driver.md b/docs/api/driver.md index 40149d8fc604..cbab3b66160f 100644 --- a/docs/api/driver.md +++ b/docs/api/driver.md @@ -698,6 +698,9 @@ interface ZWaveOptions extends ZWaveHostOptions { /** How long generated nonces are valid */ nonce: number; // [3000...20000], default: 5000 ms + /** How long to wait before retrying a command when the controller is jammed */ + retryJammed: number; // [10...5000], default: 1000 ms + /** * How long to wait without pending commands before sending a node back to sleep. * Should be as short as possible to save battery, but long enough to give applications time to react. @@ -736,6 +739,9 @@ interface ZWaveOptions extends ZWaveHostOptions { /** How often the driver should try sending SendData commands before giving up */ sendData: number; // [1...5], default: 3 + /** How often the driver should retry SendData commands while the controller is jammed */ + sendDataJammed: number; // [1...10], default: 5 + /** * How many attempts should be made for each node interview before giving up */ diff --git a/docs/config-files/file-format.md b/docs/config-files/file-format.md index a23174ba8b24..90fc592bc6f1 100644 --- a/docs/config-files/file-format.md +++ b/docs/config-files/file-format.md @@ -364,6 +364,12 @@ Several command classes are refreshed regularly (every couple of hours) if they By default, received `Basic CC::Report` commands are mapped to a more appropriate CC. Setting `disableBasicMapping` to `true` disables this feature. +### `disableCallbackFunctionTypeCheck` + +By default, responses or callbacks for Serial API commands must have the same function type (command identifier) in order to be recognized. However, in some situations, certain controllers send a callback with an invalid function type. In this case, the faulty commands may be listed in the `disableCallbackFunctionTypeCheck` array to disable the check for a matching function type. + +> [!NOTE] This compat flag requires command-specific support and is not a generic escape hatch. + ### `disableStrictEntryControlDataValidation` The specifications mandate strict rules for the data and sequence numbers in `Entry Control CC Notifications`, which some devices do not follow, causing the notifications to get dropped. Setting `disableStrictEntryControlDataValidation` to `true` disables these strict checks. diff --git a/maintenance/schemas/device-config.json b/maintenance/schemas/device-config.json index cc41d9a053e4..85141039e110 100644 --- a/maintenance/schemas/device-config.json +++ b/maintenance/schemas/device-config.json @@ -568,6 +568,14 @@ "disableBasicMapping": { "const": true }, + "disableCallbackFunctionTypeCheck": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + }, + "minItems": 1 + }, "disableStrictEntryControlDataValidation": { "const": true }, diff --git a/package.json b/package.json index 8abbe45643f3..56d43cf9d78e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/repo", - "version": "12.0.2", + "version": "12.0.4", "private": true, "description": "Z-Wave driver written entirely in JavaScript/TypeScript", "keywords": [], @@ -40,7 +40,7 @@ "@dprint/json": "^0.17.4", "@dprint/markdown": "^0.16.0", "@dprint/typescript": "^0.87.1", - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@monorepo-utils/workspaces-to-typescript-project-references": "^2.10.2", "@tsconfig/node18": "^18.2.1", "@types/fs-extra": "^11.0.1", diff --git a/packages/cc/package.json b/packages/cc/package.json index a5a9eda89998..a04cd3ec0748 100644 --- a/packages/cc/package.json +++ b/packages/cc/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/cc", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: Command Classes", "keywords": [], "publishConfig": { @@ -71,7 +71,7 @@ "reflect-metadata": "^0.1.13" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/fs-extra": "^11.0.1", "@types/node": "^18.17.14", "@zwave-js/maintenance": "workspace:*", diff --git a/packages/cc/src/cc/MultilevelSensorCC.ts b/packages/cc/src/cc/MultilevelSensorCC.ts index b40d1c23e366..7bc3cc8f4c6e 100644 --- a/packages/cc/src/cc/MultilevelSensorCC.ts +++ b/packages/cc/src/cc/MultilevelSensorCC.ts @@ -670,7 +670,7 @@ export class MultilevelSensorCCReport extends MultilevelSensorCC { if (supportedSensorTypes?.length) { validatePayload.withReason( `Unsupported sensor type ${ - sensorType!.label + applHost.configManager.getSensorTypeName(this.type) } or corrupted data`, )(supportedSensorTypes.includes(this.type)); } diff --git a/packages/config/api.md b/packages/config/api.md index ebd6418eb9bc..3fbf2a3d13e4 100644 --- a/packages/config/api.md +++ b/packages/config/api.md @@ -144,6 +144,8 @@ export class ConditionalCompatConfig implements ConditionalItem { // (undocumented) readonly disableBasicMapping?: boolean; // (undocumented) + readonly disableCallbackFunctionTypeCheck?: number[]; + // (undocumented) readonly disableStrictEntryControlDataValidation?: boolean; // (undocumented) readonly disableStrictMeasurementValidation?: boolean; diff --git a/packages/config/config/devices/0x0000/husbzb-1.json b/packages/config/config/devices/0x0000/husbzb-1.json index a037369ffe78..86bd1a8521b5 100644 --- a/packages/config/config/devices/0x0000/husbzb-1.json +++ b/packages/config/config/devices/0x0000/husbzb-1.json @@ -12,5 +12,9 @@ "firmwareVersion": { "min": "0.0", "max": "255.255" + }, + "compat": { + // Workaround for a firmware bug in 500 series controllers + "$import": "~/templates/master_template.json#500_series_controller_compat_flags" } } diff --git a/packages/config/config/devices/0x0086/zw090.json b/packages/config/config/devices/0x0086/zw090.json index ffde9637b1b3..1152ac38ab0d 100644 --- a/packages/config/config/devices/0x0086/zw090.json +++ b/packages/config/config/devices/0x0086/zw090.json @@ -58,5 +58,9 @@ "metadata": { "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + }, + "compat": { + // Workaround for a firmware bug in 500 series controllers + "$import": "~/templates/master_template.json#500_series_controller_compat_flags" } } diff --git a/packages/config/config/devices/0x0118/tz35s.json b/packages/config/config/devices/0x0118/tz35s_tz35d_tz55s_tz55d.json similarity index 89% rename from packages/config/config/devices/0x0118/tz35s.json rename to packages/config/config/devices/0x0118/tz35s_tz35d_tz55s_tz55d.json index 3677d26f4322..ce33ae9f8a70 100644 --- a/packages/config/config/devices/0x0118/tz35s.json +++ b/packages/config/config/devices/0x0118/tz35s_tz35d_tz55s_tz55d.json @@ -1,8 +1,8 @@ { "manufacturer": "TKB Home", "manufacturerId": "0x0118", - "label": "TZ55S", - "description": "Single Paddle Wall Dimmer", + "label": "TZ35S / TZ35D / TZ55S / TZ55D", + "description": "Single/Dual Paddle Wall Dimmer", "devices": [ { "productType": "0x0808", @@ -97,5 +97,9 @@ } ] } - ] + ], + "compat": { + // The right paddle sends its status via Basic Set commands + "treatBasicSetAsEvent": true + } } diff --git a/packages/config/config/devices/0x027a/zac38.json b/packages/config/config/devices/0x027a/zac38.json new file mode 100644 index 000000000000..ad268305e513 --- /dev/null +++ b/packages/config/config/devices/0x027a/zac38.json @@ -0,0 +1,61 @@ +{ + "manufacturer": "Zooz", + "manufacturerId": "0x027a", + "label": "ZAC38", + "description": "Range Extender", + "devices": [ + { + "productType": "0x0004", + "productId": "0x0510" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": [ + { + "#": "1", + "$import": "templates/zooz_template.json#low_battery_alarm_threshold", + "defaultValue": 10 + }, + { + "#": "2", + "$import": "~/templates/master_template.json#base_enable_disable", + "label": "Enable Battery Threshold Reports", + "defaultValue": 1 + }, + { + "#": "3", + "$import": "templates/zooz_template.json#battery_report_threshold", + "minValue": 5 + }, + { + "#": "4", + "label": "Battery Check Interval", + "description": "How often the device checks the battery level.", + "valueSize": 2, + "unit": "seconds", + "minValue": 1, + "maxValue": 65535, + "defaultValue": 600, + "unsigned": true + }, + { + "#": "5", + "$import": "~/templates/master_template.json#base_enable_disable", + "label": "Enable Timed Battery Reports", + "defaultValue": 1 + }, + { + "#": "6", + "label": "Battery Report Interval", + "valueSize": 2, + "unit": "seconds", + "minValue": 30, + "maxValue": 65535, + "defaultValue": 3600, + "unsigned": true + } + ] +} diff --git a/packages/config/config/devices/0x027a/zst10.json b/packages/config/config/devices/0x027a/zst10.json index 7522db68d1b8..4ffed3a31d4d 100644 --- a/packages/config/config/devices/0x027a/zst10.json +++ b/packages/config/config/devices/0x027a/zst10.json @@ -12,5 +12,9 @@ "firmwareVersion": { "min": "0.0", "max": "255.255" + }, + "compat": { + // Workaround for a firmware bug in 500 series controllers + "$import": "~/templates/master_template.json#500_series_controller_compat_flags" } } diff --git a/packages/config/config/devices/templates/master_template.json b/packages/config/config/devices/templates/master_template.json index 3c08bfebc998..affa0a3a467a 100644 --- a/packages/config/config/devices/templates/master_template.json +++ b/packages/config/config/devices/templates/master_template.json @@ -675,5 +675,15 @@ "text": "Firmware version 7.19.3 has a bug that causes the controller to randomly hang during transmission until it is restarted. It is currently unclear if this bug is fixed in a later firmware version." } ] + }, + "500_series_controller_compat_flags": { + // It seems that all 500 series controllers have a firmware bug: + + // When failing, AssignSUCReturnRoute and DeleteSUCReturnRoute get answered with a wrong callback function type, + // triggering Z-Wave JS's unresponsive controller check. + "disableCallbackFunctionTypeCheck": [ + 81, // AssignSUCReturnRoute + 85 // DeleteSUCReturnRoute + ] } } diff --git a/packages/config/config/notifications.json b/packages/config/config/notifications.json index 459e346dd318..c37ab18e8221 100644 --- a/packages/config/config/notifications.json +++ b/packages/config/config/notifications.json @@ -928,7 +928,7 @@ } }, "0x0a": { - "name": "System", + "name": "Emergency Alarm", "events": { "0x01": { "label": "Contact police" diff --git a/packages/config/package.json b/packages/config/package.json index 5383a4c00463..9a0a4edcc4a5 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/config", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: configuration files", "publishConfig": { "access": "public" @@ -66,7 +66,7 @@ "winston": "^3.10.0" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/fs-extra": "^11.0.1", "@types/js-levenshtein": "^1.1.1", "@types/json-logic-js": "^2.0.2", @@ -88,7 +88,7 @@ "sinon": "^15.2.0", "ts-pegjs": "^0.3.1", "typescript": "5.2.2", - "xml2js": "^0.5.0", + "xml2js": "^0.6.2", "yargs": "^17.7.2" } } diff --git a/packages/config/src/devices/CompatConfig.ts b/packages/config/src/devices/CompatConfig.ts index 3a513d5fdbf3..ce20fa61028f 100644 --- a/packages/config/src/devices/CompatConfig.ts +++ b/packages/config/src/devices/CompatConfig.ts @@ -88,6 +88,23 @@ compat option disableBasicMapping must be true or omitted`, this.disableBasicMapping = definition.disableBasicMapping; } + if (definition.disableCallbackFunctionTypeCheck != undefined) { + if ( + !isArray(definition.disableCallbackFunctionTypeCheck) + || !definition.disableCallbackFunctionTypeCheck.every( + (d: any) => typeof d === "number" && d % 1 === 0 && d > 0, + ) + ) { + throwInvalidConfig( + "devices", + `config/devices/${filename}: +when present, compat option disableCallbackFunctionTypeCheck msut be an array of positive integers`, + ); + } + this.disableCallbackFunctionTypeCheck = + definition.disableCallbackFunctionTypeCheck; + } + if (definition.disableStrictEntryControlDataValidation != undefined) { if (definition.disableStrictEntryControlDataValidation !== true) { throwInvalidConfig( @@ -568,6 +585,7 @@ compat option overrideQueries must be an object!`, public readonly disableBasicMapping?: boolean; public readonly disableStrictEntryControlDataValidation?: boolean; public readonly disableStrictMeasurementValidation?: boolean; + public readonly disableCallbackFunctionTypeCheck?: number[]; public readonly enableBasicSetMapping?: boolean; public readonly forceNotificationIdleReset?: boolean; public readonly forceSceneControllerGroupCount?: number; @@ -608,6 +626,7 @@ compat option overrideQueries must be an object!`, "removeCCs", "disableAutoRefresh", "disableBasicMapping", + "disableCallbackFunctionTypeCheck", "disableStrictEntryControlDataValidation", "disableStrictMeasurementValidation", "enableBasicSetMapping", diff --git a/packages/core/api.md b/packages/core/api.md index 3f4fcb0c3038..ad8008f316be 100644 --- a/packages/core/api.md +++ b/packages/core/api.md @@ -1218,17 +1218,25 @@ export function isMessagePriority(val: unknown): val is MessagePriority; // Warning: (ae-missing-release-tag) "isMissingControllerACK" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function isMissingControllerACK(e: unknown): e is ZWaveError; +export function isMissingControllerACK(e: unknown): e is ZWaveError & { + code: ZWaveErrorCodes.Controller_Timeout; + context: "ACK"; +}; // Warning: (ae-missing-release-tag) "isMissingControllerCallback" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function isMissingControllerCallback(e: unknown): e is ZWaveError; +export function isMissingControllerCallback(e: unknown): e is ZWaveError & { + code: ZWaveErrorCodes.Controller_Timeout; + context: "callback"; +}; // Warning: (ae-missing-release-tag) "isRecoverableZWaveError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function isRecoverableZWaveError(e: unknown): e is ZWaveError; +export function isRecoverableZWaveError(e: unknown): e is ZWaveError & { + code: ZWaveErrorCodes.Controller_InterviewRestarted | ZWaveErrorCodes.Controller_NodeRemoved; +}; // Warning: (ae-missing-release-tag) "isRssiError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2627,6 +2635,14 @@ export enum TransmitStatus { // @public export function tryParseDSKFromQRCodeString(qr: string): string | undefined; +// Warning: (ae-missing-release-tag) "tryParseParamNumber" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function tryParseParamNumber(str: string): { + parameter: number; + valueBitMask?: number; +} | undefined; + // Warning: (ae-missing-release-tag) "TXReport" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/core/package.json b/packages/core/package.json index 11f52548323e..24a5a18b9397 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/core", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: core components", "keywords": [], "publishConfig": { @@ -69,7 +69,7 @@ "winston-transport": "^4.5.0" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/node": "^18.17.14", "@types/sinon": "^10.0.16", "@types/triple-beam": "^1.3.2", diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 6fcac0c4e862..842fdc09a349 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/eslint-plugin", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: custom ESLint rules", "private": true, "keywords": [], diff --git a/packages/flash/package.json b/packages/flash/package.json index 50b11f89757b..b033866b5304 100644 --- a/packages/flash/package.json +++ b/packages/flash/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/flash", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: firmware flash utility", "keywords": [], "publishConfig": { diff --git a/packages/host/package.json b/packages/host/package.json index 2675f5e6fa29..9d26e92fa2ee 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/host", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: Host abstractions", "keywords": [], "publishConfig": { @@ -55,7 +55,7 @@ "alcalzone-shared": "^4.0.8" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/node": "^18.17.14", "del-cli": "^5.1.0", "typescript": "5.2.2" diff --git a/packages/maintenance/package.json b/packages/maintenance/package.json index 67115e166224..d7994c316c21 100644 --- a/packages/maintenance/package.json +++ b/packages/maintenance/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/maintenance", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: maintenance scripts", "private": true, "keywords": [], diff --git a/packages/nvmedit/package.json b/packages/nvmedit/package.json index 88d2c461b90d..ed468d7823c4 100644 --- a/packages/nvmedit/package.json +++ b/packages/nvmedit/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/nvmedit", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: library to edit NVM backups", "keywords": [], "publishConfig": { @@ -62,7 +62,7 @@ "test:dirty": "node -r ../../maintenance/esbuild-register.js ../maintenance/src/resolveDirtyTests.ts --run" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/fs-extra": "^11.0.1", "@types/node": "^18.17.14", "@types/semver": "^7.5.1", diff --git a/packages/serial/api.md b/packages/serial/api.md index e969db92054c..4ac14b6a9282 100644 --- a/packages/serial/api.md +++ b/packages/serial/api.md @@ -467,7 +467,7 @@ export class Message { getResponseTimeout(): number | undefined; hasCallbackId(): boolean; // (undocumented) - protected host: ZWaveHost; + readonly host: ZWaveHost; static isComplete(data?: Buffer): boolean; isExpectedCallback(msg: Message): boolean; isExpectedNodeUpdate(msg: Message): boolean; diff --git a/packages/serial/package.json b/packages/serial/package.json index d07462e416da..3ddf599d1b75 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/serial", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: Serialport driver", "publishConfig": { "access": "public" @@ -64,7 +64,7 @@ "winston": "^3.10.0" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@serialport/binding-mock": "^10.2.2", "@serialport/bindings-interface": "*", "@types/node": "^18.17.14", diff --git a/packages/serial/src/message/Message.ts b/packages/serial/src/message/Message.ts index bd6a61d419db..4ef2e7d83240 100644 --- a/packages/serial/src/message/Message.ts +++ b/packages/serial/src/message/Message.ts @@ -70,7 +70,7 @@ export type MessageOptions = */ export class Message { public constructor( - protected host: ZWaveHost, + public readonly host: ZWaveHost, options: MessageOptions = {}, ) { // decide which implementation we follow @@ -355,12 +355,18 @@ export class Message { /** Checks if a message is an expected callback for this message */ public isExpectedCallback(msg: Message): boolean { if (msg.type !== MessageType.Request) return false; - // If a received request included a callback id, enforce that the response contains the same - if ( - this.hasCallbackId() - && (!msg.hasCallbackId() || this._callbackId !== msg._callbackId) - ) { - return false; + + // Some controllers have a bug causing them to send a callback with a function type of 0 and no callback ID + // To prevent this from triggering the unresponsive controller detection we need to forward these messages as if they were correct + if (msg.functionType !== 0 as any) { + // If a received request included a callback id, enforce that the response contains the same + if ( + this.hasCallbackId() + && (!msg.hasCallbackId() + || this._callbackId !== msg._callbackId) + ) { + return false; + } } return this.testMessage(msg, this.expectedCallback); diff --git a/packages/shared/package.json b/packages/shared/package.json index 2c352923b88e..3e45b62beb04 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/shared", - "version": "12.0.0", + "version": "12.0.3", "description": "zwave-js: shared utilities", "keywords": [], "publishConfig": { @@ -55,7 +55,7 @@ "test:dirty": "node -r ../../maintenance/esbuild-register.js ../maintenance/src/resolveDirtyTests.ts --run" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/fs-extra": "^11.0.1", "@types/node": "^18.17.14", "@types/sinon": "^10.0.16", diff --git a/packages/testing/api.md b/packages/testing/api.md index e53f35fde54d..eb2efdf05a43 100644 --- a/packages/testing/api.md +++ b/packages/testing/api.md @@ -101,6 +101,11 @@ export interface EnergyProductionCCCapabilities { }; } +// Warning: (ae-missing-release-tag) "getDefaultSupportedFunctionTypes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function getDefaultSupportedFunctionTypes(): FunctionType[]; + // Warning: (ae-missing-release-tag) "LazyMockZWaveFrame" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/packages/testing/package.json b/packages/testing/package.json index f0d818aa7c91..a1051fbd5ac8 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/testing", - "version": "12.0.2", + "version": "12.0.4", "description": "zwave-js: testing utilities", "keywords": [], "publishConfig": { @@ -46,7 +46,7 @@ "ansi-colors": "^4.1.3" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/node": "^18.17.14", "@types/triple-beam": "^1.3.2", "del-cli": "^5.1.0", diff --git a/packages/testing/src/MockControllerCapabilities.ts b/packages/testing/src/MockControllerCapabilities.ts index f4c0a4a99b9f..19700d231b24 100644 --- a/packages/testing/src/MockControllerCapabilities.ts +++ b/packages/testing/src/MockControllerCapabilities.ts @@ -31,23 +31,27 @@ export interface MockControllerCapabilities { watchdogEnabled: boolean; } +export function getDefaultSupportedFunctionTypes(): FunctionType[] { + return [ + FunctionType.GetSerialApiInitData, + FunctionType.GetControllerCapabilities, + FunctionType.SendData, + FunctionType.SendDataMulticast, + FunctionType.GetControllerVersion, + FunctionType.GetControllerId, + FunctionType.GetNodeProtocolInfo, + FunctionType.RequestNodeInfo, + FunctionType.AssignSUCReturnRoute, + ]; +} + export function getDefaultMockControllerCapabilities(): MockControllerCapabilities { return { firmwareVersion: "1.0", manufacturerId: 0xffff, productType: 0xffff, productId: 0xfffe, - supportedFunctionTypes: [ - FunctionType.GetSerialApiInitData, - FunctionType.GetControllerCapabilities, - FunctionType.SendData, - FunctionType.SendDataMulticast, - FunctionType.GetControllerVersion, - FunctionType.GetControllerId, - FunctionType.GetNodeProtocolInfo, - FunctionType.RequestNodeInfo, - FunctionType.AssignSUCReturnRoute, - ], + supportedFunctionTypes: getDefaultSupportedFunctionTypes(), controllerType: ZWaveLibraryTypes["Static Controller"], libraryVersion: "Z-Wave 7.17.99", diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 8039e368caba..6a59d60e57f9 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -1,5 +1,6 @@ export * from "./CCSpecificCapabilities"; export * from "./MockController"; +export { getDefaultSupportedFunctionTypes } from "./MockControllerCapabilities"; export * from "./MockNode"; export { ccCaps } from "./MockNodeCapabilities"; export type { PartialCCCapabilities } from "./MockNodeCapabilities"; diff --git a/packages/zwave-js/api.md b/packages/zwave-js/api.md index 97e01f528341..184cba7c2e15 100644 --- a/packages/zwave-js/api.md +++ b/packages/zwave-js/api.md @@ -1658,6 +1658,7 @@ export interface ZWaveOptions extends ZWaveHostOptions { attempts: { controller: number; sendData: number; + sendDataJammed: number; nodeInterview: number; }; disableOptimisticValueUpdate?: boolean; @@ -1710,6 +1711,7 @@ export interface ZWaveOptions extends ZWaveHostOptions { sendDataCallback: number; report: number; nonce: number; + retryJammed: number; sendToSleep: number; refreshValue: number; refreshValueAfterTransition: number; diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index 716533e9f3e8..f16be8d2a2b2 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -1,6 +1,6 @@ { "name": "zwave-js", - "version": "12.0.2", + "version": "12.0.4", "description": "Z-Wave driver written entirely in JavaScript/TypeScript", "keywords": [], "main": "build/index.js", @@ -107,7 +107,7 @@ "xstate": "4.38.2" }, "devDependencies": { - "@microsoft/api-extractor": "^7.36.4", + "@microsoft/api-extractor": "^7.37.3", "@types/fs-extra": "^11.0.1", "@types/node": "^18.17.14", "@types/proper-lockfile": "^4.1.2", diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index d8d4a47dd04b..571dfa70f4c8 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -215,7 +215,7 @@ import { } from "../serialapi/network-mgmt/AssignReturnRouteMessages"; import { AssignSUCReturnRouteRequest, - type AssignSUCReturnRouteRequestTransmitReport, + AssignSUCReturnRouteRequestTransmitReport, } from "../serialapi/network-mgmt/AssignSUCReturnRouteMessages"; import { DeleteReturnRouteRequest, @@ -223,7 +223,7 @@ import { } from "../serialapi/network-mgmt/DeleteReturnRouteMessages"; import { DeleteSUCReturnRouteRequest, - type DeleteSUCReturnRouteRequestTransmitReport, + DeleteSUCReturnRouteRequestTransmitReport, } from "../serialapi/network-mgmt/DeleteSUCReturnRouteMessages"; import { GetPriorityRouteRequest, @@ -4175,7 +4175,7 @@ supported CCs: ${ } // 2. re-create the SUC return route, just in case - node.hasSUCReturnRoute ||= await this.assignSUCReturnRoutes(nodeId); + node.hasSUCReturnRoute = await this.assignSUCReturnRoutes(nodeId); // 3. delete all return routes to get rid of potential priority return routes for (let attempt = 1; attempt <= maxAttempts; attempt++) { @@ -4199,7 +4199,7 @@ supported CCs: ${ } } - // 4. Assign return routes to all association destinations. + // 4. Assign return routes to all association destinations... let associatedNodes: number[] = []; try { associatedNodes = distinct( @@ -4208,14 +4208,14 @@ supported CCs: ${ (assocs: AssociationAddress[]) => assocs.map((a) => a.nodeId), ), - ).sort(); + ) + // ...except the controller itself, which was handled by step 2 + .filter((id) => id !== this._ownNodeId!) + .sort(); } catch { /* ignore */ } - // One of those should probably be the controller. Not sure if the SUC return route is enough. - if (!associatedNodes.includes(this._ownNodeId!)) { - associatedNodes.unshift(this._ownNodeId!); - } + this.driver.controllerLog.logNode(nodeId, { message: `assigning return routes to the following nodes: ${associatedNodes.join(", ")}`, @@ -4319,14 +4319,23 @@ ${associatedNodes.join(", ")}`, await this.deleteSUCReturnRoutes(nodeId); try { - const result = await this.driver.sendMessage< - AssignSUCReturnRouteRequestTransmitReport - >( + const result = await this.driver.sendMessage( new AssignSUCReturnRouteRequest(this.driver, { nodeId, }), ); + if ( + !(result instanceof AssignSUCReturnRouteRequestTransmitReport) + ) { + this.driver.controllerLog.logNode( + nodeId, + `Assigning SUC return route failed: Invalid callback received`, + "error", + ); + return false; + } + const success = this.handleRouteAssignmentTransmitReport( result, nodeId, @@ -4492,14 +4501,23 @@ ${associatedNodes.join(", ")}`, }); try { - const result = await this.driver.sendMessage< - DeleteSUCReturnRouteRequestTransmitReport - >( + const result = await this.driver.sendMessage( new DeleteSUCReturnRouteRequest(this.driver, { nodeId, }), ); + if ( + !(result instanceof DeleteSUCReturnRouteRequestTransmitReport) + ) { + this.driver.controllerLog.logNode( + nodeId, + `Deleting SUC return route failed: Invalid callback received`, + "error", + ); + return false; + } + const success = this.handleRouteAssignmentTransmitReport( result, nodeId, @@ -5244,7 +5262,11 @@ ${associatedNodes.join(", ")}`, // Except to the controller itself - this route is already known ).filter((id) => id !== this.ownNodeId); for (const id of destinationNodeIDs) { - await this.assignReturnRoutes(source.nodeId, id); + if (id === this._ownNodeId) { + await this.assignSUCReturnRoutes(source.nodeId); + } else { + await this.assignReturnRoutes(source.nodeId, id); + } } } diff --git a/packages/zwave-js/src/lib/controller/FirmwareUpdateService.ts b/packages/zwave-js/src/lib/controller/FirmwareUpdateService.ts index 41afabf05477..f8ea1597955f 100644 --- a/packages/zwave-js/src/lib/controller/FirmwareUpdateService.ts +++ b/packages/zwave-js/src/lib/controller/FirmwareUpdateService.ts @@ -226,6 +226,7 @@ export async function getAvailableFirmwareUpdates( return result.map((update) => ({ device: deviceId, ...update, + channel: update.channel ?? "stable", })); } diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index af8c4cca3219..022af36393c8 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -249,11 +249,14 @@ const defaultOptions: ZWaveOptions = { timeouts: { ack: 1000, byte: 150, - response: 10000, + // Ideally we'd want to have this as low as possible, but some + // 500 series controllers can take upwards of 10 seconds to respond sometimes. + response: 30000, report: 1000, // ReportTime timeout SHOULD be set to CommandTime + 1 second nonce: 5000, sendDataCallback: 65000, // as defined in INS13954 sendToSleep: 250, // The default should be enough time for applications to react to devices waking up + retryJammed: 1000, refreshValue: 5000, // Default should handle most slow devices until we have a better solution refreshValueAfterTransition: 1000, // To account for delays in the device serialAPIStarted: 5000, @@ -262,6 +265,7 @@ const defaultOptions: ZWaveOptions = { openSerialPort: 10, controller: 3, sendData: 3, + sendDataJammed: 5, nodeInterview: 5, }, disableOptimisticValueUpdate: false, @@ -295,9 +299,9 @@ function checkOptions(options: ZWaveOptions): void { ZWaveErrorCodes.Driver_InvalidOptions, ); } - if (options.timeouts.response < 500 || options.timeouts.response > 20000) { + if (options.timeouts.response < 500 || options.timeouts.response > 60000) { throw new ZWaveError( - `The Response timeout must be between 500 and 20000 milliseconds!`, + `The Response timeout must be between 500 and 60000 milliseconds!`, ZWaveErrorCodes.Driver_InvalidOptions, ); } @@ -313,6 +317,14 @@ function checkOptions(options: ZWaveOptions): void { ZWaveErrorCodes.Driver_InvalidOptions, ); } + if ( + options.timeouts.retryJammed < 10 || options.timeouts.retryJammed > 5000 + ) { + throw new ZWaveError( + `The timeout for retrying while jammed must be between 10 and 5000 milliseconds!`, + ZWaveErrorCodes.Driver_InvalidOptions, + ); + } if ( options.timeouts.sendToSleep < 10 || options.timeouts.sendToSleep > 5000 ) { @@ -369,6 +381,15 @@ function checkOptions(options: ZWaveOptions): void { ZWaveErrorCodes.Driver_InvalidOptions, ); } + if ( + options.attempts.sendDataJammed < 1 + || options.attempts.sendDataJammed > 10 + ) { + throw new ZWaveError( + `The SendData attempts while jammed must be between 1 and 10!`, + ZWaveErrorCodes.Driver_InvalidOptions, + ); + } if ( options.attempts.nodeInterview < 1 || options.attempts.nodeInterview > 10 @@ -4629,7 +4650,8 @@ ${handlers.length} left`, // Step through the transaction as long as it gives us a next message while ((msg = await transaction.generateNextMessage(prevResult))) { - // TODO: refactor this nested loop or make it part of executeSerialAPICommand + // Keep track of how often the controller failed to send a command, to prevent ending up in an infinite loop + let jammedAttempts = 0; attemptMessage: for (let attemptNumber = 1;; attemptNumber++) { try { prevResult = await this.queueSerialAPICommand( @@ -4647,11 +4669,34 @@ ${handlers.length} left`, // Ensure the controller didn't actually transmit && prevResult.txReport?.txTicks === 0 ) { - // The controller is jammed. Wait a second, then try again. - this.controller.setStatus(ControllerStatus.Jammed); - await wait(1000, true); - - continue attemptMessage; + jammedAttempts++; + if ( + jammedAttempts + < this.options.attempts.sendDataJammed + ) { + // The controller is jammed. Wait a bit, then try again. + this.controller.setStatus( + ControllerStatus.Jammed, + ); + await wait( + this.options.timeouts.retryJammed, + true, + ); + + continue attemptMessage; + } else { + // Maybe this isn't actually the controller being jammed. Give up on this command. + this.controller.setStatus( + ControllerStatus.Ready, + ); + + throw new ZWaveError( + `Failed to send the command after ${jammedAttempts} attempts`, + ZWaveErrorCodes.Controller_MessageDropped, + prevResult, + transaction.stack, + ); + } } if ( @@ -4699,12 +4744,18 @@ ${handlers.length} left`, } else if (isMissingControllerACK(e)) { // The controller is unresponsive. Reject the transaction, so we can attempt to recover throw e; + } else if ( + e.code === ZWaveErrorCodes.Controller_MessageDropped + ) { + // We gave up on this command, so don't retry it + throw e; } if ( this.mayRetrySerialAPICommand( msg, - attemptNumber, + // Ignore the number of attempts while jammed + attemptNumber - jammedAttempts, e.code, ) ) { @@ -5516,7 +5567,9 @@ ${handlers.length} left`, if (this.isMissingNodeACK(transaction, error)) { if (this.handleMissingNodeACK(transaction as any, error)) return; } else if ( - isMissingControllerACK(error) || isMissingControllerCallback(error) + isMissingControllerACK(error) + || (isSendData(transaction.message) + && isMissingControllerCallback(error)) ) { if (this.handleUnresponsiveController(transaction, error)) return; } diff --git a/packages/zwave-js/src/lib/driver/ZWaveOptions.ts b/packages/zwave-js/src/lib/driver/ZWaveOptions.ts index 646e7227c618..b02293e414b8 100644 --- a/packages/zwave-js/src/lib/driver/ZWaveOptions.ts +++ b/packages/zwave-js/src/lib/driver/ZWaveOptions.ts @@ -18,7 +18,7 @@ export interface ZWaveOptions extends ZWaveHostOptions { * How long to wait for a controller response. Usually this timeout should never elapse, * so this is merely a safeguard against the driver stalling. */ - response: number; // [500...20000], default: 10000 ms + response: number; // [500...60000], default: 30000 ms /** How long to wait for a callback from the host for a SendData[Multicast]Request */ sendDataCallback: number; // >=10000, default: 65000 ms @@ -29,6 +29,9 @@ export interface ZWaveOptions extends ZWaveHostOptions { /** How long generated nonces are valid */ nonce: number; // [3000...20000], default: 5000 ms + /** How long to wait before retrying a command when the controller is jammed */ + retryJammed: number; // [10...5000], default: 1000 ms + /** * How long to wait without pending commands before sending a node back to sleep. * Should be as short as possible to save battery, but long enough to give applications time to react. @@ -73,6 +76,9 @@ export interface ZWaveOptions extends ZWaveHostOptions { /** How often the driver should try sending SendData commands before giving up */ sendData: number; // [1...5], default: 3 + /** How often the driver should retry SendData commands while the controller is jammed */ + sendDataJammed: number; // [1...10], default: 5 + /** * How many attempts should be made for each node interview before giving up */ diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts index e70b3f3a4394..1eb2f7fc64e5 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts @@ -53,8 +53,25 @@ export interface AssignSUCReturnRouteRequestOptions extends MessageBaseOptions { nodeId: number; } +function testAssignSUCReturnRouteCallback( + sent: AssignSUCReturnRouteRequest, + callback: Message, +): boolean { + // Some controllers have a bug where they incorrectly respond with DeleteSUCReturnRoute + if ( + callback.host + .getDeviceConfig?.(callback.host.ownNodeId) + ?.compat + ?.disableCallbackFunctionTypeCheck + ?.includes(FunctionType.AssignSUCReturnRoute) + ) { + return true; + } + return callback.functionType === FunctionType.AssignSUCReturnRoute; +} + @expectedResponse(FunctionType.AssignSUCReturnRoute) -@expectedCallback(FunctionType.AssignSUCReturnRoute) +@expectedCallback(testAssignSUCReturnRouteCallback) export class AssignSUCReturnRouteRequest extends AssignSUCReturnRouteRequestBase implements INodeQuery { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts index 86e62ad70e51..40960535f621 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts @@ -2,8 +2,6 @@ import { type MessageOrCCLogEntry, MessagePriority, TransmitStatus, - ZWaveError, - ZWaveErrorCodes, encodeNodeID, } from "@zwave-js/core"; import type { ZWaveHost } from "@zwave-js/host"; @@ -14,6 +12,7 @@ import { type MessageBaseOptions, type MessageDeserializationOptions, type MessageOptions, + MessageOrigin, MessageType, expectedCallback, expectedResponse, @@ -27,12 +26,24 @@ import { getEnumMemberName } from "@zwave-js/shared"; @priority(MessagePriority.Normal) export class DeleteSUCReturnRouteRequestBase extends Message { public constructor(host: ZWaveHost, options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== DeleteSUCReturnRouteRequestTransmitReport - ) { - return new DeleteSUCReturnRouteRequestTransmitReport(host, options); + if (gotDeserializationOptions(options)) { + if ( + options.origin === MessageOrigin.Host + && (new.target as any) !== DeleteSUCReturnRouteRequest + ) { + return new DeleteSUCReturnRouteRequest(host, options); + } else if ( + options.origin !== MessageOrigin.Host + && (new.target as any) + !== DeleteSUCReturnRouteRequestTransmitReport + ) { + return new DeleteSUCReturnRouteRequestTransmitReport( + host, + options, + ); + } } + super(host, options); } } @@ -41,8 +52,25 @@ export interface DeleteSUCReturnRouteRequestOptions extends MessageBaseOptions { nodeId: number; } +function testDeleteSUCReturnRouteCallback( + sent: DeleteSUCReturnRouteRequest, + callback: Message, +): boolean { + // Some controllers have a bug where they incorrectly respond with DeleteSUCReturnRoute + if ( + callback.host + .getDeviceConfig?.(callback.host.ownNodeId) + ?.compat + ?.disableCallbackFunctionTypeCheck + ?.includes(FunctionType.DeleteSUCReturnRoute) + ) { + return true; + } + return callback.functionType === FunctionType.DeleteSUCReturnRoute; +} + @expectedResponse(FunctionType.DeleteSUCReturnRoute) -@expectedCallback(FunctionType.DeleteSUCReturnRoute) +@expectedCallback(testDeleteSUCReturnRouteCallback) export class DeleteSUCReturnRouteRequest extends DeleteSUCReturnRouteRequestBase implements INodeQuery { @@ -54,10 +82,8 @@ export class DeleteSUCReturnRouteRequest extends DeleteSUCReturnRouteRequestBase ) { super(host, options); if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); + this.nodeId = this.payload[0]; + this.callbackId = this.payload[1]; } else { this.nodeId = options.nodeId; } @@ -73,16 +99,26 @@ export class DeleteSUCReturnRouteRequest extends DeleteSUCReturnRouteRequestBase } } +interface DeleteSUCReturnRouteResponseOptions extends MessageBaseOptions { + wasExecuted: boolean; +} + @messageTypes(MessageType.Response, FunctionType.DeleteSUCReturnRoute) export class DeleteSUCReturnRouteResponse extends Message implements SuccessIndicator { public constructor( host: ZWaveHost, - options: MessageDeserializationOptions, + options: + | MessageDeserializationOptions + | DeleteSUCReturnRouteResponseOptions, ) { super(host, options); - this.wasExecuted = this.payload[0] !== 0; + if (gotDeserializationOptions(options)) { + this.wasExecuted = this.payload[0] !== 0; + } else { + this.wasExecuted = options.wasExecuted; + } } public isOK(): boolean { @@ -91,6 +127,11 @@ export class DeleteSUCReturnRouteResponse extends Message public readonly wasExecuted: boolean; + public serialize(): Buffer { + this.payload = Buffer.from([this.wasExecuted ? 0x01 : 0]); + return super.serialize(); + } + public toLogEntry(): MessageOrCCLogEntry { return { ...super.toLogEntry(), @@ -99,18 +140,32 @@ export class DeleteSUCReturnRouteResponse extends Message } } +interface DeleteSUCReturnRouteRequestTransmitReportOptions + extends MessageBaseOptions +{ + transmitStatus: TransmitStatus; + callbackId: number; +} + export class DeleteSUCReturnRouteRequestTransmitReport extends DeleteSUCReturnRouteRequestBase implements SuccessIndicator { public constructor( host: ZWaveHost, - options: MessageDeserializationOptions, + options: + | MessageDeserializationOptions + | DeleteSUCReturnRouteRequestTransmitReportOptions, ) { super(host, options); - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; + if (gotDeserializationOptions(options)) { + this.callbackId = this.payload[0]; + this.transmitStatus = this.payload[1]; + } else { + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + } } public isOK(): boolean { @@ -122,6 +177,11 @@ export class DeleteSUCReturnRouteRequestTransmitReport public readonly transmitStatus: TransmitStatus; + public serialize(): Buffer { + this.payload = Buffer.from([this.callbackId, this.transmitStatus]); + return super.serialize(); + } + public toLogEntry(): MessageOrCCLogEntry { return { ...super.toLogEntry(), diff --git a/packages/zwave-js/src/lib/test/compat/invalidCallbackFunctionTypes.test.ts b/packages/zwave-js/src/lib/test/compat/invalidCallbackFunctionTypes.test.ts new file mode 100644 index 000000000000..5e5d05951f9e --- /dev/null +++ b/packages/zwave-js/src/lib/test/compat/invalidCallbackFunctionTypes.test.ts @@ -0,0 +1,241 @@ +import { WakeUpTime, ZWaveProtocolCCAssignSUCReturnRoute } from "@zwave-js/cc"; +import { TransmitStatus, ZWaveDataRate } from "@zwave-js/core"; +import { FunctionType } from "@zwave-js/serial"; +import { + type MockControllerBehavior, + createMockZWaveRequestFrame, + getDefaultSupportedFunctionTypes, +} from "@zwave-js/testing"; +import { + MockControllerCommunicationState, + MockControllerStateKeys, +} from "../../controller/MockControllerState"; +import { + AssignSUCReturnRouteRequest, + AssignSUCReturnRouteResponse, +} from "../../serialapi/network-mgmt/AssignSUCReturnRouteMessages"; +import { + DeleteSUCReturnRouteRequest, + DeleteSUCReturnRouteRequestTransmitReport, + DeleteSUCReturnRouteResponse, +} from "../../serialapi/network-mgmt/DeleteSUCReturnRouteMessages"; +import { integrationTest } from "../integrationTestSuite"; + +// Repro for https://github.com/zwave-js/node-zwave-js/issues/6363 + +integrationTest( + "Invalid callback function types don't trigger the unresponsive controller detection", + { + // debug: true, + + controllerCapabilities: { + // Aeotec Z-Stick Gen5+, FW 1.2 + manufacturerId: 0x0086, + productType: 0x0001, + productId: 0x005a, + firmwareVersion: "1.2", + supportedFunctionTypes: [ + ...getDefaultSupportedFunctionTypes(), + FunctionType.AssignSUCReturnRoute, + FunctionType.DeleteSUCReturnRoute, + ], + }, + + customSetup: async (driver, controller, mockNode) => { + // Incorrectly respond to AssignSUCReturnRoute with DeleteSUCReturnRoute + const handleAssignSUCReturnRoute: MockControllerBehavior = { + async onHostMessage(host, controller, msg) { + if (msg instanceof AssignSUCReturnRouteRequest) { + // Check if this command is legal right now + const state = controller.state.get( + MockControllerStateKeys.CommunicationState, + ) as MockControllerCommunicationState | undefined; + if ( + state != undefined + && state !== MockControllerCommunicationState.Idle + ) { + throw new Error( + "Received AssignSUCReturnRouteRequest while not idle", + ); + } + + // Put the controller into sending state + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.Sending, + ); + + const expectCallback = msg.callbackId !== 0; + + // Send the command to the node + const node = controller.nodes.get(msg.getNodeId()!)!; + const command = new ZWaveProtocolCCAssignSUCReturnRoute( + host, + { + nodeId: node.id, + destinationNodeId: controller.host.ownNodeId, + repeaters: [], // don't care + routeIndex: 0, // don't care + destinationSpeed: ZWaveDataRate["100k"], + destinationWakeUp: WakeUpTime.None, + }, + ); + const frame = createMockZWaveRequestFrame(command, { + ackRequested: expectCallback, + }); + const ackPromise = controller.sendToNode(node, frame); + + // Notify the host that the message was sent + const res = new AssignSUCReturnRouteResponse(host, { + wasExecuted: true, + }); + await controller.sendToHost(res.serialize()); + + let ack = false; + if (expectCallback) { + // Put the controller into waiting state + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.WaitingForNode, + ); + + // Wait for the ACK and notify the host + try { + const ackResult = await ackPromise; + ack = !!ackResult?.ack; + } catch { + // No response + } + } + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.Idle, + ); + + if (expectCallback) { + const cb = + new DeleteSUCReturnRouteRequestTransmitReport( + host, + { + callbackId: msg.callbackId, + transmitStatus: ack + ? TransmitStatus.OK + : TransmitStatus.NoAck, + }, + ); + + await controller.sendToHost(cb.serialize()); + } + return true; + } + }, + }; + controller.defineBehavior(handleAssignSUCReturnRoute); + + // Incorrectly respond to DeleteSUCReturnRoute with a message with function type 0 + const handleDeleteSUCReturnRoute: MockControllerBehavior = { + async onHostMessage(host, controller, msg) { + if (msg instanceof DeleteSUCReturnRouteRequest) { + // Check if this command is legal right now + const state = controller.state.get( + MockControllerStateKeys.CommunicationState, + ) as MockControllerCommunicationState | undefined; + if ( + state != undefined + && state !== MockControllerCommunicationState.Idle + ) { + throw new Error( + "Received DeleteSUCReturnRouteRequest while not idle", + ); + } + + // Put the controller into sending state + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.Sending, + ); + + const expectCallback = msg.callbackId !== 0; + + // Send the command to the node + const node = controller.nodes.get(msg.getNodeId()!)!; + const command = new ZWaveProtocolCCAssignSUCReturnRoute( + host, + { + nodeId: node.id, + destinationNodeId: controller.host.ownNodeId, + repeaters: [], // don't care + routeIndex: 0, // don't care + destinationSpeed: ZWaveDataRate["100k"], + destinationWakeUp: WakeUpTime.None, + }, + ); + const frame = createMockZWaveRequestFrame(command, { + ackRequested: expectCallback, + }); + const ackPromise = controller.sendToNode(node, frame); + + // Notify the host that the message was sent + const res = new DeleteSUCReturnRouteResponse(host, { + wasExecuted: true, + }); + await controller.sendToHost(res.serialize()); + + let ack = false; + if (expectCallback) { + // Put the controller into waiting state + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.WaitingForNode, + ); + + // Wait for the ACK and notify the host + try { + const ackResult = await ackPromise; + ack = !!ackResult?.ack; + } catch { + // No response + } + } + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.Idle, + ); + + if (expectCallback) { + const cb = + new DeleteSUCReturnRouteRequestTransmitReport( + host, + { + callbackId: msg.callbackId, + transmitStatus: ack + ? TransmitStatus.OK + : TransmitStatus.NoAck, + }, + ); + // @ts-expect-error 0 is not a valid function type + cb.functionType = 0; + + await controller.sendToHost(cb.serialize()); + } + return true; + } + }, + }; + controller.defineBehavior(handleDeleteSUCReturnRoute); + }, + testBody: async (t, driver, node, mockController, mockNode) => { + mockController.clearReceivedHostMessages(); + driver.options.timeouts.sendDataCallback = 1000; + let result = await driver.controller.assignSUCReturnRoutes( + node.id, + ); + t.false(result); + + result = await driver.controller.deleteSUCReturnRoutes( + node.id, + ); + t.false(result); + }, + }, +); diff --git a/packages/zwave-js/src/lib/test/driver/controllerJammed.test.ts b/packages/zwave-js/src/lib/test/driver/controllerJammed.test.ts index 17c4707ccd5e..8f9b08aaf7be 100644 --- a/packages/zwave-js/src/lib/test/driver/controllerJammed.test.ts +++ b/packages/zwave-js/src/lib/test/driver/controllerJammed.test.ts @@ -1,4 +1,10 @@ -import { ControllerStatus, NodeStatus, TransmitStatus } from "@zwave-js/core"; +import { + ControllerStatus, + NodeStatus, + TransmitStatus, + ZWaveErrorCodes, + assertZWaveError, +} from "@zwave-js/core"; import { type MockControllerBehavior } from "@zwave-js/testing"; import { wait } from "alcalzone-shared/async"; import sinon from "sinon"; @@ -13,6 +19,7 @@ import { SendDataResponse, } from "../../serialapi/transport/SendDataMessages"; import { integrationTest } from "../integrationTestSuite"; +import { integrationTest as integrationTestMulti } from "../integrationTestSuiteMulti"; let shouldFail = false; @@ -130,3 +137,119 @@ integrationTest("update the controller status and wait if TX status is Fail", { ]); }, }); + +integrationTestMulti( + "Prevent an infinite loop when the controller is unable to transmit a command to a specific node", + { + // debug: true, + // provisioningDirectory: path.join( + // __dirname, + // "__fixtures/supervision_binary_switch", + // ), + + additionalDriverOptions: { + testingHooks: { + skipNodeInterview: true, + }, + }, + + nodeCapabilities: [ + { + id: 2, + capabilities: { + isListening: true, + }, + }, + { + id: 3, + capabilities: { + isListening: true, + }, + }, + ], + + customSetup: async (driver, controller, mockNodes) => { + // Return a TX status of Fail when desired + const handleSendData: MockControllerBehavior = { + async onHostMessage(host, controller, msg) { + if (msg instanceof SendDataRequest) { + // Commands to node 3 work normally + if (msg.getNodeId() === 3) { + // Defer to the default behavior + return false; + } + + // Check if this command is legal right now + const state = controller.state.get( + MockControllerStateKeys.CommunicationState, + ) as MockControllerCommunicationState | undefined; + if ( + state != undefined + && state !== MockControllerCommunicationState.Idle + ) { + throw new Error( + "Received SendDataRequest while not idle", + ); + } + + // Put the controller into sending state + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.Sending, + ); + + // Notify the host that the message was sent + const res = new SendDataResponse(host, { + wasSent: true, + }); + await controller.sendToHost(res.serialize()); + + await wait(100); + + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.Idle, + ); + + const cb = new SendDataRequestTransmitReport(host, { + callbackId: msg.callbackId, + transmitStatus: TransmitStatus.Fail, + txReport: { + txTicks: 0, + routeSpeed: 0 as any, + routingAttempts: 0, + ackRSSI: 0, + }, + }); + await controller.sendToHost(cb.serialize()); + + return true; + } else if (msg instanceof SendDataAbort) { + // Put the controller into idle state + controller.state.set( + MockControllerStateKeys.CommunicationState, + MockControllerCommunicationState.Idle, + ); + } + }, + }; + controller.defineBehavior(handleSendData); + }, + testBody: async (t, driver, nodes, mockController, mockNodes) => { + const [node2, node3] = nodes; + node2.markAsAlive(); + node3.markAsAlive(); + + driver.options.timeouts.retryJammed = 100; + + t.true(await node3.ping()); + await assertZWaveError( + t, + () => node2.commandClasses.Basic.set(99), + { + errorCode: ZWaveErrorCodes.Controller_MessageDropped, + }, + ); + }, + }, +); diff --git a/packages/zwave-js/src/lib/test/driver/sendDataMissingCallbackAbort.test.ts b/packages/zwave-js/src/lib/test/driver/sendDataMissingCallbackAbort.test.ts index 7c12f8382a0b..f4e453f33eb7 100644 --- a/packages/zwave-js/src/lib/test/driver/sendDataMissingCallbackAbort.test.ts +++ b/packages/zwave-js/src/lib/test/driver/sendDataMissingCallbackAbort.test.ts @@ -9,6 +9,10 @@ import { import { ZWaveErrorCodes, assertZWaveError } from "@zwave-js/core"; import Sinon from "sinon"; import { SoftResetRequest } from "../../serialapi/misc/SoftResetRequest"; +import { + RequestNodeInfoRequest, + RequestNodeInfoResponse, +} from "../../serialapi/network-mgmt/RequestNodeInfoMessages"; import { SendDataAbort, SendDataRequest, @@ -318,3 +322,44 @@ integrationTest( }, }, ); + +integrationTest( + "Missing callback recovery only kicks in for SendData commands", + { + // debug: true, + + additionalDriverOptions: { + testingHooks: { + skipNodeInterview: true, + }, + }, + + customSetup: async (driver, mockController, mockNode) => { + // This is almost a 1:1 copy of the default behavior, except that the callback never gets sent + const handleBrokenRequestNodeInfo: MockControllerBehavior = { + async onHostMessage(host, controller, msg) { + if (msg instanceof RequestNodeInfoRequest) { + // Notify the host that the message was sent + const res = new RequestNodeInfoResponse(host, { + wasSent: true, + }); + await controller.sendToHost(res.serialize()); + + // And never send a callback + return true; + } + }, + }; + mockController.defineBehavior(handleBrokenRequestNodeInfo); + }, + testBody: async (t, driver, node, mockController, mockNode) => { + // Circumvent the options validation so the test doesn't take forever + driver.options.timeouts.sendDataCallback = 1500; + + await assertZWaveError(t, () => node.requestNodeInfo(), { + errorCode: ZWaveErrorCodes.Controller_Timeout, + context: "callback", + }); + }, + }, +); diff --git a/packages/zwave-js/src/lib/test/integrationTestSuite.ts b/packages/zwave-js/src/lib/test/integrationTestSuite.ts index fe733f0e1cd8..95a8f6afc715 100644 --- a/packages/zwave-js/src/lib/test/integrationTestSuite.ts +++ b/packages/zwave-js/src/lib/test/integrationTestSuite.ts @@ -1,7 +1,8 @@ import type { MockPortBinding } from "@zwave-js/serial/mock"; -import { type DeepPartial, noop } from "@zwave-js/shared"; +import { noop } from "@zwave-js/shared"; import { type MockController, + type MockControllerOptions, type MockNode, type MockNodeOptions, } from "@zwave-js/testing"; @@ -12,7 +13,7 @@ import crypto from "node:crypto"; import os from "node:os"; import path from "node:path"; import type { Driver } from "../driver/Driver"; -import type { ZWaveOptions } from "../driver/ZWaveOptions"; +import type { PartialZWaveOptions } from "../driver/ZWaveOptions"; import type { ZWaveNode } from "../node/Node"; import { prepareDriver, prepareMocks } from "./integrationTestSuiteShared"; @@ -23,6 +24,7 @@ interface IntegrationTestOptions { provisioningDirectory?: string; /** Whether the recorded messages and frames should be cleared before executing the test body. Default: true. */ clearMessageStatsBeforeTest?: boolean; + controllerCapabilities?: MockControllerOptions["capabilities"]; nodeCapabilities?: MockNodeOptions["capabilities"]; customSetup?: ( driver: Driver, @@ -36,7 +38,7 @@ interface IntegrationTestOptions { mockController: MockController, mockNode: MockNode, ) => Promise; - additionalDriverOptions?: DeepPartial; + additionalDriverOptions?: PartialZWaveOptions; } export interface IntegrationTestFn { @@ -55,6 +57,7 @@ function suite( modifier?: "only" | "skip", ) { const { + controllerCapabilities, nodeCapabilities, customSetup, testBody, @@ -98,12 +101,18 @@ function suite( ({ mockController, mockNodes: [mockNode], - } = prepareMocks(mockPort, undefined, [ + } = prepareMocks( + mockPort, { - id: 2, - capabilities: nodeCapabilities, + capabilities: controllerCapabilities, }, - ])); + [ + { + id: 2, + capabilities: nodeCapabilities, + }, + ], + )); if (customSetup) { await customSetup(driver, mockController, mockNode); diff --git a/packages/zwave-js/src/lib/test/integrationTestSuiteMulti.ts b/packages/zwave-js/src/lib/test/integrationTestSuiteMulti.ts index ef8e05b6702a..f71fa4683738 100644 --- a/packages/zwave-js/src/lib/test/integrationTestSuiteMulti.ts +++ b/packages/zwave-js/src/lib/test/integrationTestSuiteMulti.ts @@ -2,6 +2,7 @@ import type { MockPortBinding } from "@zwave-js/serial/mock"; import { noop } from "@zwave-js/shared"; import { type MockController, + type MockControllerOptions, type MockNode, type MockNodeOptions, } from "@zwave-js/testing"; @@ -12,7 +13,7 @@ import crypto from "node:crypto"; import os from "node:os"; import path from "node:path"; import type { Driver } from "../driver/Driver"; -import type { ZWaveOptions } from "../driver/ZWaveOptions"; +import type { PartialZWaveOptions } from "../driver/ZWaveOptions"; import type { ZWaveNode } from "../node/Node"; import { prepareDriver, prepareMocks } from "./integrationTestSuiteShared"; @@ -23,6 +24,7 @@ interface IntegrationTestOptions { provisioningDirectory?: string; /** Whether the recorded messages and frames should be cleared before executing the test body. Default: true. */ clearMessageStatsBeforeTest?: boolean; + controllerCapabilities?: MockControllerOptions["capabilities"]; nodeCapabilities?: Pick[]; customSetup?: ( driver: Driver, @@ -36,7 +38,7 @@ interface IntegrationTestOptions { mockController: MockController, mockNodes: MockNode[], ) => Promise; - additionalDriverOptions?: Partial; + additionalDriverOptions?: PartialZWaveOptions; } export interface IntegrationTestFn { @@ -55,6 +57,7 @@ function suite( modifier?: "only" | "skip", ) { const { + controllerCapabilities, nodeCapabilities, customSetup, testBody, @@ -96,7 +99,9 @@ function suite( )); ({ mockController, mockNodes } = prepareMocks( mockPort, - undefined, + { + capabilities: controllerCapabilities, + }, // TODO: This isn't ideal as it requires us to provide the // node capabilities in addition to the provisioning directory nodeCapabilities, diff --git a/packages/zwave-js/src/lib/test/integrationTestSuiteShared.ts b/packages/zwave-js/src/lib/test/integrationTestSuiteShared.ts index cd73b010bf28..d7c453d9e692 100644 --- a/packages/zwave-js/src/lib/test/integrationTestSuiteShared.ts +++ b/packages/zwave-js/src/lib/test/integrationTestSuiteShared.ts @@ -1,5 +1,4 @@ import { type MockPortBinding } from "@zwave-js/serial/mock"; -import { type DeepPartial } from "@zwave-js/shared"; import { MockController, type MockControllerOptions, @@ -15,12 +14,12 @@ import { type CreateAndStartDriverWithMockPortResult, createAndStartDriverWithMockPort, } from "../driver/DriverMock"; -import { type ZWaveOptions } from "../driver/ZWaveOptions"; +import { type PartialZWaveOptions } from "../driver/ZWaveOptions"; export function prepareDriver( cacheDir: string = path.join(__dirname, "cache"), logToFile: boolean = false, - additionalOptions: DeepPartial = {}, + additionalOptions: PartialZWaveOptions = {}, ): Promise { return createAndStartDriverWithMockPort({ ...additionalOptions, diff --git a/yarn.lock b/yarn.lock index 3de9f418aca7..a246bfa74ecf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -838,27 +838,27 @@ __metadata: languageName: node linkType: hard -"@microsoft/api-extractor-model@npm:7.27.6": - version: 7.27.6 - resolution: "@microsoft/api-extractor-model@npm:7.27.6" +"@microsoft/api-extractor-model@npm:7.28.2": + version: 7.28.2 + resolution: "@microsoft/api-extractor-model@npm:7.28.2" dependencies: "@microsoft/tsdoc": 0.14.2 "@microsoft/tsdoc-config": ~0.16.1 - "@rushstack/node-core-library": 3.59.7 - checksum: 7867feaf3a0e5accfcce3a77681248a319952a266cffc644e4f8f7df1c9e1d55adb5124df901e8cca594bb3e12d361d1fcb2bffbdbb4b20fe3113928f6535975 + "@rushstack/node-core-library": 3.61.0 + checksum: 0eb1cb511414813eeb890778af7dc57e5adcd078ba040a91a736a63964b306a1d31f8b97a76286884432a7884808960a16160d49720c46e23472124f035b9023 languageName: node linkType: hard -"@microsoft/api-extractor@npm:^7.36.4": - version: 7.36.4 - resolution: "@microsoft/api-extractor@npm:7.36.4" +"@microsoft/api-extractor@npm:^7.37.3": + version: 7.37.3 + resolution: "@microsoft/api-extractor@npm:7.37.3" dependencies: - "@microsoft/api-extractor-model": 7.27.6 + "@microsoft/api-extractor-model": 7.28.2 "@microsoft/tsdoc": 0.14.2 "@microsoft/tsdoc-config": ~0.16.1 - "@rushstack/node-core-library": 3.59.7 - "@rushstack/rig-package": 0.4.1 - "@rushstack/ts-command-line": 4.15.2 + "@rushstack/node-core-library": 3.61.0 + "@rushstack/rig-package": 0.5.1 + "@rushstack/ts-command-line": 4.16.1 colors: ~1.2.1 lodash: ~4.17.15 resolve: ~1.22.1 @@ -867,7 +867,7 @@ __metadata: typescript: ~5.0.4 bin: api-extractor: bin/api-extractor - checksum: 92559325cf2407fa27cb9675772956511fa35005f295cdb4dc47abd7ef9c77ba61b0f684c2e952301a76dd2cfa9e398840c8f3d9117d621300e12b0ecfbf8147 + checksum: 6d17df06316508ddbfa0cc2ec6fbba7d63e498cb7d9816be2d993c98507645254ae852d1b536197586e4023777f510242a185f3b1d341d267962c5aad15b2740 languageName: node linkType: hard @@ -1111,9 +1111,9 @@ __metadata: languageName: node linkType: hard -"@rushstack/node-core-library@npm:3.59.7": - version: 3.59.7 - resolution: "@rushstack/node-core-library@npm:3.59.7" +"@rushstack/node-core-library@npm:3.61.0": + version: 3.61.0 + resolution: "@rushstack/node-core-library@npm:3.61.0" dependencies: colors: ~1.2.1 fs-extra: ~7.0.1 @@ -1127,29 +1127,29 @@ __metadata: peerDependenciesMeta: "@types/node": optional: true - checksum: 57819d62fd662a6cf3306bf7d39c11204e094a2d5c2210639c2ac5baee58c183c02023203963cd0484a5623fd9f5dea7a223df843fb52b46a18508e6118cdc19 + checksum: a6f790cd521ca5b0b10ee918d8352c7dd7a0b2457aaf6a4f37d8f7bedee680d7d0126476f5ee5147952e08b11dea37926acb45f7432cd16c828690d3b9bfd34b languageName: node linkType: hard -"@rushstack/rig-package@npm:0.4.1": - version: 0.4.1 - resolution: "@rushstack/rig-package@npm:0.4.1" +"@rushstack/rig-package@npm:0.5.1": + version: 0.5.1 + resolution: "@rushstack/rig-package@npm:0.5.1" dependencies: resolve: ~1.22.1 strip-json-comments: ~3.1.1 - checksum: 68c5ec6c446c35939fca0444fa48e5beda736e3a5816e8b44d83df6ba8b9a2caf0ceddbdc866cd8ad3b523e42877cf6ecd467bc7839e3d618a9bb1c4b3e0b5a5 + checksum: 2d45af13568590cc7f6396b7a075fa27f9676bc04deb39a3867a6f912d43cad45481d8d44482ff6a49c7bd9d428499c2701032602a8241740fc10b19c45dec0f languageName: node linkType: hard -"@rushstack/ts-command-line@npm:4.15.2": - version: 4.15.2 - resolution: "@rushstack/ts-command-line@npm:4.15.2" +"@rushstack/ts-command-line@npm:4.16.1": + version: 4.16.1 + resolution: "@rushstack/ts-command-line@npm:4.16.1" dependencies: "@types/argparse": 1.0.38 argparse: ~1.0.9 colors: ~1.2.1 string-argv: ~0.3.1 - checksum: c80dcfc99630ee51c6654c58ff41f69a3bd89c38e41d9871692bc73ee3c938ced79f8b75e182e492cafb2f6ddeb0628606856af494a0259ff6fac5b248996bed + checksum: f8309a274bdc9d9c87258f5f56b3905b8467319c87cdc757d98bf582b7c4a6925b389bce0ce4125a625a402335f195668dc55547b754f0e9a5d0014154c32d2d languageName: node linkType: hard @@ -1177,14 +1177,7 @@ __metadata: languageName: node linkType: hard -"@serialport/bindings-interface@npm:*, @serialport/bindings-interface@npm:^1.2.1": - version: 1.2.1 - resolution: "@serialport/bindings-interface@npm:1.2.1" - checksum: 9d3b8231ecbcf67cc85eb0d4f1db617de3353d71f6ab8124c68ec7009a18ef47bdf9c226e8174e393120def37941a7f61a997bd806370a737c63c5e6ec13a0de - languageName: node - linkType: hard - -"@serialport/bindings-interface@npm:1.2.2": +"@serialport/bindings-interface@npm:*, @serialport/bindings-interface@npm:1.2.2, @serialport/bindings-interface@npm:^1.2.1": version: 1.2.2 resolution: "@serialport/bindings-interface@npm:1.2.2" checksum: 66154b9abe8b3cfc466431f2197ca1d6ed832e121bab15aed9ffcbda5c1d3bc0d11be3cb5c343d3cfc2b0ea69a393900c5be6a77fe9ec646e3004bf6d6a3756d @@ -1927,7 +1920,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/cc@workspace:packages/cc" dependencies: - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/fs-extra": ^11.0.1 "@types/node": ^18.17.14 "@zwave-js/core": "workspace:*" @@ -1951,7 +1944,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/config@workspace:packages/config" dependencies: - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/fs-extra": ^11.0.1 "@types/js-levenshtein": ^1.1.1 "@types/json-logic-js": ^2.0.2 @@ -1982,7 +1975,7 @@ __metadata: ts-pegjs: ^0.3.1 typescript: 5.2.2 winston: ^3.10.0 - xml2js: ^0.5.0 + xml2js: ^0.6.2 yargs: ^17.7.2 languageName: unknown linkType: soft @@ -1992,7 +1985,7 @@ __metadata: resolution: "@zwave-js/core@workspace:packages/core" dependencies: "@alcalzone/jsonl-db": ^3.1.0 - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/node": ^18.17.14 "@types/sinon": ^10.0.16 "@types/triple-beam": ^1.3.2 @@ -2063,7 +2056,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/host@workspace:packages/host" dependencies: - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/node": ^18.17.14 "@zwave-js/config": "workspace:*" "@zwave-js/core": "workspace:*" @@ -2111,7 +2104,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/nvmedit@workspace:packages/nvmedit" dependencies: - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/fs-extra": ^11.0.1 "@types/node": ^18.17.14 "@types/semver": ^7.5.1 @@ -2149,7 +2142,7 @@ __metadata: "@dprint/json": ^0.17.4 "@dprint/markdown": ^0.16.0 "@dprint/typescript": ^0.87.1 - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@monorepo-utils/workspaces-to-typescript-project-references": ^2.10.2 "@tsconfig/node18": ^18.2.1 "@types/fs-extra": ^11.0.1 @@ -2205,7 +2198,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/serial@workspace:packages/serial" dependencies: - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@serialport/binding-mock": ^10.2.2 "@serialport/bindings-interface": "*" "@serialport/stream": ^12.0.0 @@ -2231,7 +2224,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/shared@workspace:packages/shared" dependencies: - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/fs-extra": ^11.0.1 "@types/node": ^18.17.14 "@types/sinon": ^10.0.16 @@ -2250,7 +2243,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/testing@workspace:packages/testing" dependencies: - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/node": ^18.17.14 "@types/triple-beam": ^1.3.2 "@zwave-js/core": "workspace:*" @@ -9073,13 +9066,13 @@ __metadata: languageName: node linkType: hard -"xml2js@npm:^0.5.0": - version: 0.5.0 - resolution: "xml2js@npm:0.5.0" +"xml2js@npm:^0.6.2": + version: 0.6.2 + resolution: "xml2js@npm:0.6.2" dependencies: sax: ">=0.6.0" xmlbuilder: ~11.0.0 - checksum: 1aa71d62e5bc2d89138e3929b9ea46459157727759cbc62ef99484b778641c0cd21fb637696c052d901a22f82d092a3e740a16b4ce218e81ac59b933535124ea + checksum: 458a83806193008edff44562c0bdb982801d61ee7867ae58fd35fab781e69e17f40dfeb8fc05391a4648c9c54012066d3955fe5d993ffbe4dc63399023f32ac2 languageName: node linkType: hard @@ -9267,7 +9260,7 @@ __metadata: dependencies: "@alcalzone/jsonl-db": ^3.1.0 "@alcalzone/pak": ^0.9.0 - "@microsoft/api-extractor": ^7.36.4 + "@microsoft/api-extractor": ^7.37.3 "@types/fs-extra": ^11.0.1 "@types/node": ^18.17.14 "@types/proper-lockfile": ^4.1.2