From 478bc12c531f69ebf2c4a7e73e3ff5a6e978785b Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 8 Jul 2024 10:36:18 +0200 Subject: [PATCH 1/9] chore: update zwave-js and zwave-js-server to v13 beta --- package-lock.json | 129 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 66 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 715fd3a4cfa..cd2596220b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@jamescoyle/vue-icon": "^0.1.2", "@kvaster/zwavejs-prom": "^0.0.2", "@mdi/js": "7.4.47", - "@zwave-js/server": "^1.36.0", + "@zwave-js/server": "github:zwave-js/zwave-js-server#zwave-js-13", "@zwave-js/winston-daily-rotate-file": "^4.5.6-1", "ansi_up": "^6.0.2", "archiver": "^7.0.1", @@ -58,7 +58,7 @@ "vuedraggable": "^2.24.3", "vuetify": "^2.7.2", "winston": "^3.13.0", - "zwave-js": "^12.12.1" + "zwave-js": "^13.0.0-beta.1" }, "bin": { "zwave-js-ui": "server/bin/www.js" @@ -179,9 +179,9 @@ } }, "node_modules/@alcalzone/pak": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@alcalzone/pak/-/pak-0.10.2.tgz", - "integrity": "sha512-v+kM7HlfIVNLDlGBcbZvrG3yVK3rPLH5kIoGRJbCcoHwpUqQbfEMzXAy1ZrfP+zbI5phHw2PhgrXZr3z6nh7Ow==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@alcalzone/pak/-/pak-0.11.0.tgz", + "integrity": "sha512-S6s2Xug8VJ04Xgam7kV+dUydVB2gJmTem+Kr7oxneeXndWddgoQxphQNI9WqgpsifTkonC9wiAbj3qkMFlNeAA==", "dependencies": { "axios": "^1.6.2", "execa": "~5.0.1", @@ -4729,14 +4729,14 @@ } }, "node_modules/@zwave-js/cc": { - "version": "12.12.1", - "resolved": "https://registry.npmjs.org/@zwave-js/cc/-/cc-12.12.1.tgz", - "integrity": "sha512-7GqcI77Rlf/xSrrRfC/96VOyn6+wcTKqCHwV6LSlX2GW6HZdN/4CJaO6aN0XkWARRHxbYVI/wm0z4mucOA6TQQ==", - "dependencies": { - "@zwave-js/core": "12.12.0", - "@zwave-js/host": "12.12.0", - "@zwave-js/serial": "12.12.0", - "@zwave-js/shared": "12.11.1", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/cc/-/cc-13.0.0-beta.1.tgz", + "integrity": "sha512-LSOl8bRJb64GRkVmHNZ9lCI9MUzpQMZZ8vWcBFkzWg2bvlZmie+4xDPwFvc/uF0eVRYhJ6MbAwo4J9lCL5IgGQ==", + "dependencies": { + "@zwave-js/core": "13.0.0-beta.1", + "@zwave-js/host": "13.0.0-beta.1", + "@zwave-js/serial": "13.0.0-beta.1", + "@zwave-js/shared": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8", "ansi-colors": "^4.1.3", "reflect-metadata": "^0.2.2" @@ -4749,12 +4749,12 @@ } }, "node_modules/@zwave-js/config": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/@zwave-js/config/-/config-12.12.0.tgz", - "integrity": "sha512-bkUk3dchbslC/yGsElOgp5FItGjLHk2Gb6LoRXz2rfmiswCo3BzyJJlzVKYvg3vTALHKsbpcxekGJeYsGFBaRQ==", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/config/-/config-13.0.0-beta.1.tgz", + "integrity": "sha512-0Qwnek4EP2mgyfcltkeqOY/RUV+ldIKRmFceLa3sj0Z9v55rL4Q7FDHSNJWcygDEj8mZzzWUV8uU8IM1pZqngw==", "dependencies": { - "@zwave-js/core": "12.12.0", - "@zwave-js/shared": "12.11.1", + "@zwave-js/core": "13.0.0-beta.1", + "@zwave-js/shared": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8", "ansi-colors": "^4.1.3", "fs-extra": "^11.2.0", @@ -4771,12 +4771,12 @@ } }, "node_modules/@zwave-js/core": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/@zwave-js/core/-/core-12.12.0.tgz", - "integrity": "sha512-zi0CbkS7ySYCOadL/XFZYACwNo3yjnc5fI4Bxt5c0KM1BJ2Nw912RGHP/RBLC/xLfnHFJ08U8rhj7e6oqU0NbA==", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/core/-/core-13.0.0-beta.1.tgz", + "integrity": "sha512-rqqZ9c/Gm2iKH5dyuXu7Ye5CcFLP/rvXV7aMikIhirli2/tUCntKGYGN7edL7POWwdiAbtJ943ZwCz8Ovc+RlQ==", "dependencies": { "@alcalzone/jsonl-db": "^3.1.1", - "@zwave-js/shared": "12.11.1", + "@zwave-js/shared": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8", "ansi-colors": "^4.1.3", "dayjs": "^1.11.10", @@ -4804,13 +4804,13 @@ } }, "node_modules/@zwave-js/host": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/@zwave-js/host/-/host-12.12.0.tgz", - "integrity": "sha512-tKs+vlpTzwlaKbbuZKcme2Tqojv0S68azD1QOKWmx8CmJ8O6ZLBS1bn2dbE2YQXuiPXNMEdYZRauzJy0HiRg/w==", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/host/-/host-13.0.0-beta.1.tgz", + "integrity": "sha512-Qjd/S5RloW3YXL97HRHVmbp1iHNwGlnoaamLOTNe/L7vDHDR7+RMauuHjMWBLwalW2ACLqvSM3x3oGuD2T4LbQ==", "dependencies": { - "@zwave-js/config": "12.12.0", - "@zwave-js/core": "12.12.0", - "@zwave-js/shared": "12.11.1", + "@zwave-js/config": "13.0.0-beta.1", + "@zwave-js/core": "13.0.0-beta.1", + "@zwave-js/shared": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8" }, "engines": { @@ -4821,12 +4821,12 @@ } }, "node_modules/@zwave-js/nvmedit": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/@zwave-js/nvmedit/-/nvmedit-12.12.0.tgz", - "integrity": "sha512-aiD1crUNdMglkfbVon4uB5UxkC0RHxySE7Zwl1rfZw5vr4zch+mxbDw9l9QzUY7lqRhxTM6LTIIxUH0pDKx4BA==", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/nvmedit/-/nvmedit-13.0.0-beta.1.tgz", + "integrity": "sha512-Y1HnjZuTHXZbt6Cum4qJMzkdXXcTwUAw3P7F6ovtikOfwi1tgmILApVEBEBSvY+SYjkG2pCaQo1JI74UQaxEww==", "dependencies": { - "@zwave-js/core": "12.12.0", - "@zwave-js/shared": "12.11.1", + "@zwave-js/core": "13.0.0-beta.1", + "@zwave-js/shared": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8", "fs-extra": "^11.2.0", "reflect-metadata": "^0.2.2", @@ -4938,14 +4938,14 @@ } }, "node_modules/@zwave-js/serial": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/@zwave-js/serial/-/serial-12.12.0.tgz", - "integrity": "sha512-Dg5RcdiUcN2e+bNV1d2a2KyxV5jT9pX6HpTPCqEfzmb8NQxgW6Jp8LbOjmdavNktunZUu8rBst4mA66ICPOuEw==", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/serial/-/serial-13.0.0-beta.1.tgz", + "integrity": "sha512-KJBLHF+V13fszJF+bj45VsPwVeXjnGneGvj/enF7/SzlFIlK0OQ2VkDgWB2yQhqvq4ErDDmfWFroYekutsOLWQ==", "dependencies": { "@serialport/stream": "^12.0.0", - "@zwave-js/core": "12.12.0", - "@zwave-js/host": "12.12.0", - "@zwave-js/shared": "12.11.1", + "@zwave-js/core": "13.0.0-beta.1", + "@zwave-js/host": "13.0.0-beta.1", + "@zwave-js/shared": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8", "serialport": "^12.0.0", "winston": "^3.13.0" @@ -4959,8 +4959,7 @@ }, "node_modules/@zwave-js/server": { "version": "1.36.0", - "resolved": "https://registry.npmjs.org/@zwave-js/server/-/server-1.36.0.tgz", - "integrity": "sha512-yqBRL4ostEx/h5HN8PlwzrbrQQQ11EGxNSoO/vNLdnNXpl7eUvbngms00K5i3a/ZEQvxdV0DdLWC5js7ZAqrHA==", + "resolved": "git+ssh://git@github.com/zwave-js/zwave-js-server.git#1719062c9d3433704d9c95817bcfded57e24b54b", "dependencies": { "@homebridge/ciao": "^1.1.7", "minimist": "^1.2.8", @@ -4971,13 +4970,13 @@ "zwave-server": "dist/bin/server.js" }, "peerDependencies": { - "zwave-js": "^12.11.0" + "zwave-js": "^13.0.0-beta.1" } }, "node_modules/@zwave-js/shared": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/@zwave-js/shared/-/shared-12.11.1.tgz", - "integrity": "sha512-/uNlHLEWgqwao0D2em/BpVFx3M8x1EL//KqxY9JNteYxNV175Bhu4uRcRHXAKPiqcPNyCmmU2/fHXHgRpwtOVw==", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/shared/-/shared-13.0.0-beta.1.tgz", + "integrity": "sha512-L3GNJVQsIU9CoVLwHAa9NTC7v2obaKF/62Q1v0TQ20DlFLfeN9tf6hK+Wl5n2Nn13Cb67cg/xiIy1qIuFLAg/A==", "dependencies": { "alcalzone-shared": "^4.0.8", "fs-extra": "^11.2.0" @@ -4990,14 +4989,14 @@ } }, "node_modules/@zwave-js/testing": { - "version": "12.12.0", - "resolved": "https://registry.npmjs.org/@zwave-js/testing/-/testing-12.12.0.tgz", - "integrity": "sha512-8++C4cnumn0PImCuIUIbrsX85vP1piD5k8KRcyVNCMJ6DB+8CHJjSVp9mWsGs6H8X9XPkmhTcwtz9vuyai22yQ==", - "dependencies": { - "@zwave-js/core": "12.12.0", - "@zwave-js/host": "12.12.0", - "@zwave-js/serial": "12.12.0", - "@zwave-js/shared": "12.11.1", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@zwave-js/testing/-/testing-13.0.0-beta.1.tgz", + "integrity": "sha512-Xo2TdBx9gCgqcFVi1wWJ/gkrtdFzgpJj+PwpTz0OmDRERdKP6X+mrMv0lwm7y/BNKXd4T3M0pv/FE+PAwqJqjg==", + "dependencies": { + "@zwave-js/core": "13.0.0-beta.1", + "@zwave-js/host": "13.0.0-beta.1", + "@zwave-js/serial": "13.0.0-beta.1", + "@zwave-js/shared": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8", "ansi-colors": "^4.1.3" }, @@ -20508,21 +20507,21 @@ } }, "node_modules/zwave-js": { - "version": "12.12.1", - "resolved": "https://registry.npmjs.org/zwave-js/-/zwave-js-12.12.1.tgz", - "integrity": "sha512-ajNor247PXrGKBQALlaWUWKmMd5dcTvUfExsAzTfRULvqtYb3AQFdgkufzmQ+mXgO/1Bt+x+B/GXUV73X9Sq3A==", + "version": "13.0.0-beta.1", + "resolved": "https://registry.npmjs.org/zwave-js/-/zwave-js-13.0.0-beta.1.tgz", + "integrity": "sha512-JA3eJF6ckxUt1s6Fm7t1pYyKR9av1WscxdbJ+m6qvtWv3yOzG3Fq7BB0dFJ3JWk8kisMiSjv6LdXVnGqOxQ2qA==", "dependencies": { "@alcalzone/jsonl-db": "^3.1.1", - "@alcalzone/pak": "^0.10.1", + "@alcalzone/pak": "^0.11.0", "@homebridge/ciao": "^1.2.0", - "@zwave-js/cc": "12.12.1", - "@zwave-js/config": "12.12.0", - "@zwave-js/core": "12.12.0", - "@zwave-js/host": "12.12.0", - "@zwave-js/nvmedit": "12.12.0", - "@zwave-js/serial": "12.12.0", - "@zwave-js/shared": "12.11.1", - "@zwave-js/testing": "12.12.0", + "@zwave-js/cc": "13.0.0-beta.1", + "@zwave-js/config": "13.0.0-beta.1", + "@zwave-js/core": "13.0.0-beta.1", + "@zwave-js/host": "13.0.0-beta.1", + "@zwave-js/nvmedit": "13.0.0-beta.1", + "@zwave-js/serial": "13.0.0-beta.1", + "@zwave-js/shared": "13.0.0-beta.1", + "@zwave-js/testing": "13.0.0-beta.1", "alcalzone-shared": "^4.0.8", "ansi-colors": "^4.1.3", "execa": "^5.1.1", diff --git a/package.json b/package.json index 4efb2fad3ec..4b337c0a88f 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@jamescoyle/vue-icon": "^0.1.2", "@kvaster/zwavejs-prom": "^0.0.2", "@mdi/js": "7.4.47", - "@zwave-js/server": "^1.36.0", + "@zwave-js/server": "github:zwave-js/zwave-js-server#zwave-js-13", "@zwave-js/winston-daily-rotate-file": "^4.5.6-1", "ansi_up": "^6.0.2", "archiver": "^7.0.1", @@ -110,7 +110,7 @@ "vuedraggable": "^2.24.3", "vuetify": "^2.7.2", "winston": "^3.13.0", - "zwave-js": "^12.12.1" + "zwave-js": "^13.0.0-beta.1" }, "devDependencies": { "@actions/github": "^6.0.0", From c75187976323ea2984b4a4c7805dc32428887688 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 8 Jul 2024 11:23:22 +0200 Subject: [PATCH 2/9] fix: compatibility with zwave-js v13 --- api/app.ts | 23 +++++++++-------------- api/lib/Constants.ts | 18 ++++++------------ api/lib/Gateway.ts | 1 - api/lib/ZwaveClient.ts | 26 ++++++++++++-------------- 4 files changed, 27 insertions(+), 41 deletions(-) diff --git a/api/app.ts b/api/app.ts index eb56c53c126..a3f077c2521 100644 --- a/api/app.ts +++ b/api/app.ts @@ -9,12 +9,7 @@ import jsonStore from './lib/jsonStore' import * as loggers from './lib/logger' import MqttClient from './lib/MqttClient' import SocketManager from './lib/SocketManager' -import ZWaveClient, { - CallAPIResult, - configManager, - loadManager, - SensorTypeScale, -} from './lib/ZwaveClient' +import ZWaveClient, { CallAPIResult, SensorTypeScale } from './lib/ZwaveClient' import multer, { diskStorage } from 'multer' import extract from 'extract-zip' import { serverVersion } from '@zwave-js/server' @@ -49,6 +44,7 @@ import backupManager from './lib/BackupManager' import { readFile, realpath } from 'fs/promises' import { generate } from 'selfsigned' import ZnifferManager, { ZnifferConfig } from './lib/ZnifferManager' +import { getAllNamedScaleGroups, getAllSensors } from '@zwave-js/core' const createCertificate = promisify(generate) @@ -258,7 +254,6 @@ export async function startServer(port: number | string, host?: string) { setupSocket(server) setupInterceptor() await loadSnippets() - await loadManager() startZniffer(settings.zniffer) await startGateway(settings) } @@ -1061,15 +1056,15 @@ app.get( apisLimiter, isAuthenticated, async function (req, res) { - const sensorTypes = configManager.sensorTypes - const sensorScalesGroups = configManager.namedScales + const allSensors = getAllSensors() + const namedScaleGroups = getAllNamedScaleGroups() const scales: SensorTypeScale[] = [] - for (const [key, group] of sensorScalesGroups) { - for (const [, scale] of group) { + for (const group of namedScaleGroups) { + for (const scale of Object.values(group.scales)) { scales.push({ - key: key, + key: group.name, sensor: group.name, unit: scale.unit, label: scale.label, @@ -1078,8 +1073,8 @@ app.get( } } - for (const [, sensor] of sensorTypes) { - for (const [, scale] of sensor.scales) { + for (const sensor of allSensors) { + for (const scale of Object.values(sensor.scales)) { scales.push({ key: sensor.key, sensor: sensor.label, diff --git a/api/lib/Constants.ts b/api/lib/Constants.ts index 13c26ee3a04..a5555960966 100644 --- a/api/lib/Constants.ts +++ b/api/lib/Constants.ts @@ -1,4 +1,4 @@ -import { ConfigManager } from '@zwave-js/config' +import { getMeter, getMeterScale } from '@zwave-js/core' interface IGenericMap { [key: number]: string @@ -113,19 +113,13 @@ export function productionType(index: number): Record { }, } } -export function meterType( - ccSpecific: IMeterCCSpecific, - configManager: ConfigManager, -): any { - const meter = configManager.lookupMeter(ccSpecific.meterType) - const scale = configManager.lookupMeterScale( - ccSpecific.meterType, - ccSpecific.scale, - ) +export function meterType(ccSpecific: IMeterCCSpecific): any { + const meter = getMeter(ccSpecific.meterType) + const scale = getMeterScale(ccSpecific.meterType, ccSpecific.scale) const cfg = { - sensor: meter ? meter.name : 'unknown', - objectId: scale ? scale.label : `unknown${ccSpecific.scale}`, + sensor: meter?.name || 'unknown', + objectId: scale?.label || `unknown${ccSpecific.scale}`, props: {}, } diff --git a/api/lib/Gateway.ts b/api/lib/Gateway.ts index 197f81c20d3..32afa9b6c06 100644 --- a/api/lib/Gateway.ts +++ b/api/lib/Gateway.ts @@ -1566,7 +1566,6 @@ export default class Gateway { if (valueId.ccSpecific) { sensor = Constants.meterType( valueId.ccSpecific as IMeterCCSpecific, - this._zwave.driver.configManager, ) sensor.objectId += '_' + valueId.property diff --git a/api/lib/ZwaveClient.ts b/api/lib/ZwaveClient.ts index 70e0c4c790b..eb5fb9f0f6c 100644 --- a/api/lib/ZwaveClient.ts +++ b/api/lib/ZwaveClient.ts @@ -102,6 +102,7 @@ import { InclusionUserCallbacks, InclusionState, ProvisioningEntryStatus, + AssociationCheckResult, } from 'zwave-js' import { getEnumMemberName, parseQRCodeString } from 'zwave-js/Utils' import { logsDir, nvmBackupsDir, storeDir } from '../config/app' @@ -127,11 +128,6 @@ export const configManager = new ConfigManager({ deviceConfigPriorityDir, }) -export async function loadManager() { - await configManager.loadNamedScales() - await configManager.loadSensorTypes() -} - const logger = LogManager.module('Z-Wave') // eslint-disable-next-line @typescript-eslint/no-var-requires const loglevels = require('triple-beam').configs.npm.levels @@ -1778,13 +1774,13 @@ class ZwaveClient extends TypedEventEmitter { if (zwaveNode) { try { for (const a of associations) { - if ( - this._driver.controller.isAssociationAllowed( + const checkResult = + this._driver.controller.checkAssociation( source, groupId, a, ) - ) { + if (checkResult === AssociationCheckResult.OK) { this.logNode( zwaveNode, 'info', @@ -1802,7 +1798,8 @@ class ZwaveClient extends TypedEventEmitter { this.logNode( zwaveNode, 'warn', - `Unable to add Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}, association not allowed`, + // FIXME: We should explain the reason why it failed in human-readable form, see doc comments of AssociationCheckResult + `Unable to add Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}: ${getEnumMemberName(AssociationCheckResult, checkResult)}`, ) } } @@ -4293,8 +4290,7 @@ class ZwaveClient extends TypedEventEmitter { if (!this._controllerListenersAdded) { this._controllerListenersAdded = true this.driver.controller - .on( - 'inclusion started', + .on('inclusion started', (e) => this._onInclusionStarted.bind(this), ) .on( @@ -4602,9 +4598,11 @@ class ZwaveClient extends TypedEventEmitter { } } - private _onInclusionStarted(secure: boolean) { + private _onInclusionStarted(strategy: InclusionStrategy) { + const secure = strategy !== InclusionStrategy.Insecure const message = `${secure ? 'Secure' : 'Non-secure'} inclusion started` this._updateControllerStatus(message) + // FIXME: Should the frontend also accept the strategy instead of the boolean? this.emit('event', EventSource.CONTROLLER, 'inclusion started', secure) } @@ -5571,7 +5569,7 @@ class ZwaveClient extends TypedEventEmitter { this.logNode( endpoint.nodeId, 'error', - `Notification received but node doesn't exists`, + `Notification received but node doesn't exist`, ) return @@ -5985,7 +5983,7 @@ class ZwaveClient extends TypedEventEmitter { node.keepAwake = zwaveNode.keepAwake node.maxDataRate = zwaveNode.maxDataRate node.deviceClass = { - basic: zwaveNode.deviceClass?.basic.key, + basic: zwaveNode.deviceClass?.basic, generic: zwaveNode.deviceClass?.generic.key, specific: zwaveNode.deviceClass?.specific.key, } From 80c1cd86edf77c6b375ae2046e7d506d52f483d3 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 8 Jul 2024 11:25:58 +0200 Subject: [PATCH 3/9] fix: typo --- api/lib/ZwaveClient.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/lib/ZwaveClient.ts b/api/lib/ZwaveClient.ts index eb5fb9f0f6c..07a2c5499e4 100644 --- a/api/lib/ZwaveClient.ts +++ b/api/lib/ZwaveClient.ts @@ -4290,7 +4290,8 @@ class ZwaveClient extends TypedEventEmitter { if (!this._controllerListenersAdded) { this._controllerListenersAdded = true this.driver.controller - .on('inclusion started', (e) => + .on( + 'inclusion started', this._onInclusionStarted.bind(this), ) .on( From 4a9ca51930417ef3c7e9f043a0957c90d4982756 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Mon, 8 Jul 2024 13:52:18 +0200 Subject: [PATCH 4/9] fix: return association result on addAssociation --- api/lib/ZwaveClient.ts | 66 ++++++++----------- .../nodes-table/AssociationGroups.vue | 10 ++- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/api/lib/ZwaveClient.ts b/api/lib/ZwaveClient.ts index 07a2c5499e4..bbd5dd9a945 100644 --- a/api/lib/ZwaveClient.ts +++ b/api/lib/ZwaveClient.ts @@ -1771,54 +1771,41 @@ class ZwaveClient extends TypedEventEmitter { (source.endpoint ? ' Endpoint ' + source.endpoint : '') }` - if (zwaveNode) { - try { - for (const a of associations) { - const checkResult = - this._driver.controller.checkAssociation( - source, - groupId, - a, - ) - if (checkResult === AssociationCheckResult.OK) { - this.logNode( - zwaveNode, - 'info', - `Adding Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}`, - ) + if (!zwaveNode) { + throw new Error(`Node ${source.nodeId} not found`) + } - await this._driver.controller.addAssociations( - source, - groupId, - [a], - ) + const result: AssociationCheckResult[] = [] - return true - } else { - this.logNode( - zwaveNode, - 'warn', - // FIXME: We should explain the reason why it failed in human-readable form, see doc comments of AssociationCheckResult - `Unable to add Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}: ${getEnumMemberName(AssociationCheckResult, checkResult)}`, - ) - } - } - } catch (error) { + for (const a of associations) { + const checkResult = this._driver.controller.checkAssociation( + source, + groupId, + a, + ) + + result.push(checkResult) + + if (checkResult === AssociationCheckResult.OK) { + this.logNode( + zwaveNode, + 'info', + `Adding Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}`, + ) + + await this._driver.controller.addAssociations(source, groupId, [ + a, + ]) + } else { this.logNode( zwaveNode, 'warn', - `Error while adding associations to ${sourceMsg}: ${error.message}`, + `Unable to add Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}: ${getEnumMemberName(AssociationCheckResult, checkResult)}`, ) } - } else { - this.logNode( - zwaveNode, - 'warn', - `Error while adding associations to ${sourceMsg}, node not found`, - ) } - return false + return result } /** @@ -4603,7 +4590,6 @@ class ZwaveClient extends TypedEventEmitter { const secure = strategy !== InclusionStrategy.Insecure const message = `${secure ? 'Secure' : 'Non-secure'} inclusion started` this._updateControllerStatus(message) - // FIXME: Should the frontend also accept the strategy instead of the boolean? this.emit('event', EventSource.CONTROLLER, 'inclusion started', secure) } diff --git a/src/components/nodes-table/AssociationGroups.vue b/src/components/nodes-table/AssociationGroups.vue index 3089d38ab16..0440289ad81 100644 --- a/src/components/nodes-table/AssociationGroups.vue +++ b/src/components/nodes-table/AssociationGroups.vue @@ -79,6 +79,7 @@ import { mapState, mapActions } from 'pinia' import useBaseStore from '../../stores/base.js' import InstancesMixin from '../../mixins/InstancesMixin.js' +import { AssociationCheckResult, getEnumMemberName } from 'zwave-js/safe' export default { components: { @@ -179,11 +180,16 @@ export default { const response = await this.app.apiRequest('addAssociations', args) if (response.success) { - if (response.result) { + const checkResult = response.result[0] + + if (checkResult === AssociationCheckResult.OK) { this.showSnackbar('Association added', 'success') this.getAssociations() } else { - this.showSnackbar('Error while adding association', 'error') + this.showSnackbar( + `Error while adding association: ${getEnumMemberName(AssociationCheckResult, checkResult)}`, + 'error', + ) } } this.dialogAssociation = false From b818e7c1384056f4fd70ae7d7bc4cade032eeb0c Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Mon, 8 Jul 2024 14:58:54 +0200 Subject: [PATCH 5/9] fix(ui): `AssociationCheckResult` import --- src/components/nodes-table/AssociationGroups.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/nodes-table/AssociationGroups.vue b/src/components/nodes-table/AssociationGroups.vue index 0440289ad81..f2328a36330 100644 --- a/src/components/nodes-table/AssociationGroups.vue +++ b/src/components/nodes-table/AssociationGroups.vue @@ -79,7 +79,8 @@ import { mapState, mapActions } from 'pinia' import useBaseStore from '../../stores/base.js' import InstancesMixin from '../../mixins/InstancesMixin.js' -import { AssociationCheckResult, getEnumMemberName } from 'zwave-js/safe' +import { getEnumMemberName } from 'zwave-js/safe' +import { AssociationCheckResult } from '@zwave-js/cc/safe' export default { components: { From 405585d73b92f300bd1ef7bbfca3c202f8f1804c Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 9 Jul 2024 15:01:03 +0200 Subject: [PATCH 6/9] feat: validate association before calling api Fixes #3801 --- api/lib/ZwaveClient.ts | 16 ++++ src/components/dialogs/DialogAssociation.vue | 81 +++++++++++++++++++ .../nodes-table/AssociationGroups.vue | 8 +- src/lib/utils.js | 7 ++ 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/api/lib/ZwaveClient.ts b/api/lib/ZwaveClient.ts index bbd5dd9a945..cfbce1cd2e9 100644 --- a/api/lib/ZwaveClient.ts +++ b/api/lib/ZwaveClient.ts @@ -157,6 +157,7 @@ export const allowedApis = validateMethods([ 'getNodeNeighbors', 'discoverNodeNeighbors', 'getAssociations', + 'checkAssociation', 'addAssociations', 'removeAssociations', 'removeAllAssociations', @@ -1756,6 +1757,21 @@ class ZwaveClient extends TypedEventEmitter { return toReturn } + /** + * Check if a given association is allowed + */ + checkAssociation( + source: AssociationAddress, + groupId: number, + association: AssociationAddress, + ) { + return this.driver.controller.checkAssociation( + source, + groupId, + association, + ) + } + /** * Add a node to the array of specified [associations](https://zwave-js.github.io/node-zwave-js/#/api/controller?id=association-interface) */ diff --git a/src/components/dialogs/DialogAssociation.vue b/src/components/dialogs/DialogAssociation.vue index 68f8736fefd..d55315163be 100644 --- a/src/components/dialogs/DialogAssociation.vue +++ b/src/components/dialogs/DialogAssociation.vue @@ -94,6 +94,17 @@ :items="targetEndpoints" > + + + + {{ associationError }} + + @@ -121,8 +132,13 @@ import { Protocols } from '@zwave-js/core/safe' import { mapState } from 'pinia' import useBaseStore from '../../stores/base.js' +import { getAssociationAddress } from '../../lib/utils' +import { AssociationCheckResult } from '@zwave-js/cc/safe' +import { getEnumMemberName } from 'zwave-js/safe' +import InstancesMixin from '../../mixins/InstancesMixin.js' export default { + mixins: [InstancesMixin], props: { value: Boolean, associations: Array, @@ -132,6 +148,15 @@ export default { value() { this.$refs.form && this.$refs.form.resetValidation() this.resetGroup() + this.associationError = '' + }, + group: { + deep: true, + handler() { + if (this.$refs.form?.validate()) { + this.allowedAssociation() + } + }, }, }, computed: { @@ -185,11 +210,67 @@ export default { return { valid: true, group: {}, + associationError: '', defaultGroup: { endpoint: null }, required: (v) => !!v || 'This field is required', } }, methods: { + getAssociationAddress, + async allowedAssociation() { + const association = this.group + const target = !isNaN(association.target) + ? parseInt(association.target) + : association.target?.id + + if (isNaN(target)) { + this.associationError = '' + return + } + + const group = association.group + + if (!group) { + this.associationError = '' + return + } + + const toAdd = { nodeId: target } + + if (group.multiChannel && association.targetEndpoint >= 0) { + toAdd.endpoint = association.targetEndpoint + } + + const args = [ + this.getAssociationAddress({ + nodeId: this.node.id, + endpoint: association.endpoint, + }), + group.value, + toAdd, + ] + + const response = await this.app.apiRequest( + 'checkAssociation', + args, + { + showInfo: false, + }, + ) + + if (response.success) { + const checkResult = response.result + + if (checkResult === AssociationCheckResult.OK) { + this.associationError = '' + } else { + this.associationError = `Association not allowed: ${getEnumMemberName( + AssociationCheckResult, + checkResult, + )}` + } + } + }, resetGroup() { this.group = Object.assign({}, this.defaultGroup) }, diff --git a/src/components/nodes-table/AssociationGroups.vue b/src/components/nodes-table/AssociationGroups.vue index f2328a36330..f36b87875d5 100644 --- a/src/components/nodes-table/AssociationGroups.vue +++ b/src/components/nodes-table/AssociationGroups.vue @@ -81,6 +81,7 @@ import useBaseStore from '../../stores/base.js' import InstancesMixin from '../../mixins/InstancesMixin.js' import { getEnumMemberName } from 'zwave-js/safe' import { AssociationCheckResult } from '@zwave-js/cc/safe' +import { getAssociationAddress } from '../../lib/utils' export default { components: { @@ -112,12 +113,7 @@ export default { }, methods: { ...mapActions(useBaseStore, ['showSnackbar']), - getAssociationAddress(ass) { - return { - nodeId: ass.nodeId, - endpoint: ass.endpoint === null ? undefined : ass.endpoint, - } - }, + getAssociationAddress, getNodeName(nodeId) { const node = this.nodes[this.nodesMap.get(nodeId)] return node ? node._name : 'NodeID_' + nodeId diff --git a/src/lib/utils.js b/src/lib/utils.js index 2bfa0b21bbd..a639345207f 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -329,3 +329,10 @@ export function getProtocolIcon(protocol) { description: getProtocol({ protocol }), } } + +export function getAssociationAddress(ass) { + return { + nodeId: ass.nodeId, + endpoint: ass.endpoint === null ? undefined : ass.endpoint, + } +} From ac8b1c4596a81b15e1b7f1fe0d1def9fd94ca3e6 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 9 Jul 2024 15:55:32 +0200 Subject: [PATCH 7/9] feat: explain why association is not allowed --- src/components/dialogs/DialogAssociation.vue | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/dialogs/DialogAssociation.vue b/src/components/dialogs/DialogAssociation.vue index d55315163be..30f5c970f36 100644 --- a/src/components/dialogs/DialogAssociation.vue +++ b/src/components/dialogs/DialogAssociation.vue @@ -263,7 +263,23 @@ export default { if (checkResult === AssociationCheckResult.OK) { this.associationError = '' + } else if ( + checkResult === + AssociationCheckResult.Forbidden_SecurityClassMismatch + ) { + this.associationError = `Association not allowed: Node ${this.node.id} does not have the same security class as Node ${target}!` + } else if ( + checkResult === + AssociationCheckResult.Forbidden_DestinationSecurityClassNotGranted + ) { + this.associationError = `Association not allowed: Node ${this.node.id} was not granted the highest security class of Node ${target}!` + } else if ( + checkResult === + AssociationCheckResult.Forbidden_NoSupportedCCs + ) { + this.associationError = `Association not allowed: Node ${this.node.id} sends no commands in this group that Node ${target} supports!` } else { + // This should not happen, but just in case this.associationError = `Association not allowed: ${getEnumMemberName( AssociationCheckResult, checkResult, From 5d107f772c7440503108b92082afb2783b923125 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 9 Jul 2024 17:30:07 +0200 Subject: [PATCH 8/9] fix: do not make alert dismissable and disable add when association not allowed --- src/components/dialogs/DialogAssociation.vue | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/dialogs/DialogAssociation.vue b/src/components/dialogs/DialogAssociation.vue index 30f5c970f36..c333f1736fb 100644 --- a/src/components/dialogs/DialogAssociation.vue +++ b/src/components/dialogs/DialogAssociation.vue @@ -96,12 +96,7 @@ - + {{ associationError }} @@ -119,7 +114,7 @@ ADD From c7b6420d97a1b891fd1b82c4791f686de6ecff4d Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 9 Jul 2024 22:23:24 +0200 Subject: [PATCH 9/9] fix: toLogEntry no longer needs a Zniffer instance --- api/lib/ZnifferManager.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/lib/ZnifferManager.ts b/api/lib/ZnifferManager.ts index 14b33275539..20a847d3061 100644 --- a/api/lib/ZnifferManager.ts +++ b/api/lib/ZnifferManager.ts @@ -215,9 +215,7 @@ export default class ZnifferManager extends TypedEventEmitter { try { - const parsed: Record = commandClass.toLogEntry( - this.zniffer as any, - ) + const parsed: Record = commandClass.toLogEntry() if (isEncapsulatingCommandClass(commandClass)) { parsed.encapsulated = [