matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
508 lines • 26.6 kB
JavaScript
import { bridgedNode } from 'matterbridge';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createDefaultAdvancedFeature } from '../../model/RoborockPluginPlatformConfig.js';
import { DeviceConfigurator } from '../../platform/deviceConfigurator.js';
import { DeviceModel } from '../../roborockCommunication/models/index.js';
import { RoborockVacuumCleaner } from '../../types/roborockVacuumCleaner.js';
import { asPartial, createMockConfigManager, createMockDeviceRegistry, createMockLogger, createMockRoborockService, } from '../helpers/testUtils.js';
vi.mock('../../core/application/models/index.js', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
RoomMap: {
...actual.RoomMap,
fromMapInfo: vi.fn().mockResolvedValue({
activeMapId: 0,
mapInfo: { maps: [], allRooms: [], hasRooms: false, getActiveMapId: vi.fn().mockReturnValue(0) },
roomMap: { rooms: [], hasRooms: false },
}),
},
};
});
vi.mock('../../share/behaviorFactory.js', () => ({
configureBehavior: vi.fn().mockReturnValue({
onBatteryUpdate: vi.fn(),
onStatusChanged: vi.fn(),
onCleanModeUpdate: vi.fn(),
onServiceAreaUpdate: vi.fn(),
onError: vi.fn(),
onAdditionalProps: vi.fn(),
}),
}));
vi.mock('../../types/roborockVacuumCleaner.js', () => ({
RoborockVacuumCleaner: vi.fn(function (device) {
this.device = device;
this.deviceName = `Roborock-${device.duid}`;
this.serialNumber = device.serialNumber ?? device.duid;
this.configureHandler = vi.fn();
this.getClusterServerOptions = vi.fn().mockReturnValue({
softwareVersion: 1,
softwareVersionString: '1.0.0',
hardwareVersion: 1,
hardwareVersionString: '1.0.0',
deviceTypeList: [],
});
this.createDefaultIdentifyClusterServer = vi.fn();
this.createDefaultBridgedDeviceBasicInformationClusterServer = vi.fn();
this.mode = undefined;
this.deviceTypes = new Map();
this.softwareVersion = 1;
this.softwareVersionString = '1.0.0';
this.hardwareVersion = 1;
this.hardwareVersionString = '1.0.0';
this.vendorName = 'Matterbridge';
this.productName = 'Roborock';
this.vendorId = 65521;
this.productId = 32768;
}),
}));
vi.mock('../../core/domain/entities/Home.js', () => ({
HomeEntity: vi.fn(function () {
this.rawRooms = [];
}),
}));
describe('DeviceConfigurator', () => {
let log;
let configManager;
let registry;
let platformRunner;
let platform;
let roborockService;
let configurator;
let snackbarMessage;
const makeMockDevice = (duid) => asPartial({
duid,
name: `Vacuum-${duid}`,
rrHomeId: 1,
fv: '1.0.0',
specs: asPartial({ model: DeviceModel.S7, firmwareVersion: '1.0.0' }),
store: asPartial({
homeData: { id: 1, name: 'Home', rooms: [], products: [], devices: [], receivedDevices: [] },
}),
});
beforeEach(() => {
log = createMockLogger();
snackbarMessage = vi.fn();
configManager = createMockConfigManager({
isCustomCleanModeMappingEnabled: false,
forceRunAtDefault: false,
overrideMatterConfiguration: false,
});
Object.defineProperty(configManager, 'cleanModeSettings', { get: () => ({}), configurable: true });
platformRunner = asPartial({
updateRobotWithPayload: vi.fn(),
requestHomeData: vi.fn().mockResolvedValue(undefined),
activateHandlerFunctions: vi.fn(),
});
platform = asPartial({
version: '1.0.0',
matterbridge: asPartial({
matterbridgeVersion: '3.5.5',
}),
log: createMockLogger(),
validateDevice: vi.fn().mockReturnValue(true),
setSelectDevice: vi.fn(),
registerDevice: vi.fn().mockResolvedValue(undefined),
onConfigChanged: vi.fn().mockResolvedValue(undefined),
});
roborockService = createMockRoborockService({
setDeviceNotify: vi.fn(),
activateDeviceNotify: vi.fn(),
initializeMessageClientForLocal: vi.fn().mockResolvedValue(true),
});
});
describe('onConfigureDevice', () => {
it('should return early and log error when registry has no devices', async () => {
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(false),
getAllDevices: vi.fn().mockReturnValue([]),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(log.error).toHaveBeenCalledWith('Initializing: No supported devices found');
expect(roborockService.initializeMessageClientForLocal).not.toHaveBeenCalled();
});
it('should configure devices and activate handlers when devices exist', async () => {
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(roborockService.initializeMessageClientForLocal).toHaveBeenCalledWith(device);
expect(platformRunner.requestHomeData).toHaveBeenCalled();
expect(platformRunner.activateHandlerFunctions).toHaveBeenCalled();
});
it('should set rrHomeId when device is configured successfully', async () => {
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(configurator.rrHomeId).toBe(device.rrHomeId);
});
it('should activate notify for successfully configured robots', async () => {
const device = makeMockDevice('duid-1');
const robot = asPartial({ device });
const robotsMap = new Map([['duid-1', robot]]);
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap,
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(roborockService.activateDeviceNotify).toHaveBeenCalledWith(device);
});
it('should handle requestHomeData failure gracefully', async () => {
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
platformRunner = asPartial({
updateRobotWithPayload: vi.fn(),
requestHomeData: vi.fn().mockRejectedValue(new Error('Network error')),
activateHandlerFunctions: vi.fn(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('requestHomeData (initial) failed'));
expect(platformRunner.activateHandlerFunctions).toHaveBeenCalled();
});
it('should log warn and continue when device fails to connect to local network', async () => {
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
roborockService = createMockRoborockService({
setDeviceNotify: vi.fn(),
activateDeviceNotify: vi.fn(),
initializeMessageClientForLocal: vi.fn().mockResolvedValue(false),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('could not connect to local network'));
expect(configurator.rrHomeId).toBe(device.rrHomeId);
});
it('should activate notify for device configured in MQTT-only mode', async () => {
const device = makeMockDevice('duid-1');
const robot = asPartial({ device });
const robotsMap = new Map([['duid-1', robot]]);
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap,
});
roborockService = createMockRoborockService({
setDeviceNotify: vi.fn(),
activateDeviceNotify: vi.fn(),
initializeMessageClientForLocal: vi.fn().mockResolvedValue(false),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(roborockService.activateDeviceNotify).toHaveBeenCalledWith(device);
});
it('should fetch serialNumber via getSerialNumber when device has no serialNumber', async () => {
const device = makeMockDevice('duid-1'); // no serialNumber set
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(roborockService.getSerialNumber).toHaveBeenCalledWith(device.duid);
});
it('should call snackbarMessage when device fails to connect to local network', async () => {
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
roborockService = createMockRoborockService({
setDeviceNotify: vi.fn(),
activateDeviceNotify: vi.fn(),
initializeMessageClientForLocal: vi.fn().mockResolvedValue(false),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(snackbarMessage).toHaveBeenCalledWith(expect.stringContaining(device.name), 5000, 'error');
});
});
describe('applyVersionInfo (via addDevice)', () => {
it('should call registerDevice when serialNumber and deviceName are set', async () => {
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
it('should set softwareVersionString to Unknown when platform version is empty string', async () => {
const emptyVersionPlatform = asPartial({
version: '',
matterbridge: asPartial({
matterbridgeVersion: '3.5.5',
}),
log: createMockLogger(),
validateDevice: vi.fn().mockReturnValue(true),
setSelectDevice: vi.fn(),
registerDevice: vi.fn().mockResolvedValue(undefined),
onConfigChanged: vi.fn().mockResolvedValue(undefined),
});
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(emptyVersionPlatform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(emptyVersionPlatform.registerDevice).toHaveBeenCalled();
});
});
describe('overrideMatterConfiguration (via addDevice)', () => {
const mockMatterOverride = {
matterVendorName: 'TestVendor',
matterProductName: 'TestProduct',
matterVendorId: 1234,
matterProductId: 5678,
};
beforeEach(() => {
configManager = asPartial({
overrideMatterConfiguration: true,
matterOverrideSettings: mockMatterOverride,
getProductNameForDevice: vi.fn().mockReturnValue(''),
ensureDeviceProductNameEntry: vi.fn().mockReturnValue(false),
rawConfig: {},
});
});
it('should call onConfigChanged when ensureDeviceProductNameEntry returns true', async () => {
configManager = asPartial({
...configManager,
ensureDeviceProductNameEntry: vi.fn().mockReturnValue(true),
});
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.onConfigChanged).toHaveBeenCalled();
});
it('should not call onConfigChanged when ensureDeviceProductNameEntry returns false', async () => {
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.onConfigChanged).not.toHaveBeenCalled();
});
it('should use Matterbridge as vendorName when matterVendorName is empty', async () => {
configManager = asPartial({
...configManager,
matterOverrideSettings: { ...mockMatterOverride, matterVendorName: '' },
});
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
it('should use perDeviceProductName when it is non-empty', async () => {
configManager = asPartial({
...configManager,
getProductNameForDevice: vi.fn().mockReturnValue('PerDeviceName'),
});
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
it('should use vacuumData.model as productName when both perDeviceName and matterProductName are empty', async () => {
configManager = asPartial({
...configManager,
matterOverrideSettings: { ...mockMatterOverride, matterProductName: '' },
});
const device = makeMockDevice('duid-1');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
});
describe('addDevice edge cases', () => {
it('should return undefined and log warn when rvc has no serialNumber', async () => {
const device = makeMockDevice('duid-no-serial');
vi.mocked(RoborockVacuumCleaner).mockImplementationOnce(function () {
Object.assign(this, asPartial({
device,
deviceName: `Roborock-${device.duid}`,
serialNumber: undefined,
configureHandler: vi.fn(),
getClusterServerOptions: vi.fn().mockReturnValue(null),
createDefaultIdentifyClusterServer: vi.fn(),
createDefaultBridgedDeviceBasicInformationClusterServer: vi.fn(),
mode: undefined,
deviceTypes: new Map(),
}));
});
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('Cannot add device: missing rvc or deviceName'));
expect(platform.registerDevice).not.toHaveBeenCalled();
});
it('should skip BridgedDeviceBasicInformation cluster options when getClusterServerOptions returns null', async () => {
const device = makeMockDevice('duid-null-options');
vi.mocked(RoborockVacuumCleaner).mockImplementationOnce(function () {
Object.assign(this, asPartial({
device,
deviceName: `Roborock-${device.duid}`,
serialNumber: device.duid,
configureHandler: vi.fn(),
getClusterServerOptions: vi.fn().mockReturnValue(null),
createDefaultIdentifyClusterServer: vi.fn(),
createDefaultBridgedDeviceBasicInformationClusterServer: vi.fn(),
mode: undefined,
deviceTypes: new Map(),
}));
});
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
it('should return undefined when validateDevice returns false', async () => {
const validateFalsePlatform = asPartial({
...platform,
validateDevice: vi.fn().mockReturnValue(false),
});
const device = makeMockDevice('duid-no-validate');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(validateFalsePlatform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(validateFalsePlatform.registerDevice).not.toHaveBeenCalled();
});
it('should use matterbridgeVersion as hardwareVersionString when firmwareVersion and fv are both undefined', async () => {
const device = asPartial({
duid: 'duid-no-fw',
name: 'Vacuum-duid-no-fw',
rrHomeId: 1,
fv: undefined,
specs: asPartial({ model: DeviceModel.S7, firmwareVersion: undefined }),
store: asPartial({
homeData: { id: 1, name: 'Home', rooms: [], products: [], devices: [], receivedDevices: [] },
}),
});
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
it('should use default vendorId and productId when matterVendorId and matterProductId are 0', async () => {
configManager = asPartial({
overrideMatterConfiguration: true,
matterOverrideSettings: {
matterVendorName: 'Vendor',
matterProductName: 'Product',
matterVendorId: 0,
matterProductId: 0,
},
getProductNameForDevice: vi.fn().mockReturnValue(''),
ensureDeviceProductNameEntry: vi.fn().mockReturnValue(false),
rawConfig: {},
isCustomCleanModeMappingEnabled: false,
forceRunAtDefault: false,
cleanModeSettings: createDefaultAdvancedFeature().settings.cleanModeSettings,
});
const device = makeMockDevice('duid-default-ids');
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
it('should not duplicate bridgedNode in deviceTypeList when it is already present', async () => {
const device = makeMockDevice('duid-dup-bridged');
vi.mocked(RoborockVacuumCleaner).mockImplementationOnce(function () {
Object.assign(this, asPartial({
device,
deviceName: `Roborock-${device.duid}`,
serialNumber: device.duid,
configureHandler: vi.fn(),
getClusterServerOptions: vi.fn().mockReturnValue({
softwareVersion: 1,
softwareVersionString: '1.0.0',
hardwareVersion: 1,
hardwareVersionString: '1.0.0',
deviceTypeList: [{ deviceType: bridgedNode.code, revision: bridgedNode.revision }],
}),
createDefaultIdentifyClusterServer: vi.fn(),
createDefaultBridgedDeviceBasicInformationClusterServer: vi.fn(),
mode: undefined,
deviceTypes: new Map(),
}));
});
registry = createMockDeviceRegistry({
hasDevices: vi.fn().mockReturnValue(true),
getAllDevices: vi.fn().mockReturnValue([device]),
robotsMap: new Map(),
});
configurator = new DeviceConfigurator(platform, configManager, registry, () => platformRunner, snackbarMessage, log);
await configurator.onConfigureDevice(roborockService);
expect(platform.registerDevice).toHaveBeenCalled();
});
});
});
//# sourceMappingURL=deviceConfigurator.test.js.map