UNPKG

homebridge-tsvesync

Version:

Homebridge plugin for VeSync devices including Levoit air purifiers, humidifiers, and Etekcity smart outlets

526 lines (490 loc) 14.5 kB
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>; };