UNPKG

homebridge-tsvesync

Version:

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

467 lines (390 loc) 17 kB
/** * Integration tests for AirPurifierAccessory with combined air quality and filter features */ import { API } from 'homebridge'; import { Logger } from 'homebridge'; import { TSVESyncPlatform } from '../../platform'; import { VeSync } from 'tsvesync'; import { AirPurifierAccessory } from '../../accessories/air-purifier.accessory'; import { createMockLogger, createMockVeSync, createMockService } from '../utils/test-helpers'; // Complete mock for air purifier with all features interface CompleteMockAirPurifier { deviceName: string; deviceType: string; cid: string; uuid: string; deviceStatus: string; airQualityValue: number; pm1: number; pm10: number; filterLife: number; hasFeature: jest.Mock; details: { air_quality_value?: number; pm25?: number; pm1?: number; pm10?: number; filter_life?: number | { percent: number }; }; getDetails: jest.Mock; turnOn: jest.Mock; turnOff: jest.Mock; } describe('AirPurifierAccessory Integration Tests', () => { let mockAPI: jest.Mocked<API>; let platform: TSVESyncPlatform; let logger: jest.Mocked<Logger>; let api: jest.Mocked<VeSync>; beforeEach(() => { logger = createMockLogger(); api = createMockVeSync(); mockAPI = { hap: { Characteristic: { // Air Quality AirQuality: jest.fn(), PM2_5Density: jest.fn(), PM10Density: jest.fn(), // Filter Maintenance FilterChangeIndication: jest.fn(), FilterLifeLevel: jest.fn(), // Air Purifier Active: jest.fn(), CurrentAirPurifierState: jest.fn(), TargetAirPurifierState: jest.fn(), RotationSpeed: jest.fn(), // Device Info Manufacturer: jest.fn(), Model: jest.fn(), SerialNumber: jest.fn(), }, Service: { AirPurifier: jest.fn(), AirQualitySensor: jest.fn(), FilterMaintenance: jest.fn(), AccessoryInformation: jest.fn(), }, uuid: { generate: jest.fn(), }, }, platformAccessory: jest.fn(), } as any; platform = new TSVESyncPlatform(logger, {} as any, mockAPI); (platform as any).api = api; }); function createCompleteAirPurifier(config: Partial<CompleteMockAirPurifier> = {}): CompleteMockAirPurifier { return { deviceName: config.deviceName || 'Test Air Purifier', deviceType: config.deviceType || 'Core300S', cid: config.cid || 'test-cid', uuid: config.uuid || 'test-uuid', deviceStatus: config.deviceStatus || 'on', airQualityValue: config.airQualityValue !== undefined ? config.airQualityValue : 25, pm1: config.pm1 !== undefined ? config.pm1 : 15, pm10: config.pm10 !== undefined ? config.pm10 : 35, filterLife: config.filterLife !== undefined ? config.filterLife : 75, details: { air_quality_value: config.airQualityValue !== undefined ? config.airQualityValue : 25, pm1: config.pm1 !== undefined ? config.pm1 : 15, pm10: config.pm10 !== undefined ? config.pm10 : 35, filter_life: config.filterLife !== undefined ? config.filterLife : 75, ...config.details }, hasFeature: jest.fn().mockImplementation((feature: string) => { return feature === 'air_quality'; // Default to supporting air quality }), getDetails: jest.fn().mockResolvedValue(true), turnOn: jest.fn().mockResolvedValue(true), turnOff: jest.fn().mockResolvedValue(true), }; } function createMockAccessory() { const services = { airPurifier: createMockService(), airQuality: createMockService(), filter: createMockService(), info: { setCharacteristic: jest.fn().mockReturnThis(), }, }; return { getService: jest.fn((service) => { if (service === platform.Service.AirPurifier) return services.airPurifier; if (service === platform.Service.AirQualitySensor) return null; // Will be created if (service === platform.Service.FilterMaintenance) return null; // Will be created if (service === platform.Service.AccessoryInformation) return services.info; return null; }), addService: jest.fn((service) => { if (service === platform.Service.AirQualitySensor) return services.airQuality; if (service === platform.Service.FilterMaintenance) return services.filter; return services.airPurifier; }), }; } describe('Complete Device Setup', () => { it('should create all services for a fully featured device', () => { const mockDevice = createCompleteAirPurifier(); const accessory = createMockAccessory(); const airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); // Verify all services were created expect(accessory.addService).toHaveBeenCalledWith(platform.Service.AirQualitySensor); expect(accessory.addService).toHaveBeenCalledWith(platform.Service.FilterMaintenance); }); it('should setup characteristics for all services', () => { const mockDevice = createCompleteAirPurifier(); const accessory = createMockAccessory(); new AirPurifierAccessory(platform, accessory as any, mockDevice as any); // Verify characteristics were set up (exact verification depends on implementation details) expect(accessory.getService).toHaveBeenCalledWith(platform.Service.AirPurifier); }); }); describe('Real-world Scenarios', () => { let airPurifier: AirPurifierAccessory; let mockDevice: CompleteMockAirPurifier; let accessory: any; beforeEach(() => { mockDevice = createCompleteAirPurifier(); accessory = createMockAccessory(); airPurifier = new AirPurifierAccessory(platform, accessory, mockDevice as any); }); it('should handle good air quality with healthy filter', async () => { mockDevice.airQualityValue = 15; // Good air mockDevice.pm1 = 10; mockDevice.pm10 = 20; mockDevice.filterLife = 80; // Healthy filter const airQuality = await (airPurifier as any).getAirQuality(); const pm25Density = await (airPurifier as any).getPM25Density(); const filterChange = await (airPurifier as any).getFilterChangeIndication(); const filterLife = await (airPurifier as any).getFilterLifeLevel(); expect(airQuality).toBe(2); // GOOD expect(pm25Density).toBe(15); expect(filterChange).toBe(0); // FILTER_OK expect(filterLife).toBe(80); }); it('should handle poor air quality with old filter', async () => { mockDevice.airQualityValue = 180; // Very poor air mockDevice.pm1 = 120; mockDevice.pm10 = 220; mockDevice.filterLife = 5; // Old filter const airQuality = await (airPurifier as any).getAirQuality(); const pm25Density = await (airPurifier as any).getPM25Density(); const filterChange = await (airPurifier as any).getFilterChangeIndication(); const filterLife = await (airPurifier as any).getFilterLifeLevel(); expect(airQuality).toBe(4); // INFERIOR expect(pm25Density).toBe(180); expect(filterChange).toBe(1); // CHANGE_FILTER expect(filterLife).toBe(5); }); it('should handle filter replacement scenario', async () => { // Old filter with poor air quality mockDevice.airQualityValue = 75; mockDevice.filterLife = 3; let airQuality = await (airPurifier as any).getAirQuality(); let filterChange = await (airPurifier as any).getFilterChangeIndication(); expect(airQuality).toBe(4); // INFERIOR expect(filterChange).toBe(1); // CHANGE_FILTER // Replace filter - air quality should improve over time mockDevice.filterLife = 100; mockDevice.airQualityValue = 20; // Improved after filter change airQuality = await (airPurifier as any).getAirQuality(); filterChange = await (airPurifier as any).getFilterChangeIndication(); expect(airQuality).toBe(2); // GOOD expect(filterChange).toBe(0); // FILTER_OK }); it('should handle seasonal air quality changes', async () => { const seasonalScenarios = [ { season: 'Spring', pm25: 30, expected: 2 }, // GOOD - mild pollen { season: 'Summer', pm25: 15, expected: 2 }, // GOOD - clean air { season: 'Fall', pm25: 45, expected: 3 }, // FAIR - more particles { season: 'Winter', pm25: 80, expected: 4 }, // INFERIOR - heating pollution ]; for (const scenario of seasonalScenarios) { mockDevice.airQualityValue = scenario.pm25; mockDevice.details.air_quality_value = scenario.pm25; const airQuality = await (airPurifier as any).getAirQuality(); expect(airQuality).toBe(scenario.expected); } }); }); describe('Device-Specific Behaviors', () => { it('should handle Core300S with complete feature set', async () => { const core300s = createCompleteAirPurifier({ deviceType: 'Core300S', airQualityValue: 28, filterLife: 65, details: { filter_life: 65 } // Number format for Core series }); const accessory = createMockAccessory(); const airPurifier = new AirPurifierAccessory(platform, accessory as any, core300s as any); const airQuality = await (airPurifier as any).getAirQuality(); const filterLife = await (airPurifier as any).getFilterLifeLevel(); expect(airQuality).toBe(2); // GOOD expect(filterLife).toBe(65); }); it('should handle LV-PUR131S with object filter format', async () => { const lvPur131s = createCompleteAirPurifier({ deviceType: 'LV-PUR131S', airQualityValue: 35, filterLife: 42, details: { filter_life: { percent: 42 } } // Object format for LV series }); const accessory = createMockAccessory(); const airPurifier = new AirPurifierAccessory(platform, accessory as any, lvPur131s as any); const airQuality = await (airPurifier as any).getAirQuality(); const filterLife = await (airPurifier as any).getFilterLifeLevel(); expect(airQuality).toBe(2); // GOOD expect(filterLife).toBe(42); }); it('should handle LAP-V102S with extended PM data', async () => { const lapV102s = createCompleteAirPurifier({ deviceType: 'LAP-V102S-AASR', airQualityValue: 22, pm1: 18, pm10: 28, filterLife: 88, details: { air_quality_value: 22, pm1: 18, pm10: 28, filter_life: 88 } }); const accessory = createMockAccessory(); const airPurifier = new AirPurifierAccessory(platform, accessory as any, lapV102s as any); const airQuality = await (airPurifier as any).getAirQuality(); const pm25 = await (airPurifier as any).getPM25Density(); const pm10 = await (airPurifier as any).getPM10Density(); expect(airQuality).toBe(2); // GOOD expect(pm25).toBe(22); expect(pm10).toBe(28); }); }); describe('State Synchronization', () => { let airPurifier: AirPurifierAccessory; let mockDevice: CompleteMockAirPurifier; let characteristics: any; beforeEach(() => { characteristics = { airQuality: { updateValue: jest.fn() }, pm25: { updateValue: jest.fn() }, pm10: { updateValue: jest.fn() }, filterChange: { updateValue: jest.fn() }, filterLife: { updateValue: jest.fn() }, }; mockDevice = createCompleteAirPurifier(); const accessory = { getService: jest.fn(() => ({ getCharacteristic: jest.fn((char) => { if (char === platform.Characteristic.AirQuality) return characteristics.airQuality; if (char === platform.Characteristic.PM2_5Density) return characteristics.pm25; if (char === platform.Characteristic.PM10Density) return characteristics.pm10; if (char === platform.Characteristic.FilterChangeIndication) return characteristics.filterChange; if (char === platform.Characteristic.FilterLifeLevel) return characteristics.filterLife; return { updateValue: jest.fn() }; }), })), addService: jest.fn(() => ({ getCharacteristic: jest.fn(() => ({ updateValue: jest.fn() })), })), }; airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); }); it('should update all characteristics when device state changes', async () => { // Simulate device state change mockDevice.airQualityValue = 45; mockDevice.pm1 = 35; mockDevice.pm10 = 55; mockDevice.filterLife = 25; // Trigger update (implementation-dependent) await (airPurifier as any).updateDeviceState(); // Verify updates were called (exact verification depends on implementation) // This is a placeholder for the actual implementation expect(mockDevice.getDetails).toHaveBeenCalled(); }); it('should handle partial updates gracefully', async () => { // Update only some values mockDevice.airQualityValue = 60; // Leave other values unchanged const airQuality = await (airPurifier as any).getAirQuality(); expect(airQuality).toBe(4); // INFERIOR for PM2.5 = 60 }); }); describe('Error Recovery', () => { let airPurifier: AirPurifierAccessory; let mockDevice: CompleteMockAirPurifier; beforeEach(() => { mockDevice = createCompleteAirPurifier(); const accessory = createMockAccessory(); airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); }); it('should handle API timeouts gracefully', async () => { mockDevice.getDetails = jest.fn().mockImplementation(() => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100)) ); // Should not throw and should use cached values const airQuality = await (airPurifier as any).getAirQuality(); const filterLife = await (airPurifier as any).getFilterLifeLevel(); expect(airQuality).toBeDefined(); expect(filterLife).toBeDefined(); }); it('should handle corrupted device data', async () => { mockDevice.details = null as any; mockDevice.airQualityValue = NaN; mockDevice.filterLife = NaN; const airQuality = await (airPurifier as any).getAirQuality(); const filterLife = await (airPurifier as any).getFilterLifeLevel(); expect(airQuality).toBe(1); // Default to EXCELLENT expect(filterLife).toBe(0); // Default to empty }); it('should recover from network interruptions', async () => { // Simulate network failure mockDevice.getDetails = jest.fn().mockRejectedValueOnce(new Error('Network error')); // First call should handle error await (airPurifier as any).getAirQuality(); // Simulate recovery mockDevice.getDetails = jest.fn().mockResolvedValue(true); mockDevice.airQualityValue = 30; const airQuality = await (airPurifier as any).getAirQuality(); expect(airQuality).toBe(2); // GOOD }); }); describe('Performance and Efficiency', () => { it('should not make redundant API calls for multiple characteristic reads', async () => { const mockDevice = createCompleteAirPurifier(); const accessory = createMockAccessory(); const airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); // Read multiple characteristics await Promise.all([ (airPurifier as any).getAirQuality(), (airPurifier as any).getPM25Density(), (airPurifier as any).getFilterLifeLevel(), (airPurifier as any).getFilterChangeIndication(), ]); // Should use cached data, not make multiple API calls expect(mockDevice.getDetails).toHaveBeenCalledTimes(0); // No additional calls beyond setup }); it('should handle high-frequency updates efficiently', async () => { const mockDevice = createCompleteAirPurifier(); const accessory = createMockAccessory(); const airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); // Simulate rapid updates const updates = Array.from({ length: 10 }, (_, i) => ({ airQualityValue: 20 + i * 5, filterLife: 80 - i * 2, })); const startTime = Date.now(); for (const update of updates) { mockDevice.airQualityValue = update.airQualityValue; mockDevice.filterLife = update.filterLife; await (airPurifier as any).getAirQuality(); await (airPurifier as any).getFilterLifeLevel(); } const endTime = Date.now(); const duration = endTime - startTime; // Should complete within reasonable time expect(duration).toBeLessThan(1000); // Less than 1 second }); }); });