ember-zli
Version:
Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver
174 lines (173 loc) • 8.12 kB
JavaScript
import { existsSync, readdirSync, readFileSync, renameSync, statSync } from "node:fs";
import { dirname, extname, join } from "node:path";
import { input, select } from "@inquirer/prompts";
import { DEFAULT_STACK_CONFIG } from "zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js";
import { IEEE802154CcaMode } from "zigbee-herdsman/dist/adapter/ember/enums.js";
import { halCommonCrc16, highByte, lowByte } from "zigbee-herdsman/dist/adapter/ember/utils/math.js";
import { fromUnifiedBackup } from "zigbee-herdsman/dist/utils/backup.js";
import { CONF_STACK, DATA_FOLDER, logger } from "../index.js";
// @from zigbee2mqtt-frontend
export const toHex = (input, padding = 4) => {
const padStr = "0".repeat(padding);
return `0x${(padStr + input.toString(16)).slice(-1 * padding).toUpperCase()}`;
};
export const loadStackConfig = () => {
try {
const customConfig = JSON.parse(readFileSync(CONF_STACK, "utf8"));
// set any undefined config to default
const config = { ...DEFAULT_STACK_CONFIG, ...customConfig };
const inRange = (value, min, max) => !(value == null || value < min || value > max);
if (!["high", "low"].includes(config.CONCENTRATOR_RAM_TYPE)) {
config.CONCENTRATOR_RAM_TYPE = DEFAULT_STACK_CONFIG.CONCENTRATOR_RAM_TYPE;
logger.error("[CONF STACK] Invalid CONCENTRATOR_RAM_TYPE, using default.");
}
if (!inRange(config.CONCENTRATOR_MIN_TIME, 1, 60) || config.CONCENTRATOR_MIN_TIME >= config.CONCENTRATOR_MAX_TIME) {
config.CONCENTRATOR_MIN_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MIN_TIME;
logger.error("[CONF STACK] Invalid CONCENTRATOR_MIN_TIME, using default.");
}
if (!inRange(config.CONCENTRATOR_MAX_TIME, 30, 300) || config.CONCENTRATOR_MAX_TIME <= config.CONCENTRATOR_MIN_TIME) {
config.CONCENTRATOR_MAX_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_TIME;
logger.error("[CONF STACK] Invalid CONCENTRATOR_MAX_TIME, using default.");
}
if (!inRange(config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, 1, 100)) {
config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_ROUTE_ERROR_THRESHOLD;
logger.error("[CONF STACK] Invalid CONCENTRATOR_ROUTE_ERROR_THRESHOLD, using default.");
}
if (!inRange(config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, 1, 100)) {
config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD;
logger.error("[CONF STACK] Invalid CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, using default.");
}
if (!inRange(config.CONCENTRATOR_MAX_HOPS, 0, 30)) {
config.CONCENTRATOR_MAX_HOPS = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_HOPS;
logger.error("[CONF STACK] Invalid CONCENTRATOR_MAX_HOPS, using default.");
}
if (!inRange(config.MAX_END_DEVICE_CHILDREN, 6, 64)) {
config.MAX_END_DEVICE_CHILDREN = DEFAULT_STACK_CONFIG.MAX_END_DEVICE_CHILDREN;
logger.error("[CONF STACK] Invalid MAX_END_DEVICE_CHILDREN, using default.");
}
if (!inRange(config.TRANSIENT_DEVICE_TIMEOUT, 0, 65535)) {
config.TRANSIENT_DEVICE_TIMEOUT = DEFAULT_STACK_CONFIG.TRANSIENT_DEVICE_TIMEOUT;
logger.error("[CONF STACK] Invalid TRANSIENT_DEVICE_TIMEOUT, using default.");
}
if (!inRange(config.END_DEVICE_POLL_TIMEOUT, 0, 14)) {
config.END_DEVICE_POLL_TIMEOUT = DEFAULT_STACK_CONFIG.END_DEVICE_POLL_TIMEOUT;
logger.error("[CONF STACK] Invalid END_DEVICE_POLL_TIMEOUT, using default.");
}
if (!inRange(config.TRANSIENT_KEY_TIMEOUT_S, 0, 65535)) {
config.TRANSIENT_KEY_TIMEOUT_S = DEFAULT_STACK_CONFIG.TRANSIENT_KEY_TIMEOUT_S;
logger.error("[CONF STACK] Invalid TRANSIENT_KEY_TIMEOUT_S, using default.");
}
config.CCA_MODE = config.CCA_MODE ?? undefined; // always default to undefined
if (config.CCA_MODE && IEEE802154CcaMode[config.CCA_MODE] === undefined) {
config.CCA_MODE = undefined;
logger.error("[STACK CONFIG] Invalid CCA_MODE, ignoring.");
}
logger.info(`Using stack config ${JSON.stringify(config)}.`);
return config;
}
catch {
/* empty */
}
logger.info("Using default stack config.");
return DEFAULT_STACK_CONFIG;
};
export const browseToFile = async (message, defaultValue, toWrite = false) => {
const pathOpt = await select({
choices: [
{ name: `Use default (${defaultValue})`, value: 0 },
{ name: "Enter path manually", value: 1 },
{ name: `Select in data folder (${DATA_FOLDER})`, value: 2 },
],
message,
});
let filepath = defaultValue;
switch (pathOpt) {
case 1: {
filepath = await input({
message: "Enter path to file",
validate(value) {
return existsSync(dirname(value)) && extname(value) === extname(defaultValue);
},
});
break;
}
case 2: {
const files = readdirSync(DATA_FOLDER);
const fileChoices = [{ name: "Go back", value: "-1" }];
for (const file of files) {
if (extname(file) === extname(defaultValue)) {
const { size, mtime, birthtime } = statSync(join(DATA_FOLDER, file));
fileChoices.push({
name: file,
value: file,
description: `Size: ${size} bytes | Created: ${birthtime.toISOString()} | Last Modified: ${mtime.toISOString()}`,
});
}
}
let chosenFile = "-1";
if (fileChoices.length === 1) {
logger.error(`Found no file in '${DATA_FOLDER}'.`);
}
else {
chosenFile = await select({ choices: fileChoices, message });
}
filepath = chosenFile === "-1" ? await browseToFile(message, defaultValue, toWrite) : join(DATA_FOLDER, chosenFile);
break;
}
}
if (toWrite && existsSync(filepath)) {
const rename = await select({
choices: [
{ name: "Overwrite", value: 0 },
{ name: "Rename", value: 1 },
],
message: "File already exists",
});
if (rename === 1) {
const renamed = `${filepath}-${Date.now()}.old`;
logger.info(`Renaming existing file to '${renamed}'.`);
renameSync(filepath, renamed);
}
}
return filepath;
};
export const getBackupFromFile = (backupFile) => {
try {
const data = JSON.parse(readFileSync(backupFile, "utf8"));
if (data.metadata.format === "zigpy/open-coordinator-backup") {
if (data.metadata.version !== 1) {
logger.error(`Unsupported open coordinator backup version (version=${data.metadata.version}).`);
return undefined;
}
return fromUnifiedBackup(data);
}
logger.error("Unknown backup format.");
}
catch (error) {
logger.error(`Not valid backup found. ${error}`);
}
return undefined;
};
export const computeCRC16 = (data, init = 0) => {
let crc = init;
for (const byte of data) {
crc = halCommonCrc16(byte, crc);
}
return Buffer.from([highByte(crc), lowByte(crc)]);
};
export const computeCRC16CITTKermit = (data, init = 0) => {
let crc = init;
for (const byte of data) {
let t = crc ^ byte;
t = (t ^ (t << 4)) & 0xff;
crc = (crc >> 8) ^ (t << 8) ^ (t >> 4) ^ (t << 3);
}
return Buffer.from([lowByte(crc), highByte(crc)]);
};
export async function fetchJson(pageUrl) {
const response = await fetch(pageUrl);
if (!response.ok || !response.body) {
throw new Error(`Invalid response from ${pageUrl} status=${response.status}.`);
}
return (await response.json());
}