matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
518 lines (434 loc) • 21.7 kB
text/typescript
import { AnsiLogger } from 'matterbridge/logger';
import { ServiceArea } from 'matterbridge/matter/clusters';
import RoborockService from '../roborockService';
import { MessageProcessor } from '../roborockCommunication/broadcast/messageProcessor';
import { Device, MultipleMap, RequestMessage } from '../roborockCommunication';
import { RoomIndexMap } from '../model/roomIndexMap';
describe('RoborockService - startClean', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
let mockMessageProcessor: jest.Mocked<MessageProcessor>;
let mockLoginApi: any;
let mockMapInfo: any;
let mockMessageClient: any;
let mockIotApi: any;
beforeEach(() => {
mockLogger = {
debug: jest.fn(),
notice: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
} as any;
mockMessageProcessor = {
startClean: jest.fn(),
startRoomClean: jest.fn(),
} as any;
mockLoginApi = {
loginWithPassword: jest.fn(),
loginWithUserData: jest.fn(),
};
mockMapInfo = jest.fn();
roborockService = new RoborockService(() => mockLoginApi, jest.fn(), 10, {} as any, mockLogger);
roborockService['auth'] = jest.fn((ud) => ud);
roborockService['messageProcessorMap'] = new Map<string, MessageProcessor>([['test-duid', mockMessageProcessor]]);
mockIotApi = { getCustom: jest.fn() };
roborockService['iotApi'] = mockIotApi;
});
it('should return result from iotApi.getCustom', async () => {
mockIotApi.getCustom.mockResolvedValue({ foo: 'bar' });
const result = await roborockService.getCustomAPI('http://test');
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - getCustomAPI', 'http://test');
expect(result).toEqual({ foo: 'bar' });
});
it('should log error and return error object if iotApi.getCustom throws', async () => {
mockIotApi.getCustom.mockRejectedValue(new Error('fail'));
const result = await roborockService.getCustomAPI('http://test');
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('Failed to get custom API with url http://test:'));
expect(result).toEqual({ result: undefined, error: expect.stringContaining('Failed to get custom API with url http://test') });
});
it('should return MapInfo if response contains maps', async () => {
const mapData = [
{
map_info: [
{
rooms: [{ id: 1, iot_name_id: 'room1', tag: 0, iot_name: 'Living Room' }],
mapFlag: 1,
name: 'Living Room Map',
},
],
},
] as MultipleMap[];
mockMessageClient = {
get: jest.fn(),
};
roborockService.messageClient = mockMessageClient;
mockMessageClient.get.mockResolvedValue(mapData);
mockMapInfo.mockImplementation((data) => ({ map: data }));
const result = await roborockService.getMapInformation('duid');
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - getMapInformation', 'duid');
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - getMapInformation response', expect.anything());
expect(result?.maps.length).toEqual(1);
});
it('should return undefined if response is empty', async () => {
mockMessageClient = { get: jest.fn() };
mockMessageClient.get.mockResolvedValue(undefined);
roborockService.messageClient = mockMessageClient;
const result = await roborockService.getMapInformation('duid');
expect(result).toBeUndefined();
});
it('should return vacuumRoom if present', async () => {
roborockService.customGet = jest.fn();
(roborockService.customGet as jest.Mock).mockResolvedValue({ vacuumRoom: 42 });
const result = await roborockService.getRoomIdFromMap('duid');
expect(roborockService.customGet).toHaveBeenCalledWith('duid', expect.any(Object));
expect(result).toBe(42);
});
it('should return undefined if vacuumRoom is not present', async () => {
roborockService.customGet = jest.fn();
(roborockService.customGet as jest.Mock).mockResolvedValue({});
const result = await roborockService.getRoomIdFromMap('duid');
expect(result).toBeUndefined();
});
it('should login with password if no saved user data', async () => {
const username = 'user';
const password = 'pass';
const userData = { foo: 'bar' };
mockLoginApi.loginWithPassword.mockResolvedValue(userData);
const loadSavedUserData = jest.fn().mockResolvedValue(undefined);
const savedUserData = jest.fn().mockResolvedValue(undefined);
const result = await roborockService.loginWithPassword(username, password, loadSavedUserData, savedUserData);
expect(mockLogger.debug).toHaveBeenCalledWith('No saved user data found, logging in with password');
expect(mockLoginApi.loginWithPassword).toHaveBeenCalledWith(username, password);
expect(savedUserData).toHaveBeenCalledWith(userData);
expect(roborockService['auth']).toHaveBeenCalledWith(userData);
expect(result).toBe(userData);
});
it('should login with user data if saved user data exists', async () => {
const username = 'user';
const password = 'pass';
const userData = { foo: 'bar' };
mockLoginApi.loginWithUserData.mockResolvedValue(userData);
const loadSavedUserData = jest.fn().mockResolvedValue(userData);
const savedUserData = jest.fn();
const result = await roborockService.loginWithPassword(username, password, loadSavedUserData, savedUserData);
expect(mockLogger.debug).toHaveBeenCalledWith('Using saved user data for login', expect.anything());
expect(mockLoginApi.loginWithUserData).toHaveBeenCalledWith(username, userData);
expect(roborockService['auth']).toHaveBeenCalledWith(userData);
expect(result).toBe(userData);
});
it('should start global clean when no areas or selected areas are provided', async () => {
const duid = 'test-duid';
roborockService['supportedAreas'].set(duid, []);
roborockService['selectedAreas'].set(duid, []);
await roborockService.startClean(duid);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - startGlobalClean');
expect(mockMessageProcessor.startClean).toHaveBeenCalledWith(duid);
});
it('should start room clean when selected areas match supported areas', async () => {
const duid = 'test-duid';
const supportedAreas: ServiceArea.Area[] = [
{ areaId: 1, mapId: null, areaInfo: {} as any },
{ areaId: 2, mapId: null, areaInfo: {} as any },
];
const selectedAreas = [1];
roborockService['supportedAreas'].set(duid, supportedAreas);
roborockService['selectedAreas'].set(duid, selectedAreas);
await roborockService.startClean(duid);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - startRoomClean', expect.anything());
expect(mockMessageProcessor.startRoomClean).toHaveBeenCalledWith(duid, selectedAreas, 1);
});
it('should start global clean when all selected areas match all supported areas', async () => {
const duid = 'test-duid';
const supportedAreas: ServiceArea.Area[] = [
{ areaId: 1, mapId: null, areaInfo: {} as any },
{ areaId: 2, mapId: null, areaInfo: {} as any },
];
const selectedAreas = [1, 2];
roborockService['supportedAreas'].set(duid, supportedAreas);
roborockService['selectedAreas'].set(duid, selectedAreas);
await roborockService.startClean(duid);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - startGlobalClean');
expect(mockMessageProcessor.startClean).toHaveBeenCalledWith(duid);
});
it('should start scene when a routine is selected', async () => {
const duid = 'test-duid';
const supportedAreas: ServiceArea.Area[] = [
{ areaId: 1, mapId: null, areaInfo: {} as any },
{ areaId: 2, mapId: null, areaInfo: {} as any },
];
const supportedRoutines: ServiceArea.Area[] = [{ areaId: 99, mapId: null, areaInfo: {} as any }];
const selectedAreas = [99];
roborockService['supportedAreas'].set(duid, supportedAreas);
roborockService['supportedRoutines'].set(duid, supportedRoutines);
roborockService['selectedAreas'].set(duid, selectedAreas);
roborockService['iotApi'] = {
startScene: jest.fn(),
} as any;
await roborockService.startClean(duid);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - startScene', expect.anything());
expect(roborockService['iotApi']!.startScene).toHaveBeenCalledWith(99);
});
it('should warn when multiple routines are selected', async () => {
const duid = 'test-duid';
const supportedAreas: ServiceArea.Area[] = [
{ areaId: 1, mapId: null, areaInfo: {} as any },
{ areaId: 2, mapId: null, areaInfo: {} as any },
];
const supportedRoutines: ServiceArea.Area[] = [
{ areaId: 99, mapId: null, areaInfo: {} as any },
{ areaId: 100, mapId: null, areaInfo: {} as any },
];
const selectedAreas = [99, 100];
roborockService['supportedAreas'].set(duid, supportedAreas);
roborockService['supportedRoutines'].set(duid, supportedRoutines);
roborockService['selectedAreas'].set(duid, selectedAreas);
await roborockService.startClean(duid);
expect(mockLogger.warn).toHaveBeenCalledWith('RoborockService - Multiple routines selected, which is not supported.', expect.anything());
});
it('should start global clean if all selected rooms match supportedRooms even with routines defined', async () => {
const duid = 'test-duid';
const supportedAreas: ServiceArea.Area[] = [
{ areaId: 1, mapId: null, areaInfo: {} as any },
{ areaId: 2, mapId: null, areaInfo: {} as any },
];
const supportedRoutines: ServiceArea.Area[] = [{ areaId: 99, mapId: null, areaInfo: {} as any }];
const selectedAreas = [1, 2];
roborockService['supportedAreas'].set(duid, supportedAreas);
roborockService['supportedRoutines'].set(duid, supportedRoutines);
roborockService['selectedAreas'].set(duid, selectedAreas);
await roborockService.startClean(duid);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - startGlobalClean');
expect(mockMessageProcessor.startClean).toHaveBeenCalledWith(duid);
});
it('should start room clean if only rooms are selected and not all rooms', async () => {
const duid = 'test-duid';
const supportedAreas: ServiceArea.Area[] = [
{ areaId: 1, mapId: null, areaInfo: {} as any },
{ areaId: 2, mapId: null, areaInfo: {} as any },
{ areaId: 3, mapId: null, areaInfo: {} as any },
];
const supportedRoutines: ServiceArea.Area[] = [{ areaId: 99, mapId: null, areaInfo: {} as any }];
const selectedAreas = [1, 3];
roborockService['supportedAreas'].set(duid, supportedAreas);
roborockService['supportedRoutines'].set(duid, supportedRoutines);
roborockService['selectedAreas'].set(duid, selectedAreas);
await roborockService.startClean(duid);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - startRoomClean', expect.anything());
expect(mockMessageProcessor.startRoomClean).toHaveBeenCalledWith(duid, [1, 3], 1);
});
it('should initialize and store MessageProcessor for the given duid', () => {
const duid = 'test-duid';
roborockService.initializeMessageClientForLocal({ duid } as Device);
const storedProcessor = roborockService['messageProcessorMap'].get(duid);
expect(storedProcessor).not.toBeUndefined();
});
});
describe('RoborockService - basic setters/getters', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
beforeEach(() => {
mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
});
it('setSelectedAreas should set selected areas', () => {
roborockService.setSupportedAreaIndexMap(
'duid',
new RoomIndexMap(
new Map([
[1, { roomId: 1, mapId: 0 }],
[2, { roomId: 2, mapId: 1 }],
]),
),
);
roborockService.setSelectedAreas('duid', [1, 2]);
expect(roborockService['selectedAreas'].get('duid')).toEqual([1, 2]);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - setSelectedAreas', [1, 2]);
});
it('setSupportedAreas should set supported areas', () => {
const areas = [{ areaId: 1, mapId: null, areaInfo: {} as any }];
roborockService.setSupportedAreas('duid', areas);
expect(roborockService['supportedAreas'].get('duid')).toEqual(areas);
});
it('setSupportedScenes should set supported routines', () => {
const routines = [{ areaId: 99, mapId: null, areaInfo: {} as any }];
roborockService.setSupportedScenes('duid', routines);
expect(roborockService['supportedRoutines'].get('duid')).toEqual(routines);
});
it('getSupportedAreas should return supported areas', () => {
const areas = [{ areaId: 1, mapId: null, areaInfo: {} as any }];
roborockService['supportedAreas'].set('duid', areas);
expect(roborockService.getSupportedAreas('duid')).toEqual(areas);
});
});
describe('RoborockService - getMessageProcessor', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
let mockMessageProcessor: jest.Mocked<MessageProcessor>;
beforeEach(() => {
mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
mockMessageProcessor = { startClean: jest.fn() } as any;
roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
});
it('should return processor if present', () => {
roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
expect(roborockService.getMessageProcessor('duid')).toBe(mockMessageProcessor);
});
it('should log error if processor not present', () => {
expect(roborockService.getMessageProcessor('unknown')).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('MessageApi is not initialized.');
});
});
describe('RoborockService - getCleanModeData', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
let mockMessageProcessor: jest.Mocked<MessageProcessor>;
beforeEach(() => {
mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
mockMessageProcessor = { getCleanModeData: jest.fn() } as any;
roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
});
it('should return clean mode data', async () => {
mockMessageProcessor.getCleanModeData.mockResolvedValue({ suctionPower: 1, waterFlow: 2, distance_off: 3, mopRoute: 4 });
const result = await roborockService.getCleanModeData('duid');
expect(result).toEqual({ suctionPower: 1, waterFlow: 2, distance_off: 3, mopRoute: 4 });
expect(mockLogger.notice).toHaveBeenCalledWith('RoborockService - getCleanModeData');
});
});
describe('RoborockService - changeCleanMode', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
let mockMessageProcessor: jest.Mocked<MessageProcessor>;
beforeEach(() => {
mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
mockMessageProcessor = { changeCleanMode: jest.fn() } as any;
roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
});
it('should call changeCleanMode on processor', async () => {
await roborockService.changeCleanMode('duid', { suctionPower: 1, waterFlow: 2, distance_off: 3, mopRoute: 4 });
expect(mockLogger.notice).toHaveBeenCalledWith('RoborockService - changeCleanMode');
expect(mockMessageProcessor.changeCleanMode).toHaveBeenCalledWith('duid', 1, 2, 4, 3);
});
});
describe('RoborockService - pause/stop/resume/playSound', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
let mockMessageProcessor: jest.Mocked<MessageProcessor>;
beforeEach(() => {
mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
mockMessageProcessor = {
pauseClean: jest.fn(),
gotoDock: jest.fn(),
resumeClean: jest.fn(),
findMyRobot: jest.fn(),
} as any;
roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
});
it('pauseClean should call processor and log', async () => {
await roborockService.pauseClean('duid');
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - pauseClean');
expect(mockMessageProcessor.pauseClean).toHaveBeenCalledWith('duid');
});
it('stopAndGoHome should call processor and log', async () => {
await roborockService.stopAndGoHome('duid');
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - stopAndGoHome');
expect(mockMessageProcessor.gotoDock).toHaveBeenCalledWith('duid');
});
it('resumeClean should call processor and log', async () => {
await roborockService.resumeClean('duid');
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - resumeClean');
expect(mockMessageProcessor.resumeClean).toHaveBeenCalledWith('duid');
});
it('playSoundToLocate should call processor and log', async () => {
await roborockService.playSoundToLocate('duid');
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - findMe');
expect(mockMessageProcessor.findMyRobot).toHaveBeenCalledWith('duid');
});
});
describe('RoborockService - customGet/customGetInSecure/customSend', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
let mockMessageProcessor: jest.Mocked<MessageProcessor>;
beforeEach(() => {
mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
mockMessageProcessor = {
getCustomMessage: jest.fn(),
sendCustomMessage: jest.fn(),
} as any;
roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
});
it('customGet should call getCustomMessage', async () => {
mockMessageProcessor.getCustomMessage.mockResolvedValue('result');
const result = await roborockService.customGet('duid', { method: 'method', params: undefined, secure: true } as RequestMessage);
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customSend-message', 'method', undefined, true);
expect(mockMessageProcessor.getCustomMessage).toHaveBeenCalledWith('duid', expect.any(Object));
expect(result).toBe('result');
});
it('customSend should call sendCustomMessage', async () => {
const req = { foo: 'bar' } as any;
await roborockService.customSend('duid', req);
expect(mockMessageProcessor.sendCustomMessage).toHaveBeenCalledWith('duid', req);
});
});
describe('RoborockService - stopService', () => {
let roborockService: RoborockService;
let mockLogger: AnsiLogger;
let mockMessageClient: any;
let mockLocalClient: any;
let mockMessageProcessor: any;
beforeEach(() => {
mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
mockMessageClient = { disconnect: jest.fn() };
mockLocalClient = { disconnect: jest.fn(), isConnected: jest.fn() };
mockMessageProcessor = {};
roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
roborockService.messageClient = mockMessageClient;
roborockService.localClientMap.set('duid', mockLocalClient);
roborockService.messageProcessorMap.set('duid', mockMessageProcessor);
roborockService.requestDeviceStatusInterval = setInterval(() => {
jest.fn();
}, 1000);
});
afterEach(() => {
jest.clearAllTimers();
});
it('should disconnect messageClient, localClient, remove processors, clear interval', () => {
roborockService.stopService();
expect(mockMessageClient.disconnect).toHaveBeenCalled();
expect(mockLocalClient.disconnect).toHaveBeenCalled();
expect(roborockService.localClientMap.size).toBe(0);
expect(roborockService.messageProcessorMap.size).toBe(0);
expect(roborockService.requestDeviceStatusInterval).toBeUndefined();
});
});
describe('RoborockService - setDeviceNotify', () => {
it('should set deviceNotify callback', () => {
const roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, {} as any);
const cb = jest.fn();
roborockService.setDeviceNotify(cb);
expect(roborockService.deviceNotify).toBe(cb);
});
});
describe('RoborockService - sleep', () => {
it('should resolve after ms', async () => {
const roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, {} as any);
const start = Date.now();
await roborockService['sleep'](100);
expect(Date.now() - start).toBeGreaterThanOrEqual(0);
});
});
describe('RoborockService - auth', () => {
it('should set userdata and iotApi', () => {
const mockLogger = {} as any;
const mockIotApiFactory = jest.fn().mockReturnValue('iotApi');
const roborockService = new RoborockService(jest.fn(), mockIotApiFactory, 10, {} as any, mockLogger);
const userData = { foo: 'bar' } as any;
const result = roborockService['auth'](userData);
expect(roborockService['userdata']).toBe(userData);
expect(roborockService['iotApi']).toBe('iotApi');
expect(result).toBe(userData);
});
});