UNPKG

matterbridge-aeg-robot

Version:

A Matterbridge plugin that connects AEG RX 9 / Electrolux Pure i9 robot vacuums to the Matter smart home ecosystem.

245 lines 9.74 kB
// Matterbridge plugin for AEG RX9 / Electrolux Pure i9 robot vacuum // Copyright © 2025 Alexander Thoukydides import { AEGApplianceRX9Log } from './aeg-appliance-log-rx9.js'; import { formatList, logError, MS } from './utils.js'; import { RX9BatteryStatus, RX9Dustbin, RX9RobotStatus } from './aegapi-rx9-types.js'; import EventEmitter from 'events'; import { PeriodicOp } from './periodic-op.js'; import { AEGApplianceRX9CtrlActivity } from './aeg-appliance-rx9-ctrl-activity.js'; ; // Timings for polling the status const POLL_INTERVAL_RAPID = 1 * MS; const POLL_TIMEOUT_MULTIPLE = 2; const POLL_TIMEOUT_OFFSET = 10 * MS; // An AEG RX 9 / Electrolux Pure i9 robot manager export class AEGApplianceRX9 extends EventEmitter { log; config; api; // Control the robot setActivity; // Static information // ... from getAppliances applianceId; applianceName; applianceType; created; // ... from getApplianceInfo brand; colour; deviceType; model; pnc; serialNumber; variant; // ... from getApplianceState capabilities; platform; // Dynamic information about the robot state = { connected: false, enabled: false, batteryStatus: RX9BatteryStatus.Dead, dustbinStatus: RX9Dustbin.Unknown, firmwareVersion: '', robotStatus: RX9RobotStatus.Error, fauxStatus: RX9RobotStatus.Error, messages: [], isDocked: false, isCharging: false }; emittedState = {}; // Messages about the robot emittedMessages = new Set(); // Periodic API polling poll; // Create a new robot manager constructor(log, config, api, appliance, info, state) { super({ captureRejections: true }); this.log = log; this.config = config; this.api = api; super.on('error', err => { logError(this.log, 'Event', err); }); // Initialise static information about the robot // ... from getAppliances this.applianceId = appliance.applianceId; this.applianceName = appliance.applianceName; this.applianceType = appliance.applianceType; this.created = appliance.created; // ... from getApplianceInfo const { applianceInfo } = info; this.brand = applianceInfo.brand; this.colour = applianceInfo.colour; this.deviceType = applianceInfo.deviceType; this.model = applianceInfo.model; this.pnc = applianceInfo.pnc; this.serialNumber = applianceInfo.serialNumber; this.variant = applianceInfo.variant; // ... from getApplianceState const { reported } = state.properties; this.platform = reported.platform; this.capabilities = Object.keys(reported.capabilities); // Initialise dynamic information this.updateFromApplianceState(state); // Allow the robot to be controlled this.setActivity = new AEGApplianceRX9CtrlActivity(this).makeSetter(); // Start logging information about this robot new AEGApplianceRX9Log(this); } // Start polling the appliance async start() { if (this.poll) return; // Ensure that events are generated for the initial state this.emittedState = {}; this.emittedMessages.clear(); // Start polling the status this.poll = new PeriodicOp(this.log, { name: 'Appliance state', interval: this.config.pollIntervalSeconds * MS, intervalRapid: POLL_INTERVAL_RAPID, timeout: this.config.pollIntervalSeconds * MS * POLL_TIMEOUT_MULTIPLE + POLL_TIMEOUT_OFFSET, onOp: this.pollApplianceState.bind(this), onError: this.pollError.bind(this) }); return Promise.resolve(); } // Stop polling the appliance async stop() { await this.poll?.stop(); this.poll = undefined; } // Periodically poll the appliance state async pollApplianceState() { const state = await this.api.getApplianceState(); this.updateFromApplianceState(state); this.updateDerivedAndEmit(); } // Handle a status update for a periodic action pollError(err) { this.state.apiError = err; this.updateDerivedAndEmit(); } // Describe this robot toString() { const { brand, model, applianceName, applianceId } = this; return `${brand} ${model} '${applianceName}' (Product ID ${applianceId})`; } // Update dynamic robot state updateFromApplianceState(state) { // Extract the relevant information const { reported } = state.properties; this.updateStatus({ connected: state.connectionState === 'Connected', enabled: state.status === 'enabled', batteryStatus: reported.batteryStatus, dustbinStatus: reported.dustbinStatus, firmwareVersion: reported.firmwareVersion, robotStatus: reported.robotStatus, messages: reported.messageList.messages, ecoMode: 'ecoMode' in reported ? reported.ecoMode : undefined, powerMode: 'powerMode' in reported ? reported.powerMode : undefined }); } // Updated derived values and emit changes updateDerivedAndEmit() { this.updateDerived(); this.emit('preEmitPatch', this.state); this.emitMessages(); this.emitChangeEvents(); } // Update derived values updateDerived() { const { robotStatus, batteryStatus, isDocked: wasDocked } = this.state; const activityMap = { // Docked Charging [RX9RobotStatus.Cleaning]: [false, false], [RX9RobotStatus.PausedCleaning]: [false, false], [RX9RobotStatus.SpotCleaning]: [false, false], [RX9RobotStatus.PausedSpotCleaning]: [false, false], [RX9RobotStatus.Return]: [false, false], [RX9RobotStatus.PausedReturn]: [false, false], [RX9RobotStatus.ReturnForPitstop]: [false, false], [RX9RobotStatus.PausedReturnForPitstop]: [false, false], [RX9RobotStatus.Charging]: [true, true], [RX9RobotStatus.Sleeping]: [null, false], [RX9RobotStatus.Error]: [null, false], [RX9RobotStatus.Pitstop]: [true, true], [RX9RobotStatus.ManualSteering]: [false, false], [RX9RobotStatus.FirmwareUpgrade]: [null, false] }; const [isDockedStatus, isCharging] = activityMap[robotStatus]; // Use heuristics to determine whether docked state when unspecified const isDockedInferred = batteryStatus === RX9BatteryStatus.FullyCharged ? true : batteryStatus === RX9BatteryStatus.High ? wasDocked : false; const isDocked = isDockedStatus ?? isDockedInferred; // Update the status this.updateStatus({ isDocked, isCharging, fauxStatus: robotStatus }); } // Apply a partial update to the robot status updateStatus(update) { Object.assign(this.state, update); } // Apply updates to the robot status and emit events for changes emitChangeEvents() { // Identify the values that have changed const keys = Object.keys(this.state); const changed = keys.filter(key => { const a = this.state[key], b = this.emittedState[key]; if (Array.isArray(a) && Array.isArray(b)) { return a.length !== b.length || a.some((element, index) => element !== b[index]); } else { return a !== b; } }); if (!changed.length) return; // Log a summary of the changes const toText = (value) => { switch (typeof (value)) { case 'undefined': return '?'; case 'string': return /[- <>:,]/.test(value) ? `"${value}"` : value; case 'number': return value.toString(); case 'boolean': return value.toString(); default: return JSON.stringify(value); } }; const summary = changed.map(key => `${key}: ${toText(this.emittedState[key])}->${toText(this.state[key])}`); this.log.debug(formatList(summary)); // Emit an event listing all of the keys that have changed this.emit('changed', keys, this.state); // Emit events for each change changed.forEach(key => this.emit(key, this.state[key], this.emittedState[key])); // Store a copy of the updated values this.emittedState = { ...this.state }; } // Emit events for any new messages emitMessages() { const { messages } = this.state; // If there are no current messages then just flush the cache if (!messages.length) { this.emittedMessages.clear(); return; } // Emit events for any new messages messages.forEach(message => { const { id } = message; if (!(this.emittedMessages.has(id))) { this.emittedMessages.add(id); this.emit('message', message); } }); } // Create a new AEG RX 9 / Electrolux Pure i9 robot manager static async create(log, config, api, appliance) { // Read the full appliance details const info = await api.getApplianceInfo(); const state = await api.getApplianceState(); // Create the robot manager return new AEGApplianceRX9(log, config, api, appliance, info, state); } } //# sourceMappingURL=aeg-appliance-rx9.js.map