homebridge-hatch-baby-rest
Version:
Homebridge plugin for Hatch Rest/Restore WiFi sound machines
119 lines (118 loc) • 4.32 kB
JavaScript
import { BehaviorSubject, firstValueFrom, skip, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { delay, logDebug, logError } from "../shared/util.js";
function assignState(previousState, changes) {
const state = Object.assign({}, previousState);
for (const key in changes) {
if (typeof changes[key] === 'object') {
state[key] = assignState(previousState[key] || {}, changes[key]);
}
else {
state[key] = changes[key];
}
}
return state;
}
export const MAX_IOT_VALUE = 65535;
export function convertFromPercentage(percentage) {
return Math.ceil((percentage / 100) * MAX_IOT_VALUE);
}
export function convertToPercentage(value) {
return Math.floor((value * 100) / MAX_IOT_VALUE);
}
export class IotDevice {
onCurrentState = new BehaviorSubject(null);
get mqttClient() {
return this.onIotClient.getValue();
}
onStatusToken = new Subject();
previousUpdatePromise = Promise.resolve();
onState = this.onCurrentState.pipe(filter((state) => state !== null));
get id() {
return this.info.id;
}
get name() {
return this.info.name;
}
get macAddress() {
return this.info.macAddress;
}
info;
onIotClient;
constructor(info, onIotClient) {
this.info = info;
this.onIotClient = onIotClient;
onIotClient
.pipe(skip(1))
.subscribe((client) => this.registerMqttClient(client));
this.registerMqttClient(onIotClient.getValue());
}
registerMqttClient(mqttClient) {
const { thingName } = this.info;
let getClientToken;
mqttClient.on('close', () => {
logDebug('MQTT client closed');
});
mqttClient.on('offline', () => {
logDebug('MQTT client offline');
});
mqttClient.on('status', (topic, message, clientToken, status) => {
if (topic !== thingName) {
// status for a different thing
return;
}
this.onStatusToken.next(clientToken);
if (clientToken === getClientToken) {
const { state } = status;
this.onCurrentState.next(assignState(state.reported, state.desired));
}
});
mqttClient.on('foreignStateChange', (topic, message, s) => {
const currentState = this.onCurrentState.getValue();
if (!currentState || topic !== thingName) {
return;
}
this.onCurrentState.next(assignState(assignState(currentState, s.state.reported), s.state.desired));
});
this.previousUpdatePromise = this.previousUpdatePromise
.catch((_) => {
// ignore errors, they shouldn't be possible
void _;
})
.then(() => new Promise((resolve) => {
mqttClient.on('connect', () => {
mqttClient.register(thingName, {}, () => {
getClientToken = mqttClient.get(thingName);
resolve(firstValueFrom(this.onStatusToken.pipe(filter((token) => token === getClientToken))));
});
});
}));
}
getCurrentState() {
return firstValueFrom(this.onState);
}
update(update) {
this.previousUpdatePromise = this.previousUpdatePromise
.catch((_) => {
// ignore errors, they shouldn't be possible
void _;
})
.then(() => {
if (!this.mqttClient) {
logError(`Unable to Update ${this.name} - No MQTT Client Registered`);
return;
}
const updateToken = this.mqttClient.update(this.info.thingName, {
state: {
desired: update,
},
});
if (!updateToken) {
logError(`Failed to apply update to ${this.name} because another update was in progress: ${JSON.stringify(update)}`);
}
const requestComplete = firstValueFrom(this.onStatusToken.pipe(filter((token) => token === updateToken)));
// wait a max of 30 seconds to finish request
return Promise.race([requestComplete, delay(30000)]);
});
}
}