UNPKG

homebridge-aeg-robot

Version:

AEG RX9 / Electrolux Pure i9 robot vacuum plugin for Homebridge

200 lines 8.65 kB
// Homebridge plugin for AEG RX 9 / Electrolux Pure i9 robot vacuum // Copyright © 2022-2023 Alexander Thoukydides import nodePersist from 'node-persist'; import { AEGAPIAuthorisationError } from './aegapi-error.js'; import { assertIsString, logError } from './utils.js'; // A Homebridge platform accessory handler export class AEGAccessory { platform; accessory; name; Service; Characteristic; HapStatusError; log; // Services restored from cache but not longer required obsoleteServices; // The primary service primaryService; // Service names set via HomeKit customNames = new Map(); persistPromise; // Characteristic used to indicate a long-term error state errorCharacteristic; // Create a accessory handler constructor(platform, accessory, name) { this.platform = platform; this.accessory = accessory; this.name = name; this.Service = platform.hb.hap.Service; this.Characteristic = platform.hb.hap.Characteristic; this.HapStatusError = platform.hb.hap.HapStatusError; this.log = platform.log; this.obsoleteServices = [...this.accessory.services]; // Load any persistent data this.persistPromise = this.loadPersist(); } // Get or add a service makeService(serviceConstructor, suffix = '', subtype) { // Use the accessory name as a prefix for the service name const displayName = `${this.accessory.displayName} ${suffix}`; // Check whether the service already exists let service = subtype ? this.accessory.getServiceById(serviceConstructor, subtype) : this.accessory.getService(serviceConstructor); if (service) { // Remove from the list of obsolete services const serviceIndex = this.obsoleteServices.indexOf(service); if (serviceIndex !== -1) this.obsoleteServices.splice(serviceIndex, 1); } else { // Create a new service this.log.debug(`Adding new service "${displayName}"`); service = this.accessory.addService(serviceConstructor, displayName, subtype); } // If this is the first service to be added then select it as the primary if (!this.primaryService && service.UUID !== this.Service.AccessoryInformation.UUID) { service.setPrimaryService(true); this.primaryService = service; } // Add a Configured Name characteristic if a custom name was supplied if (suffix.length) this.addServiceName(service, suffix, displayName); // Return the service return service; } // Add a read-only Configured Name characteristic addServiceName(service, suffix, defaultName) { // Add the configured name characteristic if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); } const characteristic = service.getCharacteristic(this.Characteristic.ConfiguredName); characteristic.setProps({ perms: ["ev" /* Perms.NOTIFY */, "pr" /* Perms.PAIRED_READ */, "pw" /* Perms.PAIRED_WRITE */] }); assertIsString(characteristic.value); let currentName = characteristic.value; // Set the initial value void this.withPersist('read-only', () => { if (currentName === this.customNames.get(suffix)) { // Name was set via HomeKit, so preserve it this.log.debug(`Preserving ${suffix} service name "${currentName}" set via HomeKit`); } else { // Probably not changed by the user via HomeKit, so set explicitly if (currentName !== defaultName) { if (currentName === '') this.log.debug(`Naming ${suffix} service as "${defaultName}"`); else this.log.info(`Renaming ${suffix} service to "${defaultName}" (was "${currentName}")`); } characteristic.updateValue(defaultName); currentName = defaultName; } }); // Monitor changes to the name characteristic.onSet(value => { assertIsString(value); void this.withPersist('read-write', () => { if (value !== currentName) { currentName = value; this.log.debug(`${suffix} Configured Name => "${value}"`); if (value === defaultName) { this.log.info(`Removing HomeKit override on ${suffix} service name`); this.customNames.delete(suffix); } else { if (this.customNames.get(suffix) === undefined) this.log.info(`HomeKit override on ${suffix} service name ("${value}")`); this.customNames.set(suffix, value); } } }); }); } // Perform an operation using persistent data async withPersist(type, operation) { while (this.persistPromise) await this.persistPromise; await operation(); if (type === 'read-write') { this.persistPromise = this.savePersist(); await this.persistPromise; } } // Restore any persistent data async loadPersist() { try { const persist = await nodePersist.getItem(this.accessory.UUID); if (persist) this.customNames = new Map(Object.entries(persist.customNames)); } catch (err) { logError(this.log, 'Load persistent data', err); } finally { this.persistPromise = undefined; } } // Save changes to the persistent data async savePersist() { try { const persist = { customNames: Object.fromEntries(this.customNames) }; await nodePersist.setItem(this.accessory.UUID, persist); } catch (err) { logError(this.log, 'Save persistent data', err); } finally { this.persistPromise = undefined; } } // Check and tidy services after the accessory has been configured cleanupServices() { // Remove any services that were restored from cache but no longer required this.obsoleteServices.forEach(service => { this.log.info(`Removing obsolete service "${service.displayName}"`); this.accessory.removeService(service); }); } // Set or clear a long-term error state setError(cause) { if (cause === undefined) { // Error has cleared, so restore any previous characteristic value if (this.errorCharacteristic) { const { characteristic, originalValue } = this.errorCharacteristic; if (characteristic.value instanceof this.HapStatusError) characteristic.updateValue(originalValue); } } else { // Set the accessory state on the first error this.errorCharacteristic ??= AEGAccessory.setError(this.platform, this.accessory, cause); } } // Place an accessory in a long-term error state static setError(platform, accessory, cause) { const { Service, Characteristic, HapStatusError } = platform.hb.hap; // Select a service (preferably the primary) to report the error const services = accessory.services; if (services[0] === undefined) return; const service = services.find(service => service.isPrimaryService) ?? services.find(service => service.UUID !== Service.AccessoryInformation.UUID) ?? services[0]; // Pick a characteristic; ideally one with Perms.NOTIFY const characteristic = service.characteristics.find(characteristic => characteristic.UUID !== Characteristic.Name.UUID); if (!characteristic) return; // Report the error to HomeKit const originalValue = characteristic.value; const hapStatus = cause instanceof AEGAPIAuthorisationError ? -70411 /* HAPStatus.INSUFFICIENT_AUTHORIZATION */ : -70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */; characteristic.updateValue(new HapStatusError(hapStatus)); return { characteristic, originalValue }; } } //# sourceMappingURL=accessory.js.map