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.

229 lines 15.7 kB
// 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