UNPKG

homebridge-tsvesync

Version:

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

831 lines 28.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createFilterLifeTestData = exports.createAirQualityTestData = exports.filterLifeScenarios = exports.airQualityScenarios = exports.createMockAirPurifier = exports.createMockDimmer = exports.createMockBulb = exports.createMockFan = exports.createMockLight = exports.createMockSwitch = exports.createMockVeSync = exports.createMockOutlet = exports.simulateNetworkDelay = exports.runAsyncTest = exports.flushPromises = exports.createMockDevice = exports.createMockInfoService = exports.createMockService = exports.createMockRetryManager = exports.createMockPluginLogger = exports.createMockLogger = void 0; const logger_1 = require("../../utils/logger"); const retry_1 = require("../../utils/retry"); const TEST_DEVICE_MIN_KELVIN = 2700; const TEST_DEVICE_MAX_KELVIN = 6500; const percentToKelvin = (percent) => TEST_DEVICE_MIN_KELVIN + ((TEST_DEVICE_MAX_KELVIN - TEST_DEVICE_MIN_KELVIN) * Math.max(0, Math.min(100, percent)) / 100); /** * Creates a mock Logger instance for testing */ const createMockLogger = () => ({ debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), log: jest.fn(), prefix: undefined }); exports.createMockLogger = createMockLogger; /** * Creates a mock PluginLogger instance for testing */ const createMockPluginLogger = (log = (0, exports.createMockLogger)()) => { const logger = new logger_1.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) => message), }; }; exports.createMockPluginLogger = createMockPluginLogger; /** * Creates a mock RetryManager instance for testing */ const createMockRetryManager = () => { const manager = new retry_1.RetryManager((0, exports.createMockLogger)(), { maxRetries: 3, }); return { ...manager, execute: jest.fn(), getRetryCount: jest.fn().mockReturnValue(0), }; }; exports.createMockRetryManager = createMockRetryManager; /** * Creates a mock service instance for testing */ const createMockService = () => ({ getCharacteristic: jest.fn().mockReturnValue({ onSet: jest.fn().mockReturnThis(), onGet: jest.fn().mockReturnThis(), updateValue: jest.fn().mockReturnThis(), setProps: jest.fn().mockReturnThis(), }), setCharacteristic: jest.fn().mockReturnThis(), testCharacteristic: jest.fn().mockReturnValue(false), removeCharacteristic: jest.fn().mockReturnThis(), addCharacteristic: jest.fn().mockReturnValue({ onSet: jest.fn().mockReturnThis(), onGet: jest.fn().mockReturnThis(), updateValue: jest.fn().mockReturnThis(), setProps: jest.fn().mockReturnThis(), }), }); exports.createMockService = createMockService; /** * Creates a mock info service instance for testing */ const createMockInfoService = () => ({ setCharacteristic: jest.fn().mockReturnThis(), }); exports.createMockInfoService = createMockInfoService; /** * Creates a mock device instance for testing */ const createMockDevice = (config = {}) => ({ deviceName: config.deviceName || 'Test Device', uuid: config.uuid || '12345', deviceType: config.deviceType || 'outlet', getDetails: config.getDetails || jest.fn(), }); exports.createMockDevice = createMockDevice; /** * Waits for all promises in the queue to resolve */ const flushPromises = () => new Promise(resolve => setImmediate(resolve)); exports.flushPromises = flushPromises; /** * Helper to run async tests with proper timeout and error handling */ const runAsyncTest = async (testFn, timeout = 5000) => { 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); }); }); }; exports.runAsyncTest = runAsyncTest; /** * Simulates a network delay */ const simulateNetworkDelay = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms)); exports.simulateNetworkDelay = simulateNetworkDelay; /** * Creates a mock outlet instance for testing */ const createMockOutlet = (config = {}) => { 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, }; }; exports.createMockOutlet = createMockOutlet; /** * Creates a mock VeSync client for testing */ const createMockVeSync = () => { 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(), }; }; exports.createMockVeSync = createMockVeSync; /** * Creates a mock switch instance for testing */ const createMockSwitch = (config = {}) => { 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; }), }; return mockSwitch; }; exports.createMockSwitch = createMockSwitch; function createMockLight(options) { 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) => { state.brightness = brightness; mockLight.brightness = state.brightness; return Promise.resolve(true); }); const setColorTemperature = jest.fn().mockImplementation((colorTemp) => { state.colorTemp = colorTemp; mockLight.colorTemp = state.colorTemp; return Promise.resolve(true); }); const setColor = jest.fn().mockImplementation((hue, saturation) => { 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, }; return mockLight; } exports.createMockLight = createMockLight; /** * Creates a mock fan instance for testing */ const createMockFan = (config = {}) => { 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) => { state.speed = speed; mockFan.speed = state.speed; return true; }), setRotationDirection: jest.fn().mockImplementation(async (direction) => { state.rotationDirection = direction; mockFan.rotationDirection = state.rotationDirection; return true; }), setOscillation: jest.fn().mockImplementation(async (enabled) => { state.oscillationState = enabled; mockFan.oscillationState = state.oscillationState; return true; }), setChildLock: jest.fn().mockImplementation(async (enabled) => { state.childLock = enabled; mockFan.childLock = state.childLock; return true; }), setMode: jest.fn().mockImplementation(async (mode) => { state.mode = mode; mockFan.mode = state.mode; return true; }), setSwingMode: jest.fn().mockImplementation(async (enabled) => { state.oscillationState = enabled; mockFan.oscillationState = state.oscillationState; return true; }) }; return mockFan; }; exports.createMockFan = createMockFan; /** * Creates a mock bulb instance for testing */ const createMockBulb = (config = {}) => { var _a, _b, _c, _d; const deviceType = config.deviceType || 'ESL100MC'; const features = new Set(['dimmable']); if (deviceType.includes('CW') || deviceType === 'XYD0001') { features.add('color_temp'); } if (deviceType.includes('MC') || deviceType === 'XYD0001') { features.add('rgb_shift'); } const colorModel = features.has('rgb_shift') ? (deviceType === 'XYD0001' ? 'hsv' : 'rgb') : 'none'; const hsvToRgb = (h, s, v) => { let hue = h % 360; if (hue < 0) { hue += 360; } const saturation = Math.max(0, Math.min(100, s)) / 100; const value = Math.max(0, Math.min(100, v)) / 100; const c = value * saturation; const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)); const m = value - c; let rPrime = 0; let gPrime = 0; let bPrime = 0; if (hue < 60) { rPrime = c; gPrime = x; } else if (hue < 120) { rPrime = x; gPrime = c; } else if (hue < 180) { gPrime = c; bPrime = x; } else if (hue < 240) { gPrime = x; bPrime = c; } else if (hue < 300) { rPrime = x; bPrime = c; } else { rPrime = c; bPrime = x; } return { red: Math.round((rPrime + m) * 255), green: Math.round((gPrime + m) * 255), blue: Math.round((bPrime + m) * 255) }; }; const state = { brightness: (_a = config.brightness) !== null && _a !== void 0 ? _a : 100, colorTempPercent: (_b = config.colorTemp) !== null && _b !== void 0 ? _b : 50, hue: (_c = config.hue) !== null && _c !== void 0 ? _c : 0, saturation: (_d = config.saturation) !== null && _d !== void 0 ? _d : 0, deviceStatus: 'off', value: 100, rgb: { red: 0, green: 0, blue: 0 } }; if (colorModel === 'rgb') { state.rgb = hsvToRgb(state.hue, state.saturation, state.value); } const mockBulb = { deviceName: config.deviceName || 'Test Bulb', deviceType, cid: config.cid || 'test-cid', uuid: config.uuid || 'test-uuid', deviceStatus: state.deviceStatus, brightness: state.brightness, colorTemp: state.colorTempPercent, 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().mockImplementation(async () => { state.deviceStatus = 'on'; mockBulb.deviceStatus = 'on'; return true; }), turnOff: jest.fn().mockImplementation(async () => { state.deviceStatus = 'off'; mockBulb.deviceStatus = 'off'; return true; }), setBrightness: jest.fn().mockImplementation(async (value) => { state.brightness = value; mockBulb.brightness = value; if (value > 0) { state.deviceStatus = 'on'; mockBulb.deviceStatus = 'on'; } return true; }), setColorTemperature: jest.fn().mockImplementation(async (percent) => { state.colorTempPercent = percent; mockBulb.colorTemp = percent; return true; }), setColor: jest.fn().mockImplementation(async (hue, saturation, value = 100) => { state.hue = hue; state.saturation = saturation; state.value = value; mockBulb.hue = hue; mockBulb.saturation = saturation; if (colorModel === 'rgb') { state.rgb = hsvToRgb(hue, saturation, value); } return true; }), hasFeature: jest.fn().mockImplementation((feature) => features.has(feature)), getColorModel: jest.fn().mockImplementation(() => colorModel), getBrightness: jest.fn().mockImplementation(() => state.brightness), getColorTempPercent: jest.fn().mockImplementation(() => state.colorTempPercent), getColorTempKelvin: jest.fn().mockImplementation(() => percentToKelvin(state.colorTempPercent)), getColorHue: jest.fn().mockImplementation(() => state.hue), getColorSaturation: jest.fn().mockImplementation(() => state.saturation), getColorValue: jest.fn().mockImplementation(() => state.value), getRGBValues: jest.fn().mockImplementation(() => state.rgb), }; return mockBulb; }; exports.createMockBulb = createMockBulb; const createMockDimmer = (config = {}) => { var _a, _b, _c, _d; const state = { brightness: (_a = config.brightness) !== null && _a !== void 0 ? _a : 50, deviceStatus: (_b = config.deviceStatus) !== null && _b !== void 0 ? _b : 'off', rgbLightStatus: (_c = config.rgbLightStatus) !== null && _c !== void 0 ? _c : 'off', indicatorLightStatus: (_d = config.indicatorLightStatus) !== null && _d !== void 0 ? _d : 'off', rgbLightValue: { red: 0, green: 0, blue: 0 }, }; const dimmer = { deviceName: config.deviceName || 'Test Dimmer', deviceType: config.deviceType || 'ESWD16', cid: config.cid || 'test-dimmer-cid', uuid: config.uuid || 'test-dimmer-uuid', deviceStatus: state.deviceStatus, brightness: state.brightness, rgbLightStatus: state.rgbLightStatus, indicatorLightStatus: state.indicatorLightStatus, rgbLightValue: state.rgbLightValue, getDetails: jest.fn().mockResolvedValue(true), turnOn: jest.fn().mockImplementation(async () => { state.deviceStatus = 'on'; dimmer.deviceStatus = 'on'; return true; }), turnOff: jest.fn().mockImplementation(async () => { state.deviceStatus = 'off'; dimmer.deviceStatus = 'off'; state.brightness = 0; dimmer.brightness = 0; return true; }), setBrightness: jest.fn().mockImplementation(async (value) => { if (config.failBrightnessOnZero && value === 0) { return false; } state.brightness = value; dimmer.brightness = value; if (value > 0) { state.deviceStatus = 'on'; dimmer.deviceStatus = 'on'; } else { state.deviceStatus = 'off'; dimmer.deviceStatus = 'off'; } return true; }), rgbColorSet: jest.fn().mockImplementation(async (red, green, blue) => { state.rgbLightValue = { red, green, blue }; dimmer.rgbLightValue = state.rgbLightValue; state.rgbLightStatus = 'on'; dimmer.rgbLightStatus = 'on'; return true; }), rgbColorOn: jest.fn().mockImplementation(async () => { state.rgbLightStatus = 'on'; dimmer.rgbLightStatus = 'on'; return true; }), rgbColorOff: jest.fn().mockImplementation(async () => { state.rgbLightStatus = 'off'; dimmer.rgbLightStatus = 'off'; return true; }), indicatorLightOn: jest.fn().mockImplementation(async () => { state.indicatorLightStatus = 'on'; dimmer.indicatorLightStatus = 'on'; return true; }), indicatorLightOff: jest.fn().mockImplementation(async () => { state.indicatorLightStatus = 'off'; dimmer.indicatorLightStatus = 'off'; return true; }), }; return dimmer; }; exports.createMockDimmer = createMockDimmer; /** * Creates a mock air purifier with air quality and filter features */ const createMockAirPurifier = (config = {}) => { const state = { deviceStatus: 'on', speed: 3, mode: 'auto', airQualityValue: config.airQualityValue || 25, pm1: config.pm1 || 15, pm10: config.pm10 || 35, aqPercent: config.aqPercent || 80, filterLife: typeof config.filterLife === 'object' ? config.filterLife.percent : (config.filterLife || 75), }; const mockAirPurifier = { deviceName: config.deviceName || 'Test Air Purifier', deviceType: config.deviceType || 'Core300S', cid: config.cid || 'test-cid', uuid: config.uuid || 'test-uuid', deviceStatus: state.deviceStatus, speed: state.speed, mode: state.mode, maxSpeed: 4, rotationDirection: 'clockwise', oscillationState: false, childLock: false, airQualityValue: state.airQualityValue, pm1: state.pm1, pm10: state.pm10, aqPercent: state.aqPercent, filterLife: state.filterLife, subDeviceNo: 0, isSubDevice: false, deviceRegion: 'US', configModule: 'VeSyncAirBypass', macId: '00:11:22:33:44:55', deviceCategory: 'fan', connectionStatus: 'online', hasFeature: jest.fn().mockImplementation((feature) => { if (feature === 'air_quality') return config.hasAirQuality !== false; if (feature === 'filter_life') return config.hasFilter !== false; return true; }), getDetails: jest.fn().mockImplementation(async () => { return { deviceStatus: state.deviceStatus, speed: state.speed, mode: state.mode, air_quality_value: state.airQualityValue, pm1: state.pm1, pm10: state.pm10, aq_percent: state.aqPercent, filter_life: config.filterLife || state.filterLife }; }), setApiBaseUrl: jest.fn(), turnOn: jest.fn().mockImplementation(async () => { state.deviceStatus = 'on'; mockAirPurifier.deviceStatus = state.deviceStatus; return true; }), turnOff: jest.fn().mockImplementation(async () => { state.deviceStatus = 'off'; mockAirPurifier.deviceStatus = state.deviceStatus; return true; }), changeFanSpeed: jest.fn().mockImplementation(async (speed) => { state.speed = speed; mockAirPurifier.speed = state.speed; return true; }), changeMode: jest.fn().mockImplementation(async (mode) => { state.mode = mode; mockAirPurifier.mode = state.mode; return true; }), setMode: jest.fn().mockImplementation(async (mode) => { state.mode = mode; mockAirPurifier.mode = state.mode; return true; }), setRotationDirection: jest.fn().mockImplementation(async (direction) => { mockAirPurifier.rotationDirection = direction; return true; }), setOscillation: jest.fn().mockImplementation(async (enabled) => { mockAirPurifier.oscillationState = enabled; return true; }), setChildLock: jest.fn().mockImplementation(async (enabled) => { mockAirPurifier.childLock = enabled; return true; }), setSwingMode: jest.fn().mockImplementation(async (enabled) => { mockAirPurifier.oscillationState = enabled; return true; }) }; return mockAirPurifier; }; exports.createMockAirPurifier = createMockAirPurifier; /** * Air quality test scenarios for different conditions */ exports.airQualityScenarios = { excellent: { pm25: 8, pm1: 5, pm10: 12, level: 1, description: 'Excellent air quality' }, good: { pm25: 22, pm1: 15, pm10: 28, level: 2, description: 'Good air quality' }, fair: { pm25: 42, pm1: 30, pm10: 55, level: 3, description: 'Fair air quality' }, poor: { pm25: 85, pm1: 60, pm10: 120, level: 4, description: 'Poor air quality' }, veryPoor: { pm25: 200, pm1: 150, pm10: 300, level: 5, description: 'Very poor air quality' } }; /** * Filter life test scenarios */ exports.filterLifeScenarios = { new: { percent: 100, needsReplacement: false, description: 'New filter' }, good: { percent: 75, needsReplacement: false, description: 'Good filter condition' }, fair: { percent: 50, needsReplacement: false, description: 'Fair filter condition' }, low: { percent: 15, needsReplacement: false, description: 'Low filter life' }, critical: { percent: 8, needsReplacement: true, description: 'Critical filter life' }, empty: { percent: 0, needsReplacement: true, description: 'Filter needs replacement' } }; /** * Creates air quality test data for device mocking */ const createAirQualityTestData = (scenario) => { const data = exports.airQualityScenarios[scenario]; return { air_quality: data.level, air_quality_value: data.pm25, pm1: data.pm1, pm10: data.pm10, aq_percent: Math.max(0, 100 - data.pm25), // Rough approximation }; }; exports.createAirQualityTestData = createAirQualityTestData; /** * Creates filter life test data for device mocking */ const createFilterLifeTestData = (scenario, format = 'number') => { const data = exports.filterLifeScenarios[scenario]; if (format === 'object') { return { filter_life: { percent: data.percent, replace_indicator: data.needsReplacement } }; } return { filter_life: data.percent }; }; exports.createFilterLifeTestData = createFilterLifeTestData; //# sourceMappingURL=test-helpers.js.map