UNPKG

homebridge-aeg-robot

Version:

AEG RX9 / Electrolux Pure i9 robot vacuum plugin for Homebridge

180 lines 7.36 kB
// Homebridge plugin for AEG RX 9 / Electrolux Pure i9 robot vacuum // Copyright © 2022-2023 Alexander Thoukydides import { SimpleActivity } from './aeg-robot.js'; import { MS, assertIsNotUndefined, logError } from './utils.js'; import { RX9RobotStatus } from './aegapi-rx9-types.js'; import { once } from 'node:events'; // Timeout waiting for changes, as a multiple of the status polling interval const TIMEOUT_REQUEST_POLL_MULTIPLE = 1; const TIMEOUT_APPLIED_POLL_MULTIPLE = 3; // An abstract AEG RX 9 / Electrolux Pure i9 robot controller class AEGRobotCtrl { robot; name; // Plugin configuration config; // Logger log; // Electrolux Group API for an AEG RX9.1 or RX9.2 robot vacuum cleaner api; // The target value target; // Abort waiting for a previous target to be applied abortController; // Timeout in milliseconds for requesting and waiting for changes requestTimeout; appliedTimeout; // Optional mapping of enum target values to text toText; // Create a new robot controller constructor(robot, name) { this.robot = robot; this.name = name; this.config = robot.account.config; this.log = robot.log; this.api = robot.api; const pollInterval = this.config.pollIntervals.statusSeconds * MS; this.requestTimeout = pollInterval * TIMEOUT_REQUEST_POLL_MULTIPLE; this.appliedTimeout = pollInterval * TIMEOUT_APPLIED_POLL_MULTIPLE; robot.on('preUpdate', () => { if (this.target !== undefined) this.overrideStatus(this.target); }); } // Return a set method bound to this instance makeSetter() { return (target) => void (async () => { await this.set(target); })(); } // Request a change to the robot async set(target) { // No new action required if already setting the requested state const description = this.description(target); if (target === this.target) { this.log.debug(`Ignoring duplicate request to ${description}`); return; } // No action required if already in the required state if (this.isTargetSet(target)) { this.log.debug(`Ignoring unnecessary request to ${description}`); return; } // Temporarily override the reported status this.target = target; this.robot.updateDerivedAndEmit(); // Replace any previous unfinished request if (this.abortController) { this.abortController.abort(); this.log.debug(`Changing pending request to ${description}`); return; } // Start a new request this.log.debug(`New request to ${description}`); try { do { // Create AbortController to abandon waiting for status update this.abortController = new AbortController(); // Attempt to apply the requested change target = this.target; await this.trySet(target, this.abortController.signal); } while (target !== this.target); } catch (err) { // Failed to apply the update logError(this.log, `Setting ${this.name}`, err); } finally { // Clear the status override delete this.abortController; delete this.target; this.robot.updateDerivedAndEmit(); } } // Attempt to apply a single change async trySet(target, signal) { const description = this.description(target); this.log.info(`Attempting to ${description}`); let result = 'Failed to'; try { // Apply the change const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(this.requestTimeout)]); await this.setTarget(target, requestSignal); // Wait for status update, change of target state, or timeout const appliedSignal = AbortSignal.any([signal, AbortSignal.timeout(this.appliedTimeout)]); do { await once(this.robot, 'appliance', { signal: appliedSignal }); } while (!this.isTargetSet(target)); result = 'Successfully'; } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') result = 'Aborted'; else if (err instanceof DOMException && err.name === 'TimeoutError') result = 'Timed out'; else throw err; } finally { // Log the result this.log.info(`${result} ${description}`); } } // Describe setting the target value description(target) { const value = this.toText ? this.toText[target] : `"${target}"`; return `set ${this.name} to ${value}`; } } // Robot controller for changing the activity export class AEGRobotCtrlActivity extends AEGRobotCtrl { robot; // Create a new robot controller for changing the name constructor(robot) { super(robot, 'activity'); this.robot = robot; } // Check whether the robot is performing the required activity isTargetSet(command) { if (this.robot.status.activity === undefined) return null; const commandIndex = ['play', 'pause', 'home', 'stop']; const commandSet = { [RX9RobotStatus.Cleaning]: [true, false, false, false], [RX9RobotStatus.PausedCleaning]: [false, true, false, false], [RX9RobotStatus.SpotCleaning]: [true, false, false, false], [RX9RobotStatus.PausedSpotCleaning]: [false, true, false, false], [RX9RobotStatus.Return]: [true, false, true, false], [RX9RobotStatus.PausedReturn]: [false, true, false, false], [RX9RobotStatus.ReturnForPitstop]: [true, false, true, false], [RX9RobotStatus.PausedReturnForPitstop]: [false, true, false, false], [RX9RobotStatus.Charging]: [false, true, true, true], [RX9RobotStatus.Sleeping]: [false, true, null, true], [RX9RobotStatus.Error]: [false, true, null, true], [RX9RobotStatus.Pitstop]: [true, true, true, false], [RX9RobotStatus.ManualSteering]: [false, true, false, false], [RX9RobotStatus.FirmwareUpgrade]: [false, true, false, true] }; const index = commandIndex.indexOf(command); let isSet = commandSet[this.robot.status.activity][index]; if (isSet === null && command === 'home' && this.robot.status.isDocked !== undefined) { isSet = this.robot.status.isDocked; } assertIsNotUndefined(isSet); return isSet; } // Attempt to set the requested state async setTarget(command, signal) { await this.api.sendCleaningCommand(command, signal); } // Override the status while a requested change is pending overrideStatus(command) { const commandToActivity = { play: SimpleActivity.Clean, pause: SimpleActivity.Pause, home: SimpleActivity.Return, stop: SimpleActivity.Other }; this.robot.status.simpleActivity = commandToActivity[command]; } } //# sourceMappingURL=aeg-robot-ctrl.js.map