matterbridge-aeg-robot
Version:
A Matterbridge plugin that connects AEG RX 9 / Electrolux Pure i9 robot vacuums to the Matter smart home ecosystem.
134 lines • 6.97 kB
JavaScript
// Matterbridge plugin for AEG RX9 / Electrolux Pure i9 robot vacuum
// Copyright © 2025 Alexander Thoukydides
import { PowerSource } from 'matterbridge/matter/clusters';
import { RX92PowerMode, RX9BatteryStatus, RX9Dustbin, RX9RobotStatus } from './aegapi-rx9-types.js';
import { RvcRunModeRX9, RvcCleanModeRX9, RvcOperationalStateRX9 } from './behavior-rx9.js';
import { RvcOperationalStateError } from './error-rx9.js';
const STATUS_MAP = {
// runMode isSpotClean operationalState
[RX9RobotStatus.Cleaning]: ['Cleaning', false, 'Running'],
[RX9RobotStatus.PausedCleaning]: ['Cleaning', false, 'Paused'],
[RX9RobotStatus.SpotCleaning]: ['Cleaning', true, 'Running'],
[RX9RobotStatus.PausedSpotCleaning]: ['Cleaning', true, 'Paused'],
[RX9RobotStatus.Return]: ['Idle', false, 'SeekingCharger'],
[RX9RobotStatus.PausedReturn]: ['Idle', false, 'Paused'],
[RX9RobotStatus.ReturnForPitstop]: ['Cleaning', false, 'SeekingCharger'],
[RX9RobotStatus.PausedReturnForPitstop]: ['Cleaning', false, 'Paused'],
[RX9RobotStatus.Charging]: ['Idle', false, 'Charging'],
[RX9RobotStatus.Sleeping]: ['Idle', false, 'Docked'],
[RX9RobotStatus.Error]: ['Idle', false, 'Error'],
[RX9RobotStatus.Pitstop]: ['Cleaning', false, 'Charging'],
[RX9RobotStatus.ManualSteering]: ['Idle', false, 'ManualSteering'],
[RX9RobotStatus.FirmwareUpgrade]: ['Idle', false, 'FirmwareUpgrade']
};
function mapStatus(status) {
const [runMode, isSpotClean, operationalState] = STATUS_MAP[status];
return { runMode, isSpotClean, operationalState };
}
// Translation of dynamic appliance information to Matter attributes
export const BABEL_DYNAMIC_RX9 = {
// Version number of the software running on the node
softwareVersion: ({ firmwareVersion }) => {
return firmwareVersion;
},
// Is the appliance reachable?
reachable: ({ connected, enabled, apiError }) => {
return connected && enabled && !apiError;
},
// Battery charge level and charging status
batteryStatus: ({ batteryStatus, isCharging }) => {
const batteryMap = {
// % Status ChargeLevel ChargeState
[RX9BatteryStatus.Dead]: [0, 'Unavailable', 'Critical', 'IsNotCharging'],
[RX9BatteryStatus.CriticalLow]: [20, 'Active', 'Critical', 'IsNotCharging'],
[RX9BatteryStatus.Low]: [40, 'Active', 'Warning', 'IsNotCharging'],
[RX9BatteryStatus.Medium]: [60, 'Active', 'Ok', 'IsNotCharging'],
[RX9BatteryStatus.High]: [80, 'Active', 'Ok', 'IsNotCharging'],
[RX9BatteryStatus.FullyCharged]: [100, 'Active', 'Ok', 'IsAtFullCharge']
};
const [batPercentRemaining, statusEnum, batChargeLevelEnum, batChargeStateEnum] = batteryMap[batteryStatus];
return {
status: PowerSource.PowerSourceStatus[statusEnum],
batPercentRemaining: batPercentRemaining * 2,
batChargeLevel: PowerSource.BatChargeLevel[batChargeLevelEnum],
batChargeState: PowerSource.BatChargeState[isCharging ? 'IsCharging' : batChargeStateEnum]
};
},
// Is the appliance active?
runMode: ({ fauxStatus }) => {
const { runMode } = mapStatus(fauxStatus);
return RvcRunModeRX9[runMode];
},
// Type of cleaning and power level
cleanMode: ({ fauxStatus, powerMode, ecoMode }) => {
const { isSpotClean } = mapStatus(fauxStatus);
powerMode ??= ecoMode === true ? RX92PowerMode.Quiet : RX92PowerMode.Power;
const PowerMap = {
[RX92PowerMode.Quiet]: ['Quiet', 'QuietSpot'],
[RX92PowerMode.Smart]: ['Smart', 'SmartSpot'],
[RX92PowerMode.Power]: ['Power', 'PowerSpot']
};
const cleanMode = PowerMap[powerMode][isSpotClean ? 1 : 0];
return RvcCleanModeRX9[cleanMode];
},
// Current operational state
operationalState: ({ connected, enabled, apiError, fauxStatus, batteryStatus, dustbinStatus, isDocked, messages }) => {
try {
// Start with a direct mapping of the appliance state
let { operationalState } = mapStatus(fauxStatus);
// Cannot trust anything unless the robot is connected
// (these should mark the whole device as non-reachable anyway)
if (apiError instanceof Error)
throw apiError;
else if (apiError)
throw new Error('API error');
else if (!connected)
throw new Error('Not connected to cloud servers');
else if (!enabled)
throw new Error('Appliance not enabled');
// Attempt to identify known error messages
if (messages.some(m => m.type === 0 && (m.userErrorID === 15 || m.internalErrorID === 10005))) {
throw new RvcOperationalStateError.Stuck(); // 'Please help me get free'
}
// If any messages are being reported then pick the most recent
if (messages.length) {
const { text } = messages.reduce((latest, current) => latest.timestamp < current.timestamp ? current : latest);
throw new Error(text);
}
// Report any dustbin issues at low priority only when idle
const { runMode } = mapStatus(fauxStatus);
if (runMode === 'Idle') {
switch (dustbinStatus) {
case RX9Dustbin.Missing: throw new RvcOperationalStateError.DustBinMissing();
case RX9Dustbin.Full: throw new RvcOperationalStateError.DustBinFull();
}
}
// Finally, special behaviour for some robot states
switch (fauxStatus) {
// Use heuristics when the robot is sleeping
case RX9RobotStatus.Sleeping:
if (batteryStatus === RX9BatteryStatus.Dead)
throw new RvcOperationalStateError.FailedToFindChargingDock();
operationalState = isDocked ? 'Docked' : 'Stopped';
break;
// Robot reporting an error, but no obvious cause
case RX9RobotStatus.Error: throw new Error('Unidentified error');
}
// Return the (non-error) operational state
return {
operationalState: RvcOperationalStateRX9[operationalState],
operationalError: RvcOperationalStateError.toStruct(),
isActive: runMode !== 'Idle'
};
}
catch (err) {
// Any error forces the operational state to Error
return {
operationalState: RvcOperationalStateRX9.Error,
operationalError: RvcOperationalStateError.toStruct(err),
isActive: false
};
}
}
};
//# sourceMappingURL=babel-dynamic-rx9.js.map