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.
229 lines • 15.7 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, powerSource, roboticVacuumCleaner } from 'matterbridge';
import { PowerSource, RvcOperationalState, ServiceArea } from 'matterbridge/matter/clusters';
import { batteryPowerSourceBehavior, createBatteryPowerSourceClusterServer, createRvcCleanModeClusterServer, createRvcOperationalStateClusterServer, createRvcRunModeClusterServer, createServiceAreaClusterServer, rvcCleanModeBehavior, rvcOperationalStateBehavior, rvcRunModeBehavior, serviceAreaBehavior } from './endpoint-360-rvc.js';
import { EndpointBase, formatEnumLog } from './endpoint-base.js';
import { ifValueChanged } from './decorator-changed.js';
import { assertIsDefined, formatList, formatSeconds, MS, plural } from './utils.js';
import { AN, AV, RI } from './logger-options.js';
import { Behavior360, BehaviorDevice360, RvcCleanMode360, RvcRunMode360 } from './endpoint-360-behavior.js';
// A Matterbridge endpoint with robot vacuum cleaner clusters
let Endpoint360 = (() => {
let _classSuper = EndpointBase;
let _instanceExtraInitializers = [];
let _updatePowerSource_decorators;
let _updateRvcRunMode_decorators;
let _updateRvcCleanMode_decorators;
let _updateRvcOperationalState_decorators;
let _updateServiceArea_decorators;
return class Endpoint360 extends _classSuper {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_updatePowerSource_decorators = [ifValueChanged];
_updateRvcRunMode_decorators = [ifValueChanged];
_updateRvcCleanMode_decorators = [ifValueChanged];
_updateRvcOperationalState_decorators = [ifValueChanged];
_updateServiceArea_decorators = [ifValueChanged];
__esDecorate(this, null, _updatePowerSource_decorators, { kind: "method", name: "updatePowerSource", static: false, private: false, access: { has: obj => "updatePowerSource" in obj, get: obj => obj.updatePowerSource }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _updateRvcRunMode_decorators, { kind: "method", name: "updateRvcRunMode", static: false, private: false, access: { has: obj => "updateRvcRunMode" in obj, get: obj => obj.updateRvcRunMode }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _updateRvcCleanMode_decorators, { kind: "method", name: "updateRvcCleanMode", static: false, private: false, access: { has: obj => "updateRvcCleanMode" in obj, get: obj => obj.updateRvcCleanMode }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _updateRvcOperationalState_decorators, { kind: "method", name: "updateRvcOperationalState", static: false, private: false, access: { has: obj => "updateRvcOperationalState" in obj, get: obj => obj.updateRvcOperationalState }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _updateServiceArea_decorators, { kind: "method", name: "updateServiceArea", static: false, private: false, access: { has: obj => "updateServiceArea" in obj, get: obj => obj.updateServiceArea }, metadata: _metadata }, null, _instanceExtraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
options = __runInitializers(this, _instanceExtraInitializers);
// Command handlers
behaviorDevice360;
// Start time of the most recent activity
startActive = 0;
// Construct a new endpoint
constructor(log, config, options) {
const definition = [roboticVacuumCleaner, bridgedNode, powerSource];
super(log, config, options, definition);
this.options = options;
// Create the device-specific clusters
createBatteryPowerSourceClusterServer(this, options.powerSource);
createRvcRunModeClusterServer(this);
createRvcCleanModeClusterServer(this, options.rvcCleanMode);
createRvcOperationalStateClusterServer(this);
if (options.supportsMaps)
createServiceAreaClusterServer(this);
// Add a command handler behavior
this.behaviorDevice360 = new BehaviorDevice360(this.log);
this.behaviors.require(Behavior360, { device: this.behaviorDevice360 });
}
// Set a command handler
setCommandHandler360(command, handler) {
this.behaviorDevice360.setCommandHandler(command, handler);
return this;
}
// Update the Power Source cluster attributes when required
async updatePowerSource(attributes) {
const { status, batPercentRemaining, batChargeLevel, batChargeState, activeBatChargeFaults, activeBatFaults } = attributes;
const logBattery = [
formatEnumLog(PowerSource.BatChargeLevel, batChargeLevel),
formatEnumLog(PowerSource.PowerSourceStatus, status),
formatEnumLog(PowerSource.BatChargeState, batChargeState)
];
if (batPercentRemaining !== null)
logBattery.unshift(`${AV}${batPercentRemaining / 2}${RI}%`);
if (activeBatFaults.length) {
const faults = activeBatFaults.map(v => formatEnumLog(PowerSource.BatFault, v));
logBattery.push(`${AN}${plural(faults.length, 'battery fault', false)}${RI} [${formatList(faults)}${RI}]`);
}
if (activeBatChargeFaults.length) {
const faults = activeBatChargeFaults.map(v => formatEnumLog(PowerSource.BatChargeFault, v));
logBattery.push(`${AN}${plural(faults.length, 'charge fault', false)}${RI} [${formatList(faults)}${RI}]`);
}
this.log.info(`${AN}Battery status${RI}: ${formatList(logBattery)}`);
await this.updateAttribute(batteryPowerSourceBehavior, 'status', status, this.log);
await this.updateAttribute(batteryPowerSourceBehavior, 'batPercentRemaining', batPercentRemaining, this.log);
await this.updateAttribute(batteryPowerSourceBehavior, 'batChargeLevel', batChargeLevel, this.log);
await this.updateAttribute(batteryPowerSourceBehavior, 'batChargeState', batChargeState, this.log);
await this.updateAttribute(batteryPowerSourceBehavior, 'activeBatChargeFaults', activeBatChargeFaults, this.log);
await this.updateAttribute(batteryPowerSourceBehavior, 'activeBatFaults', activeBatFaults, this.log);
// Trigger BatFaultChange event if activeBatFaults has changed
const prevActiveBatFaults = (this.changed.prevValues.get('activeBatFaults') ?? []);
if (this.changed.isChanged('activeBatFaults', activeBatFaults)) {
const payload = {
current: activeBatFaults,
previous: prevActiveBatFaults
};
this.log.info(`${AN}Battery Fault Change event${RI}`);
await this.triggerEvent(batteryPowerSourceBehavior, 'batFaultChange', payload, this.log);
}
// Trigger BatChargeFaultChange event if activeBatChargeFaults has changed
const prevActiveBatChargeFaults = (this.changed.prevValues.get('activeBatChargeFaults') ?? []);
if (this.changed.isChanged('activeBatChargeFaults', activeBatChargeFaults)) {
const payload = {
current: activeBatChargeFaults,
previous: prevActiveBatChargeFaults
};
this.log.info(`${AN}Battery Charge Fault Change event${RI}`);
await this.triggerEvent(batteryPowerSourceBehavior, 'batChargeFaultChange', payload, this.log);
}
}
// Update the RVC Run Mode cluster attributes when required
async updateRvcRunMode(runMode) {
this.log.info(`${AN}RVC Run Mode${RI}: ${formatEnumLog(RvcRunMode360, runMode)}`);
await this.updateAttribute(rvcRunModeBehavior, 'currentMode', runMode, this.log);
}
// Update the RVC Clean Mode cluster attributes when required
async updateRvcCleanMode(cleanMode) {
this.log.info(`${AN}RVC Clean Mode${RI}: ${formatEnumLog(RvcCleanMode360, cleanMode)}`);
await this.updateAttribute(rvcCleanModeBehavior, 'currentMode', cleanMode, this.log);
}
// Update the RVC Operational State cluster attributes when required
async updateRvcOperationalState(attributes) {
const { operationalState, operationalError, isActive } = attributes;
this.log.info(`${AN}RVC Operational State${RI}: ${formatEnumLog(RvcOperationalState.OperationalState, operationalState)}`);
await this.updateAttribute(rvcOperationalStateBehavior, 'operationalState', operationalState, this.log);
await this.updateAttribute(rvcOperationalStateBehavior, 'operationalError', operationalError, this.log);
// Trigger OperationCompletion event when changing from active to idle
const { errorStateId, errorStateLabel, errorStateDetails } = operationalError;
const isError = errorStateId !== RvcOperationalState.ErrorState.NoError;
if (this.changed.isChanged('isActive', isActive)) {
if (isActive) {
this.log.info(`(${AN}RVC Operation Started${RI})`);
this.startActive = Date.now();
}
else if (this.startActive) {
const totalOperationalTime = Math.round((Date.now() - this.startActive) / MS);
this.log.info(`${AN}RVC Operation Completion event${RI} in ${AV}${formatSeconds(totalOperationalTime)}${RI}`);
const payload = {
completionErrorCode: errorStateId,
totalOperationalTime
};
await this.triggerEvent(rvcOperationalStateBehavior, 'operationCompletion', payload, this.log);
}
}
// Trigger OperationalError event if there is a new error
if (this.changed.isChanged('operationalError', operationalError)) {
if (isError) {
const errorName = RvcOperationalState.ErrorState[errorStateId];
let logMessage = `${AN}RVC Operational Error event${RI}:`
+ ` ${errorName ? `${AV}${errorName}${RI} (${AV}${errorStateId}${RI})` : `${AV}${errorStateId}${RI}`}`;
if (errorStateLabel)
logMessage += ` [${AV}${errorStateLabel}${RI}]`;
if (errorStateDetails)
logMessage += `: ${AV}${errorStateDetails}${RI}`;
this.log.info(logMessage);
const payload = {
errorState: operationalError
};
await this.triggerEvent(rvcOperationalStateBehavior, 'operationalError', payload, this.log);
}
else {
this.log.info(`${AN}RVC Operational Error${RI}: ${AV}Error cleared${RI}`);
}
}
}
// Update the Service Area cluster attributes when required
async updateServiceArea(attributes) {
if (!this.options.supportsMaps)
return;
const { currentArea, progress, selectedAreas, supportedAreas, supportedMaps } = attributes;
const areaName = (areaId) => formatAreaName(supportedMaps, supportedAreas, areaId);
const progressStatus = progress.map(({ areaId, status }) => `${areaName(areaId)}: ${AV}${ServiceArea.OperationalStatus[status]}${RI} (${AV}${status}${RI})`);
const logMessage = `${AN}Service Area${RI}:`
+ ` ${AV}${plural(supportedMaps.length, 'map')}${RI}, ${AV}${plural(supportedAreas.length, 'area')}${RI},`
+ ` selected [${selectedAreas.map(areaName).join(', ')}],`
+ ` @ ${areaName(currentArea)}, status [${progressStatus.join(', ')}]`;
this.log.info(logMessage);
await this.updateAttribute(serviceAreaBehavior, 'supportedMaps', supportedMaps, this.log);
await this.updateAttribute(serviceAreaBehavior, 'supportedAreas', supportedAreas, this.log);
await this.updateAttribute(serviceAreaBehavior, 'currentArea', currentArea, this.log);
await this.updateAttribute(serviceAreaBehavior, 'progress', progress, this.log);
await this.updateAttribute(serviceAreaBehavior, 'selectedAreas', selectedAreas, this.log);
}
};
})();
export { Endpoint360 };
// Format a Service Area area identifier for logging
export function formatAreaName(supportedMaps, supportedAreas, areaId) {
if (areaId === null)
return `${AV}n/a${RI}`;
const area = supportedAreas.find(a => a.areaId === areaId);
assertIsDefined(area);
assertIsDefined(area.areaInfo.locationInfo);
const map = supportedMaps.find(m => m.mapId === area.mapId);
assertIsDefined(map);
const name = `${map.name}:${area.areaInfo.locationInfo.locationName}`.replaceAll(/\s+/g, '_');
return `${AV}${name}${RI} (${AV}${area.mapId}:${areaId}${RI})`;
}
//# sourceMappingURL=endpoint-360.js.map