UNPKG

matterbridge-xiaomi-roborock

Version:
146 lines (145 loc) 5.94 kB
import { BehaviorSubject, distinct, exhaustMap, filter, Subject, takeUntil, timer } from 'rxjs'; import * as miio from 'node-miio'; import { cleaningStatuses } from '../utils/constants.js'; const GET_STATE_INTERVAL_MS = 10000; export class DeviceManager { log; internalDevice$ = new BehaviorSubject(undefined); ip; token; internalErrorChanged$ = new Subject(); internalStateChanged$ = new Subject(); stop$ = new Subject(); errorChanged$ = this.internalErrorChanged$.pipe(distinct()); stateChanged$ = this.internalStateChanged$.asObservable(); deviceConnected$ = this.internalDevice$.pipe(filter(Boolean)); connectingPromise = null; connectRetry = setTimeout(() => void 0, 100); constructor(log, config) { this.log = log; if (!config.ip) { throw new Error('You must provide an ip address of the vacuum cleaner.'); } this.ip = config.ip; if (!config.token) { throw new Error('You must provide a token of the vacuum cleaner.'); } this.token = config.token; this.connect().catch(() => { }); } get model() { return this.internalDevice$.value?.miioModel || 'unknown model'; } get state() { return this.property('state'); } get isCleaning() { return cleaningStatuses.includes(this.state); } get isPaused() { return this.state === 'paused'; } get device() { if (!this.internalDevice$.value) { throw new Error('Not connected yet'); } return this.internalDevice$.value; } property(propertyName) { return this.device.property(propertyName); } async ensureDevice(callingMethod) { try { if (!this.internalDevice$.value) { const errMsg = `${callingMethod} | No vacuum cleaner is discovered yet.`; this.log.error(errMsg); throw new Error(errMsg); } if (this.internalDevice$.value.handle.api.parent.socket) { this.log.debug(`DEB ensureDevice | ${this.model} | The socket is still on. Reusing it.`); } } catch (error) { const err = error; if (/destroyed/i.test(err.message) || /No vacuum cleaner is discovered yet/.test(err.message)) { this.log.info(`INF ensureDevice | ${this.model} | The socket was destroyed or not initialised, initialising the device`); await this.connect(); } else { this.log.error(err.message, err); throw err; } } } stop() { this.internalStateChanged$.complete(); this.internalErrorChanged$.complete(); this.stop$.next(); this.stop$.complete(); this.internalDevice$.value?.destroy(); this.internalDevice$.complete(); } async connect() { if (this.connectingPromise === null) { this.connectingPromise = this.initializeDevice().catch((error) => { this.log.error(`ERR connect | miio.device, next try in 10 seconds | ${error}`); clearTimeout(this.connectRetry); this.connectRetry = setTimeout(() => this.connect().catch(() => { }), 10000); throw error; }); } try { await this.connectingPromise; clearTimeout(this.connectRetry); } finally { this.connectingPromise = null; } } async initializeDevice() { this.log.debug('DEB getDevice | Discovering vacuum cleaner'); const device = await miio.device({ address: this.ip, token: this.token }); if (device.matches('type:vaccuum')) { this.internalDevice$.next(device); this.log.setModel(this.model); this.log.info(`STA getDevice | Connected to: ${this.ip}`); this.log.info(`STA getDevice | Model: ${this.model}`); this.log.info(`STA getDevice | State: ${this.property('state')}`); this.log.info(`STA getDevice | FanSpeed: ${this.property('fanSpeed')}`); this.log.info(`STA getDevice | BatteryLevel: ${this.property('batteryLevel')}`); this.device.on('errorChanged', (error) => this.internalErrorChanged$.next(error)); this.device.on('stateChanged', (state) => this.internalStateChanged$.next(state)); timer(0, GET_STATE_INTERVAL_MS) .pipe(takeUntil(this.stop$), exhaustMap(() => this.getState())) .subscribe(); } else { const model = (device || {}).miioModel; this.log.error(`Device "${model}" is not registered as a vacuum cleaner! If you think it should be, please open an issue at https://github.com/afharo/matterbridge-xiaomi-roborock/issues/new and provide this line.`); this.log.debug(device); device.destroy(); } } async getState() { try { this.log.debug(`DEB getState | ${this.model} | Polling...`); await this.ensureDevice('getState'); await this.device.poll(); const state = await this.device.state(); this.log.debug(`DEB getState | ${this.model} | State ${JSON.stringify(state)} | Props ${JSON.stringify(this.device.properties)}`); Object.entries(state).forEach(([key, value]) => { if (key === 'error') { this.internalErrorChanged$.next(value); } else { this.internalStateChanged$.next({ key, value }); } }); Object.entries(this.device.properties).forEach(([key, value]) => this.internalStateChanged$.next({ key, value })); } catch (err) { this.log.error(`getState | ${err}`, err); } } }