diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ec53617585..3da2c8eb8489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ +## 13.10.0 (2024-10-24) +### Features +* `mock-server` now supports putting the simulated controller into add and remove mode (#7314) + ## 13.9.1 (2024-10-17) ### Bugfixes * Fixed an issue where preferred scales were not being found when set as a string (#7286) diff --git a/package.json b/package.json index 07c138460376..0bfdf1376cf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/repo", - "version": "13.9.1", + "version": "13.10.0", "private": true, "description": "Z-Wave driver written entirely in JavaScript/TypeScript", "keywords": [], diff --git a/packages/flash/package.json b/packages/flash/package.json index 81c7de34719e..1be920ff56af 100644 --- a/packages/flash/package.json +++ b/packages/flash/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/flash", - "version": "13.9.1", + "version": "13.10.0", "description": "zwave-js: firmware flash utility", "keywords": [], "publishConfig": { diff --git a/packages/testing/package.json b/packages/testing/package.json index 3c7438357d2c..395b234e42aa 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@zwave-js/testing", - "version": "13.9.1", + "version": "13.10.0", "description": "zwave-js: testing utilities", "keywords": [], "publishConfig": { diff --git a/packages/testing/src/MockControllerCapabilities.ts b/packages/testing/src/MockControllerCapabilities.ts index 007444df63cc..fd448b55a571 100644 --- a/packages/testing/src/MockControllerCapabilities.ts +++ b/packages/testing/src/MockControllerCapabilities.ts @@ -43,6 +43,8 @@ export function getDefaultSupportedFunctionTypes(): FunctionType[] { FunctionType.GetNodeProtocolInfo, FunctionType.RequestNodeInfo, FunctionType.AssignSUCReturnRoute, + FunctionType.AddNodeToNetwork, + FunctionType.RemoveNodeFromNetwork, ]; } diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index e6d97f1621cc..4849db905325 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -1,6 +1,6 @@ { "name": "zwave-js", - "version": "13.9.1", + "version": "13.10.0", "description": "Z-Wave driver written entirely in JavaScript/TypeScript", "keywords": [], "type": "commonjs", diff --git a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts index 3c5eb2a7f90a..010bbab67471 100644 --- a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts +++ b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts @@ -55,6 +55,12 @@ import { GetControllerIdResponse, } from "../serialapi/memory/GetControllerIdMessages"; import { SoftResetRequest } from "../serialapi/misc/SoftResetRequest"; +import { + AddNodeStatus, + AddNodeToNetworkRequest, + AddNodeToNetworkRequestStatusReport, + AddNodeType, +} from "../serialapi/network-mgmt/AddNodeToNetworkRequest"; import { AssignSUCReturnRouteRequest, AssignSUCReturnRouteRequestTransmitReport, @@ -68,6 +74,12 @@ import { GetSUCNodeIdRequest, GetSUCNodeIdResponse, } from "../serialapi/network-mgmt/GetSUCNodeIdMessages"; +import { + RemoveNodeFromNetworkRequest, + RemoveNodeFromNetworkRequestStatusReport, + RemoveNodeStatus, + RemoveNodeType, +} from "../serialapi/network-mgmt/RemoveNodeFromNetworkRequest"; import { RequestNodeInfoRequest, RequestNodeInfoResponse, @@ -82,6 +94,7 @@ import { } from "../serialapi/transport/SendDataMessages"; import { MockControllerCommunicationState, + MockControllerInclusionState, MockControllerStateKeys, } from "./MockControllerState"; import { determineNIF } from "./NodeInformationFrame"; @@ -619,6 +632,150 @@ const handleAssignSUCReturnRoute: MockControllerBehavior = { }, }; +const handleAddNode: MockControllerBehavior = { + async onHostMessage(host, controller, msg) { + if (msg instanceof AddNodeToNetworkRequest) { + // Check if this command is legal right now + const state = controller.state.get( + MockControllerStateKeys.InclusionState, + ) as MockControllerInclusionState | undefined; + + const expectCallback = msg.callbackId !== 0; + let cb: AddNodeToNetworkRequestStatusReport | undefined; + if ( + state === MockControllerInclusionState.AddingNode + ) { + // While adding, only accept stop commands + if (msg.addNodeType === AddNodeType.Stop) { + controller.state.set( + MockControllerStateKeys.InclusionState, + MockControllerInclusionState.Idle, + ); + cb = new AddNodeToNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: AddNodeStatus.Failed, + }, + ); + } else { + cb = new AddNodeToNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: AddNodeStatus.Failed, + }, + ); + } + } else if (state === MockControllerInclusionState.RemovingNode) { + // Cannot start adding nodes while removing one + cb = new AddNodeToNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: AddNodeStatus.Failed, + }, + ); + } else { + // Idle + + // Set the controller into "adding node" state + // For now we don't actually do anything in that state + controller.state.set( + MockControllerStateKeys.InclusionState, + MockControllerInclusionState.AddingNode, + ); + + cb = new AddNodeToNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: AddNodeStatus.Ready, + }, + ); + } + + if (expectCallback && cb) { + await controller.sendToHost(cb.serialize()); + } + + return true; + } + }, +}; + +const handleRemoveNode: MockControllerBehavior = { + async onHostMessage(host, controller, msg) { + if (msg instanceof RemoveNodeFromNetworkRequest) { + // Check if this command is legal right now + const state = controller.state.get( + MockControllerStateKeys.InclusionState, + ) as MockControllerInclusionState | undefined; + + const expectCallback = msg.callbackId !== 0; + let cb: RemoveNodeFromNetworkRequestStatusReport | undefined; + if ( + state === MockControllerInclusionState.RemovingNode + ) { + // While removing, only accept stop commands + if (msg.removeNodeType === RemoveNodeType.Stop) { + controller.state.set( + MockControllerStateKeys.InclusionState, + MockControllerInclusionState.Idle, + ); + cb = new RemoveNodeFromNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: RemoveNodeStatus.Failed, + }, + ); + } else { + cb = new RemoveNodeFromNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: RemoveNodeStatus.Failed, + }, + ); + } + } else if (state === MockControllerInclusionState.AddingNode) { + // Cannot start removing nodes while adding one + cb = new RemoveNodeFromNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: RemoveNodeStatus.Failed, + }, + ); + } else { + // Idle + + // Set the controller into "removing node" state + // For now we don't actually do anything in that state + controller.state.set( + MockControllerStateKeys.InclusionState, + MockControllerInclusionState.RemovingNode, + ); + + cb = new RemoveNodeFromNetworkRequestStatusReport( + host, + { + callbackId: msg.callbackId, + status: RemoveNodeStatus.Ready, + }, + ); + } + + if (expectCallback && cb) { + await controller.sendToHost(cb.serialize()); + } + + return true; + } + }, +}; + const forwardCommandClassesToHost: MockControllerBehavior = { async onNodeFrame(host, controller, node, frame) { if ( @@ -682,6 +839,8 @@ export function createDefaultBehaviors(): MockControllerBehavior[] { handleSendDataMulticast, handleRequestNodeInfo, handleAssignSUCReturnRoute, + handleAddNode, + handleRemoveNode, forwardCommandClassesToHost, forwardUnsolicitedNIF, ]; diff --git a/packages/zwave-js/src/lib/controller/MockControllerState.ts b/packages/zwave-js/src/lib/controller/MockControllerState.ts index b36f56bec019..fa267ad72130 100644 --- a/packages/zwave-js/src/lib/controller/MockControllerState.ts +++ b/packages/zwave-js/src/lib/controller/MockControllerState.ts @@ -1,5 +1,6 @@ export enum MockControllerStateKeys { CommunicationState = "communicationState", + InclusionState = "inclusionState", } export enum MockControllerCommunicationState { @@ -7,3 +8,9 @@ export enum MockControllerCommunicationState { Sending, WaitingForNode, } + +export enum MockControllerInclusionState { + Idle, + AddingNode, + RemovingNode, +} diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index ace5128f2ff0..947deff90ca8 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -715,7 +715,9 @@ export class Driver extends TypedEventEmitter } private set queueIdle(value: boolean) { if (this._queueIdle !== value) { - this.driverLog.print(`all queues ${value ? "idle" : "busy"}`); + this.driverLog.print( + value ? "all queues idle" : "one or more queues busy", + ); this._queueIdle = value; this.handleQueueIdleChange(value); } diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts index 945b465b893a..3b8e7045fbb5 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts @@ -5,7 +5,9 @@ import { MessagePriority, type MessageRecord, NodeType, + type NodeUpdatePayload, Protocols, + encodeNodeUpdatePayload, parseNodeID, parseNodeUpdatePayload, } from "@zwave-js/core"; @@ -17,6 +19,7 @@ import { type MessageBaseOptions, type MessageDeserializationOptions, type MessageOptions, + MessageOrigin, MessageType, expectedCallback, gotDeserializationOptions, @@ -90,12 +93,21 @@ export function computeNeighborDiscoveryTimeout( @priority(MessagePriority.Controller) export class AddNodeToNetworkRequestBase extends Message { public constructor(host: ZWaveHost, options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== AddNodeToNetworkRequestStatusReport - ) { - return new AddNodeToNetworkRequestStatusReport(host, options); + if (gotDeserializationOptions(options)) { + if ( + options.origin === MessageOrigin.Host + && (new.target as any) !== AddNodeToNetworkRequest + ) { + return new AddNodeToNetworkRequest(host, options); + } else if ( + options.origin !== MessageOrigin.Host + && (new.target as any) + !== AddNodeToNetworkRequestStatusReport + ) { + return new AddNodeToNetworkRequestStatusReport(host, options); + } } + super(host, options); } } @@ -131,13 +143,23 @@ function testCallbackForAddNodeRequest( export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { public constructor( host: ZWaveHost, - options: AddNodeToNetworkRequestOptions = {}, + options: + | MessageDeserializationOptions + | AddNodeToNetworkRequestOptions = {}, ) { super(host, options); - this.addNodeType = options.addNodeType; - this.highPower = !!options.highPower; - this.networkWide = !!options.networkWide; + if (gotDeserializationOptions(options)) { + const config = this.payload[0]; + this.highPower = !!(config & AddNodeFlags.HighPower); + this.networkWide = !!(config & AddNodeFlags.NetworkWide); + this.addNodeType = config & 0b1111; + this.callbackId = this.payload[1]; + } else { + this.addNodeType = options.addNodeType; + this.highPower = !!options.highPower; + this.networkWide = !!options.networkWide; + } } /** The type of node to add */ @@ -265,43 +287,70 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { } } +export type AddNodeToNetworkRequestStatusReportOptions = { + status: + | AddNodeStatus.Ready + | AddNodeStatus.NodeFound + | AddNodeStatus.ProtocolDone + | AddNodeStatus.Failed; +} | { + status: AddNodeStatus.Done; + nodeId: number; +} | { + status: AddNodeStatus.AddingController | AddNodeStatus.AddingSlave; + nodeInfo: NodeUpdatePayload; +}; + export class AddNodeToNetworkRequestStatusReport extends AddNodeToNetworkRequestBase implements SuccessIndicator { public constructor( host: ZWaveHost, - options: MessageDeserializationOptions, + options: + | MessageDeserializationOptions + | (AddNodeToNetworkRequestStatusReportOptions & MessageBaseOptions), ) { super(host, options); - this.callbackId = this.payload[0]; - this.status = this.payload[1]; - switch (this.status) { - case AddNodeStatus.Ready: - case AddNodeStatus.NodeFound: - case AddNodeStatus.ProtocolDone: - case AddNodeStatus.Failed: - // no context for the status to parse - break; - - case AddNodeStatus.Done: { - const { nodeId } = parseNodeID( - this.payload, - host.nodeIdType, - 2, - ); - this.statusContext = { nodeId }; - break; - } - case AddNodeStatus.AddingController: - case AddNodeStatus.AddingSlave: { - // the payload contains a node information frame - this.statusContext = parseNodeUpdatePayload( - this.payload.subarray(2), - host.nodeIdType, - ); - break; + if (gotDeserializationOptions(options)) { + this.callbackId = this.payload[0]; + this.status = this.payload[1]; + switch (this.status) { + case AddNodeStatus.Ready: + case AddNodeStatus.NodeFound: + case AddNodeStatus.ProtocolDone: + case AddNodeStatus.Failed: + // no context for the status to parse + break; + + case AddNodeStatus.Done: { + const { nodeId } = parseNodeID( + this.payload, + host.nodeIdType, + 2, + ); + this.statusContext = { nodeId }; + break; + } + + case AddNodeStatus.AddingController: + case AddNodeStatus.AddingSlave: { + // the payload contains a node information frame + this.statusContext = parseNodeUpdatePayload( + this.payload.subarray(2), + host.nodeIdType, + ); + break; + } + } + } else { + this.callbackId = options.callbackId; + this.status = options.status; + if ("nodeId" in options) { + this.statusContext = { nodeId: options.nodeId }; + } else if ("nodeInfo" in options) { + this.statusContext = options.nodeInfo; } } } @@ -315,6 +364,20 @@ export class AddNodeToNetworkRequestStatusReport public readonly status: AddNodeStatus; public readonly statusContext: AddNodeStatusContext | undefined; + public serialize(): Buffer { + this.payload = Buffer.from([this.callbackId, this.status]); + if (this.statusContext?.basicDeviceClass != undefined) { + this.payload = Buffer.concat([ + this.payload, + encodeNodeUpdatePayload( + this.statusContext as NodeUpdatePayload, + this.host.nodeIdType, + ), + ]); + } + return super.serialize(); + } + public toLogEntry(): MessageOrCCLogEntry { return { ...super.toLogEntry(), diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts index d5b06589f785..afab88dc9a64 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts @@ -1,6 +1,7 @@ import { type CommandClasses, MessagePriority, + encodeNodeID, parseNodeID, } from "@zwave-js/core"; import type { ZWaveHost } from "@zwave-js/host"; @@ -11,6 +12,7 @@ import { type MessageBaseOptions, type MessageDeserializationOptions, type MessageOptions, + MessageOrigin, MessageType, expectedCallback, gotDeserializationOptions, @@ -52,12 +54,24 @@ interface RemoveNodeFromNetworkRequestOptions extends MessageBaseOptions { @priority(MessagePriority.Controller) export class RemoveNodeFromNetworkRequestBase extends Message { public constructor(host: ZWaveHost, options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== RemoveNodeFromNetworkRequestStatusReport - ) { - return new RemoveNodeFromNetworkRequestStatusReport(host, options); + if (gotDeserializationOptions(options)) { + if ( + options.origin === MessageOrigin.Host + && (new.target as any) !== RemoveNodeFromNetworkRequest + ) { + return new RemoveNodeFromNetworkRequest(host, options); + } else if ( + options.origin !== MessageOrigin.Host + && (new.target as any) + !== RemoveNodeFromNetworkRequestStatusReport + ) { + return new RemoveNodeFromNetworkRequestStatusReport( + host, + options, + ); + } } + super(host, options); } } @@ -95,13 +109,23 @@ export class RemoveNodeFromNetworkRequest { public constructor( host: ZWaveHost, - options: RemoveNodeFromNetworkRequestOptions = {}, + options: + | MessageDeserializationOptions + | RemoveNodeFromNetworkRequestOptions = {}, ) { super(host, options); - this.removeNodeType = options.removeNodeType; - this.highPower = !!options.highPower; - this.networkWide = !!options.networkWide; + if (gotDeserializationOptions(options)) { + const config = this.payload[0]; + this.highPower = !!(config & RemoveNodeFlags.HighPower); + this.networkWide = !!(config & RemoveNodeFlags.NetworkWide); + this.removeNodeType = config & 0b11111; + this.callbackId = this.payload[1]; + } else { + this.removeNodeType = options.removeNodeType; + this.highPower = !!options.highPower; + this.networkWide = !!options.networkWide; + } } /** The type of node to remove */ @@ -122,38 +146,65 @@ export class RemoveNodeFromNetworkRequest } } +export type RemoveNodeFromNetworkRequestStatusReportOptions = { + status: + | RemoveNodeStatus.Ready + | RemoveNodeStatus.NodeFound + | RemoveNodeStatus.Failed + | RemoveNodeStatus.Done; +} | { + status: + | RemoveNodeStatus.RemovingController + | RemoveNodeStatus.RemovingSlave; + nodeId: number; +}; + export class RemoveNodeFromNetworkRequestStatusReport extends RemoveNodeFromNetworkRequestBase implements SuccessIndicator { public constructor( host: ZWaveHost, - options: MessageDeserializationOptions, + options: + | MessageDeserializationOptions + | ( + & RemoveNodeFromNetworkRequestStatusReportOptions + & MessageBaseOptions + ), ) { super(host, options); - this.callbackId = this.payload[0]; - this.status = this.payload[1]; - switch (this.status) { - case RemoveNodeStatus.Ready: - case RemoveNodeStatus.NodeFound: - case RemoveNodeStatus.Failed: - case RemoveNodeStatus.Done: - // no context for the status to parse - // TODO: - // An application MUST time out waiting for the REMOVE_NODE_STATUS_REMOVING_SLAVE status - // if it does not receive the indication within a 14 sec after receiving the - // REMOVE_NODE_STATUS_NODE_FOUND status. - break; - - case RemoveNodeStatus.RemovingController: - case RemoveNodeStatus.RemovingSlave: { - // the payload contains the node ID - const { nodeId } = parseNodeID( - this.payload.subarray(2), - this.host.nodeIdType, - ); - this.statusContext = { nodeId }; - break; + + if (gotDeserializationOptions(options)) { + this.callbackId = this.payload[0]; + this.status = this.payload[1]; + switch (this.status) { + case RemoveNodeStatus.Ready: + case RemoveNodeStatus.NodeFound: + case RemoveNodeStatus.Failed: + case RemoveNodeStatus.Done: + // no context for the status to parse + // TODO: + // An application MUST time out waiting for the REMOVE_NODE_STATUS_REMOVING_SLAVE status + // if it does not receive the indication within a 14 sec after receiving the + // REMOVE_NODE_STATUS_NODE_FOUND status. + break; + + case RemoveNodeStatus.RemovingController: + case RemoveNodeStatus.RemovingSlave: { + // the payload contains the node ID + const { nodeId } = parseNodeID( + this.payload.subarray(2), + this.host.nodeIdType, + ); + this.statusContext = { nodeId }; + break; + } + } + } else { + this.callbackId = options.callbackId; + this.status = options.status; + if ("nodeId" in options) { + this.statusContext = { nodeId: options.nodeId }; } } } @@ -164,6 +215,18 @@ export class RemoveNodeFromNetworkRequestStatusReport return this.status !== RemoveNodeStatus.Failed; } + public serialize(): Buffer { + this.payload = Buffer.from([this.callbackId, this.status]); + if (this.statusContext?.nodeId != undefined) { + this.payload = Buffer.concat([ + this.payload, + encodeNodeID(this.statusContext.nodeId, this.host.nodeIdType), + ]); + } + + return super.serialize(); + } + public readonly status: RemoveNodeStatus; public readonly statusContext: RemoveNodeStatusContext | undefined; }