UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

689 lines 37 kB
import { FirmwareDownloadStatus, FirmwareUpdateMetaDataCC, FirmwareUpdateMetaDataCCGet, FirmwareUpdateMetaDataCCReport, FirmwareUpdateMetaDataCCRequestReport, FirmwareUpdateMetaDataCCStatusReport, FirmwareUpdateMetaDataCCValues, FirmwareUpdateRequestStatus, FirmwareUpdateStatus, getEffectiveCCVersion, } from "@zwave-js/cc"; import { CRC16_CCITT, CommandClasses, EncapsulationFlags, SecurityClass, ZWaveError, ZWaveErrorCodes, randomBytes, securityClassIsS2, timespan, } from "@zwave-js/core"; import { containsCC } from "@zwave-js/serial/serialapi"; import { getEnumMemberName, throttle } from "@zwave-js/shared"; import { waitFor } from "@zwave-js/waddle"; import { distinct } from "alcalzone-shared/arrays"; import { wait } from "alcalzone-shared/async"; import { createDeferredPromise, } from "alcalzone-shared/deferred-promise"; import { roundTo } from "alcalzone-shared/math"; import { isArray } from "alcalzone-shared/typeguards"; import { TaskPriority, } from "../../driver/Task.js"; import { SchedulePollMixin } from "./60_ScheduledPoll.js"; /** Checks if a task belongs to a route rebuilding process */ export function isFirmwareUpdateOTATask(t) { return t.tag?.id === "firmware-update-ota"; } export class FirmwareUpdateMixin extends SchedulePollMixin { async getFirmwareUpdateCapabilities() { const api = this.commandClasses["Firmware Update Meta Data"]; const meta = await api.getMetaData(); if (!meta) { throw new ZWaveError(`Failed to request firmware update capabilities: The node did not respond in time!`, ZWaveErrorCodes.Controller_NodeTimeout); } else if (!meta.firmwareUpgradable) { return { firmwareUpgradable: false, }; } return { firmwareUpgradable: true, // TODO: Targets are not the list of IDs - maybe expose the IDs as well? firmwareTargets: Array .from({ length: 1 + meta.additionalFirmwareIDs.length }) .fill(0) .map((_, i) => i), continuesToFunction: meta.continuesToFunction, supportsActivation: meta.supportsActivation, supportsResuming: meta.supportsResuming, supportsNonSecureTransfer: meta.supportsNonSecureTransfer, }; } getFirmwareUpdateCapabilitiesCached() { const firmwareUpgradable = this.getValue(FirmwareUpdateMetaDataCCValues.firmwareUpgradable.id); const supportsActivation = this.getValue(FirmwareUpdateMetaDataCCValues.supportsActivation.id); const continuesToFunction = this.getValue(FirmwareUpdateMetaDataCCValues.continuesToFunction.id); const additionalFirmwareIDs = this.getValue(FirmwareUpdateMetaDataCCValues.additionalFirmwareIDs.id); const supportsResuming = this.getValue(FirmwareUpdateMetaDataCCValues.supportsResuming.id); const supportsNonSecureTransfer = this.getValue(FirmwareUpdateMetaDataCCValues.supportsNonSecureTransfer.id); // Ensure all information was queried if (!firmwareUpgradable || !isArray(additionalFirmwareIDs)) { return { firmwareUpgradable: false }; } return { firmwareUpgradable: true, // TODO: Targets are not the list of IDs - maybe expose the IDs as well? firmwareTargets: Array .from({ length: 1 + additionalFirmwareIDs.length }) .fill(0) .map((_, i) => i), continuesToFunction, supportsActivation, supportsResuming, supportsNonSecureTransfer, }; } _abortFirmwareUpdate; async abortFirmwareUpdate() { if (!this._abortFirmwareUpdate) return; await this._abortFirmwareUpdate(); } // Stores the CRC of the previously transferred firmware image. // Allows detecting whether resuming is supported and where to continue in a multi-file transfer. _previousFirmwareCRC; /** Is used to remember fragment requests that came in before they were able to be handled */ _firmwareUpdatePrematureRequest; async updateFirmware(updates, options = {}) { if (updates.length === 0) { throw new ZWaveError(`At least one update must be provided`, ZWaveErrorCodes.Argument_Invalid); } // Check that each update has a buffer with at least 1 byte if (updates.some((u) => u.data.length === 0)) { throw new ZWaveError(`All firmware updates must have a non-empty data buffer`, ZWaveErrorCodes.Argument_Invalid); } // Check that the targets are not duplicates if (distinct(updates.map((u) => u.firmwareTarget ?? 0)).length !== updates.length) { throw new ZWaveError(`The target of all provided firmware updates must be unique`, ZWaveErrorCodes.Argument_Invalid); } // Don't start the process twice if (this.driver.isOTWFirmwareUpdateInProgress()) { throw new ZWaveError(`Failed to start the update: An OTW upgrade of the controller is in progress!`, ZWaveErrorCodes.FirmwareUpdateCC_Busy); } // Don't allow starting two firmware updates for the same node const task = this.getUpdateFirmwareTask(updates, options); if (task instanceof Promise) { throw new ZWaveError(`Failed to start the update: A firmware update is already in progress for this node!`, ZWaveErrorCodes.FirmwareUpdateCC_Busy); } // Queue the task return this.driver.scheduler.queueTask(task); } isFirmwareUpdateInProgress() { return !!this.driver.scheduler.findTask(isFirmwareUpdateOTATask); } getUpdateFirmwareTask(updates, options = {}) { const self = this; // This task should only run once at a time const existingTask = this.driver.scheduler.findTask((t) => t.tag?.id === "firmware-update-ota" && t.tag.nodeId === self.id); if (existingTask) return existingTask; let keepAwake; return { // Firmware updates cause a lot of traffic. Execute them in the background. priority: TaskPriority.Lower, tag: { id: "firmware-update-ota", nodeId: self.id }, task: async function* firmwareUpdateTask() { // Keep battery powered nodes awake during the process keepAwake = self.keepAwake; self.keepAwake = true; // Support aborting the update const abortContext = { abort: false, tooLateToAbort: false, abortPromise: createDeferredPromise(), }; self._abortFirmwareUpdate = async () => { if (abortContext.tooLateToAbort) { throw new ZWaveError(`The firmware update was transmitted completely, cannot abort anymore.`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToAbort); } self.driver.controllerLog.logNode(self.id, { message: `Aborting firmware update...`, direction: "outbound", }); // Trigger the abort abortContext.abort = true; const aborted = await abortContext.abortPromise; if (!aborted) { throw new ZWaveError(`The node did not acknowledge the aborted update`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToAbort); } self.driver.controllerLog.logNode(self.id, { message: `Firmware update aborted`, direction: "inbound", }); }; // Prepare the firmware update let fragmentSizeSecure; let fragmentSizeNonSecure; let hardwareVersion; let meta; try { const prepareResult = await self .prepareFirmwareUpdateInternal(updates.map((u) => u.firmwareTarget ?? 0), abortContext); // Handle early aborts if (abortContext.abort) { const result = { success: false, status: FirmwareUpdateStatus .Error_TransmissionFailed, reInterview: false, }; self._emit("firmware update finished", self, result); return result; } // If the firmware update was not aborted, prepareResult is definitely defined ({ fragmentSizeSecure, fragmentSizeNonSecure, hardwareVersion, ...meta } = prepareResult); } catch { // Not sure what the error is, but we'll label it "transmission failed" const result = { success: false, status: FirmwareUpdateStatus.Error_TransmissionFailed, reInterview: false, }; return result; } yield; // Give the task scheduler time to do something else // The resume and non-secure transfer features may not be supported by the node // If not, disable them, even though the application requested them if (!meta.supportsResuming) options.resume = false; const securityClass = self.getHighestSecurityClass(); const isSecure = securityClass === SecurityClass.S0_Legacy || securityClassIsS2(securityClass); if (!isSecure) { // The nonSecureTransfer option is only relevant for secure devices options.nonSecureTransfer = false; } else if (!meta.supportsNonSecureTransfer) { options.nonSecureTransfer = false; } // Throttle the progress emitter so applications can handle the load of events const notifyProgress = throttle((progress) => self._emit("firmware update progress", self, progress), 250, true); // If resuming is supported and desired, try to figure out with which file to continue const updatesWithChecksum = updates.map((u) => ({ ...u, checksum: CRC16_CCITT(u.data), })); let skipFinishedFiles = -1; let shouldResume = options.resume && self._previousFirmwareCRC != undefined; if (shouldResume) { skipFinishedFiles = updatesWithChecksum.findIndex((u) => u.checksum === self._previousFirmwareCRC); if (skipFinishedFiles === -1) shouldResume = false; } // Perform all firmware updates in sequence let updateResult; let conservativeWaitTime; const totalBytes = updatesWithChecksum.reduce((total, update) => total + update.data.length, 0); let sentBytesOfPreviousFiles = 0; for (let i = 0; i < updatesWithChecksum.length; i++) { const { // If the firmware target is not given, update the Z-Wave chip firmwareTarget: target = 0, // If the firmware ID is not given, use the current one for the given chip firmwareId = target === 0 ? meta.firmwareId : meta.additionalFirmwareIDs[target - 1], data, checksum, } = updatesWithChecksum[i]; if (i < skipFinishedFiles) { // If we are resuming, skip this file since it was already done before self.driver.controllerLog.logNode(self.id, `Skipping already completed firmware update (part ${i + 1} / ${updatesWithChecksum.length})...`); sentBytesOfPreviousFiles += data.length; continue; } self.driver.controllerLog.logNode(self.id, `Updating firmware (part ${i + 1} / ${updatesWithChecksum.length})...`); // For determining the initial fragment size, assume the node respects our choice. // If the node is not secure, these two values are identical anyways. let fragmentSize = options.nonSecureTransfer ? fragmentSizeNonSecure : fragmentSizeSecure; // Tell the node to start requesting fragments const { resume, nonSecureTransfer } = yield* self .beginFirmwareUpdateInternal(data, meta.manufacturerId, target, firmwareId, fragmentSize, checksum, hardwareVersion, shouldResume, options.nonSecureTransfer); // If the node did not accept non-secure transfer, revisit our choice of fragment size if (options.nonSecureTransfer && !nonSecureTransfer) { fragmentSize = fragmentSizeSecure; } // Remember the checksum, so we can resume if necessary self._previousFirmwareCRC = checksum; if (shouldResume) { self.driver.controllerLog.logNode(self.id, `Node ${resume ? "accepted" : "did not accept"} resuming the update...`); } if (nonSecureTransfer) { self.driver.controllerLog.logNode(self.id, `Firmware will be transferred without encryption...`); } yield; // Give the task scheduler time to do something else // Listen for firmware update fragment requests and handle them updateResult = yield* self.doFirmwareUpdateInternal(data, fragmentSize, nonSecureTransfer, abortContext, (fragment, total) => { const progress = { currentFile: i + 1, totalFiles: updatesWithChecksum.length, sentFragments: fragment, totalFragments: total, progress: roundTo(((sentBytesOfPreviousFiles + Math.min(fragment * fragmentSize, data.length)) / totalBytes) * 100, 2), }; notifyProgress(progress); // When this file is done, add the fragments to the total, so we can compute the total progress correctly if (fragment === total) { sentBytesOfPreviousFiles += data.length; } }); // If we wait, wait a bit longer than the device told us, so it is actually ready to use conservativeWaitTime = self.driver .getConservativeWaitTimeAfterFirmwareUpdate(updateResult.waitTime); if (!updateResult.success) { self.driver.controllerLog.logNode(self.id, { message: `Firmware update (part ${i + 1} / ${updatesWithChecksum.length}) failed with status ${getEnumMemberName(FirmwareUpdateStatus, updateResult.status)}`, direction: "inbound", }); const result = { ...updateResult, waitTime: undefined, reInterview: false, }; self._emit("firmware update finished", self, result); return result; } else if (i < updatesWithChecksum.length - 1) { // Update succeeded, but we're not done yet self.driver.controllerLog.logNode(self.id, { message: `Firmware update (part ${i + 1} / ${updatesWithChecksum.length}) succeeded with status ${getEnumMemberName(FirmwareUpdateStatus, updateResult.status)}`, direction: "inbound", }); self.driver.controllerLog.logNode(self.id, `Continuing with next part in ${conservativeWaitTime} seconds...`); // If we've resumed the previous file, there's no need to resume the next one too shouldResume = false; yield* waitFor(wait(conservativeWaitTime * 1000, true)); } } // We're done. No need to resume this update self._previousFirmwareCRC = undefined; const result = { ...updateResult, waitTime: conservativeWaitTime, reInterview: true, }; // After a successful firmware update, we want to interview sleeping nodes immediately, // so don't send them to sleep when they wake up keepAwake = true; self._emit("firmware update finished", self, result); return result; }, cleanup() { self._abortFirmwareUpdate = undefined; self._firmwareUpdatePrematureRequest = undefined; // Make sure that the keepAwake flag gets reset at the end self.keepAwake = keepAwake; if (!keepAwake) { setImmediate(() => { self.driver.debounceSendNodeToSleep(self); }); } return Promise.resolve(); }, }; } /** Prepares the firmware update of a single target by collecting the necessary information */ async prepareFirmwareUpdateInternal(targets, abortContext) { const api = this.commandClasses["Firmware Update Meta Data"]; // ================================ // STEP 1: // Check if this update is possible const meta = await api.getMetaData(); if (!meta) { throw new ZWaveError(`Failed to start the update: The node did not respond in time!`, ZWaveErrorCodes.Controller_NodeTimeout); } for (const target of targets) { if (target === 0) { if (!meta.firmwareUpgradable) { throw new ZWaveError(`Failed to start the update: The Z-Wave chip firmware is not upgradable!`, ZWaveErrorCodes.FirmwareUpdateCC_NotUpgradable); } } else { if (api.version < 3) { throw new ZWaveError(`Failed to start the update: The node does not support upgrading a different firmware target than 0!`, ZWaveErrorCodes.FirmwareUpdateCC_TargetNotFound); } else if (meta.additionalFirmwareIDs[target - 1] == undefined) { throw new ZWaveError(`Failed to start the update: Firmware target #${target} not found on this node!`, ZWaveErrorCodes.FirmwareUpdateCC_TargetNotFound); } } } // ================================ // STEP 2: // Determine the fragment size const fcc = new FirmwareUpdateMetaDataCC({ nodeId: this.id }); fcc.toggleEncapsulationFlag(EncapsulationFlags.Security, this.driver.isCCSecure(fcc.ccId, this.id)); const maxGrossPayloadSizeSecure = this.driver .computeNetCCPayloadSize(fcc); const maxGrossPayloadSizeNonSecure = this.driver .computeNetCCPayloadSize(fcc, true); const ccVersion = getEffectiveCCVersion(this.driver, fcc); const maxNetPayloadSizeSecure = maxGrossPayloadSizeSecure - 2 // report number - (ccVersion >= 2 ? 2 : 0); // checksum const maxNetPayloadSizeNonSecure = maxGrossPayloadSizeNonSecure - 2 // report number - (ccVersion >= 2 ? 2 : 0); // checksum // Use the smallest allowed payload const fragmentSizeSecure = Math.min(maxNetPayloadSizeSecure, meta.maxFragmentSize ?? Number.POSITIVE_INFINITY); const fragmentSizeNonSecure = Math.min(maxNetPayloadSizeNonSecure, meta.maxFragmentSize ?? Number.POSITIVE_INFINITY); if (abortContext.abort) { abortContext.abortPromise.resolve(true); return; } else { return { ...meta, fragmentSizeSecure, fragmentSizeNonSecure, }; } } async handleUnexpectedFirmwareUpdateGet(command) { // This method will only be called under two circumstances: // 1. The node is currently busy responding to a firmware update request -> remember the request if (this.isFirmwareUpdateInProgress()) { this._firmwareUpdatePrematureRequest = command; return; } // 2. No firmware update is in progress -> abort this.driver.controllerLog.logNode(this.id, { message: `Received Firmware Update Get, but no firmware update is in progress. Forcing the node to abort...`, direction: "inbound", }); // Since no update is in progress, we need to determine the fragment size again const fcc = new FirmwareUpdateMetaDataCC({ nodeId: this.id }); fcc.toggleEncapsulationFlag(EncapsulationFlags.Security, !!(command.encapsulationFlags & EncapsulationFlags.Security)); const ccVersion = getEffectiveCCVersion(this.driver, fcc); const fragmentSize = this.driver.computeNetCCPayloadSize(fcc) - 2 // report number - (ccVersion >= 2 ? 2 : 0); // checksum const fragment = randomBytes(fragmentSize); try { await this.sendCorruptedFirmwareUpdateReport(command.reportNumber, fragment); } catch { // ignore } } /** Kicks off a firmware update of a single target. Returns whether the node accepted resuming and non-secure transfer */ async *beginFirmwareUpdateInternal(data, manufacturerId, target, firmwareId, fragmentSize, checksum, hardwareVersion, resume, nonSecureTransfer) { const api = this.commandClasses["Firmware Update Meta Data"]; // ================================ // STEP 3: // Start the update this.driver.controllerLog.logNode(this.id, { message: `Starting firmware update...`, direction: "outbound", }); // Request the node to start the upgrade let result = await api.requestUpdate({ // TODO: Should manufacturer id be provided externally? manufacturerId, firmwareId, firmwareTarget: target, fragmentSize, checksum, hardwareVersion, resume, nonSecureTransfer, }); // On some devices the response can take a minute or so to be received, // probably because of a manual out-of-band activation. if (!result) { result = yield* waitFor(this.driver .waitForCommand((cc) => cc instanceof FirmwareUpdateMetaDataCCRequestReport && cc.nodeId === this.id, 60000)); } switch (result.status) { case FirmwareUpdateRequestStatus.Error_AuthenticationExpected: throw new ZWaveError(`Failed to start the update: A manual authentication event (e.g. button push) was expected!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart); case FirmwareUpdateRequestStatus.Error_BatteryLow: throw new ZWaveError(`Failed to start the update: The battery level is too low!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart); case FirmwareUpdateRequestStatus .Error_FirmwareUpgradeInProgress: throw new ZWaveError(`Failed to start the update: A firmware upgrade is already in progress!`, ZWaveErrorCodes.FirmwareUpdateCC_Busy); case FirmwareUpdateRequestStatus .Error_InvalidManufacturerOrFirmwareID: throw new ZWaveError(`Failed to start the update: Invalid manufacturer or firmware id!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart); case FirmwareUpdateRequestStatus.Error_InvalidHardwareVersion: throw new ZWaveError(`Failed to start the update: Invalid hardware version!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart); case FirmwareUpdateRequestStatus.Error_NotUpgradable: throw new ZWaveError(`Failed to start the update: Firmware target #${target} is not upgradable!`, ZWaveErrorCodes.FirmwareUpdateCC_NotUpgradable); case FirmwareUpdateRequestStatus.Error_FragmentSizeTooLarge: throw new ZWaveError(`Failed to start the update: The chosen fragment size is too large!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart); case FirmwareUpdateRequestStatus.OK: // All good, we have started! // Keep the node awake until the update is done. this.keepAwake = true; } return { resume: !!result.resume, nonSecureTransfer: !!result.nonSecureTransfer, }; } async handleFirmwareUpdateMetaDataGet(command) { const endpoint = this.getEndpoint(command.endpointIndex) ?? this; // We are being queried, so the device may actually not support the CC, just control it. // Using the commandClasses property would throw in that case const api = endpoint .createAPI(CommandClasses["Firmware Update Meta Data"], false) .withOptions({ // Answer with the same encapsulation as asked, but omit // Supervision as it shouldn't be used for Get-Report flows encapsulationFlags: command.encapsulationFlags & ~EncapsulationFlags.Supervision, }); // We do not support the firmware to be upgraded. await api.reportMetaData({ manufacturerId: this.driver.options.vendor?.manufacturerId ?? 0xffff, firmwareUpgradable: false, hardwareVersion: this.driver.options.vendor?.hardwareVersion ?? 0, // We must advertise Z-Wave JS itself as firmware 1 // No firmware is upgradable, so we advertise firmware id 0 additionalFirmwareIDs: [0], }); } async handleFirmwareUpdateRequestGet(command) { const endpoint = this.getEndpoint(command.endpointIndex) ?? this; // We are being queried, so the device may actually not support the CC, just control it. // Using the commandClasses property would throw in that case const api = endpoint .createAPI(CommandClasses["Firmware Update Meta Data"], false) .withOptions({ // Answer with the same encapsulation as asked, but omit // Supervision as it shouldn't be used for Get-Report flows encapsulationFlags: command.encapsulationFlags & ~EncapsulationFlags.Supervision, }); // We do not support the firmware to be upgraded. await api.respondToUpdateRequest({ status: FirmwareUpdateRequestStatus.Error_NotUpgradable, }); } async handleFirmwareUpdatePrepareGet(command) { const endpoint = this.getEndpoint(command.endpointIndex) ?? this; // We are being queried, so the device may actually not support the CC, just control it. // Using the commandClasses property would throw in that case const api = endpoint .createAPI(CommandClasses["Firmware Update Meta Data"], false) .withOptions({ // Answer with the same encapsulation as asked, but omit // Supervision as it shouldn't be used for Get-Report flows encapsulationFlags: command.encapsulationFlags & ~EncapsulationFlags.Supervision, }); // We do not support the firmware to be downloaded await api.respondToDownloadRequest({ status: FirmwareDownloadStatus.Error_NotDownloadable, checksum: 0x0000, }); } async sendCorruptedFirmwareUpdateReport(reportNum, fragment, nonSecureTransfer = false) { try { await this.commandClasses["Firmware Update Meta Data"] .withOptions({ // Only encapsulate if the transfer is secure autoEncapsulate: !nonSecureTransfer, }) .sendFirmwareFragment(reportNum, true, fragment); } catch { // ignore } } hasPendingFirmwareUpdateFragment(fragmentNumber) { // Avoid queuing duplicate fragments const isCurrentFirmwareFragment = (t) => t.message.getNodeId() === this.id && containsCC(t.message) && t.message.command instanceof FirmwareUpdateMetaDataCCReport && t.message.command.reportNumber === fragmentNumber; return this.driver.hasPendingTransactions(isCurrentFirmwareFragment); } async *doFirmwareUpdateInternal(data, fragmentSize, nonSecureTransfer, abortContext, onProgress) { const numFragments = Math.ceil(data.length / fragmentSize); // Make sure we're not responding to an outdated request immediately this._firmwareUpdatePrematureRequest = undefined; // ================================ // STEP 4: // Respond to fragment requests from the node update: while (true) { yield; // Give the task scheduler time to do something else // During ongoing firmware updates, it can happen that the next request is received before the callback for the previous response // is back. In that case we can immediately handle the premature request. Otherwise wait for the next request. let fragmentRequest; if (this._firmwareUpdatePrematureRequest) { fragmentRequest = this._firmwareUpdatePrematureRequest; this._firmwareUpdatePrematureRequest = undefined; } else { try { fragmentRequest = yield* waitFor(this.driver .waitForCommand((cc) => cc.nodeId === this.id && cc instanceof FirmwareUpdateMetaDataCCGet, // Wait up to 2 minutes for each fragment request. // Some users try to update devices with unstable connections, where 30s can be too short. timespan.minutes(2))); } catch { // In some cases it can happen that the device stops requesting update frames // We need to timeout the update in this case so it can be restarted this.driver.controllerLog.logNode(this.id, { message: `Firmware update timed out`, direction: "none", level: "warn", }); return { success: false, status: FirmwareUpdateStatus.Error_Timeout, }; } } // When a node requests a firmware update fragment, it must be awake this.markAsAwake(); if (fragmentRequest.reportNumber > numFragments) { this.driver.controllerLog.logNode(this.id, { message: `Received Firmware Update Get for an out-of-bounds fragment. Forcing the node to abort...`, direction: "inbound", }); await this.sendCorruptedFirmwareUpdateReport(fragmentRequest.reportNumber, randomBytes(fragmentSize), nonSecureTransfer); // This will cause the node to abort the process, wait for that break update; } // Actually send the requested frames request: for (let num = fragmentRequest.reportNumber; num < fragmentRequest.reportNumber + fragmentRequest.numReports; num++) { yield; // Give the task scheduler time to do something else // Check if the node requested more fragments than are left if (num > numFragments) { break; } const fragment = data.subarray((num - 1) * fragmentSize, num * fragmentSize); if (abortContext.abort) { await this.sendCorruptedFirmwareUpdateReport(fragmentRequest.reportNumber, randomBytes(fragment.length), nonSecureTransfer); // This will cause the node to abort the process, wait for that break update; } else { // Avoid queuing duplicate fragments if (this.hasPendingFirmwareUpdateFragment(num)) { this.driver.controllerLog.logNode(this.id, { message: `Firmware fragment ${num} already queued`, level: "warn", }); continue request; } this.driver.controllerLog.logNode(this.id, { message: `Sending firmware fragment ${num} / ${numFragments}`, direction: "outbound", }); const isLast = num === numFragments; try { await this .commandClasses["Firmware Update Meta Data"] .withOptions({ // Only encapsulate if the transfer is secure autoEncapsulate: !nonSecureTransfer, }) .sendFirmwareFragment(num, isLast, fragment); onProgress(num, numFragments); // If that was the last one wait for status report from the node and restart interview if (isLast) { abortContext.tooLateToAbort = true; break update; } } catch { // When transmitting fails, simply stop responding to this request and wait for the node to re-request the fragment this.driver.controllerLog.logNode(this.id, { message: `Failed to send firmware fragment ${num} / ${numFragments}`, direction: "outbound", level: "warn", }); break request; } } } } yield; // Give the task scheduler time to do something else // ================================ // STEP 5: // Finalize the update process const statusReport = yield* waitFor(this.driver .waitForCommand((cc) => cc.nodeId === this.id && cc instanceof FirmwareUpdateMetaDataCCStatusReport, // Wait up to 5 minutes. It should never take that long, but the specs // don't say anything specific 5 * 60000) .catch(() => undefined)); if (abortContext.abort) { abortContext.abortPromise.resolve(statusReport?.status === FirmwareUpdateStatus.Error_TransmissionFailed); } if (!statusReport) { this.driver.controllerLog.logNode(this.id, `The node did not acknowledge the completed update`, "warn"); return { success: false, status: FirmwareUpdateStatus.Error_Timeout, }; } const { status, waitTime } = statusReport; // Actually, OK_WaitingForActivation should never happen since we don't allow // delayed activation in the RequestGet command const success = status >= FirmwareUpdateStatus.OK_WaitingForActivation; return { success, status, waitTime, }; } } //# sourceMappingURL=70_FirmwareUpdate.js.map