matterbridge-xiaomi-roborock
Version:
Matterbridge Xiaomi Roborock Plugin
146 lines (145 loc) • 5.94 kB
JavaScript
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);
}
}
}