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