From 87c21c2e95de81176e133a8cc536c741faed5f6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:17:02 +0200 Subject: [PATCH 01/22] build(deps): bump glob-parent from 5.1.1 to 5.1.2 (#2859) Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.1 to 5.1.2. - [Release notes](https://github.com/gulpjs/glob-parent/releases) - [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md) - [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.1...v5.1.2) --- updated-dependencies: - dependency-name: glob-parent dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 603f158b9cf0..8d164e18e7e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5511,14 +5511,7 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -glob-parent@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob-parent@^5.1.1, glob-parent@^5.1.2: +glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== From 8d1a99b233f903ea260bef2b9b46bd1912fee6e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:17:31 +0200 Subject: [PATCH 02/22] build(deps): bump handlebars from 4.7.6 to 4.7.7 (#2861) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7. - [Release notes](https://github.com/wycats/handlebars.js/releases) - [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) - [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7) --- updated-dependencies: - dependency-name: handlebars dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8d164e18e7e0..6f6e2ff7c7b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5609,9 +5609,9 @@ growly@^1.3.0: integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= handlebars@^4.7.6: - version "4.7.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" - integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== dependencies: minimist "^1.2.5" neo-async "^2.6.0" @@ -10000,9 +10000,9 @@ typescript@^4.3.2: integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== uglify-js@^3.1.4: - version "3.12.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.4.tgz#93de48bb76bb3ec0fc36563f871ba46e2ee5c7ee" - integrity sha512-L5i5jg/SHkEqzN18gQMTWsZk3KelRsfD1wUVNqtq0kzqWQqcJjyL8yc1o8hJgRrWqrAl2mUFbhfznEIoi7zi2A== + version "3.13.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.9.tgz#4d8d21dcd497f29cfd8e9378b9df123ad025999b" + integrity sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g== uid-number@0.0.6: version "0.0.6" From 721547e07361a20eb686908e09d04283cd96d468 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:17:42 +0200 Subject: [PATCH 03/22] build(deps): bump ws from 7.4.2 to 7.4.6 (#2860) Bumps [ws](https://github.com/websockets/ws) from 7.4.2 to 7.4.6. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.4.2...7.4.6) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6f6e2ff7c7b7..3fc516f70339 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10380,9 +10380,9 @@ write-pkg@^4.0.0: write-json-file "^3.2.0" ws@^7.2.3: - version "7.4.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd" - integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA== + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== xml-name-validator@^3.0.0: version "3.0.0" From 609a4a6cb59db1aefa7c14df8bd35d2556c92370 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:18:04 +0200 Subject: [PATCH 04/22] build(deps): bump lodash from 4.17.20 to 4.17.21 (#2862) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) --- updated-dependencies: - dependency-name: lodash dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3fc516f70339..2e485f4dd879 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7181,12 +7181,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -lodash@^4.17.21: +lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== From 1867e5ae2c302e6d111bc064d76e8ae7366babaf Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Tue, 15 Jun 2021 23:56:56 +0200 Subject: [PATCH 05/22] chore: replace moment with dayjs, moment-timezone with a binary search (#2866) --- packages/core/package.json | 3 +- packages/core/src/util/date.test.ts | 31 ++++++ packages/core/src/util/date.ts | 100 +++++++++++------- .../zwave-js/src/lib/commandclass/TimeCC.ts | 3 +- yarn.lock | 14 ++- 5 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 packages/core/src/util/date.test.ts diff --git a/packages/core/package.json b/packages/core/package.json index 7982bd547aae..e3e8d7068bd6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,8 +35,7 @@ "@zwave-js/shared": "7.7.3", "alcalzone-shared": "^3.0.4", "ansi-colors": "^4.1.1", - "moment": "^2.29.0", - "moment-timezone": "^0.5.33", + "dayjs": "^1.10.5", "nrf-intel-hex": "^1.3.0", "winston": "^3.3.3", "winston-daily-rotate-file": "^4.5.5" diff --git a/packages/core/src/util/date.test.ts b/packages/core/src/util/date.test.ts new file mode 100644 index 000000000000..d244fb524eef --- /dev/null +++ b/packages/core/src/util/date.test.ts @@ -0,0 +1,31 @@ +import { formatDate, getDSTInfo } from "./date"; + +describe("formatDate()", () => { + it("should work correctly", () => { + expect(formatDate(new Date(2021, 5, 15), "YYYY-MM-DD")).toEqual( + "2021-06-15", + ); + }); +}); + +describe("getDSTInfo()", () => { + if (Intl.DateTimeFormat().resolvedOptions().timeZone === "Europe/Berlin") { + it("should work correctly", () => { + let dstInfo = getDSTInfo(new Date("2021-05-15T01:00:00.000Z")); + expect(dstInfo).toEqual({ + dstOffset: 120, + endDate: new Date("2021-10-31T01:00:00.000Z"), + standardOffset: 60, + startDate: new Date("2021-03-28T01:00:00.000Z"), + }); + + dstInfo = getDSTInfo(new Date("2020-11-12T01:00:00.000Z")); + expect(dstInfo).toEqual({ + dstOffset: 120, + endDate: new Date("2020-10-25T01:00:00.000Z"), + standardOffset: 60, + startDate: new Date("2021-03-28T01:00:00.000Z"), + }); + }); + } +}); diff --git a/packages/core/src/util/date.ts b/packages/core/src/util/date.ts index 0d5d4fc056cc..33964f5aab01 100644 --- a/packages/core/src/util/date.ts +++ b/packages/core/src/util/date.ts @@ -1,5 +1,4 @@ -import moment from "moment"; -import { tz } from "moment-timezone"; +import dayjs from "dayjs"; export interface DSTInfo { startDate: Date; @@ -26,53 +25,76 @@ export function getDefaultDSTInfo(defaultOffset?: number): DSTInfo { }; } -/** Returns the current system's daylight savings time information if possible */ -export function getDSTInfo(): DSTInfo | undefined { - const thisYear = new Date().getUTCFullYear(); - // find out which timezone we're in - const zoneName = tz.guess(); - const zone = tz.zone(zoneName); - if (!zone) return; +/** + * Finds the first date on which the given timezone offset is in effect. + * date1 must be the smaller one of the two dates + */ +function findSwitchDate( + date1: Date, + date2: Date, + offset: number, +): Date | undefined { + const stepSize = 60000; // 1 minute + + function middleDate(date1: Date, date2: Date): Date { + const middleTime = + Math.floor((date1.getTime() + date2.getTime()) / 2 / stepSize) * + stepSize; + return new Date(middleTime); + } - // moment-timezone stores the end dates of each timespan in zone.untils - // iterate through them to find this year's dates - const indizes: number[] = []; - const dates: Date[] = []; - const offsets: number[] = []; - for (let i = 0; i < zone.untils.length; i++) { - const date = new Date(zone.untils[i]); - if (date.getUTCFullYear() === thisYear) { - indizes.push(i); - dates.push(date); - // Javascript has the offsets inverted, we use the normal interpretation - offsets.push(-zone.offsets[i]); + while (date1 < date2) { + const mid = middleDate(date1, date2); + if (mid.getTimezoneOffset() === offset) { + date2 = mid; + } else { + date1 = new Date(mid.getTime() + stepSize); } } - // We can only work with exactly two dates -> start and end of DST - switch (indizes.length) { - case 1: - // if we have exactly 1 index, we use that offset information to construct the fallback info - return getDefaultDSTInfo(offsets[0]); - case 2: - // if we have exactly 2 indizes, we know there's a start and end date - break; // continue further down - default: - // otherwise we cannot construct dst info - return undefined; + + if (date1.getTimezoneOffset() !== offset) return undefined; + return date1; +} + +/** Returns the current system's daylight savings time information if possible */ +export function getDSTInfo(now: Date = new Date()): DSTInfo { + const halfAYearAgo = dayjs(now).subtract(6, "months").toDate(); + const inAHalfYear = dayjs(now).add(6, "months").toDate(); + if ( + now.getTimezoneOffset() === halfAYearAgo.getTimezoneOffset() || + now.getTimezoneOffset() === inAHalfYear.getTimezoneOffset() + ) { + // There is no DST in this timezone + return getDefaultDSTInfo(); } + + // Javascript has the offsets inverted, we use the normal interpretation + const offsets = [ + -now.getTimezoneOffset(), + -inAHalfYear.getTimezoneOffset(), + ]; + const dates = [ + findSwitchDate(halfAYearAgo, now, -offsets[0]), + findSwitchDate(now, inAHalfYear, -offsets[1]), + ]; + if (dates[0] == undefined || dates[1] == undefined) { + // This shouldn't happen, but better be sure + return getDefaultDSTInfo(); + } + if (offsets[0] > offsets[1]) { - // index 0 is end of DST, index 1 is start + // DST is always the higher offset return { - endDate: dates[0], - startDate: dates[1], + startDate: dates[0], + endDate: dates[1], dstOffset: offsets[0], standardOffset: offsets[1], }; } else { - // index 0 is start of DST, index 1 is end return { - startDate: dates[0], - endDate: dates[1], + // We don't have DST now, this is the next start date + startDate: dates[1], + endDate: dates[0], dstOffset: offsets[1], standardOffset: offsets[0], }; @@ -93,5 +115,5 @@ export const timespan = Object.freeze({ }); export function formatDate(date: Date, format: string): string { - return moment(date).format(format); + return dayjs(date).format(format); } diff --git a/packages/zwave-js/src/lib/commandclass/TimeCC.ts b/packages/zwave-js/src/lib/commandclass/TimeCC.ts index 5623bf201381..188a058f116c 100644 --- a/packages/zwave-js/src/lib/commandclass/TimeCC.ts +++ b/packages/zwave-js/src/lib/commandclass/TimeCC.ts @@ -2,7 +2,6 @@ import { CommandClasses, DSTInfo, formatDate, - getDefaultDSTInfo, getDSTInfo, Maybe, MessageOrCCLogEntry, @@ -152,7 +151,7 @@ export class TimeCC extends CommandClass { direction: "outbound", }); // Set the correct timezone on this node - const timezone = getDSTInfo() || getDefaultDSTInfo(); + const timezone = getDSTInfo(); await api.setTimezone(timezone); } diff --git a/yarn.lock b/yarn.lock index 2e485f4dd879..62f6592cc818 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4386,6 +4386,11 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986" + integrity sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g== + debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -7559,14 +7564,7 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment-timezone@^0.5.33: - version "0.5.33" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" - integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.9.0", moment@^2.11.2, moment@^2.29.0: +moment@^2.11.2: version "2.29.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== From 5e41e3222c5fbea51c2acf6a7c839fe2b1f59a0c Mon Sep 17 00:00:00 2001 From: Fredrik Andersson Date: Tue, 15 Jun 2021 23:57:04 +0200 Subject: [PATCH 06/22] docs: fix links to config-files documents (#2855) Co-authored-by: AlCalzone --- CONTRIBUTING.md | 2 +- docs/config-files/contributing-files.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae52e9216450..a31654909e45 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Open an issue with the **feature request template** and fill it out. For complic ## Do you want to contribute device configuration files? -Thanks, just go ahead and open a PR! Please make sure to follow the [[Style guide](config-files/style-guide.md) and use the [available templates](config-files/using-templates.md), while following the other documentation on [conditional parameters and settings](config-files/conditional-settings.md) and [partial parameters](config-files/partial-parameters.md). +Thanks, just go ahead and open a PR! Please make sure to follow our [standards and contribution guidelines for configuration files](https://zwave-js.github.io/node-zwave-js/#/config-files/contributing-files). ## Do you want to help out with a PR? diff --git a/docs/config-files/contributing-files.md b/docs/config-files/contributing-files.md index a1fd136303d9..c10db39c174b 100644 --- a/docs/config-files/contributing-files.md +++ b/docs/config-files/contributing-files.md @@ -12,6 +12,14 @@ We provide scripts to allow you to import a pre-existing device file from the Op Additional information is available at: [Importing files from other sources](config-files/importing-from-others.md) +## Style Guide + +As mentioned, we have standards and specific expectations for how things will be described and options presented. Labels and descriptions from other projects will likely need to be amended. Descriptions provided in manufacturer documentation will also likely need to be shortened and simplified. To aid you, we have created a Style Guide. Converting existing files is a work-in-progress, so please do not be offended if you are asked to fix something despite having seen it used elsewhere. + +> [!NOTE] The style guide is mandatory. Configuration files that don't follow it will not be accepted. + +Additional information is available at: [Style guide](config-files/style-guide.md) + ## Partial Parameters Some devices use a single parameter number to configure several, sometimes unrelated options. We present these complex bitmask-type parameters as individual parameters. Doing so requires converting such parameters to what we call [partial parameters](config-files/file-format.md#partial-parameters). @@ -32,14 +40,6 @@ In order to ensure consistency among devices and to ease future improvements, we Additional information is available at: [Using templates](config-files/using-templates.md) -## Style Guide - -As mentioned, we have standards and specific expectations for how things will be described and options presented. Labels and descriptions from other projects will likely need to be amended. Descriptions provided in manufacturer documentation will also likely need to be shortened and simplified. To aid you, we have created a Style Guide. Converting existing files is a work-in-progress, so please do not be offended if you are asked to fix something despite having seen it used elsewhere. - -> [!NOTE] The style guide is mandatory. Configuration files that don't follow it will not be accepted. - -Additional information is available at: [Style guide](config-files/style-guide.md) - ## Process to Submit Device Files In order to get your configuration file included in this library: From f43feb6003159537dec4776164aee20c4550fb38 Mon Sep 17 00:00:00 2001 From: kpine Date: Wed, 16 Jun 2021 01:29:32 -0700 Subject: [PATCH 07/22] docs: fix link to troubleshooting documentation (#2867) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a31654909e45..90b4064b161e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ We're happy that you're considering helping us out. Although the goal of this pr ## Do you have a problem? Something not working? Configuration missing? -Please read the [troubleshooting](https://zwave-js.github.io/node-zwave-js/#/development/troubleshooting) section of the documentation. Your problem might already be answered there. +Please read the [troubleshooting](https://zwave-js.github.io/node-zwave-js/#/troubleshooting/index) section of the documentation. Your problem might already be answered there. If not, consider opening an issue. Please **use the issue templates** and fill them out as best as you can. ## Do you want to propose a new feature? From 82d564c9ecec59cd7c38b4d86e2bcc02aa6bb906 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Wed, 16 Jun 2021 10:29:43 +0200 Subject: [PATCH 08/22] ci: test on Node 16, run maintenance workflows on Node 14 (#2870) --- .../workflows-disabled/approve-workflow-runs.yml | 2 +- .github/workflows/cc-bot.yml | 2 +- .github/workflows/generate-docs.yml | 2 +- .github/workflows/nightly-config-publish.yml | 2 +- .github/workflows/test-and-release.yml | 12 ++++++------ .github/workflows/to-log-entry-overview.yml | 2 +- .github/workflows/zwave-js-bot_comment.yml | 15 +++++++++------ 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows-disabled/approve-workflow-runs.yml b/.github/workflows-disabled/approve-workflow-runs.yml index bea193189ad8..ebf24d608f4f 100644 --- a/.github/workflows-disabled/approve-workflow-runs.yml +++ b/.github/workflows-disabled/approve-workflow-runs.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code diff --git a/.github/workflows/cc-bot.yml b/.github/workflows/cc-bot.yml index 05037d53d18a..f3ef6fa5fd09 100644 --- a/.github/workflows/cc-bot.yml +++ b/.github/workflows/cc-bot.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code diff --git a/.github/workflows/generate-docs.yml b/.github/workflows/generate-docs.yml index 03db4b42d02b..d2138810bd40 100644 --- a/.github/workflows/generate-docs.yml +++ b/.github/workflows/generate-docs.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS outputs: CHANGES: ${{ steps.changes.outputs.CHANGES }} diff --git a/.github/workflows/nightly-config-publish.yml b/.github/workflows/nightly-config-publish.yml index 96ee23ec24ec..d66fa702d269 100644 --- a/.github/workflows/nightly-config-publish.yml +++ b/.github/workflows/nightly-config-publish.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index 3820e6380f11..89f568ae1766 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code @@ -142,7 +142,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [10.x, 12.x, 14.x, 16.x] os: [ubuntu-latest] steps: @@ -188,7 +188,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code @@ -236,7 +236,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code @@ -292,7 +292,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code diff --git a/.github/workflows/to-log-entry-overview.yml b/.github/workflows/to-log-entry-overview.yml index 26e8d223d742..ee36d43430fd 100644 --- a/.github/workflows/to-log-entry-overview.yml +++ b/.github/workflows/to-log-entry-overview.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout code diff --git a/.github/workflows/zwave-js-bot_comment.yml b/.github/workflows/zwave-js-bot_comment.yml index f97120674eb0..880b42af603f 100644 --- a/.github/workflows/zwave-js-bot_comment.yml +++ b/.github/workflows/zwave-js-bot_comment.yml @@ -13,8 +13,11 @@ jobs: (github.event.comment.user.login == 'AlCalzone') runs-on: [ubuntu-latest] - steps: + strategy: + matrix: + node-version: [14.x] # This should be LTS + steps: - uses: actions/github-script@v3 with: github-token: ${{secrets.BOT_TOKEN}} @@ -82,7 +85,7 @@ jobs: runs-on: [ubuntu-latest] strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout master branch @@ -188,7 +191,7 @@ jobs: runs-on: [ubuntu-latest] strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout master branch @@ -286,7 +289,7 @@ jobs: runs-on: [ubuntu-latest] strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout master branch @@ -376,7 +379,7 @@ jobs: runs-on: [ubuntu-latest] strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout master branch @@ -498,7 +501,7 @@ jobs: runs-on: [ubuntu-latest] strategy: matrix: - node-version: [14.x] + node-version: [14.x] # This should be LTS steps: - name: Checkout master branch From 134d0aecd8935bc101004307bce75b729b21e654 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 16 Jun 2021 10:55:32 +0200 Subject: [PATCH 09/22] fix(driver): prevent simultaneous config updates (#2852) Co-authored-by: Dominic Griesel --- package.json | 1 + packages/zwave-js/package.json | 3 ++- .../zwave-js/src/lib/driver/UpdateConfig.ts | 27 +++++++++++++++++++ yarn.lock | 12 +++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a01ea4e3b99..51499dfc096b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/node": "^15.12.2", "@types/pegjs": "^0.10.2", "@types/prettier": "^2.2.3", + "@types/proper-lockfile": "^4.1.1", "@types/semver": "^7.3.6", "@types/serialport": "^8.0.1", "@types/triple-beam": "^1.3.2", diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index bd6ad4932f5e..2aad41a4e5a1 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -72,6 +72,7 @@ "ansi-colors": "^4.1.1", "axios": "^0.21.1", "fs-extra": "^9.0.1", + "proper-lockfile": "^4.1.2", "reflect-metadata": "^0.1.13", "serialport": "^9.1.0", "source-map-support": "^0.5.19", @@ -88,4 +89,4 @@ "lint:zwave": "yarn b lint", "ts": "node -r esbuild-register" } -} +} \ No newline at end of file diff --git a/packages/zwave-js/src/lib/driver/UpdateConfig.ts b/packages/zwave-js/src/lib/driver/UpdateConfig.ts index 9a3a700ee75c..ac1d5559f77f 100644 --- a/packages/zwave-js/src/lib/driver/UpdateConfig.ts +++ b/packages/zwave-js/src/lib/driver/UpdateConfig.ts @@ -2,6 +2,8 @@ import { detectPackageManager, PackageManager } from "@alcalzone/pak"; import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; import { isObject } from "alcalzone-shared/typeguards"; import axios from "axios"; +import * as path from "path"; +import * as lockfile from "proper-lockfile"; import * as semver from "semver"; /** @@ -56,6 +58,7 @@ export async function installConfigUpdate(newVersion: string): Promise { pak = await detectPackageManager({ cwd: __dirname, requireLockfile: false, + setCwdToPackageRoot: true, }); } catch { throw new ZWaveError( @@ -64,11 +67,35 @@ export async function installConfigUpdate(newVersion: string): Promise { ); } + const packageJsonPath = path.join(pak.cwd, "package.json"); + try { + await lockfile.lock(packageJsonPath, { + onCompromised: () => { + // do nothing + }, + }); + } catch { + throw new ZWaveError( + `Config update failed: Another installation is already in progress!`, + ZWaveErrorCodes.Config_Update_InstallFailed, + ); + } + // And install it const result = await pak.overrideDependencies({ "@zwave-js/config": newVersion, }); + + // Free the lock + try { + if (await lockfile.check(packageJsonPath)) + await lockfile.unlock(packageJsonPath); + } catch { + // whatever - just don't crash + } + if (result.success) return; + throw new ZWaveError( `Config update failed: Package manager exited with code ${result.exitCode} ${result.stderr}`, diff --git a/yarn.lock b/yarn.lock index 62f6592cc818..1f313aaea726 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,6 +2975,18 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0" integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA== +"@types/proper-lockfile@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz#99f026cbfdbe6305bdd454ffd5fefc1bd064beb1" + integrity sha512-HAjVfDa73pFgivViHyDu8HHHcds+W4MgOuZZAdyFJrHS8ngtCXmhl4hc2YXqSOwO6Bsa+iF2Sgxb2+gv874VOQ== + dependencies: + "@types/retry" "*" + +"@types/retry@*": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@types/semver@^7.3.6": version "7.3.6" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.6.tgz#e9831776f4512a7ba6da53e71c26e5fb67882d63" From 0a2267c31bad28d1a26281e400b7f8b5f78c4c2b Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 16 Jun 2021 15:46:17 +0200 Subject: [PATCH 10/22] chore: update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc8282e804d..d589142da0af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ +## __WORK IN PROGRESS__ +### Bugfixes +* Nodes are no longer sent to sleep while a scheduled verification poll is pending +* Simultaneous config DB updates are now prevented with a lockfile + +### Changes under the hood +* The build process now uses ESBuild instead of `ts-node` and `gulp` +* Replaced the `moment` package with the much smaller `Day.js` +* Removed the `moment-timezone` package +* Security updates to some dependencies +* Added Node.js 16 to the testing suite + ## 7.7.4 (2021-06-14) ### Bugfixes * Don't require V1 alarms to be supported to preserve legacy alarm values From 13dd7c31f4506b60c9676035d139b55c81068338 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 16 Jun 2021 15:46:52 +0200 Subject: [PATCH 11/22] chore: release v7.7.5 ### Bugfixes * Nodes are no longer sent to sleep while a scheduled verification poll is pending * Simultaneous config DB updates are now prevented with a lockfile ### Changes under the hood * The build process now uses ESBuild instead of `ts-node` and `gulp` * Replaced the `moment` package with the much smaller `Day.js` * Removed the `moment-timezone` package * Security updates to some dependencies * Added Node.js 16 to the testing suite --- CHANGELOG.md | 2 +- lerna.json | 2 +- packages/config/package.json | 4 ++-- packages/core/package.json | 2 +- packages/serial/package.json | 4 ++-- packages/zwave-js/package.json | 10 +++++----- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d589142da0af..f180b5b61b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ -## __WORK IN PROGRESS__ +## 7.7.5 (2021-06-16) ### Bugfixes * Nodes are no longer sent to sleep while a scheduled verification poll is pending * Simultaneous config DB updates are now prevented with a lockfile diff --git a/lerna.json b/lerna.json index 746f63bc16c0..bef94770f41a 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ ], "useWorkspaces": true, "npmClient": "yarn", - "version": "7.7.4", + "version": "7.7.5", "command": { "run": { "stream": true diff --git a/packages/config/package.json b/packages/config/package.json index ca4dba2217eb..fcdafaed3582 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/config", - "version": "7.7.4", + "version": "7.7.5", "description": "zwave-js: configuration files", "publishConfig": { "access": "public" @@ -32,7 +32,7 @@ "node": ">=10.0.0" }, "dependencies": { - "@zwave-js/core": "7.7.4", + "@zwave-js/core": "7.7.5", "@zwave-js/shared": "7.7.3", "alcalzone-shared": "^3.0.4", "ansi-colors": "^4.1.1", diff --git a/packages/core/package.json b/packages/core/package.json index e3e8d7068bd6..b846f8b6ef75 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/core", - "version": "7.7.4", + "version": "7.7.5", "description": "zwave-js: core components", "publishConfig": { "access": "public" diff --git a/packages/serial/package.json b/packages/serial/package.json index 363a5369d54e..157f1dbca370 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/serial", - "version": "7.7.4", + "version": "7.7.5", "description": "zwave-js: Serialport driver", "publishConfig": { "access": "public" @@ -31,7 +31,7 @@ "node": ">=10.0.0" }, "dependencies": { - "@zwave-js/core": "7.7.4", + "@zwave-js/core": "7.7.5", "alcalzone-shared": "^3.0.4", "serialport": "^9.1.0", "winston": "^3.3.3" diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index 2aad41a4e5a1..9b83ffe846d7 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -1,6 +1,6 @@ { "name": "zwave-js", - "version": "7.7.4", + "version": "7.7.5", "description": "Z-Wave driver written entirely in JavaScript/TypeScript", "keywords": [], "main": "index.js", @@ -64,9 +64,9 @@ "@alcalzone/pak": "^0.6.0", "@sentry/integrations": "^6.5.1", "@sentry/node": "^6.5.1", - "@zwave-js/config": "7.7.4", - "@zwave-js/core": "7.7.4", - "@zwave-js/serial": "7.7.4", + "@zwave-js/config": "7.7.5", + "@zwave-js/core": "7.7.5", + "@zwave-js/serial": "7.7.5", "@zwave-js/shared": "7.7.3", "alcalzone-shared": "^3.0.4", "ansi-colors": "^4.1.1", @@ -89,4 +89,4 @@ "lint:zwave": "yarn b lint", "ts": "node -r esbuild-register" } -} \ No newline at end of file +} From 7e144f3ed7346bd0aef2899e3d38aa33e2aaaaaa Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 16 Jun 2021 23:19:12 +0200 Subject: [PATCH 12/22] chore: link to search dialog in missing config template --- .github/ISSUE_TEMPLATE/device_config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/device_config.md b/.github/ISSUE_TEMPLATE/device_config.md index deb8a441574a..071e40ae0733 100644 --- a/.github/ISSUE_TEMPLATE/device_config.md +++ b/.github/ISSUE_TEMPLATE/device_config.md @@ -32,7 +32,7 @@ Product ID: 0x0101 🚨🚨🚨 WARNING: We will close this issue without warning if the following points do not apply. 🚨🚨🚨 --> -- [ ] It is not in the [configuration DB](https://devices.zwave-js.io/) +- [ ] It is not in the [configuration DB](https://devices.zwave-js.io/?search=3) - [ ] It was not [merged recently](https://github.com/zwave-js/node-zwave-js/pulls?q=is%3Apr+is%3Aclosed) or has a [pending PR](https://github.com/zwave-js/node-zwave-js/pulls?q=is%3Aopen+is%3Apr) - [ ] It is not in the [TODO list](https://github.com/zwave-js/node-zwave-js/issues/1600) From 0b7b251cdb8eb1dbce5db198e70b423b96859032 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 18 Jun 2021 00:07:11 +0200 Subject: [PATCH 13/22] fix: handle internal and filename-less stack traces in Sentry filter (#2871) --- .../zwave-js/src/lib/telemetry/sentry.test.ts | 302 ++++++++++++++++++ packages/zwave-js/src/lib/telemetry/sentry.ts | 265 ++++++++------- 2 files changed, 458 insertions(+), 109 deletions(-) create mode 100644 packages/zwave-js/src/lib/telemetry/sentry.test.ts diff --git a/packages/zwave-js/src/lib/telemetry/sentry.test.ts b/packages/zwave-js/src/lib/telemetry/sentry.test.ts new file mode 100644 index 000000000000..2deb660f5f13 --- /dev/null +++ b/packages/zwave-js/src/lib/telemetry/sentry.test.ts @@ -0,0 +1,302 @@ +import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; +import path from "path"; +import { createSentryContext, SentryContext } from "./sentry"; + +describe("The Sentry telemetry", () => { + let context: SentryContext; + beforeAll(() => { + context = createSentryContext(path.join(__dirname, "../../..")); + }); + + it("should ignore errors that are caused outside zwave-js", () => { + const event = { + exception: { + values: [ + { + type: "SyntaxError", + value: "Unexpected token o in JSON at position 1", + stacktrace: { + frames: [ + { + function: "process._tickCallback", + module: "next_tick", + filename: "internal/process/next_tick.js", + abs_path: "internal/process/next_tick.js", + }, + { + function: "null.", + module: "node-red-contrib-nextcloud:nextcloud", + filename: + "/home/pi/.node-red/node_modules/node-red-contrib-nextcloud/nextcloud.js", + abs_path: + "/home/pi/.node-red/node_modules/node-red-contrib-nextcloud/nextcloud.js", + }, + { + function: "JSON.parse", + in_app: true, + }, + ], + }, + mechanism: { + type: "onunhandledrejection", + handled: false, + }, + }, + ], + }, + } as any; + expect(context.shouldIgnore(event)).toBeTrue(); + }); + + it("should NOT ignore errors that are explicitly whitelisted", () => { + const event = { + exception: { + values: [ + { + type: "TypeError", + value: "Cannot read property 'nodeId' of undefined", + stacktrace: { + frames: [ + { + function: "processImmediate", + module: "timers", + filename: "internal/timers.js", + abs_path: "internal/timers.js", + }, + { + function: "Immediate.", + module: "iobroker.js-controller.lib:adapter", + filename: + "/opt/iobroker/node_modules/iobroker.js-controller/lib/adapter.js", + abs_path: + "/opt/iobroker/node_modules/iobroker.js-controller/lib/adapter.js", + }, + { + function: "ZWave2.EventEmitter.emit", + module: "domain", + filename: "domain.js", + abs_path: "domain.js", + }, + { + function: "ZWave2.emit", + module: "events", + filename: "events.js", + abs_path: "events.js", + }, + { + function: "ZWave2.onStateChange", + module: "iobroker.zwave2.src:main.ts", + filename: + "/opt/iobroker/node_modules/iobroker.zwave2/src/main.ts", + abs_path: + "/opt/iobroker/node_modules/iobroker.zwave2/src/main.ts", + }, + ], + }, + mechanism: { + type: "onunhandledrejection", + handled: false, + }, + }, + ], + }, + } as any; + expect(context.shouldIgnore(event)).toBeFalse(); + }); + + it("should ignore errors that must be handled by the developer", () => { + const event = { + exception: { + values: [ + { + type: "ZWaveError", + value: "Node 6 did not respond after 3 attempts, it is presumed dead", + stacktrace: { + frames: [ + { + function: "__awaiter", + module: "devices:powerswitch", + filename: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + abs_path: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + in_app: true, + }, + { + function: "new Promise", + in_app: true, + }, + { + function: "null.", + module: "devices:powerswitch", + filename: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + abs_path: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + in_app: true, + }, + { + function: "Generator.next", + in_app: true, + }, + { + function: "PowerSwitch.", + module: "devices:powerswitch", + filename: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + abs_path: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + lineno: 134, + colno: 24, + }, + { + function: "new Promise", + in_app: true, + }, + { + function: "null.", + module: "devices:powerswitch", + filename: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + abs_path: + "/home/michel/dashboard/servers/zwave/devices/powerswitch.js", + }, + { + function: "Proxy.set", + module: "zwave-js.src.lib.commandclass:BinarySwitchCC.ts", + filename: + "/home/michel/dashboard/servers/zwave/node_modules/zwave-js/src/lib/commandclass/BinarySwitchCC.ts", + abs_path: + "/home/michel/dashboard/servers/zwave/node_modules/zwave-js/src/lib/commandclass/BinarySwitchCC.ts", + }, + { + function: "Driver.sendCommand", + module: "zwave-js.src.lib.driver:Driver.ts", + filename: + "/home/michel/dashboard/servers/zwave/node_modules/zwave-js/src/lib/driver/Driver.ts", + abs_path: + "/home/michel/dashboard/servers/zwave/node_modules/zwave-js/src/lib/driver/Driver.ts", + }, + { + function: "Driver.sendMessage", + module: "zwave-js.src.lib.driver:Driver.ts", + filename: + "/home/michel/dashboard/servers/zwave/node_modules/zwave-js/src/lib/driver/Driver.ts", + abs_path: + "/home/michel/dashboard/servers/zwave/node_modules/zwave-js/src/lib/driver/Driver.ts", + }, + ], + }, + mechanism: { + type: "onunhandledrejection", + handled: false, + }, + }, + ], + }, + } as any; + const hint = { + originalException: new ZWaveError( + "This should be handled by the dev", + ZWaveErrorCodes.Controller_MessageDropped, + ), + } as any; + expect(context.shouldIgnore(event, hint)).toBeTrue(); + }); + + it("should ignore errors that must be handled by the developer, unless whitelisted", () => { + const event = { + exception: { + values: [ + { + type: "ZWaveError", + value: "Timeout while waiting for an ACK from the controller", + stacktrace: { + frames: [ + { + function: "processImmediate", + module: "timers", + filename: "internal/timers.js", + abs_path: "internal/timers.js", + }, + { + function: "Immediate._onImmediate", + module: "@iobroker.db-states-redis.lib.states:statesInRedisClient", + filename: + "/opt/iobroker/node_modules/@iobroker/db-states-redis/lib/states/statesInRedisClient.js", + abs_path: + "/opt/iobroker/node_modules/@iobroker/db-states-redis/lib/states/statesInRedisClient.js", + }, + { + function: "change", + module: "iobroker.js-controller.lib:adapter", + filename: + "/opt/iobroker/node_modules/iobroker.js-controller/lib/adapter.js", + abs_path: + "/opt/iobroker/node_modules/iobroker.js-controller/lib/adapter.js", + }, + { + function: "ZWave2.EventEmitter.emit", + module: "domain", + filename: "domain.js", + abs_path: "domain.js", + }, + { + function: "ZWave2.emit", + module: "events", + filename: "events.js", + abs_path: "events.js", + }, + { + function: "ZWave2.onMessage", + module: "iobroker.zwave2.src:main.ts", + filename: + "/opt/iobroker/node_modules/iobroker.zwave2/src/main.ts", + abs_path: + "/opt/iobroker/node_modules/iobroker.zwave2/src/main.ts", + }, + { function: "Array.map", in_app: true }, + { + function: "null.", + module: "iobroker.zwave2.src:main.ts", + filename: + "/opt/iobroker/node_modules/iobroker.zwave2/src/main.ts", + abs_path: + "/opt/iobroker/node_modules/iobroker.zwave2/src/main.ts", + }, + { + function: + "ZWaveController.getNodeNeighbors", + module: "zwave-js.src.lib.controller:Controller.ts", + filename: + "/opt/iobroker/node_modules/zwave-js/src/lib/controller/Controller.ts", + abs_path: + "/opt/iobroker/node_modules/zwave-js/src/lib/controller/Controller.ts", + }, + { + function: "Driver.sendMessage", + module: "zwave-js.src.lib.driver:Driver.ts", + filename: + "/opt/iobroker/node_modules/zwave-js/src/lib/driver/Driver.ts", + abs_path: + "/opt/iobroker/node_modules/zwave-js/src/lib/driver/Driver.ts", + }, + ], + }, + mechanism: { + type: "onunhandledrejection", + handled: false, + }, + }, + ], + }, + } as any; + const hint = { + originalException: new ZWaveError( + "This should be handled by the dev", + ZWaveErrorCodes.Controller_MessageDropped, + ), + } as any; + expect(context.shouldIgnore(event, hint)).toBeFalse(); + }); +}); diff --git a/packages/zwave-js/src/lib/telemetry/sentry.ts b/packages/zwave-js/src/lib/telemetry/sentry.ts index db3097a95af0..ff4b6c2a3c16 100644 --- a/packages/zwave-js/src/lib/telemetry/sentry.ts +++ b/packages/zwave-js/src/lib/telemetry/sentry.ts @@ -18,11 +18,14 @@ function isZWaveError( return "code" in err && typeof (err as any).code === "number"; } -export async function initSentry( - libraryRootDir: string, - libName: string, - libVersion: string, -): Promise { +/** @internal */ +export interface SentryContext { + isPartOfThisLib(filename: string): boolean; + shouldIgnore(event: Sentry.Event, hint?: Sentry.EventHint): boolean; + getFingerprint(): Promise; +} + +export function createSentryContext(libraryRootDir: string): SentryContext { /** Checks if a filename is part of this library. Paths outside will be excluded from Sentry error reporting */ function isPartOfThisLib(filename: string): boolean { const relative = path.relative(libraryRootDir, filename); @@ -51,6 +54,152 @@ export async function initSentry( return fingerprint; } + /** Returns whether any line in the given stacktrace is whitelisted and none is blacklisted */ + function anyWhitelisted(filenames: string[]): boolean { + const normalizedFilenames = filenames.map((f) => path.normalize(f)); + const normalizedWhitelists = pathWhitelists.map((w) => + path.normalize(w), + ); + const normalizedBlacklists = pathBlacklists.map((b) => + path.normalize(b), + ); + return ( + normalizedFilenames.some((f) => + normalizedWhitelists.some((w) => f.includes(w)), + ) && + !normalizedFilenames.some((f) => + normalizedBlacklists.some((b) => f.includes(b)), + ) + ); + } + + /** Returns whether the given Sentry event should be ignored */ + function shouldIgnore( + event: Sentry.Event, + hint?: Sentry.EventHint, + ): boolean { + // Sentry orders stack traces from outside (index 0) to inside (index 0). + // In order to figure out if the error was caused inside zwave-js, we need to + // ignore all traces without a filename or from Node.js internals + const filenames = + event.exception?.values?.[0]?.stacktrace?.frames + ?.map((f) => f.filename) + ?.filter( + (f): f is string => !!f && !f.startsWith("internal/"), + ) ?? []; + // Definitely ignore errors which have nothing to do with this library, unless whitelisted + if (!filenames.some((f) => isPartOfThisLib(f))) { + return !anyWhitelisted(filenames); + } + + let ignore = false; + + const culprit = filenames[filenames.length - 1]; + const culpritIsPartOfThisLib = isPartOfThisLib(culprit); + + // Maybe ignore errors that are raised outside zwave-js + if (!culpritIsPartOfThisLib) ignore = true; + + // Filter out specific errors that are raised by zwave-js, + // but shouldn't create a report on Sentry because they should be + // handled by the library user + if (culpritIsPartOfThisLib && hint) { + if (isZWaveError(hint.originalException)) { + switch (hint.originalException.code) { + // we don't care about timeouts + case ZWaveErrorCodes.Controller_MessageDropped: + // We don't care about failed node removal + case ZWaveErrorCodes.RemoveFailedNode_Failed: + case ZWaveErrorCodes.RemoveFailedNode_NodeOK: + // Or failed inclusion processes: + case ZWaveErrorCodes.Controller_InclusionFailed: + case ZWaveErrorCodes.Controller_ExclusionFailed: + // Or users that don't read the changelog: + case ZWaveErrorCodes.Driver_NoErrorHandler: + ignore = true; + break; + // Or users that try to manage associations on nodes that don't support it + case ZWaveErrorCodes.CC_NotSupported: + if ( + /does not support.+associations/.test( + hint.originalException.message, + ) + ) { + ignore = true; + } + break; + } + + // Try to attach transaction context this way + if (!ignore && hint.originalException.transactionSource) { + event.contexts = { + transaction: { + stack: hint.originalException.transactionSource, + }, + }; + } + } else if (hint.originalException) { + try { + const msg = hint.originalException.toString(); + if ( + /(no such file|permission denied|cannot open|file not found)/i.test( + msg, + ) && + /(\/dev\/|\/mqtt\/|COM\d+|Select Port)/i.test(msg) + ) { + // No such file or directory, cannot open /dev/ttyACM0 + // no such file or directory, rename '/usr/src/app/store/mqtt/incoming~' + // Opening COM18: File not found + // No such file or directory, cannot open Select Port + ignore = true; + } else if ( + /(EROFS|ENODEV|ENOSPC)/i.test(msg) && + /(read-only file system|no such device|no space left)/i.test( + msg, + ) + ) { + // EROFS: read-only file system, write + // ENODEV: no such device, write + // ENOSPC: no space left on device, write + ignore = true; + } else if (/unknown system error/i.test(msg)) { + // Unknown system error -116: Unknown system error -116, write + ignore = true; + } else if (/custom baud rate/i.test(msg)) { + // Input/output error setting custom baud rate of 115200 + ignore = true; + } else if (/bindings\.node/i.test(msg)) { + // Could not locate the bindings file + ignore = true; + } + } catch { + // This doesn't seem to be representable as a string + } + } + } + + // Don't ignore explicitly whitelisted paths + if (ignore && anyWhitelisted(filenames)) { + ignore = false; + } + + return ignore; + } + + return { + isPartOfThisLib, + shouldIgnore, + getFingerprint, + }; +} + +export async function initSentry( + libraryRootDir: string, + libName: string, + libVersion: string, +): Promise { + const context: SentryContext = createSentryContext(libraryRootDir); + Sentry.init({ release: `${libName}@${libVersion}`, dsn: "https://a66de07edd064106853cc639407ebe64@sentry.iobroker.net/119", @@ -66,114 +215,12 @@ export async function initSentry( ], maxBreadcrumbs: 30, beforeSend(event, hint) { - let ignore = false; - // By default we ignore errors that original outside this library - // Look at the last stackframe to figure out the filename - const filename = - event.exception?.values?.[0]?.stacktrace?.frames?.slice(-1)[0] - ?.filename; - - if (filename && !isPartOfThisLib(filename)) { - ignore = true; - } - - // Filter out specific errors that shouldn't create a report on sentry - // because they should be handled by the library user - if (!ignore && hint) { - if (isZWaveError(hint.originalException)) { - switch (hint.originalException.code) { - // we don't care about timeouts - case ZWaveErrorCodes.Controller_MessageDropped: - // We don't care about failed node removal - case ZWaveErrorCodes.RemoveFailedNode_Failed: - case ZWaveErrorCodes.RemoveFailedNode_NodeOK: - // Or failed inclusion processes: - case ZWaveErrorCodes.Controller_InclusionFailed: - case ZWaveErrorCodes.Controller_ExclusionFailed: - // Or users that don't read the changelog: - case ZWaveErrorCodes.Driver_NoErrorHandler: - ignore = true; - break; - // Or users that try to manage associations on nodes that don't support it - case ZWaveErrorCodes.CC_NotSupported: - if ( - /does not support.+associations/.test( - hint.originalException.message, - ) - ) { - ignore = true; - } - break; - } - - // Try to attach transaction context this way - if (!ignore && hint.originalException.transactionSource) { - event.contexts = { - transaction: { - stack: hint.originalException.transactionSource, - }, - }; - } - } else if (hint.originalException) { - try { - const msg = hint.originalException.toString(); - if ( - /(no such file|permission denied|cannot open|file not found)/i.test( - msg, - ) && - /(\/dev\/|\/mqtt\/|COM\d+|Select Port)/i.test(msg) - ) { - // No such file or directory, cannot open /dev/ttyACM0 - // no such file or directory, rename '/usr/src/app/store/mqtt/incoming~' - // Opening COM18: File not found - // No such file or directory, cannot open Select Port - ignore = true; - } else if ( - /(EROFS|ENODEV|ENOSPC)/i.test(msg) && - /(read-only file system|no such device|no space left)/i.test( - msg, - ) - ) { - // EROFS: read-only file system, write - // ENODEV: no such device, write - // ENOSPC: no space left on device, write - ignore = true; - } else if (/unknown system error/i.test(msg)) { - // Unknown system error -116: Unknown system error -116, write - ignore = true; - } else if (/custom baud rate/i.test(msg)) { - // Input/output error setting custom baud rate of 115200 - ignore = true; - } else if (/bindings\.node/i.test(msg)) { - // Could not locate the bindings file - ignore = true; - } - } catch { - // This doesn't seem to be representable as a string - } - } - } - - // Don't ignore explicitly whitelisted paths - if ( - ignore && - filename && - pathWhitelists.some((w) => - path.normalize(filename).includes(path.normalize(w)), - ) && - !pathBlacklists.some((b) => - path.normalize(filename).includes(path.normalize(b)), - ) - ) { - ignore = false; - } - - return ignore ? null : event; + return context.shouldIgnore(event, hint) ? null : event; }, }); // Try to group events by user (anonymously) try { - const fingerprint = await getFingerprint(); + const fingerprint = await context.getFingerprint(); Sentry.configureScope((scope) => { scope.setUser({ id: fingerprint }); }); From 059f5e5c1013433f64311c6c5bddfe2513bc7256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=94=81?= <52o@qq52o.cn> Date: Fri, 18 Jun 2021 14:46:57 +0800 Subject: [PATCH 14/22] docs: fix namespace error (#2877) --- docs/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.html b/docs/index.html index adebed379d24..5e958282a920 100644 --- a/docs/index.html +++ b/docs/index.html @@ -48,7 +48,9 @@ // onlyCover: true, // Avoid collisions with other docs on the same domain - namespace: "node-zwave-js", + search: { + namespace: "node-zwave-js", + }, plugins: [ EditOnGithubPlugin.create( From 4076f1a77cbd660770b7bfba81731c69cb075a3e Mon Sep 17 00:00:00 2001 From: blhoward2 <33612110+blhoward2@users.noreply.github.com> Date: Fri, 18 Jun 2021 03:47:09 -0400 Subject: [PATCH 15/22] fix(config): add fingerprint to Kwikset 912 (#2879) --- packages/config/config/devices/0x0090/912.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/config/config/devices/0x0090/912.json b/packages/config/config/devices/0x0090/912.json index 5b401ad96516..2d1ca415f689 100644 --- a/packages/config/config/devices/0x0090/912.json +++ b/packages/config/config/devices/0x0090/912.json @@ -9,6 +9,10 @@ "productId": "0x0336", "zwaveAllianceId": [1388, 1393] }, + { + "productType": "0x0003", + "productId": "0x0036" + }, { "productType": "0x0003", "productId": "0x0236" From b682ff934ab748d9bca3b21e169577a3d6b0b2d3 Mon Sep 17 00:00:00 2001 From: disforw Date: Fri, 18 Jun 2021 03:49:20 -0400 Subject: [PATCH 16/22] fix(config): update Nortek Device Configs (#2869) Co-authored-by: blhoward2 --- .../config/devices/0x014f/ps15emz51.json | 59 +++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/config/config/devices/0x014f/ps15emz51.json b/packages/config/config/devices/0x014f/ps15emz51.json index bbe9899c5246..6a8dc8a60586 100644 --- a/packages/config/config/devices/0x014f/ps15emz51.json +++ b/packages/config/config/devices/0x014f/ps15emz51.json @@ -1,7 +1,7 @@ { "manufacturer": "Nortek Security & Control LLC", "manufacturerId": "0x014f", - "label": "PS15EMZ51", + "label": "PS15EMZ5-1", "description": "Appliance Module", "devices": [ { @@ -13,17 +13,16 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, "paramInformation": { "2": { - "label": "LED Intensity", + "label": "LED Indicator: Brightness", "valueSize": 1, "minValue": 0, "maxValue": 100, "defaultValue": 100 }, "3": { - "label": "Night Light", + "label": "LED Indicator: Mode", "valueSize": 1, "minValue": 0, "maxValue": 3, @@ -31,29 +30,69 @@ "allowManualEntry": false, "options": [ { - "label": "LED ON when the load is ON", + "label": "On when the load is on", "value": 0 }, { - "label": "LED OFF when the load is on", + "label": "On when the load is off", "value": 1 }, { - "label": "LED is always ON", + "label": "Always ON", "value": 2 }, { - "label": "LED is always off", + "label": "Always off", "value": 3 } ] }, + "11": { + "label": "LED Indicator: Power Monitoring Display", + "valueSize": 1, + "minValue": 0, + "maxValue": 1, + "defaultValue": 1, + "allowManualEntry": false, + "options": [ + { + "label": "Disable", + "value": 0 + }, + { + "label": "Enable", + "value": 1 + } + ] + }, + "12": { + "label": "LED Indicator: Power Monitoring Display Duration", + "description": "Sets the length of time for which the LED will reflect the current power usage", + "valueSize": 2, + "unit": "seconds", + "minValue": 1, + "maxValue": 255, + "defaultValue": 60, + "options": [ + { + "label": "Continuous", + "value": 255 + } + ] + }, "13": { - "label": "Energy Monitoring", + "label": "Energy Usage Reporting Interval", "valueSize": 2, + "unit": "minutes", "minValue": 0, "maxValue": 255, - "defaultValue": 6 + "defaultValue": 6, + "options": [ + { + "label": "Disable", + "value": 0 + } + ] } } } From c91f84ba9bf643a0904c6b172362350d38adaf4d Mon Sep 17 00:00:00 2001 From: disforw Date: Fri, 18 Jun 2021 06:49:41 -0400 Subject: [PATCH 17/22] fix(config): add additional productID for the Yale YKFCON (#2842) Co-authored-by: AlCalzone Co-authored-by: Z-Wave JS Bot --- packages/config/config/devices/0x0129/ykfcon.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/config/config/devices/0x0129/ykfcon.json b/packages/config/config/devices/0x0129/ykfcon.json index f31ce56ec2f3..5fd6308bd66b 100644 --- a/packages/config/config/devices/0x0129/ykfcon.json +++ b/packages/config/config/devices/0x0129/ykfcon.json @@ -8,6 +8,10 @@ "productType": "0x0006", "productId": "0x0001", "zwaveAllianceId": 2743 + }, + { + "productType": "0x0600", + "productId": "0x0000" } ], "firmwareVersion": { @@ -29,7 +33,7 @@ "$import": "templates/yale_template.json#auto_relock" }, "3": { - "$import": "templates/yale_template.json#auto_relock_time_180" + "$import": "templates/yale_template.json#manual_relock_time" }, "6": { "$import": "templates/yale_template.json#remote_relock_time" From f74cb1163e5788a1e92d89fb4dc7ac8ffbe3e65e Mon Sep 17 00:00:00 2001 From: disforw Date: Fri, 18 Jun 2021 08:35:07 -0400 Subject: [PATCH 18/22] fix(config): add additional association groups for Elexa/Dome Home Automation Products (#2840) Co-authored-by: AlCalzone --- .../config/config/devices/0x021f/dmdp1.json | 20 ++++++++++++++++++- .../config/config/devices/0x021f/dmms1.json | 20 ++++++++++++++++++- .../config/config/devices/0x021f/dmmz1.json | 20 ++++++++++++++++++- .../config/config/devices/0x021f/dmof1.json | 16 ++++++++++++++- .../config/config/devices/0x021f/dms01.json | 16 ++++++++++++++- .../config/config/devices/0x021f/dmwd1.json | 20 ++++++++++++++++++- .../config/config/devices/0x021f/dmws1.json | 20 ++++++++++++++++++- 7 files changed, 125 insertions(+), 7 deletions(-) diff --git a/packages/config/config/devices/0x021f/dmdp1.json b/packages/config/config/devices/0x021f/dmdp1.json index bf0777507de5..c2352e16e9af 100644 --- a/packages/config/config/devices/0x021f/dmdp1.json +++ b/packages/config/config/devices/0x021f/dmdp1.json @@ -13,7 +13,25 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, + "associations": { + "1": { + "label": "Lifeline", + "maxNodes": 5, + "isLifeline": true + }, + "2": { + "label": "Basic Set", + "maxNodes": 5 + }, + "3": { + "label": "Notification Report", + "maxNodes": 5 + }, + "4": { + "label": "Binary Sensor Report", + "maxNodes": 5 + } + }, "paramInformation": { "1": { "label": "Enable/Disable LED Indicator", diff --git a/packages/config/config/devices/0x021f/dmms1.json b/packages/config/config/devices/0x021f/dmms1.json index 4ed39883a7f2..7504cf7983d2 100644 --- a/packages/config/config/devices/0x021f/dmms1.json +++ b/packages/config/config/devices/0x021f/dmms1.json @@ -13,7 +13,25 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, + "associations": { + "1": { + "label": "Lifeline", + "maxNodes": 5, + "isLifeline": true + }, + "2": { + "label": "Basic Set", + "maxNodes": 5 + }, + "3": { + "label": "Notification Report", + "maxNodes": 5 + }, + "4": { + "label": "Binary Sensor Report", + "maxNodes": 5 + } + }, "paramInformation": { "1": { "label": "Motion Sensitivity", diff --git a/packages/config/config/devices/0x021f/dmmz1.json b/packages/config/config/devices/0x021f/dmmz1.json index fec6d528f17d..efb4e1c32dab 100644 --- a/packages/config/config/devices/0x021f/dmmz1.json +++ b/packages/config/config/devices/0x021f/dmmz1.json @@ -13,7 +13,25 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, + "associations": { + "1": { + "label": "Lifeline", + "maxNodes": 5, + "isLifeline": true + }, + "2": { + "label": "Basic Set", + "maxNodes": 5 + }, + "3": { + "label": "Notification Report", + "maxNodes": 5 + }, + "4": { + "label": "Binary Sensor Report", + "maxNodes": 5 + } + }, "paramInformation": { "1": { "label": "BASIC_SET Level", diff --git a/packages/config/config/devices/0x021f/dmof1.json b/packages/config/config/devices/0x021f/dmof1.json index 78a3b9216cef..10273f3aacca 100644 --- a/packages/config/config/devices/0x021f/dmof1.json +++ b/packages/config/config/devices/0x021f/dmof1.json @@ -13,7 +13,21 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, + "associations": { + "1": { + "label": "Lifeline", + "maxNodes": 5, + "isLifeline": true + }, + "2": { + "label": "Basic Set", + "maxNodes": 5 + }, + "3": { + "label": "Notification Report", + "maxNodes": 5 + } + }, "paramInformation": { "1": { "label": "Disable Meter Functionality", diff --git a/packages/config/config/devices/0x021f/dms01.json b/packages/config/config/devices/0x021f/dms01.json index 5786017d054d..f96f2d9e26ac 100644 --- a/packages/config/config/devices/0x021f/dms01.json +++ b/packages/config/config/devices/0x021f/dms01.json @@ -13,7 +13,21 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, + "associations": { + "1": { + "label": "Lifeline", + "maxNodes": 5, + "isLifeline": true + }, + "2": { + "label": "Basic Set", + "maxNodes": 5 + }, + "3": { + "label": "Notification Report", + "maxNodes": 5 + } + }, "paramInformation": { "1": { "label": "Primary Notification Volume", diff --git a/packages/config/config/devices/0x021f/dmwd1.json b/packages/config/config/devices/0x021f/dmwd1.json index 347147f39faf..54ecef02c0b0 100644 --- a/packages/config/config/devices/0x021f/dmwd1.json +++ b/packages/config/config/devices/0x021f/dmwd1.json @@ -13,7 +13,25 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, + "associations": { + "1": { + "label": "Lifeline", + "maxNodes": 5, + "isLifeline": true + }, + "2": { + "label": "Basic Set", + "maxNodes": 5 + }, + "3": { + "label": "Notification Report", + "maxNodes": 5 + }, + "4": { + "label": "Binary Sensor Report", + "maxNodes": 5 + } + }, "paramInformation": { "1": { "label": "BASIC_SET Off Delay", diff --git a/packages/config/config/devices/0x021f/dmws1.json b/packages/config/config/devices/0x021f/dmws1.json index 41ae2dcd0c82..7fb85de7c2f5 100644 --- a/packages/config/config/devices/0x021f/dmws1.json +++ b/packages/config/config/devices/0x021f/dmws1.json @@ -13,7 +13,25 @@ "min": "0.0", "max": "255.255" }, - "supportsZWavePlus": true, + "associations": { + "1": { + "label": "Lifeline", + "maxNodes": 5, + "isLifeline": true + }, + "2": { + "label": "Basic Set", + "maxNodes": 5 + }, + "3": { + "label": "Notification Report", + "maxNodes": 5 + }, + "4": { + "label": "Binary Sensor Report", + "maxNodes": 5 + } + }, "paramInformation": { "1": { "label": "Total Alarm Duration", From 3b12450505bc3620ba82559fbf90def36e372197 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 18 Jun 2021 17:38:49 +0200 Subject: [PATCH 19/22] fix: detect of serial disconnection, destroy driver on serial/socket failure (#2874) Co-authored-by: Dominic Griesel --- packages/serial/src/ZWaveSerialPort.ts | 18 +++++++++++++++++- packages/zwave-js/src/lib/driver/Driver.ts | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/serial/src/ZWaveSerialPort.ts b/packages/serial/src/ZWaveSerialPort.ts index ecd1c9d79d1c..44b1afcd9c1e 100644 --- a/packages/serial/src/ZWaveSerialPort.ts +++ b/packages/serial/src/ZWaveSerialPort.ts @@ -1,7 +1,11 @@ -import type { ZWaveLogContainer } from "@zwave-js/core"; +import { ZWaveError, ZWaveErrorCodes, ZWaveLogContainer } from "@zwave-js/core"; import type SerialPort from "serialport"; import { ZWaveSerialPortBase } from "./ZWaveSerialPortBase"; +interface DisconnectError extends Error { + disconnected: boolean; +} + /** The default version of the Z-Wave serial binding that works using node-serialport */ export class ZWaveSerialPort extends ZWaveSerialPortBase { constructor(port: string, loggers: ZWaveLogContainer); @@ -27,6 +31,18 @@ export class ZWaveSerialPort extends ZWaveSerialPortBase { }), open: (serial: SerialPort) => new Promise((resolve) => { + // detect serial disconnection errors + serial.on("close", (err?: DisconnectError) => { + if (err?.disconnected === true) { + this.emit( + "error", + new ZWaveError( + `The serial port closed unexpectedly!`, + ZWaveErrorCodes.Driver_Failed, + ), + ); + } + }); serial.once("open", resolve).open(); }), close: (serial: SerialPort) => diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 59f61e1707a9..241ec399a4b2 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -644,8 +644,8 @@ export class Driver extends EventEmitter { this.serialport_onError(err); } else { spOpenPromise.reject(err); - void this.destroy(); } + void this.destroy(); }); // If the port is already open, close it first if (this.serial.isOpen) await this.serial.close(); From 672240eed1ada142f8e8281a1e4dc332d5bbb818 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 18 Jun 2021 17:39:02 +0200 Subject: [PATCH 20/22] feat: support bulk-setting normal and partial config parameters, support bulk-getting and -resetting (#2875) --- docs/api/CCs/Configuration.md | 44 +- .../src/lib/commandclass/ConfigurationCC.ts | 459 ++++++++++++++++-- 2 files changed, 474 insertions(+), 29 deletions(-) diff --git a/docs/api/CCs/Configuration.md b/docs/api/CCs/Configuration.md index 53bfd6b3e191..f178f253d1c9 100644 --- a/docs/api/CCs/Configuration.md +++ b/docs/api/CCs/Configuration.md @@ -21,6 +21,26 @@ This may timeout and return `undefined` if the node does not respond. If the node replied with a different parameter number, a `ConfigurationCCError` is thrown with the `argument` property set to the reported parameter number. +### `getBulk` + +```ts +async getBulk( + options: { + parameter: number; + bitMask?: number; + }[], +): Promise< + { + parameter: number; + bitMask?: number; + value: ConfigValue | undefined; + }[] +>; +``` + +Requests the current value of the config parameters from the device. +When the node does not respond due to a timeout, the `value` in the returned array will be `undefined`. + ### `set` ```ts @@ -28,12 +48,24 @@ async set( parameter: number, value: ConfigValue, valueSize: 1 | 2 | 4, - valueFormat: ConfigValueFormat = ConfigValueFormat.SignedInteger, + valueFormat?: ConfigValueFormat, ): Promise; + +async set(options: ConfigurationCCAPISetOptions): Promise; ``` Sets a new value for a given config parameter of the device. +### `setBulk` + +```ts +async setBulk( + values: ConfigurationCCAPISetOptions[], +): Promise; +``` + +Sets new values for multiple config parameters of the device. Uses the `BulkSet` command if supported, otherwise falls back to individual `Set` commands. + ### `reset` ```ts @@ -44,6 +76,16 @@ Resets a configuration parameter to its default value. WARNING: This will throw on legacy devices (ConfigurationCC v3 and below). +### `resetBulk` + +```ts +async resetBulk(parameters: number[]): Promise; +``` + +Resets multiple configuration parameters to their default value. Uses BulkSet if supported, otherwise falls back to individual Set commands. + +WARNING: This will throw on legacy devices (ConfigurationCC v3 and below). + ### `resetAll` ```ts diff --git a/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts b/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts index f8f0c6f37bea..0ad93ee26fa4 100644 --- a/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts @@ -28,6 +28,8 @@ import { composeObject } from "alcalzone-shared/objects"; import { padStart } from "alcalzone-shared/strings"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; +import { Endpoint } from "../node/Endpoint"; +import type { VirtualEndpoint } from "../node/VirtualEndpoint"; import { CCAPI, PollValueImplementation, @@ -104,6 +106,147 @@ const isParamInfoFromConfigValueId: ValueID = { property: "isParamInformationFromConfig", }; +export type ConfigurationCCAPISetOptions = { + parameter: number; +} & ( + | { + // Variant 1: Normal parameter, defined in a config file + bitMask?: undefined; + value: ConfigValue; + } + | { + // Variant 2: Normal parameter, not defined in a config file + bitMask?: undefined; + value: ConfigValue; + valueSize: 1 | 2 | 4; + valueFormat: ConfigValueFormat; + } + | { + // Variant 3: Partial parameter, must be defined in a config file + bitMask: number; + value: number; + } +); + +type NormalizedConfigurationCCAPISetOptions = { + parameter: number; + valueSize: 1 | 2 | 4; + valueFormat: ConfigValueFormat; +} & ( + | { bitMask?: undefined; value: ConfigValue } + | { bitMask: number; value: number } +); + +function normalizeConfigurationCCAPISetOptions( + endpoint: Endpoint | VirtualEndpoint, + options: ConfigurationCCAPISetOptions, +): NormalizedConfigurationCCAPISetOptions { + if ("bitMask" in options && options.bitMask) { + // Variant 3: Partial param, look it up in the device config + // TODO: This is fucking ugly + const ccc = + endpoint instanceof Endpoint + ? endpoint.createCCInstance(ConfigurationCC)! + : endpoint.node.physicalNodes[0].createCCInstance( + ConfigurationCC, + )!; + const paramInfo = ccc.getParamInformation( + options.parameter, + options.bitMask, + ); + if (!paramInfo.isFromConfig) { + throw new ZWaveError( + "Setting a partial configuration parameter requires it to be defined in a device config file!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + return { + parameter: options.parameter, + bitMask: options.bitMask, + value: options.value, + valueSize: paramInfo.valueSize as any, + valueFormat: paramInfo.format!, + }; + } else if ("valueSize" in options) { + // Variant 2: Normal parameter, not defined in a config file + return pick(options, [ + "parameter", + "value", + "valueSize", + "valueFormat", + ]); + } else { + // Variant 1: Normal parameter, defined in a config file + // TODO: This is fucking ugly + const ccc = + endpoint instanceof Endpoint + ? endpoint.createCCInstance(ConfigurationCC)! + : endpoint.node.physicalNodes[0].createCCInstance( + ConfigurationCC, + )!; + const paramInfo = ccc.getParamInformation( + options.parameter, + options.bitMask, + ); + if (!paramInfo.isFromConfig) { + throw new ZWaveError( + "Setting a configuration parameter without specifying a value size and format requires it to be defined in a device config file!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + return { + parameter: options.parameter, + value: options.value, + valueSize: paramInfo.valueSize as any, + valueFormat: paramInfo.format!, + }; + } +} + +function bulkMergePartialParamValues( + endpoint: Endpoint | VirtualEndpoint, + options: NormalizedConfigurationCCAPISetOptions[], +): (NormalizedConfigurationCCAPISetOptions & { bitMask?: undefined })[] { + // Merge partial parameters before doing anything else. Therefore, take the non-partials, ... + const allParams = options.filter((o) => o.bitMask == undefined); + // ... group the partials by parameter + const unmergedPartials = new Map< + number, + NormalizedConfigurationCCAPISetOptions[] + >(); + for (const partial of options.filter((o) => o.bitMask != undefined)) { + if (!unmergedPartials.has(partial.parameter)) { + unmergedPartials.set(partial.parameter, []); + } + unmergedPartials.get(partial.parameter)!.push(partial); + } + // and push the merged result into the array we'll be working with + if (unmergedPartials.size) { + const ccc = + endpoint instanceof Endpoint + ? endpoint.createCCInstance(ConfigurationCC)! + : endpoint.node.physicalNodes[0].createCCInstance( + ConfigurationCC, + )!; + + for (const [parameter, partials] of unmergedPartials) { + allParams.push({ + parameter, + value: ccc.composePartialParamValues( + parameter, + partials.map((p) => ({ + bitMask: p.bitMask!, + partialValue: p.value as number, + })), + ), + valueSize: partials[0].valueSize, + valueFormat: partials[0].valueFormat, + }); + } + } + return allParams as any; +} + @API(CommandClasses.Configuration) export class ConfigurationCCAPI extends CCAPI { public supportsCommand(cmd: ConfigurationCommand): Maybe { @@ -246,7 +389,12 @@ export class ConfigurationCCAPI extends CCAPI { ); } - await this.set(property, targetValue, valueSize as any, valueFormat); + await this.set({ + parameter: property, + value: targetValue, + valueSize: valueSize as any, + valueFormat, + }); if ((this as ConfigurationCCAPI).isSinglecast()) { // Verify the current value after a delay @@ -336,31 +484,227 @@ export class ConfigurationCCAPI extends CCAPI { ); } + /** + * Requests the current value of the config parameters from the device. + * When the node does not respond due to a timeout, the `value` in the returned array will be `undefined`. + */ + public async getBulk( + options: { + parameter: number; + bitMask?: number; + }[], + ): Promise< + { + parameter: number; + bitMask?: number; + value: ConfigValue | undefined; + }[] + > { + // Get-type commands are only possible in singlecast + this.assertPhysicalEndpoint(this.endpoint); + + let values: ReadonlyMap; + + // If the parameters are consecutive, we may use BulkGet + const distinctParameters = distinct(options.map((o) => o.parameter)); + if ( + this.supportsCommand(ConfigurationCommand.BulkGet) && + isConsecutiveArray(distinctParameters) + ) { + const cc = new ConfigurationCCBulkGet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + parameters: distinctParameters, + }); + const response = + await this.driver.sendCommand( + cc, + this.commandOptions, + ); + if (response) values = response.values; + } else { + this.assertSupportsCommand( + ConfigurationCommand, + ConfigurationCommand.Get, + ); + + const _values = new Map(); + for (const parameter of distinctParameters) { + const cc = new ConfigurationCCGet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + parameter, + }); + const response = + await this.driver.sendCommand( + cc, + this.commandOptions, + ); + if (response) { + _values.set(response.parameter, response.value); + } + } + values = _values; + } + + // Combine the returned values with the requested ones + const cc = this.endpoint.createCCInstance(ConfigurationCC)!; + return options.map((o) => { + let value = values.get(o.parameter); + if (typeof value === "number" && o.bitMask) { + const paramInfo = cc.getParamInformation( + o.parameter, + o.bitMask, + ); + value = parsePartial( + value, + o.bitMask, + (paramInfo.format ?? ConfigValueFormat.SignedInteger) === + ConfigValueFormat.SignedInteger, + ); + } + return { ...o, value }; + }); + } + /** * Sets a new value for a given config parameter of the device. + * @deprecated Use the overload with an options object instead */ public async set( parameter: number, value: ConfigValue, valueSize: 1 | 2 | 4, - valueFormat: ConfigValueFormat = ConfigValueFormat.SignedInteger, + valueFormat?: ConfigValueFormat, + ): Promise; + + /** + * Sets a new value for a given config parameter of the device. + */ + public async set(options: ConfigurationCCAPISetOptions): Promise; + + /** + * Sets a new value for a given config parameter of the device. + */ + public async set( + ...args: + | [ + parameter: number, + value: ConfigValue, + valueSize: 1 | 2 | 4, + valueFormat?: ConfigValueFormat, + ] + | [ConfigurationCCAPISetOptions] ): Promise { this.assertSupportsCommand( ConfigurationCommand, ConfigurationCommand.Set, ); - const cc = new ConfigurationCCSet(this.driver, { - nodeId: this.endpoint.nodeId, - // Don't set an endpoint here, Configuration is device specific, not endpoint specific - parameter, - value, - valueSize, - valueFormat, - }); + let cc: ConfigurationCCSet; + if (args.length === 1) { + const options = normalizeConfigurationCCAPISetOptions( + this.endpoint, + args[0], + ); + let value = options.value; + if (options.bitMask) { + const ccc = + this.endpoint instanceof Endpoint + ? this.endpoint.createCCInstance(ConfigurationCC)! + : this.endpoint.node.physicalNodes[0].createCCInstance( + ConfigurationCC, + )!; + value = ccc.composePartialParamValue( + options.parameter, + options.bitMask, + options.value, + ); + } + cc = new ConfigurationCCSet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + resetToDefault: false, + parameter: options.parameter, + value, + valueSize: options.valueSize, + valueFormat: options.valueFormat, + }); + } else { + cc = new ConfigurationCCSet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + resetToDefault: false, + parameter: args[0], + value: args[1], + valueSize: args[2], + valueFormat: args[3] ?? ConfigValueFormat.SignedInteger, + }); + } + await this.driver.sendCommand(cc, this.commandOptions); } + /** + * Sets new values for multiple config parameters of the device. Uses the `BulkSet` command if supported, otherwise falls back to individual `Set` commands. + */ + public async setBulk( + values: ConfigurationCCAPISetOptions[], + ): Promise { + // Normalize the values so we can better work with them + const normalized = values.map((v) => + normalizeConfigurationCCAPISetOptions(this.endpoint, v), + ); + // And merge multiple partials that belong the same "full" value + const allParams = bulkMergePartialParamValues( + this.endpoint, + normalized, + ); + + const canUseBulkSet = + this.supportsCommand(ConfigurationCommand.BulkSet) && + // For Bulk Set we need consecutive parameters + isConsecutiveArray(allParams.map((v) => v.parameter)) && + // and identical format + new Set(allParams.map((v) => v.valueFormat)).size === 1 && + // and identical size + new Set(allParams.map((v) => v.valueSize)).size === 1; + + if (canUseBulkSet) { + const cc = new ConfigurationCCBulkSet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + parameters: allParams.map((v) => v.parameter), + valueSize: allParams[0].valueSize, + valueFormat: allParams[0].valueFormat, + values: allParams.map((v) => v.value as number), + handshake: true, + }); + await this.driver.sendCommand(cc, this.commandOptions); + } else { + this.assertSupportsCommand( + ConfigurationCommand, + ConfigurationCommand.Set, + ); + for (const { + parameter, + value, + valueSize, + valueFormat, + } of allParams) { + const cc = new ConfigurationCCSet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + parameter, + value, + valueSize, + valueFormat, + }); + await this.driver.sendCommand(cc, this.commandOptions); + } + } + } + /** * Resets a configuration parameter to its default value. * @@ -381,6 +725,43 @@ export class ConfigurationCCAPI extends CCAPI { await this.driver.sendCommand(cc, this.commandOptions); } + /** + * Resets multiple configuration parameters to their default value. Uses BulkSet if supported, otherwise falls back to individual Set commands. + * + * WARNING: This will throw on legacy devices (ConfigurationCC v3 and below) + */ + public async resetBulk(parameters: number[]): Promise { + if ( + isConsecutiveArray(parameters) && + this.supportsCommand(ConfigurationCommand.BulkSet) + ) { + const cc = new ConfigurationCCBulkSet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + parameters, + resetToDefault: true, + }); + await this.driver.sendCommand(cc, this.commandOptions); + } else { + this.assertSupportsCommand( + ConfigurationCommand, + ConfigurationCommand.Set, + ); + const CCs = distinct(parameters).map( + (parameter) => + new ConfigurationCCSet(this.driver, { + nodeId: this.endpoint.nodeId, + // Don't set an endpoint here, Configuration is device specific, not endpoint specific + parameter, + resetToDefault: true, + }), + ); + for (const cc of CCs) { + await this.driver.sendCommand(cc, this.commandOptions); + } + } + } + /** Resets all configuration parameters to their default value */ public async resetAll(): Promise { // This is dangerous - don't allow resetting all parameters via multicast @@ -783,11 +1164,9 @@ alters capabilities: ${!!properties.altersCapabilities}`; ): ConfigurationMetadata { const valueDB = this.getValueDB(); const valueId = getParamInformationValueID(parameter, valueBitMask); - return ( - (valueDB.getMetadata(valueId) as ConfigurationMetadata) ?? { - ...ValueMetadata.Any, - } - ); + return (valueDB.getMetadata(valueId) ?? { + ...ValueMetadata.Any, + }) as ConfigurationMetadata; } /** @@ -822,12 +1201,27 @@ alters capabilities: ${!!properties.altersCapabilities}`; } /** - * Returns stored config parameter metadata for all partial config params addressed with the given parameter number + * Computes the full value of a parameter after applying a partial param value */ public composePartialParamValue( parameter: number, - valueBitMask: number, - maskedValue: number, + bitMask: number, + partialValue: number, + ): number { + return this.composePartialParamValues(parameter, [ + { bitMask, partialValue }, + ]); + } + + /** + * Computes the full value of a parameter after applying multiple partial param values + */ + public composePartialParamValues( + parameter: number, + partials: { + bitMask: number; + partialValue: number; + }[], ): number { const valueDB = this.getValueDB(); // Add the other values together @@ -836,7 +1230,7 @@ alters capabilities: ${!!properties.altersCapabilities}`; id.commandClass === this.ccId && id.property === parameter && id.propertyKey != undefined && - id.propertyKey !== valueBitMask, + !partials.some((p) => id.propertyKey === p.bitMask), ); let ret = 0; for (const { @@ -845,9 +1239,11 @@ alters capabilities: ${!!properties.altersCapabilities}`; } of otherValues) { ret = encodePartial(ret, partialValue as number, bitMask as number); } - return encodePartial(ret, maskedValue, valueBitMask); + for (const { bitMask, partialValue } of partials) { + ret = encodePartial(ret, partialValue, bitMask); + } + return ret; } - public serializeValuesForCache(): CacheValue[] { // Leave out the paramInformation if we have loaded it from a config file let values = super.serializeValuesForCache(); @@ -1265,6 +1661,7 @@ type ConfigurationCCBulkSetOptions = CCCommandOptions & { | { resetToDefault?: false; valueSize: number; + valueFormat?: ConfigValueFormat; values: number[]; } ); @@ -1306,9 +1703,14 @@ export class ConfigurationCCBulkSet extends ConfigurationCC { this._resetToDefault = !!options.resetToDefault; if (!!options.resetToDefault) { this._valueSize = 1; + this._valueFormat = ConfigValueFormat.SignedInteger; this._values = this._parameters.map(() => 0); } else { this._valueSize = options.valueSize; + this._valueFormat = + options.valueFormat ?? + this.getParamInformation(this._parameters[0]).format ?? + ConfigValueFormat.SignedInteger; this._values = options.values; } } @@ -1326,6 +1728,10 @@ export class ConfigurationCCBulkSet extends ConfigurationCC { public get valueSize(): number { return this._valueSize; } + private _valueFormat: ConfigValueFormat; + public get valueFormat(): ConfigValueFormat { + return this._valueFormat; + } private _values: number[]; public get values(): number[] { return this._values; @@ -1349,18 +1755,15 @@ export class ConfigurationCCBulkSet extends ConfigurationCC { for (let i = 0; i < this.parameters.length; i++) { const value = this._values[i]; const param = this._parameters[i]; - const valueFormat = - this.getParamInformation(param).format ?? - ConfigValueFormat.SignedInteger; // Make sure that the given value fits into the value size - if (!isSafeValue(value, valueSize, valueFormat)) { + if (!isSafeValue(value, valueSize, this._valueFormat)) { // If there is a value size configured, check that the given value is compatible throwInvalidValueError( value, param, valueSize, - valueFormat, + this._valueFormat, ); } @@ -1369,7 +1772,7 @@ export class ConfigurationCCBulkSet extends ConfigurationCC { this.payload, 4 + i * valueSize, valueSize, - valueFormat, + this._valueFormat, value, ); } catch (e) { @@ -1378,7 +1781,7 @@ export class ConfigurationCCBulkSet extends ConfigurationCC { value, param, valueSize, - valueFormat, + this._valueFormat, ); } } From 2b2b7d45f0a50416cda84b2f6046a51dd1e87470 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 18 Jun 2021 17:39:11 +0200 Subject: [PATCH 21/22] fix(cc): create V1 alarm metadata on demand (#2880) --- .../src/lib/commandclass/NotificationCC.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/zwave-js/src/lib/commandclass/NotificationCC.ts b/packages/zwave-js/src/lib/commandclass/NotificationCC.ts index fb7030e54736..cdefb5d0237f 100644 --- a/packages/zwave-js/src/lib/commandclass/NotificationCC.ts +++ b/packages/zwave-js/src/lib/commandclass/NotificationCC.ts @@ -889,16 +889,24 @@ export class NotificationCCReport extends NotificationCC { const valueDB = this.getValueDB(); if (this.alarmType != undefined) { - valueDB.setValue( - getAlarmTypeValueId(this.endpointIndex), - this.alarmType, - ); + const valueId = getAlarmTypeValueId(this.endpointIndex); + if (!valueDB.hasMetadata(valueId)) { + valueDB.setMetadata(valueId, { + ...ValueMetadata.ReadOnlyUInt8, + label: "Alarm Type", + }); + } + valueDB.setValue(valueId, this.alarmType); } if (this.alarmLevel != undefined) { - valueDB.setValue( - getAlarmLevelValueId(this.endpointIndex), - this.alarmLevel, - ); + const valueId = getAlarmLevelValueId(this.endpointIndex); + if (!valueDB.hasMetadata(valueId)) { + valueDB.setMetadata(valueId, { + ...ValueMetadata.ReadOnlyUInt8, + label: "Alarm Level", + }); + } + valueDB.setValue(valueId, this.alarmLevel); } return true; From 3c024094f1da941edb2591f6f8eea6e687decdb9 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 18 Jun 2021 17:39:19 +0200 Subject: [PATCH 22/22] feat: use serial API version as firmware version for the controller node (#2884) --- packages/zwave-js/src/lib/commandclass/VersionCC.ts | 12 ++++++++---- packages/zwave-js/src/lib/controller/Controller.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/zwave-js/src/lib/commandclass/VersionCC.ts b/packages/zwave-js/src/lib/commandclass/VersionCC.ts index ab26b3f0fdd1..615fb0cc9c89 100644 --- a/packages/zwave-js/src/lib/commandclass/VersionCC.ts +++ b/packages/zwave-js/src/lib/commandclass/VersionCC.ts @@ -39,6 +39,13 @@ export function getFirmwareVersionsValueId(): ValueID { }; } +export function getFirmwareVersionsMetadata(): ValueMetadata { + return { + ...ValueMetadata.ReadOnly, + label: "Z-Wave chip firmware versions", + }; +} + export enum VersionCommand { Get = 0x11, Report = 0x12, @@ -400,10 +407,7 @@ export class VersionCCReport extends VersionCC { private _firmwareVersions: string[]; @ccValue() - @ccValueMetadata({ - ...ValueMetadata.ReadOnly, - label: "Z-Wave chip firmware versions", - }) + @ccValueMetadata(getFirmwareVersionsMetadata()) public get firmwareVersions(): string[] { return this._firmwareVersions; } diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 45de943fd2ec..b9f83c29df31 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -46,6 +46,10 @@ import type { EndpointAddress, MultiChannelAssociationCC, } from "../commandclass/MultiChannelAssociationCC"; +import { + getFirmwareVersionsMetadata, + getFirmwareVersionsValueId, +} from "../commandclass/VersionCC"; import type { Driver, RequestHandler } from "../driver/Driver"; import { FunctionType } from "../message/Constants"; import type { Message } from "../message/Message"; @@ -680,6 +684,15 @@ export class ZWaveController extends EventEmitter { controllerValueDB.setValue(getProductTypeValueId(), this._productType); controllerValueDB.setValue(getProductIdValueId(), this._productId); + // Set firmware version information for the controller node + controllerValueDB.setMetadata( + getFirmwareVersionsValueId(), + getFirmwareVersionsMetadata(), + ); + controllerValueDB.setValue(getFirmwareVersionsValueId(), [ + this._serialApiVersion, + ]); + if ( this.type !== ZWaveLibraryTypes["Bridge Controller"] && this.isFunctionSupported(FunctionType.SetSerialApiTimeouts)