matterbridge-dyson-robot
Version:
A Matterbridge plugin that connects Dyson robot vacuums and air treatment devices to the Matter smart home ecosystem via their local or cloud MQTT APIs.
85 lines • 3.56 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025-2026 Alexander Thoukydides
import { formatList, MS, plural } from './utils.js';
import { CC, RI } from './logger-options.js';
import { setImmediate } from 'node:timers/promises';
// Timeout waiting for the next update
const UPDATE_TIMEOUT = 5 * MS; // 5 seconds
// Command serialiser for Dyson air purifier settings
export class DysonAirSerialise {
log;
mqtt;
flushChanged;
// Operation currently in progress
activePromise;
// Pending operation
pendingProductState = {};
pendingDescriptions = [];
pendingPromise;
// Create a new Dyson air purifier command serialiser
constructor(log, mqtt, flushChanged) {
this.log = log;
this.mqtt = mqtt;
this.flushChanged = flushChanged;
}
// Queue an MQTT command to set the product state
async setState(description, productState) {
if (this.activePromise)
this.log.info(`(Delaying: ${description})`);
// Merge with the pending command
Object.assign(this.pendingProductState, productState);
this.pendingDescriptions.push(description);
this.pendingPromise ??= this.startPending();
return this.pendingPromise;
}
// Wait for any active command to complete and issue a new one
async startPending() {
// Wait for any active command to complete
try {
await this.activePromise;
}
catch { /* empty */ }
await setImmediate();
// Process the pending command and prepare for the next
this.activePromise = this.startActive(this.pendingDescriptions, this.pendingProductState);
this.pendingProductState = {};
this.pendingDescriptions = [];
this.pendingPromise = undefined;
// Wait for the active command to complete
try {
await this.activePromise;
}
finally {
this.activePromise = undefined;
}
}
// Send an MQTT command to set the product state and wait for a status update
async startActive(descriptions, productState) {
// Log details of the command
if (descriptions.length === 1) {
this.log.info(descriptions[0] ?? '');
}
else {
const unique = [...new Set(descriptions.toReversed())].reverse();
const duplicateCount = descriptions.length - unique.length;
this.log.info(`Coalescing ${plural(descriptions.length, 'delayed command')}`
+ `${duplicateCount ? ` (${plural(duplicateCount, 'duplicate')} hidden)` : ''}:`);
for (const description of unique)
this.log.info(` ${description}`);
}
const values = Object.entries(productState).map(([key, value]) => `${CC}${key}=${value}${RI}`);
this.log.info(`Setting state: ${formatList(values)}`);
// Publish the command and process the next MQTT status update
await this.mqtt.commandStateSet(productState);
this.flushChanged();
// Wait for the next MQTT status update or a timeout
const timeoutSignal = AbortSignal.timeout(UPDATE_TIMEOUT);
await this.mqtt.onceAsync('status', timeoutSignal);
}
// Should attributes be updated in response to MQTT status messages
get ignoreMqttUpdates() {
// Ignore the status update from a command that will be superseded
return this.pendingPromise !== undefined;
}
}
//# sourceMappingURL=dyson-device-air-serialise.js.map