UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

403 lines (373 loc) 12.1 kB
import { CommandClass, isCommandClassContainer, Security2CCMessageEncapsulation, Security2CCNonceGet, Security2CCNonceReport, } from "@zwave-js/cc"; import { SecurityCCCommandEncapsulation, SecurityCCNonceGet, SecurityCCNonceReport, } from "@zwave-js/cc/SecurityCC"; import { SendCommandOptions, SPANState, ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; import type { Message } from "@zwave-js/serial"; import { createDeferredPromise, DeferredPromise, } from "alcalzone-shared/deferred-promise"; import { isSendData, isTransmitReport, } from "../serialapi/transport/SendDataShared"; import type { Driver } from "./Driver"; import { sendDataErrorToZWaveError } from "./StateMachineShared"; import type { MessageGenerator } from "./Transaction"; export type MessageGeneratorImplementation = ( /** A reference to the driver */ driver: Driver, /** The "primary" message */ message: Message, /** * A hook to get notified about each sent message and the result of the Serial API call * without waiting for the message generator to finish completely. */ onMessageSent: (msg: Message, result: Message | undefined) => void, /** Can be used to extend the timeout waiting for a response from a node to the sent message */ additionalCommandTimeoutMs?: number, ) => AsyncGenerator<Message, Message, Message>; export async function waitForNodeUpdate<T extends Message>( driver: Driver, msg: Message, timeoutMs: number, ): Promise<T> { try { return await driver.waitForMessage<T>((received) => { return msg.isExpectedNodeUpdate(received); }, timeoutMs); } catch (e) { throw new ZWaveError( `Timed out while waiting for a response from the node`, ZWaveErrorCodes.Controller_NodeTimeout, ); } } /** A simple message generator that simply sends a message, waits for the ACK (and the response if one is expected) */ export const simpleMessageGenerator: MessageGeneratorImplementation = async function* ( driver, msg, onMessageSent, additionalCommandTimeoutMs = 0, ) { // Pass this message to the send thread and wait for it to be sent let result: Message; let commandTimeMs: number; try { // The yield can throw and must be handled here result = yield msg; // Figure out how long the message took to be handled msg.markAsCompleted(); commandTimeMs = Math.ceil(msg.rtt! / 1e6); onMessageSent(msg, result); } catch (e) { msg.markAsCompleted(); throw e; } // If the message was sent to a node and came back with a NOK callback, // we want to inspect the callback, for example to look at TX statistics // or update the node status. // // We now need to throw because the callback was passed through so we could inspect it. if (isTransmitReport(result) && !result.isOK()) { // Throw the message in order to short-circuit all possible generators throw result; } // If the sent message expects an update from the node, wait for it if (msg.expectsNodeUpdate()) { // CommandTime is measured by the application // ReportTime timeout SHOULD be set to CommandTime + 1 second. const timeout = commandTimeMs + driver.options.timeouts.report + additionalCommandTimeoutMs; return waitForNodeUpdate(driver, msg, timeout); } return result; }; /** A simple (internal) generator that simply sends a command, and optionally returns the response command */ async function* sendCommandGenerator< TResponse extends CommandClass = CommandClass, >( driver: Driver, command: CommandClass, onMessageSent: (msg: Message, result: Message | undefined) => void, options: SendCommandOptions, ) { const msg = driver.createSendDataMessage(command, options); const resp = yield* simpleMessageGenerator(driver, msg, onMessageSent); if (resp && isCommandClassContainer(resp)) { driver.unwrapCommands(resp); return resp.command as TResponse; } } /** A message generator for security encapsulated messages (S0) */ export const secureMessageGeneratorS0: MessageGeneratorImplementation = async function* (driver, msg, onMessageSent) { if (!isSendData(msg)) { throw new ZWaveError( "Cannot use the S0 message generator for a command that's not a SendData message!", ZWaveErrorCodes.Argument_Invalid, ); } else if (typeof msg.command.nodeId !== "number") { throw new ZWaveError( "Cannot use the S0 message generator for multicast commands!", ZWaveErrorCodes.Argument_Invalid, ); } else if (!(msg.command instanceof SecurityCCCommandEncapsulation)) { throw new ZWaveError( "The S0 message generator can only be used for Security S0 command encapsulation!", ZWaveErrorCodes.Argument_Invalid, ); } // Step 1: Acquire a nonce const secMan = driver.securityManager!; const nodeId = msg.command.nodeId; let additionalTimeoutMs: number | undefined; // Try to get a free nonce before requesting a new one let nonce: Buffer | undefined = secMan.getFreeNonce(nodeId); if (!nonce) { // No free nonce, request a new one const cc = new SecurityCCNonceGet(driver, { nodeId: nodeId, endpoint: msg.command.endpointIndex, }); const nonceResp = yield* sendCommandGenerator<SecurityCCNonceReport>( driver, cc, (msg, result) => { additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6); onMessageSent(msg, result); }, { // Only try getting a nonce once maxSendAttempts: 1, }, ); if (!nonceResp) { throw new ZWaveError( "No nonce received from the node, cannot send secure command!", ZWaveErrorCodes.SecurityCC_NoNonce, ); } nonce = nonceResp.nonce; } msg.command.nonce = nonce; // Now send the actual secure command return yield* simpleMessageGenerator( driver, msg, onMessageSent, additionalTimeoutMs, ); }; /** A message generator for security encapsulated messages (S2) */ export const secureMessageGeneratorS2: MessageGeneratorImplementation = async function* (driver, msg, onMessageSent) { if (!isSendData(msg)) { throw new ZWaveError( "Cannot use the S2 message generator for a command that's not a SendData message!", ZWaveErrorCodes.Argument_Invalid, ); } else if (typeof msg.command.nodeId !== "number") { throw new ZWaveError( "Cannot use the S2 message generator for multicast commands!", // (yet) ZWaveErrorCodes.Argument_Invalid, ); } else if (!(msg.command instanceof Security2CCMessageEncapsulation)) { throw new ZWaveError( "The S2 message generator can only be used for Security S2 command encapsulation!", ZWaveErrorCodes.Argument_Invalid, ); } const secMan = driver.securityManager2!; const nodeId = msg.command.nodeId; const spanState = secMan.getSPANState(nodeId); let additionalTimeoutMs: number | undefined; if ( spanState.type === SPANState.None || spanState.type === SPANState.LocalEI ) { // Request a new nonce // No free nonce, request a new one const cc = new Security2CCNonceGet(driver, { nodeId: nodeId, endpoint: msg.command.endpointIndex, }); const nonceResp = yield* sendCommandGenerator<Security2CCNonceReport>( driver, cc, (msg, result) => { additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6); onMessageSent(msg, result); }, { // Only try getting a nonce once maxSendAttempts: 1, }, ); if (!nonceResp) { throw new ZWaveError( "No nonce received from the node, cannot send secure command!", ZWaveErrorCodes.Security2CC_NoSPAN, ); } // Storing the nonce is not necessary, this will be done automatically when the nonce is received } // Now send the actual secure command let response = yield* simpleMessageGenerator( driver, msg, onMessageSent, additionalTimeoutMs, ); if ( isCommandClassContainer(response) && response.command instanceof Security2CCNonceReport ) { const command = response.command; if (command.SOS && command.receiverEI) { // The node couldn't decrypt the last command we sent it. Invalidate // the shared SPAN, since it did the same secMan.storeRemoteEI(nodeId, command.receiverEI); } driver.controllerLog.logNode(nodeId, { message: `failed to decode the message, retrying with SPAN extension...`, direction: "none", }); // Prepare the message for re-transmission msg.callbackId = undefined; msg.command.unsetSequenceNumber(); // Send the message again response = yield* simpleMessageGenerator( driver, msg, onMessageSent, additionalTimeoutMs, ); if ( isCommandClassContainer(response) && response.command instanceof Security2CCNonceReport ) { // No dice 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 ZWaveError( "The node failed to decode the message.", ZWaveErrorCodes.Security2CC_CannotDecode, ); } } return response; }; export function createMessageGenerator<TResponse extends Message = Message>( driver: Driver, msg: Message, onMessageSent: (msg: Message, result: Message | undefined) => void, ): { generator: MessageGenerator; resultPromise: DeferredPromise<TResponse>; } { const resultPromise = createDeferredPromise<TResponse>(); const generator: MessageGenerator = { parent: undefined as any, // The transaction will set this field on creation current: undefined, self: undefined, start: () => { const resetGenerator = () => { generator.current = undefined; generator.self = undefined; }; async function* gen() { // Determine which message generator implementation should be used let implementation: MessageGeneratorImplementation = simpleMessageGenerator; if (isSendData(msg)) { if ( msg.command instanceof Security2CCMessageEncapsulation ) { implementation = secureMessageGeneratorS2; } else if ( msg.command instanceof SecurityCCCommandEncapsulation ) { implementation = secureMessageGeneratorS0; } } // Step through the generator so we can easily cancel it and don't // accidentally forget to unset this.current at the end const gen = implementation(driver, msg, onMessageSent); let sendResult: Message | undefined; let result: Message | undefined; while (true) { // This call passes the previous send result (if it exists already) to the generator and saves the // generated or returned message in `value`. When `done` is true, `value` contains the returned result of the message generator try { const { value, done } = await gen.next(sendResult!); if (done) { result = value; break; } // Pass the generated message to the transaction machine and remember the result for the next iteration generator.current = value; sendResult = yield generator.current; } catch (e) { if (e instanceof Error) { // There was an actual error, reject the transaction resultPromise.reject(e); } else if (isTransmitReport(e) && !e.isOK()) { // The generator was prematurely ended by throwing a NOK transmit report. // If this cannot be handled (e.g. by moving the messages to the wakeup queue), we need // to treat this as an error if ( driver.handleMissingNodeACK( generator.parent as any, ) ) { resetGenerator(); return; } else { resultPromise.reject( sendDataErrorToZWaveError( "callback NOK", generator.parent, e, ), ); } } else { // The generator was prematurely ended by throwing a Message resultPromise.resolve(e as TResponse); } break; } } resultPromise.resolve(result as TResponse); resetGenerator(); return; } generator.self = gen(); return generator.self; }, }; return { resultPromise, generator }; }