homebridge-tsvesync 1.3.11 → 1.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,483 +0,0 @@
1
- /**
2
- * Unit tests for AirPurifierAccessory air quality features
3
- */
4
-
5
- import { API } from 'homebridge';
6
- import { Logger } from 'homebridge';
7
- import { CharacteristicValue } from 'homebridge';
8
- import { TSVESyncPlatform } from '../../platform';
9
- import { VeSync } from 'tsvesync';
10
- import { AirPurifierAccessory } from '../../accessories/air-purifier.accessory';
11
- import { createMockLogger, createMockVeSync, createMockService } from '../utils/test-helpers';
12
-
13
- // Extended mock for air purifier with air quality features
14
- interface MockAirPurifierWithAirQuality {
15
- deviceName: string;
16
- deviceType: string;
17
- cid: string;
18
- uuid: string;
19
- deviceStatus: string;
20
- hasFeature: jest.Mock;
21
- airQualityValue: number;
22
- pm1: number;
23
- pm10: number;
24
- filterLife: number;
25
- details?: {
26
- air_quality_value?: number;
27
- pm25?: number;
28
- pm1?: number;
29
- pm10?: number;
30
- filter_life?: number | { percent: number };
31
- };
32
- getDetails: jest.Mock;
33
- turnOn: jest.Mock;
34
- turnOff: jest.Mock;
35
- }
36
-
37
- describe('AirPurifierAccessory Air Quality Features', () => {
38
- let mockAPI: jest.Mocked<API>;
39
- let platform: TSVESyncPlatform;
40
- let logger: jest.Mocked<Logger>;
41
- let api: jest.Mocked<VeSync>;
42
- let airQualityService: any;
43
- let airPurifierService: any;
44
- let infoService: any;
45
-
46
- beforeEach(() => {
47
- logger = createMockLogger();
48
- api = createMockVeSync();
49
-
50
- mockAPI = {
51
- hap: {
52
- Characteristic: {
53
- AirQuality: jest.fn(),
54
- PM2_5Density: jest.fn(),
55
- PM10Density: jest.fn(),
56
- Active: jest.fn(),
57
- CurrentAirPurifierState: jest.fn(),
58
- TargetAirPurifierState: jest.fn(),
59
- RotationSpeed: jest.fn(),
60
- Manufacturer: jest.fn(),
61
- Model: jest.fn(),
62
- SerialNumber: jest.fn(),
63
- },
64
- Service: {
65
- AirPurifier: jest.fn(),
66
- AirQualitySensor: jest.fn(),
67
- AccessoryInformation: jest.fn(),
68
- },
69
- uuid: {
70
- generate: jest.fn(),
71
- },
72
- },
73
- platformAccessory: jest.fn(),
74
- } as any;
75
-
76
- platform = new TSVESyncPlatform(logger, {} as any, mockAPI);
77
- (platform as any).api = api;
78
-
79
- // Setup mock services
80
- airQualityService = createMockService();
81
- airPurifierService = createMockService();
82
- infoService = {
83
- setCharacteristic: jest.fn().mockReturnThis(),
84
- };
85
- });
86
-
87
- function createMockAirPurifier(config: Partial<MockAirPurifierWithAirQuality> = {}): MockAirPurifierWithAirQuality {
88
- return {
89
- deviceName: config.deviceName || 'Test Air Purifier',
90
- deviceType: config.deviceType || 'Core300S',
91
- cid: config.cid || 'test-cid',
92
- uuid: config.uuid || 'test-uuid',
93
- deviceStatus: config.deviceStatus || 'on',
94
- airQualityValue: config.airQualityValue || 25,
95
- pm1: config.pm1 || 15,
96
- pm10: config.pm10 || 35,
97
- filterLife: config.filterLife || 75,
98
- details: config.details || {
99
- air_quality_value: config.airQualityValue || 25,
100
- pm1: config.pm1 || 15,
101
- pm10: config.pm10 || 35,
102
- filter_life: config.filterLife || 75,
103
- },
104
- hasFeature: jest.fn().mockImplementation((feature: string) => {
105
- return feature === 'air_quality';
106
- }),
107
- getDetails: jest.fn().mockResolvedValue(true),
108
- turnOn: jest.fn().mockResolvedValue(true),
109
- turnOff: jest.fn().mockResolvedValue(true),
110
- };
111
- }
112
-
113
- describe('Air Quality Service Setup', () => {
114
- it('should create air quality service for devices with air quality support', () => {
115
- const mockDevice = createMockAirPurifier({ airQualityValue: 20 });
116
-
117
- const accessory = {
118
- getService: jest.fn((service) => {
119
- if (service === platform.Service.AccessoryInformation) return infoService;
120
- if (service === platform.Service.AirPurifier) return airPurifierService;
121
- if (service === platform.Service.AirQualitySensor) return null; // Not created yet
122
- return null;
123
- }),
124
- addService: jest.fn((service) => {
125
- if (service === platform.Service.AirQualitySensor) return airQualityService;
126
- return airQualityService;
127
- }),
128
- };
129
-
130
- const airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any);
131
-
132
- // Verify air quality service was added
133
- expect(accessory.addService).toHaveBeenCalledWith(platform.Service.AirQualitySensor);
134
- });
135
-
136
- it('should not create air quality service for devices without air quality support', () => {
137
- const mockDevice = createMockAirPurifier();
138
- mockDevice.hasFeature = jest.fn().mockReturnValue(false);
139
-
140
- const accessory = {
141
- getService: jest.fn((service) => {
142
- if (service === platform.Service.AccessoryInformation) return infoService;
143
- if (service === platform.Service.AirPurifier) return airPurifierService;
144
- return null;
145
- }),
146
- addService: jest.fn(),
147
- };
148
-
149
- const airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any);
150
-
151
- // Verify air quality service was not added
152
- expect(accessory.addService).not.toHaveBeenCalledWith(platform.Service.AirQualitySensor);
153
- });
154
- });
155
-
156
- describe('Air Quality Value Conversion', () => {
157
- let airPurifier: AirPurifierAccessory;
158
- let mockDevice: MockAirPurifierWithAirQuality;
159
-
160
- beforeEach(() => {
161
- mockDevice = createMockAirPurifier();
162
-
163
- const accessory = {
164
- getService: jest.fn((service) => {
165
- if (service === platform.Service.AccessoryInformation) return infoService;
166
- if (service === platform.Service.AirPurifier) return airPurifierService;
167
- if (service === platform.Service.AirQualitySensor) return airQualityService;
168
- return null;
169
- }),
170
- addService: jest.fn(),
171
- };
172
-
173
- airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any);
174
- });
175
-
176
- it('should convert PM2.5 values to HomeKit air quality scale', async () => {
177
- const testCases = [
178
- { pm25: 0, expected: 1 }, // EXCELLENT
179
- { pm25: 12, expected: 1 }, // EXCELLENT (0-12)
180
- { pm25: 13, expected: 2 }, // GOOD (13-35)
181
- { pm25: 35, expected: 2 }, // GOOD
182
- { pm25: 36, expected: 3 }, // FAIR (36-55)
183
- { pm25: 55, expected: 3 }, // FAIR
184
- { pm25: 56, expected: 4 }, // INFERIOR (56-150)
185
- { pm25: 150, expected: 4 }, // INFERIOR
186
- { pm25: 151, expected: 5 }, // POOR (>150)
187
- { pm25: 300, expected: 5 }, // POOR
188
- ];
189
-
190
- for (const testCase of testCases) {
191
- mockDevice.airQualityValue = testCase.pm25;
192
- mockDevice.details!.air_quality_value = testCase.pm25;
193
-
194
- const result = await (airPurifier as any).getAirQuality();
195
- expect(result).toBe(testCase.expected);
196
- }
197
- });
198
-
199
- it('should handle edge cases in PM2.5 conversion', async () => {
200
- // Test boundary values
201
- mockDevice.airQualityValue = 12.5;
202
- mockDevice.details!.air_quality_value = 12.5;
203
- let result = await (airPurifier as any).getAirQuality();
204
- expect(result).toBe(1); // Should round down to EXCELLENT
205
-
206
- mockDevice.airQualityValue = 35.5;
207
- mockDevice.details!.air_quality_value = 35.5;
208
- result = await (airPurifier as any).getAirQuality();
209
- expect(result).toBe(3); // Should round to FAIR
210
- });
211
-
212
- it('should handle zero PM2.5 values', async () => {
213
- mockDevice.airQualityValue = 0;
214
- mockDevice.details!.air_quality_value = 0;
215
-
216
- const result = await (airPurifier as any).getAirQuality();
217
- expect(result).toBe(1); // EXCELLENT
218
- });
219
-
220
- it('should handle extremely high PM2.5 values', async () => {
221
- mockDevice.airQualityValue = 999;
222
- mockDevice.details!.air_quality_value = 999;
223
-
224
- const result = await (airPurifier as any).getAirQuality();
225
- expect(result).toBe(5); // POOR
226
- });
227
- });
228
-
229
- describe('PM2.5 Density Characteristic', () => {
230
- let airPurifier: AirPurifierAccessory;
231
- let mockDevice: MockAirPurifierWithAirQuality;
232
-
233
- beforeEach(() => {
234
- mockDevice = createMockAirPurifier();
235
-
236
- const accessory = {
237
- getService: jest.fn((service) => {
238
- if (service === platform.Service.AirQualitySensor) return airQualityService;
239
- return airPurifierService;
240
- }),
241
- addService: jest.fn(),
242
- };
243
-
244
- airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any);
245
- });
246
-
247
- it('should return PM2.5 density within HomeKit limits', async () => {
248
- mockDevice.airQualityValue = 42;
249
- mockDevice.details!.air_quality_value = 42;
250
-
251
- const result = await (airPurifier as any).getPM25Density();
252
- expect(result).toBe(42);
253
- expect(result).toBeGreaterThanOrEqual(0);
254
- expect(result).toBeLessThanOrEqual(1000);
255
- });
256
-
257
- it('should clamp PM2.5 values above 1000', async () => {
258
- mockDevice.airQualityValue = 1500;
259
- mockDevice.details!.air_quality_value = 1500;
260
-
261
- const result = await (airPurifier as any).getPM25Density();
262
- expect(result).toBe(1000); // Clamped to maximum
263
- });
264
-
265
- it('should handle negative PM2.5 values', async () => {
266
- mockDevice.airQualityValue = -10;
267
- mockDevice.details!.air_quality_value = -10;
268
-
269
- const result = await (airPurifier as any).getPM25Density();
270
- expect(result).toBe(0); // Clamped to minimum
271
- });
272
-
273
- it('should fallback to pm25 field if air_quality_value is missing', async () => {
274
- mockDevice.airQualityValue = 35;
275
- mockDevice.details!.pm25 = 35;
276
- delete mockDevice.details!.air_quality_value;
277
-
278
- const result = await (airPurifier as any).getPM25Density();
279
- expect(result).toBe(35);
280
- });
281
- });
282
-
283
- describe('PM10 Density Characteristic', () => {
284
- let airPurifier: AirPurifierAccessory;
285
- let mockDevice: MockAirPurifierWithAirQuality;
286
-
287
- beforeEach(() => {
288
- mockDevice = createMockAirPurifier({ pm10: 45 });
289
-
290
- const accessory = {
291
- getService: jest.fn((service) => {
292
- if (service === platform.Service.AirQualitySensor) return airQualityService;
293
- return airPurifierService;
294
- }),
295
- addService: jest.fn(),
296
- };
297
-
298
- airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any);
299
- });
300
-
301
- it('should return PM10 density when available', async () => {
302
- const result = await (airPurifier as any).getPM10Density();
303
- expect(result).toBe(45);
304
- });
305
-
306
- it('should handle PM10 values above HomeKit limit', async () => {
307
- mockDevice.pm10 = 1200;
308
- mockDevice.details!.pm10 = 1200;
309
-
310
- const result = await (airPurifier as any).getPM10Density();
311
- expect(result).toBe(1000); // Clamped to maximum
312
- });
313
-
314
- it('should handle missing PM10 data', async () => {
315
- mockDevice.pm10 = 0;
316
- delete mockDevice.details!.pm10;
317
-
318
- const result = await (airPurifier as any).getPM10Density();
319
- expect(result).toBe(0);
320
- });
321
-
322
- it('should only add PM10 characteristic when data is available', () => {
323
- // Device without PM10 data
324
- const deviceWithoutPM10 = createMockAirPurifier({ pm10: 0 });
325
- delete deviceWithoutPM10.details!.pm10;
326
-
327
- const accessory = {
328
- getService: jest.fn(() => airQualityService),
329
- addService: jest.fn(() => airQualityService),
330
- };
331
-
332
- const setupCharacteristicSpy = jest.spyOn(AirPurifierAccessory.prototype as any, 'setupCharacteristic');
333
-
334
- new AirPurifierAccessory(platform, accessory as any, deviceWithoutPM10 as any);
335
-
336
- // PM10 characteristic should not be set up
337
- const pm10Calls = setupCharacteristicSpy.mock.calls.filter(call =>
338
- call[0] === platform.Characteristic.PM10Density
339
- );
340
- expect(pm10Calls).toHaveLength(0);
341
- });
342
- });
343
-
344
- describe('Air Quality Data Updates', () => {
345
- let airPurifier: AirPurifierAccessory;
346
- let mockDevice: MockAirPurifierWithAirQuality;
347
- let mockCharacteristic: any;
348
-
349
- beforeEach(() => {
350
- mockCharacteristic = {
351
- updateValue: jest.fn(),
352
- };
353
-
354
- mockDevice = createMockAirPurifier();
355
-
356
- const accessory = {
357
- getService: jest.fn(() => ({
358
- getCharacteristic: jest.fn(() => mockCharacteristic),
359
- })),
360
- addService: jest.fn(),
361
- };
362
-
363
- airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any);
364
- });
365
-
366
- it('should update air quality characteristics when device updates', async () => {
367
- // Simulate device update
368
- mockDevice.airQualityValue = 55;
369
- mockDevice.pm10 = 75;
370
-
371
- await (airPurifier as any).updateDeviceState();
372
-
373
- // Verify characteristics were updated (exact calls depend on implementation)
374
- expect(mockCharacteristic.updateValue).toHaveBeenCalled();
375
- });
376
-
377
- it('should handle real-time air quality changes', async () => {
378
- const initialPM25 = 20;
379
- const updatedPM25 = 80;
380
-
381
- // Initial state
382
- mockDevice.airQualityValue = initialPM25;
383
- await (airPurifier as any).getAirQuality();
384
-
385
- // Update state
386
- mockDevice.airQualityValue = updatedPM25;
387
- mockDevice.details!.air_quality_value = updatedPM25;
388
-
389
- const newResult = await (airPurifier as any).getAirQuality();
390
- expect(newResult).toBe(4); // Should be INFERIOR for PM2.5 = 80
391
- });
392
-
393
- it('should handle air quality improvements', async () => {
394
- // Start with poor air quality
395
- mockDevice.airQualityValue = 200;
396
- let result = await (airPurifier as any).getAirQuality();
397
- expect(result).toBe(5); // POOR
398
-
399
- // Air quality improves
400
- mockDevice.airQualityValue = 25;
401
- mockDevice.details!.air_quality_value = 25;
402
- result = await (airPurifier as any).getAirQuality();
403
- expect(result).toBe(2); // GOOD
404
- });
405
- });
406
-
407
- describe('Error Handling', () => {
408
- let airPurifier: AirPurifierAccessory;
409
- let mockDevice: MockAirPurifierWithAirQuality;
410
-
411
- beforeEach(() => {
412
- mockDevice = createMockAirPurifier();
413
-
414
- const accessory = {
415
- getService: jest.fn(() => airQualityService),
416
- addService: jest.fn(),
417
- };
418
-
419
- airPurifier = new AirPurifierAccessory(platform, accessory as any, mockDevice as any);
420
- });
421
-
422
- it('should handle missing air quality data gracefully', async () => {
423
- mockDevice.airQualityValue = 0;
424
- delete mockDevice.details!.air_quality_value;
425
- delete mockDevice.details!.pm25;
426
-
427
- const result = await (airPurifier as any).getAirQuality();
428
- expect(result).toBe(1); // Default to EXCELLENT for 0 value
429
- });
430
-
431
- it('should handle device API errors', async () => {
432
- mockDevice.getDetails = jest.fn().mockRejectedValue(new Error('API Error'));
433
-
434
- // Should not throw error
435
- await expect((airPurifier as any).getAirQuality()).resolves.toBeDefined();
436
- });
437
-
438
- it('should handle corrupted device data', async () => {
439
- mockDevice.details = null as any;
440
-
441
- const result = await (airPurifier as any).getAirQuality();
442
- expect(result).toBe(1); // Default to EXCELLENT
443
- });
444
-
445
- it('should handle non-numeric air quality values', async () => {
446
- mockDevice.airQualityValue = NaN;
447
- mockDevice.details!.air_quality_value = NaN;
448
-
449
- const result = await (airPurifier as any).getAirQuality();
450
- expect(result).toBe(1); // Default to EXCELLENT for NaN
451
- });
452
- });
453
-
454
- describe('Feature Detection', () => {
455
- it('should detect air quality support correctly', () => {
456
- const deviceWithAirQuality = createMockAirPurifier();
457
- deviceWithAirQuality.hasFeature = jest.fn().mockReturnValue(true);
458
-
459
- const accessory = {
460
- getService: jest.fn(),
461
- addService: jest.fn(() => airQualityService),
462
- };
463
-
464
- const airPurifier = new AirPurifierAccessory(platform, accessory as any, deviceWithAirQuality as any);
465
-
466
- expect(deviceWithAirQuality.hasFeature).toHaveBeenCalledWith('air_quality');
467
- });
468
-
469
- it('should handle devices without air quality support', () => {
470
- const deviceWithoutAirQuality = createMockAirPurifier();
471
- deviceWithoutAirQuality.hasFeature = jest.fn().mockReturnValue(false);
472
-
473
- const accessory = {
474
- getService: jest.fn(),
475
- addService: jest.fn(),
476
- };
477
-
478
- new AirPurifierAccessory(platform, accessory as any, deviceWithoutAirQuality as any);
479
-
480
- expect(accessory.addService).not.toHaveBeenCalledWith(platform.Service.AirQualitySensor);
481
- });
482
- });
483
- });