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.
123 lines • 5.73 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025 Alexander Thoukydides
import { PowerSource } from 'matterbridge/matter/clusters';
import { assert } from 'console';
import { RvcOperationalStateError } from './error-360.js';
import { DYSON_360_FAULT_CATEGORIES, DYSON_360_FAULT_CODES, DYSON_360_FAULT_STATES } from './dyson-device-360-faults-table.js';
import { assertIsDefined } from './utils.js';
// Parse a fault code into a numeric tuple
function parseFaultCode(code) {
const parts = code.split('.').map(Number);
if (parts.length !== 3 || parts.some(isNaN))
throw new Error(`Invalid fault code: ${code}`);
return parts;
}
// Parse a fault pattern into a numeric array
function parseFaultPattern(codeOrPattern) {
const parts = codeOrPattern.split('.');
const wildcardIndex = parts.indexOf('#');
if (wildcardIndex !== -1)
parts.length = wildcardIndex;
return parts.map(Number);
}
// Parse a fault pattern or range into an array of numeric ranges
function parseFaultRange(patternOrRange) {
const low = parseFaultPattern(Array.isArray(patternOrRange) ? patternOrRange[0] : patternOrRange);
const high = parseFaultPattern(Array.isArray(patternOrRange) ? patternOrRange[1] : patternOrRange);
assert(low.length === high.length);
const zipped = low.map((l, i) => [l, high[i]]);
return zipped;
}
// Test whether a fault code is in a range, returning its specificity
function isFaultInRange(patternOrRange, code) {
const range = parseFaultRange(patternOrRange);
const value = parseFaultCode(code);
assert(range.length <= value.length);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (range.some(([l, h], i) => value[i] < l || h < value[i]))
return 0;
return range.length;
}
// Find the most specific match for a fault code
function findFaultMatch(fault) {
let bestDetail;
let bestSpecificity = 0;
for (const [patternOrRange, detail] of DYSON_360_FAULT_CODES) {
const specificity = isFaultInRange(patternOrRange, fault);
if (bestSpecificity < specificity) {
bestSpecificity = specificity;
bestDetail = detail;
}
}
return bestDetail;
}
// Find the matches for a robot state in priority order
function getFaultDetails(log, state, faults) {
const detailList = [];
// Highest priority are specific matches for each active fault
for (const [category, fault] of Object.entries(faults ?? {})) {
if (fault.active) {
const detail = findFaultMatch(fault.description);
if (detail) {
log.debug(`Mapped ${category} fault ${fault.description} to ${JSON.stringify(detail)}`);
detailList.push(detail);
}
else {
log.warn(`Received unknown ${category} fault: ${fault.description}`);
}
}
}
// Next are mappings from the active fault categories
for (const [category, fault] of Object.entries(faults ?? {})) {
if (fault.active) {
const detail = DYSON_360_FAULT_CATEGORIES[category];
log.debug(`Mapped ${category} fault category to ${JSON.stringify(detail)}`);
detailList.push(detail);
}
}
// Lowest priority is any fault from the robot state
const stateDetail = DYSON_360_FAULT_STATES.get(state);
if (stateDetail) {
log.debug(`Mapped ${state} state to ${JSON.stringify(stateDetail)}`);
detailList.push(stateDetail);
}
return detailList;
}
// Map a list of fault details to the most relevant RVC Operational State error
function dyson360FaultDetailsToError(log, detailList) {
// If there is an RVC Operational State Error then use the first
const opError = detailList.find(detail => detail.opError !== undefined);
if (opError) {
log.debug(`Selected RVC Operational State Error: ${opError.opError}("${opError.msg}")`);
assertIsDefined(opError.opError);
const constructor = RvcOperationalStateError.create(opError.opError);
return new constructor(opError.msg);
}
// Otherwise create a generic Error
const detail = detailList[0];
if (detail) {
log.debug(`Selected manufacturer-specific Error: "${detail.msg}"`);
return new Error(detail.msg);
}
}
// Map Dyson robot vacuum state and active faults to cluster attributes
export function mapDyson360Faults(log, state, faults) {
// Map the state and active faults to a list of matching fault details
const detailList = getFaultDetails(log, state, faults);
// Construct the most relevant RVC Operational State error
const err = dyson360FaultDetailsToError(log, detailList);
const operationalError = RvcOperationalStateError.toStruct(err);
// Construct a list of active battery faults
const batFaults = new Set(detailList.map(detail => detail.batFault).filter(detail => detail !== undefined));
if (batFaults.has('Unspecified') && 1 < batFaults.size)
batFaults.delete('Unspecified');
const activeBatFaults = Array.from(batFaults, name => PowerSource.BatFault[name]);
// Construct a list of active battery charger faults
const chargeFaults = new Set(detailList.map(detail => detail.chargeFault).filter(detail => detail !== undefined));
if (chargeFaults.has('Unspecified') && 1 < chargeFaults.size)
chargeFaults.delete('Unspecified');
const activeBatChargeFaults = Array.from(chargeFaults, name => PowerSource.BatChargeFault[name]);
// Return the resulting attribute values
return { operationalError, activeBatFaults, activeBatChargeFaults };
}
//# sourceMappingURL=dyson-device-360-faults.js.map