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.
93 lines • 4.05 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025-2026 Alexander Thoukydides
import { DysonMqttClient } from './dyson-mqtt-client-base.js';
import { readFile } from 'node:fs/promises';
import { assertIsDefined, formatList, MS, plural } from './utils.js';
import { setTimeout } from 'node:timers/promises';
// Interval between messages when replaying the log file
const INITIALISE_INTERVAL = 35 * MS; // 35 seconds
const MESSAGE_INTERVAL = 100; // 10 milliseconds
// Comments start with # and continue to the end of the line
const COMMENT_REGEXP = /^#.*|#[^{}]*$/;
// Marker for initialisation delay
const INITIALISE_MARKER = '---';
// A Dyson MQTT client using a log file as the source of messages
export class DysonMqttClientMock extends DysonMqttClient {
deviceConfig;
// The remaining log file lines
lines = [];
// MQTT topic for the device status
topic = '';
// Construct a new MQTT client
constructor(log, config, deviceConfig) {
super(log, config);
this.deviceConfig = deviceConfig;
}
// Start (re)connecting the MQTT client (resolves after initiating connect)
async connect() {
// Read and parse the log file, stripping comments and blank lines
const log = await readFile(this.deviceConfig.filename, 'utf8');
const lines = log.split('\n');
this.lines = lines.map(l => l.replace(COMMENT_REGEXP, '').trim()).filter(l => l.length);
this.log.debug(`MQTT log file contains ${plural(this.lines.length, 'message')}`);
// Emit a connection event
const packet = { cmd: 'connack', sessionPresent: false };
this.emit('connect', packet);
// Start emitting messages from the log file
void this.emitNextMessage();
}
// Emit the next message from the log file, if any
async emitNextMessage() {
// Delay between messages
await setTimeout(MESSAGE_INTERVAL);
// Check for more messages
const line = this.lines.shift();
if (!line) {
this.log.debug('End of MQTT log file reached');
return;
}
if (line === INITIALISE_MARKER) {
// Extra delay for initialisation after the first few messages
this.log.debug('Pausing MQTT log file playback');
await setTimeout(INITIALISE_INTERVAL);
this.log.debug('Resuming MQTT log file playback');
}
else {
// Assume all messages come from the status topic
const topic = this.topic;
const payload = Buffer.from(line);
const packet = { cmd: 'publish', topic, payload, qos: 0, dup: false, retain: false };
this.emit('message', topic, payload, packet);
}
// Emit the next message
void this.emitNextMessage();
}
// Ignore requests to publish messages
publishAsync(...args) {
const [topic] = args;
this.log.debug(`Ignoring MQTT publish to ${topic}`);
return Promise.resolve(undefined);
}
// Grant subscription requests, but do nothing
subscribeAsync(...args) {
const [topicObject] = args;
const topics = typeof topicObject === 'string' ? [topicObject]
: Array.isArray(topicObject) ? topicObject
: Object.keys(topicObject).filter(t => t !== 'resubscribe');
this.log.debug(`Subscribed to MQTT topics: ${formatList(topics)}`);
// Select one of the topics as the device status topic
const statusTopic = topics.find(t => t.includes('status'));
assertIsDefined(statusTopic);
this.topic = statusTopic;
// Return a successful subscription grant for each topic
const grant = topics.map(topic => ({ topic, qos: 0 }));
return Promise.resolve(grant);
}
// Stop mocking MQTT messages
stop() {
this.log.debug('Stopping mock MQTT client');
this.lines = [];
return Promise.resolve();
}
}
//# sourceMappingURL=dyson-mqtt-client-mock.js.map