UNPKG

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
// 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