homebridge-tsvesync
Version:
Homebridge plugin for VeSync devices including Levoit air purifiers, humidifiers, and Etekcity smart outlets
313 lines (264 loc) • 10.5 kB
text/typescript
/**
* 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
});
});
});