UNPKG

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.

211 lines 9.09 kB
// Matterbridge plugin for Dyson robot vacuum and air treatment devices // Copyright © 2025-2026 Alexander Thoukydides import { Behavior } from 'matterbridge/matter'; import { ClusterModel, FieldElement } from 'matterbridge/matter'; import { RvcOperationalState } from 'matterbridge/matter/clusters'; import { RvcCleanModeBehavior, RvcOperationalStateBehavior, RvcRunModeBehavior, ServiceAreaBehavior } from 'matterbridge/matter/behaviors'; import { ChangeToModeError, RvcOperationalStateError, SelectAreaError } from './error-360.js'; import { assertIsDefined, assertIsInstanceOf, formatList } from './utils.js'; import { logError } from './log-error.js'; import { isDeepStrictEqual } from 'util'; // Robot Vacuum Cleaner Run Mode cluster modes export var RvcRunMode360; (function (RvcRunMode360) { RvcRunMode360[RvcRunMode360["Idle"] = 0] = "Idle"; RvcRunMode360[RvcRunMode360["Cleaning"] = 1] = "Cleaning"; RvcRunMode360[RvcRunMode360["Mapping"] = 2] = "Mapping"; })(RvcRunMode360 || (RvcRunMode360 = {})); // Robot Vacuum Cleaner Clean Mode cluster modes export var RvcCleanMode360; (function (RvcCleanMode360) { RvcCleanMode360[RvcCleanMode360["Quiet"] = 0] = "Quiet"; RvcCleanMode360[RvcCleanMode360["Quick"] = 1] = "Quick"; RvcCleanMode360[RvcCleanMode360["High"] = 2] = "High"; RvcCleanMode360[RvcCleanMode360["MaxBoost"] = 3] = "MaxBoost"; RvcCleanMode360[RvcCleanMode360["Auto"] = 4] = "Auto"; // Vis Nav: Auto })(RvcCleanMode360 || (RvcCleanMode360 = {})); // OperationalStatus manufacturer error export const VENDOR_ERROR_360 = 0x80; // Command handling behaviour for the endpoint export class BehaviorDevice360 { log; // Registered command handlers commands = {}; // Construct new command handling behaviour constructor(log) { this.log = log; } // Set a command handler setCommandHandler(command, handler) { if (this.commands[command]) throw new Error(`Handler already registered for command ${command}`); this.commands[command] = handler; } // Execute a command handler async executeCommand(command, ...args) { const handler = this.commands[command]; if (!handler) throw new Error(`${command} not implemented`); await handler(...args); } } export class Behavior360 extends Behavior { static id = 'dyson-rvc'; } // eslint-disable-next-line @typescript-eslint/no-namespace (function (Behavior360) { class State { device; } Behavior360.State = State; })(Behavior360 || (Behavior360 = {})); // Implement command handlers for the RVC Run Mode cluster export class RvcRunModeServer360 extends RvcRunModeBehavior { // ChangeToMode command handler async changeToMode({ newMode }) { const { device } = this.agent.get(Behavior360).state; const { log } = device; try { // Check whether it is a valid request log.debug(`RVC Run Mode command: ChangeToMode ${newMode}...`); const supported = this.state.supportedModes.some(({ mode }) => mode === newMode); if (!supported) throw new ChangeToModeError.UnsupportedMode; // Attempt to change the mode await device.executeCommand('ChangeRunMode', newMode); // Success log.debug(`RVC Run Mode command: ChangeToMode ${newMode} - OK`); return ChangeToModeError.toResponse(); } catch (err) { logError(log, 'RVC Run Mode ChangeToMode', err); return ChangeToModeError.toResponse(err); } } } // Implement command handlers for the RVC Clean Mode cluster export class RvcCleanModeServer360 extends RvcCleanModeBehavior { // ChangeToMode command handler async changeToMode({ newMode }) { const { device } = this.agent.get(Behavior360).state; const { log } = device; try { // Check whether it is a valid request log.debug(`RVC Clean Mode command: ChangeToMode ${newMode}...`); const supported = this.state.supportedModes.some(({ mode }) => mode === newMode); if (!supported) throw new ChangeToModeError.UnsupportedMode; // Attempt to change the mode await device.executeCommand('ChangeCleanMode', newMode); // Success log.debug(`RVC Clean Mode command: ChangeToMode ${newMode} - OK`); return ChangeToModeError.toResponse(); } catch (err) { logError(log, 'RVC Clean Mode ChangeToMode', err); return ChangeToModeError.toResponse(err); } } } // Implement command handlers for the RVC Operational State cluster export class RvcOperationalStateServer360 extends RvcOperationalStateBehavior { static { const schema = RvcOperationalStateServer360.schema; assertIsInstanceOf(schema, ClusterModel); // Add a manufacturer-specific ErrorState value extendEnum(schema, 'ErrorStateEnum', [ FieldElement({ name: 'OtherError', id: VENDOR_ERROR_360, conformance: 'O', description: 'The device has an error that is not covered by the Matter-defined error states' }) ]); } // Common command handler async command(command, defaultErrorId) { const { device } = this.agent.get(Behavior360).state; const { log } = device; try { log.debug(`RVC Operational State command: ${command}...`); await device.executeCommand(command); log.debug(`RVC Operational State command: ${command} - OK`); return RvcOperationalStateError.toResponse(); } catch (err) { logError(log, `RVC Operational State ${command}`, err); return RvcOperationalStateError.toResponse(err, defaultErrorId); } } // Pause command handler pause() { return this.command('Pause', RvcOperationalState.ErrorState.CommandInvalidInState); } // Resume command handler resume() { return this.command('Resume', RvcOperationalState.ErrorState.UnableToStartOrResume); } // GoHome command handler goHome() { return this.command('GoHome', RvcOperationalState.ErrorState.CommandInvalidInState); } } // Implement command handlers for the Service Areas cluster export class ServiceAreaServer360 extends ServiceAreaBehavior { // SelectAreas command handler async selectAreas({ newAreas }) { const { device } = this.agent.get(Behavior360).state; const { log } = device; try { // Remove any duplicated areas from the list log.info(`Service Area command: SelectAreas ${formatList(newAreas.map(id => String(id)))}`); newAreas = [...new Set(newAreas)]; // Check whether it is a valid request const maps = new Set(); for (const area of newAreas) { const supportedArea = this.state.supportedAreas.find(({ areaId }) => areaId === area); if (!supportedArea) throw new SelectAreaError.UnsupportedArea(`${area} is not a supported area`); maps.add(supportedArea.mapId); } // If all areas are specified then treat it as an empty list if (newAreas.length === this.state.supportedAreas.length) newAreas = []; else if (maps.size !== 1) throw new SelectAreaError.InvalidSet('Areas must all be from the same map'); // Attempt to select the areas await device.executeCommand('SelectAreas', newAreas); this.state.selectedAreas = newAreas; // Success return SelectAreaError.toResponse(); } catch (err) { if (isDeepStrictEqual(new Set(this.state.selectedAreas), new Set(newAreas))) { // Matter requires Success status if the areas are unchanged logError(log, 'Service Area SelectAreas (error ignored)', err); return SelectAreaError.toResponse(); } else { logError(log, 'Service Area SelectAreas', err); return SelectAreaError.toResponse(err); } } } } // Extend a Matter.js schema enum with new values function extendEnum(schema, name, values) { const element = schema.datatypes.find(e => e.name === name); assertIsDefined(element); for (const value of values) { // Re-use any existing definition of the same value if (element.children.some(e => e.id === value.id)) continue; // Ensure new values have unique names let name = value.name; let suffix = 0; while (element.children.some(e => e.name === name)) name += `_${++suffix}`; element.children = [...element.children, { ...value, name }]; } } //# sourceMappingURL=endpoint-360-behavior.js.map