homebridge-kasa-python
Version:
Plugin that uses Python-Kasa API to communicate with Kasa Devices.
218 lines • 7.97 kB
JavaScript
import axios from 'axios';
import fs from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
export function deferAndCombine(fn, timeout, runNowFn) {
let requests = [];
let timer = null;
const processRequests = () => {
const currentRequests = requests;
requests = [];
let result;
if (fn.length === 0) {
result = fn();
}
else {
result = fn(currentRequests.length);
}
result
.then(value => currentRequests.forEach(req => req.resolve(value)))
.catch(error => currentRequests.forEach(req => req.reject(error)))
.finally(() => {
timer = null;
});
};
return (arg) => {
if (runNowFn && arg !== undefined) {
runNowFn(arg);
}
return new Promise((resolve, reject) => {
requests.push({ resolve, reject });
if (!timer) {
timer = setTimeout(processRequests, timeout);
}
});
};
}
export function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export function isObjectLike(candidate) {
return typeof candidate === 'object' && candidate !== null || typeof candidate === 'function';
}
export async function loadPackageConfig(logger) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageConfigPath = path.join(__dirname, '..', 'package.json');
const log = prefixLogger(logger, '[Package Config]');
log.debug('Loading package configuration from:', packageConfigPath);
try {
const packageConfigData = await fs.readFile(packageConfigPath, 'utf8');
return JSON.parse(packageConfigData);
}
catch (error) {
log.error(`Error reading package.json: ${error}`);
throw error;
}
}
export function lookup(object, compareFn, value) {
const compare = compareFn ?? ((objectProp, search) => objectProp === search);
if (isObjectLike(object)) {
return Object.keys(object).find(key => compare(object[key], value));
}
return undefined;
}
export function lookupCharacteristicNameByUUID(characteristic, uuid) {
return Object.keys(characteristic).find(key => (characteristic[key].UUID === uuid));
}
export function prefixLogger(logger, prefix) {
const methods = ['info', 'warn', 'error', 'debug', 'log'];
const clonedLogger = methods.reduce((acc, method) => {
acc[method] = (...args) => {
const prefixString = typeof prefix === 'function' ? prefix() : prefix;
if (method === 'log') {
const [level, message, ...parameters] = args;
logger[method](level, `${prefixString} ${message}`, ...parameters);
}
else {
const [message, ...parameters] = args;
logger[method](`${prefixString} ${message}`, ...parameters);
}
};
return acc;
}, {});
clonedLogger.prefix = typeof logger.prefix === 'string' ? `${prefix} ${logger.prefix}` : prefix;
return clonedLogger;
}
export async function runCommand(logger, command, args = [], options, hideStdout = false, hideStderr = false, returnProcess = false, suppressErrors = []) {
const MAX_BUFFER_SIZE = 1024 * 1024;
let stdout = '';
let stderr = '';
let outputFile = null;
const filteredArgs = args.filter(arg => {
if (arg.startsWith('>')) {
outputFile = arg.substring(1).trim();
return false;
}
return true;
});
logger.debug(`Running command: ${command} ${filteredArgs.join(' ')}`);
const env = {
...process.env,
...(options?.env || {}),
};
const p = spawn(command, filteredArgs, {
...options,
env,
});
logger.debug(`Command PID: ${p.pid}`);
p.stdout.setEncoding('utf8').on('data', data => {
stdout += data;
if (stdout.length > MAX_BUFFER_SIZE) {
stdout = stdout.slice(-MAX_BUFFER_SIZE);
}
if (!hideStdout) {
logger.debug(`STDOUT: ${data.trim()}`);
}
});
p.stderr.setEncoding('utf8').on('data', data => {
stderr += data;
if (stderr.length > MAX_BUFFER_SIZE) {
stderr = stderr.slice(-MAX_BUFFER_SIZE);
}
if (!hideStderr) {
logger.error(`STDERR: ${data.trim()}`);
}
});
if (returnProcess) {
logger.debug('Command started and returning process.');
const stderrReady = new Promise((resolve) => {
p.stderr.once('data', () => {
logger.debug('Process data received.');
resolve();
});
});
await stderrReady;
return [stdout, stderr, null, p];
}
const exitCode = await new Promise((resolve, reject) => {
p.on('close', (code) => {
logger.debug(`Command closed with exit code: ${code}`);
resolve(code);
}).on('error', (error) => {
const errorMessage = error.message.toLowerCase();
const shouldSuppress = suppressErrors.some(err => (error.code && error.code.toString().toLowerCase() === err.toLowerCase()) ||
errorMessage.includes(err.toLowerCase()));
if (!shouldSuppress) {
logger.error('Command encountered an error:', error);
}
reject(error);
});
});
p.stdout.destroy();
p.stderr.destroy();
p.kill();
if (outputFile) {
logger.debug(`Writing command output to file: ${outputFile}`);
await fs.writeFile(outputFile, stdout);
}
logger.debug('Command finished.');
return [stdout, stderr, exitCode];
}
export function satisfiesVersion(currentVersion, requiredVersion) {
const parseVersionParts = (version) => {
const [major = '0', minor = '0', patch = '0'] = version.replace('^', '').replace('v', '').split('.');
return [Number(major) || 0, Number(minor) || 0, Number(patch) || 0];
};
const versions = requiredVersion.split('||').map(v => v.trim());
const [currentMajor, currentMinor, currentPatch] = parseVersionParts(currentVersion);
return versions.some(version => {
const [requiredMajor, requiredMinor, requiredPatch] = parseVersionParts(version);
if (currentMajor > requiredMajor) {
return true;
}
if (currentMajor < requiredMajor) {
return false;
}
if (currentMinor > requiredMinor) {
return true;
}
if (currentMinor < requiredMinor) {
return false;
}
return currentPatch >= requiredPatch;
});
}
export async function waitForServer(url, log, timeout = 30000, interval = 1000) {
const startTime = Date.now();
log.debug(`Waiting for server at ${url} with timeout ${timeout}ms and interval ${interval}ms`);
while (Date.now() - startTime < timeout) {
try {
const response = await axios.get(url);
if (response.status === 200) {
log.debug('Server responded successfully');
return;
}
}
catch {
log.debug('Server not responding yet, retrying...');
}
await new Promise(resolve => setTimeout(resolve, interval));
}
log.error(`Server did not respond within ${timeout / 1000} seconds`);
throw new Error(`Server did not respond within ${timeout / 1000} seconds`);
}
export async function getAvailablePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on('error', reject);
});
}
//# sourceMappingURL=utils.js.map