homebridge-tsvesync
Version:
Homebridge plugin for VeSync devices including Levoit air purifiers, humidifiers, and Etekcity smart outlets
526 lines (490 loc) • 14.5 kB
text/typescript
import { Logger } from 'homebridge';
import { PluginLogger } from '../../utils/logger';
import { RetryManager } from '../../utils/retry';
import { VeSyncOutlet } from '../../types/device.types';
import { VeSync } from 'tsvesync';
import { VeSyncSwitch } from '../../types/device.types';
import { VeSyncBulb } from '../../types/device.types';
import { VeSyncFan } from '../../types/device.types';
/**
* Creates a mock Logger instance for testing
*/
export const createMockLogger = (): jest.Mocked<Logger> => ({
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
log: jest.fn(),
prefix: undefined
});
/**
* Creates a mock PluginLogger instance for testing
*/
export const createMockPluginLogger = (log: Logger = createMockLogger()): jest.Mocked<PluginLogger> => {
const logger = new PluginLogger(log, true);
return {
...logger,
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
stateChange: jest.fn(),
operationStart: jest.fn(),
operationEnd: jest.fn(),
pollingEvent: jest.fn(),
formatMessage: jest.fn((message: string) => message),
} as unknown as jest.Mocked<PluginLogger>;
};
/**
* Creates a mock RetryManager instance for testing
*/
export const createMockRetryManager = (): jest.Mocked<RetryManager> => {
const manager = new RetryManager(createMockLogger(), {
maxRetries: 3,
});
return {
...manager,
execute: jest.fn(),
getRetryCount: jest.fn().mockReturnValue(0),
} as unknown as jest.Mocked<RetryManager>;
};
/**
* Creates a mock service instance for testing
*/
export const createMockService = () => ({
getCharacteristic: jest.fn().mockReturnValue({
onSet: jest.fn(),
onGet: jest.fn(),
updateValue: jest.fn(),
}),
setCharacteristic: jest.fn().mockReturnThis(),
});
/**
* Creates a mock info service instance for testing
*/
export const createMockInfoService = () => ({
setCharacteristic: jest.fn().mockReturnThis(),
});
/**
* Type for mock device configuration
*/
export interface MockDeviceConfig {
deviceName?: string;
uuid?: string;
deviceType?: string;
getDetails?: jest.Mock;
}
/**
* Creates a mock device instance for testing
*/
export const createMockDevice = (config: MockDeviceConfig = {}) => ({
deviceName: config.deviceName || 'Test Device',
uuid: config.uuid || '12345',
deviceType: config.deviceType || 'outlet',
getDetails: config.getDetails || jest.fn(),
});
/**
* Waits for all promises in the queue to resolve
*/
export const flushPromises = () => new Promise(resolve => setImmediate(resolve));
/**
* Helper to run async tests with proper timeout and error handling
*/
export const runAsyncTest = async (
testFn: () => Promise<void>,
timeout: number = 5000
): Promise<void> => {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`Test timed out after ${timeout}ms`));
}, timeout);
testFn()
.then(() => {
clearTimeout(timeoutId);
resolve();
})
.catch((error) => {
clearTimeout(timeoutId);
reject(error);
});
});
};
/**
* Simulates a network delay
*/
export const simulateNetworkDelay = (ms: number = 100): Promise<void> =>
new Promise(resolve => setTimeout(resolve, ms));
/**
* Type for mock outlet configuration
*/
export interface MockOutletConfig {
deviceName?: string;
deviceType?: string;
cid?: string;
uuid?: string;
power?: number;
voltage?: number;
energy?: number;
current?: number;
}
/**
* Creates a mock outlet instance for testing
*/
export const createMockOutlet = (config: MockOutletConfig = {}): jest.Mocked<VeSyncOutlet> => {
const details = {
power: config.power || 0,
voltage: config.voltage || 120,
energy: config.energy || 0,
current: config.current || 0,
};
return {
deviceName: config.deviceName || 'Test Outlet',
deviceType: config.deviceType || 'wifi-switch-1.3',
cid: config.cid || 'test-cid',
uuid: config.uuid || 'test-uuid',
deviceStatus: 'on',
subDeviceNo: 0,
isSubDevice: false,
deviceRegion: 'US',
configModule: 'Outlet',
macId: '00:11:22:33:44:55',
deviceCategory: 'outlet',
connectionStatus: 'online',
getDetails: jest.fn().mockResolvedValue(details),
setApiBaseUrl: jest.fn(),
turnOn: jest.fn().mockResolvedValue(true),
turnOff: jest.fn().mockResolvedValue(true),
...details,
} as unknown as jest.Mocked<VeSyncOutlet>;
};
/**
* Creates a mock VeSync client for testing
*/
export const createMockVeSync = (): jest.Mocked<VeSync> => {
return {
login: jest.fn().mockResolvedValue(true),
getDevices: jest.fn().mockResolvedValue(true),
fans: [],
outlets: [],
switches: [],
bulbs: [],
humidifiers: [],
purifiers: [],
_debug: false,
_redact: false,
_energyUpdateInterval: 0,
_energyCheck: false,
username: 'test@example.com',
password: 'test-password',
token: 'test-token',
accountID: 'test-account',
apiKey: 'test-key',
apiBase: 'test-base',
timezone: 'UTC',
debug: false,
redact: false,
traceSocket: false,
apiUrl: 'test-url',
initialized: true,
setToken: jest.fn(),
setAccountID: jest.fn(),
setAPIKey: jest.fn(),
setAPIBase: jest.fn(),
setInitialized: jest.fn(),
setDevices: jest.fn(),
getDevicesByType: jest.fn(),
getDeviceByUUID: jest.fn(),
getDeviceByCid: jest.fn(),
getDeviceByName: jest.fn(),
getDevicesByCategory: jest.fn(),
getDevicesByMacID: jest.fn(),
getDevicesByDeviceType: jest.fn(),
getDevicesByConfigModule: jest.fn(),
getDevicesByDeviceRegion: jest.fn(),
getDevicesByConnectionStatus: jest.fn(),
} as unknown as jest.Mocked<VeSync>;
};
/**
* Type for mock switch configuration
*/
export interface MockSwitchConfig {
deviceName?: string;
deviceType?: string;
cid?: string;
uuid?: string;
power?: boolean;
}
/**
* Creates a mock switch instance for testing
*/
export const createMockSwitch = (config: MockSwitchConfig = {}): jest.Mocked<VeSyncSwitch> => {
const state = {
power: config.power || false,
deviceStatus: config.power ? 'on' : 'off',
};
const mockSwitch = {
deviceName: config.deviceName || 'Test Switch',
deviceType: config.deviceType || 'ESW01-EU',
cid: config.cid || 'test-cid',
uuid: config.uuid || 'test-uuid',
deviceStatus: state.deviceStatus,
power: state.power,
subDeviceNo: 0,
isSubDevice: false,
deviceRegion: 'US',
configModule: 'Switch',
macId: '00:11:22:33:44:55',
deviceCategory: 'switch',
connectionStatus: 'online',
getDetails: jest.fn().mockImplementation(async () => {
return {
deviceStatus: state.deviceStatus,
power: state.power,
};
}),
setApiBaseUrl: jest.fn(),
turnOn: jest.fn().mockImplementation(async () => {
state.power = true;
state.deviceStatus = 'on';
mockSwitch.power = state.power;
mockSwitch.deviceStatus = state.deviceStatus;
return true;
}),
turnOff: jest.fn().mockImplementation(async () => {
state.power = false;
state.deviceStatus = 'off';
mockSwitch.power = state.power;
mockSwitch.deviceStatus = state.deviceStatus;
return true;
}),
} as unknown as jest.Mocked<VeSyncSwitch>;
return mockSwitch;
};
export interface MockLightOptions {
deviceName: string;
deviceType: string;
cid: string;
uuid: string;
power?: boolean;
brightness?: number;
colorTemp?: number;
hue?: number;
saturation?: number;
subDeviceNo?: number;
isSubDevice?: boolean;
}
export function createMockLight(options: MockLightOptions): VeSyncBulb {
const state = {
deviceStatus: options.power ? 'on' : 'off',
brightness: options.brightness || 100,
colorTemp: options.colorTemp || 140,
hue: options.hue || 0,
saturation: options.saturation || 0,
};
const turnOn = jest.fn().mockImplementation(() => {
state.deviceStatus = 'on';
mockLight.deviceStatus = state.deviceStatus;
return Promise.resolve(true);
});
const turnOff = jest.fn().mockImplementation(() => {
state.deviceStatus = 'off';
mockLight.deviceStatus = state.deviceStatus;
return Promise.resolve(true);
});
const setBrightness = jest.fn().mockImplementation((brightness: number) => {
state.brightness = brightness;
mockLight.brightness = state.brightness;
return Promise.resolve(true);
});
const setColorTemperature = jest.fn().mockImplementation((colorTemp: number) => {
state.colorTemp = colorTemp;
mockLight.colorTemp = state.colorTemp;
return Promise.resolve(true);
});
const setColor = jest.fn().mockImplementation((hue: number, saturation: number) => {
state.hue = hue;
state.saturation = saturation;
mockLight.hue = state.hue;
mockLight.saturation = state.saturation;
return Promise.resolve(true);
});
const getDetails = jest.fn().mockImplementation(() => {
return Promise.resolve({
deviceStatus: state.deviceStatus,
brightness: state.brightness,
colorTemp: state.colorTemp,
hue: state.hue,
saturation: state.saturation,
});
});
const mockLight = {
deviceName: options.deviceName,
deviceType: options.deviceType,
cid: options.cid,
uuid: options.uuid,
deviceStatus: state.deviceStatus,
brightness: state.brightness,
colorTemp: state.colorTemp,
hue: state.hue,
saturation: state.saturation,
subDeviceNo: options.subDeviceNo || 0,
isSubDevice: options.isSubDevice || false,
deviceRegion: 'US',
configModule: 'Light',
macId: '00:11:22:33:44:55',
deviceCategory: 'light',
connectionStatus: 'online',
setApiBaseUrl: jest.fn(),
turnOn,
turnOff,
setBrightness,
setColorTemperature,
setColor,
getDetails,
} as VeSyncBulb;
return mockLight;
}
/**
* Type for mock fan configuration
*/
export interface MockFanConfig {
deviceName?: string;
deviceType?: string;
cid?: string;
uuid?: string;
speed?: number;
rotationDirection?: 'clockwise' | 'counterclockwise';
oscillationState?: boolean;
childLock?: boolean;
mode?: 'normal' | 'auto' | 'sleep' | 'turbo';
}
/**
* Creates a mock fan instance for testing
*/
export const createMockFan = (config: MockFanConfig = {}): jest.Mocked<VeSyncFan> => {
const state = {
deviceStatus: 'on',
speed: config.speed || 3,
rotationDirection: config.rotationDirection || 'clockwise',
oscillationState: config.oscillationState || false,
childLock: config.childLock || false,
mode: config.mode || 'normal'
};
const mockFan = {
deviceName: config.deviceName || 'Test Fan',
deviceType: config.deviceType || 'LTF-F422',
cid: config.cid || 'test-cid',
uuid: config.uuid || 'test-uuid',
deviceStatus: state.deviceStatus,
speed: state.speed,
maxSpeed: 5,
rotationDirection: state.rotationDirection,
oscillationState: state.oscillationState,
childLock: state.childLock,
mode: state.mode,
subDeviceNo: 0,
isSubDevice: false,
deviceRegion: 'US',
configModule: 'Fan',
macId: '00:11:22:33:44:55',
deviceCategory: 'fan',
connectionStatus: 'online',
getDetails: jest.fn().mockImplementation(async () => {
return {
deviceStatus: state.deviceStatus,
speed: state.speed,
rotationDirection: state.rotationDirection,
oscillationState: state.oscillationState,
childLock: state.childLock,
mode: state.mode
};
}),
setApiBaseUrl: jest.fn(),
turnOn: jest.fn().mockImplementation(async () => {
state.deviceStatus = 'on';
mockFan.deviceStatus = state.deviceStatus;
return true;
}),
turnOff: jest.fn().mockImplementation(async () => {
state.deviceStatus = 'off';
mockFan.deviceStatus = state.deviceStatus;
return true;
}),
changeFanSpeed: jest.fn().mockImplementation(async (speed: number) => {
state.speed = speed;
mockFan.speed = state.speed;
return true;
}),
setRotationDirection: jest.fn().mockImplementation(async (direction: 'clockwise' | 'counterclockwise') => {
state.rotationDirection = direction;
mockFan.rotationDirection = state.rotationDirection;
return true;
}),
setOscillation: jest.fn().mockImplementation(async (enabled: boolean) => {
state.oscillationState = enabled;
mockFan.oscillationState = state.oscillationState;
return true;
}),
setChildLock: jest.fn().mockImplementation(async (enabled: boolean) => {
state.childLock = enabled;
mockFan.childLock = state.childLock;
return true;
}),
setMode: jest.fn().mockImplementation(async (mode: 'normal' | 'auto' | 'sleep' | 'turbo') => {
state.mode = mode;
mockFan.mode = state.mode;
return true;
})
} as unknown as jest.Mocked<VeSyncFan>;
return mockFan;
};
/**
* Type for mock bulb configuration
*/
export interface MockBulbConfig {
deviceName?: string;
deviceType?: string;
cid?: string;
uuid?: string;
brightness?: number;
colorTemp?: number;
hue?: number;
saturation?: number;
}
/**
* Creates a mock bulb instance for testing
*/
export const createMockBulb = (config: MockBulbConfig = {}): jest.Mocked<VeSyncBulb> => {
const state = {
brightness: config.brightness || 100,
colorTemp: config.colorTemp || 200,
hue: config.hue || 0,
saturation: config.saturation || 0,
deviceStatus: 'off',
};
return {
deviceName: config.deviceName || 'Test Bulb',
deviceType: config.deviceType || 'ESL100MC',
cid: config.cid || 'test-cid',
uuid: config.uuid || 'test-uuid',
deviceStatus: state.deviceStatus,
brightness: state.brightness,
colorTemp: state.colorTemp,
hue: state.hue,
saturation: state.saturation,
subDeviceNo: 0,
isSubDevice: false,
deviceRegion: 'US',
configModule: 'Bulb',
macId: '00:11:22:33:44:55',
deviceCategory: 'bulb',
connectionStatus: 'online',
getDetails: jest.fn().mockResolvedValue(true),
setApiBaseUrl: jest.fn(),
turnOn: jest.fn().mockResolvedValue(true),
turnOff: jest.fn().mockResolvedValue(true),
setBrightness: jest.fn().mockResolvedValue(true),
setColorTemperature: jest.fn().mockResolvedValue(true),
setColor: jest.fn().mockResolvedValue(true),
} as unknown as jest.Mocked<VeSyncBulb>;
};