UNPKG

homebridge-tsvesync

Version:

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

313 lines (264 loc) 10.5 kB
/** * Simple unit tests for air quality method logic without complex accessory setup */ describe('Air Quality Methods', () => { describe('PM2.5 to HomeKit Air Quality Conversion', () => { // This is the conversion logic that should be in the air purifier accessory const convertPM25ToHomeKit = (pm25: number): number => { if (pm25 <= 12) return 1; // EXCELLENT if (pm25 <= 35) return 2; // GOOD if (pm25 <= 55) return 3; // FAIR return 4; // INFERIOR (VeSync exposes four levels) }; it('should convert PM2.5 values to correct HomeKit air quality levels', () => { const testCases = [ { pm25: 0, expected: 1, quality: 'EXCELLENT' }, { pm25: 12, expected: 1, quality: 'EXCELLENT' }, { pm25: 13, expected: 2, quality: 'GOOD' }, { pm25: 35, expected: 2, quality: 'GOOD' }, { pm25: 36, expected: 3, quality: 'FAIR' }, { pm25: 55, expected: 3, quality: 'FAIR' }, { pm25: 56, expected: 4, quality: 'INFERIOR' }, { pm25: 150, expected: 4, quality: 'INFERIOR' }, { pm25: 151, expected: 4, quality: 'INFERIOR' }, { pm25: 300, expected: 4, quality: 'INFERIOR' }, ]; testCases.forEach(({ pm25, expected, quality }) => { const result = convertPM25ToHomeKit(pm25); expect(result).toBe(expected); }); }); it('should handle edge cases', () => { expect(convertPM25ToHomeKit(12.5)).toBe(2); // Just above EXCELLENT boundary expect(convertPM25ToHomeKit(35.5)).toBe(3); // Just above GOOD boundary expect(convertPM25ToHomeKit(999)).toBe(4); // Extreme value capped at INFERIOR }); it('should handle zero and negative values', () => { expect(convertPM25ToHomeKit(0)).toBe(1); expect(convertPM25ToHomeKit(-5)).toBe(1); // Treat negative as excellent }); }); describe('Filter Change Indication Logic', () => { const shouldChangeFilter = (filterLife: number): boolean => { return filterLife < 10; }; it('should indicate filter change needed when below 10%', () => { expect(shouldChangeFilter(0)).toBe(true); expect(shouldChangeFilter(5)).toBe(true); expect(shouldChangeFilter(9)).toBe(true); expect(shouldChangeFilter(9.9)).toBe(true); }); it('should not indicate filter change when at or above 10%', () => { expect(shouldChangeFilter(10)).toBe(false); expect(shouldChangeFilter(15)).toBe(false); expect(shouldChangeFilter(50)).toBe(false); expect(shouldChangeFilter(100)).toBe(false); }); it('should handle edge cases', () => { expect(shouldChangeFilter(10.0)).toBe(false); // Exact boundary expect(shouldChangeFilter(-5)).toBe(true); // Negative (corrupted data) expect(shouldChangeFilter(NaN)).toBe(false); // NaN should not trigger change }); }); describe('PM Density Clamping', () => { const clampPMDensity = (value: number): number => { if (isNaN(value) || value < 0) return 0; if (value > 1000) return 1000; return Math.round(value); }; it('should clamp PM values to HomeKit range (0-1000)', () => { expect(clampPMDensity(25)).toBe(25); expect(clampPMDensity(0)).toBe(0); expect(clampPMDensity(1000)).toBe(1000); expect(clampPMDensity(1500)).toBe(1000); // Clamped expect(clampPMDensity(-10)).toBe(0); // Clamped }); it('should handle invalid values', () => { expect(clampPMDensity(NaN)).toBe(0); expect(clampPMDensity(Infinity)).toBe(1000); expect(clampPMDensity(-Infinity)).toBe(0); }); it('should round decimal values', () => { expect(clampPMDensity(25.4)).toBe(25); expect(clampPMDensity(25.6)).toBe(26); expect(clampPMDensity(25.5)).toBe(26); }); }); describe('Filter Life Level Processing', () => { const processFilterLife = (filterLife: number): number => { if (isNaN(filterLife) || filterLife < 0) return 0; if (filterLife > 100) return 100; return Math.round(filterLife); }; it('should clamp filter life to valid percentage range (0-100)', () => { expect(processFilterLife(50)).toBe(50); expect(processFilterLife(0)).toBe(0); expect(processFilterLife(100)).toBe(100); expect(processFilterLife(150)).toBe(100); // Clamped expect(processFilterLife(-10)).toBe(0); // Clamped }); it('should handle invalid values', () => { expect(processFilterLife(NaN)).toBe(0); expect(processFilterLife(Infinity)).toBe(100); expect(processFilterLife(-Infinity)).toBe(0); }); it('should round decimal values', () => { expect(processFilterLife(67.4)).toBe(67); expect(processFilterLife(67.6)).toBe(68); expect(processFilterLife(67.5)).toBe(68); }); }); describe('Air Quality Data Extraction', () => { const extractAirQualityData = (deviceData: any) => { return { pm25: deviceData?.air_quality_value ?? deviceData?.pm25 ?? 0, pm1: deviceData?.pm1 ?? 0, pm10: deviceData?.pm10 ?? 0, airQuality: deviceData?.air_quality ?? '', filterLife: typeof deviceData?.filter_life === 'object' ? deviceData.filter_life?.percent ?? 0 : deviceData?.filter_life ?? 0 }; }; it('should extract air quality data from device response', () => { const deviceData = { air_quality_value: 25, pm1: 15, pm10: 35, air_quality: 'Good', filter_life: 75 }; const result = extractAirQualityData(deviceData); expect(result.pm25).toBe(25); expect(result.pm1).toBe(15); expect(result.pm10).toBe(35); expect(result.airQuality).toBe('Good'); expect(result.filterLife).toBe(75); }); it('should handle object format filter life (LV-PUR131S)', () => { const deviceData = { air_quality_value: 30, filter_life: { percent: 42, replace_indicator: false } }; const result = extractAirQualityData(deviceData); expect(result.pm25).toBe(30); expect(result.filterLife).toBe(42); }); it('should fallback to pm25 field when air_quality_value missing', () => { const deviceData = { pm25: 35, air_quality: 2, filter_life: 60 }; const result = extractAirQualityData(deviceData); expect(result.pm25).toBe(35); expect(result.airQuality).toBe(2); expect(result.filterLife).toBe(60); }); it('should handle missing data gracefully', () => { const deviceData = {}; const result = extractAirQualityData(deviceData); expect(result.pm25).toBe(0); expect(result.pm1).toBe(0); expect(result.pm10).toBe(0); expect(result.airQuality).toBe(''); expect(result.filterLife).toBe(0); }); it('should handle null/undefined device data', () => { expect(extractAirQualityData(null)).toEqual({ pm25: 0, pm1: 0, pm10: 0, airQuality: '', filterLife: 0 }); expect(extractAirQualityData(undefined)).toEqual({ pm25: 0, pm1: 0, pm10: 0, airQuality: '', filterLife: 0 }); }); it('should handle malformed filter life object', () => { const deviceData = { filter_life: { other: 'value' } // Missing percent }; const result = extractAirQualityData(deviceData); expect(result.filterLife).toBe(0); }); }); describe('Real-world Scenarios', () => { const processDeviceData = (deviceData: any) => { const extracted = { pm25: deviceData?.air_quality_value ?? deviceData?.pm25 ?? 0, pm1: deviceData?.pm1 ?? 0, pm10: deviceData?.pm10 ?? 0, filterLife: typeof deviceData?.filter_life === 'object' ? deviceData.filter_life?.percent ?? 0 : deviceData?.filter_life ?? 0 }; return { homeKitAirQuality: extracted.pm25 <= 12 ? 1 : extracted.pm25 <= 35 ? 2 : extracted.pm25 <= 55 ? 3 : 4, pm25Density: Math.min(1000, Math.max(0, Math.round(extracted.pm25))), pm10Density: Math.min(1000, Math.max(0, Math.round(extracted.pm10))), filterChangeNeeded: extracted.filterLife < 10, filterLifeLevel: Math.min(100, Math.max(0, Math.round(extracted.filterLife))) }; }; it('should handle good air quality with healthy filter', () => { const deviceData = { air_quality_value: 15, // Good air pm1: 10, pm10: 20, filter_life: 80 // Healthy filter }; const result = processDeviceData(deviceData); expect(result.homeKitAirQuality).toBe(2); // GOOD expect(result.pm25Density).toBe(15); expect(result.pm10Density).toBe(20); expect(result.filterChangeNeeded).toBe(false); expect(result.filterLifeLevel).toBe(80); }); it('should handle poor air quality with old filter', () => { const deviceData = { air_quality_value: 180, // Very poor air pm1: 120, pm10: 220, filter_life: 5 // Old filter }; const result = processDeviceData(deviceData); expect(result.homeKitAirQuality).toBe(4); // INFERIOR expect(result.pm25Density).toBe(180); expect(result.pm10Density).toBe(220); expect(result.filterChangeNeeded).toBe(true); expect(result.filterLifeLevel).toBe(5); }); it('should handle LV-PUR131S data format', () => { const deviceData = { air_quality: 2, air_quality_value: 28, filter_life: { percent: 33, replace_indicator: false } }; const result = processDeviceData(deviceData); expect(result.homeKitAirQuality).toBe(2); // GOOD expect(result.pm25Density).toBe(28); expect(result.filterChangeNeeded).toBe(false); expect(result.filterLifeLevel).toBe(33); }); it('should handle extreme values gracefully', () => { const deviceData = { air_quality_value: 999, // Extreme PM2.5 pm10: 1500, // Above HomeKit limit filter_life: 150 // Above 100% }; const result = processDeviceData(deviceData); expect(result.homeKitAirQuality).toBe(4); // INFERIOR expect(result.pm25Density).toBe(999); expect(result.pm10Density).toBe(1000); // Clamped expect(result.filterLifeLevel).toBe(100); // Clamped }); }); });