/**
	 * @internal
	 * Starts or resumes the interview of a Z-Wave node. It is advised to NOT
	 * await this method as it can take a very long time (minutes to hours)!
	 *
	 * WARNING: Do not call this method from application code. To refresh the information
	 * for a specific node, use `node.refreshInfo()` instead
	 */
public async interviewNode(node: ZWaveNode): Promise<void> {
		if (node.interviewStage === InterviewStage.Complete) {
			return;
		}

		// Avoid having multiple restart timeouts active
		if (this.retryNodeInterviewTimeouts.has(node.id)) {
			clearTimeout(this.retryNodeInterviewTimeouts.get(node.id)!);
			this.retryNodeInterviewTimeouts.delete(node.id);
		}

		// Drop all pending messages that come from a previous interview attempt
		this.rejectTransactions(
			(t) =>
				t.message.getNodeId() === node.id &&
				(t.priority === MessagePriority.NodeQuery ||
					t.tag === "interview"),
			"The interview was restarted",
			ZWaveErrorCodes.Controller_InterviewRestarted,
		);

		const maxInterviewAttempts = this.options.attempts.nodeInterview;

		try {
			if (!(await node.interview())) {
				// Find out if we may retry the interview
				if (node.status === NodeStatus.Dead) {
					this.controllerLog.logNode(
						node.id,
						`Interview attempt (${node.interviewAttempts}/${maxInterviewAttempts}) failed, node is dead.`,
						"warn",
					);
					node.emit("interview failed", node, {
						errorMessage: "The node is dead",
						isFinal: true,
					});
				} else if (node.interviewAttempts < maxInterviewAttempts) {
					// This is most likely because the node is unable to handle our load of requests now. Give it some time
					const retryTimeout = Math.min(
						30000,
						node.interviewAttempts * 5000,
					);
					this.controllerLog.logNode(
						node.id,
						`Interview attempt ${node.interviewAttempts}/${maxInterviewAttempts} failed, retrying in ${retryTimeout} ms...`,
						"warn",
					);
					node.emit("interview failed", node, {
						errorMessage: `Attempt ${node.interviewAttempts}/${maxInterviewAttempts} failed`,
						isFinal: false,
						attempt: node.interviewAttempts,
						maxAttempts: maxInterviewAttempts,
					});
					// Schedule the retry and remember the timeout instance
					this.retryNodeInterviewTimeouts.set(
						node.id,
						setTimeout(() => {
							this.retryNodeInterviewTimeouts.delete(node.id);
							void this.interviewNode(node);
						}, retryTimeout).unref(),
					);
				} else {
					this.controllerLog.logNode(
						node.id,
						`Failed all interview attempts, giving up.`,
						"warn",
					);
					node.emit("interview failed", node, {
						errorMessage: `Maximum interview attempts reached`,
						isFinal: true,
						attempt: maxInterviewAttempts,
						maxAttempts: maxInterviewAttempts,
					});
				}
			} else if (
				node.manufacturerId != undefined &&
				node.productType != undefined &&
				node.productId != undefined &&
				node.firmwareVersion != undefined &&
				!node.deviceConfig &&
				process.env.NODE_ENV !== "test"
			) {
				// The interview succeeded, but we don't have a device config for this node.
				// Report it, so we can add a config file

				// eslint-disable-next-line @typescript-eslint/no-empty-function
				void reportMissingDeviceConfig(node as any).catch(() => {});
			}
		} catch (e) {
			if (isZWaveError(e)) {
				if (
					e.code === ZWaveErrorCodes.Driver_NotReady ||
					e.code === ZWaveErrorCodes.Controller_NodeRemoved
				) {
					// This only happens when a node is removed during the interview - we don't log this
					return;
				} else if (
					e.code === ZWaveErrorCodes.Controller_InterviewRestarted
				) {
					// The interview was restarted by a user - we don't log this
					return;
				}
				this.controllerLog.logNode(
					node.id,
					`Error during node interview: ${e.message}`,
					"error",
				);
			} else {
				throw e;
			}
		}
	}


/** Adds the necessary event handlers for a node instance */
private addNodeEventHandlers(node: ZWaveNode): void {
		node.on("wake up", this.onNodeWakeUp.bind(this))
			.on("sleep", this.onNodeSleep.bind(this))
			.on("alive", this.onNodeAlive.bind(this))
			.on("dead", this.onNodeDead.bind(this))
			.on("interview completed", this.onNodeInterviewCompleted.bind(this))
			.on("ready", this.onNodeReady.bind(this))
			.on(
				"firmware update finished",
				this.onNodeFirmwareUpdated.bind(this),
			);
	}


/** Removes a node's event handlers that were added with addNodeEventHandlers */
private removeNodeEventHandlers(node: ZWaveNode): void {
		node.removeAllListeners("wake up")
			.removeAllListeners("sleep")
			.removeAllListeners("alive")
			.removeAllListeners("dead")
			.removeAllListeners("interview completed")
			.removeAllListeners("ready")
			.removeAllListeners("firmware update finished");
	}


/** Is called when a node wakes up */
private onNodeWakeUp(node: ZWaveNode, oldStatus: NodeStatus): void {
		this.controllerLog.logNode(
			node.id,
			`The node is ${
				oldStatus === NodeStatus.Unknown ? "" : "now "
			}awake.`,
		);

		// Make sure to handle the pending messages as quickly as possible
		if (oldStatus === NodeStatus.Asleep) {
			this.sendThread.send({
				type: "reduce",
				reducer: ({ message }) => {
					// Ignore messages that are not for this node
					if (message.getNodeId() !== node.id)
						return { type: "keep" };
					// Resolve pings, so we don't need to send them (we know the node is awake)
					if (messageIsPing(message))
						return { type: "resolve", message: undefined };
					// Re-queue all other transactions for this node, so they get added in front of the others
					return { type: "requeue" };
				},
			});
		}
	}


/** Is called when a node goes to sleep */
private onNodeSleep(node: ZWaveNode, oldStatus: NodeStatus): void {
		this.controllerLog.logNode(
			node.id,
			`The node is ${
				oldStatus === NodeStatus.Unknown ? "" : "now "
			}asleep.`,
		);

		// Move all its pending messages to the WakeupQueue
		// This clears the current transaction and continues sending the next messages
		this.moveMessagesToWakeupQueue(node.id);
	}


/** Is called when a previously dead node starts communicating again */
private onNodeAlive(node: ZWaveNode, oldStatus: NodeStatus): void {
		this.controllerLog.logNode(
			node.id,
			`The node is ${
				oldStatus === NodeStatus.Unknown ? "" : "now "
			}alive.`,
		);
		if (
			oldStatus === NodeStatus.Dead &&
			node.interviewStage !== InterviewStage.Complete
		) {
			void this.interviewNode(node);
		}
	}


/** Is called when a node is marked as dead */
private onNodeDead(node: ZWaveNode, oldStatus: NodeStatus): void {
		this.controllerLog.logNode(
			node.id,
			`The node is ${
				oldStatus === NodeStatus.Unknown ? "" : "now "
			}dead.`,
		);

		// This could mean that we need to ignore it in the all nodes ready check,
		// so perform the check again
		this.checkAllNodesReady();
	}


/** Is called when a node is ready to be used */
private onNodeReady(node: ZWaveNode): void {
		this._nodesReady.add(node.id);
		this.controllerLog.logNode(node.id, "The node is ready to be used");

		// Regularly query listening nodes for updated values
		node.scheduleManualValueRefreshes();

		this.checkAllNodesReady();
	}


/** Checks if all nodes are ready and emits the "all nodes ready" event if they are */
private checkAllNodesReady(): void {
		// Only emit "all nodes ready" once
		if (this._nodesReadyEventEmitted) return;

		for (const [id, node] of this.controller.nodes) {
			// Ignore dead nodes or the all nodes ready event will never be emitted without physical user interaction
			if (node.status === NodeStatus.Dead) continue;

			if (!this._nodesReady.has(id)) return;
		}
		// All nodes are ready
		this.controllerLog.print("All nodes are ready to be used");
		this.emit("all nodes ready");
		this._nodesReadyEventEmitted = true;

		// We know we have all data, this is the time to send statistics (when enabled)
		void this.compileAndSendStatistics().catch(() => {
			/* ignore */
		});
	}


/**
	 * Enable sending usage statistics. Although this does not include any sensitive information, we expect that you
	 * inform your users before enabling statistics.
	 */
public enableStatistics(
		appInfo: Pick<AppInfo, "applicationName" | "applicationVersion">,
	): void {
		if (this._statisticsEnabled) return;
		this._statisticsEnabled = true;

		if (
			!isObject(appInfo) ||
			typeof appInfo.applicationName !== "string" ||
			typeof appInfo.applicationVersion !== "string"
		) {
			throw new ZWaveError(
				`The application statistics must be an object with two string properties "applicationName" and "applicationVersion"!`,
				ZWaveErrorCodes.Driver_InvalidOptions,
			);
		} else if (appInfo.applicationName.length > 100) {
			throw new ZWaveError(
				`The applicationName for statistics must be maximum 100 characters long!`,
				ZWaveErrorCodes.Driver_InvalidOptions,
			);
		} else if (appInfo.applicationVersion.length > 100) {
			throw new ZWaveError(
				`The applicationVersion for statistics must be maximum 100 characters long!`,
				ZWaveErrorCodes.Driver_InvalidOptions,
			);
		}

		this.statisticsAppInfo = appInfo;

		// If we're already ready, send statistics
		if (this._nodesReadyEventEmitted) {
			void this.compileAndSendStatistics().catch(() => {
				/* ignore */
			});
		}
	}


/**
	 * Disable sending usage statistics
	 */
public disableStatistics(): void {
		this._statisticsEnabled = false;
		this.statisticsAppInfo = undefined;
		if (this.statisticsTimeout) {
			clearTimeout(this.statisticsTimeout);
			this.statisticsTimeout = undefined;
		}
	}


/** @internal */
// eslint-disable-next-line @typescript-eslint/require-await
public async getUUID(): Promise<string> {
		// To anonymously identify a network, we create a unique ID and use it to salt the Home ID
		if (!this._valueDB!.has("uuid")) {
			this._valueDB!.set("uuid", randomBytes(32).toString("hex"));
		}
		const ret = this._valueDB!.get("uuid") as string;
		return ret;
	}


private async compileAndSendStatistics(): Promise<void> {
		// Don't send anything if statistics are not enabled
		if (!this.statisticsEnabled || !this.statisticsAppInfo) return;

		if (this.statisticsTimeout) {
			clearTimeout(this.statisticsTimeout);
			this.statisticsTimeout = undefined;
		}

		let success: number | boolean = false;
		try {
			const statistics = await compileStatistics(this, {
				driverVersion: libVersion,
				...this.statisticsAppInfo,
				nodeVersion: process.versions.node,
				os: process.platform,
				arch: process.arch,
			});
			success = await sendStatistics(statistics);
		} catch {
			// Didn't work - try again in a few hours
			success = false;
		} finally {
			if (typeof success === "number") {
				this.driverLog.print(
					`Sending usage statistics was rate limited - next attempt scheduled in ${success} seconds.`,
					"verbose",
				);
				// Wait at most 6 hours to try again
				const retryMs = Math.max(
					timespan.minutes(1),
					Math.min(success * 1000, timespan.hours(6)),
				);
				this.statisticsTimeout = setTimeout(() => {
					void this.compileAndSendStatistics();
				}, retryMs).unref();
			} else {
				this.driverLog.print(
					success
						? `Usage statistics sent - next transmission scheduled in 23 hours.`
						: `Failed to send usage statistics - next transmission scheduled in 6 hours.`,
					"verbose",
				);
				this.statisticsTimeout = setTimeout(() => {
					void this.compileAndSendStatistics();
				}, timespan.hours(success ? 23 : 6)).unref();
			}
		}
	}


/** Is called when a node interview is completed */
private onNodeInterviewCompleted(node: ZWaveNode): void {
		this.debounceSendNodeToSleep(node);
	}


/** This is called when a new node has been added to the network */
private onNodeAdded(node: ZWaveNode): void {
		this.addNodeEventHandlers(node);
		if (!this.options.interview.skipInterview) {
			// Interview the node
			// don't await the interview, because it may take a very long time
			// if a node is asleep
			void this.interviewNode(node);
		}
	}


/** This is called when a node was removed from the network */
private onNodeRemoved(node: ZWaveNode, replaced: boolean): void {
		// Remove all listeners
		this.removeNodeEventHandlers(node);
		// purge node values from the DB
		node.valueDB.clear();

		this.rejectAllTransactionsForNode(
			node.id,
			"The node was removed from the network",
			ZWaveErrorCodes.Controller_NodeRemoved,
		);

		if (!replaced) {
			// Asynchronously remove the node from all possible associations, ignore potential errors
			// but only if the node is not getting replaced, because the removal will interfere with
			// bootstrapping the new node
			this.controller
				.removeNodeFromAllAssociations(node.id)
				.catch((err) => {
					this.driverLog.print(
						`Failed to remove node ${node.id} from all associations: ${err.message}`,
						"error",
					);
				});
		}

		// And clean up all remaining resources used by the node
		node.destroy();

		// If this was a failed node it could mean that all nodes are now ready
		this.checkAllNodesReady();
	}


/** This is called when a node's firmware was updated */
private onNodeFirmwareUpdated(
		node: ZWaveNode,
		status: FirmwareUpdateStatus,
		waitTime?: number,
	): void {
		// Don't do this for non-successful updates
		if (status < FirmwareUpdateStatus.OK_WaitingForActivation) return;

		// Wait at least 5 seconds
		if (!waitTime) waitTime = 5;
		this.controllerLog.logNode(
			node.id,
			`Firmware updated, scheduling interview in ${waitTime} seconds...`,
		);
		// We reuse the retryNodeInterviewTimeouts here because they serve a similar purpose
		this.retryNodeInterviewTimeouts.set(
			node.id,
			setTimeout(() => {
				this.retryNodeInterviewTimeouts.delete(node.id);
				void node.refreshInfo();
			}, waitTime * 1000).unref(),
		);
	}


/** Checks if there are any pending messages for the given node */
private hasPendingMessages(node: ZWaveNode): boolean {
		// First check if there are messages in the queue
		if (
			this.hasPendingTransactions(
				(t) => t.message.getNodeId() === node.id,
			)
		) {
			return true;
		}

		// Then check if there are scheduled polls
		return node.scheduledPolls.size > 0;
	}


/** Checks if there are any pending transactions that match the given predicate */
public hasPendingTransactions(
		predicate: (t: Transaction) => boolean,
	): boolean {
		const { queue, currentTransaction } = this.sendThread.state.context;
		return (
			(currentTransaction && predicate(currentTransaction)) ||
			!!queue.find((t) => predicate(t))
		);
	}


/**
	 * Retrieves the maximum version of a command class the given endpoint supports.
	 * Returns 0 when the CC is not supported. Also returns 0 when the node was not found.
	 * Falls back to querying the root endpoint if an endpoint was not found on the node
	 *
	 * @param cc The command class whose version should be retrieved
	 * @param nodeId The node for which the CC version should be retrieved
	 * @param endpointIndex The endpoint in question
	 */
public getSupportedCCVersionForEndpoint(
		cc: CommandClasses,
		nodeId: number,
		endpointIndex: number = 0,
	): number {
		if (!this._controller?.nodes.has(nodeId)) {
			return 0;
		}
		const node = this.controller.nodes.get(nodeId)!;
		const endpoint = node.getEndpoint(endpointIndex);
		if (endpoint) return endpoint.getCCVersion(cc);
		// We sometimes receive messages from an endpoint, but can't find that endpoint.
		// In that case fall back to the root endpoint to determine the supported version.
		return node.getCCVersion(cc);
	}


/**
	 * Retrieves the maximum version of a command class that can be used to communicate with a node.
	 * Returns 1 if the node claims that it does not support a CC.
	 * Throws if the CC is not implemented in this library yet.
	 *
	 * @param cc The command class whose version should be retrieved
	 * @param nodeId The node for which the CC version should be retrieved
	 * @param endpointIndex The endpoint for which the CC version should be retrieved
	 */
public getSafeCCVersionForNode(
		cc: CommandClasses,
		nodeId: number,
		endpointIndex: number = 0,
	): number {
		const supportedVersion = this.getSupportedCCVersionForEndpoint(
			cc,
			nodeId,
			endpointIndex,
		);
		if (supportedVersion === 0) {
			// For unsupported CCs use version 1, no matter what
			return 1;
		} else {
			// For supported versions find the maximum version supported by both the
			// node and this library
			const implementedVersion = getImplementedVersion(cc);
			if (
				implementedVersion !== 0 &&
				implementedVersion !== Number.POSITIVE_INFINITY
			) {
				return Math.min(supportedVersion, implementedVersion);
			}
			throw new ZWaveError(
				"Cannot retrieve the version of a CC that is not implemented",
				ZWaveErrorCodes.CC_NotSupported,
			);
		}
	}


/**
	 * Performs a hard reset on the controller. This wipes out all configuration!
	 *
	 * The returned Promise resolves when the hard reset has been performed.
	 * It does not wait for the initialization process which is started afterwards.
	 */
public async hardReset(): Promise<void> {
		this.ensureReady(true);
		// Calling ensureReady with true ensures that _controller is defined
		await this._controller!.hardReset();

		// Clean up
		this.rejectTransactions(() => true, `The controller was hard-reset`);
		this.sendNodeToSleepTimers.forEach((timeout) => clearTimeout(timeout));
		this.sendNodeToSleepTimers.clear();
		this.retryNodeInterviewTimeouts.forEach((timeout) =>
			clearTimeout(timeout),
		);
		this.retryNodeInterviewTimeouts.clear();

		this._controllerInterviewed = false;
		void this.initializeControllerAndNodes();
	}


/**
	 * Ensures that the driver is ready to communicate (serial port open and not destroyed).
	 * If desired, also checks that the controller interview has been completed.
	 */
private ensureReady(includingController: boolean = false): void {
		if (!this._wasStarted || !this._isOpen || this.wasDestroyed) {
			throw new ZWaveError(
				"The driver is not ready or has been destroyed",
				ZWaveErrorCodes.Driver_NotReady,
			);
		}
		if (includingController && !this._controllerInterviewed) {
			throw new ZWaveError(
				"The controller is not ready yet",
				ZWaveErrorCodes.Driver_NotReady,
			);
		}
	}


/**
	 * Terminates the driver instance and closes the underlying serial connection.
	 * Must be called under any circumstances.
	 */
public async destroy(): Promise<void> {
		// Ensure this is only called once and all subsequent calls block
		if (this._destroyPromise) return this._destroyPromise;
		this._destroyPromise = createDeferredPromise();

		this.driverLog.print("destroying driver instance...");

		// First stop the send thread machine and close the serial port, so nothing happens anymore
		if (this.sendThread.initialized) this.sendThread.stop();
		if (this.serial != undefined) {
			if (this.serial.isOpen) await this.serial.close();
			this.serial = undefined;
		}

		try {
			// Attempt to save the network to cache
			await this.saveNetworkToCacheInternal();
		} catch (e) {
			this.driverLog.print(
				`Saving the network to cache failed: ${getErrorMessage(e)}`,
				"error",
			);
		}

		try {
			// Attempt to close the value DBs
			await this._valueDB?.close();
			await this._metadataDB?.close();
		} catch (e) {
			this.driverLog.print(
				`Closing the value DBs failed: ${getErrorMessage(e)}`,
				"error",
			);
		}

		// Remove all timeouts
		for (const timeout of [
			this.saveToCacheTimer,
			...this.sendNodeToSleepTimers.values(),
			...this.retryNodeInterviewTimeouts.values(),
			this.statisticsTimeout,
		]) {
			if (timeout) clearTimeout(timeout);
		}

		// Destroy all nodes
		this._controller?.nodes.forEach((n) => n.destroy());

		process.removeListener("exit", this._cleanupHandler);
		process.removeListener("SIGINT", this._cleanupHandler);
		process.removeListener("uncaughtException", this._cleanupHandler);

		// destroy loggers as the very last thing
		this._logContainer.destroy();

		this._destroyPromise.resolve();
	}


private serialport_onError(err: Error): void {
		this.emit("error", err);
	}


/**
	 * Is called when the serial port has received a single-byte message or a complete message buffer
	 */
private async serialport_onData(
		data:
			| Buffer
			| MessageHeaders.ACK
			| MessageHeaders.CAN
			| MessageHeaders.NAK,
	): Promise<void> {
		if (typeof data === "number") {
			switch (data) {
				// single-byte messages - just forward them to the send thread
				case MessageHeaders.ACK: {
					this.sendThread.send("ACK");
					return;
				}
				case MessageHeaders.NAK: {
					this.sendThread.send("NAK");
					this._controller?.incrementStatistics("NAK");
					return;
				}
				case MessageHeaders.CAN: {
					this.sendThread.send("CAN");
					this._controller?.incrementStatistics("CAN");
					return;
				}
			}
		}

		let msg: Message | undefined;
		try {
			// Parse the message while remembering potential decoding errors in embedded CCs
			// This way we can log the invalid CC contents
			msg = Message.from(this, data);
			// Then ensure there are no errors
			if (isCommandClassContainer(msg)) {
				assertValidCCs(msg);
				msg.getNodeUnsafe()?.incrementStatistics("commandsRX");
			} else {
				this._controller?.incrementStatistics("messagesRX");
			}
			// all good, send ACK
			await this.writeHeader(MessageHeaders.ACK);
		} catch (e: any) {
			try {
				if (await this.handleSecurityS2DecodeError(e, msg)) {
					// TODO
				} else {
					const response = this.handleDecodeError(e, data, msg);
					if (response) await this.writeHeader(response);
					if (isCommandClassContainer(msg)) {
						msg.getNodeUnsafe()?.incrementStatistics(
							"commandsDroppedRX",
						);

						// Figure out if the command was received with supervision encapsulation
						const supervisionSessionId = SupervisionCC.getSessionId(
							msg.command,
						);
						if (
							supervisionSessionId !== undefined &&
							msg.command instanceof InvalidCC
						) {
							// If it was, we need to notify the sender that we couldn't decode the command
							await msg
								.getNodeUnsafe()
								?.commandClasses.Supervision.sendReport({
									sessionId: supervisionSessionId,
									moreUpdatesFollow: false,
									status: SupervisionStatus.NoSupport,
									secure: msg.command.secure,
								});
							return;
						}
					} else {
						this._controller?.incrementStatistics(
							"messagesDroppedRX",
						);
					}
				}
			} catch (ee) {
				if (ee instanceof Error) {
					if (/serial port is not open/.test(ee.message)) {
						this.emit("error", ee);
						void this.destroy();
						return;
					}
					// Print something, so we know what is wrong
					this._driverLog.print(ee.stack ?? ee.message, "error");
				}
			}
			// Don't keep handling the message
			msg = undefined;
		}

		// If the message could be decoded, forward it to the send thread
		if (msg) {
			let wasMessageLogged = false;
			if (isCommandClassContainer(msg)) {
				// SecurityCCCommandEncapsulationNonceGet is two commands in one, but
				// we're not set up to handle things like this. Reply to the nonce get
				// and handle the encapsulation part normally
				if (
					msg.command instanceof
					SecurityCCCommandEncapsulationNonceGet
				) {
					void msg.getNodeUnsafe()?.handleSecurityNonceGet();
				}

				// Transport Service commands must be handled before assembling partial CCs
				if (isTransportServiceEncapsulation(msg.command)) {
					// Log Transport Service commands before doing anything else
					this.driverLog.logMessage(msg, {
						secondaryTags: ["partial"],
						direction: "inbound",
					});
					wasMessageLogged = true;

					void this.handleTransportServiceCommand(msg.command).catch(
						() => {
							// Don't care about errors in incoming transport service commands
						},
					);
				}

				// Assemble partial CCs on the driver level. Only forward complete messages to the send thread machine
				if (!this.assemblePartialCCs(msg)) return;
				// Transport Service CC can be eliminated from the encapsulation stack, since it is always the outermost CC
				if (isTransportServiceEncapsulation(msg.command)) {
					msg.command = msg.command.encapsulated;
					// Now we do want to log the command again, so we can see what was inside
					wasMessageLogged = false;
				}
			}

			try {
				if (!wasMessageLogged) {
					this.driverLog.logMessage(msg, { direction: "inbound" });
				}

				if (process.env.NODE_ENV !== "test") {
					// Enrich error data in case something goes wrong
					Sentry.addBreadcrumb({
						category: "message",
						timestamp: Date.now() / 1000,
						type: "debug",
						data: {
							direction: "inbound",
							msgType: msg.type,
							functionType: msg.functionType,
							name: msg.constructor.name,
							nodeId: msg.getNodeId(),
							...msg.toLogEntry(),
						},
					});
				}
			} catch (e) {
				// We shouldn't throw just because logging a message fails
				this.driverLog.print(
					`Logging a message failed: ${getErrorMessage(e)}`,
				);
			}
			this.sendThread.send({ type: "message", message: msg });
		}
	}


/** Handles a decoding error and returns the desired reply to the stick */
private handleDecodeError(
		e: Error,
		data: Buffer,
		msg: Message | undefined,
	): MessageHeaders | undefined {
		if (isZWaveError(e)) {
			switch (e.code) {
				case ZWaveErrorCodes.PacketFormat_Invalid:
				case ZWaveErrorCodes.PacketFormat_Checksum:
				case ZWaveErrorCodes.PacketFormat_Truncated:
					this.driverLog.print(
						`Dropping message because it contains invalid data`,
						"warn",
					);
					return MessageHeaders.NAK;

				case ZWaveErrorCodes.Deserialization_NotImplemented:
				case ZWaveErrorCodes.CC_NotImplemented:
					this.driverLog.print(
						`Dropping message because it could not be deserialized: ${e.message}`,
						"warn",
					);
					return MessageHeaders.ACK;

				case ZWaveErrorCodes.Driver_NotReady:
					this.driverLog.print(
						`Dropping message because the driver is not ready to handle it yet.`,
						"warn",
					);
					return MessageHeaders.ACK;

				case ZWaveErrorCodes.PacketFormat_InvalidPayload:
					if (msg) {
						this.driverLog.print(
							`Dropping message with invalid payload`,
							"warn",
						);
						try {
							this.driverLog.logMessage(msg, {
								direction: "inbound",
							});
						} catch (e) {
							// We shouldn't throw just because logging a message fails
							this.driverLog.print(
								`Logging a message failed: ${getErrorMessage(
									e,
								)}`,
							);
						}
					} else {
						this.driverLog.print(
							`Dropping message with invalid payload${
								typeof e.context === "string"
									? ` (Reason: ${e.context})`
									: ""
							}:
0x${data.toString("hex")}`,
							"warn",
						);
					}
					return MessageHeaders.ACK;

				case ZWaveErrorCodes.Driver_NoSecurity:
				case ZWaveErrorCodes.Security2CC_NotInitialized:
					this.driverLog.print(
						`Dropping message because network keys are not set or the driver is not yet ready to receive secure messages.`,
						"warn",
					);
					return MessageHeaders.ACK;
			}
		} else {
			if (/database is not open/.test(e.message)) {
				// The JSONL-DB is not open yet
				this.driverLog.print(
					`Dropping message because the driver is not ready to handle it yet.`,
					"warn",
				);
				return MessageHeaders.ACK;
			}
		}
		// Pass all other errors through
		throw e;
	}


private async handleSecurityS2DecodeError(
		e: Error,
		msg: Message | undefined,
	): Promise<boolean> {
		if (!isZWaveError(e)) return false;
		if (
			(e.code === ZWaveErrorCodes.Security2CC_NoSPAN ||
				e.code === ZWaveErrorCodes.Security2CC_CannotDecode) &&
			isCommandClassContainer(msg)
		) {
			// Decoding the command failed because no SPAN has been established with the other node
			const nodeId = msg.getNodeId()!;
			// If the node isn't known, ignore this error
			const node = this._controller?.nodes.get(nodeId);
			if (!node) return false;

			// Before we can send anything, ACK the command
			await this.writeHeader(MessageHeaders.ACK);

			this.driverLog.logMessage(msg, { direction: "inbound" });
			node.incrementStatistics("commandsDroppedRX");

			// We might receive this before the node has been interviewed. If that case, we need to mark Security S2 as
			// supported or we won't ever be able to communicate with the node
			if (node.interviewStage < InterviewStage.NodeInfo) {
				node.addCC(CommandClasses["Security 2"], {
					isSupported: true,
					version: 1,
				});
			}

			// Ensure that we're not flooding the queue with unnecessary NonceReports
			const isS2NonceReport = (t: Transaction) =>
				t.message.getNodeId() === nodeId &&
				isCommandClassContainer(t.message) &&
				t.message.command instanceof Security2CCNonceReport;

			const message: string =
				e.code === ZWaveErrorCodes.Security2CC_CannotDecode
					? "Message authentication failed"
					: "No SPAN is established yet";

			if (!this.hasPendingTransactions(isS2NonceReport)) {
				this.controllerLog.logNode(nodeId, {
					message: `${message}, cannot decode command. Requesting a nonce...`,
					level: "verbose",
					direction: "outbound",
				});
				// Send the node our nonce
				node.commandClasses["Security 2"].sendNonce().catch(() => {
					// Ignore errors
				});
			} else {
				this.controllerLog.logNode(nodeId, {
					message: `${message}, cannot decode command.`,
					level: "verbose",
					direction: "none",
				});
			}

			return true;
		}
		return false;
	}


/** Checks if a transaction failed because a node didn't respond in time */
private isMissingNodeACK(
		transaction: Transaction,
		e: ZWaveError,
	): transaction is Transaction & {
		message: SendDataRequest | SendDataBridgeRequest;
	} {
		return (
			// If the node does not acknowledge our request, it is either asleep or dead
			e.code === ZWaveErrorCodes.Controller_CallbackNOK &&
			(transaction.message instanceof SendDataRequest ||
				transaction.message instanceof SendDataBridgeRequest) &&
			// Ignore pre-transmit handshakes because the actual transaction will be retried
			transaction.priority !== MessagePriority.PreTransmitHandshake
		);
	}


/**
	 * Handles the case that a node failed to respond in time.
	 * Returns `true` if the transaction failure was handled, `false` if it needs to be rejected.
	 */
private handleMissingNodeACK(
		transaction: Transaction & {
			message: SendDataRequest | SendDataBridgeRequest;
		},
	): boolean {
		const node = transaction.message.getNodeUnsafe();
		if (!node) return false; // This should never happen, but whatever

		if (!transaction.changeNodeStatusOnTimeout) {
			// The sender of this transaction doesn't want it to change the status of the node
			return false;
		} else if (node.canSleep) {
			this.controllerLog.logNode(
				node.id,
				`The node did not respond after ${transaction.message.maxSendAttempts} attempts.
It is probably asleep, moving its messages to the wakeup queue.`,
				"warn",
			);
			// Mark the node as asleep
			// The handler for the asleep status will move the messages to the wakeup queue
			// We need to re-add the current transaction if that is allowed because otherwise it will be dropped silently
			if (this.mayMoveToWakeupQueue(transaction)) {
				this.sendThread.send({ type: "add", transaction });
			} else {
				transaction.promise.reject(
					new ZWaveError(
						`The node is asleep`,
						ZWaveErrorCodes.Controller_MessageDropped,
					),
				);
			}
			node.markAsAsleep();
			return true;
		} else {
			const errorMsg = `Node ${node.id} did not respond after ${transaction.message.maxSendAttempts} attempts, it is presumed dead`;
			this.controllerLog.logNode(node.id, errorMsg, "warn");

			node.markAsDead();
			this.rejectAllTransactionsForNode(node.id, errorMsg);
			// The above call will reject the transaction, no need to do it again
			return false;
		}
	}


private getPartialCCSession(
		command: CommandClass,
		createIfMissing: false,
	): { partialSessionKey: string; session?: CommandClass[] } | undefined;


private getPartialCCSession(
		command: CommandClass,
		createIfMissing: true,
	): { partialSessionKey: string; session: CommandClass[] } | undefined;


private getPartialCCSession(
		command: CommandClass,
		createIfMissing: boolean,
	): { partialSessionKey: string; session?: CommandClass[] } | undefined {
		const sessionId = command.getPartialCCSessionId();

		if (sessionId) {
			// This CC belongs to a partial session
			const partialSessionKey = JSON.stringify({
				nodeId: command.nodeId,
				ccId: command.ccId,
				ccCommand: command.ccCommand!,
				...sessionId,
			});
			if (
				createIfMissing &&
				!this.partialCCSessions.has(partialSessionKey)
			) {
				this.partialCCSessions.set(partialSessionKey, []);
			}
			return {
				partialSessionKey,
				session: this.partialCCSessions.get(partialSessionKey),
			};
		}
	}


/**
	 * Assembles partial CCs of in a message body. Returns `true` when the message is complete and can be handled further.
	 * If the message expects another partial one, this returns `false`.
	 */
private assemblePartialCCs(msg: Message & ICommandClassContainer): boolean {
		let command: CommandClass | undefined = msg.command;
		// We search for the every CC that provides us with a session ID
		// There might be newly-completed CCs that contain a partial CC,
		// so investigate the entire CC encapsulation stack.
		while (true) {
			const { partialSessionKey, session } =
				this.getPartialCCSession(command, true) ?? {};
			if (session) {
				// This CC belongs to a partial session
				if (command.expectMoreMessages(session)) {
					// this is not the final one, store it
					session.push(command);
					if (!isTransportServiceEncapsulation(msg.command)) {
						// and don't handle the command now
						this.driverLog.logMessage(msg, {
							secondaryTags: ["partial"],
							direction: "inbound",
						});
					}
					return false;
				} else {
					// this is the final one, merge the previous responses
					this.partialCCSessions.delete(partialSessionKey!);
					try {
						command.mergePartialCCs(session);
					} catch (e) {
						if (isZWaveError(e)) {
							switch (e.code) {
								case ZWaveErrorCodes.Deserialization_NotImplemented:
								case ZWaveErrorCodes.CC_NotImplemented:
									this.driverLog.print(
										`Dropping message because it could not be deserialized: ${e.message}`,
										"warn",
									);
									// Don't continue handling this message
									return false;

								case ZWaveErrorCodes.PacketFormat_InvalidPayload:
									this.driverLog.print(
										`Could not assemble partial CCs because the payload is invalid. Dropping them.`,
										"warn",
									);
									// Don't continue handling this message
									return false;

								case ZWaveErrorCodes.Driver_NotReady:
									this.driverLog.print(
										`Could not assemble partial CCs because the driver is not ready yet. Dropping them`,
										"warn",
									);
									// Don't continue handling this message
									return false;
							}
						}
						throw e;
					}
					// Assembling this CC was successful - but it might contain another partial CC
				}
			} else {
				// No partial CC, just continue
			}

			// If this is an encapsulating CC, we need to look one level deeper
			if (isEncapsulatingCommandClass(command)) {
				command = command.encapsulated;
			} else {
				break;
			}
		}
		return true;
	}


/** Is called when a Transport Service command is received */
private async handleTransportServiceCommand(
		command:
			| TransportServiceCCFirstSegment
			| TransportServiceCCSubsequentSegment,
	): Promise<void> {
		const nodeSessions = this.ensureNodeSessions(command.nodeId);
		// If this command belongs to an existing session, just forward it to the state machine
		const transportSession = nodeSessions.transportService.get(
			command.sessionId,
		);
		if (command instanceof TransportServiceCCFirstSegment) {
			// This is the first message in a sequence. Create or re-initialize the session
			// We don't delete finished sessions when the last message is received in order to be able to
			// handle when the SegmentComplete message gets lost. As soon as the node initializes a new session,
			// we do know that the previous one is finished.
			if (transportSession) {
				transportSession.interpreter.stop();
			}
			nodeSessions.transportService.clear();

			this.controllerLog.logNode(command.nodeId, {
				message: `Beginning Transport Service RX session #${command.sessionId}...`,
				level: "debug",
				direction: "inbound",
			});

			const RXStateMachine = createTransportServiceRXMachine(
				{
					requestMissingSegment: async (index: number) => {
						this.controllerLog.logNode(command.nodeId, {
							message: `Transport Service RX session #${command.sessionId}: Segment ${index} missing - requesting it...`,
							level: "debug",
							direction: "outbound",
						});
						const cc = new TransportServiceCCSegmentRequest(this, {
							nodeId: command.nodeId,
							sessionId: command.sessionId,
							datagramOffset:
								index * transportSession!.fragmentSize,
						});
						await this.sendCommand(cc, {
							maxSendAttempts: 1,
							priority: MessagePriority.Handshake,
						});
					},
					sendSegmentsComplete: async () => {
						this.controllerLog.logNode(command.nodeId, {
							message: `Transport Service RX session #${command.sessionId} complete`,
							level: "debug",
							direction: "outbound",
						});
						const cc = new TransportServiceCCSegmentComplete(this, {
							nodeId: command.nodeId,
							sessionId: command.sessionId,
						});
						await this.sendCommand(cc, {
							maxSendAttempts: 1,
							priority: MessagePriority.Handshake,
						});
					},
				},
				{
					// TODO: Figure out how to know which timeout is the correct one. For now use the larger one
					missingSegmentTimeout:
						TransportServiceTimeouts.requestMissingSegmentR2,
					numSegments: Math.ceil(
						command.datagramSize / command.partialDatagram.length,
					),
				},
			);

			const interpreter = interpret(RXStateMachine).start();
			nodeSessions.transportService.set(command.sessionId, {
				fragmentSize: command.partialDatagram.length,
				interpreter,
			});

			interpreter.onTransition((state) => {
				if (state.changed && state.value === "failure") {
					this.controllerLog.logNode(command.nodeId, {
						message: `Transport Service RX session #${command.sessionId} failed`,
						level: "error",
						direction: "none",
					});
					// TODO: Update statistics
					interpreter.stop();
					nodeSessions.transportService.delete(command.sessionId);
				}
			});
		} else {
			// This is a subsequent message in a sequence. Just forward it to the state machine if there is one
			if (transportSession) {
				transportSession.interpreter.send({
					type: "segment",
					index: Math.floor(
						command.datagramOffset / transportSession.fragmentSize,
					),
				});
			} else {
				// This belongs to a session we don't know... tell the sending node to try again
				const cc = new TransportServiceCCSegmentWait(this, {
					nodeId: command.nodeId,
					pendingSegments: 0,
				});
				await this.sendCommand(cc, {
					maxSendAttempts: 1,
					priority: MessagePriority.Handshake,
				});
			}
		}
	}


/**
	 * Is called when a message is received that does not belong to any ongoing transactions
	 * @param msg The decoded message
	 */
private async handleUnsolicitedMessage(msg: Message): Promise<void> {
		if (msg.type === MessageType.Request) {
			// This is a request we might have registered handlers for
			try {
				await this.handleRequest(msg);
			} catch (e) {
				if (
					isZWaveError(e) &&
					e.code === ZWaveErrorCodes.Driver_NotReady
				) {
					this.driverLog.print(
						`Cannot handle message because the driver is not ready to handle it yet.`,
						"warn",
					);
				} else {
					throw e;
				}
			}
		} else {
			this.driverLog.transactionResponse(msg, undefined, "unexpected");
			this.driverLog.print("unexpected response, discarding...", "warn");
		}
	}


/**
	 * Registers a handler for messages that are not handled by the driver as part of a message exchange.
	 * The handler function needs to return a boolean indicating if the message has been handled.
	 * Registered handlers are called in sequence until a handler returns `true`.
	 *
	 * @param fnType The function type to register the handler for
	 * @param handler The request handler callback
	 * @param oneTime Whether the handler should be removed after its first successful invocation
	 */
public registerRequestHandler<T extends Message>(
		fnType: FunctionType,
		handler: RequestHandler<T>,
		oneTime: boolean = false,
	): void {
		const handlers: RequestHandlerEntry<T>[] = this.requestHandlers.has(
			fnType,
		)
			? this.requestHandlers.get(fnType)!
			: [];
		const entry: RequestHandlerEntry<T> = { invoke: handler, oneTime };
		handlers.push(entry);
		this.driverLog.print(
			`added${oneTime ? " one-time" : ""} request handler for ${
				FunctionType[fnType]
			} (${num2hex(fnType)})...
${handlers.length} registered`,
		);
		this.requestHandlers.set(fnType, handlers as RequestHandlerEntry[]);
	}


/**
	 * Unregisters a message handler that has been added with `registerRequestHandler`
	 * @param fnType The function type to unregister the handler for
	 * @param handler The previously registered request handler callback
	 */
public unregisterRequestHandler(
		fnType: FunctionType,
		handler: RequestHandler,
	): void {
		const handlers = this.requestHandlers.has(fnType)
			? this.requestHandlers.get(fnType)!
			: [];
		for (let i = 0, entry = handlers[i]; i < handlers.length; i++) {
			// remove the handler if it was found
			if (entry.invoke === handler) {
				handlers.splice(i, 1);
				break;
			}
		}
		this.driverLog.print(
			`removed request handler for ${FunctionType[fnType]} (${fnType})...
${handlers.length} left`,
		);
		this.requestHandlers.set(fnType, handlers);
	}


/**
	 * Checks whether a CC may be handled or should be ignored.
	 * This method expects `cc` to be unwrapped.
	 */
private mayHandleUnsolicitedCommand(cc: CommandClass): boolean {
		// This should only be necessary for unsolicited commands, since the response matching
		// is pretty strict and looks at the encapsulation order

		// From SDS11847:
		// A controlling node MUST discard a received Report/Notification type command if it is
		// not received using S0 encapsulation and the corresponding Command Class is supported securely only

		if (
			cc.secure &&
			cc.ccId !== CommandClasses.Security &&
			cc.ccId !== CommandClasses["Security 2"]
		) {
			const commandName = cc.constructor.name;
			if (
				commandName.endsWith("Report") ||
				commandName.endsWith("Notification")
			) {
				// Check whether there was a security encapsulation
				if (
					cc.isEncapsulatedWith(CommandClasses.Security) ||
					cc.isEncapsulatedWith(CommandClasses["Security 2"])
				) {
					return true;
				}
				// none found, don't accept the CC
				this.controllerLog.logNode(
					cc.nodeId as number,
					`command must be encrypted but was received without Security encapsulation - discarding it...`,
					"warn",
				);
				return false;
			}
		}
		return true;
	}


/**
	 * Returns the next callback ID. Callback IDs are used to correllate requests
	 * to the controller/nodes with its response
	 */
public getNextCallbackId(): number {
		this.lastCallbackId = (this.lastCallbackId + 1) & 0xff;
		if (this.lastCallbackId < 1) this.lastCallbackId = 1;
		return this.lastCallbackId;
	}


private encapsulateCommands(msg: Message & ICommandClassContainer): void {
		// The encapsulation order (from outside to inside) is as follows:
		// 5. Any one of the following combinations:
		//   a. Security (S0 or S2) followed by transport service
		//   b. Transport Service
		//   c. Security (S0 or S2)
		//   d. CRC16
		// b and d are mutually exclusive, security is not
		// 4. Multi Channel
		// 3. Supervision
		// 2. Multi Command
		// 1. Encapsulated Command Class (payload), e.g. Basic Set

		// TODO: 2.

		// 3.
		if (SupervisionCC.requiresEncapsulation(msg.command)) {
			msg.command = SupervisionCC.encapsulate(this, msg.command);
		}

		// 4.
		if (MultiChannelCC.requiresEncapsulation(msg.command)) {
			msg.command = MultiChannelCC.encapsulate(this, msg.command);
		}

		// 5.

		// When a node supports S2 and has a valid security class, the command
		// must be S2-encapsulated
		const node = msg.command.getNode();
		if (node?.supportsCC(CommandClasses["Security 2"])) {
			const securityClass = node.getHighestSecurityClass();
			if (
				((securityClass != undefined &&
					securityClass !== SecurityClass.S0_Legacy) ||
					this.securityManager2?.tempKeys.has(node.id)) &&
				Security2CC.requiresEncapsulation(msg.command)
			) {
				msg.command = Security2CC.encapsulate(this, msg.command);
			}
		}
		// This check will return false for S2-encapsulated commands
		if (SecurityCC.requiresEncapsulation(msg.command)) {
			msg.command = SecurityCC.encapsulate(this, msg.command);
		}
	}


private unwrapCommands(msg: Message & ICommandClassContainer): void {
		// Unwrap encapsulating CCs until we get to the core
		while (
			isEncapsulatingCommandClass(msg.command) ||
			isMultiEncapsulatingCommandClass(msg.command)
		) {
			const unwrapped = msg.command.encapsulated;
			if (isArray(unwrapped)) {
				this.driverLog.print(
					`Received a command that contains multiple CommandClasses. This is not supported yet! Discarding the message...`,
					"warn",
				);
				return;
			}
			msg.command = unwrapped;
		}
	}


/**
	 * Sends a message to the Z-Wave stick.
	 * @param msg The message to send
	 * @param options (optional) Options regarding the message transmission
	 */
public async sendMessage<TResponse extends Message = Message>(
		msg: Message,
		options: SendMessageOptions = {},
	): Promise<TResponse> {
		this.ensureReady();

		let node: ZWaveNode | undefined;

		// Don't send messages to dead nodes
		if (isNodeQuery(msg) || isCommandClassContainer(msg)) {
			node = msg.getNodeUnsafe();
			if (!messageIsPing(msg) && node?.status === NodeStatus.Dead) {
				// Instead of throwing immediately, try to ping the node first - if it responds, continue
				if (!(await node.ping())) {
					throw new ZWaveError(
						`The message cannot be sent because node ${node.id} is dead`,
						ZWaveErrorCodes.Controller_MessageDropped,
					);
				}
			}
		}

		if (options.priority == undefined)
			options.priority = getDefaultPriority(msg);
		if (options.priority == undefined) {
			const className = msg.constructor.name;
			const msgTypeName = FunctionType[msg.functionType];
			throw new ZWaveError(
				`No default priority has been defined for ${className} (${msgTypeName}), so you have to provide one for your message`,
				ZWaveErrorCodes.Driver_NoPriority,
			);
		}

		if (options.supportCheck == undefined) options.supportCheck = true;
		if (
			options.supportCheck &&
			this._controller != undefined &&
			!this._controller.isFunctionSupported(msg.functionType)
		) {
			throw new ZWaveError(
				`Your hardware does not support the ${
					FunctionType[msg.functionType]
				} function`,
				ZWaveErrorCodes.Driver_NotSupported,
			);
		}

		// When sending a message to a node that is known to be sleeping,
		// the priority must be WakeUp, so the message gets deprioritized
		// in comparison with messages to awake nodes.
		// However there are a few exceptions...
		if (
			!!node &&
			// Pings can be used to check if a node is really asleep, so they should be sent regardless
			!messageIsPing(msg) &&
			// Nodes that can sleep and support the Wake Up CC should have their messages queued for wakeup
			node.canSleep &&
			(node.supportsCC(CommandClasses["Wake Up"]) ||
				// If we don't know the Wake Up support yet, also change the priority or RequestNodeInfoRequests will get stuck
				// in front of the queue
				node.interviewStage < InterviewStage.NodeInfo) &&
			// If we move multicasts to the wakeup queue, it is unlikely
			// that there is ever a points where all targets are awake
			!(msg instanceof SendDataMulticastRequest) &&
			!(msg instanceof SendDataMulticastBridgeRequest) &&
			// Handshake messages are meant to be sent immediately
			options.priority !== MessagePriority.Handshake &&
			options.priority !== MessagePriority.PreTransmitHandshake
		) {
			if (options.priority === MessagePriority.NodeQuery) {
				// Remember that this transaction was part of an interview
				options.tag = "interview";
			}
			options.priority = MessagePriority.WakeUp;
		}

		// create the transaction and enqueue it
		const promise = createDeferredPromise<TResponse>();
		const transaction = new Transaction(
			this,
			msg,
			promise,
			options.priority,
		);

		if (options.changeNodeStatusOnMissingACK != undefined) {
			transaction.changeNodeStatusOnTimeout =
				options.changeNodeStatusOnMissingACK;
		}
		transaction.tag = options.tag;

		// start sending now (maybe)
		this.sendThread.send({ type: "add", transaction });

		// If the transaction should expire, start the timeout
		let expirationTimeout: NodeJS.Timeout | undefined;
		if (options.expire) {
			expirationTimeout = setTimeout(() => {
				this.sendThread.send({
					type: "reduce",
					reducer: (t: Transaction) => {
						if (t === transaction)
							return {
								type: "reject",
								message: `The message has expired`,
								code: ZWaveErrorCodes.Controller_MessageExpired,
							};
						return { type: "keep" };
					},
				});
			}, options.expire).unref();
		}

		try {
			const ret = await promise;
			// The message was transmitted, so it can no longer expire
			if (expirationTimeout) clearTimeout(expirationTimeout);
			// Update statistics
			if (isSendData(msg)) {
				node?.incrementStatistics("commandsTX");
			} else {
				this._controller?.incrementStatistics("messagesTX");
			}
			// Track and potentially update the status of the node when communication succeeds
			if (node) {
				if (node.canSleep) {
					// Do not update the node status when we just responded to a nonce request
					if (options.priority !== MessagePriority.Handshake) {
						// If the node is not meant to be kept awake, try to send it back to sleep
						if (!node.keepAwake) {
							this.debounceSendNodeToSleep(node);
						}
						// The node must be awake because it answered
						node.markAsAwake();
					}
				} else if (node.status !== NodeStatus.Alive) {
					// The node status was unknown or dead - in either case it must be alive because it answered
					node.markAsAlive();
				}
			}
			return ret;
		} catch (e) {
			if (isZWaveError(e)) {
				if (
					// If a controller command failed (that is not SendData), pass the response/callback through
					(e.code === ZWaveErrorCodes.Controller_ResponseNOK ||
						e.code === ZWaveErrorCodes.Controller_CallbackNOK) &&
					e.context instanceof Message &&
					// We need to check the function type here because context can be the transmit reports
					e.context.functionType !== FunctionType.SendData &&
					e.context.functionType !== FunctionType.SendDataMulticast &&
					e.context.functionType !== FunctionType.SendDataBridge &&
					e.context.functionType !==
						FunctionType.SendDataMulticastBridge
				) {
					this._controller?.incrementStatistics("messagesDroppedTX");
					return e.context as TResponse;
				} else if (e.code === ZWaveErrorCodes.Controller_NodeTimeout) {
					// If the node failed to respond in time, remember this for the statistics
					node?.incrementStatistics("timeoutResponse");
				}
			}
			throw e;
		}
	}


/**
	 * 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
	 */
public async sendCommand<TResponse extends CommandClass = CommandClass>(
		command: CommandClass,
		options: SendCommandOptions = {},
	): Promise<TResponse | undefined> {
		let msg: SendDataMessage;
		if (command.isSinglecast()) {
			if (
				this.controller.isFunctionSupported(FunctionType.SendDataBridge)
			) {
				// Prioritize Bridge commands when they are supported
				msg = new SendDataBridgeRequest(this, { command });
			} else {
				msg = new SendDataRequest(this, { command });
			}
		} else if (command.isMulticast()) {
			if (
				this.controller.isFunctionSupported(FunctionType.SendDataBridge)
			) {
				// Prioritize Bridge commands when they are supported
				msg = new SendDataMulticastBridgeRequest(this, { command });
			} else {
				msg = new SendDataMulticastRequest(this, { command });
			}
		} else {
			throw new ZWaveError(
				`A CC must either be singlecast or multicast`,
				ZWaveErrorCodes.Argument_Invalid,
			);
		}
		// Specify the number of send attempts for the request
		if (options.maxSendAttempts != undefined) {
			msg.maxSendAttempts = options.maxSendAttempts;
		}

		// Automatically encapsulate commands before sending
		if (options.autoEncapsulate !== false) this.encapsulateCommands(msg);

		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;
			}
		} catch (e) {
			// A timeout always has to be expected. In this case return nothing.
			if (
				isZWaveError(e) &&
				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;
			}
		}
	}


/**
	 * Sends a supervised command to a Z-Wave node. When status updates are requested, the passed callback will be executed for every non-final update.
	 * @param command The command to send
	 * @param options (optional) Options regarding the message transmission
	 */
public async sendSupervisedCommand(
		command: CommandClass,
		options: SendSupervisedCommandOptions = {
			requestStatusUpdates: false,
		},
	): Promise<SupervisionResult | undefined> {
		const nodeId = command.nodeId;
		if (typeof nodeId !== "number") {
			throw new ZWaveError(
				`Sending a supervised command is only supported with singlecast!`,
				ZWaveErrorCodes.CC_NotSupported,
			);
		}

		// Check if the target supports this command
		if (!command.getNode()?.supportsCC(CommandClasses.Supervision)) {
			throw new ZWaveError(
				`Node ${nodeId} does not support the Supervision CC!`,
				ZWaveErrorCodes.CC_NotSupported,
			);
		}

		// Create the encapsulating CC so we have a session ID
		command = SupervisionCC.encapsulate(
			this,
			command,
			options.requestStatusUpdates,
		);

		const resp = await this.sendCommand<SupervisionCCReport>(
			command,
			options,
		);
		if (!resp) return;

		// If future updates are expected, listen for them
		if (options.requestStatusUpdates && resp.moreUpdatesFollow) {
			this.ensureNodeSessions(nodeId).supervision.set(
				(command as SupervisionCCGet).sessionId,
				options.onUpdate,
			);
		}
		// In any case, return the status
		return {
			status: resp.status,
			remainingDuration: resp.duration,
		};
	}


/**
	 * Sends a supervised command to a Z-Wave node if the Supervision CC is supported. If not, a normal command is sent.
	 * This does not return any Report values, so only use this for Set-type commands.
	 *
	 * @param command The command to send
	 * @param options (optional) Options regarding the message transmission
	 */
public async trySendCommandSupervised(
		command: CommandClass,
		options?: SendSupervisedCommandOptions,
	): Promise<SupervisionResult | undefined> {
		if (command.getNode()?.supportsCC(CommandClasses.Supervision)) {
			return this.sendSupervisedCommand(command, options);
		} else {
			await this.sendCommand(command, options);
		}
	}


/**
	 * Sends a low-level message like ACK, NAK or CAN immediately
	 * @param message The low-level message to send
	 */
private writeHeader(header: MessageHeaders): Promise<void> {
		// ACK, CAN, NAK
		return this.writeSerial(Buffer.from([header]));
	}


/** Sends a raw datagram to the serialport (if that is open) */
private async writeSerial(data: Buffer): Promise<void> {
		return this.serial?.writeAsync(data);
	}


/**
	 * Waits until a command is received or a timeout has elapsed. Returns the received command.
	 * @param timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected
	 * @param predicate A predicate function to test all incoming command classes
	 */
public waitForCommand<T extends CommandClass>(
		predicate: (cc: CommandClass) => boolean,
		timeout: number,
	): Promise<T> {
		return new Promise<T>((resolve, reject) => {
			const entry: AwaitedCommandEntry = {
				predicate,
				promise: createDeferredPromise<CommandClass>(),
				timeout: undefined,
			};
			this.awaitedCommands.push(entry);
			const removeEntry = () => {
				if (entry.timeout) clearTimeout(entry.timeout);
				const index = this.awaitedCommands.indexOf(entry);
				if (index !== -1) this.awaitedCommands.splice(index, 1);
			};
			// When the timeout elapses, remove the wait entry and reject the returned Promise
			entry.timeout = setTimeout(() => {
				removeEntry();
				reject(
					new ZWaveError(
						`Received no matching command within the provided timeout!`,
						ZWaveErrorCodes.Controller_NodeTimeout,
					),
				);
			}, timeout).unref();
			// When the promise is resolved, remove the wait entry and resolve the returned Promise
			void entry.promise.then((cc) => {
				removeEntry();
				resolve(cc as T);
			});
		});
	}


/** Checks if a message is allowed to go into the wakeup queue */
private mayMoveToWakeupQueue(transaction: Transaction): boolean {
		const msg = transaction.message;
		switch (true) {
			// Pings and handshake responses will block the send queue until wakeup,
			// so they must be dropped
			case messageIsPing(msg):
			case transaction.priority === MessagePriority.Handshake:
			// Outgoing handshake requests are very likely not valid after wakeup, so drop them too
			case transaction.priority === MessagePriority.PreTransmitHandshake:
			// We also don't want to immediately send the node to sleep when it wakes up
			case isCommandClassContainer(msg) &&
				msg.command instanceof WakeUpCCNoMoreInformation:
			// compat queries because they will be recreated when the node wakes up
			case transaction.tag === "compat":
				return false;
		}

		return true;
	}


/** Moves all messages for a given node into the wakeup queue */
private moveMessagesToWakeupQueue(nodeId: number): void {
		const reject: TransactionReducerResult = {
			type: "reject",
			message: `The node is asleep`,
			code: ZWaveErrorCodes.Controller_MessageDropped,
		};
		const requeue: TransactionReducerResult = {
			type: "requeue",
			priority: MessagePriority.WakeUp,
		};
		const requeueAndTagAsInterview: TransactionReducerResult = {
			...requeue,
			tag: "interview",
		};

		const reducer: TransactionReducer = (transaction, _source) => {
			const msg = transaction.message;
			if (msg.getNodeId() !== nodeId) return { type: "keep" };
			// Drop all messages that are not allowed in the wakeup queue
			// For all other messages, change the priority to wakeup
			return this.mayMoveToWakeupQueue(transaction)
				? transaction.priority === MessagePriority.NodeQuery
					? requeueAndTagAsInterview
					: requeue
				: reject;
		};

		// Apply the reducer to the send thread
		this.sendThread.send({ type: "reduce", reducer });
	}


/**
	 * @internal
	 * Rejects all pending transactions that match a predicate and removes them from the send queue
	 */
public rejectTransactions(
		predicate: (t: Transaction) => boolean,
		errorMsg: string = `The message has been removed from the queue`,
		errorCode: ZWaveErrorCodes = ZWaveErrorCodes.Controller_MessageDropped,
	): void {
		const reducer: TransactionReducer = (transaction) => {
			if (predicate(transaction)) {
				return {
					type: "reject",
					message: errorMsg,
					code: errorCode,
				};
			} else {
				return { type: "keep" };
			}
		};
		// Apply the reducer to the send thread
		this.sendThread.send({ type: "reduce", reducer });
	}


/**
	 * @internal
	 * Rejects all pending transactions for a node and removes them from the send queue
	 */
public rejectAllTransactionsForNode(
		nodeId: number,
		errorMsg: string = `The node is dead`,
		errorCode: ZWaveErrorCodes = ZWaveErrorCodes.Controller_MessageDropped,
	): void {
		this.rejectTransactions(
			(t) => t.message.getNodeId() === nodeId,
			errorMsg,
			errorCode,
		);
	}


/** Re-sorts the send queue */
private sortSendQueue(): void {
		this.sendThread.send("sortQueue");
	}


/** Re-sends the current command if it is S2 encapsulated */
public resendS2EncapsulatedCommand(): void {
		// If this is called, a receiving node couldn't decode the last message we sent it
		const { currentTransaction } = this.sendThread.state.context;
		if (
			currentTransaction &&
			isCommandClassContainer(currentTransaction.message) &&
			currentTransaction.message.command instanceof
				Security2CCMessageEncapsulation
		) {
			const cmd = currentTransaction.message.command;
			if (cmd.wasRetriedAfterDecodeFailure) {
				this._controllerLog.logNode(cmd.nodeId as number, {
					message: `failed to decode the message after re-transmission with SPAN extension, dropping the message.`,
					direction: "none",
					level: "warn",
				});
				this.sendThread.send({
					type: "reduce",
					reducer: (_t, source) => {
						if (source === "current") {
							return {
								type: "reject",
								code: ZWaveErrorCodes.Security2CC_CannotDecode,
								message:
									"The node failed to decode the message.",
							};
						} else {
							return { type: "keep" };
						}
					},
				});
			} else {
				this._controllerLog.logNode(cmd.nodeId as number, {
					message: `failed to decode the message, retrying with SPAN extension...`,
					direction: "none",
				});
				cmd.prepareRetryAfterDecodeFailure();
				this.sendThread.send("resend");
			}
		}
	}


/**
	 * Does the work for saveNetworkToCache. This is not throttled, so any call
	 * to this method WILL save the network.
	 */
private async saveNetworkToCacheInternal(): Promise<void> {
		// Avoid overwriting the cache with empty data if the controller wasn't interviewed yet
		if (
			!this._controller ||
			this._controller.homeId == undefined ||
			!this._controllerInterviewed
		)
			return;

		await this.options.storage.driver.ensureDir(this.cacheDir);
		const cacheFile = path.join(
			this.cacheDir,
			this._controller.homeId.toString(16) + ".json",
		);

		const serializedObj = this._controller.serialize();
		const jsonString = stringify(serializedObj);
		await this.options.storage.driver.writeFile(
			cacheFile,
			jsonString,
			"utf8",
		);
	}


/**
	 * Saves the current configuration and collected data about the controller and all nodes to a cache file.
	 * For performance reasons, these calls may be throttled.
	 */
public async saveNetworkToCache(): Promise<void> {
		// TODO: Detect if the network needs to be saved at all
		if (!this._controller || this._controller.homeId == undefined) return;
		// Ensure this method isn't being executed too often
		if (
			this.isSavingToCache ||
			Date.now() - this.lastSaveToCache < this.saveToCacheInterval
		) {
			// Schedule a save in a couple of ms to collect changes
			if (!this.saveToCacheTimer) {
				this.saveToCacheTimer = setTimeout(
					() => void this.saveNetworkToCache(),
					this.saveToCacheInterval,
				);
			}
			return;
		} else {
			this.saveToCacheTimer = undefined;
		}
		this.isSavingToCache = true;
		await this.saveNetworkToCacheInternal();
		this.isSavingToCache = false;
		this.lastSaveToCache = Date.now();
	}


/**
	 * Restores a previously stored Z-Wave network state from cache to speed up the startup process
	 */
public async restoreNetworkStructureFromCache(): Promise<void> {
		if (!this._controller || !this.controller.homeId) return;

		const cacheFile = path.join(
			this.cacheDir,
			`${this.controller.homeId.toString(16)}.json`,
		);
		if (!(await this.options.storage.driver.pathExists(cacheFile))) return;

		try {
			this.driverLog.print(
				`Cache file for homeId ${num2hex(
					this.controller.homeId,
				)} found, attempting to restore the network from cache...`,
			);
			const cacheString = await this.options.storage.driver.readFile(
				cacheFile,
				"utf8",
			);
			await this.controller.deserialize(JSON.parse(cacheString));
			this.driverLog.print(
				`Restoring the network from cache was successful!`,
			);
		} catch (e) {
			const message = `Restoring the network from cache failed: ${getErrorMessage(
				e,
				true,
			)}`;
			this.emit(
				"error",
				new ZWaveError(message, ZWaveErrorCodes.Driver_InvalidCache),
			);
			this.driverLog.print(message, "error");
		}
	}


/**
	 * @internal
	 * Marks a node for a later sleep command. Every call refreshes the period until the node actually goes to sleep
	 */
public debounceSendNodeToSleep(node: ZWaveNode): void {
		// TODO: This should be a single command to the send thread
		// Delete old timers if any exist
		if (this.sendNodeToSleepTimers.has(node.id)) {
			clearTimeout(this.sendNodeToSleepTimers.get(node.id)!);
		}

		// Sends a node to sleep if it has no more messages.
		const sendNodeToSleep = (node: ZWaveNode): void => {
			this.sendNodeToSleepTimers.delete(node.id);
			if (!this.hasPendingMessages(node)) {
				void node.sendNoMoreInformation().catch(() => {
					/* ignore errors */
				});
			}
		};

		// If a sleeping node has no messages pending (and supports the Wake Up CC), we may send it back to sleep
		if (
			node.supportsCC(CommandClasses["Wake Up"]) &&
			!this.hasPendingMessages(node)
		) {
			const wakeUpInterval = node.getValue<number>(
				getWakeUpIntervalValueId(),
			);
			// GH#2179: when a device only wakes up manually, don't send it back to sleep
			// Best case, the user wanted to interact with it.
			// Worst case, the device won't ACK this and cause a delay
			if (wakeUpInterval !== 0) {
				this.sendNodeToSleepTimers.set(
					node.id,
					setTimeout(() => sendNodeToSleep(node), 1000).unref(),
				);
			}
		}
	}


/** Computes the maximum net CC payload size for the given CC or SendDataRequest */
public computeNetCCPayloadSize(
		commandOrMsg: CommandClass | SendDataRequest | SendDataBridgeRequest,
	): number {
		// Recreate the correct encapsulation structure
		let msg: SendDataRequest | SendDataBridgeRequest;
		if (isSendDataSinglecast(commandOrMsg)) {
			msg = commandOrMsg;
		} else {
			const SendDataConstructor = this.getSendDataSinglecastConstructor();
			msg = new SendDataConstructor(this, { command: commandOrMsg });
		}
		this.encapsulateCommands(msg);
		return msg.command.getMaxPayloadLength(msg.getMaxPayloadLength());
	}


/** Returns the preferred constructor to use for singlecast SendData commands */
public getSendDataSinglecastConstructor():
		| typeof SendDataRequest
		| typeof SendDataBridgeRequest {
		return this._controller?.isFunctionSupported(
			FunctionType.SendDataBridge,
		)
			? SendDataBridgeRequest
			: SendDataRequest;
	}


/** Returns the preferred constructor to use for multicast SendData commands */
public getSendDataMulticastConstructor():
		| typeof SendDataMulticastRequest
		| typeof SendDataMulticastBridgeRequest {
		return this._controller?.isFunctionSupported(
			FunctionType.SendDataMulticastBridge,
		)
			? SendDataMulticastBridgeRequest
			: SendDataMulticastRequest;
	}


// This does not all need to be printed to the console
public [util.inspect.custom](): string {
		return "[Driver]";
	}


/**
	 * Checks whether there is a compatible update for the currently installed config package.
	 * Returns the new version if there is an update, `undefined` otherwise.
	 */
public async checkForConfigUpdates(
		silent: boolean = false,
	): Promise<string | undefined> {
		this.ensureReady();

		try {
			if (!silent)
				this.driverLog.print("Checking for configuration updates...");
			const ret = await checkForConfigUpdates(this.configVersion);
			if (ret) {
				if (!silent)
					this.driverLog.print(
						`Configuration update available: ${ret}`,
					);
			} else {
				if (!silent)
					this.driverLog.print(
						"No configuration update available...",
					);
			}
			return ret;
		} catch (e) {
			this.driverLog.print(getErrorMessage(e), "error");
		}
	}


/**
	 * Installs an update for the embedded configuration DB if there is a compatible one.
	 * Returns `true` when an update was installed, `false` otherwise.
	 *
	 * **Note:** Bugfixes and changes to device configuration generally require a restart or re-interview to take effect.
	 */
public async installConfigUpdate(): Promise<boolean> {
		this.ensureReady();

		const newVersion = await this.checkForConfigUpdates(true);
		if (!newVersion) return false;

		try {
			this.driverLog.print(
				`Installing version ${newVersion} of configuration DB...`,
			);
			// We have 3 variants of this.
			const extConfigDir = externalConfigDir();
			if (this.configManager.useExternalConfig && extConfigDir) {
				// 1. external config dir, leave node_modules alone
				await installConfigUpdateInDocker(newVersion, {
					cacheDir: this.options.storage.cacheDir,
					configDir: extConfigDir,
				});
			} else if (isDocker()) {
				// 2. Docker, but no external config dir, extract into node_modules
				await installConfigUpdateInDocker(newVersion);
			} else {
				// 3. normal environment, use npm/yarn to install a new version of @zwave-js/config
				await installConfigUpdate(newVersion);
			}
		} catch (e) {
			this.driverLog.print(getErrorMessage(e), "error");
			return false;
		}
		this.driverLog.print(
			`Configuration DB updated to version ${newVersion}, activating...`,
		);

		// Reload the config files
		await this.configManager.loadAll();

		// Now try to apply them to all known devices
		if (this._controller) {
			for (const node of this._controller.nodes.values()) {
				if (node.ready) await node["loadDeviceConfig"]();
			}
		}

		return true;
	}


function createBaseGenesisState(): GenesisState {
  return {
    startingProposalId: "0",
    deposits: [],
    votes: [],
    proposals: [],
    depositParams: undefined,
    votingParams: undefined,
    tallyParams: undefined,
  };
}


function longToString(long: Long) {
  return long.toString();
}


function isSet(value: any): boolean {
  return value !== null && value !== undefined;
}


(e: any) => Deposit.fromJSON(e)


(e: any) => Vote.fromJSON(e)


(e: any) => Proposal.fromJSON(e)


(e) =>
        e ? Deposit.toJSON(e) : undefined


(e) => (e ? Vote.toJSON(e) : undefined)


(e) =>
        e ? Proposal.toJSON(e) : undefined


(e) => Deposit.fromPartial(e)


(e) => Vote.fromPartial(e)


(e) => Proposal.fromPartial(e)


/** GenesisState defines the gov module's genesis state. */
export interface GenesisState {
  /** starting_proposal_id is the ID of the starting proposal. */
  startingProposalId: string;
  /** deposits defines all the deposits present at genesis. */
  deposits: Deposit[];
  /** votes defines all the votes present at genesis. */
  votes: Vote[];
  /** proposals defines all the proposals present at genesis. */
  proposals: Proposal[];
  /** params defines all the paramaters of related to deposit. */
  depositParams?: DepositParams;
  /** params defines all the paramaters of related to voting. */
  votingParams?: VotingParams;
  /** params defines all the paramaters of related to tally. */
  tallyParams?: TallyParams;
}


type Builtin =
  | Date
  | Function
  | Uint8Array
  | string
  | number
  | boolean
  | undefined;


export type DeepPartial<T> = T extends Builtin
  ? T
  : T extends Array<infer U>
  ? Array<DeepPartial<U>>
  : T extends ReadonlyArray<infer U>
  ? ReadonlyArray<DeepPartial<U>>
  : T extends {}
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : Partial<T>;


type KeysOfUnion<T> = T extends T ? keyof T : never;


export type Exact<P, I extends P> = P extends Builtin
  ? P
  : P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<
        Exclude<keyof I, KeysOfUnion<P>>,
        never
      >;


encode(
    message: GenesisState,
    writer: _m0.Writer = _m0.Writer.create(),
  ): _m0.Writer {
    if (message.startingProposalId !== "0") {
      writer.uint32(8).uint64(message.startingProposalId);
    }
    for (const v of message.deposits) {
      Deposit.encode(v!, writer.uint32(18).fork()).ldelim();
    }
    for (const v of message.votes) {
      Vote.encode(v!, writer.uint32(26).fork()).ldelim();
    }
    for (const v of message.proposals) {
      Proposal.encode(v!, writer.uint32(34).fork()).ldelim();
    }
    if (message.depositParams !== undefined) {
      DepositParams.encode(
        message.depositParams,
        writer.uint32(42).fork(),
      ).ldelim();
    }
    if (message.votingParams !== undefined) {
      VotingParams.encode(
        message.votingParams,
        writer.uint32(50).fork(),
      ).ldelim();
    }
    if (message.tallyParams !== undefined) {
      TallyParams.encode(
        message.tallyParams,
        writer.uint32(58).fork(),
      ).ldelim();
    }
    return writer;
  }


decode(input: _m0.Reader | Uint8Array, length?: number): GenesisState {
    const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseGenesisState();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          message.startingProposalId = longToString(reader.uint64() as Long);
          break;
        case 2:
          message.deposits.push(Deposit.decode(reader, reader.uint32()));
          break;
        case 3:
          message.votes.push(Vote.decode(reader, reader.uint32()));
          break;
        case 4:
          message.proposals.push(Proposal.decode(reader, reader.uint32()));
          break;
        case 5:
          message.depositParams = DepositParams.decode(reader, reader.uint32());
          break;
        case 6:
          message.votingParams = VotingParams.decode(reader, reader.uint32());
          break;
        case 7:
          message.tallyParams = TallyParams.decode(reader, reader.uint32());
          break;
        default:
          reader.skipType(tag & 7);
          break;
      }
    }
    return message;
  }


fromJSON(object: any): GenesisState {
    return {
      startingProposalId: isSet(object.startingProposalId)
        ? String(object.startingProposalId)
        : "0",
      deposits: Array.isArray(object?.deposits)
        ? object.deposits.map((e: any) => Deposit.fromJSON(e))
        : [],
      votes: Array.isArray(object?.votes)
        ? object.votes.map((e: any) => Vote.fromJSON(e))
        : [],
      proposals: Array.isArray(object?.proposals)
        ? object.proposals.map((e: any) => Proposal.fromJSON(e))
        : [],
      depositParams: isSet(object.depositParams)
        ? DepositParams.fromJSON(object.depositParams)
        : undefined,
      votingParams: isSet(object.votingParams)
        ? VotingParams.fromJSON(object.votingParams)
        : undefined,
      tallyParams: isSet(object.tallyParams)
        ? TallyParams.fromJSON(object.tallyParams)
        : undefined,
    };
  }


toJSON(message: GenesisState): unknown {
    const obj: any = {};
    message.startingProposalId !== undefined &&
      (obj.startingProposalId = message.startingProposalId);
    if (message.deposits) {
      obj.deposits = message.deposits.map((e) =>
        e ? Deposit.toJSON(e) : undefined,
      );
    } else {
      obj.deposits = [];
    }
    if (message.votes) {
      obj.votes = message.votes.map((e) => (e ? Vote.toJSON(e) : undefined));
    } else {
      obj.votes = [];
    }
    if (message.proposals) {
      obj.proposals = message.proposals.map((e) =>
        e ? Proposal.toJSON(e) : undefined,
      );
    } else {
      obj.proposals = [];
    }
    message.depositParams !== undefined &&
      (obj.depositParams = message.depositParams
        ? DepositParams.toJSON(message.depositParams)
        : undefined);
    message.votingParams !== undefined &&
      (obj.votingParams = message.votingParams
        ? VotingParams.toJSON(message.votingParams)
        : undefined);
    message.tallyParams !== undefined &&
      (obj.tallyParams = message.tallyParams
        ? TallyParams.toJSON(message.tallyParams)
        : undefined);
    return obj;
  }


fromPartial<I extends Exact<DeepPartial<GenesisState>, I>>(
    object: I,
  ): GenesisState {
    const message = createBaseGenesisState();
    message.startingProposalId = object.startingProposalId ?? "0";
    message.deposits =
      object.deposits?.map((e) => Deposit.fromPartial(e)) || [];
    message.votes = object.votes?.map((e) => Vote.fromPartial(e)) || [];
    message.proposals =
      object.proposals?.map((e) => Proposal.fromPartial(e)) || [];
    message.depositParams =
      object.depositParams !== undefined && object.depositParams !== null
        ? DepositParams.fromPartial(object.depositParams)
        : undefined;
    message.votingParams =
      object.votingParams !== undefined && object.votingParams !== null
        ? VotingParams.fromPartial(object.votingParams)
        : undefined;
    message.tallyParams =
      object.tallyParams !== undefined && object.tallyParams !== null
        ? TallyParams.fromPartial(object.tallyParams)
        : undefined;
    return message;
  }


function done2(error?: Error) {
        if (error) {
          done(error);
        } else {
          completeCount += 1;
          if (completeCount === 2) {
            done();
          }
        }
      }


(
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.includes('127.0.0.1:50051'));
          // We would check for the IPv6 address but it needs to be omitted on some Node versions
          done();
        }


(error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        }


(
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.includes('127.0.0.1:443'));
          // We would check for the IPv6 address but it needs to be omitted on some Node versions
          done();
        }


(
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.length > 0);
          done();
        }


(
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.length > 0);
          done2();
        }


(error: StatusObject) => {
          done2(new Error(`Failed with status ${error.details}`));
        }


done => {
      const target = 'unix:socket';
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.includes('socket'));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener);
      resolver.updateResolution();
    }


(
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.includes('socket'));
          done();
        }


done => {
      const target = 'unix:///tmp/socket';
      const listener: resolverManager.ResolverListener = {
        onSuccessfulResolution: (
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.includes('/tmp/socket'));
          done();
        },
        onError: (error: StatusObject) => {
          done(new Error(`Failed with status ${error.details}`));
        },
      };
      const resolver = resolverManager.createResolver(target, listener);
      resolver.updateResolution();
    }


(
          addressList: string[],
          serviceConfig: ServiceConfig | null,
          serviceConfigError: StatusObject | null
        ) => {
          assert(addressList.includes('/tmp/socket'));
          done();
        }


export type ListIdentitiesExceptionsUnion = never;


(): void => {
	it('should have the correct rule', (): void => {
		expect(Object.keys(rule).length).toBe(1);
		expect(Rules.Complexity in rule).toBe(true);
	});

	it('should be turned off', (): void => {
		expect(rule[Rules.Complexity][0]).toBe(Severity.off);
	});
}


(): void => {
		expect(Object.keys(rule).length).toBe(1);
		expect(Rules.Complexity in rule).toBe(true);
	}


(): void => {
		expect(rule[Rules.Complexity][0]).toBe(Severity.off);
	}


function Example() {
  const [show, setShow] = useState(false);

  return (
    <div>
      <Button onPress={() => setShow(true)}


() => <Example />


export class Article {
		title = def.stringColumn()
		category = def.manyHasOne(Category, 'articles')
	}


export class Category {
		articles = def.oneHasMany(Article, 'category')
	}


export class Article {
		title = def.stringColumn()
		categories = def.manyHasMany(Category, 'articles')
	}


export class Category {
		articles = def.manyHasManyInverse(Article, 'categories')
	}


@Injectable()
export class BooksService {
  constructor(private readonly prismaService: PrismaService) {}

  create(createBookDto: Prisma.BookCreateInput) {
    return this.prismaService.book.create({ data: createBookDto });
  }

  findAll() {
    return this.prismaService.book.findMany();
  }

  findOne(bookWhereUniqueInput: Prisma.BookWhereUniqueInput) {
    return this.prismaService.book.findUnique({ where: bookWhereUniqueInput });
  }

  update(
    where: Prisma.BookWhereUniqueInput,
    updateBookUniqueInput: Prisma.BookUpdateInput,
  ) {
    return this.prismaService.book.update({
      data: updateBookUniqueInput,
      where: where,
    });
  }

  remove(id: number) {
    return `This action removes a #${id} book`;
  }
}


create(createBookDto: Prisma.BookCreateInput) {
    return this.prismaService.book.create({ data: createBookDto });
  }


findAll() {
    return this.prismaService.book.findMany();
  }


findOne(bookWhereUniqueInput: Prisma.BookWhereUniqueInput) {
    return this.prismaService.book.findUnique({ where: bookWhereUniqueInput });
  }


update(
    where: Prisma.BookWhereUniqueInput,
    updateBookUniqueInput: Prisma.BookUpdateInput,
  ) {
    return this.prismaService.book.update({
      data: updateBookUniqueInput,
      where: where,
    });
  }


remove(id: number) {
    return `This action removes a #${id} book`;
  }


/**
     * Class representing a checkbox menu item.  This is just a convenience class
     * that extends {@link goog.ui.MenuItem} by making it checkable.
     *
     * @extends {goog.ui.MenuItem}
     */
class CheckBoxMenuItem extends __CheckBoxMenuItem {}


abstract class __CheckBoxMenuItem extends goog.ui.__MenuItem {
        /**
         * @param {goog.ui.ControlContent} content Text caption or DOM structure to
         *     display as the content of the item (use to add icons or styling to
         *     menus).
         * @param {*=} opt_model Data/model associated with the menu item.
         * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper used for
         *     document interactions.
         */
        constructor(content: goog.ui.ControlContent, opt_model?: any, opt_domHelper?: goog.dom.DomHelper);
    }


(i) =>
      this.web3.eth.getBlock(latest - res.flags.lookback! + i + 1)


(resolve, reject) => {
      process.stdout.write('\n', (err: any) => {
        err ? reject(err) : resolve()
      })
    }


(err: any) => {
        err ? reject(err) : resolve()
      }


export default class ValidatorSignedBlocks extends BaseCommand {
  static description =
    "Display a graph of blocks and whether the given signer's signature is included in each. A green '.' indicates the signature is present in that block, a red '✘' indicates the signature is not present. A yellow '~' indicates the signer is not elected for that block."

  static flags = {
    ...BaseCommand.flagsWithoutLocalAddresses(),
    signer: Flags.address({
      description: 'address of the signer to check for signatures',
      required: true,
    }),
    'at-block': flags.integer({
      description: 'latest block to examine for sginer activity',
      exclusive: ['follow'],
    }),
    lookback: flags.integer({
      description: 'how many blocks to look back for signer activity',
      default: 120,
    }),
    width: flags.integer({
      description: 'line width for printing marks',
      default: 40,
    }),
    /* TODO(victor): Fix this the follow functionality.
    follow: flags.boolean({
      char: 'f',
      default: false,
      exclusive: ['at-block'],
      hidden: true,
    }),
    */
  }

  static examples = [
    'heartbeat --signer 0x5409ED021D9299bf6814279A6A1411A7e866A631',
    'heartbeat --at-block 100000 --signer 0x5409ED021D9299bf6814279A6A1411A7e866A631',
    'heartbeat --lookback 500 --signer 0x5409ED021D9299bf6814279A6A1411A7e866A631',
    'heartbeat --lookback 50 --width 10 --signer 0x5409ED021D9299bf6814279A6A1411A7e866A631',
  ]

  async run() {
    const res = this.parse(ValidatorSignedBlocks)
    const election = await this.kit.contracts.getElection()
    const validators = await this.kit.contracts.getValidators()
    const epochSize = await validators.getEpochSize()
    const electionCache = new ElectionResultsCache(election, epochSize.toNumber())

    const latest = res.flags['at-block'] ?? (await this.web3.eth.getBlock('latest')).number

    const blocks = await concurrentMap(10, [...Array(res.flags.lookback).keys()], (i) =>
      this.web3.eth.getBlock(latest - res.flags.lookback! + i + 1)
    )
    const printer = new MarkPrinter(res.flags.width!)
    try {
      for (const block of blocks) {
        const elected = await electionCache.elected(res.flags.signer, block.number - 1)
        const signed = elected && (await electionCache.signedParent(res.flags.signer, block))
        printer.addMark(block.number - 1, elected, signed)
      }

      // TODO(victor) Fix the follow flag functionality.
      /*
      if (res.flags.follow) {
        const subscription = this.web3.eth.subscribe("newBlockHeaders", (error) =>  {
          if (error) { this.error(error) }
        }).on("data", (block) => {
          const elected = electionCache.elected(res.flags.signer, block.number)
          const signed = elected && electionCache.signed(res.flags.signer, block)
          printer.addMark(block.number, elected, signed)
        }).on("error", (error) => {
          this.error(`error in block header subscription: ${error}`)
        })

        try {
          let response: string
          do {
            response = await cli.prompt('', {prompt: '', type: 'single', required: false})
          } while (response !== 'q' && response !== '\u0003' / ctrl-c /)
        } finally {
          await subscription.unsubscribe()
        }
      }
      */
    } finally {
      await printer.done()
    }
  }
}


/**
 * Printer object to output marks in a grid to indicate signing status.
 */
// tslint:disable-next-line:max-classes-per-file
class MarkPrinter {
  private previousBlockNumber: number | null = null

  constructor(private width: number) {}

  addMark(blockNumber: number, elected: boolean, signed: boolean) {
    if (this.previousBlockNumber === null) {
      const labelNumber = Math.floor(blockNumber / this.width) * this.width
      this.previousBlockNumber = labelNumber - 1
    }
    if (blockNumber <= this.previousBlockNumber - 1) {
      throw new Error(
        `cannot add mark for ${blockNumber} which is not after ${this.previousBlockNumber}`
      )
    }

    for (let i = this.previousBlockNumber + 1; i <= blockNumber; i++) {
      if (i % this.width === 0) {
        this.printLineLabel(i)
      }
      if (i < blockNumber) {
        process.stdout.write(' ')
      } else {
        process.stdout.write(this.mark(elected, signed))
      }
    }
    this.previousBlockNumber = blockNumber
  }

  async done() {
    // Print a final newline to complete the line.
    return new Promise((resolve, reject) => {
      process.stdout.write('\n', (err: any) => {
        err ? reject(err) : resolve()
      })
    })
  }

  private mark(elected: boolean, signed: boolean) {
    return elected ? (signed ? chalk.green('.') : chalk.red('✘')) : chalk.yellow('~')
  }

  private printLineLabel(blockNumber: number, newline: boolean = true) {
    if (newline) {
      process.stdout.write('\n')
    }
    process.stdout.write(`${blockNumber} `.padStart(8, ' '))
  }
}


async run() {
    const res = this.parse(ValidatorSignedBlocks)
    const election = await this.kit.contracts.getElection()
    const validators = await this.kit.contracts.getValidators()
    const epochSize = await validators.getEpochSize()
    const electionCache = new ElectionResultsCache(election, epochSize.toNumber())

    const latest = res.flags['at-block'] ?? (await this.web3.eth.getBlock('latest')).number

    const blocks = await concurrentMap(10, [...Array(res.flags.lookback).keys()], (i) =>
      this.web3.eth.getBlock(latest - res.flags.lookback! + i + 1)
    )
    const printer = new MarkPrinter(res.flags.width!)
    try {
      for (const block of blocks) {
        const elected = await electionCache.elected(res.flags.signer, block.number - 1)
        const signed = elected && (await electionCache.signedParent(res.flags.signer, block))
        printer.addMark(block.number - 1, elected, signed)
      }

      // TODO(victor) Fix the follow flag functionality.
      /*
      if (res.flags.follow) {
        const subscription = this.web3.eth.subscribe("newBlockHeaders", (error) =>  {
          if (error) { this.error(error) }
        }).on("data", (block) => {
          const elected = electionCache.elected(res.flags.signer, block.number)
          const signed = elected && electionCache.signed(res.flags.signer, block)
          printer.addMark(block.number, elected, signed)
        }).on("error", (error) => {
          this.error(`error in block header subscription: ${error}`)
        })

        try {
          let response: string
          do {
            response = await cli.prompt('', {prompt: '', type: 'single', required: false})
          } while (response !== 'q' && response !== '\u0003' / ctrl-c /)
        } finally {
          await subscription.unsubscribe()
        }
      }
      */
    } finally {
      await printer.done()
    }
  }


addMark(blockNumber: number, elected: boolean, signed: boolean) {
    if (this.previousBlockNumber === null) {
      const labelNumber = Math.floor(blockNumber / this.width) * this.width
      this.previousBlockNumber = labelNumber - 1
    }
    if (blockNumber <= this.previousBlockNumber - 1) {
      throw new Error(
        `cannot add mark for ${blockNumber} which is not after ${this.previousBlockNumber}`
      )
    }

    for (let i = this.previousBlockNumber + 1; i <= blockNumber; i++) {
      if (i % this.width === 0) {
        this.printLineLabel(i)
      }
      if (i < blockNumber) {
        process.stdout.write(' ')
      } else {
        process.stdout.write(this.mark(elected, signed))
      }
    }
    this.previousBlockNumber = blockNumber
  }


async done() {
    // Print a final newline to complete the line.
    return new Promise((resolve, reject) => {
      process.stdout.write('\n', (err: any) => {
        err ? reject(err) : resolve()
      })
    })
  }


private mark(elected: boolean, signed: boolean) {
    return elected ? (signed ? chalk.green('.') : chalk.red('✘')) : chalk.yellow('~')
  }


private printLineLabel(blockNumber: number, newline: boolean = true) {
    if (newline) {
      process.stdout.write('\n')
    }
    process.stdout.write(`${blockNumber} `.padStart(8, ' '))
  }


export interface IProps {
  genres: IGenre[];
  onSelectGenre: Function;
}


merge(existing = [], incoming) {
              return [...existing, ...incoming];
            }


async function run() {
  let md = '# Export size\n\n'

  md += 'generated by [export-size](https://github.com/antfu/export-size)<br>\n'
  md += `version: ${version}<br>\n`
  md += `date: ${new Date().toISOString()}\n\n`

  md += '> Please note this is bundle size for each individual APIs (excluding Vue). '
  md += 'Since we have a lot shared utilities underneath each function, importing two '
  md += 'different functions does NOT necessarily mean the bundle size will be the sum of them (usually smaller). '
  md += 'Depends on the bundler and minifier you use, the final result might vary, this list is for reference only.'
  md += '\n\n'

  for (const pkg of [...packages.slice(1), packages[0]]) {
    const { exports, packageJSON } = await getExportsSize({
      pkg: `./packages/${pkg.name}/dist`,
      output: false,
      bundler: 'rollup',
      external: ['vue-demi', ...(pkg.external || [])],
      includes: ['@vueuse/shared'],
    })

    md += `<kbd>${packageJSON.name}</kbd>\n\n`

    md += markdownTable([
      ['Function', 'min+gzipped'],
      ...exports.map((i) => {
        return [`\`${i.name}\``, filesize(i.minzipped)]
      }),
    ])

    md += '\n\n'
  }

  await fs.writeFile('packages/Export-Size.md', md, 'utf-8')
}


(i) => {
        return [`\`${i.name}\``, filesize(i.minzipped)]
      }