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.
96 lines • 4 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025 Alexander Thoukydides
import EventEmitter from 'events';
import { formatList, plural, tryListener } from './utils.js';
// Manage Dyson MQTT topic subscriptions
export class DysonMqttSubscribe extends EventEmitter {
log;
mqtt;
config;
topics;
root_topic;
username;
// Construct a new subscription manager
constructor(log, mqtt, config, topics, root_topic, username) {
super({ captureRejections: true });
this.log = log;
this.mqtt = mqtt;
this.config = config;
this.topics = topics;
this.root_topic = root_topic;
this.username = username;
// (Re)subscribe to topics when the MQTT (re)connects
mqtt.on('connect', tryListener(this, async (_connack) => {
await this.subscribe();
this.emit('subscribed');
}));
}
// Subscribe to topics when the client (re)connects
async subscribe() {
// Select the required subscription topics
const replacePlaceholders = (topics) => topics.map(t => this.replaceTopicPlaceholders(t));
const topics = replacePlaceholders(this.topics.subscribe);
if (this.config.wildcardTopic) {
if (this.config.provisioningMethod !== 'Remote Account') {
// Use full wildcard topic for local connections
topics.push('#');
}
else {
// AWS IoT disconnects on wildcards; subscribe to command topic
topics.push(this.replaceTopicPlaceholders(this.topics.command));
}
}
// Attempt the subscription
this.log.debug(`MQTT subscribe: ${formatList(topics)}`);
const grant = await this.mqtt.subscribeAsync(topics, { qos: 1 });
// Check whether
const failures = topics.filter(topic => !grant.some(g => g.topic === topic));
if (!grant.length) {
throw new Error(`MQTT subscribe unsuccessful: all ${plural(topics.length, 'topic')} rejected`);
}
else if (failures.length) {
this.log.warn('MQTT subscribe partially successful: '
+ `${failures.length} of ${plural(topics.length, 'topic')} rejected`);
failures.forEach(topic => { this.log.warn(` '${topic}'`); });
}
else {
this.log.info(`MQTT subscribe successful: all ${plural(grant.length, 'topic')} granted`);
}
}
// Warn if a received message's topic is unexpected
checkTopic(topic) {
// Check whether the topic is expected
const isTopics = (topics) => topics.some(t => this.replaceTopicPlaceholders(t) === topic);
if (isTopics([this.topics.command]))
return 'command';
if (isTopics(this.topics.subscribe))
return 'subscribed';
if (isTopics(this.topics.other ?? []))
return 'expected';
// Attempt to diagnose common problems
const [root_topic, username] = topic.split('/', 2);
if (root_topic !== this.root_topic) {
this.log.warn('MQTT topic root (product type) mismatch:'
+ ` expected '${this.root_topic}', received '${root_topic}'`);
}
else if (username !== this.username) {
this.log.warn('MQTT topic username (product serial number) mismatch:'
+ ` expected '${this.username}', received '${username}'`);
}
else {
this.log.warn(`Unexpected MQTT topic received: ${topic}`);
}
return 'unexpected';
}
// The full topic for publishing commands
get commandTopic() {
return this.replaceTopicPlaceholders(this.topics.command);
}
// Replace any placeholders in topic
replaceTopicPlaceholders(topic) {
return topic
.replace('@', this.root_topic)
.replace('@', this.username);
}
}
//# sourceMappingURL=dyson-mqtt-subscribe.js.map