UNPKG

@homebridge-plugins/homebridge-air

Version:

The AirNow plugin allows you to monitor the current AirQuality for your Zip Code from HomeKit and Siri.

278 lines 11.1 kB
import { describe, expect, it } from 'vitest'; describe('aqicnData interface', () => { it('should handle valid AQICN response with all pollutants', () => { const mockAqicnResponse = { status: 'ok', data: { idx: 123, aqi: 85, time: { s: '2025-08-31 10:00:00', tz: '+02:00', }, city: { name: 'Winterthur', geo: [47.5, 8.7], url: 'https://aqicn.org/city/winterthur', }, attributions: [ { name: 'World Air Quality Index Project', url: 'https://waqi.info/', }, ], iaqi: { pm25: { v: 45 }, pm10: { v: 55 }, o3: { v: 35 }, no2: { v: 25 }, so2: { v: 15 }, co: { v: 5 }, }, forecast: { daily: { pm25: [{ v: 40 }], pm10: [{ v: 50 }], o3: [{ v: 30 }], uvi: [{ v: 3 }], }, }, }, }; // Verify that all pollutant properties can be accessed expect(mockAqicnResponse.status).toBe('ok'); expect(mockAqicnResponse.data.aqi).toBe(85); expect(mockAqicnResponse.data.iaqi.pm25?.v).toBe(45); expect(mockAqicnResponse.data.iaqi.pm10?.v).toBe(55); expect(mockAqicnResponse.data.iaqi.o3?.v).toBe(35); expect(mockAqicnResponse.data.iaqi.no2?.v).toBe(25); expect(mockAqicnResponse.data.iaqi.so2?.v).toBe(15); expect(mockAqicnResponse.data.iaqi.co?.v).toBe(5); }); it('should handle AQICN response with missing pollutants', () => { const mockAqicnResponse = { status: 'ok', data: { idx: 123, aqi: 85, time: { s: '2025-08-31 10:00:00', tz: '+02:00', }, city: { name: 'Winterthur', geo: [47.5, 8.7], url: 'https://aqicn.org/city/winterthur', }, attributions: [ { name: 'World Air Quality Index Project', url: 'https://waqi.info/', }, ], iaqi: { pm25: { v: 45 }, // Other pollutants may be missing }, forecast: { daily: { pm25: [{ v: 40 }], pm10: [{ v: 50 }], o3: [{ v: 30 }], uvi: [{ v: 3 }], }, }, }, }; // Verify that missing pollutants are handled gracefully expect(mockAqicnResponse.data.iaqi.pm25?.v).toBe(45); expect(mockAqicnResponse.data.iaqi.pm10?.v).toBeUndefined(); expect(mockAqicnResponse.data.iaqi.o3?.v).toBeUndefined(); expect(mockAqicnResponse.data.iaqi.no2?.v).toBeUndefined(); expect(mockAqicnResponse.data.iaqi.so2?.v).toBeUndefined(); expect(mockAqicnResponse.data.iaqi.co?.v).toBeUndefined(); }); it('should handle AQICN error response', () => { const mockErrorResponse = { status: 'error', data: 'Invalid city', }; expect(mockErrorResponse.status).toBe('error'); }); it('should support AQICN pollutant parsing logic', () => { const mockDeviceStatus = { idx: 123, aqi: 85, time: { s: '2025-08-31 10:00:00', tz: '+02:00', }, city: { name: 'Winterthur', geo: [47.5, 8.7], url: 'https://aqicn.org/city/winterthur', }, attributions: [ { name: 'World Air Quality Index Project', url: 'https://waqi.info/', }, ], iaqi: { pm25: { v: 45 }, pm10: { v: 55 }, o3: { v: 35 }, no2: { v: 25 }, so2: { v: 15 }, co: { v: 5 }, }, forecast: { daily: { pm25: [{ v: 40 }], pm10: [{ v: 50 }], o3: [{ v: 30 }], uvi: [{ v: 3 }], }, }, }; // Simulate the parsing logic from airqualitysensor.ts const pollutants = ['o3', 'no2', 'so2', 'pm25', 'pm10', 'co']; const parsedValues = {}; pollutants.forEach((pollutant) => { const param = mockDeviceStatus.iaqi[pollutant]?.v; if (param !== undefined) { const aqi = Number.parseFloat(param.toString()); if (!Number.isNaN(aqi)) { parsedValues[pollutant] = aqi; } } }); // Verify that all pollutants are parsed correctly expect(parsedValues.pm25).toBe(45); expect(parsedValues.pm10).toBe(55); expect(parsedValues.o3).toBe(35); expect(parsedValues.no2).toBe(25); expect(parsedValues.so2).toBe(15); expect(parsedValues.co).toBe(5); }); it('should handle AQICN device status as object not array', () => { // This test validates the fix for issue #22 where AQICN data was incorrectly accessed as array const mockDeviceStatus = { idx: 123, aqi: 85, iaqi: { pm25: { v: 45 }, o3: { v: 35 }, }, city: { name: 'Winterthur', geo: [47.5, 8.7], }, }; // Verify that AQICN data structure is accessible as object, not array expect(mockDeviceStatus.aqi).toBe(85); expect(mockDeviceStatus.iaqi.pm25?.v).toBe(45); expect(mockDeviceStatus.iaqi.o3?.v).toBe(35); // This should NOT be accessed as array (this was the bug) expect(Array.isArray(mockDeviceStatus)).toBe(false); // Verify accessing as array would fail (demonstrating the original bug) expect(() => { // Testing incorrect array access that was causing the original issue const wrongAccess = mockDeviceStatus[0]; return wrongAccess; }).not.toThrow(); // The access returns undefined, doesn't throw // Testing incorrect array access expect(mockDeviceStatus[0]).toBeUndefined(); }); it('should handle real-world AQICN API response with weather data (issue #28)', () => { // This test reproduces the actual API response structure from Winterthur that caused the bug const realWorldResponse = { status: 'ok', data: { aqi: 4, idx: 9023, attributions: [ { url: 'https://www.ostluft.ch/', name: 'OSTLUFT - die Luftqualitätsüberwachung der Ostschweizer Kantone und des Fürstentums Liechtenstein', }, { url: 'https://waqi.info/', name: 'World Air Quality Index Project', }, ], city: { geo: [47.508150298795, 8.7203729897032], name: 'Winterthur Veltheim, Switzerland', url: 'https://aqicn.org/city/switzerland/winterthur-veltheim', }, iaqi: { // Weather data that's not pollutants dew: { v: 12 }, h: { v: 82 }, p: { v: 1016 }, t: { v: 15 }, w: { v: 2 }, wg: { v: 16.9 }, // Actual pollutant data pm10: { v: 4 }, }, time: { s: '2025-09-03 01:00:00', tz: '+02:00', }, forecast: { daily: { o3: [{ v: 8 }], pm10: [{ v: 4 }], pm25: [{ v: 9 }], uvi: [{ v: 0 }], }, }, }, }; // Verify the response can be parsed correctly expect(realWorldResponse.status).toBe('ok'); expect(realWorldResponse.data.aqi).toBe(4); expect(realWorldResponse.data.iaqi.pm10?.v).toBe(4); // Verify weather data is present but should be ignored during pollutant parsing expect(realWorldResponse.data.iaqi.dew?.v).toBe(12); expect(realWorldResponse.data.iaqi.h?.v).toBe(82); expect(realWorldResponse.data.iaqi.p?.v).toBe(1016); // Test that pollutant parsing logic works with this structure const pollutants = ['o3', 'no2', 'so2', 'pm25', 'pm10', 'co']; const parsedValues = {}; pollutants.forEach((pollutant) => { const param = realWorldResponse.data.iaqi[pollutant]?.v; if (param !== undefined) { const aqi = Number.parseFloat(param.toString()); if (!Number.isNaN(aqi)) { parsedValues[pollutant] = aqi; } } }); // Only pm10 should be parsed from this response expect(parsedValues.pm10).toBe(4); expect(parsedValues.o3).toBeUndefined(); expect(parsedValues.pm25).toBeUndefined(); expect(parsedValues.no2).toBeUndefined(); expect(parsedValues.so2).toBeUndefined(); expect(parsedValues.co).toBeUndefined(); }); it('should validate undici request timeout configuration (issue #49)', () => { // Test that timeout configuration is properly structured to prevent ETIMEDOUT errors const timeoutConfig = { headersTimeout: 30000, // 30 seconds for headers bodyTimeout: 30000, // 30 seconds for body }; // Validate timeout values are reasonable (should be > 0 and < 60000ms) expect(timeoutConfig.headersTimeout).toBeGreaterThan(0); expect(timeoutConfig.headersTimeout).toBeLessThanOrEqual(60000); expect(timeoutConfig.bodyTimeout).toBeGreaterThan(0); expect(timeoutConfig.bodyTimeout).toBeLessThanOrEqual(60000); // Ensure both timeout values are present expect(typeof timeoutConfig.headersTimeout).toBe('number'); expect(typeof timeoutConfig.bodyTimeout).toBe('number'); }); }); //# sourceMappingURL=aqicn.test.js.map