UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

254 lines (253 loc) 10.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var FirmwareUpdateService_exports = {}; __export(FirmwareUpdateService_exports, { downloadFirmwareUpdate: () => downloadFirmwareUpdate, getAvailableFirmwareUpdates: () => getAvailableFirmwareUpdates, getAvailableFirmwareUpdatesBulk: () => getAvailableFirmwareUpdatesBulk }); module.exports = __toCommonJS(FirmwareUpdateService_exports); var import_core = require("@zwave-js/core"); var import_shared = require("@zwave-js/shared"); function serviceURL() { return (0, import_shared.getenv)("ZWAVEJS_FW_SERVICE_URL") || "https://firmware.zwave-js.io"; } __name(serviceURL, "serviceURL"); const DOWNLOAD_TIMEOUT = 6e4; const CHECK_TIMEOUT = 3e4; const MAX_CACHE_SECONDS = 60 * 60 * 24; const CLEAN_CACHE_INTERVAL_MS = 60 * 60 * 1e3; const deviceFirmwareCache = new import_shared.ObjectKeyMap(); let requestQueue; let cleanCacheTimeout; function cleanCache() { cleanCacheTimeout?.clear(); cleanCacheTimeout = void 0; const now = Date.now(); for (const [deviceKey, cached] of deviceFirmwareCache) { if (cached.staleDate < now) { deviceFirmwareCache.delete(deviceKey); } } if (deviceFirmwareCache.size > 0) { cleanCacheTimeout = (0, import_shared.setTimer)(cleanCache, CLEAN_CACHE_INTERVAL_MS).unref(); } } __name(cleanCache, "cleanCache"); function calculateCacheExpiry(response) { if (response.status === 200 && response.headers.has("cache-control")) { const cacheControl = response.headers.get("cache-control"); const age = response.headers.get("age"); const date = response.headers.get("date"); let maxAge; const maxAgeMatch = cacheControl.match(/max-age=(\d+)/); if (maxAgeMatch) { maxAge = Math.max(0, parseInt(maxAgeMatch[1], 10)); } if (maxAge) { let currentAge; if (age) { currentAge = parseInt(age, 10); } else if (date) { currentAge = (Date.now() - Date.parse(date)) / 1e3; } else { currentAge = 0; } currentAge = Math.max(0, currentAge); if (maxAge > currentAge) { return Date.now() + Math.min(MAX_CACHE_SECONDS, maxAge - currentAge) * 1e3; } } } return Date.now() + MAX_CACHE_SECONDS * 1e3; } __name(calculateCacheExpiry, "calculateCacheExpiry"); async function makeRequest(url, config) { const { default: ky } = await import("ky"); const response = await ky(url, config); const responseJson = await response.json(); return { data: responseJson, expiry: calculateCacheExpiry(response) }; } __name(makeRequest, "makeRequest"); function hasExtension(pathname) { return /\.[a-z0-9_]+$/i.test(pathname); } __name(hasExtension, "hasExtension"); function rfRegionToUpdateServiceRegion(rfRegion) { switch (rfRegion) { case import_core.RFRegion.Europe: case import_core.RFRegion["Europe (Long Range)"]: return "europe"; case import_core.RFRegion.USA: case import_core.RFRegion["USA (Long Range)"]: return "usa"; case import_core.RFRegion["Australia/New Zealand"]: return "australia/new zealand"; case import_core.RFRegion["Hong Kong"]: return "hong kong"; case import_core.RFRegion.India: return "india"; case import_core.RFRegion.Israel: return "israel"; case import_core.RFRegion.Russia: return "russia"; case import_core.RFRegion.China: return "china"; case import_core.RFRegion.Japan: return "japan"; case import_core.RFRegion.Korea: return "korea"; } } __name(rfRegionToUpdateServiceRegion, "rfRegionToUpdateServiceRegion"); async function getAvailableFirmwareUpdatesBulk(deviceIds, options) { const uniqueDeviceIds = deviceIds.filter((device, index) => index === deviceIds.findIndex((d) => d.manufacturerId === device.manufacturerId && d.productType === device.productType && d.productId === device.productId && d.firmwareVersion === device.firmwareVersion)); const now = Date.now(); const freshDevices = []; const staleDevices = []; for (const device of uniqueDeviceIds) { const cached = deviceFirmwareCache.get(device); if (cached && cached.staleDate > now) { freshDevices.push(device); } else { staleDevices.push(device); } } if (staleDevices.length > 0) { const headers = new Headers({ "User-Agent": options.userAgent, "Content-Type": "application/json" }); if (options.apiKey) { headers.set("X-API-Key", options.apiKey); } const body = { devices: staleDevices.map((device) => ({ manufacturerId: (0, import_shared.formatId)(device.manufacturerId), productType: (0, import_shared.formatId)(device.productType), productId: (0, import_shared.formatId)(device.productId), firmwareVersion: device.firmwareVersion })) }; const rfRegion = rfRegionToUpdateServiceRegion(options.rfRegion); if (rfRegion) { body.region = rfRegion; } const url = `${serviceURL()}/api/v4/updates`; const config = { method: "POST", json: body, headers, timeout: CHECK_TIMEOUT }; if (!requestQueue) { const PQueue = (await import("p-queue")).default; requestQueue = new PQueue({ concurrency: 2 }); } const { data: result, expiry } = await requestQueue.add(() => makeRequest(url, config)); for (const deviceResponse of result) { const originalDevice = staleDevices.find((device) => (0, import_shared.formatId)(device.manufacturerId) === deviceResponse.manufacturerId && (0, import_shared.formatId)(device.productType) === deviceResponse.productType && (0, import_shared.formatId)(device.productId) === deviceResponse.productId && (0, import_shared.padVersion)(device.firmwareVersion) === (0, import_shared.padVersion)(deviceResponse.firmwareVersion)); if (originalDevice) { const updates = deviceResponse.updates.map((update) => ({ device: originalDevice, ...update, channel: update.channel ?? "stable" })); deviceFirmwareCache.set(originalDevice, { updates, staleDate: expiry }); } } } const ret = new import_shared.ObjectKeyMap(); for (const deviceId of uniqueDeviceIds) { const updates = deviceFirmwareCache.get(deviceId)?.updates; if (updates) { ret.set(deviceId, updates); } } if (!cleanCacheTimeout) { cleanCacheTimeout = (0, import_shared.setTimer)(cleanCache, CLEAN_CACHE_INTERVAL_MS).unref(); } return ret; } __name(getAvailableFirmwareUpdatesBulk, "getAvailableFirmwareUpdatesBulk"); async function getAvailableFirmwareUpdates(deviceId, options) { const bulkResult = await getAvailableFirmwareUpdatesBulk([deviceId], options); return bulkResult.get(deviceId) || []; } __name(getAvailableFirmwareUpdates, "getAvailableFirmwareUpdates"); async function downloadFirmwareUpdate(file) { const [hashAlgorithm, expectedHash] = file.integrity.split(":", 2); if (hashAlgorithm !== "sha256") { throw new import_core.ZWaveError(`Unsupported hash algorithm ${hashAlgorithm} for integrity check`, import_core.ZWaveErrorCodes.Argument_Invalid); } const { default: ky } = await import("ky"); const downloadResponse = await ky.get(file.url, { timeout: DOWNLOAD_TIMEOUT // TODO: figure out how to do maxContentLength: MAX_FIRMWARE_SIZE, }); const rawData = new Uint8Array(await downloadResponse.arrayBuffer()); const requestedPathname = new URL(file.url).pathname; let actualPathname; try { actualPathname = new URL(downloadResponse.url).pathname; } catch { } let filename; const contentDisposition = downloadResponse.headers.get("content-disposition"); if (contentDisposition?.startsWith("attachment; filename=")) { filename = contentDisposition.split("filename=")[1].replace(/^"/, "").replace(/[";]$/, ""); } else if (actualPathname && hasExtension(actualPathname)) { filename = actualPathname; } else { filename = requestedPathname; } const format = (0, import_core.guessFirmwareFileFormat)(filename, rawData); const firmware = await (0, import_core.extractFirmware)(rawData, format); const actualHash = import_shared.Bytes.view(await (0, import_core.digest)("sha-256", firmware.data)).toString("hex"); if (actualHash !== expectedHash) { throw new import_core.ZWaveError(`Integrity check failed. Expected hash ${expectedHash}, got ${actualHash}`, import_core.ZWaveErrorCodes.FWUpdateService_IntegrityCheckFailed); } return { data: firmware.data, // Don't trust the guessed firmware target, use the one from the provided info firmwareTarget: file.target }; } __name(downloadFirmwareUpdate, "downloadFirmwareUpdate"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { downloadFirmwareUpdate, getAvailableFirmwareUpdates, getAvailableFirmwareUpdatesBulk }); //# sourceMappingURL=FirmwareUpdateService.js.map