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.
146 lines • 6.5 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025 Alexander Thoukydides
import { INSPECT_SINGLE_LINE, MM, MP, MR, RD, SR, ST } from './logger-options.js';
import { MS, tryListener } from './utils.js';
import { dysonMqttParse } from './dyson-mqtt-parse.js';
import { DysonMqttSubscribe } from './dyson-mqtt-subscribe.js';
import { DysonMqttConnection } from './dyson-mqtt-connect.js';
import { inspect } from 'util';
import { DysonMQTTFilter } from './dyson-mqtt-filter.js';
import { AsyncEventEmitter } from './async-eventemitter.js';
import { DysonMqttClientLocal, DysonMqttClientRemote } from './dyson-mqtt-client.js';
;
// Minimum disconnection time before indicating device not reachable
const MIN_DOWN_TIME = 5 * MS; // 5 seconds
// Messages that indicate that the device is unreachable
const UNREACHABLE_MESSAGES = ['GOODBYE', 'GONE-AWAY'];
// Dyson MQTT client for all device types
export class DysonMqtt extends AsyncEventEmitter {
log;
config;
deviceConfig;
mqttConfig;
// The MQTT client
mqtt;
mqttConnection;
mqttSubscribe;
mqttFilter;
// The current status
status = {
reachable: false,
initialised: false
};
// Construct a new MQTT client
constructor(log, config, deviceConfig, mqttConfig) {
super({ captureRejections: true });
this.log = log;
this.config = config;
this.deviceConfig = deviceConfig;
this.mqttConfig = mqttConfig;
// Create the MQTT client
this.mqtt = 'password' in deviceConfig
? new DysonMqttClientLocal(log, config, deviceConfig)
: new DysonMqttClientRemote(log, config, deviceConfig);
this.mqtt.on('close', tryListener(this, () => { this.updateReachable('mqtt', false); }));
// Create an MQTT connection manager
this.mqttConnection = new DysonMqttConnection(log, config, this.mqtt);
// Manage MQTT topic subscriptions
const { rootTopic, serialNumber } = deviceConfig;
this.mqttSubscribe = new DysonMqttSubscribe(log, this.mqtt, config, mqttConfig.topics, rootTopic, serialNumber);
this.mqttSubscribe.on('error', err => this.emit('error', err));
this.mqttSubscribe.on('subscribed', () => {
this.emit('subscribed');
this.updateReachable('mqtt', true);
});
// Handle received MQTT messages
this.mqttFilter = new DysonMQTTFilter(log);
this.mqtt.on('message', tryListener(this, (topic, payload) => {
// Check the received topic and message
const topicStatus = this.mqttSubscribe.checkTopic(topic);
const normalise = topicStatus !== 'command';
const msg = dysonMqttParse(log, mqttConfig.messages, topic, normalise, payload);
const filter = this.mqttFilter.filter(msg);
// Dispatch the validated message and indicate a status update
this.logPayload('receive', topic, msg, filter);
if (!filter && topicStatus === 'subscribed') {
this.updateReachable('msg', !UNREACHABLE_MESSAGES.includes(msg.msg));
this.emit('message', msg);
this.emit('status');
}
}));
}
// Update the device reachability, with hysteresis on unreachable indications
downTimerHandle = new Map();
updateReachable(key, reachable) {
if (reachable) {
// Cancel any pending down timer for this reason
const handle = this.downTimerHandle.get(key);
clearTimeout(handle);
this.downTimerHandle.delete(key);
// Mark the device as reachable if there are no active down timers
if (!this.status.reachable && this.downTimerHandle.size === 0) {
this.status.reachable = true;
this.emit('status');
}
}
else {
// Only start down timer if not already running
if (!this.downTimerHandle.has(key) && this.status.reachable) {
this.log.debug(`Starting down timer for '${key}'`);
const handle = setTimeout(() => {
// Mark the device as not reachable after a delay
if (this.status.reachable) {
this.log.error('Unreachable');
this.status.reachable = false;
this.emit('status');
}
}, MIN_DOWN_TIME);
this.downTimerHandle.set(key, handle);
}
}
}
// Wait until the device is reachable and all initial state has been received
async waitUntilInitialised() {
while (!this.status.reachable || !this.status.initialised) {
await this.onceAsync('status');
}
}
// Stop the MQTT client
async stop() {
await this.mqttConnection.stop();
}
// Publish a command
async publish(...[msg, params]) {
// Construct the full message
const time = new Date().toISOString();
const fullMsg = { msg, ...params, time };
const payload = JSON.stringify(fullMsg);
// Publish to the command topic
const topic = this.mqttSubscribe.commandTopic;
this.logPayload('publish', topic, fullMsg);
await this.mqtt.publishAsync(topic, payload, { qos: 1, retain: false });
}
// Log received or transmitted message payloads
logPayload(direction, topic, payload, filter) {
if (!this.config.debugFeatures.includes('Log MQTT Payloads'))
return;
// List the fixed fields first
const { msg, time, ...other } = payload;
const properties = [
`msg: ${direction === 'publish' ? MP : MR}'${msg}'${MM}`,
`time: ${RD}'${time}'${MM}`
];
// Include the other fields from the message, unless it is a duplicate
if (filter === 'duplicate') {
properties.push('...');
}
else {
properties.push(...Object.entries(other).sort().map(([key, value]) => `${key}: ${inspect(value, INSPECT_SINGLE_LINE)}`));
}
// Log the message (formatting with strikethrough if dropped by filter)
const object = `${MM}{ ${properties.join(', ')} }${RD}`;
this.log.debug(filter ? `MQTT ${direction}: ${ST}${object}${SR} (${filter})`
: `MQTT ${direction}: ${object} topic '${topic}'`);
}
}
//# sourceMappingURL=dyson-mqtt.js.map