UNPKG

homebridge-tsvesync

Version:

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

232 lines (199 loc) 7.3 kB
import { API, Logger } from 'homebridge'; import { TSVESyncPlatform } from '../../platform'; import { VeSync } from 'tsvesync'; import { AirQualitySensorAccessory } from '../../accessories/air-quality-sensor.accessory'; import { createMockLogger, createMockVeSync } from '../utils/test-helpers'; interface MockAirQualityDevice { deviceName: string; deviceType: string; cid: string; uuid: string; deviceStatus: string; airQualityValue: number; details: Record<string, any>; getDetails: jest.Mock; getNormalizedAirQuality?: jest.Mock; airQualityLevel?: number; airQualityLabel?: string; } describe('AirQualitySensorAccessory', () => { let mockAPI: jest.Mocked<API>; let platform: TSVESyncPlatform; let logger: jest.Mocked<Logger>; let api: jest.Mocked<VeSync>; const AirQualityEnum = { UNKNOWN: 0, EXCELLENT: 1, GOOD: 2, FAIR: 3, INFERIOR: 4, POOR: 5, } as const; const ServiceEnum = { AirQualitySensor: 'AirQualitySensor', AccessoryInformation: 'AccessoryInformation', } as const; const createDevice = (overrides: Partial<MockAirQualityDevice> = {}): MockAirQualityDevice => ({ deviceName: overrides.deviceName || 'Dining Room AQ', deviceType: overrides.deviceType || 'Core300S', cid: overrides.cid || 'cid-123', uuid: overrides.uuid || 'uuid-123', deviceStatus: overrides.deviceStatus || 'on', airQualityValue: overrides.airQualityValue ?? 18, details: overrides.details || { air_quality_value: overrides.airQualityValue ?? 18, air_quality_level: overrides.airQualityLevel, air_quality: overrides.airQualityLabel ?? 'good', pm10: 22, }, getDetails: overrides.getDetails || jest.fn().mockResolvedValue(true), getNormalizedAirQuality: overrides.getNormalizedAirQuality, airQualityLevel: overrides.airQualityLevel, airQualityLabel: overrides.airQualityLabel, }); const buildAccessory = (device: MockAirQualityDevice) => { const airQualityService = { getCharacteristic: jest.fn().mockImplementation((characteristic) => { if (characteristic === AirQualityEnum) { return { setProps: jest.fn().mockReturnThis(), onGet: jest.fn().mockReturnThis(), updateValue: jest.fn().mockReturnThis(), }; } return { onGet: jest.fn().mockReturnThis(), updateValue: jest.fn().mockReturnThis(), setProps: jest.fn().mockReturnThis(), }; }), setCharacteristic: jest.fn().mockReturnThis(), updateCharacteristic: jest.fn().mockReturnThis(), } as any; const infoService = { setCharacteristic: jest.fn().mockReturnThis(), } as any; const accessory = { getService: jest.fn((service) => { if (service === ServiceEnum.AccessoryInformation) return infoService; if (service === ServiceEnum.AirQualitySensor) return null; return null; }), addService: jest.fn((service) => { if (service === ServiceEnum.AirQualitySensor) { return airQualityService; } return airQualityService; }), } as any; const accessoryInstance = new AirQualitySensorAccessory(platform, accessory, device as any); return { accessoryInstance, airQualityService, infoService, accessory, }; }; beforeEach(() => { logger = createMockLogger(); api = createMockVeSync(); mockAPI = { hap: { Characteristic: { AirQuality: AirQualityEnum, PM2_5Density: 'PM2_5Density', PM10Density: 'PM10Density', Manufacturer: 'Manufacturer', Model: 'Model', SerialNumber: 'SerialNumber', Name: 'Name', }, Service: ServiceEnum, uuid: { generate: jest.fn(), }, }, platformAccessory: jest.fn(), } as unknown as jest.Mocked<API>; platform = new TSVESyncPlatform(logger, { debug: false, retry: { maxRetries: 3 } } as any, mockAPI); (platform as any).api = api; }); it('configures AirQuality characteristic to expose VeSync levels', () => { const device = createDevice({ getNormalizedAirQuality: jest.fn().mockReturnValue({ level: 2, label: 'good' }), airQualityLevel: 2, airQualityLabel: 'good', }); const { airQualityService } = buildAccessory(device); expect(airQualityService.getCharacteristic).toHaveBeenCalledWith(AirQualityEnum); const setter = airQualityService.getCharacteristic.mock.results[0].value.setProps; expect(setter).toHaveBeenCalledWith({ validValues: [ AirQualityEnum.UNKNOWN, AirQualityEnum.EXCELLENT, AirQualityEnum.GOOD, AirQualityEnum.FAIR, AirQualityEnum.INFERIOR, ], }); }); it('returns normalized air quality levels when provided by the device', async () => { const device = createDevice({ getNormalizedAirQuality: jest.fn().mockReturnValue({ level: 3, label: 'moderate' }), airQualityValue: 42, }); const { accessoryInstance } = buildAccessory(device); const value = await (accessoryInstance as any).getAirQuality(); expect(value).toBe(AirQualityEnum.FAIR); expect(device.getNormalizedAirQuality).toHaveBeenCalled(); }); it('falls back to direct level properties when normalized metadata is unavailable', async () => { const device = createDevice({ getNormalizedAirQuality: undefined, airQualityLevel: 4, details: { air_quality_level: 4, air_quality_value: 80, }, airQualityValue: 80, }); const { accessoryInstance } = buildAccessory(device); const value = await (accessoryInstance as any).getAirQuality(); expect(value).toBe(AirQualityEnum.INFERIOR); }); it('falls back to PM2.5 heuristic when no level information is available', async () => { const device = createDevice({ getNormalizedAirQuality: undefined, airQualityLevel: undefined, details: { air_quality_value: 120, }, airQualityValue: 120, }); const { accessoryInstance } = buildAccessory(device); const value = await (accessoryInstance as any).getAirQuality(); expect(value).toBe(AirQualityEnum.INFERIOR); }); it('clamps PM2.5 density to HomeKit-supported range', async () => { const device = createDevice({ airQualityValue: 1450, details: { air_quality_value: 1450, }, }); const { accessoryInstance } = buildAccessory(device); const density = await (accessoryInstance as any).getPM25Density(); expect(density).toBe(1000); }); it('updates characteristics during refresh cycle', async () => { const device = createDevice({ airQualityValue: 24, getNormalizedAirQuality: jest.fn().mockReturnValue({ level: 2, label: 'good' }), }); const { accessoryInstance, airQualityService } = buildAccessory(device); await (accessoryInstance as any).updateCharacteristics(); expect(device.getDetails).toHaveBeenCalled(); expect(airQualityService.updateCharacteristic).toHaveBeenCalledWith(AirQualityEnum, AirQualityEnum.GOOD); expect(airQualityService.updateCharacteristic).toHaveBeenCalledWith('PM2_5Density', 24); }); });