UNPKG

homebridge-tsvesync

Version:

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

966 lines (904 loc) 27.6 kB
import { Logger } from 'homebridge'; import { PluginLogger } from '../../utils/logger'; import { RetryManager } from '../../utils/retry'; import { VeSyncOutlet, VeSyncSwitch, VeSyncBulb, VeSyncFan, VeSyncDimmerSwitch } from '../../types/device.types'; import { VeSync } from 'tsvesync'; const TEST_DEVICE_MIN_KELVIN = 2700; const TEST_DEVICE_MAX_KELVIN = 6500; const percentToKelvin = (percent: number): number => 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 */ 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().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(), }), }); /** * 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), update: jest.fn().mockResolvedValue(undefined), 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; }), setSwingMode: jest.fn().mockImplementation(async (enabled: boolean) => { state.oscillationState = enabled; mockFan.oscillationState = state.oscillationState; 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 deviceType = config.deviceType || 'ESL100MC'; const features = new Set<string>(['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: number, s: number, v: number): { red: number; green: number; blue: number } => { 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: config.brightness ?? 100, colorTempPercent: config.colorTemp ?? 50, hue: config.hue ?? 0, saturation: config.saturation ?? 0, deviceStatus: 'off' as 'on' | 'off', value: 100, rgb: { red: 0, green: 0, blue: 0 } }; if (colorModel === 'rgb') { state.rgb = hsvToRgb(state.hue, state.saturation, state.value); } const mockBulb: any = { 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: number) => { state.brightness = value; mockBulb.brightness = value; if (value > 0) { state.deviceStatus = 'on'; mockBulb.deviceStatus = 'on'; } return true; }), setColorTemperature: jest.fn().mockImplementation(async (percent: number) => { state.colorTempPercent = percent; mockBulb.colorTemp = percent; return true; }), setColor: jest.fn().mockImplementation(async (hue: number, saturation: number, value: number = 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: string) => 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 as jest.Mocked<VeSyncBulb>; }; export interface MockDimmerConfig { deviceName?: string; deviceType?: string; cid?: string; uuid?: string; brightness?: number; deviceStatus?: 'on' | 'off'; rgbLightStatus?: 'on' | 'off'; indicatorLightStatus?: 'on' | 'off'; failBrightnessOnZero?: boolean; } export const createMockDimmer = (config: MockDimmerConfig = {}): jest.Mocked<VeSyncDimmerSwitch> => { const state = { brightness: config.brightness ?? 50, deviceStatus: config.deviceStatus ?? 'off', rgbLightStatus: config.rgbLightStatus ?? 'off', indicatorLightStatus: config.indicatorLightStatus ?? 'off', rgbLightValue: { red: 0, green: 0, blue: 0 }, }; const dimmer: Partial<VeSyncDimmerSwitch> = { 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: number) => { 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: number, green: number, blue: number) => { 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 as jest.Mocked<VeSyncDimmerSwitch>; }; /** * Type for mock air purifier configuration */ export interface MockAirPurifierConfig { deviceName?: string; deviceType?: string; cid?: string; uuid?: string; airQualityValue?: number; pm1?: number; pm10?: number; aqPercent?: number; filterLife?: number | { percent: number }; hasAirQuality?: boolean; hasFilter?: boolean; } /** * Creates a mock air purifier with air quality and filter features */ export const createMockAirPurifier = (config: MockAirPurifierConfig = {}): jest.Mocked<VeSyncFan> => { const state = { deviceStatus: 'on', speed: 3, mode: 'auto' as 'normal' | 'auto' | 'sleep' | 'turbo', 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' as 'clockwise' | 'counterclockwise', 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: string) => { 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: number) => { state.speed = speed; mockAirPurifier.speed = state.speed; return true; }), changeMode: jest.fn().mockImplementation(async (mode: 'normal' | 'auto' | 'sleep' | 'turbo') => { state.mode = mode; mockAirPurifier.mode = state.mode; return true; }), setMode: jest.fn().mockImplementation(async (mode: 'normal' | 'auto' | 'sleep' | 'turbo') => { state.mode = mode; mockAirPurifier.mode = state.mode; return true; }), setRotationDirection: jest.fn().mockImplementation(async (direction: 'clockwise' | 'counterclockwise') => { mockAirPurifier.rotationDirection = direction; return true; }), setOscillation: jest.fn().mockImplementation(async (enabled: boolean) => { mockAirPurifier.oscillationState = enabled; return true; }), setChildLock: jest.fn().mockImplementation(async (enabled: boolean) => { mockAirPurifier.childLock = enabled; return true; }), setSwingMode: jest.fn().mockImplementation(async (enabled: boolean) => { mockAirPurifier.oscillationState = enabled; return true; }) } as unknown as jest.Mocked<VeSyncFan>; return mockAirPurifier; }; /** * Air quality test scenarios for different conditions */ export const 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 */ export const 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 */ export const createAirQualityTestData = (scenario: keyof typeof airQualityScenarios) => { const data = 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 }; }; /** * Creates filter life test data for device mocking */ export const createFilterLifeTestData = (scenario: keyof typeof filterLifeScenarios, format: 'number' | 'object' = 'number') => { const data = filterLifeScenarios[scenario]; if (format === 'object') { return { filter_life: { percent: data.percent, replace_indicator: data.needsReplacement } }; } return { filter_life: data.percent }; };