UNPKG

homebridge-kobold

Version:

A Vorwerk Kobold vacuum robot plugin for homebridge.

260 lines 10.1 kB
import Debug from 'debug'; import control from 'node-kobold-control'; import { buildSpotCharacteristics } from './customCharacteristics.js'; import { KoboldVacuumAccessory } from './platformAccessory.js'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; const debug = Debug('homebridge-kobold'); export class KoboldHomebridgePlatform { log; config; api; Service; Characteristic; accessories = new Map(); discoveredCacheUUIDs = []; robots = []; nextRoom = null; language; hiddenServices; refresh; spotCharacteristics; token; constructor(log, config, api) { this.log = log; this.config = config; this.api = api; this.Service = api.hap.Service; this.Characteristic = api.hap.Characteristic; this.spotCharacteristics = buildSpotCharacteristics(this.api); const platformConfig = (config ?? {}); this.token = platformConfig.token ?? ''; this.language = ['en', 'de', 'fr'].includes(platformConfig.language ?? '') ? platformConfig.language : 'en'; let hiddenServices = []; if (platformConfig.disabled !== undefined) { hiddenServices = platformConfig.disabled; } if (platformConfig.hidden !== undefined) { hiddenServices = platformConfig.hidden; } this.hiddenServices = hiddenServices; this.refresh = this.parseRefresh(platformConfig); this.log(`Refresh is set to: ${this.refresh}${this.refresh !== 'auto' ? ' seconds' : ''}`); if (!this.token) { this.log.error('No Kobold token configured. Please update your config and restart Homebridge.'); } this.api.on('didFinishLaunching', () => { debug('Executed didFinishLaunching callback'); void this.discoverRobots(); }); } configureAccessory(accessory) { this.accessories.set(accessory.UUID, accessory); } isServiceHidden(key) { if (Array.isArray(this.hiddenServices)) { return this.hiddenServices.includes(key); } if (typeof this.hiddenServices === 'string') { return this.hiddenServices.indexOf(key) !== -1; } return false; } parseRefresh(config) { if ('refresh' in config && config.refresh !== undefined && config.refresh !== 'auto') { const parsed = parseInt(String(config.refresh), 10); if (Number.isNaN(parsed) || parsed < 0) { return 60; } if (parsed > 0 && parsed < 60) { this.log.warn('Minimum refresh time is 60 seconds to not overload the Vorwerk servers'); return 60; } if (parsed === 0) { return 0; } return parsed; } return 'auto'; } async discoverRobots() { if (!this.token) { return; } this.discoveredCacheUUIDs.length = 0; try { const robots = await this.loadRobots(); this.robots.length = 0; robots.forEach(robot => { this.robots.push(robot); }); this.robots.forEach((robot, index) => { this.log.info(`Found robot #${index + 1} named "${robot.device.name}" with serial "${robot.device._serial.substring(0, 9)}XXXXXXXXXXXX"`); this.setupMainAccessory(robot); this.updateRobotTimer(robot.device._serial); }); } catch (error) { const message = error instanceof Error ? error.message : String(error); this.log.error(`Failed to discover robots: ${message}`); } this.cleanupAccessories(); } async loadRobots() { debug('Loading your robots'); const client = new control.Client(); await new Promise((resolve, reject) => { client.authorize(this.token, (err) => { if (err) { this.log.error(`Can't log on to Vorwerk cloud. Please check your internet connection and your token. Try again later if the Vorwerk servers have issues: ${err}`); reject(err); } else { resolve(); } }); }); const robots = await new Promise((resolve, reject) => { client.getRobots((error, robotList) => { if (error) { this.log.error(`Successful login but can't connect to your Vorwerk robot: ${error}`); reject(error); } else { resolve(robotList); } }); }); if (!robots.length) { this.log.error('Successful login but no robots associated with your account.'); return []; } const robotRecords = []; for (const robot of robots) { const state = await new Promise((resolve, reject) => { robot.getState((error, result) => { if (error) { this.log.error(`Error getting robot meta information: ${error}: ${result}`); reject(error); } else { resolve(result); } }); }); const record = { device: robot, meta: state.meta ?? {}, availableServices: state.availableServices ?? {}, }; const maps = await new Promise((resolve, reject) => { robot.getPersistentMaps((error, robotMaps) => { if (error) { this.log.error(`Error updating persistent maps: ${error}: ${robotMaps}`); reject(error); } else { resolve(robotMaps || []); } }); }); if (!maps.length) { robot.maps = []; } else { await Promise.all(maps.map((map) => new Promise(resolve => { robot.getMapBoundaries(map.id, (error, result) => { if (error) { this.log.error(`Error getting boundaries: ${error}: ${result}`); } else { map.boundaries = result.boundaries; } resolve(); }); }))); robot.maps = maps; } robotRecords.push(record); } return robotRecords; } setupMainAccessory(robot) { const uuid = this.api.hap.uuid.generate(robot.device._serial); const displayName = robot.device.name; const accessory = this.prepareAccessory(uuid, displayName); accessory.context.robotSerial = robot.device._serial; accessory.context.boundaryId = null; accessory.context.boundaryName = null; const handler = new KoboldVacuumAccessory(this, accessory, robot); robot.mainAccessory = handler; this.discoveredCacheUUIDs.push(uuid); } prepareAccessory(uuid, name) { const existingAccessory = this.accessories.get(uuid); if (existingAccessory) { existingAccessory.displayName = name; this.api.updatePlatformAccessories([existingAccessory]); this.accessories.set(uuid, existingAccessory); return existingAccessory; } const accessory = new this.api.platformAccessory(name, uuid); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); this.accessories.set(uuid, accessory); return accessory; } cleanupAccessories() { for (const [uuid, accessory] of this.accessories) { if (!this.discoveredCacheUUIDs.includes(uuid)) { this.log.info('Removing existing accessory from cache:', accessory.displayName); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); this.accessories.delete(uuid); } } } getRobot(serial) { return this.robots.find(robot => robot.device._serial === serial); } async updateRobot(serial) { const robot = this.getRobot(serial); if (!robot) { return; } if (robot.lastUpdate && new Date().getTime() - robot.lastUpdate.getTime() < 2000) { return; } debug(`${robot.device.name}: ++ Updating robot state`); robot.lastUpdate = new Date(); await new Promise(resolve => { robot.device.getState((error) => { if (error) { this.log.error('Cannot update robot. Check if robot is online. ' + error); } resolve(); }); }); } updateRobotTimer(serial) { const robot = this.getRobot(serial); if (!robot) { return; } void this.updateRobot(serial).finally(() => { clearTimeout(robot.timer); robot.mainAccessory?.updated(); if (this.refresh !== 'auto' && this.refresh !== 0) { debug(`${robot.device.name}: ++ Next background update in ${this.refresh} seconds`); robot.timer = setTimeout(() => this.updateRobotTimer(serial), this.refresh * 1000); } else if (this.refresh === 'auto' && robot.device.canPause) { debug(`${robot.device.name}: ++ Next background update in 60 seconds while cleaning (auto mode)`); robot.timer = setTimeout(() => this.updateRobotTimer(serial), 60 * 1000); } else { debug(`${robot.device.name}: ++ Stopped background updates`); } }); } } //# sourceMappingURL=platform.js.map