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.
181 lines • 10.2 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025-2026 Alexander Thoukydides
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
import { bridgedNode, MatterbridgeEndpoint, roboticVacuumCleaner } from 'matterbridge';
import { BasicInformationServer, BridgedDeviceBasicInformationServer } from 'matterbridge/matter/behaviors';
import { Changed, ifValueChanged } from './decorator-changed.js';
import { AN, AV, CN, RI } from './logger-options.js';
// A Matterbridge endpoint with a Bridged Device Basic Information cluster
let EndpointBase = (() => {
let _classSuper = MatterbridgeEndpoint;
let _instanceExtraInitializers = [];
let _updateReachable_decorators;
return class EndpointBase extends _classSuper {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_updateReachable_decorators = [ifValueChanged];
__esDecorate(this, null, _updateReachable_decorators, { kind: "method", name: "updateReachable", static: false, private: false, access: { has: obj => "updateReachable" in obj, get: obj => obj.updateReachable }, metadata: _metadata }, null, _instanceExtraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
config = __runInitializers(this, _instanceExtraInitializers);
options;
// Decorator support
changed;
// Construct a new endpoint
constructor(log, config, options, definition) {
// Use Matterbridge's 'server' mode for robotic vacuums if enabled
const definitionArray = Array.isArray(definition) ? definition : [definition];
let mode;
if (config.enableServerRvc && definitionArray.includes(roboticVacuumCleaner)) {
mode = 'server';
definition = definitionArray.filter(d => d !== bridgedNode);
}
// Initialise the base class
const { id } = options;
const debug = config.debugFeatures.includes('Log Endpoint Debug');
super(definition, { id, mode }, debug);
this.config = config;
this.options = options;
// Use supplied logger instead of the one created by the base class
this.log = log;
// Matterbridge requires a unique name for each endpoint
this.deviceName = options.matterbridgeDeviceName;
// Copy of values (possibly) required by Matterbridge
// (Do NOT use the nodeLabel for deviceName; it might not be unique)
const info = options.basicInformation;
this.hardwareVersion = parseOptionalUnsigned(info.hardwareVersion);
this.hardwareVersionString = info.hardwareVersion;
this.productId = info.productId;
this.productName = info.productName;
this.serialNumber = info.serialNumber;
this.softwareVersion = parseOptionalUnsigned(info.softwareVersion);
this.softwareVersionString = info.softwareVersion;
this.uniqueId = info.uniqueId;
this.vendorId = info.vendorId;
this.vendorName = info.vendorName;
// Create the basic clusters required on all endpoints
this.createDefaultIdentifyClusterServer();
if (!mode)
this.createBridgedDeviceBasicInformationClusterServer(info);
// (Matterbridge creates the Basic Information cluster in 'server' mode)
// Prepare the decorator support
this.changed = new Changed(log);
// Identify the device
this.addCommandHandler('identify', () => {
this.log.info(`${CN}Identify device${RI}`);
});
}
// Perform any post-registration setup
async postRegister() {
// Matterbridge incorrectly sets Basic Information cluster attributes
if (this.serverNode) {
this.log.info('Patching Basic Information cluster attributes');
const info = this.options.basicInformation;
await this.patchBasicInformationClusterServer(this.serverNode, info);
}
}
// Patch the Basic Information cluster attributes with correct values
async patchBasicInformationClusterServer(serverNode, info) {
await serverNode.setStateOf(BasicInformationServer, {
// Mandatory attributes that should already be set correctly:
// productId, productName, vendorId, vendorName
// Mandatory attributes incorrectly set by Matterbridge
hardwareVersion: parseOptionalUnsigned(info.hardwareVersion) ?? 0,
hardwareVersionString: info.hardwareVersion?.substring(0, 64) ?? '?',
nodeLabel: info.nodeLabel.substring(0, 32),
softwareVersion: parseOptionalUnsigned(info.softwareVersion) ?? 0,
softwareVersionString: info.softwareVersion?.substring(0, 64) ?? '?',
// Optional attributes incorrectly set by Matterbridge
manufacturingDate: info.manufacturingDate?.substring(0, 16),
partNumber: info.partNumber?.substring(0, 32),
productAppearance: info.productAppearance,
productLabel: info.productLabel?.substring(0, 64),
productUrl: info.productUrl?.substring(0, 256),
serialNumber: info.serialNumber?.substring(0, 32)
});
}
// Create the Bridged Device Basic Information cluster
createBridgedDeviceBasicInformationClusterServer(info) {
this.behaviors.require(BridgedDeviceBasicInformationServer.enable({
events: {
leave: true,
reachableChanged: true
}
}), {
// Mandatory attributes
reachable: true,
uniqueId: info.uniqueId.substring(0, 32),
// Optional attributes
hardwareVersion: parseOptionalUnsigned(info.hardwareVersion),
hardwareVersionString: info.hardwareVersion?.substring(0, 64),
manufacturingDate: info.manufacturingDate?.substring(0, 16),
nodeLabel: info.nodeLabel.substring(0, 32),
partNumber: info.partNumber?.substring(0, 32),
productAppearance: info.productAppearance,
productId: info.productId,
productLabel: info.productLabel?.substring(0, 64),
productName: info.productName.substring(0, 32),
productUrl: info.productUrl?.substring(0, 256),
serialNumber: info.serialNumber?.substring(0, 32),
softwareVersion: parseOptionalUnsigned(info.softwareVersion),
softwareVersionString: info.softwareVersion?.substring(0, 64),
vendorId: info.vendorId,
vendorName: info.vendorName.substring(0, 32)
});
return this;
}
// Update the (Bridged Device) Basic Information cluster attributes
async updateReachable(reachable) {
this.log.info(`${AN}Reachable${RI}: ${AV}${reachable}${RI}`);
if (this.serverNode)
await this.serverNode.setStateOf(BasicInformationServer, { reachable });
else
await this.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
}
};
})();
export { EndpointBase };
// Format an enum value for logging
export function formatEnumLog(enumMap, value) {
const label = enumMap[value];
return `${AV}${label}${RI} (${AV}${value}${RI})`;
}
// Safely parse a string as an optional unsigned integer
function parseOptionalUnsigned(value) {
const parsed = parseInt(value ?? '', 10);
return isNaN(parsed) || parsed < 0 ? undefined : parsed;
}
//# sourceMappingURL=endpoint-base.js.map