zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
520 lines (519 loc) • 24.6 kB
JavaScript
;
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