homebridge-aeg-robot
Version:
AEG RX9 / Electrolux Pure i9 robot vacuum plugin for Homebridge
145 lines • 6.93 kB
JavaScript
// 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';
// 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 {
// 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