matterbridge-dyson-robot
Version:
A Matterbridge plugin that connects Dyson robot vacuums and air treatment devices to the Matter smart home ecosystem via their local or cloud MQTT APIs.
150 lines • 5.67 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025 Alexander Thoukydides
import assert from 'assert';
// Milliseconds in a second
export const MS = 1000;
// Type assertions
export function assertIsDefined(value) {
assert.notStrictEqual(value, undefined);
assert.notStrictEqual(value, null);
}
export function assertIsNotUndefined(value) {
assert.notStrictEqual(value, undefined);
}
export function assertIsBoolean(value) {
assert.strictEqual(typeof value, 'boolean');
}
export function assertIsNumber(value) {
assert.strictEqual(typeof value, 'number');
}
export function assertIsInstanceOf(value, type) {
assert(value instanceof type, `Not an instance of ${type.name}`);
}
// Log an error
export function logError(log, when, err) {
try {
// Log the error message itself
log.error(`[${when}] ${String(err)}`);
// Log any stack backtrace
if (err instanceof Error && err.stack)
log.debug(err.stack);
}
catch { /* empty */ }
}
// Format a milliseconds duration
export function formatMilliseconds(ms, maxParts = 2) {
if (ms < 1)
return 'n/a';
// Split the duration into components
const duration = [
['day', Math.floor(ms / (24 * 60 * 60 * MS))],
['hour', Math.floor(ms / (60 * 60 * MS)) % 24],
['minute', Math.floor(ms / (60 * MS)) % 60],
['second', Math.floor(ms / MS) % 60],
['millisecond', Math.floor(ms) % MS]
];
// Remove any leading zero components
while (duration[0]?.[1] === 0)
duration.shift();
// Combine the required number of remaining components
return duration.slice(0, maxParts)
.filter(([_key, value]) => value !== 0)
.map(([key, value]) => plural(value, key))
.join(' ');
}
// Format a seconds duration
export function formatSeconds(seconds, maxParts = 2) {
return formatMilliseconds(seconds * 1000, maxParts);
}
// Format a date as YYYYMMDD or YYYY-MM-DD
export function formatDateISO8601(date, separator) {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
return [year, month, day].join(separator);
}
// Format a list (with Oxford comma)
export function formatList(items) {
switch (items.length) {
case 0: return 'n/a';
case 1: return items[0] ?? '';
case 2: return `${items[0]} and ${items[1]}`;
default: return [...items.slice(0, -1), `and ${items[items.length - 1]}`].join(', ');
}
}
// Format a counted noun (handling most regular cases automatically)
export function plural(count, noun, showCount = true) {
const [singular, plural] = Array.isArray(noun) ? noun : [noun, ''];
noun = count === 1 ? singular : plural;
if (!noun) {
// Apply regular rules
const rules = [
['on$', 'a', 2], // phenomenon/phenomena criterion/criteria
['us$', 'i', 1], // cactus/cacti focus/foci
['[^aeiou]y$', 'ies', 1], // cty/cites puppy/puppies
['(ch|is|o|s|sh|x|z)$', 'es', 0], // iris/irises truss/trusses
['', 's', 0] // cat/cats house/houses
];
const rule = rules.find(([ending]) => new RegExp(ending, 'i').test(singular));
assertIsDefined(rule);
const matchCase = (s) => singular === singular.toUpperCase() ? s.toUpperCase() : s;
noun = singular.substring(0, singular.length - rule[2]).concat(matchCase(rule[1]));
}
return showCount ? `${count} ${noun}` : noun;
}
// Format strings in columns
export function columns(rows, separator = ' ') {
// Determine the required column widths
const width = [];
rows.forEach(row => {
row.forEach((value, index) => {
width[index] = Math.max(width[index] ?? 0, value.length);
});
});
width.splice(-1, 1, 0);
// Format the rows
return rows.map(row => row.map((value, index) => value.padEnd(width[index] ?? 0)).join(separator));
}
// Recursive object assignment, skipping undefined values
export function deepMerge(...objects) {
const isObject = (value) => value !== undefined && typeof value === 'object' && !Array.isArray(value);
return objects.reduce((acc, object) => {
Object.entries(object).forEach(([key, value]) => {
const accValue = acc[key];
if (value === undefined)
return;
if (isObject(accValue) && isObject(value))
acc[key] = deepMerge(accValue, value);
else
acc[key] = value;
});
return acc;
}, {});
}
// Convert checker validation error into lines of text
export function getValidationTree(errors) {
const lines = [];
errors.forEach((error, index) => {
const prefix = (a, b) => index < errors.length - 1 ? a : b;
lines.push(`${prefix('├─ ', '└─ ')}${error.path} ${error.message}`);
if (error.nested) {
const nested = getValidationTree(error.nested);
lines.push(...nested.map(line => `${prefix('│ ', ' ')} ${line}`));
}
});
return lines;
}
// Convert any errors in an event listener to 'error' events
export function tryListener(emitter, op, errorHandler = (err) => emitter.emit('error', err)) {
return (...args) => {
void (async () => {
try {
await op(...args);
}
catch (err) {
errorHandler(err);
}
})();
};
}
//# sourceMappingURL=utils.js.map