UNPKG

homebridge-tsvesync

Version:

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

483 lines (394 loc) 16.4 kB
/** * Unit tests for AirPurifierAccessory air quality features */ import { API } from 'homebridge'; import { Logger } from 'homebridge'; import { CharacteristicValue } 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'; // Extended mock for air purifier with air quality features interface MockAirPurifierWithAirQuality { deviceName: string; deviceType: string; cid: string; uuid: string; deviceStatus: string; hasFeature: jest.Mock; airQualityValue: number; pm1: number; pm10: number; filterLife: number; 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 Air Quality Features', () => { let mockAPI: jest.Mocked<API>; let platform: TSVESyncPlatform; let logger: jest.Mocked<Logger>; let api: jest.Mocked<VeSync>; let airQualityService: any; let airPurifierService: any; let infoService: any; beforeEach(() => { logger = createMockLogger(); api = createMockVeSync(); mockAPI = { hap: { Characteristic: { AirQuality: jest.fn(), PM2_5Density: jest.fn(), PM10Density: jest.fn(), Active: jest.fn(), CurrentAirPurifierState: jest.fn(), TargetAirPurifierState: jest.fn(), RotationSpeed: jest.fn(), Manufacturer: jest.fn(), Model: jest.fn(), SerialNumber: jest.fn(), }, Service: { AirPurifier: jest.fn(), AirQualitySensor: 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; // Setup mock services airQualityService = createMockService(); airPurifierService = createMockService(); infoService = { setCharacteristic: jest.fn().mockReturnThis(), }; }); function createMockAirPurifier(config: Partial<MockAirPurifierWithAirQuality> = {}): MockAirPurifierWithAirQuality { 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 || 25, pm1: config.pm1 || 15, pm10: config.pm10 || 35, filterLife: config.filterLife || 75, details: config.details || { air_quality_value: config.airQualityValue || 25, pm1: config.pm1 || 15, pm10: config.pm10 || 35, filter_life: config.filterLife || 75, }, hasFeature: jest.fn().mockImplementation((feature: string) => { return feature === 'air_quality'; }), getDetails: jest.fn().mockResolvedValue(true), turnOn: jest.fn().mockResolvedValue(true), turnOff: jest.fn().mockResolvedValue(true), }; } describe('Air Quality Service Setup', () => { it('should create air quality service for devices with air quality support', () => { const mockDevice = createMockAirPurifier({ airQualityValue: 20 }); const accessory = { getService: jest.fn((service) => { if (service === platform.Service.AccessoryInformation) return infoService; if (service === platform.Service.AirPurifier) return airPurifierService; if (service === platform.Service.AirQualitySensor) return null; // Not created yet return null; }), addService: jest.fn((service) => { if (service === platform.Service.AirQualitySensor) return airQualityService; return airQualityService; }), }; const airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); // Verify air quality service was added expect(accessory.addService).toHaveBeenCalledWith(platform.Service.AirQualitySensor); }); it('should not create air quality service for devices without air quality support', () => { const mockDevice = createMockAirPurifier(); mockDevice.hasFeature = jest.fn().mockReturnValue(false); const accessory = { getService: jest.fn((service) => { if (service === platform.Service.AccessoryInformation) return infoService; if (service === platform.Service.AirPurifier) return airPurifierService; return null; }), addService: jest.fn(), }; const airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); // Verify air quality service was not added expect(accessory.addService).not.toHaveBeenCalledWith(platform.Service.AirQualitySensor); }); }); describe('Air Quality Value Conversion', () => { let airPurifier: AirPurifierAccessory; let mockDevice: MockAirPurifierWithAirQuality; beforeEach(() => { mockDevice = createMockAirPurifier(); const accessory = { getService: jest.fn((service) => { if (service === platform.Service.AccessoryInformation) return infoService; if (service === platform.Service.AirPurifier) return airPurifierService; if (service === platform.Service.AirQualitySensor) return airQualityService; return null; }), addService: jest.fn(), }; airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); }); it('should convert PM2.5 values to HomeKit air quality scale', async () => { const testCases = [ { pm25: 0, expected: 1 }, // EXCELLENT { pm25: 12, expected: 1 }, // EXCELLENT (0-12) { pm25: 13, expected: 2 }, // GOOD (13-35) { pm25: 35, expected: 2 }, // GOOD { pm25: 36, expected: 3 }, // FAIR (36-55) { pm25: 55, expected: 3 }, // FAIR { pm25: 56, expected: 4 }, // INFERIOR (56-150) { pm25: 150, expected: 4 }, // INFERIOR { pm25: 151, expected: 5 }, // POOR (>150) { pm25: 300, expected: 5 }, // POOR ]; for (const testCase of testCases) { mockDevice.airQualityValue = testCase.pm25; mockDevice.details!.air_quality_value = testCase.pm25; const result = await (airPurifier as any).getAirQuality(); expect(result).toBe(testCase.expected); } }); it('should handle edge cases in PM2.5 conversion', async () => { // Test boundary values mockDevice.airQualityValue = 12.5; mockDevice.details!.air_quality_value = 12.5; let result = await (airPurifier as any).getAirQuality(); expect(result).toBe(1); // Should round down to EXCELLENT mockDevice.airQualityValue = 35.5; mockDevice.details!.air_quality_value = 35.5; result = await (airPurifier as any).getAirQuality(); expect(result).toBe(3); // Should round to FAIR }); it('should handle zero PM2.5 values', async () => { mockDevice.airQualityValue = 0; mockDevice.details!.air_quality_value = 0; const result = await (airPurifier as any).getAirQuality(); expect(result).toBe(1); // EXCELLENT }); it('should handle extremely high PM2.5 values', async () => { mockDevice.airQualityValue = 999; mockDevice.details!.air_quality_value = 999; const result = await (airPurifier as any).getAirQuality(); expect(result).toBe(5); // POOR }); }); describe('PM2.5 Density Characteristic', () => { let airPurifier: AirPurifierAccessory; let mockDevice: MockAirPurifierWithAirQuality; beforeEach(() => { mockDevice = createMockAirPurifier(); const accessory = { getService: jest.fn((service) => { if (service === platform.Service.AirQualitySensor) return airQualityService; return airPurifierService; }), addService: jest.fn(), }; airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); }); it('should return PM2.5 density within HomeKit limits', async () => { mockDevice.airQualityValue = 42; mockDevice.details!.air_quality_value = 42; const result = await (airPurifier as any).getPM25Density(); expect(result).toBe(42); expect(result).toBeGreaterThanOrEqual(0); expect(result).toBeLessThanOrEqual(1000); }); it('should clamp PM2.5 values above 1000', async () => { mockDevice.airQualityValue = 1500; mockDevice.details!.air_quality_value = 1500; const result = await (airPurifier as any).getPM25Density(); expect(result).toBe(1000); // Clamped to maximum }); it('should handle negative PM2.5 values', async () => { mockDevice.airQualityValue = -10; mockDevice.details!.air_quality_value = -10; const result = await (airPurifier as any).getPM25Density(); expect(result).toBe(0); // Clamped to minimum }); it('should fallback to pm25 field if air_quality_value is missing', async () => { mockDevice.airQualityValue = 35; mockDevice.details!.pm25 = 35; delete mockDevice.details!.air_quality_value; const result = await (airPurifier as any).getPM25Density(); expect(result).toBe(35); }); }); describe('PM10 Density Characteristic', () => { let airPurifier: AirPurifierAccessory; let mockDevice: MockAirPurifierWithAirQuality; beforeEach(() => { mockDevice = createMockAirPurifier({ pm10: 45 }); const accessory = { getService: jest.fn((service) => { if (service === platform.Service.AirQualitySensor) return airQualityService; return airPurifierService; }), addService: jest.fn(), }; airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); }); it('should return PM10 density when available', async () => { const result = await (airPurifier as any).getPM10Density(); expect(result).toBe(45); }); it('should handle PM10 values above HomeKit limit', async () => { mockDevice.pm10 = 1200; mockDevice.details!.pm10 = 1200; const result = await (airPurifier as any).getPM10Density(); expect(result).toBe(1000); // Clamped to maximum }); it('should handle missing PM10 data', async () => { mockDevice.pm10 = 0; delete mockDevice.details!.pm10; const result = await (airPurifier as any).getPM10Density(); expect(result).toBe(0); }); it('should only add PM10 characteristic when data is available', () => { // Device without PM10 data const deviceWithoutPM10 = createMockAirPurifier({ pm10: 0 }); delete deviceWithoutPM10.details!.pm10; const accessory = { getService: jest.fn(() => airQualityService), addService: jest.fn(() => airQualityService), }; const setupCharacteristicSpy = jest.spyOn(AirPurifierAccessory.prototype as any, 'setupCharacteristic'); new AirPurifierAccessory(platform, accessory as any, deviceWithoutPM10 as any); // PM10 characteristic should not be set up const pm10Calls = setupCharacteristicSpy.mock.calls.filter(call => call[0] === platform.Characteristic.PM10Density ); expect(pm10Calls).toHaveLength(0); }); }); describe('Air Quality Data Updates', () => { let airPurifier: AirPurifierAccessory; let mockDevice: MockAirPurifierWithAirQuality; let mockCharacteristic: any; beforeEach(() => { mockCharacteristic = { updateValue: jest.fn(), }; mockDevice = createMockAirPurifier(); const accessory = { getService: jest.fn(() => ({ getCharacteristic: jest.fn(() => mockCharacteristic), })), addService: jest.fn(), }; airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); }); it('should update air quality characteristics when device updates', async () => { // Simulate device update mockDevice.airQualityValue = 55; mockDevice.pm10 = 75; await (airPurifier as any).updateDeviceState(); // Verify characteristics were updated (exact calls depend on implementation) expect(mockCharacteristic.updateValue).toHaveBeenCalled(); }); it('should handle real-time air quality changes', async () => { const initialPM25 = 20; const updatedPM25 = 80; // Initial state mockDevice.airQualityValue = initialPM25; await (airPurifier as any).getAirQuality(); // Update state mockDevice.airQualityValue = updatedPM25; mockDevice.details!.air_quality_value = updatedPM25; const newResult = await (airPurifier as any).getAirQuality(); expect(newResult).toBe(4); // Should be INFERIOR for PM2.5 = 80 }); it('should handle air quality improvements', async () => { // Start with poor air quality mockDevice.airQualityValue = 200; let result = await (airPurifier as any).getAirQuality(); expect(result).toBe(5); // POOR // Air quality improves mockDevice.airQualityValue = 25; mockDevice.details!.air_quality_value = 25; result = await (airPurifier as any).getAirQuality(); expect(result).toBe(2); // GOOD }); }); describe('Error Handling', () => { let airPurifier: AirPurifierAccessory; let mockDevice: MockAirPurifierWithAirQuality; beforeEach(() => { mockDevice = createMockAirPurifier(); const accessory = { getService: jest.fn(() => airQualityService), addService: jest.fn(), }; airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any); }); it('should handle missing air quality data gracefully', async () => { mockDevice.airQualityValue = 0; delete mockDevice.details!.air_quality_value; delete mockDevice.details!.pm25; const result = await (airPurifier as any).getAirQuality(); expect(result).toBe(1); // Default to EXCELLENT for 0 value }); it('should handle device API errors', async () => { mockDevice.getDetails = jest.fn().mockRejectedValue(new Error('API Error')); // Should not throw error await expect((airPurifier as any).getAirQuality()).resolves.toBeDefined(); }); it('should handle corrupted device data', async () => { mockDevice.details = null as any; const result = await (airPurifier as any).getAirQuality(); expect(result).toBe(1); // Default to EXCELLENT }); it('should handle non-numeric air quality values', async () => { mockDevice.airQualityValue = NaN; mockDevice.details!.air_quality_value = NaN; const result = await (airPurifier as any).getAirQuality(); expect(result).toBe(1); // Default to EXCELLENT for NaN }); }); describe('Feature Detection', () => { it('should detect air quality support correctly', () => { const deviceWithAirQuality = createMockAirPurifier(); deviceWithAirQuality.hasFeature = jest.fn().mockReturnValue(true); const accessory = { getService: jest.fn(), addService: jest.fn(() => airQualityService), }; const airPurifier = new AirPurifierAccessory(platform, accessory as any, deviceWithAirQuality as any); expect(deviceWithAirQuality.hasFeature).toHaveBeenCalledWith('air_quality'); }); it('should handle devices without air quality support', () => { const deviceWithoutAirQuality = createMockAirPurifier(); deviceWithoutAirQuality.hasFeature = jest.fn().mockReturnValue(false); const accessory = { getService: jest.fn(), addService: jest.fn(), }; new AirPurifierAccessory(platform, accessory as any, deviceWithoutAirQuality as any); expect(accessory.addService).not.toHaveBeenCalledWith(platform.Service.AirQualitySensor); }); }); });