UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

520 lines (519 loc) 24.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var MessageGenerators_exports = {}; __export(MessageGenerators_exports, { createMessageGenerator: () => createMessageGenerator, maybeTransportServiceGenerator: () => maybeTransportServiceGenerator, secureMessageGeneratorS0: () => secureMessageGeneratorS0, secureMessageGeneratorS2: () => secureMessageGeneratorS2, secureMessageGeneratorS2Multicast: () => secureMessageGeneratorS2Multicast, simpleMessageGenerator: () => simpleMessageGenerator, waitForNodeUpdate: () => waitForNodeUpdate }); module.exports = __toCommonJS(MessageGenerators_exports); var import_cc = require("@zwave-js/cc"); var import_SecurityCC = require("@zwave-js/cc/SecurityCC"); var import_TransportServiceCC = require("@zwave-js/cc/TransportServiceCC"); var import_core = require("@zwave-js/core"); var import_serialapi = require("@zwave-js/serial/serialapi"); var import_shared = require("@zwave-js/shared"); var import_async = require("alcalzone-shared/async"); var import_deferred_promise = require("alcalzone-shared/deferred-promise"); function maybePartialNodeUpdate(ctx, sent, received) { if (!(0, import_serialapi.containsCC)(sent) || !(0, import_serialapi.containsCC)(received)) { return false; } if (sent.getNodeId() !== received.getNodeId()) return false; if (received.command.ccId === import_core.CommandClasses["Transport Service"]) { return true; } const sentCommand = (0, import_cc.getInnermostCommandClass)(sent.command); const receivedCommand = (0, import_cc.getInnermostCommandClass)(received.command); return sentCommand.isExpectedCCResponse(ctx, receivedCommand); } __name(maybePartialNodeUpdate, "maybePartialNodeUpdate"); async function waitForNodeUpdate(driver, msg, timeoutMs) { try { return await driver.waitForMessage((received) => msg.isExpectedNodeUpdate(driver, received), timeoutMs, (received) => maybePartialNodeUpdate(driver, msg, received)); } catch { throw new import_core.ZWaveError(`Timed out while waiting for a response from the node`, import_core.ZWaveErrorCodes.Controller_NodeTimeout); } } __name(waitForNodeUpdate, "waitForNodeUpdate"); function getNodeUpdateTimeout(driver, msg, additionalCommandTimeoutMs = 0) { const commandTimeMs = Math.ceil((msg.rtt ?? 0) / 1e6); return commandTimeMs + driver.getReportTimeout(msg) + additionalCommandTimeoutMs; } __name(getNodeUpdateTimeout, "getNodeUpdateTimeout"); const simpleMessageGenerator = /* @__PURE__ */ __name(async function* (driver, ctx, msg, onMessageSent, additionalCommandTimeoutMs = 0) { if ((0, import_serialapi.isSendData)(msg) && await driver.exceedsMaxPayloadLength(msg)) { let fail2 = function() { throw new import_core.ZWaveError("Cannot send this message because it would exceed the maximum payload length!", import_core.ZWaveErrorCodes.Controller_MessageTooLarge); }; var fail = fail2; __name(fail2, "fail"); if (msg.transmitOptions & import_core.TransmitOptions.Explore) { msg.transmitOptions &= ~import_core.TransmitOptions.Explore; if (await driver.exceedsMaxPayloadLength(msg)) { fail2(); } driver.controllerLog.logNode(msg.getNodeId(), { message: "Disabling explorer frames for this message due to its size", level: "warn" }); } else { fail2(); } } let result; msg.prematureNodeUpdate = void 0; try { result = yield msg; msg.markAsCompleted(); onMessageSent(msg, result); } catch (e) { msg.markAsCompleted(); throw e; } if (msg.expectsNodeUpdate(driver) && msg.prematureNodeUpdate) { return msg.prematureNodeUpdate; } if ((0, import_serialapi.isTransmitReport)(result) && !result.isOK()) { throw result; } if (msg.expectsNodeUpdate(driver)) { const timeout = getNodeUpdateTimeout(driver, msg, additionalCommandTimeoutMs); return waitForNodeUpdate(driver, msg, timeout); } return result; }, "simpleMessageGenerator"); const maybeTransportServiceGenerator = /* @__PURE__ */ __name(async function* (driver, ctx, msg, onMessageSent, additionalCommandTimeoutMs) { if (typeof msg.command.nodeId !== "number") { throw new import_core.ZWaveError("Cannot use the Transport Service message generator for multicast commands!", import_core.ZWaveErrorCodes.Argument_Invalid); } const node = msg.tryGetNode(driver); const mayUseTransportService = node?.supportsCC(import_core.CommandClasses["Transport Service"]) && node.getCCVersion(import_core.CommandClasses["Transport Service"]) >= 2; if (!mayUseTransportService || !await driver.exceedsMaxPayloadLength(msg)) { return yield* simpleMessageGenerator(driver, ctx, msg, onMessageSent, additionalCommandTimeoutMs); } const payload = await msg.serializeCC(ctx); const numSegments = Math.ceil(payload.length / import_TransportServiceCC.MAX_SEGMENT_SIZE); const segmentDelay = numSegments > import_TransportServiceCC.RELAXED_TIMING_THRESHOLD ? import_TransportServiceCC.TransportServiceTimeouts.relaxedTimingDelayR2 : 0; const sessionId = driver.getNextTransportServiceSessionId(); const nodeId = msg.command.nodeId; driver.driverLog.print("The following message is too large, using Transport Service to transmit it:"); driver.driverLog.logMessage(msg, { direction: "outbound" }); let unhandledResponses = []; const { unregister: unregisterHandler } = driver.registerCommandHandler((cc) => cc.nodeId === nodeId && (cc instanceof import_TransportServiceCC.TransportServiceCCSegmentWait || cc instanceof import_TransportServiceCC.TransportServiceCCSegmentRequest && cc.sessionId === sessionId), (cc) => { unhandledResponses.push(cc); }); const receivedSegmentWait = /* @__PURE__ */ __name(() => { const index = unhandledResponses.findIndex((cc) => cc instanceof import_TransportServiceCC.TransportServiceCCSegmentWait); if (index >= 0) { const cc = unhandledResponses[index]; unhandledResponses.splice(index, 1); return cc; } }, "receivedSegmentWait"); const receivedSegmentRequest = /* @__PURE__ */ __name(() => { const index = unhandledResponses.findIndex((cc) => cc instanceof import_TransportServiceCC.TransportServiceCCSegmentRequest); if (index >= 0) { const cc = unhandledResponses[index]; unhandledResponses.splice(index, 1); return cc; } }, "receivedSegmentRequest"); let result; try { attempts: for (let attempt = 1; attempt <= 2; attempt++) { driver.controllerLog.logNode(nodeId, { message: `Beginning Transport Service TX session #${sessionId}...`, level: "debug", direction: "outbound" }); unhandledResponses = []; const unsentSegments = Array.from({ length: numSegments }).fill(0).map((_, i) => i); let didRetryLastSegment = false; let isFirstTransferredSegment = true; while (unsentSegments.length > 0) { if (isFirstTransferredSegment) { isFirstTransferredSegment = false; } else if (segmentDelay) { await (0, import_async.wait)(segmentDelay, true); } const segment = unsentSegments.shift(); const chunk = payload.subarray(segment * import_TransportServiceCC.MAX_SEGMENT_SIZE, (segment + 1) * import_TransportServiceCC.MAX_SEGMENT_SIZE); let cc; if (segment === 0) { cc = new import_TransportServiceCC.TransportServiceCCFirstSegment({ nodeId, sessionId, datagramSize: payload.length, partialDatagram: chunk }); } else { cc = new import_TransportServiceCC.TransportServiceCCSubsequentSegment({ nodeId, sessionId, datagramSize: payload.length, datagramOffset: segment * import_TransportServiceCC.MAX_SEGMENT_SIZE, partialDatagram: chunk }); } const tmsg = driver.createSendDataMessage(cc, { autoEncapsulate: false, maxSendAttempts: msg.maxSendAttempts, transmitOptions: msg.transmitOptions }); result = yield* simpleMessageGenerator(driver, ctx, tmsg, onMessageSent); let segmentComplete = void 0; if (segment === numSegments - 1) { segmentComplete = await driver.waitForCommand((cc2) => cc2.nodeId === nodeId && cc2 instanceof import_TransportServiceCC.TransportServiceCCSegmentComplete && cc2.sessionId === sessionId, import_TransportServiceCC.TransportServiceTimeouts.segmentCompleteR2).catch(() => void 0); } if (segmentComplete) { driver.controllerLog.logNode(nodeId, { message: `Transport Service TX session #${sessionId} complete`, level: "debug", direction: "outbound" }); break attempts; } const segmentWait = receivedSegmentWait(); if (segmentWait) { const waitTime = segmentWait.pendingSegments * 100; driver.controllerLog.logNode(nodeId, { message: `Restarting Transport Service TX session #${sessionId} in ${waitTime} ms...`, level: "debug" }); await (0, import_async.wait)(waitTime, true); continue attempts; } let segmentRequest = void 0; let readdedSegments = false; while (segmentRequest = receivedSegmentRequest()) { unsentSegments.push(segmentRequest.datagramOffset / import_TransportServiceCC.MAX_SEGMENT_SIZE); readdedSegments = true; } if (readdedSegments) continue; if (segment === numSegments - 1) { if (didRetryLastSegment) { driver.controllerLog.logNode(nodeId, { message: `Transport Service TX session #${sessionId} failed`, level: "debug", direction: "outbound" }); break attempts; } else { driver.controllerLog.logNode(nodeId, { message: `Transport Service TX session #${sessionId}: Segment Complete missing - re-transmitting last segment...`, level: "debug", direction: "outbound" }); didRetryLastSegment = true; unsentSegments.unshift(segment); continue; } } } } } finally { unregisterHandler(); } if (msg.expectsNodeUpdate(driver)) { const timeout = getNodeUpdateTimeout(driver, msg, additionalCommandTimeoutMs); return waitForNodeUpdate(driver, msg, timeout); } return result; }, "maybeTransportServiceGenerator"); async function* sendCommandGenerator(driver, ctx, command, onMessageSent, options) { const msg = driver.createSendDataMessage(command, options); const resp = yield* maybeTransportServiceGenerator(driver, ctx, msg, onMessageSent); if (resp && (0, import_serialapi.containsCC)(resp)) { driver.unwrapCommands(resp); return resp.command; } } __name(sendCommandGenerator, "sendCommandGenerator"); const secureMessageGeneratorS0 = /* @__PURE__ */ __name(async function* (driver, ctx, msg, onMessageSent) { if (typeof msg.command.nodeId !== "number") { throw new import_core.ZWaveError("Cannot use the S0 message generator for multicast commands!", import_core.ZWaveErrorCodes.Argument_Invalid); } else if (!(msg.command instanceof import_SecurityCC.SecurityCCCommandEncapsulation)) { throw new import_core.ZWaveError("The S0 message generator can only be used for Security S0 command encapsulation!", import_core.ZWaveErrorCodes.Argument_Invalid); } const secMan = driver.securityManager; const nodeId = msg.command.nodeId; let additionalTimeoutMs; let nonce = secMan.getFreeNonce(nodeId); if (!nonce) { const cc = new import_SecurityCC.SecurityCCNonceGet({ nodeId, endpointIndex: msg.command.endpointIndex }); const nonceResp = yield* sendCommandGenerator(driver, ctx, cc, (msg2, result) => { additionalTimeoutMs = Math.ceil(msg2.rtt / 1e6); onMessageSent(msg2, result); }, { // Only try getting a nonce once maxSendAttempts: 1 }); if (!nonceResp) { throw new import_core.ZWaveError("No nonce received from the node, cannot send secure command!", import_core.ZWaveErrorCodes.SecurityCC_NoNonce); } nonce = nonceResp.nonce; } msg.command.nonce = nonce; return yield* simpleMessageGenerator(driver, ctx, msg, onMessageSent, additionalTimeoutMs); }, "secureMessageGeneratorS0"); const secureMessageGeneratorS2 = /* @__PURE__ */ __name(async function* (driver, ctx, msg, onMessageSent) { if (!(0, import_serialapi.isSendData)(msg) || !(0, import_serialapi.containsCC)(msg)) { throw new import_core.ZWaveError("Cannot use the S2 message generator for a command that's not a SendData message!", import_core.ZWaveErrorCodes.Argument_Invalid); } else if (typeof msg.command.nodeId !== "number") { throw new import_core.ZWaveError("Cannot use the S2 message generator for multicast commands!", import_core.ZWaveErrorCodes.Argument_Invalid); } else if (!(msg.command instanceof import_cc.Security2CCMessageEncapsulation)) { throw new import_core.ZWaveError("The S2 message generator can only be used for Security S2 command encapsulation!", import_core.ZWaveErrorCodes.Argument_Invalid); } const nodeId = msg.command.nodeId; const secMan = driver.getSecurityManager2(nodeId); const spanState = secMan.getSPANState(nodeId); let additionalTimeoutMs; const expectedSecurityClass = msg.command.securityClass ?? driver.getHighestSecurityClass(nodeId); if (spanState.type === import_core.SPANState.None || spanState.type === import_core.SPANState.LocalEI || spanState.type === import_core.SPANState.SPAN && spanState.securityClass !== import_core.SecurityClass.Temporary && spanState.securityClass !== expectedSecurityClass) { const cc = new import_cc.Security2CCNonceGet({ nodeId, endpointIndex: msg.command.endpointIndex }); const nonceResp = yield* sendCommandGenerator(driver, ctx, cc, (msg2, result) => { additionalTimeoutMs = Math.ceil(msg2.rtt / 1e6); onMessageSent(msg2, result); }, { // Only try getting a nonce once maxSendAttempts: 1 }); if (!nonceResp) { throw new import_core.ZWaveError("No nonce received from the node, cannot send secure command!", import_core.ZWaveErrorCodes.Security2CC_NoSPAN); } } let response = yield* maybeTransportServiceGenerator(driver, ctx, msg, onMessageSent, additionalTimeoutMs); let nonceReport; if ((0, import_serialapi.isTransmitReport)(response) && msg.command.verifyDelivery && !msg.command.expectsCCResponse(driver) && !msg.command.getEncapsulatedCC(import_core.CommandClasses.Supervision, import_cc.SupervisionCommand.Get)) { nonceReport = await driver.waitForCommand((cc) => cc.nodeId === nodeId && cc instanceof import_cc.Security2CCNonceReport, 500).catch(() => void 0); } else if ((0, import_serialapi.containsCC)(response) && response.command instanceof import_cc.Security2CCNonceReport) { nonceReport = response.command; } if (nonceReport) { if (nonceReport.SOS && nonceReport.receiverEI) { secMan.storeRemoteEI(nodeId, nonceReport.receiverEI); } if (nonceReport.MOS) { const multicastGroupId = msg.command.getMulticastGroupId(); if (multicastGroupId != void 0) { const mpan = secMan.getInnerMPANState(multicastGroupId); if (mpan) { msg.command.extensions = msg.command.extensions.filter((e) => !(e instanceof import_cc.MGRPExtension)); msg.command.extensions.push(new import_cc.MPANExtension({ groupId: multicastGroupId, innerMPANState: mpan })); } } } driver.controllerLog.logNode(nodeId, { message: `failed to decode the message, retrying with SPAN extension...`, direction: "none" }); msg.prepareRetransmission(); response = yield* maybeTransportServiceGenerator(driver, ctx, msg, onMessageSent, additionalTimeoutMs); if ((0, import_serialapi.containsCC)(response) && response.command instanceof import_cc.Security2CCNonceReport) { driver.controllerLog.logNode(nodeId, { message: `failed to decode the message after re-transmission with SPAN extension, dropping the message.`, direction: "none", level: "warn" }); throw new import_core.ZWaveError("The node failed to decode the message.", import_core.ZWaveErrorCodes.Security2CC_CannotDecode); } } return response; }, "secureMessageGeneratorS2"); const secureMessageGeneratorS2Multicast = /* @__PURE__ */ __name(async function* (driver, ctx, msg, onMessageSent) { if (!(0, import_serialapi.isSendData)(msg) || !(0, import_serialapi.containsCC)(msg)) { throw new import_core.ZWaveError("Cannot use the S2 multicast message generator for a command that's not a SendData message!", import_core.ZWaveErrorCodes.Argument_Invalid); } else if (msg.command.isSinglecast()) { throw new import_core.ZWaveError("Cannot use the S2 multicast message generator for singlecast commands!", import_core.ZWaveErrorCodes.Argument_Invalid); } else if (!(msg.command instanceof import_cc.Security2CCMessageEncapsulation)) { throw new import_core.ZWaveError("The S2 multicast message generator can only be used for Security S2 command encapsulation!", import_core.ZWaveErrorCodes.Argument_Invalid); } const groupId = msg.command.getMulticastGroupId(); if (groupId == void 0) { throw new import_core.ZWaveError("Cannot use the S2 multicast message generator without a multicast group ID!", import_core.ZWaveErrorCodes.Argument_Invalid); } const secMan = driver.getSecurityManager2(msg.command.nodeId); const group = secMan.getMulticastGroup(groupId); if (!group) { throw new import_core.ZWaveError(`Multicast group ${groupId} does not exist!`, import_core.ZWaveErrorCodes.Argument_Invalid); } const response = yield* simpleMessageGenerator(driver, ctx, msg, onMessageSent); secMan.tryIncrementMPAN(groupId); driver.unwrapCommands(msg); const command = msg.command; const encapsulationFlags = command.encapsulationFlags; const distinctNodeIDs = [...new Set(group.nodeIDs)]; const supervisionResults = []; for (const nodeId of distinctNodeIDs) { command.nodeId = nodeId; command.encapsulationFlags = encapsulationFlags; command.toggleEncapsulationFlag(import_core.EncapsulationFlags.Supervision, import_cc.SupervisionCC.mayUseSupervision(driver, command)); const scMsg = driver.createSendDataMessage(command, { transmitOptions: msg.transmitOptions, maxSendAttempts: msg.maxSendAttempts }); scMsg.command.extensions.push(new import_cc.MGRPExtension({ groupId })); try { const scResponse = yield* secureMessageGeneratorS2(driver, ctx, scMsg, onMessageSent); if ((0, import_serialapi.containsCC)(scResponse) && scResponse.command instanceof import_cc.Security2CCMessageEncapsulation && scResponse.command.hasMOSExtension()) { const innerMPANState = secMan.getInnerMPANState(groupId); if (innerMPANState) { const cc = new import_cc.Security2CCMessageEncapsulation({ nodeId, extensions: [ new import_cc.MPANExtension({ groupId, innerMPANState }) ] }); yield* sendCommandGenerator(driver, ctx, cc, onMessageSent, { // Seems we need these options or some nodes won't accept the nonce transmitOptions: import_core.TransmitOptions.ACK | import_core.TransmitOptions.AutoRoute, // Only try sending a nonce once maxSendAttempts: 1, // Nonce requests must be handled immediately priority: import_core.MessagePriority.Immediate, // We don't want failures causing us to treat the node as asleep or dead changeNodeStatusOnMissingACK: false }); } } if ((0, import_serialapi.containsCC)(scResponse)) { const supervisionReport = scResponse.command.getEncapsulatedCC(import_core.CommandClasses.Supervision, import_cc.SupervisionCommand.Report); supervisionResults.push(supervisionReport?.toSupervisionResult()); } } catch (e) { driver.driverLog.print((0, import_shared.getErrorMessage)(e), "error"); supervisionResults.push({ status: import_core.SupervisionStatus.Fail }); } } const finalSupervisionResult = (0, import_core.mergeSupervisionResults)(supervisionResults); if (finalSupervisionResult) { const cc = new import_cc.SupervisionCCReport({ nodeId: import_core.NODE_ID_BROADCAST, sessionId: 0, // fake moreUpdatesFollow: false, // fake ...finalSupervisionResult }); const ret = new (driver.getSendDataSinglecastConstructor())({ sourceNodeId: driver.ownNodeId, command: cc }); return ret; } else { return response; } }, "secureMessageGeneratorS2Multicast"); function createMessageGenerator(driver, ctx, msg, onMessageSent) { const resultPromise = (0, import_deferred_promise.createDeferredPromise)(); const generator = { parent: void 0, // The transaction will set this field on creation current: void 0, self: void 0, reset: /* @__PURE__ */ __name(() => { generator.current = void 0; generator.self = void 0; }, "reset"), start: /* @__PURE__ */ __name(() => { async function* gen() { let implementation = simpleMessageGenerator; if ((0, import_serialapi.isSendData)(msg)) { if (!(0, import_serialapi.containsCC)(msg)) { throw new import_core.ZWaveError("Cannot create a message generator for a message that doesn't contain a command class", import_core.ZWaveErrorCodes.Argument_Invalid); } if (msg.command instanceof import_cc.Security2CCMessageEncapsulation) { implementation = msg.command.isSinglecast() ? secureMessageGeneratorS2 : secureMessageGeneratorS2Multicast; } else if (msg.command instanceof import_SecurityCC.SecurityCCCommandEncapsulation) { implementation = secureMessageGeneratorS0; } else if (msg.command.isSinglecast()) { implementation = maybeTransportServiceGenerator; } } const gen2 = implementation(driver, ctx, msg, onMessageSent); let sendResult; let result; while (true) { try { const { value, done } = await gen2.next(sendResult); if (done) { result = value; break; } generator.current = value; sendResult = yield generator.current; } catch (e) { if (e instanceof Error) { resultPromise.reject(e); } else if ((0, import_serialapi.isTransmitReport)(e) && !e.isOK()) { generator.reset(); return; } else { resultPromise.resolve(e); } break; } } resultPromise.resolve(result); generator.reset(); return; } __name(gen, "gen"); generator.self = gen(); return generator.self; }, "start") }; return { resultPromise, generator }; } __name(createMessageGenerator, "createMessageGenerator"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createMessageGenerator, maybeTransportServiceGenerator, secureMessageGeneratorS0, secureMessageGeneratorS2, secureMessageGeneratorS2Multicast, simpleMessageGenerator, waitForNodeUpdate }); //# sourceMappingURL=MessageGenerators.js.map