UNPKG

homebridge-aeg-robot

Version:

AEG RX9 / Electrolux Pure i9 robot vacuum plugin for Homebridge

152 lines 7.33 kB
// Homebridge plugin for AEG RX 9 / Electrolux Pure i9 robot vacuum // Copyright © 2022-2024 Alexander Thoukydides import NodePersist from 'node-persist'; import Path from 'path'; import { DEFAULT_CONFIG, PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; import { AEGAccessory } from './accessory.js'; import { AEGRobotAccessory } from './accessory-robot.js'; import { checkDependencyVersions } from './check-versions.js'; import { AEGAccount } from './aeg-account.js'; import { deepMerge, getValidationTree, logError, plural } from './utils.js'; import { PrefixLogger } from './logger.js'; import { checkers } from './ti/config-types.js'; // Deprecation notice for the log file const DEPRECATION_NOTICE = `DEPRECATION NOTICE > The homebridge-aeg-robot plugin has been archived and is no longer maintained. > For Matter-based native vacuum cleaner support in iOS 18.4 and later, please migrate to: > https://github.com/thoukydides/matterbridge-aeg-robot`; // A Homebridge AEG RX 9 / Electrolux Pure i9 platform export class AEGPlatform { platformConfig; hb; makeUUID; // Custom logger log; // Plugin configuration with defaults applied config; // Mapping from UUID to accessories and their implementations accessories = new Map(); // Create a new AEG RX 9 / Electrolux Pure i9 platform constructor(log, platformConfig, hb) { this.platformConfig = platformConfig; this.hb = hb; this.makeUUID = hb.hap.uuid.generate; // Use a custom logger to filter-out sensitive information this.log = new PrefixLogger(log); // Wait for Homebridge to restore cached accessories this.hb.on('didFinishLaunching', () => void this.finishedLaunching()); } // Restore a cached accessory configureAccessory(accessory) { this.accessories.set(accessory.UUID, { accessory }); } // Update list of robots after cache has been restored async finishedLaunching() { try { // Log a deprecation notice this.log.warn(DEPRECATION_NOTICE); // Check that the dependencies and configuration checkDependencyVersions(this); this.checkConfig(); // Initialise the platform accessories await this.addConfiguredAccessories(); this.removeUnconfiguredAccessories(); } catch (err) { logError(this.log, 'Plugin initialisation', err); try { this.setAccessoryErrors(err); } catch (err) { logError(this.log, 'Plugin error handler', err); } } } // Check the user's configuration checkConfig() { // Apply default values const config = deepMerge(DEFAULT_CONFIG, this.platformConfig); // Ensure that all required fields are provided and are of suitable types const checker = checkers.Config; checker.setReportedPath('<PLATFORM_CONFIG>'); const strictValidation = checker.strictValidate(config); if (!checker.test(config)) { this.log.error('Plugin unable to start due to configuration errors:'); this.logCheckerValidation("error" /* LogLevel.ERROR */, strictValidation); throw new Error('Invalid plugin configuration'); } // Warn of extraneous fields in the configuration if (strictValidation) { this.log.warn('Unsupported fields in plugin configuration will be ignored:'); this.logCheckerValidation("warn" /* LogLevel.WARN */, strictValidation); } // Use the validated configuration this.config = config; if (this.config.debug.includes('Log Debug as Info')) this.log.logDebugAsInfo(); } // Log configuration checker validation errors logCheckerValidation(level, errors) { const errorLines = errors ? getValidationTree(errors) : []; errorLines.forEach(line => { this.log.log(level, line); }); this.log.info(`${this.hb.user.configPath()}:`); const configLines = JSON.stringify(this.platformConfig, null, 4).split('\n'); configLines.forEach(line => { this.log.info(` ${line}`); }); } // Add any accessories that have been configured async addConfiguredAccessories() { // Prepare persistent storage for this plugin const persistDir = Path.join(this.hb.user.storagePath(), PLUGIN_NAME, 'persist'); await NodePersist.init({ dir: persistDir }); // Add accessories for any robots associated with the AEG account const account = new AEGAccount(this.log, this.config); const robotPromises = await account.getRobots(); this.log.info(`Found ${plural(robotPromises.length, 'robot vacuum')}`); await Promise.all(robotPromises.map(this.addRobotAccessory.bind(this))); } // Add an accessory for a robot async addRobotAccessory(robotPromise) { const robot = await robotPromise; const uuid = this.makeUUID(robot.applianceId); // Check if an accessory was restored from the cache const existingAccessory = this.accessories.get(uuid); if (existingAccessory) { // Attach functionality to the existing accessory const { accessory } = existingAccessory; this.log.info(`Restoring accessory "${accessory.displayName}" from cache for ${robot.toString()}`); const implementation = new AEGRobotAccessory(this, accessory, robot); existingAccessory.implementation = implementation; } else { // Create a new accessory for this robot this.log.info(`Creating new accessory "${robot.name}" for ${robot.toString()}`); const accessory = new this.hb.platformAccessory(robot.name, uuid); const implementation = new AEGRobotAccessory(this, accessory, robot); this.accessories.set(uuid, { accessory, implementation }); this.hb.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } // Remove any accessories that are no longer required removeUnconfiguredAccessories() { // Identify accessories that do not have an implementation const isObsolete = (linkage) => !linkage.implementation; const rmAccessories = [...this.accessories.values()] .filter(isObsolete).map(linkage => linkage.accessory); if (!rmAccessories.length) return; // Remove the identified accessories this.log.warn(`Removing ${plural(rmAccessories.length, 'cached accessory')} that are no longer required`); this.hb.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, rmAccessories); rmAccessories.forEach(accessory => this.accessories.delete(accessory.UUID)); } // Place all accessories in an error state if initialisation failed setAccessoryErrors(cause) { const accessories = [...this.accessories.values()].map(linkage => linkage.accessory); if (!accessories.length) return; // Set the error state this.log.warn('Placing all accessories in error state'); accessories.forEach(accessory => AEGAccessory.setError(this, accessory, cause)); } } //# sourceMappingURL=platform.js.map