From cd8a79d19345c318785694b1e4c461052bf00af6 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Wed, 27 Jan 2021 23:55:00 +0100 Subject: [PATCH] feat: allow and handle response timeouts for GET-type CC commands (#1505) --- packages/zwave-js/src/lib/commandclass/API.ts | 24 -- .../src/lib/commandclass/AlarmSensorCC.ts | 106 ++++---- .../src/lib/commandclass/AssociationCC.ts | 58 ++-- .../commandclass/AssociationGroupInfoCC.ts | 150 +++++------ .../zwave-js/src/lib/commandclass/BasicCC.ts | 73 +++-- .../src/lib/commandclass/BatteryCC.ts | 146 +++++----- .../src/lib/commandclass/BinarySensorCC.ts | 124 ++++----- .../src/lib/commandclass/BinarySwitchCC.ts | 47 ++-- .../src/lib/commandclass/CentralSceneCC.ts | 69 ++--- .../commandclass/ClimateControlScheduleCC.ts | 32 ++- .../zwave-js/src/lib/commandclass/ClockCC.ts | 38 +-- .../src/lib/commandclass/ColorSwitchCC.ts | 64 +++-- .../src/lib/commandclass/ConfigurationCC.ts | 252 +++++++++--------- .../src/lib/commandclass/DoorLockCC.ts | 245 +++++++++-------- .../commandclass/FirmwareUpdateMetaDataCC.ts | 28 +- .../src/lib/commandclass/IndicatorCC.ts | 109 ++++---- .../src/lib/commandclass/LanguageCC.ts | 31 ++- .../zwave-js/src/lib/commandclass/LockCC.ts | 8 +- .../commandclass/ManufacturerProprietaryCC.ts | 13 +- .../commandclass/ManufacturerSpecificCC.ts | 40 +-- .../zwave-js/src/lib/commandclass/MeterCC.ts | 205 ++++++-------- .../commandclass/MultiChannelAssociationCC.ts | 55 ++-- .../src/lib/commandclass/MultiChannelCC.ts | 179 +++++++------ .../lib/commandclass/MultilevelSensorCC.ts | 238 ++++++++--------- .../lib/commandclass/MultilevelSwitchCC.ts | 40 +-- .../src/lib/commandclass/NodeNamingCC.ts | 16 +- .../src/lib/commandclass/NotificationCC.ts | 128 +++++---- .../src/lib/commandclass/ProtectionCC.ts | 123 +++++---- .../src/lib/commandclass/SecurityCC.ts | 55 ++-- .../src/lib/commandclass/SoundSwitchCC.ts | 115 ++++---- .../src/lib/commandclass/ThermostatModeCC.ts | 77 +++--- .../ThermostatOperatingStateCC.ts | 27 +- .../lib/commandclass/ThermostatSetbackCC.ts | 27 +- .../lib/commandclass/ThermostatSetpointCC.ts | 181 ++++++------- .../zwave-js/src/lib/commandclass/TimeCC.ts | 45 ++-- .../src/lib/commandclass/TimeParametersCC.ts | 8 +- .../src/lib/commandclass/UserCodeCC.ts | 132 +++++---- .../src/lib/commandclass/VersionCC.ts | 247 ++++++++--------- .../zwave-js/src/lib/commandclass/WakeUpCC.ts | 118 ++++---- .../src/lib/commandclass/ZWavePlusCC.ts | 37 +-- .../manufacturerProprietary/Fibaro.ts | 23 +- packages/zwave-js/src/lib/driver/Driver.ts | 40 ++- .../src/lib/driver/SendThreadMachine.ts | 2 + packages/zwave-js/src/lib/node/Node.ts | 23 +- test/run.ts | 20 +- 45 files changed, 1875 insertions(+), 1943 deletions(-) diff --git a/packages/zwave-js/src/lib/commandclass/API.ts b/packages/zwave-js/src/lib/commandclass/API.ts index 535da1aeffa7..cf809e2e76b7 100644 --- a/packages/zwave-js/src/lib/commandclass/API.ts +++ b/packages/zwave-js/src/lib/commandclass/API.ts @@ -236,30 +236,6 @@ export class PhysicalCCAPI extends CCAPI { protected declare readonly endpoint: Endpoint; } -/** - * Executes the given action and ignores any node timeout errors - * Returns whether the execution was successful (`true`) or timed out (`false`) - */ -export async function ignoreTimeout( - action: () => Promise, - onTimeout?: () => void, -): Promise { - try { - await action(); - return true; - } catch (e: unknown) { - if ( - e instanceof ZWaveError && - e.code === ZWaveErrorCodes.Controller_NodeTimeout - ) { - onTimeout?.(); - return false; - } - // We don't want to swallow any other errors - throw e; - } -} - // This interface is auto-generated by maintenance/generateCCAPIInterface.ts // Do not edit it by hand or your changes will be lost export interface CCAPIs { diff --git a/packages/zwave-js/src/lib/commandclass/AlarmSensorCC.ts b/packages/zwave-js/src/lib/commandclass/AlarmSensorCC.ts index 60b112e3580e..3a0661684016 100644 --- a/packages/zwave-js/src/lib/commandclass/AlarmSensorCC.ts +++ b/packages/zwave-js/src/lib/commandclass/AlarmSensorCC.ts @@ -13,7 +13,7 @@ import { import { getEnumMemberName, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; -import { ignoreTimeout, PhysicalCCAPI } from "./API"; +import { PhysicalCCAPI } from "./API"; import { API, CCCommand, @@ -125,11 +125,11 @@ export class AlarmSensorCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, sensorType, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, ["state", "severity", "duration"]); + ); + if (response) return pick(response, ["state", "severity", "duration"]); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -143,11 +143,11 @@ export class AlarmSensorCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.supportedSensorTypes; + ); + if (response) return response.supportedSensorTypes; } } @@ -186,25 +186,23 @@ export class AlarmSensorCC extends CommandClass { // Find out which sensor types this sensor supports let supportedSensorTypes: readonly AlarmSensorType[] | undefined; if (complete) { - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying supported sensor types...", - direction: "outbound", - }); - supportedSensorTypes = await api.getSupportedSensorTypes(); - const logMessage = `received supported sensor types: ${supportedSensorTypes - .map((type) => getEnumMemberName(AlarmSensorType, type)) - .map((name) => `\n· ${name}`) - .join("")}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying supported sensor types...", + direction: "outbound", + }); + supportedSensorTypes = await api.getSupportedSensorTypes(); + if (supportedSensorTypes) { + const logMessage = `received supported sensor types: ${supportedSensorTypes + .map((type) => getEnumMemberName(AlarmSensorType, type)) + .map((name) => `\n· ${name}`) + .join("")}`; + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: @@ -221,43 +219,31 @@ export class AlarmSensorCC extends CommandClass { } // Always query (all of) the sensor's current value(s) - if (supportedSensorTypes) { - for (const type of supportedSensorTypes) { - const sensorName = getEnumMemberName(AlarmSensorType, type); - - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying current value for ${sensorName}...`, - direction: "outbound", - }); - const currentValue = await api.get(type); - let message = `received current value for ${sensorName}: + for (const type of supportedSensorTypes) { + const sensorName = getEnumMemberName(AlarmSensorType, type); + + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying current value for ${sensorName}...`, + direction: "outbound", + }); + const currentValue = await api.get(type); + if (currentValue) { + let message = `received current value for ${sensorName}: state: ${currentValue.state}`; - if (currentValue.severity != undefined) { - message += ` + if (currentValue.severity != undefined) { + message += ` severity: ${currentValue.severity}`; - } - if (currentValue.duration != undefined) { - message += ` + } + if (currentValue.duration != undefined) { + message += ` duration: ${currentValue.duration}`; - } - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Current value query timed out - skipping because it is not critical...", - level: "warn", - }); - }, - ); + } + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message, + direction: "inbound", + }); } } diff --git a/packages/zwave-js/src/lib/commandclass/AssociationCC.ts b/packages/zwave-js/src/lib/commandclass/AssociationCC.ts index b28ca44bd5ed..5db80fb699bf 100644 --- a/packages/zwave-js/src/lib/commandclass/AssociationCC.ts +++ b/packages/zwave-js/src/lib/commandclass/AssociationCC.ts @@ -128,7 +128,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { * Returns the number of association groups a node supports. * Association groups are consecutive, starting at 1. */ - public async getGroupCount(): Promise { + public async getGroupCount(): Promise { this.assertSupportsCommand( AssociationCommand, AssociationCommand.SupportedGroupingsGet, @@ -138,11 +138,11 @@ export class AssociationCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.groupCount; + ); + if (response) return response.groupCount; } /** @@ -157,14 +157,16 @@ export class AssociationCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, groupId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - maxNodes: response.maxNodes, - nodeIds: response.nodeIds, - }; + ); + if (response) { + return { + maxNodes: response.maxNodes, + nodeIds: response.nodeIds, + }; + } } /** @@ -310,7 +312,7 @@ export class AssociationCC extends CommandClass { // Even if Multi Channel Association is supported, we still need to query the number of // normal association groups since some devices report more association groups than // multi channel association groups - let groupCount: number; + let groupCount: number | undefined; if (complete) { // First find out how many groups are supported this.driver.controllerLog.logNode(node.id, { @@ -319,11 +321,21 @@ export class AssociationCC extends CommandClass { direction: "outbound", }); groupCount = await api.getGroupCount(); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `supports ${groupCount} association groups`, - direction: "inbound", - }); + if (groupCount != undefined) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `supports ${groupCount} association groups`, + direction: "inbound", + }); + } else { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying association groups timed out, skipping interview...", + level: "warn", + }); + return; + } } else { // Partial interview, read the information from cache groupCount = this.getGroupCountCached(); @@ -350,14 +362,16 @@ export class AssociationCC extends CommandClass { direction: "outbound", }); const group = await api.getGroup(groupId); - const logMessage = `received information for association group #${groupId}: + if (group != undefined) { + const logMessage = `received information for association group #${groupId}: maximum # of nodes: ${group.maxNodes} currently assigned nodes: ${group.nodeIds.map(String).join(", ")}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } } // Assign the controller to all lifeline groups diff --git a/packages/zwave-js/src/lib/commandclass/AssociationGroupInfoCC.ts b/packages/zwave-js/src/lib/commandclass/AssociationGroupInfoCC.ts index e3d4c89e2293..ae79ee6a28ab 100644 --- a/packages/zwave-js/src/lib/commandclass/AssociationGroupInfoCC.ts +++ b/packages/zwave-js/src/lib/commandclass/AssociationGroupInfoCC.ts @@ -13,7 +13,7 @@ import { import { getEnumMemberName, num2hex } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; -import { ignoreTimeout, PhysicalCCAPI } from "./API"; +import { PhysicalCCAPI } from "./API"; import type { AssociationCC } from "./AssociationCC"; import { API, @@ -285,7 +285,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { return super.supportsCommand(cmd); } - public async getGroupName(groupId: number): Promise { + public async getGroupName(groupId: number): Promise { this.assertSupportsCommand( AssociationGroupInfoCommand, AssociationGroupInfoCommand.NameGet, @@ -296,11 +296,11 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, groupId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.name; + ); + if (response) return response.name; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -316,22 +316,26 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { groupId, refreshCache, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - // SDS13782: If List Mode is set to 0, the Group Count field MUST be set to 1. - const { groupId: _, ...info } = response.groups[0]; - return { - hasDynamicInfo: response.hasDynamicInfo, - ...info, - }; + ); + if (response) { + // SDS13782: If List Mode is set to 0, the Group Count field MUST be set to 1. + const { groupId: _, ...info } = response.groups[0]; + return { + hasDynamicInfo: response.hasDynamicInfo, + ...info, + }; + } } public async getCommands( groupId: number, allowCache: boolean = true, - ): Promise { + ): Promise< + AssociationGroupInfoCCCommandListReport["commands"] | undefined + > { this.assertSupportsCommand( AssociationGroupInfoCommand, AssociationGroupInfoCommand.CommandListGet, @@ -343,11 +347,11 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { groupId, allowCache, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.commands; + ); + if (response) return response.commands; } } @@ -460,30 +464,21 @@ export class AssociationGroupInfoCC extends CommandClass { for (let groupId = 1; groupId <= associationGroupCount; groupId++) { if (complete) { - await ignoreTimeout( - async () => { - // First get the group's name - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Association group #${groupId}: Querying name...`, - direction: "outbound", - }); - const name = await api.getGroupName(groupId); - const logMessage = `Association group #${groupId} has name "${name}"`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Querying name of association group #${groupId} timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + // First get the group's name + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `Association group #${groupId}: Querying name...`, + direction: "outbound", + }); + const name = await api.getGroupName(groupId); + if (name) { + const logMessage = `Association group #${groupId} has name "${name}"`; + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } } // Even if this is a partial interview, we need to refresh information @@ -497,59 +492,36 @@ export class AssociationGroupInfoCC extends CommandClass { } if (complete || hasDynamicInfo) { - await ignoreTimeout( - async () => { - // Then its information - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Association group #${groupId}: Querying info...`, - direction: "outbound", - }); - const info = await api.getGroupInfo( - groupId, - !!hasDynamicInfo, - ); - const logMessage = `Received info for association group #${groupId}: + // Then its information + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `Association group #${groupId}: Querying info...`, + direction: "outbound", + }); + const info = await api.getGroupInfo(groupId, !!hasDynamicInfo); + if (info) { + const logMessage = `Received info for association group #${groupId}: info is dynamic: ${info.hasDynamicInfo} profile: ${getEnumMemberName( - AssociationGroupInfoProfile, - info.profile, - )}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Querying info for association group #${groupId} timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + AssociationGroupInfoProfile, + info.profile, + )}`; + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } } if (complete) { - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Association group #${groupId}: Querying command list...`, - direction: "outbound", - }); - await api.getCommands(groupId); - // Not sure how to log this - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Querying command list for association group #${groupId} timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `Association group #${groupId}: Querying command list...`, + direction: "outbound", + }); + await api.getCommands(groupId); + // Not sure how to log this } } diff --git a/packages/zwave-js/src/lib/commandclass/BasicCC.ts b/packages/zwave-js/src/lib/commandclass/BasicCC.ts index 6c8efd1a7fc0..c350e990c6c9 100644 --- a/packages/zwave-js/src/lib/commandclass/BasicCC.ts +++ b/packages/zwave-js/src/lib/commandclass/BasicCC.ts @@ -10,12 +10,11 @@ import { ValueID, ValueMetadata, } from "@zwave-js/core"; -import type { AllOrNone } from "@zwave-js/shared"; +import { AllOrNone, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -113,15 +112,13 @@ export class BasicCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - currentValue: response.currentValue, - targetValue: response.targetValue, - duration: response.duration, - }; + ); + if (response) { + return pick(response, ["currentValue", "targetValue", "duration"]); + } } private refreshTimeout: NodeJS.Timeout | undefined; @@ -181,41 +178,37 @@ export class BasicCC extends CommandClass { direction: "none", }); - // try to query the current state - the node might not respond to BasicGet - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying Basic CC state...", - direction: "outbound", - }); - - const basicResponse = await api.get(); + // try to query the current state + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying Basic CC state...", + direction: "outbound", + }); - let logMessage = `received Basic CC state: + const basicResponse = await api.get(); + if (basicResponse) { + let logMessage = `received Basic CC state: current value: ${basicResponse.currentValue}`; - if (basicResponse.targetValue != undefined) { - logMessage += ` + if (basicResponse.targetValue != undefined) { + logMessage += ` target value: ${basicResponse.targetValue} remaining duration: ${basicResponse.duration?.toString() ?? "undefined"}`; - } - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "No response to Basic Get command, assuming the node does not support Basic CC...", - }); - // SDS14223: A controlling node MUST conclude that the Basic Command Class is not supported by a node (or - // endpoint) if no Basic Report is returned. - endpoint.removeCC(CommandClasses.Basic); - }, - ); + } + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "No response to Basic Get command, assuming the node does not support Basic CC...", + }); + // SDS14223: A controlling node MUST conclude that the Basic Command Class is not supported by a node (or + // endpoint) if no Basic Report is returned. + endpoint.removeCC(CommandClasses.Basic); + } // create compat event value if necessary if (node.deviceConfig?.compat?.treatBasicSetAsEvent) { diff --git a/packages/zwave-js/src/lib/commandclass/BatteryCC.ts b/packages/zwave-js/src/lib/commandclass/BatteryCC.ts index 44ddbd0ae688..82c0714d52c1 100644 --- a/packages/zwave-js/src/lib/commandclass/BatteryCC.ts +++ b/packages/zwave-js/src/lib/commandclass/BatteryCC.ts @@ -11,11 +11,10 @@ import { validatePayload, ValueMetadata, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { - ignoreTimeout, PhysicalCCAPI, PollValueImplementation, POLL_VALUE, @@ -105,22 +104,24 @@ export class BatteryCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - level: response.level, - isLow: response.isLow, - chargingStatus: response.chargingStatus, - rechargeable: response.rechargeable, - backup: response.backup, - overheating: response.overheating, - lowFluid: response.lowFluid, - rechargeOrReplace: response.rechargeOrReplace, - lowTemperatureStatus: response.lowTemperatureStatus, - disconnected: response.disconnected, - }; + ); + if (response) { + return pick(response, [ + "level", + "isLow", + "chargingStatus", + "rechargeable", + "backup", + "overheating", + "lowFluid", + "rechargeOrReplace", + "lowTemperatureStatus", + "disconnected", + ]); + } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -131,14 +132,13 @@ export class BatteryCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - maximumCapacity: response.maximumCapacity, - temperature: response.temperature, - }; + ); + if (response) { + return pick(response, ["maximumCapacity", "temperature"]); + } } } @@ -162,84 +162,60 @@ export class BatteryCC extends CommandClass { direction: "none", }); - await ignoreTimeout( - async () => { - // always query the status - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying battery status...", - direction: "outbound", - }); - - const batteryStatus = await api.get(); + // always query the status + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying battery status...", + direction: "outbound", + }); - let logMessage = `received response for battery information: + const batteryStatus = await api.get(); + if (batteryStatus) { + let logMessage = `received response for battery information: level: ${batteryStatus.level}${ - batteryStatus.isLow ? " (low)" : "" - }`; - if (this.version >= 2) { - logMessage += ` + batteryStatus.isLow ? " (low)" : "" + }`; + if (this.version >= 2) { + logMessage += ` status: ${ - BatteryChargingStatus[batteryStatus.chargingStatus!] - } + BatteryChargingStatus[batteryStatus.chargingStatus!] + } rechargeable: ${batteryStatus.rechargeable} is backup: ${batteryStatus.backup} is overheating: ${batteryStatus.overheating} fluid is low: ${batteryStatus.lowFluid} needs to be replaced or charged: ${ - BatteryReplacementStatus[ - batteryStatus.rechargeOrReplace! - ] - } + BatteryReplacementStatus[batteryStatus.rechargeOrReplace!] + } is low temperature ${batteryStatus.lowTemperatureStatus} is disconnected: ${batteryStatus.disconnected}`; - } + } + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } + + if (this.version >= 2) { + // always query the health + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying battery health...", + direction: "outbound", + }); + + const batteryHealth = await api.getHealth(); + if (batteryHealth) { + const logMessage = `received response for battery health: +max. capacity: ${batteryHealth.maximumCapacity} % +temperature: ${batteryHealth.temperature} °C`; this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: logMessage, direction: "inbound", }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Battery status query timed out - skipping because it is not critical...", - level: "warn", - }); - }, - ); - - if (this.version >= 2) { - await ignoreTimeout( - async () => { - // always query the health - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying battery health...", - direction: "outbound", - }); - - const batteryHealth = await api.getHealth(); - - const logMessage = `received response for battery health: -max. capacity: ${batteryHealth.maximumCapacity} % -temperature: ${batteryHealth.temperature} °C`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Battery health query timed out - skipping because it is not critical...", - level: "warn", - }); - }, - ); + } } // Remember that the interview is complete diff --git a/packages/zwave-js/src/lib/commandclass/BinarySensorCC.ts b/packages/zwave-js/src/lib/commandclass/BinarySensorCC.ts index b7909b3afdc1..b1d9cabce7a7 100644 --- a/packages/zwave-js/src/lib/commandclass/BinarySensorCC.ts +++ b/packages/zwave-js/src/lib/commandclass/BinarySensorCC.ts @@ -13,7 +13,6 @@ import { getEnumMemberName } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { - ignoreTimeout, PhysicalCCAPI, PollValueImplementation, POLL_VALUE, @@ -118,7 +117,9 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { * Retrieves the current value from this sensor * @param sensorType The (optional) sensor type to retrieve the value for */ - public async get(sensorType?: BinarySensorType): Promise { + public async get( + sensorType?: BinarySensorType, + ): Promise { this.assertSupportsCommand( BinarySensorCommand, BinarySensorCommand.Get, @@ -129,12 +130,12 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, sensorType, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; + ); // We don't want to repeat the sensor type - return response.value; + return response?.value; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -148,12 +149,12 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; + ); // We don't want to repeat the sensor type - return response.supportedSensorTypes; + return response?.supportedSensorTypes; } } @@ -180,27 +181,23 @@ export class BinarySensorCC extends CommandClass { // Find out which sensor types this sensor supports let supportedSensorTypes: readonly BinarySensorType[] | undefined; if (complete && this.version >= 2) { - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying supported sensor types...", - direction: "outbound", - }); - supportedSensorTypes = await api.getSupportedSensorTypes(); - const logMessage = `received supported sensor types: ${supportedSensorTypes - .map((type) => - getEnumMemberName(BinarySensorType, type), - ) - .map((name) => `\n· ${name}`) - .join("")}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying supported sensor types...", + direction: "outbound", + }); + supportedSensorTypes = await api.getSupportedSensorTypes(); + if (supportedSensorTypes) { + const logMessage = `received supported sensor types: ${supportedSensorTypes + .map((type) => getEnumMemberName(BinarySensorType, type)) + .map((name) => `\n· ${name}`) + .join("")}`; + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: @@ -217,54 +214,35 @@ export class BinarySensorCC extends CommandClass { // Always query (all of) the sensor's current value(s) if (this.version === 1) { - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying current value...", - direction: "outbound", - }); - const currentValue = await api.get(); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying current value...", + direction: "outbound", + }); + const currentValue = await api.get(); + if (currentValue != undefined) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `received current value: ${currentValue}`, + direction: "inbound", + }); + } + } else if (supportedSensorTypes) { + for (const type of supportedSensorTypes) { + const sensorName = getEnumMemberName(BinarySensorType, type); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying current value for ${sensorName}...`, + direction: "outbound", + }); + const currentValue = await api.get(type); + if (currentValue != undefined) { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: `received current value: ${currentValue}`, + message: `received current value for ${sensorName}: ${currentValue}`, direction: "inbound", }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Current value query timed out - skipping because it is not critical...", - level: "warn", - }); - }, - ); - } else if (supportedSensorTypes) { - for (const type of supportedSensorTypes) { - const sensorName = getEnumMemberName(BinarySensorType, type); - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying current value for ${sensorName}...`, - direction: "outbound", - }); - const currentValue = await api.get(type); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `received current value for ${sensorName}: ${currentValue}`, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Current value query for ${sensorName} timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + } } } diff --git a/packages/zwave-js/src/lib/commandclass/BinarySwitchCC.ts b/packages/zwave-js/src/lib/commandclass/BinarySwitchCC.ts index 1a626c65b346..0b5f6f87578a 100644 --- a/packages/zwave-js/src/lib/commandclass/BinarySwitchCC.ts +++ b/packages/zwave-js/src/lib/commandclass/BinarySwitchCC.ts @@ -75,16 +75,18 @@ export class BinarySwitchCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - // interpret unknown values as false - currentValue: response.currentValue || false, - targetValue: response.targetValue, - duration: response.duration, - }; + ); + if (response) { + return { + // interpret unknown values as false + currentValue: response.currentValue || false, + targetValue: response.targetValue, + duration: response.duration, + }; + } } private refreshTimeout: NodeJS.Timeout | undefined; @@ -186,22 +188,21 @@ export class BinarySwitchCC extends CommandClass { direction: "outbound", }); - const binarySwitchResponse = await api.get(); - - let logMessage = `received Binary Switch state: -current value: ${binarySwitchResponse.currentValue}`; - if (binarySwitchResponse.targetValue != undefined) { - logMessage += ` -target value: ${binarySwitchResponse.targetValue} -remaining duration: ${ - binarySwitchResponse.duration?.toString() ?? "undefined" - }`; + const resp = await api.get(); + if (resp) { + let logMessage = `received Binary Switch state: +current value: ${resp.currentValue}`; + if (resp.targetValue != undefined) { + logMessage += ` +target value: ${resp.targetValue} +remaining duration: ${resp.duration?.toString() ?? "undefined"}`; + } + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); } - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); // Remember that the interview is complete this.interviewComplete = true; diff --git a/packages/zwave-js/src/lib/commandclass/CentralSceneCC.ts b/packages/zwave-js/src/lib/commandclass/CentralSceneCC.ts index 0e5446e70c39..a4bbace0b8d6 100644 --- a/packages/zwave-js/src/lib/commandclass/CentralSceneCC.ts +++ b/packages/zwave-js/src/lib/commandclass/CentralSceneCC.ts @@ -13,13 +13,12 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; import { padStart } from "alcalzone-shared/strings"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -102,15 +101,17 @@ export class CentralSceneCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - sceneCount: response.sceneCount, - supportsSlowRefresh: response.supportsSlowRefresh, - supportedKeyAttributes: response.supportedKeyAttributes, - }; + ); + if (response) { + return pick(response, [ + "sceneCount", + "supportsSlowRefresh", + "supportedKeyAttributes", + ]); + } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -124,13 +125,13 @@ export class CentralSceneCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - slowRefresh: response.slowRefresh, - }; + ); + if (response) { + return pick(response, ["slowRefresh"]); + } } public async setConfiguration(slowRefresh: boolean): Promise { @@ -256,34 +257,22 @@ export class CentralSceneCC extends CommandClass { } } - let ccSupported: - | { - sceneCount: number; - supportsSlowRefresh: boolean; - supportedKeyAttributes: ReadonlyMap< - number, - readonly CentralSceneKeys[] - >; - } - | undefined; - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "Querying supported scenes...", - direction: "outbound", - }); - ccSupported = await api.getSupported(); - const logMessage = `received supported scenes: + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "Querying supported scenes...", + direction: "outbound", + }); + const ccSupported = await api.getSupported(); + if (ccSupported) { + const logMessage = `received supported scenes: # of scenes: ${ccSupported.sceneCount} supports slow refresh: ${ccSupported.supportsSlowRefresh}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: diff --git a/packages/zwave-js/src/lib/commandclass/ClimateControlScheduleCC.ts b/packages/zwave-js/src/lib/commandclass/ClimateControlScheduleCC.ts index 740deb8d2fe2..99ad7a72a49b 100644 --- a/packages/zwave-js/src/lib/commandclass/ClimateControlScheduleCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ClimateControlScheduleCC.ts @@ -92,7 +92,9 @@ export class ClimateControlScheduleCCAPI extends CCAPI { } } - public async get(weekday: Weekday): Promise { + public async get( + weekday: Weekday, + ): Promise { this.assertSupportsCommand( ClimateControlScheduleCommand, ClimateControlScheduleCommand.Get, @@ -103,14 +105,14 @@ export class ClimateControlScheduleCCAPI extends CCAPI { endpoint: this.endpoint.index, weekday, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.switchPoints; + ); + return response?.switchPoints; } - public async getChangeCounter(): Promise { + public async getChangeCounter(): Promise { this.assertSupportsCommand( ClimateControlScheduleCommand, ClimateControlScheduleCommand.ChangedGet, @@ -120,11 +122,11 @@ export class ClimateControlScheduleCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.changeCounter; + ); + return response?.changeCounter; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -138,14 +140,16 @@ export class ClimateControlScheduleCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - type: response.overrideType, - state: response.overrideState, - }; + ); + if (response) { + return { + type: response.overrideType, + state: response.overrideState, + }; + } } public async setOverride( diff --git a/packages/zwave-js/src/lib/commandclass/ClockCC.ts b/packages/zwave-js/src/lib/commandclass/ClockCC.ts index de2585d23e2e..d2e1263c7355 100644 --- a/packages/zwave-js/src/lib/commandclass/ClockCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ClockCC.ts @@ -5,7 +5,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; import { padStart } from "alcalzone-shared/strings"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; @@ -65,15 +65,13 @@ export class ClockCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - weekday: response.weekday, - hour: response.hour, - minute: response.minute, - }; + ); + if (response) { + return pick(response, ["weekday", "hour", "minute"]); + } } public async set( @@ -123,17 +121,19 @@ export class ClockCC extends CommandClass { direction: "outbound", }); const response = await api.get(); - const logMessage = `received current clock setting: ${ - response.weekday !== Weekday.Unknown - ? Weekday[response.weekday] + ", " - : "" - }${response.hour < 10 ? "0" : ""}${response.hour}:${ - response.minute < 10 ? "0" : "" - }${response.minute}`; - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); + if (response) { + const logMessage = `received current clock setting: ${ + response.weekday !== Weekday.Unknown + ? Weekday[response.weekday] + ", " + : "" + }${response.hour < 10 ? "0" : ""}${response.hour}:${ + response.minute < 10 ? "0" : "" + }${response.minute}`; + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); + } // Remember that the interview is complete this.interviewComplete = true; diff --git a/packages/zwave-js/src/lib/commandclass/ColorSwitchCC.ts b/packages/zwave-js/src/lib/commandclass/ColorSwitchCC.ts index 6f93864f1e37..d48b786b31fa 100644 --- a/packages/zwave-js/src/lib/commandclass/ColorSwitchCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ColorSwitchCC.ts @@ -20,7 +20,6 @@ import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -177,7 +176,9 @@ export class ColorSwitchCCAPI extends CCAPI { return super.supportsCommand(cmd); } - public async getSupported(): Promise { + public async getSupported(): Promise< + readonly ColorComponent[] | undefined + > { this.assertSupportsCommand( ColorSwitchCommand, ColorSwitchCommand.SupportedGet, @@ -187,11 +188,11 @@ export class ColorSwitchCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.supportedColorComponents; + ); + return response?.supportedColorComponents; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -203,15 +204,13 @@ export class ColorSwitchCCAPI extends CCAPI { endpoint: this.endpoint.index, colorComponent: component, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - currentValue: response.currentValue, - targetValue: response.targetValue, - duration: response.duration, - }; + ); + if (response) { + return pick(response, ["currentValue", "targetValue", "duration"]); + } } public async set(options: ColorSwitchCCSetOptions): Promise { @@ -400,14 +399,24 @@ export class ColorSwitchCC extends CommandClass { direction: "none", }); - let supportedColors: readonly ColorComponent[]; + let supportedColors: readonly ColorComponent[] | undefined; if (complete) { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: "querying Color Switch CC supported colors...", + message: "querying supported colors...", direction: "outbound", }); supportedColors = await api.getSupported(); + if (!supportedColors) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying supported colors timed out, skipping interview...", + level: "warn", + }); + return; + } + this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: `received supported colors:${supportedColors @@ -441,7 +450,7 @@ export class ColorSwitchCC extends CommandClass { ColorComponent.Red, ColorComponent.Green, ColorComponent.Blue, - ].every((c) => supportedColors.includes(c)); + ].every((c) => supportedColors!.includes(c)); valueDB.setValue( getSupportsHexColorValueID(this.endpointIndex), supportsHex, @@ -461,24 +470,13 @@ export class ColorSwitchCC extends CommandClass { } for (const color of supportedColors) { - await ignoreTimeout( - async () => { - const colorName = getEnumMemberName(ColorComponent, color); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying current color state (${colorName})`, - direction: "outbound", - }); - await api.get(color); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Current color query timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + const colorName = getEnumMemberName(ColorComponent, color); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying current color state (${colorName})`, + direction: "outbound", + }); + await api.get(color); } // Remember that the interview is complete diff --git a/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts b/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts index 205273761847..68e259665c87 100644 --- a/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ConfigurationCC.ts @@ -22,13 +22,13 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; +import { distinct } from "alcalzone-shared/arrays"; import { composeObject } from "alcalzone-shared/objects"; import { padStart } from "alcalzone-shared/strings"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { - ignoreTimeout, PhysicalCCAPI, PollValueImplementation, POLL_VALUE, @@ -268,56 +268,43 @@ export class ConfigurationCCAPI extends PhysicalCCAPI { parameter, allowUnexpectedResponse, }); - try { - const response = (await this.driver.sendCommand( - cc, - this.commandOptions, - ))!; - // Nodes may respond with a different parameter, e.g. if we - // requested a non-existing one - if (response.parameter === parameter) { - if (!valueBitMask) return response.value; - // If a partial parameter was requested, extract that value - return ( - ((response.value as any) & valueBitMask) >>> - getMinimumShiftForBitMask(valueBitMask) - ); - } - this.driver.controllerLog.logNode(this.endpoint.nodeId, { - message: `Received unexpected ConfigurationReport (param = ${ - response.parameter - }, value = ${response.value.toString()})`, - direction: "inbound", - level: "error", - }); - throw new ConfigurationCCError( - `The first existing parameter on this node is ${response.parameter}`, - ZWaveErrorCodes.ConfigurationCC_FirstParameterNumber, - response.parameter, + const response = await this.driver.sendCommand( + cc, + this.commandOptions, + ); + if (!response) return; + // Nodes may respond with a different parameter, e.g. if we + // requested a non-existing one + if (response.parameter === parameter) { + if (!valueBitMask) return response.value; + // If a partial parameter was requested, extract that value + return ( + ((response.value as any) & valueBitMask) >>> + getMinimumShiftForBitMask(valueBitMask) ); - } catch (e) { - if ( - e instanceof ZWaveError && - e.code === ZWaveErrorCodes.Controller_NodeTimeout - ) { - // A timeout has to be expected. We return undefined to - // signal that no value was received - return undefined; - } - // This error was unexpected - throw e; } + this.driver.controllerLog.logNode(this.endpoint.nodeId, { + message: `Received unexpected ConfigurationReport (param = ${ + response.parameter + }, value = ${response.value.toString()})`, + direction: "inbound", + level: "error", + }); + throw new ConfigurationCCError( + `The first existing parameter on this node is ${response.parameter}`, + ZWaveErrorCodes.ConfigurationCC_FirstParameterNumber, + response.parameter, + ); } /** * Sets a new value for a given config parameter of the device. - * The return value indicates whether the command succeeded (`true`) or timed out (`false`). */ public async set( parameter: number, value: ConfigValue, valueSize: 1 | 2 | 4, - ): Promise { + ): Promise { this.assertSupportsCommand( ConfigurationCommand, ConfigurationCommand.Set, @@ -330,19 +317,15 @@ export class ConfigurationCCAPI extends PhysicalCCAPI { value, valueSize, }); - // A timeout has to be expected, don't throw in that case - return ignoreTimeout(async () => { - await this.driver.sendCommand(cc, this.commandOptions); - }); + await this.driver.sendCommand(cc, this.commandOptions); } /** * Resets a configuration parameter to its default value. - * The return value indicates whether the command succeeded (`true`) or timed out (`false`). * * WARNING: This will throw on legacy devices (ConfigurationCC v3 and below) */ - public async reset(parameter: number): Promise { + public async reset(parameter: number): Promise { this.assertSupportsCommand( ConfigurationCommand, ConfigurationCommand.Set, @@ -354,10 +337,7 @@ export class ConfigurationCCAPI extends PhysicalCCAPI { parameter, resetToDefault: true, }); - // A timeout has to be expected, don't throw in that case - return ignoreTimeout(async () => { - await this.driver.sendCommand(cc, this.commandOptions); - }); + await this.driver.sendCommand(cc, this.commandOptions); } /** Resets all configuration parameters to their default value */ @@ -381,50 +361,52 @@ export class ConfigurationCCAPI extends PhysicalCCAPI { // Don't set an endpoint here, Configuration is device specific, not endpoint specific parameter, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - valueSize: response.valueSize, - valueFormat: response.valueFormat, - minValue: response.minValue, - maxValue: response.maxValue, - defaultValue: response.defaultValue, - nextParameter: response.nextParameter, - altersCapabilities: response.altersCapabilities, - isReadonly: response.isReadonly, - isAdvanced: response.isAdvanced, - noBulkSupport: response.noBulkSupport, - }; + ); + if (response) { + return pick(response, [ + "valueSize", + "valueFormat", + "minValue", + "maxValue", + "defaultValue", + "nextParameter", + "altersCapabilities", + "isReadonly", + "isAdvanced", + "noBulkSupport", + ]); + } } /** Requests the name of a configuration parameter from the node */ - public async getName(parameter: number): Promise { + public async getName(parameter: number): Promise { const cc = new ConfigurationCCNameGet(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( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.name; + ); + return response?.name; } /** Requests usage info for a configuration parameter from the node */ - public async getInfo(parameter: number): Promise { + public async getInfo(parameter: number): Promise { const cc = new ConfigurationCCInfoGet(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( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.info; + ); + return response?.info; } /** @@ -574,53 +556,69 @@ export class ConfigurationCC extends CommandClass { direction: "none", }); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "finding first configuration parameter...", - direction: "outbound", - }); - let { nextParameter: param } = await api.getProperties(0); - if (param === 0) { + if (complete) { + // Only scan all parameters during complete interviews this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: `didn't report any config params, trying #1 just to be sure...`, - direction: "inbound", - }); - param = 1; - } - - while (param > 0) { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying parameter #${param} information...`, + message: "finding first configuration parameter...", direction: "outbound", }); - - // Query properties and the next param - const { - nextParameter, - ...properties - } = await api.getProperties(param); - - let logMessage: string; - if (properties.valueSize === 0) { - logMessage = `Parameter #${param} is unsupported. Next parameter: ${nextParameter}`; + const param0props = await api.getProperties(0); + let param: number; + if (param0props) { + param = param0props.nextParameter; + if (param === 0) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `didn't report any config params, trying #1 just to be sure...`, + direction: "inbound", + }); + param = 1; + } } else { - // Query name and info only if the parameter is supported - let name = "(unknown)"; - await ignoreTimeout(async () => { - name = await api.getName(param); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Finding first configuration parameter timed out, skipping interview...", + level: "warn", }); - // Skip the info query for bugged devices - if ( - !node.deviceConfig?.compat?.skipConfigurationInfoQuery - ) { - await ignoreTimeout(async () => { - await api.getInfo(param); + return; + } + + while (param > 0) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying parameter #${param} information...`, + direction: "outbound", + }); + + // Query properties and the next param + const props = await api.getProperties(param); + if (!props) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `Querying parameter #${param} information timed out, skipping interview...`, + level: "warn", }); + return; } + const { nextParameter, ...properties } = props; + + let logMessage: string; + if (properties.valueSize === 0) { + logMessage = `Parameter #${param} is unsupported. Next parameter: ${nextParameter}`; + } else { + // Query name and info only if the parameter is supported + const name = (await api.getName(param)) ?? "(unknown)"; + // Skip the info query for bugged devices + if ( + !node.deviceConfig?.compat + ?.skipConfigurationInfoQuery + ) { + await api.getInfo(param); + } - logMessage = `received information for parameter #${param}: + logMessage = `received information for parameter #${param}: parameter name: ${name} value format: ${getEnumMemberName(ValueFormat, properties.valueFormat)} value size: ${properties.valueSize} bytes @@ -631,24 +629,30 @@ is read-only: ${!!properties.isReadonly} is advanced (UI): ${!!properties.isAdvanced} has bulk support: ${!properties.noBulkSupport} alters capabilities: ${!!properties.altersCapabilities}`; - } - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - - // Query the current value if the parameter is supported - if (properties.valueSize !== 0) { + } this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: `querying parameter #${param} value...`, - direction: "outbound", + message: logMessage, + direction: "inbound", }); - await api.get(param); + + param = nextParameter; } + } - param = nextParameter; + // Query the values of supported parameters during every interview + const parameters = distinct( + this.getDefinedValueIDs() + .map((v) => v.property) + .filter((p): p is number => typeof p === "number"), + ); + for (const param of parameters) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying parameter #${param} value...`, + direction: "outbound", + }); + await api.get(param); } } diff --git a/packages/zwave-js/src/lib/commandclass/DoorLockCC.ts b/packages/zwave-js/src/lib/commandclass/DoorLockCC.ts index bb43a34f068c..30267457d130 100644 --- a/packages/zwave-js/src/lib/commandclass/DoorLockCC.ts +++ b/packages/zwave-js/src/lib/commandclass/DoorLockCC.ts @@ -19,7 +19,6 @@ import { isArray } from "alcalzone-shared/typeguards"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { - ignoreTimeout, PhysicalCCAPI, PollValueImplementation, POLL_VALUE, @@ -211,23 +210,25 @@ export class DoorLockCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, [ - "autoRelockSupported", - "blockToBlockSupported", - "boltSupported", - "doorSupported", - "holdAndReleaseSupported", - "latchSupported", - "twistAssistSupported", - "supportedDoorLockModes", - "supportedInsideHandles", - "supportedOperationTypes", - "supportedOutsideHandles", - ]); + ); + if (response) { + return pick(response, [ + "autoRelockSupported", + "blockToBlockSupported", + "boltSupported", + "doorSupported", + "holdAndReleaseSupported", + "latchSupported", + "twistAssistSupported", + "supportedDoorLockModes", + "supportedInsideHandles", + "supportedOperationTypes", + "supportedOutsideHandles", + ]); + } } private refreshTimeout: NodeJS.Timeout | undefined; @@ -243,21 +244,23 @@ export class DoorLockCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, [ - "currentMode", - "targetMode", - "duration", - "outsideHandlesCanOpenDoor", - "insideHandlesCanOpenDoor", - "latchStatus", - "boltStatus", - "doorStatus", - "lockTimeout", - ]); + ); + if (response) { + return pick(response, [ + "currentMode", + "targetMode", + "duration", + "outsideHandlesCanOpenDoor", + "insideHandlesCanOpenDoor", + "latchStatus", + "boltStatus", + "doorStatus", + "lockTimeout", + ]); + } } public async set(mode: DoorLockMode): Promise { @@ -315,20 +318,22 @@ export class DoorLockCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, [ - "operationType", - "outsideHandlesCanOpenDoorConfiguration", - "insideHandlesCanOpenDoorConfiguration", - "lockTimeoutConfiguration", - "autoRelockTime", - "holdAndReleaseTime", - "twistAssist", - "blockToBlock", - ]); + ); + if (response) { + return pick(response, [ + "operationType", + "outsideHandlesCanOpenDoorConfiguration", + "insideHandlesCanOpenDoorConfiguration", + "lockTimeoutConfiguration", + "autoRelockTime", + "holdAndReleaseTime", + "twistAssist", + "blockToBlock", + ]); + } } } @@ -352,6 +357,10 @@ export class DoorLockCC extends CommandClass { direction: "none", }); + // We need to do some queries after a potential timeout + // In this case, do now mark this CC as interviewed completely + let hadCriticalTimeout = false; + if (complete && this.version >= 4) { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, @@ -359,44 +368,51 @@ export class DoorLockCC extends CommandClass { direction: "outbound", }); const resp = await api.getCapabilities(); - const logMessage = `received lock capabilities: + if (resp) { + const logMessage = `received lock capabilities: supported operation types: ${resp.supportedOperationTypes - .map((t) => getEnumMemberName(DoorLockOperationType, t)) - .join(", ")} + .map((t) => getEnumMemberName(DoorLockOperationType, t)) + .join(", ")} supported door lock modes: ${resp.supportedDoorLockModes - .map((t) => getEnumMemberName(DoorLockMode, t)) - .map((str) => `\n· ${str}`) - .join(", ")} + .map((t) => getEnumMemberName(DoorLockMode, t)) + .map((str) => `\n· ${str}`) + .join(", ")} supported outside handles: ${resp.supportedOutsideHandles - .map(String) - .join(", ")} + .map(String) + .join(", ")} supported inside handles: ${resp.supportedInsideHandles.map(String).join(", ")} supports auto-relock: ${resp.autoRelockSupported} supports hold-and-release: ${resp.holdAndReleaseSupported} supports twist assist: ${resp.twistAssistSupported} supports block to block: ${resp.blockToBlockSupported}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); - // Update metadata of settable states - const valueDB = this.getValueDB(); - valueDB.setMetadata(getTargetModeValueId(this.endpointIndex), { - ...ValueMetadata.UInt8, - states: enumValuesToMetadataStates( - DoorLockMode, - resp.supportedDoorLockModes, - ), - }); - valueDB.setMetadata(getOperationTypeValueId(this.endpointIndex), { - ...ValueMetadata.UInt8, - states: enumValuesToMetadataStates( - DoorLockOperationType, - resp.supportedOperationTypes, - ), - }); + // Update metadata of settable states + const valueDB = this.getValueDB(); + valueDB.setMetadata(getTargetModeValueId(this.endpointIndex), { + ...ValueMetadata.UInt8, + states: enumValuesToMetadataStates( + DoorLockMode, + resp.supportedDoorLockModes, + ), + }); + valueDB.setMetadata( + getOperationTypeValueId(this.endpointIndex), + { + ...ValueMetadata.UInt8, + states: enumValuesToMetadataStates( + DoorLockOperationType, + resp.supportedOperationTypes, + ), + }, + ); + } else { + hadCriticalTimeout = true; + } } this.driver.controllerLog.logNode(node.id, { @@ -405,77 +421,70 @@ supports block to block: ${resp.blockToBlockSupported}`; direction: "outbound", }); const config = await api.getConfiguration(); - let logMessage = `received lock configuration: + if (config) { + let logMessage = `received lock configuration: operation type: ${getEnumMemberName( - DoorLockOperationType, - config.operationType, - )}`; - if (config.operationType === DoorLockOperationType.Timed) { - logMessage += ` + DoorLockOperationType, + config.operationType, + )}`; + if (config.operationType === DoorLockOperationType.Timed) { + logMessage += ` lock timeout: ${config.lockTimeoutConfiguration} seconds `; - } - logMessage += ` + } + logMessage += ` outside handles can open door: ${config.outsideHandlesCanOpenDoorConfiguration - .map(String) - .join(", ")} + .map(String) + .join(", ")} inside handles can open door: ${config.insideHandlesCanOpenDoorConfiguration - .map(String) - .join(", ")}`; - if (this.version >= 4) { - logMessage += ` + .map(String) + .join(", ")}`; + if (this.version >= 4) { + logMessage += ` auto-relock time ${config.autoRelockTime ?? "-"} seconds hold-and-release time ${config.holdAndReleaseTime ?? "-"} seconds twist assist ${!!config.twistAssist} block to block ${!!config.blockToBlock}`; + } + + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); } this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", + message: "requesting current lock status...", + direction: "outbound", }); - - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "requesting current lock status...", - direction: "outbound", - }); - const status = await api.get(); - logMessage = `received lock status: + const status = await api.get(); + if (status) { + let logMessage = `received lock status: current mode: ${getEnumMemberName(DoorLockMode, status.currentMode)}`; - if (status.targetMode != undefined) { - logMessage += ` + if (status.targetMode != undefined) { + logMessage += ` target mode: ${getEnumMemberName(DoorLockMode, status.targetMode)} remaining duration: ${status.duration?.toString() ?? "undefined"}`; - } - if (status.lockTimeout != undefined) { - logMessage += ` -lock timeout: ${status.lockTimeout} seconds`; - } + } + if (status.lockTimeout != undefined) { logMessage += ` +lock timeout: ${status.lockTimeout} seconds`; + } + logMessage += ` door status: ${status.doorStatus} bolt status: ${status.boltStatus} latch status: ${status.latchStatus}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Lock status query timed out - skipping because it is not critical...", - }); - }, - ); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } // Remember that the interview is complete - this.interviewComplete = true; + if (!hadCriticalTimeout) this.interviewComplete = true; } } diff --git a/packages/zwave-js/src/lib/commandclass/FirmwareUpdateMetaDataCC.ts b/packages/zwave-js/src/lib/commandclass/FirmwareUpdateMetaDataCC.ts index d1bdff91d181..d05aabeeaec7 100644 --- a/packages/zwave-js/src/lib/commandclass/FirmwareUpdateMetaDataCC.ts +++ b/packages/zwave-js/src/lib/commandclass/FirmwareUpdateMetaDataCC.ts @@ -140,21 +140,23 @@ export class FirmwareUpdateMetaDataCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, [ - "manufacturerId", - "firmwareId", - "checksum", - "firmwareUpgradable", - "maxFragmentSize", - "additionalFirmwareIDs", - "hardwareVersion", - "continuesToFunction", - "supportsActivation", - ]); + ); + if (response) { + return pick(response, [ + "manufacturerId", + "firmwareId", + "checksum", + "firmwareUpgradable", + "maxFragmentSize", + "additionalFirmwareIDs", + "hardwareVersion", + "continuesToFunction", + "supportsActivation", + ]); + } } /** diff --git a/packages/zwave-js/src/lib/commandclass/IndicatorCC.ts b/packages/zwave-js/src/lib/commandclass/IndicatorCC.ts index 41372e972849..2e6f39827bd4 100644 --- a/packages/zwave-js/src/lib/commandclass/IndicatorCC.ts +++ b/packages/zwave-js/src/lib/commandclass/IndicatorCC.ts @@ -18,7 +18,6 @@ import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -250,7 +249,7 @@ export class IndicatorCCAPI extends CCAPI { public async get( indicatorId?: number, - ): Promise { + ): Promise { this.assertSupportsCommand(IndicatorCommand, IndicatorCommand.Get); const cc = new IndicatorCCGet(this.driver, { @@ -258,10 +257,11 @@ export class IndicatorCCAPI extends CCAPI { endpoint: this.endpoint.index, indicatorId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; + ); + if (!response) return; if (response.values) return response.values; return response.value!; } @@ -288,11 +288,14 @@ export class IndicatorCCAPI extends CCAPI { public async getSupported( indicatorId: number, - ): Promise<{ - indicatorId?: number; - supportedProperties: readonly number[]; - nextIndicatorId: number; - }> { + ): Promise< + | { + indicatorId?: number; + supportedProperties: readonly number[]; + nextIndicatorId: number; + } + | undefined + > { this.assertSupportsCommand( IndicatorCommand, IndicatorCommand.SupportedGet, @@ -303,18 +306,20 @@ export class IndicatorCCAPI extends CCAPI { endpoint: this.endpoint.index, indicatorId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - // Include the actual indicator ID if 0x00 was requested - ...(indicatorId === 0x00 - ? { indicatorId: response.indicatorId } - : undefined), - supportedProperties: response.supportedProperties, - nextIndicatorId: response.nextIndicatorId, - }; + ); + if (response) { + return { + // Include the actual indicator ID if 0x00 was requested + ...(indicatorId === 0x00 + ? { indicatorId: response.indicatorId } + : undefined), + supportedProperties: response.supportedProperties, + nextIndicatorId: response.nextIndicatorId, + }; + } } /** @@ -380,24 +385,12 @@ export class IndicatorCC extends CommandClass { }); if (this.version === 1) { - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "requesting current indicator value...", - direction: "outbound", - }); - await api.get(); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Current value query timed out - skipping because it is not critical...", - level: "warn", - }); - }, - ); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "requesting current indicator value...", + direction: "outbound", + }); + await api.get(); } else { let supportedIndicatorIds: number[]; if (complete) { @@ -411,12 +404,23 @@ export class IndicatorCC extends CommandClass { supportedIndicatorIds = []; do { const supportedResponse = await api.getSupported(curId); + if (!supportedResponse) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Time out while scanning supported indicator IDs, skipping interview...", + level: "warn", + }); + return; + } + supportedIndicatorIds.push( supportedResponse.indicatorId ?? curId, ); curId = supportedResponse.nextIndicatorId; } while (curId !== 0x00); - // The IDs are not stored by the report CCs + + // The IDs are not stored by the report CCs so store them here once we have all of them this.getValueDB().setValue( getSupportedIndicatorIDsValueID(this.endpointIndex), supportedIndicatorIds, @@ -437,27 +441,14 @@ export class IndicatorCC extends CommandClass { } for (const indicatorId of supportedIndicatorIds) { - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `requesting current indicator value (id = ${num2hex( - indicatorId, - )})...`, - direction: "outbound", - }); - await api.get(indicatorId); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Querying indicator value (id = ${num2hex( - indicatorId, - )}) timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `requesting current indicator value (id = ${num2hex( + indicatorId, + )})...`, + direction: "outbound", + }); + await api.get(indicatorId); } } diff --git a/packages/zwave-js/src/lib/commandclass/LanguageCC.ts b/packages/zwave-js/src/lib/commandclass/LanguageCC.ts index d039d1fe7f86..8ea6de626471 100644 --- a/packages/zwave-js/src/lib/commandclass/LanguageCC.ts +++ b/packages/zwave-js/src/lib/commandclass/LanguageCC.ts @@ -6,6 +6,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; +import { pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI } from "./API"; @@ -51,14 +52,13 @@ export class LanguageCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - language: response.language, - country: response.country, - }; + ); + if (response) { + return pick(response, ["language", "country"]); + } } public async set(language: string, country?: string): Promise { @@ -102,14 +102,17 @@ export class LanguageCC extends CommandClass { message: "requesting language setting...", direction: "outbound", }); - const { language, country } = await api.get(); - const logMessage = `received current language setting: ${language}${ - country != undefined ? "-" + country : "" - }`; - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); + const resp = await api.get(); + if (resp) { + const { language, country } = resp; + const logMessage = `received current language setting: ${language}${ + country != undefined ? `-${country}` : "" + }`; + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); + } // Remember that the interview is complete this.interviewComplete = true; diff --git a/packages/zwave-js/src/lib/commandclass/LockCC.ts b/packages/zwave-js/src/lib/commandclass/LockCC.ts index c0ac3c745457..d04b6be56179 100644 --- a/packages/zwave-js/src/lib/commandclass/LockCC.ts +++ b/packages/zwave-js/src/lib/commandclass/LockCC.ts @@ -49,18 +49,18 @@ export class LockCCAPI extends PhysicalCCAPI { return super.supportsCommand(cmd); } - public async get(): Promise { + public async get(): Promise { this.assertSupportsCommand(LockCommand, LockCommand.Get); const cc = new LockCCGet(this.driver, { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.locked; + ); + return response?.locked; } /** diff --git a/packages/zwave-js/src/lib/commandclass/ManufacturerProprietaryCC.ts b/packages/zwave-js/src/lib/commandclass/ManufacturerProprietaryCC.ts index bdeaad844e53..7565f4f7ea71 100644 --- a/packages/zwave-js/src/lib/commandclass/ManufacturerProprietaryCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ManufacturerProprietaryCC.ts @@ -4,7 +4,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { staticExtends } from "@zwave-js/shared"; +import { pick, staticExtends } from "@zwave-js/shared"; import { isArray } from "alcalzone-shared/typeguards"; import type { Driver } from "../driver/Driver"; import { @@ -55,14 +55,13 @@ export class ManufacturerProprietaryCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - position: response.position, - tilt: response.tilt, - }; + ); + if (response) { + return pick(response, ["position", "tilt"]); + } } public async fibaroVenetianBlindsSetPosition(value: number): Promise { diff --git a/packages/zwave-js/src/lib/commandclass/ManufacturerSpecificCC.ts b/packages/zwave-js/src/lib/commandclass/ManufacturerSpecificCC.ts index 695a6ca0bf40..0b2742c195e5 100644 --- a/packages/zwave-js/src/lib/commandclass/ManufacturerSpecificCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ManufacturerSpecificCC.ts @@ -6,7 +6,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName, num2hex } from "@zwave-js/shared"; +import { getEnumMemberName, num2hex, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { PhysicalCCAPI } from "./API"; @@ -107,20 +107,22 @@ export class ManufacturerSpecificCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - manufacturerId: response.manufacturerId, - productType: response.productType, - productId: response.productId, - }; + ); + if (response) { + return pick(response, [ + "manufacturerId", + "productType", + "productId", + ]); + } } public async deviceSpecificGet( deviceIdType: DeviceIdType, - ): Promise { + ): Promise { this.assertSupportsCommand( ManufacturerSpecificCommand, ManufacturerSpecificCommand.DeviceSpecificGet, @@ -131,11 +133,11 @@ export class ManufacturerSpecificCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, deviceIdType, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.deviceId; + ); + return response?.deviceId; } } @@ -178,18 +180,20 @@ export class ManufacturerSpecificCC extends CommandClass { direction: "outbound", }); const mfResp = await api.get(); - const logMessage = `received response for manufacturer information: + if (mfResp) { + const logMessage = `received response for manufacturer information: manufacturer: ${ this.driver.configManager.lookupManufacturer(mfResp.manufacturerId) || "unknown" } (${num2hex(mfResp.manufacturerId)}) product type: ${num2hex(mfResp.productType)} product id: ${num2hex(mfResp.productId)}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } } } diff --git a/packages/zwave-js/src/lib/commandclass/MeterCC.ts b/packages/zwave-js/src/lib/commandclass/MeterCC.ts index 2022350eaaaf..5b9931728a0b 100644 --- a/packages/zwave-js/src/lib/commandclass/MeterCC.ts +++ b/packages/zwave-js/src/lib/commandclass/MeterCC.ts @@ -16,11 +16,10 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName, num2hex } from "@zwave-js/shared"; +import { getEnumMemberName, num2hex, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { - ignoreTimeout, PhysicalCCAPI, PollValueImplementation, POLL_VALUE, @@ -200,18 +199,20 @@ export class MeterCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, ...options, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - type: response.type, - scale: response.scale, - value: response.value, - previousValue: response.previousValue, - rateType: response.rateType, - deltaTime: response.deltaTime, - }; + ); + if (response) { + return pick(response, [ + "type", + "scale", + "value", + "previousValue", + "rateType", + "deltaTime", + ]); + } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -235,17 +236,17 @@ export class MeterCCAPI extends PhysicalCCAPI { const ret = []; for (const rateType of rateTypes) { for (const scale of supportedScales) { - ret.push( - await this.get({ - scale, - rateType, - }), - ); + const response = await this.get({ + scale, + rateType, + }); + if (response) ret.push(response); } } return ret; } else { - return [await this.get()]; + const response = await this.get(); + return response ? [response] : []; } } @@ -257,16 +258,18 @@ export class MeterCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - type: response.type, - supportsReset: response.supportsReset, - supportedScales: response.supportedScales, - supportedRateTypes: response.supportedRateTypes, - }; + ); + if (response) { + return pick(response, [ + "type", + "supportsReset", + "supportedScales", + "supportedRateTypes", + ]); + } } public async reset(options: MeterCCResetOptions): Promise { @@ -345,44 +348,42 @@ export class MeterCC extends CommandClass { ); if (complete || storedType == undefined) { - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying meter support...", - direction: "outbound", - }); - - ({ - type, - supportsReset, - supportedScales, - supportedRateTypes, - } = await api.getSupported()); - const logMessage = `received meter support: + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying meter support...", + direction: "outbound", + }); + + const suppResp = await api.getSupported(); + if (suppResp) { + type = suppResp.type; + supportsReset = suppResp.supportsReset; + supportedScales = suppResp.supportedScales; + supportedRateTypes = suppResp.supportedRateTypes; + + const logMessage = `received meter support: type: ${getMeterTypeName(this.driver.configManager, type)} supported scales: ${supportedScales - .map( - (s) => - this.driver.configManager.lookupMeterScale( - type, - s, - ).label, - ) - .map((label) => `\n· ${label}`) - .join("")} + .map( + (s) => + this.driver.configManager.lookupMeterScale( + type, + s, + ).label, + ) + .map((label) => `\n· ${label}`) + .join("")} supported rate types: ${supportedRateTypes - .map((rt) => getEnumMemberName(RateType, rt)) - .map((label) => `\n· ${label}`) - .join("")} + .map((rt) => getEnumMemberName(RateType, rt)) + .map((label) => `\n· ${label}`) + .join("")} supports reset: ${supportsReset}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: @@ -407,60 +408,32 @@ supports reset: ${supportsReset}`; ) ?? []; } - if ( - supportedRateTypes == undefined || - supportedScales == undefined - ) { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Cannot continue meter interview - the information is incomplete!", - level: "warn", - }); - return; - } - const rateTypes = supportedRateTypes.length ? supportedRateTypes : [undefined]; for (const rateType of rateTypes) { for (const scale of supportedScales) { - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying meter value (type = ${getMeterTypeName( - this.driver.configManager, - type, - )}, scale = ${ - this.driver.configManager.lookupMeterScale( - type, - scale, - ).label - }${ - rateType != undefined - ? `, rate type = ${getEnumMemberName( - RateType, - rateType, - )}` - : "" - })...`, - direction: "outbound", - }); - await api.get({ + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying meter value (type = ${getMeterTypeName( + this.driver.configManager, + type, + )}, scale = ${ + this.driver.configManager.lookupMeterScale( + type, scale, - rateType, - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Meter query timed out - skipping because it is not critical...", - level: "warn", - }); - }, - ); + ).label + }${ + rateType != undefined + ? `, rate type = ${getEnumMemberName( + RateType, + rateType, + )}` + : "" + })...`, + direction: "outbound", + }); + await api.get({ scale, rateType }); } } } else { @@ -469,19 +442,7 @@ supports reset: ${supportsReset}`; message: `querying default meter value...`, direction: "outbound", }); - await ignoreTimeout( - async () => { - await api.get(); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Default meter query timed out - skipping because it is not critical...", - level: "warn", - }); - }, - ); + await api.get(); } // Remember that the interview is complete diff --git a/packages/zwave-js/src/lib/commandclass/MultiChannelAssociationCC.ts b/packages/zwave-js/src/lib/commandclass/MultiChannelAssociationCC.ts index 0de4d5a83b9c..120e5b601e31 100644 --- a/packages/zwave-js/src/lib/commandclass/MultiChannelAssociationCC.ts +++ b/packages/zwave-js/src/lib/commandclass/MultiChannelAssociationCC.ts @@ -10,6 +10,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; +import { pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { PhysicalCCAPI } from "./API"; import { @@ -194,7 +195,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { * Returns the number of association groups a node supports. * Association groups are consecutive, starting at 1. */ - public async getGroupCount(): Promise { + public async getGroupCount(): Promise { this.assertSupportsCommand( MultiChannelAssociationCommand, MultiChannelAssociationCommand.SupportedGroupingsGet, @@ -207,11 +208,11 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, }, ); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.groupCount; + ); + return response?.groupCount; } /** @@ -229,15 +230,13 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, groupId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - maxNodes: response.maxNodes, - nodeIds: response.nodeIds, - endpoints: response.endpoints, - }; + ); + if (response) { + return pick(response, ["maxNodes", "nodeIds", "endpoints"]); + } } /** @@ -413,7 +412,7 @@ export class MultiChannelAssociationCC extends CommandClass { direction: "none", }); - let mcGroupCount: number; + let mcGroupCount: number | undefined; if (complete) { // First find out how many groups are supported as multi channel this.driver.controllerLog.logNode(node.id, { @@ -423,11 +422,21 @@ export class MultiChannelAssociationCC extends CommandClass { direction: "outbound", }); mcGroupCount = await mcAPI.getGroupCount(); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `supports ${mcGroupCount} multi channel association groups`, - direction: "inbound", - }); + if (mcGroupCount != undefined) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `supports ${mcGroupCount} multi channel association groups`, + direction: "inbound", + }); + } else { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying multi channel association groups timed out, skipping interview...", + level: "warn", + }); + return; + } } else { // Partial interview, read the information from cache mcGroupCount = @@ -447,6 +456,7 @@ export class MultiChannelAssociationCC extends CommandClass { direction: "outbound", }); const group = await mcAPI.getGroup(groupId); + if (!group) continue; const logMessage = `received information for multi channel association group #${groupId}: maximum # of nodes: ${group.maxNodes} currently assigned nodes: ${group.nodeIds.map(String).join(", ")} @@ -484,6 +494,7 @@ currently assigned endpoints: ${group.endpoints direction: "outbound", }); const group = await assocAPI.getGroup(groupId); + if (!group) continue; const logMessage = `received information for association group #${groupId}: maximum # of nodes: ${group.maxNodes} currently assigned nodes: ${group.nodeIds.map(String).join(", ")}`; @@ -568,8 +579,10 @@ currently assigned nodes: ${group.nodeIds.map(String).join(", ")}`; nodeIds: [ownNodeId], }); // refresh the associations - don't trust that it worked - const { nodeIds } = await mcAPI.getGroup(group); - didMCAssignmentWork = nodeIds.includes(ownNodeId); + const groupReport = await mcAPI.getGroup(group); + didMCAssignmentWork = !!groupReport?.nodeIds.includes( + ownNodeId, + ); } else if ( this.version >= 3 && !mustUseNodeAssociation && @@ -594,8 +607,8 @@ currently assigned nodes: ${group.nodeIds.map(String).join(", ")}`; endpoints: [{ nodeId: ownNodeId, endpoint: 0 }], }); // and refresh the associations - don't trust that it worked - const { endpoints } = await mcAPI.getGroup(group); - didMCAssignmentWork = !!endpoints.find( + const groupReport = await mcAPI.getGroup(group); + didMCAssignmentWork = !!groupReport?.endpoints.some( (a) => a.nodeId === ownNodeId && a.endpoint === 0, ); } diff --git a/packages/zwave-js/src/lib/commandclass/MultiChannelCC.ts b/packages/zwave-js/src/lib/commandclass/MultiChannelCC.ts index 14daabb6893c..d1f7d03116b0 100644 --- a/packages/zwave-js/src/lib/commandclass/MultiChannelCC.ts +++ b/packages/zwave-js/src/lib/commandclass/MultiChannelCC.ts @@ -16,7 +16,7 @@ import { import { num2hex } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; -import { CCAPI, ignoreTimeout } from "./API"; +import { CCAPI } from "./API"; import { API, CCCommand, @@ -123,21 +123,23 @@ export class MultiChannelCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - isDynamicEndpointCount: response.countIsDynamic, - identicalCapabilities: response.identicalCapabilities, - individualEndpointCount: response.individualCount, - aggregatedEndpointCount: response.aggregatedCount, - }; + ); + if (response) { + return { + isDynamicEndpointCount: response.countIsDynamic, + identicalCapabilities: response.identicalCapabilities, + individualEndpointCount: response.individualCount, + aggregatedEndpointCount: response.aggregatedCount, + }; + } } public async getEndpointCapabilities( endpoint: number, - ): Promise { + ): Promise { this.assertSupportsCommand( MultiChannelCommand, MultiChannelCommand.CapabilityGet, @@ -148,17 +150,17 @@ export class MultiChannelCCAPI extends CCAPI { endpoint: this.endpoint.index, requestedEndpoint: endpoint, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.capability; + ); + return response?.capability; } public async findEndpoints( genericClass: number, specificClass: number, - ): Promise { + ): Promise { this.assertSupportsCommand( MultiChannelCommand, MultiChannelCommand.EndPointFind, @@ -170,16 +172,16 @@ export class MultiChannelCCAPI extends CCAPI { genericClass, specificClass, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.foundEndpoints; + ); + return response?.foundEndpoints; } public async getAggregatedMembers( endpoint: number, - ): Promise { + ): Promise { this.assertSupportsCommand( MultiChannelCommand, MultiChannelCommand.AggregatedMembersGet, @@ -190,11 +192,11 @@ export class MultiChannelCCAPI extends CCAPI { endpoint: this.endpoint.index, requestedEndpoint: endpoint, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.members; + ); + return response?.members; } public async sendEncapsulated( @@ -215,7 +217,9 @@ export class MultiChannelCCAPI extends CCAPI { await this.driver.sendCommand(cc, this.commandOptions); } - public async getEndpointCountV1(ccId: CommandClasses): Promise { + public async getEndpointCountV1( + ccId: CommandClasses, + ): Promise { this.assertSupportsCommand( MultiChannelCommand, MultiChannelCommand.GetV1, @@ -225,11 +229,11 @@ export class MultiChannelCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, requestedCC: ccId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.endpointCount; + ); + return response?.endpointCount; } public async sendEncapsulatedV1(encapsulated: CommandClass): Promise { @@ -301,6 +305,15 @@ export class MultiChannelCC extends CommandClass { direction: "outbound", }); const multiResponse = await api.getEndpoints(); + if (!multiResponse) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying device endpoint information timed out, skipping interview...", + level: "warn", + }); + return; + } let logMessage = `received response for device endpoints: endpoint count (individual): ${multiResponse.individualEndpointCount} @@ -329,44 +342,31 @@ identical capabilities: ${multiResponse.identicalCapabilities}`; }; if (api.supportsCommand(MultiChannelCommand.EndPointFind)) { // Step 2a: Find all endpoints - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying all endpoints...", - direction: "outbound", - }); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying all endpoints...", + direction: "outbound", + }); - endpointsToQuery.push( - ...(await api.findEndpoints(0xff, 0xff)), - ); - if (!endpointsToQuery.length) { - // Create a sequential list of endpoints - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Endpoint query returned no results, assuming that endpoints are sequential`, - direction: "inbound", - }); - addSequentialEndpoints(); - } else { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `received endpoints: ${endpointsToQuery - .map(String) - .join(", ")}`, - direction: "inbound", - }); - } - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Did not respond to endpoint query, assuming that endpoints are sequential`, - level: "warn", - }); - addSequentialEndpoints(); - }, - ); + const foundEndpoints = await api.findEndpoints(0xff, 0xff); + if (foundEndpoints) endpointsToQuery.push(...foundEndpoints); + if (!endpointsToQuery.length) { + // Create a sequential list of endpoints + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `Endpoint query returned no results, assuming that endpoints are sequential`, + direction: "inbound", + }); + addSequentialEndpoints(); + } else { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `received endpoints: ${endpointsToQuery + .map(String) + .join(", ")}`, + direction: "inbound", + }); + } } else { // Step 2b: Assume that the endpoints are in sequential order this.driver.controllerLog.logNode(node.id, { @@ -391,13 +391,15 @@ identical capabilities: ${multiResponse.identicalCapabilities}`; direction: "outbound", }); const members = await api.getAggregatedMembers(endpoint); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `aggregated endpoint #${endpoint} has members ${members - .map(String) - .join(", ")}`, - direction: "inbound", - }); + if (members) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `aggregated endpoint #${endpoint} has members ${members + .map(String) + .join(", ")}`, + direction: "inbound", + }); + } } // When the device reports identical capabilities for all endpoints, @@ -420,28 +422,29 @@ identical capabilities: ${multiResponse.identicalCapabilities}`; continue; } - // TODO: When security is implemented, we need to change stuff here this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: `querying capabilities for endpoint #${endpoint}...`, direction: "outbound", }); const caps = await api.getEndpointCapabilities(endpoint); - hasQueriedCapabilities = true; - logMessage = `received response for endpoint capabilities (#${endpoint}): + if (caps) { + hasQueriedCapabilities = true; + logMessage = `received response for endpoint capabilities (#${endpoint}): generic device class: ${caps.generic.label} specific device class: ${caps.specific.label} is dynamic end point: ${caps.isDynamic} supported CCs:`; - for (const cc of caps.supportedCCs) { - const ccName = CommandClasses[cc]; - logMessage += `\n · ${ccName ? ccName : num2hex(cc)}`; + for (const cc of caps.supportedCCs) { + const ccName = CommandClasses[cc]; + logMessage += `\n · ${ccName ? ccName : num2hex(cc)}`; + } + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); } - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); } // Remember that the interview is complete @@ -474,14 +477,16 @@ supported CCs:`; direction: "outbound", }); const endpointCount = await api.getEndpointCountV1(ccId); - endpointCounts.set(ccId, endpointCount); + if (endpointCount != undefined) { + endpointCounts.set(ccId, endpointCount); - this.driver.controllerLog.logNode(node.id, { - message: `CommandClass ${getCCName( - ccId, - )} has ${endpointCount} endpoints`, - direction: "inbound", - }); + this.driver.controllerLog.logNode(node.id, { + message: `CommandClass ${getCCName( + ccId, + )} has ${endpointCount} endpoints`, + direction: "inbound", + }); + } } // Store the collected information diff --git a/packages/zwave-js/src/lib/commandclass/MultilevelSensorCC.ts b/packages/zwave-js/src/lib/commandclass/MultilevelSensorCC.ts index aaf2f0ba2bea..d14ad00ef0f6 100644 --- a/packages/zwave-js/src/lib/commandclass/MultilevelSensorCC.ts +++ b/packages/zwave-js/src/lib/commandclass/MultilevelSensorCC.ts @@ -18,7 +18,6 @@ import { import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { - ignoreTimeout, PhysicalCCAPI, PollValueImplementation, POLL_VALUE, @@ -102,8 +101,13 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { return this.get(sensorType, scale); }; - public async get(): Promise; - public async get(sensorType: number, scale: number): Promise; + public async get(): Promise< + (MultilevelSensorValue & { type: number }) | undefined + >; + public async get( + sensorType: number, + scale: number, + ): Promise; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types public async get(sensorType?: number, scale?: number) { this.assertSupportsCommand( @@ -117,10 +121,11 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { sensorType, scale, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; + ); + if (!response) return; if (sensorType === undefined) { // Overload #1: return the full response @@ -135,7 +140,9 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { } } - public async getSupportedSensorTypes(): Promise { + public async getSupportedSensorTypes(): Promise< + readonly number[] | undefined + > { this.assertSupportsCommand( MultilevelSensorCommand, MultilevelSensorCommand.GetSupportedSensor, @@ -145,16 +152,16 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.supportedSensorTypes; + ); + return response?.supportedSensorTypes; } public async getSupportedScales( sensorType: number, - ): Promise { + ): Promise { this.assertSupportsCommand( MultilevelSensorCommand, MultilevelSensorCommand.GetSupportedScale, @@ -165,11 +172,11 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, sensorType, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.sensorSupportedScales; + ); + return response?.sensorSupportedScales; } public async sendReport( @@ -215,68 +222,54 @@ export class MultilevelSensorCC extends CommandClass { if (this.version <= 4) { // Sensors up to V4 only support a single value - // This is to be requested every time - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying current sensor reading...", - direction: "outbound", - }); - const mlsResponse = await api.get(); - const sensorScale = this.driver.configManager.lookupSensorScale( - mlsResponse.type, - mlsResponse.scale.key, - ); - const logMessage = `received current sensor reading: + // This needs to be requested every time + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying current sensor reading...", + direction: "outbound", + }); + const mlsResponse = await api.get(); + if (mlsResponse) { + const sensorScale = this.driver.configManager.lookupSensorScale( + mlsResponse.type, + mlsResponse.scale.key, + ); + const logMessage = `received current sensor reading: sensor type: ${this.driver.configManager.getSensorTypeName(mlsResponse.type)} value: ${mlsResponse.value} ${sensorScale.unit || ""}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: - "Querying current sensor reading timed out, skipping interview...", - level: "warn", + message: logMessage, + direction: "inbound", }); - return; } } else { // V5+ // If we haven't yet, query the supported sensor types - let sensorTypes: readonly number[]; + let sensorTypes: readonly number[] | undefined; if (complete) { - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "retrieving supported sensor types...", - direction: "outbound", - }); - sensorTypes = await api.getSupportedSensorTypes(); - const logMessage = - "received supported sensor types:\n" + - sensorTypes - .map((t) => - this.driver.configManager.getSensorTypeName( - t, - ), - ) - .map((name) => `· ${name}`) - .join("\n"); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "retrieving supported sensor types...", + direction: "outbound", + }); + sensorTypes = await api.getSupportedSensorTypes(); + if (sensorTypes) { + const logMessage = + "received supported sensor types:\n" + + sensorTypes + .map((t) => + this.driver.configManager.getSensorTypeName(t), + ) + .map((name) => `· ${name}`) + .join("\n"); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: @@ -294,39 +287,37 @@ value: ${mlsResponse.value} ${sensorScale.unit || ""}`; }) || []; } - for (const type of sensorTypes!) { + for (const type of sensorTypes) { // If we haven't yet, query the supported scales for each sensor - let sensorScales: readonly number[]; + let sensorScales: readonly number[] | undefined; if (complete) { - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying supported scales for ${this.driver.configManager.getSensorTypeName( - type, - )} sensor`, - direction: "outbound", - }); - sensorScales = await api.getSupportedScales(type); - const logMessage = - "received supported scales:\n" + - sensorScales - .map( - (s) => - this.driver.configManager.lookupSensorScale( - type, - s, - ).label, - ) - .map((name) => `· ${name}`) - .join("\n"); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying supported scales for ${this.driver.configManager.getSensorTypeName( + type, + )} sensor`, + direction: "outbound", + }); + sensorScales = await api.getSupportedScales(type); + if (sensorScales) { + const logMessage = + "received supported scales:\n" + + sensorScales + .map( + (s) => + this.driver.configManager.lookupSensorScale( + type, + s, + ).label, + ) + .map((name) => `· ${name}`) + .join("\n"); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: @@ -346,40 +337,29 @@ value: ${mlsResponse.value} ${sensorScale.unit || ""}`; } // Always query the current sensor reading - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying ${this.driver.configManager.getSensorTypeName( - type, - )} sensor reading...`, - direction: "outbound", - }); - // TODO: Add some way to select the scale. For now use the first available one - const value = await api.get(type, sensorScales[0]); - const scale = this.driver.configManager.lookupSensorScale( - type, - sensorScales[0], - ); - const logMessage = `received current ${this.driver.configManager.getSensorTypeName( - type, - )} sensor reading: ${value} ${scale.unit || ""}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Query of ${this.driver.configManager.getSensorTypeName( - type, - )} sensor timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying ${this.driver.configManager.getSensorTypeName( + type, + )} sensor reading...`, + direction: "outbound", + }); + // TODO: Add some way to select the scale. For now use the first available one + const value = await api.get(type, sensorScales[0]); + if (value != undefined) { + const scale = this.driver.configManager.lookupSensorScale( + type, + sensorScales[0], + ); + const logMessage = `received current ${this.driver.configManager.getSensorTypeName( + type, + )} sensor reading: ${value} ${scale.unit || ""}`; + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } } } diff --git a/packages/zwave-js/src/lib/commandclass/MultilevelSwitchCC.ts b/packages/zwave-js/src/lib/commandclass/MultilevelSwitchCC.ts index 6aa2bb9de675..481565dfd098 100644 --- a/packages/zwave-js/src/lib/commandclass/MultilevelSwitchCC.ts +++ b/packages/zwave-js/src/lib/commandclass/MultilevelSwitchCC.ts @@ -11,7 +11,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { @@ -143,15 +143,13 @@ export class MultilevelSwitchCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - currentValue: response.currentValue, - targetValue: response.targetValue, - duration: response.duration, - }; + ); + if (response) { + return pick(response, ["currentValue", "targetValue", "duration"]); + } } private refreshTimeout: NodeJS.Timeout | undefined; @@ -309,7 +307,7 @@ export class MultilevelSwitchCCAPI extends CCAPI { } } - public async getSupported(): Promise { + public async getSupported(): Promise { this.assertSupportsCommand( MultilevelSwitchCommand, MultilevelSwitchCommand.SupportedGet, @@ -319,11 +317,11 @@ export class MultilevelSwitchCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.switchType; + ); + return response?.switchType; } protected [SET_VALUE]: SetValueImplementation = async ( @@ -432,14 +430,16 @@ export class MultilevelSwitchCC extends CommandClass { direction: "outbound", }); const switchType = await api.getSupported(); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `has switch type ${getEnumMemberName( - SwitchType, - switchType, - )}`, - direction: "inbound", - }); + if (switchType != undefined) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `has switch type ${getEnumMemberName( + SwitchType, + switchType, + )}`, + direction: "inbound", + }); + } } else { // requesting the switch type automatically creates the up/down actions // We need to do this manually for V1 and V2 diff --git a/packages/zwave-js/src/lib/commandclass/NodeNamingCC.ts b/packages/zwave-js/src/lib/commandclass/NodeNamingCC.ts index ab9d22c05f18..ebedc047d40b 100644 --- a/packages/zwave-js/src/lib/commandclass/NodeNamingCC.ts +++ b/packages/zwave-js/src/lib/commandclass/NodeNamingCC.ts @@ -100,7 +100,7 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { } }; - public async getName(): Promise { + public async getName(): Promise { this.assertSupportsCommand( NodeNamingAndLocationCommand, NodeNamingAndLocationCommand.NameGet, @@ -110,11 +110,11 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.name; + ); + return response?.name; } public async setName(name: string): Promise { @@ -131,7 +131,7 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { await this.driver.sendCommand(cc, this.commandOptions); } - public async getLocation(): Promise { + public async getLocation(): Promise { this.assertSupportsCommand( NodeNamingAndLocationCommand, NodeNamingAndLocationCommand.LocationGet, @@ -141,11 +141,11 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.location; + ); + return response?.location; } public async setLocation(location: string): Promise { diff --git a/packages/zwave-js/src/lib/commandclass/NotificationCC.ts b/packages/zwave-js/src/lib/commandclass/NotificationCC.ts index 846e4c12b52e..fda751fa336a 100644 --- a/packages/zwave-js/src/lib/commandclass/NotificationCC.ts +++ b/packages/zwave-js/src/lib/commandclass/NotificationCC.ts @@ -18,11 +18,11 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { JSONObject, num2hex } from "@zwave-js/shared"; +import { JSONObject, num2hex, pick } from "@zwave-js/shared"; import { isArray } from "alcalzone-shared/typeguards"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; -import { ignoreTimeout, PhysicalCCAPI } from "./API"; +import { PhysicalCCAPI } from "./API"; import { API, CCCommand, @@ -105,7 +105,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { */ public async getInternal( options: NotificationCCGetSpecificOptions, - ): Promise { + ): Promise { this.assertSupportsCommand( NotificationCommand, NotificationCommand.Get, @@ -116,10 +116,10 @@ export class NotificationCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, ...options, }); - return (await this.driver.sendCommand( + return this.driver.sendCommand( cc, this.commandOptions, - ))!; + ); } public async sendReport( @@ -141,14 +141,16 @@ export class NotificationCCAPI extends PhysicalCCAPI { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types public async get(options: NotificationCCGetSpecificOptions) { const response = await this.getInternal(options); - return { - notificationStatus: response.notificationStatus, - notificationEvent: response.notificationEvent, - alarmLevel: response.alarmLevel, - zensorNetSourceNodeId: response.zensorNetSourceNodeId, - eventParameters: response.eventParameters, - sequenceNumber: response.sequenceNumber, - }; + if (response) { + return pick(response, [ + "notificationStatus", + "notificationEvent", + "alarmLevel", + "zensorNetSourceNodeId", + "eventParameters", + "sequenceNumber", + ]); + } } public async set( @@ -180,18 +182,21 @@ export class NotificationCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - supportsV1Alarm: response.supportsV1Alarm, - supportedNotificationTypes: response.supportedNotificationTypes, - }; + ); + if (response) { + return pick(response, [ + "supportsV1Alarm", + "supportedNotificationTypes", + ]); + } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - public async getSupportedEvents(notificationType: number) { + public async getSupportedEvents( + notificationType: number, + ): Promise { this.assertSupportsCommand( NotificationCommand, NotificationCommand.EventSupportedGet, @@ -202,11 +207,11 @@ export class NotificationCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, notificationType, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.supportedEvents; + ); + return response?.supportedEvents; } } @@ -341,11 +346,11 @@ export class NotificationCC extends CommandClass { // Enable the event and request the status await api.set(type, true); try { - const { notificationStatus } = await api.get({ + const resp = await api.get({ notificationType: type, notificationEvent: events[0], }); - switch (notificationStatus) { + switch (resp?.notificationStatus) { case 0xff: return "push"; case 0xfe: @@ -404,7 +409,18 @@ export class NotificationCC extends CommandClass { direction: "outbound", }); - ({ supportedNotificationTypes } = await api.getSupported()); + const suppResponse = await api.getSupported(); + if (!suppResponse) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying supported notification types timed out, skipping interview...", + level: "warn", + }); + return; + } + supportedNotificationTypes = + suppResponse.supportedNotificationTypes; supportedNotificationNames = lookupNotificationNames(); const logMessage = `received supported notification types:${supportedNotificationNames @@ -434,14 +450,19 @@ export class NotificationCC extends CommandClass { const supportedEvents = await api.getSupportedEvents( type, ); - supportedNotificationEvents.set(type, supportedEvents); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `received supported notification events for ${name}: ${supportedEvents - .map(String) - .join(", ")}`, - direction: "inbound", - }); + if (supportedEvents) { + supportedNotificationEvents.set( + type, + supportedEvents, + ); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `received supported notification events for ${name}: ${supportedEvents + .map(String) + .join(", ")}`, + direction: "inbound", + }); + } } } } else { @@ -481,30 +502,19 @@ export class NotificationCC extends CommandClass { const type = supportedNotificationTypes[i]; const name = supportedNotificationNames[i]; - await ignoreTimeout( - async () => { - // Always query each notification for its current status - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying notification status for ${name}...`, - direction: "outbound", - }); - const response = await api.getInternal({ - notificationType: type, - }); - // NotificationReports don't store their values themselves, - // because the behaviour is too complex and spans the lifetime - // of several reports. Thus we handle it in the Node instance - await node.handleCommand(response); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying notification status for ${name} timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + // Always query each notification for its current status + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying notification status for ${name}...`, + direction: "outbound", + }); + const response = await api.getInternal({ + notificationType: type, + }); + // NotificationReports don't store their values themselves, + // because the behaviour is too complex and spans the lifetime + // of several reports. Thus we handle it in the Node instance + if (response) await node.handleCommand(response); } } /* if (notificationMode === "push") */ else { for (let i = 0; i < supportedNotificationTypes.length; i++) { diff --git a/packages/zwave-js/src/lib/commandclass/ProtectionCC.ts b/packages/zwave-js/src/lib/commandclass/ProtectionCC.ts index 4b121ddcb6d4..b9cad77160ea 100644 --- a/packages/zwave-js/src/lib/commandclass/ProtectionCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ProtectionCC.ts @@ -15,7 +15,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; import { padStart } from "alcalzone-shared/strings"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; @@ -225,14 +225,13 @@ export class ProtectionCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - local: response.local, - rf: response.rf, - }; + ); + if (response) { + return pick(response, ["local", "rf"]); + } } public async set( @@ -266,19 +265,21 @@ export class ProtectionCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - supportsExclusiveControl: response.supportsExclusiveControl, - supportsTimeout: response.supportsTimeout, - supportedLocalStates: response.supportedLocalStates, - supportedRFStates: response.supportedRFStates, - }; + ); + if (response) { + return pick(response, [ + "supportsExclusiveControl", + "supportsTimeout", + "supportedLocalStates", + "supportedRFStates", + ]); + } } - public async getExclusiveControl(): Promise { + public async getExclusiveControl(): Promise { this.assertSupportsCommand( ProtectionCommand, ProtectionCommand.ExclusiveControlGet, @@ -288,11 +289,11 @@ export class ProtectionCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.exclusiveControlNodeId; + ); + return response?.exclusiveControlNodeId; } public async setExclusiveControl(nodeId: number): Promise { @@ -314,7 +315,7 @@ export class ProtectionCCAPI extends CCAPI { } } - public async getTimeout(): Promise { + public async getTimeout(): Promise { this.assertSupportsCommand( ProtectionCommand, ProtectionCommand.TimeoutGet, @@ -324,11 +325,11 @@ export class ProtectionCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.timeout; + ); + return response?.timeout; } public async setTimeout(timeout: Timeout): Promise { @@ -372,6 +373,10 @@ export class ProtectionCC extends CommandClass { const valueDB = this.getValueDB(); + // We need to do some queries after a potential timeout + // In this case, do now mark this CC as interviewed completely + let hadCriticalTimeout = false; + // First find out what the device supports if (complete && this.version >= 2) { this.driver.controllerLog.logNode(node.id, { @@ -379,21 +384,27 @@ export class ProtectionCC extends CommandClass { direction: "outbound", }); const resp = await api.getSupported(); - const logMessage = `received protection capabilities: + if (resp) { + const logMessage = `received protection capabilities: exclusive control: ${resp.supportsExclusiveControl} timeout: ${resp.supportsTimeout} local protection states: ${resp.supportedLocalStates - .map((local) => getEnumMemberName(LocalProtectionState, local)) - .map((str) => `\n· ${str}`) - .join("")} + .map((local) => + getEnumMemberName(LocalProtectionState, local), + ) + .map((str) => `\n· ${str}`) + .join("")} RF protection states: ${resp.supportedRFStates - .map((local) => getEnumMemberName(RFProtectionState, local)) - .map((str) => `\n· ${str}`) - .join("")}`; - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); + .map((local) => getEnumMemberName(RFProtectionState, local)) + .map((str) => `\n· ${str}`) + .join("")}`; + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); + } else { + hadCriticalTimeout = true; + } } const supportsExclusiveControl = !!valueDB.getValue( @@ -409,16 +420,18 @@ RF protection states: ${resp.supportedRFStates direction: "outbound", }); const protectionResp = await api.get(); - let logMessage = `received protection status: + if (protectionResp) { + let logMessage = `received protection status: local: ${getEnumMemberName(LocalProtectionState, protectionResp.local)}`; - if (protectionResp.rf != undefined) { - logMessage += ` + if (protectionResp.rf != undefined) { + logMessage += ` rf ${getEnumMemberName(RFProtectionState, protectionResp.rf)}`; + } + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); } - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); if (supportsTimeout) { // Query the current timeout @@ -427,10 +440,12 @@ rf ${getEnumMemberName(RFProtectionState, protectionResp.rf)}`; direction: "outbound", }); const timeout = await api.getTimeout(); - this.driver.controllerLog.logNode(node.id, { - message: `received timeout: ${timeout.toString()}`, - direction: "inbound", - }); + if (timeout) { + this.driver.controllerLog.logNode(node.id, { + message: `received timeout: ${timeout.toString()}`, + direction: "inbound", + }); + } } if (supportsExclusiveControl) { @@ -440,17 +455,19 @@ rf ${getEnumMemberName(RFProtectionState, protectionResp.rf)}`; direction: "outbound", }); const nodeId = await api.getExclusiveControl(); - this.driver.controllerLog.logNode(node.id, { - message: - (nodeId !== 0 - ? `Node ${padStart(nodeId.toString(), 3, "0")}` - : `no node`) + ` has exclusive control`, - direction: "inbound", - }); + if (nodeId != undefined) { + this.driver.controllerLog.logNode(node.id, { + message: + (nodeId !== 0 + ? `Node ${padStart(nodeId.toString(), 3, "0")}` + : `no node`) + ` has exclusive control`, + direction: "inbound", + }); + } } // Remember that the interview is complete - this.interviewComplete = true; + if (!hadCriticalTimeout) this.interviewComplete = true; } } diff --git a/packages/zwave-js/src/lib/commandclass/SecurityCC.ts b/packages/zwave-js/src/lib/commandclass/SecurityCC.ts index 985149381cc8..1d6f36b51836 100644 --- a/packages/zwave-js/src/lib/commandclass/SecurityCC.ts +++ b/packages/zwave-js/src/lib/commandclass/SecurityCC.ts @@ -121,7 +121,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { /** Whether the received nonce should be stored as "free". Default: false */ storeAsFreeNonce?: boolean; } = {}, - ): Promise { + ): Promise { this.assertSupportsCommand(SecurityCommand, SecurityCommand.NonceGet); const { standalone = false, storeAsFreeNonce = false } = options; @@ -130,7 +130,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, { ...this.commandOptions, @@ -144,10 +144,11 @@ export class SecurityCCAPI extends PhysicalCCAPI { // The "real" transaction will do that for us changeNodeStatusOnMissingACK: standalone, }, - ))!; + ); - const nonce = response.nonce; + if (!response) return; + const nonce = response.nonce; if (storeAsFreeNonce) { const secMan = this.driver.securityManager!; secMan.setNonce( @@ -288,11 +289,13 @@ export class SecurityCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, ["supportedCCs", "controlledCCs"]); + ); + if (response) { + return pick(response, ["supportedCCs", "controlledCCs"]); + } } } @@ -325,6 +328,17 @@ export class SecurityCC extends CommandClass { }); const resp = await api.getSupportedCommands(); + if (!resp) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying securely supported commands timed out, skipping interview...", + level: "warn", + }); + // TODO: Abort interview? + return; + } + const logLines: string[] = [ "received secure commands", "supported CCs:", @@ -615,18 +629,21 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { public async preTransmitHandshake(): Promise { // Request a nonce const nonce = await this.getNode()!.commandClasses.Security.getNonce(); - // and store it - const secMan = this.driver.securityManager; - this.nonceId = secMan.getNonceId(nonce); - secMan.setNonce( - { - issuer: this.nodeId, - nonceId: this.nonceId, - }, - { nonce, receiver: this.driver.controller.ownNodeId }, - // The nonce is reserved for this command - { free: false }, - ); + // TODO: Handle this more intelligent + if (nonce) { + // and store it + const secMan = this.driver.securityManager; + this.nonceId = secMan.getNonceId(nonce); + secMan.setNonce( + { + issuer: this.nodeId, + nonceId: this.nonceId, + }, + { nonce, receiver: this.driver.controller.ownNodeId }, + // The nonce is reserved for this command + { free: false }, + ); + } } public serialize(): Buffer { diff --git a/packages/zwave-js/src/lib/commandclass/SoundSwitchCC.ts b/packages/zwave-js/src/lib/commandclass/SoundSwitchCC.ts index 72b15bfaf4f9..9984ae76dbca 100644 --- a/packages/zwave-js/src/lib/commandclass/SoundSwitchCC.ts +++ b/packages/zwave-js/src/lib/commandclass/SoundSwitchCC.ts @@ -15,7 +15,6 @@ import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -90,7 +89,7 @@ export class SoundSwitchCCAPI extends CCAPI { return super.supportsCommand(cmd); } - public async getToneCount(): Promise { + public async getToneCount(): Promise { this.assertSupportsCommand( SoundSwitchCommand, SoundSwitchCommand.TonesNumberGet, @@ -100,11 +99,11 @@ export class SoundSwitchCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.toneCount; + ); + return response?.toneCount; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -119,11 +118,11 @@ export class SoundSwitchCCAPI extends CCAPI { endpoint: this.endpoint.index, toneId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, ["duration", "name"]); + ); + if (response) return pick(response, ["duration", "name"]); } public async setConfiguration( @@ -160,11 +159,13 @@ export class SoundSwitchCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, ["defaultToneId", "defaultVolume"]); + ); + if (response) { + return pick(response, ["defaultToneId", "defaultVolume"]); + } } public async play(toneId: number, volume?: number): Promise { @@ -225,11 +226,13 @@ export class SoundSwitchCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, ["toneId", "volume"]); + ); + if (response) { + return pick(response, ["toneId", "volume"]); + } } protected [SET_VALUE]: SetValueImplementation = async ( @@ -316,48 +319,61 @@ export class SoundSwitchCC extends CommandClass { direction: "none", }); + this.driver.controllerLog.logNode(node.id, { + message: "requesting current sound configuration...", + direction: "outbound", + }); + const config = await api.getConfiguration(); + if (config) { + const logMessage = `received current sound configuration: +default tone ID: ${config.defaultToneId} +default volume: ${config.defaultVolume}`; + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); + } + if (complete) { this.driver.controllerLog.logNode(node.id, { message: "requesting tone count...", direction: "outbound", }); const toneCount = await api.getToneCount(); - const logMessage = `supports ${toneCount} tones`; - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); + if (toneCount != undefined) { + const logMessage = `supports ${toneCount} tones`; + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); + } else { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying tone count timed out, skipping interview...", + level: "warn", + }); + return; + } const metadataStates: Record = { 0: "off", }; for (let toneId = 1; toneId <= toneCount; toneId++) { - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - message: `requesting info for tone #${toneId}`, - direction: "outbound", - }); - const info = await api.getToneInfo(toneId); - const logMessage = `received info for tone #${toneId}: + this.driver.controllerLog.logNode(node.id, { + message: `requesting info for tone #${toneId}`, + direction: "outbound", + }); + const info = await api.getToneInfo(toneId); + if (!info) continue; + const logMessage = `received info for tone #${toneId}: name: ${info.name} duration: ${info.duration} seconds`; - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); - metadataStates[ - toneId - ] = `${info.name} (${info.duration} sec)`; - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Tone info query timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); + metadataStates[toneId] = `${info.name} (${info.duration} sec)`; } metadataStates[0xff] = "default"; @@ -371,19 +387,6 @@ duration: ${info.duration} seconds`; }); } - this.driver.controllerLog.logNode(node.id, { - message: "requesting current sound configuration...", - direction: "outbound", - }); - const config = await api.getConfiguration(); - const logMessage = `received current sound configuration: -default tone ID: ${config.defaultToneId} -default volume: ${config.defaultVolume}`; - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); - // Remember that the interview is complete this.interviewComplete = true; } diff --git a/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts b/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts index 8b44a6ec3b14..d3c658d223de 100644 --- a/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts @@ -13,12 +13,11 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { buffer2hex, getEnumMemberName } from "@zwave-js/shared"; +import { buffer2hex, getEnumMemberName, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -120,14 +119,13 @@ export class ThermostatModeCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - mode: response.mode, - manufacturerData: response.manufacturerData, - }; + ); + if (response) { + return pick(response, ["mode", "manufacturerData"]); + } } public async set( @@ -164,7 +162,9 @@ export class ThermostatModeCCAPI extends CCAPI { } } - public async getSupportedModes(): Promise { + public async getSupportedModes(): Promise< + readonly ThermostatMode[] | undefined + > { this.assertSupportsCommand( ThermostatModeCommand, ThermostatModeCommand.SupportedGet, @@ -174,11 +174,11 @@ export class ThermostatModeCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.supportedModes; + ); + return response?.supportedModes; } } @@ -211,41 +211,44 @@ export class ThermostatModeCC extends CommandClass { }); const supportedModes = await api.getSupportedModes(); - - const logMessage = `received supported thermostat modes:${supportedModes - .map((mode) => `\n· ${getEnumMemberName(ThermostatMode, mode)}`) - .join("")}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - } - - if ( - !(await ignoreTimeout(async () => { - // Always query the actual status + if (supportedModes) { + const logMessage = `received supported thermostat modes:${supportedModes + .map( + (mode) => + `\n· ${getEnumMemberName(ThermostatMode, mode)}`, + ) + .join("")}`; this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: "querying current thermostat mode...", - direction: "outbound", + message: logMessage, + direction: "inbound", }); - const currentStatus = await api.get(); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: - "received current thermostat mode: " + - getEnumMemberName(ThermostatMode, currentStatus.mode), - direction: "inbound", + "Querying supported thermostat modes timed out, skipping interview...", + level: "warn", }); - })) - ) { + return; + } + } + + // Always query the actual status + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying current thermostat mode...", + direction: "outbound", + }); + const currentStatus = await api.get(); + if (currentStatus) { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: `Thermostat mode query timed out - skipping interview...`, - level: "warn", + message: + "received current thermostat mode: " + + getEnumMemberName(ThermostatMode, currentStatus.mode), + direction: "inbound", }); - return; } // Remember that the interview is complete diff --git a/packages/zwave-js/src/lib/commandclass/ThermostatOperatingStateCC.ts b/packages/zwave-js/src/lib/commandclass/ThermostatOperatingStateCC.ts index a4e84f2d1677..19fc14486c33 100644 --- a/packages/zwave-js/src/lib/commandclass/ThermostatOperatingStateCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ThermostatOperatingStateCC.ts @@ -80,7 +80,7 @@ export class ThermostatOperatingStateCCAPI extends PhysicalCCAPI { } }; - public async get(): Promise { + public async get(): Promise { this.assertSupportsCommand( ThermostatOperatingStateCommand, ThermostatOperatingStateCommand.Get, @@ -90,11 +90,11 @@ export class ThermostatOperatingStateCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.state; + ); + return response?.state; } } @@ -126,15 +126,16 @@ export class ThermostatOperatingStateCC extends CommandClass { }); const state = await api.get(); - - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `received current thermostat operating state: ${getEnumMemberName( - ThermostatOperatingState, - state, - )}`, - direction: "inbound", - }); + if (state) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `received current thermostat operating state: ${getEnumMemberName( + ThermostatOperatingState, + state, + )}`, + direction: "inbound", + }); + } // Remember that the interview is complete this.interviewComplete = true; diff --git a/packages/zwave-js/src/lib/commandclass/ThermostatSetbackCC.ts b/packages/zwave-js/src/lib/commandclass/ThermostatSetbackCC.ts index 2ee416406b31..361186fbdc72 100644 --- a/packages/zwave-js/src/lib/commandclass/ThermostatSetbackCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ThermostatSetbackCC.ts @@ -6,7 +6,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { @@ -88,14 +88,13 @@ export class ThermostatSetbackCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - setbackType: response.setbackType, - setbackState: response.setbackState, - }; + ); + if (response) { + return pick(response, ["setbackType", "setbackState"]); + } } public async set( @@ -149,14 +148,16 @@ export class ThermostatSetbackCC extends CommandClass { direction: "outbound", }); const setbackResp = await api.get(); - const logMessage = `received current state: + if (setbackResp) { + const logMessage = `received current state: setback type: ${getEnumMemberName(SetbackType, setbackResp.setbackType)} setback state: ${setbackResp.setbackState}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } // Remember that the interview is complete this.interviewComplete = true; diff --git a/packages/zwave-js/src/lib/commandclass/ThermostatSetpointCC.ts b/packages/zwave-js/src/lib/commandclass/ThermostatSetpointCC.ts index 8b8a0300d73a..a117f37e77f5 100644 --- a/packages/zwave-js/src/lib/commandclass/ThermostatSetpointCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ThermostatSetpointCC.ts @@ -11,12 +11,11 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName } from "@zwave-js/shared"; +import { getEnumMemberName, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -214,10 +213,11 @@ export class ThermostatSetpointCCAPI extends CCAPI { endpoint: this.endpoint.index, setpointType, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; + ); + if (!response) return; return response.type === ThermostatSetpointType["N/A"] ? // not supported undefined @@ -265,16 +265,18 @@ export class ThermostatSetpointCCAPI extends CCAPI { endpoint: this.endpoint.index, setpointType, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - minValue: response.minValue, - maxValue: response.maxValue, - minValueScale: response.minValueScale, - maxValueScale: response.maxValueScale, - }; + ); + if (response) { + return pick(response, [ + "minValue", + "maxValue", + "minValueScale", + "maxValueScale", + ]); + } } /** @@ -283,7 +285,7 @@ export class ThermostatSetpointCCAPI extends CCAPI { * during node interview. */ public async getSupportedSetpointTypes(): Promise< - readonly ThermostatSetpointType[] + readonly ThermostatSetpointType[] | undefined > { this.assertSupportsCommand( ThermostatSetpointCommand, @@ -294,11 +296,11 @@ export class ThermostatSetpointCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.supportedSetpointTypes; + ); + return response?.supportedSetpointTypes; } } @@ -363,7 +365,17 @@ export class ThermostatSetpointCC extends CommandClass { message: "retrieving supported setpoint types...", direction: "outbound", }); - setpointTypes = [...(await api.getSupportedSetpointTypes())]; + const resp = await api.getSupportedSetpointTypes(); + if (!resp) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying supported setpoint types timed out, skipping interview...", + level: "warn", + }); + return; + } + setpointTypes = [...resp]; interpretation = undefined; // we don't know yet which interpretation the device uses } else { setpointTypes = @@ -424,16 +436,8 @@ export class ThermostatSetpointCC extends CommandClass { direction: "outbound", }); - let setpoint: - | { - value: number; - scale: Scale; - } - | undefined; - await ignoreTimeout(async () => { - setpoint = await api.get(type); - // If the node did not respond, assume the setpoint type is not supported - }); + const setpoint = await api.get(type); + // If the node did not respond, assume the setpoint type is not supported let logMessage: string; if (setpoint) { @@ -494,37 +498,32 @@ export class ThermostatSetpointCC extends CommandClass { // If we haven't yet, query the supported setpoint types let setpointTypes: ThermostatSetpointType[] = []; if (complete) { - if ( - !(await ignoreTimeout(async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "retrieving supported setpoint types...", - direction: "outbound", - }); - setpointTypes = [ - ...(await api.getSupportedSetpointTypes()), - ]; - const logMessage = - "received supported setpoint types:\n" + - setpointTypes - .map((type) => - getEnumMemberName( - ThermostatSetpointType, - type, - ), - ) - .map((name) => `· ${name}`) - .join("\n"); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - })) - ) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "retrieving supported setpoint types...", + direction: "outbound", + }); + const resp = await api.getSupportedSetpointTypes(); + if (resp) { + setpointTypes = [...resp]; + const logMessage = + "received supported setpoint types:\n" + + setpointTypes + .map((type) => + getEnumMemberName(ThermostatSetpointType, type), + ) + .map((name) => `· ${name}`) + .join("\n"); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: `supported setpoint type query timed out - skipping interview...`, + message: + "Querying supported setpoint types timed out, skipping interview...", level: "warn", }); return; @@ -551,56 +550,42 @@ export class ThermostatSetpointCC extends CommandClass { direction: "outbound", }); const setpointCaps = await api.getCapabilities(type); - const minValueUnit = getSetpointUnit( - this.driver.configManager, - setpointCaps.minValueScale, - ); - const maxValueUnit = getSetpointUnit( - this.driver.configManager, - setpointCaps.maxValueScale, - ); - const logMessage = `received capabilities for setpoint ${setpointName}: + if (setpointCaps) { + const minValueUnit = getSetpointUnit( + this.driver.configManager, + setpointCaps.minValueScale, + ); + const maxValueUnit = getSetpointUnit( + this.driver.configManager, + setpointCaps.maxValueScale, + ); + const logMessage = `received capabilities for setpoint ${setpointName}: minimum value: ${setpointCaps.minValue} ${minValueUnit} maximum value: ${setpointCaps.maxValue} ${maxValueUnit}`; + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } + } + // Every time, query the current value + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `querying current value of setpoint ${setpointName}...`, + direction: "outbound", + }); + const setpoint = await api.get(type); + if (setpoint) { + const logMessage = `received current value of setpoint ${setpointName}: ${ + setpoint.value + } ${setpoint.scale.unit ?? ""}`; this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, message: logMessage, direction: "inbound", }); } - // Every time, query the current value - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `querying current value of setpoint ${setpointName}...`, - direction: "outbound", - }); - const setpoint = await api.get(type); - let logMessage: string; - if (setpoint) { - logMessage = `received current value of setpoint ${setpointName}: ${ - setpoint.value - } ${setpoint.scale.unit ?? ""}`; - } else { - // This shouldn't happen since we used getSupported - // But better be sure we don't crash - logMessage = `Setpoint ${setpointName} is not supported`; - } - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Setpoint ${setpointName} query timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); } } diff --git a/packages/zwave-js/src/lib/commandclass/TimeCC.ts b/packages/zwave-js/src/lib/commandclass/TimeCC.ts index f852e11c97e3..c95bc216f188 100644 --- a/packages/zwave-js/src/lib/commandclass/TimeCC.ts +++ b/packages/zwave-js/src/lib/commandclass/TimeCC.ts @@ -10,6 +10,7 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; +import { pick } from "@zwave-js/shared"; import { padStart } from "alcalzone-shared/strings"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; @@ -63,15 +64,13 @@ export class TimeCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - hour: response.hour, - minute: response.minute, - second: response.second, - }; + ); + if (response) { + return pick(response, ["hour", "minute", "second"]); + } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -82,15 +81,13 @@ export class TimeCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - day: response.day, - month: response.month, - year: response.year, - }; + ); + if (response) { + return pick(response, ["day", "month", "year"]); + } } public async setTimezone(timezone: DSTInfo): Promise { @@ -112,23 +109,25 @@ export class TimeCCAPI extends CCAPI { } } - public async getTimezone(): Promise { + public async getTimezone(): Promise { this.assertSupportsCommand(TimeCommand, TimeCommand.TimeOffsetGet); const cc = new TimeCCTimeOffsetGet(this.driver, { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - standardOffset: response.standardOffset, - dstOffset: response.dstOffset, - startDate: response.dstStartDate, - endDate: response.dstEndDate, - }; + ); + if (response) { + return { + standardOffset: response.standardOffset, + dstOffset: response.dstOffset, + startDate: response.dstStartDate, + endDate: response.dstEndDate, + }; + } } } diff --git a/packages/zwave-js/src/lib/commandclass/TimeParametersCC.ts b/packages/zwave-js/src/lib/commandclass/TimeParametersCC.ts index 537a3081f08b..e107eb06787a 100644 --- a/packages/zwave-js/src/lib/commandclass/TimeParametersCC.ts +++ b/packages/zwave-js/src/lib/commandclass/TimeParametersCC.ts @@ -137,7 +137,7 @@ export class TimeParametersCCAPI extends CCAPI { } }; - public async get(): Promise { + public async get(): Promise { this.assertSupportsCommand( TimeParametersCommand, TimeParametersCommand.Get, @@ -147,11 +147,11 @@ export class TimeParametersCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.dateAndTime; + ); + return response?.dateAndTime; } public async set(dateAndTime: Date): Promise { diff --git a/packages/zwave-js/src/lib/commandclass/UserCodeCC.ts b/packages/zwave-js/src/lib/commandclass/UserCodeCC.ts index f433d3a1cc46..2591deb326da 100644 --- a/packages/zwave-js/src/lib/commandclass/UserCodeCC.ts +++ b/packages/zwave-js/src/lib/commandclass/UserCodeCC.ts @@ -20,7 +20,6 @@ import { pick, } from "@zwave-js/shared"; import { - ignoreTimeout, PhysicalCCAPI, PollValueImplementation, POLL_VALUE, @@ -445,7 +444,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { } }; - public async getUsersCount(): Promise { + public async getUsersCount(): Promise { this.assertSupportsCommand( UserCodeCommand, UserCodeCommand.UsersNumberGet, @@ -455,21 +454,23 @@ export class UserCodeCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.supportedUsers; + ); + return response?.supportedUsers; } public async get( userId: number, multiple?: false, - ): Promise>; + ): Promise | undefined>; public async get( userId: number, multiple: true, - ): Promise<{ userCodes: readonly UserCode[]; nextUserId: number }>; + ): Promise< + { userCodes: readonly UserCode[]; nextUserId: number } | undefined + >; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types public async get(userId: number, multiple: boolean = false) { if (userId > 255 || multiple) { @@ -484,11 +485,13 @@ export class UserCodeCCAPI extends PhysicalCCAPI { userId, reportMore: multiple, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - if (multiple) { + ); + if (!response) { + return; + } else if (multiple) { return pick(response, ["userCodes", "nextUserId"]); } else { return pick(response.userCodes[0], [ @@ -504,11 +507,11 @@ export class UserCodeCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, userId, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, ["userIdStatus", "userCode"]); + ); + if (response) return pick(response, ["userIdStatus", "userCode"]); } } @@ -592,23 +595,25 @@ export class UserCodeCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return pick(response, [ - "supportsMasterCode", - "supportsMasterCodeDeactivation", - "supportsUserCodeChecksum", - "supportsMultipleUserCodeReport", - "supportsMultipleUserCodeSet", - "supportedUserIDStatuses", - "supportedKeypadModes", - "supportedASCIIChars", - ]); + ); + if (response) { + return pick(response, [ + "supportsMasterCode", + "supportsMasterCodeDeactivation", + "supportsUserCodeChecksum", + "supportsMultipleUserCodeReport", + "supportsMultipleUserCodeSet", + "supportedUserIDStatuses", + "supportedKeypadModes", + "supportedASCIIChars", + ]); + } } - public async getKeypadMode(): Promise { + public async getKeypadMode(): Promise { this.assertSupportsCommand( UserCodeCommand, UserCodeCommand.KeypadModeGet, @@ -618,11 +623,11 @@ export class UserCodeCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.keypadMode; + ); + return response?.keypadMode; } public async setKeypadMode(keypadMode: KeypadMode): Promise { @@ -643,7 +648,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { await this.getKeypadMode(); } - public async getMasterCode(): Promise { + public async getMasterCode(): Promise { this.assertSupportsCommand( UserCodeCommand, UserCodeCommand.MasterCodeGet, @@ -653,11 +658,11 @@ export class UserCodeCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.masterCode; + ); + return response?.masterCode; } public async setMasterCode(masterCode: string): Promise { @@ -678,7 +683,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { await this.getMasterCode(); } - public async getUserCodeChecksum(): Promise { + public async getUserCodeChecksum(): Promise { this.assertSupportsCommand( UserCodeCommand, UserCodeCommand.UserCodeChecksumGet, @@ -688,11 +693,11 @@ export class UserCodeCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.userCodeChecksum; + ); + return response?.userCodeChecksum; } } @@ -719,18 +724,19 @@ export class UserCodeCC extends CommandClass { let supportsMasterCode = false; let supportsUserCodeChecksum = false; let supportedKeypadModes: readonly KeypadMode[] = []; - let supportedUsers: number; + let supportedUsers: number | undefined; if (complete) { if (this.version >= 2) { this.driver.controllerLog.logNode(node.id, { message: "querying capabilities...", direction: "outbound", }); - ({ - supportsMasterCode, - supportsUserCodeChecksum, - supportedKeypadModes, - } = await api.getCapabilities()); + const caps = await api.getCapabilities(); + if (caps) { + supportsMasterCode = caps.supportsMasterCode; + supportsUserCodeChecksum = caps.supportsUserCodeChecksum; + supportedKeypadModes = caps.supportedKeypadModes; + } } this.driver.controllerLog.logNode(node.id, { @@ -738,6 +744,15 @@ export class UserCodeCC extends CommandClass { direction: "outbound", }); supportedUsers = await api.getUsersCount(); + if (supportedUsers == undefined) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "Querying number of user codes timed out, skipping interview...", + level: "warn", + }); + return; + } } else { supportsMasterCode = node.getValue( @@ -777,7 +792,7 @@ export class UserCodeCC extends CommandClass { node.getValue( getUserCodeChecksumValueID(this.endpointIndex), ) ?? 0; - let currentUserCodeChecksum = 0; + let currentUserCodeChecksum: number | undefined = 0; if (supportsUserCodeChecksum) { this.driver.controllerLog.logNode(node.id, { message: "retrieving current user code checksum...", @@ -796,7 +811,17 @@ export class UserCodeCC extends CommandClass { }); let nextUserId = 1; while (nextUserId > 0 && nextUserId <= supportedUsers) { - ({ nextUserId } = await api.get(nextUserId, true)); + const response = await api.get(nextUserId, true); + if (response) { + nextUserId = response.nextUserId; + } else { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `Querying user code #${nextUserId} timed out, skipping the remaining interview...`, + level: "warn", + }); + break; + } } } } else { @@ -805,20 +830,9 @@ export class UserCodeCC extends CommandClass { message: "querying all user codes...", direction: "outbound", }); - await ignoreTimeout( - async () => { - for (let userId = 1; userId <= supportedUsers; userId++) { - await api.get(userId); - } - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "User code query timed out - skipping because it is not critical...", - }); - }, - ); + for (let userId = 1; userId <= supportedUsers; userId++) { + await api.get(userId); + } } // Remember that the interview is complete diff --git a/packages/zwave-js/src/lib/commandclass/VersionCC.ts b/packages/zwave-js/src/lib/commandclass/VersionCC.ts index 522ddac1745b..bea032bf0b40 100644 --- a/packages/zwave-js/src/lib/commandclass/VersionCC.ts +++ b/packages/zwave-js/src/lib/commandclass/VersionCC.ts @@ -11,11 +11,11 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; -import { getEnumMemberName, num2hex } from "@zwave-js/shared"; +import { getEnumMemberName, num2hex, pick } from "@zwave-js/shared"; import { ZWaveLibraryTypes } from "../controller/ZWaveLibraryTypes"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; -import { ignoreTimeout, PhysicalCCAPI } from "./API"; +import { PhysicalCCAPI } from "./API"; import { API, CCCommand, @@ -96,19 +96,23 @@ export class VersionCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - libraryType: response.libraryType, - protocolVersion: response.protocolVersion, - firmwareVersions: response.firmwareVersions, - hardwareVersion: response.hardwareVersion, - }; + ); + if (response) { + return pick(response, [ + "libraryType", + "protocolVersion", + "firmwareVersions", + "hardwareVersion", + ]); + } } - public async getCCVersion(requestedCC: CommandClasses): Promise { + public async getCCVersion( + requestedCC: CommandClasses, + ): Promise { this.assertSupportsCommand( VersionCommand, VersionCommand.CommandClassGet, @@ -119,11 +123,11 @@ export class VersionCCAPI extends PhysicalCCAPI { endpoint: this.endpoint.index, requestedCC, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return response.ccVersion; + ); + return response?.ccVersion; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -137,13 +141,13 @@ export class VersionCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - supportsZWaveSoftwareGet: response.supportsZWaveSoftwareGet, - }; + ); + if (response) { + return pick(response, ["supportsZWaveSoftwareGet"]); + } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -157,23 +161,23 @@ export class VersionCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - sdkVersion: response.sdkVersion, - applicationFrameworkAPIVersion: - response.applicationFrameworkAPIVersion, - applicationFrameworkBuildNumber: - response.applicationFrameworkBuildNumber, - hostInterfaceVersion: response.hostInterfaceVersion, - hostInterfaceBuildNumber: response.hostInterfaceBuildNumber, - zWaveProtocolVersion: response.zWaveProtocolVersion, - zWaveProtocolBuildNumber: response.zWaveProtocolBuildNumber, - applicationVersion: response.applicationVersion, - applicationBuildNumber: response.applicationBuildNumber, - }; + ); + if (response) { + return pick(response, [ + "sdkVersion", + "applicationFrameworkAPIVersion", + "applicationFrameworkBuildNumber", + "hostInterfaceVersion", + "hostInterfaceBuildNumber", + "zWaveProtocolVersion", + "zWaveProtocolBuildNumber", + "applicationVersion", + "applicationBuildNumber", + ]); + } } } @@ -216,36 +220,27 @@ export class VersionCC extends CommandClass { // Version information should not change (except for firmware updates) if (complete) { // Step 1: Query node versions - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying node versions...", - direction: "outbound", - }); - const versionGetResponse = await api.get(); - // prettier-ignore - let logMessage = `received response for node versions: + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying node versions...", + direction: "outbound", + }); + const versionGetResponse = await api.get(); + if (versionGetResponse) { + // prettier-ignore + let logMessage = `received response for node versions: library type: ${ZWaveLibraryTypes[versionGetResponse.libraryType]} (${num2hex(versionGetResponse.libraryType)}) protocol version: ${versionGetResponse.protocolVersion} firmware versions: ${versionGetResponse.firmwareVersions.join(", ")}`; - if (versionGetResponse.hardwareVersion != undefined) { - logMessage += `\n hardware version: ${versionGetResponse.hardwareVersion}`; - } - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Node version query timed out - skipping because it is not critical...`, - level: "warn", - }); - }, - ); + if (versionGetResponse.hardwareVersion != undefined) { + logMessage += `\n hardware version: ${versionGetResponse.hardwareVersion}`; + } + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } // Step 2: Query all CC versions this.driver.controllerLog.logNode(node.id, { @@ -266,43 +261,42 @@ export class VersionCC extends CommandClass { continue; } - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: ` querying the CC version for ${getCCName( - cc, - )}...`, - direction: "outbound", - }); - // query the CC version - const supportedVersion = await api.getCCVersion(cc); - // Remember which CC version this endpoint supports - let logMessage: string; - if (supportedVersion > 0) { - endpoint.addCC(cc, { version: supportedVersion }); - logMessage = ` supports CC ${ - CommandClasses[cc] - } (${num2hex(cc)}) in version ${supportedVersion}`; - } else { - // We were lied to - the NIF said this CC is supported, now the node claims it isn't - endpoint.removeCC(cc); - logMessage = ` does NOT support CC ${ - CommandClasses[cc] - } (${num2hex(cc)})`; - } - this.driver.controllerLog.logNode(node.id, logMessage); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `CC version query for ${getCCName( - cc, - )} timed out - assuming the node supports version 1...`, - level: "warn", + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: ` querying the CC version for ${getCCName( + cc, + )}...`, + direction: "outbound", + }); + // query the CC version + const supportedVersion = await api.getCCVersion(cc); + if (supportedVersion != undefined) { + // Remember which CC version this endpoint supports + let logMessage: string; + if (supportedVersion > 0) { + endpoint.addCC(cc, { + version: supportedVersion, }); - }, - ); + logMessage = ` supports CC ${ + CommandClasses[cc] + } (${num2hex(cc)}) in version ${supportedVersion}`; + } else { + // We were lied to - the NIF said this CC is supported, now the node claims it isn't + endpoint.removeCC(cc); + logMessage = ` does NOT support CC ${ + CommandClasses[cc] + } (${num2hex(cc)})`; + } + this.driver.controllerLog.logNode(node.id, logMessage); + } else { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `CC version query for ${getCCName( + cc, + )} timed out - assuming the node supports version 1...`, + level: "warn", + }); + } } // Step 3: Query VersionCC capabilities @@ -314,49 +308,38 @@ export class VersionCC extends CommandClass { this.endpointIndex, ) >= 3 ) { - await ignoreTimeout( - async () => { - // Step 3a: Support for SoftwareGet + // Step 3a: Support for SoftwareGet + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "querying if Z-Wave Software Get is supported...", + direction: "outbound", + }); + const capsResponse = await api.getCapabilities(); + if (capsResponse) { + const { supportsZWaveSoftwareGet } = capsResponse; + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: `Z-Wave Software Get is${ + supportsZWaveSoftwareGet ? "" : " not" + } supported`, + direction: "inbound", + }); + + if (supportsZWaveSoftwareGet) { + // Step 3b: Query Z-Wave Software versions this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: - "querying if Z-Wave Software Get is supported...", + message: "querying Z-Wave software versions...", direction: "outbound", }); - const { - supportsZWaveSoftwareGet, - } = await api.getCapabilities(); + await api.getZWaveSoftware(); this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: `Z-Wave Software Get is${ - supportsZWaveSoftwareGet ? "" : " not" - } supported`, + message: "received Z-Wave software versions", direction: "inbound", }); - - if (supportsZWaveSoftwareGet) { - // Step 3b: Query Z-Wave Software versions - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "querying Z-Wave software versions...", - direction: "outbound", - }); - await api.getZWaveSoftware(); - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: "received Z-Wave software versions", - direction: "inbound", - }); - } - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: `Version capability or software version query timed out - skipping because this is not critical...`, - level: "warn", - }); - }, - ); + } + } } } diff --git a/packages/zwave-js/src/lib/commandclass/WakeUpCC.ts b/packages/zwave-js/src/lib/commandclass/WakeUpCC.ts index 3dc5fffa8ad8..0516b186dbdd 100644 --- a/packages/zwave-js/src/lib/commandclass/WakeUpCC.ts +++ b/packages/zwave-js/src/lib/commandclass/WakeUpCC.ts @@ -6,13 +6,13 @@ import { ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; +import { pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import type { ZWaveNode } from "../node/Node"; import { NodeStatus } from "../node/Types"; import { CCAPI, - ignoreTimeout, PollValueImplementation, POLL_VALUE, SetValueImplementation, @@ -98,14 +98,13 @@ export class WakeUpCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - wakeUpInterval: response.wakeUpInterval, - controllerNodeId: response.controllerNodeId, - }; + ); + if (response) { + return pick(response, ["wakeUpInterval", "controllerNodeId"]); + } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -119,16 +118,18 @@ export class WakeUpCCAPI extends CCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - defaultWakeUpInterval: response.defaultWakeUpInterval, - minWakeUpInterval: response.minWakeUpInterval, - maxWakeUpInterval: response.maxWakeUpInterval, - wakeUpIntervalSteps: response.wakeUpIntervalSteps, - }; + ); + if (response) { + return pick(response, [ + "defaultWakeUpInterval", + "minWakeUpInterval", + "maxWakeUpInterval", + "wakeUpIntervalSteps", + ]); + } } public async setInterval( @@ -208,6 +209,10 @@ export class WakeUpCC extends CommandClass { direction: "none", }); + // We need to do some queries after a potential timeout + // In this case, do now mark this CC as interviewed completely + let hadCriticalTimeout = false; + if (node.isControllerNode()) { this.driver.controllerLog.logNode( node.id, @@ -223,34 +228,27 @@ export class WakeUpCC extends CommandClass { if (complete) { // This information does not change if (this.version >= 2) { - await ignoreTimeout( - async () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "retrieving wakeup capabilities from the device...", - direction: "outbound", - }); - const wakeupCaps = await api.getIntervalCapabilities(); - const logMessage = `received wakeup capabilities: + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: + "retrieving wakeup capabilities from the device...", + direction: "outbound", + }); + const wakeupCaps = await api.getIntervalCapabilities(); + if (wakeupCaps) { + const logMessage = `received wakeup capabilities: default wakeup interval: ${wakeupCaps.defaultWakeUpInterval} seconds minimum wakeup interval: ${wakeupCaps.minWakeUpInterval} seconds maximum wakeup interval: ${wakeupCaps.maxWakeUpInterval} seconds wakeup interval steps: ${wakeupCaps.wakeUpIntervalSteps} seconds`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - }, - () => { - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: - "Wakeup capability query timed out - skipping because it is not critical...", - }); - }, - ); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } else { + hadCriticalTimeout = true; + } } } @@ -264,33 +262,39 @@ wakeup interval steps: ${wakeupCaps.wakeUpIntervalSteps} seconds`; direction: "outbound", }); const wakeupResp = await api.getInterval(); - const logMessage = `received wakeup configuration: + if (wakeupResp) { + const logMessage = `received wakeup configuration: wakeup interval: ${wakeupResp.wakeUpInterval} seconds controller node: ${wakeupResp.controllerNodeId}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); - - const ownNodeId = this.driver.controller.ownNodeId!; - // Only change the destination if necessary - if (wakeupResp.controllerNodeId !== ownNodeId) { this.driver.controllerLog.logNode(node.id, { endpoint: this.endpointIndex, - message: "configuring wakeup destination node", - direction: "outbound", + message: logMessage, + direction: "inbound", }); - await api.setInterval(wakeupResp.wakeUpInterval, ownNodeId); - this.driver.controllerLog.logNode( - node.id, - "wakeup destination node changed!", - ); + + const ownNodeId = this.driver.controller.ownNodeId!; + // Only change the destination if necessary + if (wakeupResp.controllerNodeId !== ownNodeId) { + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: "configuring wakeup destination node", + direction: "outbound", + }); + await api.setInterval(wakeupResp.wakeUpInterval, ownNodeId); + this.driver.controllerLog.logNode( + node.id, + "wakeup destination node changed!", + ); + } + } else { + // TODO: Change destination as the first thing during bootstrapping a node + // and make it non-critical here + hadCriticalTimeout = true; } } // Remember that the interview is complete - this.interviewComplete = true; + if (!hadCriticalTimeout) this.interviewComplete = true; } } diff --git a/packages/zwave-js/src/lib/commandclass/ZWavePlusCC.ts b/packages/zwave-js/src/lib/commandclass/ZWavePlusCC.ts index 757527fe5bde..2b1236da5cdb 100644 --- a/packages/zwave-js/src/lib/commandclass/ZWavePlusCC.ts +++ b/packages/zwave-js/src/lib/commandclass/ZWavePlusCC.ts @@ -1,6 +1,6 @@ import type { Maybe, MessageOrCCLogEntry, ValueID } from "@zwave-js/core"; import { CommandClasses, validatePayload } from "@zwave-js/core"; -import { getEnumMemberName, num2hex } from "@zwave-js/shared"; +import { getEnumMemberName, num2hex, pick } from "@zwave-js/shared"; import type { Driver } from "../driver/Driver"; import { MessagePriority } from "../message/Constants"; import { PhysicalCCAPI } from "./API"; @@ -103,17 +103,19 @@ export class ZWavePlusCCAPI extends PhysicalCCAPI { nodeId: this.endpoint.nodeId, endpoint: this.endpoint.index, }); - const response = (await this.driver.sendCommand( + const response = await this.driver.sendCommand( cc, this.commandOptions, - ))!; - return { - zwavePlusVersion: response.zwavePlusVersion, - nodeType: response.nodeType, - roleType: response.roleType, - installerIcon: response.installerIcon, - userIcon: response.userIcon, - }; + ); + if (response) { + return pick(response, [ + "zwavePlusVersion", + "nodeType", + "roleType", + "installerIcon", + "userIcon", + ]); + } } } @@ -146,18 +148,19 @@ export class ZWavePlusCC extends CommandClass { }); const zwavePlusResponse = await api.get(); - - const logMessage = `received response for Z-Wave+ information: + if (zwavePlusResponse) { + const logMessage = `received response for Z-Wave+ information: Z-Wave+ version: ${zwavePlusResponse.zwavePlusVersion} role type: ${ZWavePlusRoleType[zwavePlusResponse.roleType]} node type: ${ZWavePlusNodeType[zwavePlusResponse.nodeType]} installer icon: ${num2hex(zwavePlusResponse.installerIcon)} user icon: ${num2hex(zwavePlusResponse.userIcon)}`; - this.driver.controllerLog.logNode(node.id, { - endpoint: this.endpointIndex, - message: logMessage, - direction: "inbound", - }); + this.driver.controllerLog.logNode(node.id, { + endpoint: this.endpointIndex, + message: logMessage, + direction: "inbound", + }); + } } // Remember that the interview is complete diff --git a/packages/zwave-js/src/lib/commandclass/manufacturerProprietary/Fibaro.ts b/packages/zwave-js/src/lib/commandclass/manufacturerProprietary/Fibaro.ts index ca826a33f35d..16d100dee025 100644 --- a/packages/zwave-js/src/lib/commandclass/manufacturerProprietary/Fibaro.ts +++ b/packages/zwave-js/src/lib/commandclass/manufacturerProprietary/Fibaro.ts @@ -136,22 +136,21 @@ export class FibaroVenetianBlindCC extends FibaroCC { message: "Requesting venetian blind position and tilt...", direction: "outbound", }); - const { - position, - tilt, - } = (await this.driver.sendCommand( + const resp = await this.driver.sendCommand( new FibaroVenetianBlindCCGet(this.driver, { nodeId: this.nodeId, endpoint: this.endpointIndex, }), - ))!; - const logMessage = `received venetian blind state: -position: ${position} -tilt: ${tilt}`; - this.driver.controllerLog.logNode(node.id, { - message: logMessage, - direction: "inbound", - }); + ); + if (resp) { + const logMessage = `received venetian blind state: +position: ${resp.position} +tilt: ${resp.tilt}`; + this.driver.controllerLog.logNode(node.id, { + message: logMessage, + direction: "inbound", + }); + } // Remember that the interview is complete this.interviewComplete = true; diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index ec4b8e29952a..39fd3e5b9ed5 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -2129,7 +2129,8 @@ ${handlers.length} left`, // wotan-enable no-misused-generics /** - * Sends a command to a Z-Wave node. + * Sends a command to a Z-Wave node. If the node returns a command in response, that command will be the return value. + * If the command expects no response **or** the response times out, nothing will be returned * @param command The command to send. It will be encapsulated in a SendData[Multicast]Request. * @param options (optional) Options regarding the message transmission */ @@ -2157,12 +2158,31 @@ ${handlers.length} left`, // Automatically encapsulate commands before sending this.encapsulateCommands(msg); - const resp = await this.sendMessage(msg, options); + try { + const resp = await this.sendMessage(msg, options); - // And unwrap the response if there was any - if (isCommandClassContainer(resp)) { - this.unwrapCommands(resp); - return resp.command as TResponse; + // And unwrap the response if there was any + if (isCommandClassContainer(resp)) { + this.unwrapCommands(resp); + return resp.command as TResponse; + } + } catch (e: unknown) { + // A timeout always has to be expected. In this case return nothing. + if ( + e instanceof ZWaveError && + e.code === ZWaveErrorCodes.Controller_NodeTimeout + ) { + if (command.isSinglecast()) { + this.controllerLog.logNode( + command.nodeId, + e.message, + "warn", + ); + } + } else { + // We don't want to swallow any other errors + throw e; + } } } @@ -2176,7 +2196,7 @@ ${handlers.length} left`, options: SendSupervisedCommandOptions = { requestStatusUpdates: false, }, - ): Promise { + ): Promise { // Check if the target supports this command if (!command.getNode()?.supportsCC(CommandClasses.Supervision)) { throw new ZWaveError( @@ -2194,10 +2214,12 @@ ${handlers.length} left`, options.requestStatusUpdates, ); - const resp = (await this.sendCommand( + const resp = await this.sendCommand( command, options, - ))!; + ); + if (!resp) return; + // If future updates are expected, listen for them if (options.requestStatusUpdates && resp.moreUpdatesFollow) { this.supervisionSessions.set( diff --git a/packages/zwave-js/src/lib/driver/SendThreadMachine.ts b/packages/zwave-js/src/lib/driver/SendThreadMachine.ts index c85aa601ca39..61c67dee655d 100644 --- a/packages/zwave-js/src/lib/driver/SendThreadMachine.ts +++ b/packages/zwave-js/src/lib/driver/SendThreadMachine.ts @@ -599,6 +599,8 @@ export function createSendThreadMachine( new ZWaveError( reducerResult.message, reducerResult.code, + undefined, + transaction.stack, ), ); drop.push(transaction); diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index 096d7d5db8fa..7f4a7dd491be 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -36,11 +36,7 @@ import { padStart } from "alcalzone-shared/strings"; import { isArray, isObject } from "alcalzone-shared/typeguards"; import { randomBytes } from "crypto"; import { EventEmitter } from "events"; -import { - CCAPI, - ignoreTimeout, - PollValueImplementation, -} from "../commandclass/API"; +import type { CCAPI, PollValueImplementation } from "../commandclass/API"; import { getHasLifelineValueId } from "../commandclass/AssociationCC"; import { BasicCC, @@ -1290,16 +1286,7 @@ version: ${this.version}`; } try { - const task = () => - instance.interview(!instance.interviewComplete); - if (actuatorCCs.includes(cc) || sensorCCs.includes(cc)) { - // Ignore missing node responses for sensor and actuator CCs - // because they sometimes don't respond to stuff they should respond to - await ignoreTimeout(task); - } else { - // For all other CCs, abort the node interview since we're very likely missing critical information - await task(); - } + await instance.interview(!instance.interviewComplete); } catch (e: unknown) { if ( e instanceof ZWaveError && @@ -2494,6 +2481,12 @@ version: ${this.version}`; // Check if this update is possible const meta = await api.getMetaData(); + if (!meta) { + throw new ZWaveError( + `The node did not respond in time`, + ZWaveErrorCodes.Controller_NodeTimeout, + ); + } if (target === 0 && !meta.firmwareUpgradable) { throw new ZWaveError( `The Z-Wave chip firmware is not upgradable`, diff --git a/test/run.ts b/test/run.ts index 80ff5b315eb6..1462f49ab7fc 100644 --- a/test/run.ts +++ b/test/run.ts @@ -1,7 +1,13 @@ /* wotan-disable async-function-assignability */ require("reflect-metadata"); -import { Driver } from "../packages/zwave-js/src"; + +process.on("unhandledRejection", (_r) => { + // debugger; +}); + +import { MultilevelSensorCCGet } from "../packages/zwave-js/src/lib/commandclass/MultilevelSensorCC"; +import { Driver } from "../packages/zwave-js/src/lib/driver/Driver"; const driver = new Driver("COM4", { // prettier-ignore @@ -11,6 +17,18 @@ const driver = new Driver("COM4", { }) .on("error", console.error) .once("driver ready", async () => { + await require("alcalzone-shared/async").wait(2000); + + const node = driver.controller.nodes.get(22)!; + const resp = await driver.sendCommand( + new MultilevelSensorCCGet(driver, { + nodeId: node.id, + sensorType: 1, + scale: 2, + }), + ); + console.dir(resp); + // const cc = new CommandClass(driver, { // nodeId: 24, // ccId: 0x5d,