homebridge-tsvesync 1.4.8 → 1.4.9
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.
- package/CHANGELOG.md +16 -0
- package/dist/accessories/humidifier.accessory.d.ts +21 -0
- package/dist/accessories/humidifier.accessory.d.ts.map +1 -1
- package/dist/accessories/humidifier.accessory.js +145 -65
- package/dist/accessories/humidifier.accessory.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/accessories/humidifier-write-consistency.test.ts +208 -8
- package/src/accessories/humidifier.accessory.ts +156 -83
- package/tsconfig.json +2 -1
- package/dist/__tests__/setup.d.ts +0 -10
- package/dist/__tests__/setup.d.ts.map +0 -1
- package/dist/__tests__/setup.js +0 -82
- package/dist/__tests__/setup.js.map +0 -1
- package/dist/__tests__/utils/test-helpers.d.ts +0 -279
- package/dist/__tests__/utils/test-helpers.d.ts.map +0 -1
- package/dist/__tests__/utils/test-helpers.js +0 -831
- package/dist/__tests__/utils/test-helpers.js.map +0 -1
|
@@ -219,6 +219,8 @@ const createMockHumidifier = (options?: {
|
|
|
219
219
|
syncState();
|
|
220
220
|
return true;
|
|
221
221
|
}),
|
|
222
|
+
setMistLevel: jest.fn().mockResolvedValue(true),
|
|
223
|
+
changeFanSpeed: jest.fn().mockResolvedValue(true),
|
|
222
224
|
uuid: 'uuid',
|
|
223
225
|
getDetails: jest.fn().mockImplementation(async () => {
|
|
224
226
|
if (options?.staleRefresh) {
|
|
@@ -279,14 +281,14 @@ describe('HumidifierAccessory write consistency', () => {
|
|
|
279
281
|
);
|
|
280
282
|
});
|
|
281
283
|
|
|
282
|
-
it('keeps HomeKit in
|
|
284
|
+
it('keeps HomeKit in manual mode when the first Dual200S refresh is stale after setting mode', async () => {
|
|
283
285
|
const { accessory, humidifierService } = createMockAccessory(platform);
|
|
284
286
|
const device = createMockHumidifier({
|
|
285
287
|
deviceStatus: 'on',
|
|
286
|
-
mode: '
|
|
288
|
+
mode: 'auto',
|
|
287
289
|
staleRefresh: (state) => {
|
|
288
290
|
state.deviceStatus = 'on';
|
|
289
|
-
state.mode = '
|
|
291
|
+
state.mode = 'auto';
|
|
290
292
|
},
|
|
291
293
|
});
|
|
292
294
|
|
|
@@ -294,17 +296,14 @@ describe('HumidifierAccessory write consistency', () => {
|
|
|
294
296
|
humidifierService.updateCharacteristic.mockClear();
|
|
295
297
|
logger.warn.mockClear();
|
|
296
298
|
|
|
299
|
+
// HomeKit state 0 = "Auto" in Home app → VeSync manual for Dual200S
|
|
297
300
|
await (humidifier as any).setTargetState(0);
|
|
298
301
|
|
|
299
|
-
expect(device.
|
|
302
|
+
expect(device.setManualMode).toHaveBeenCalledTimes(1);
|
|
300
303
|
expect(humidifierService.updateCharacteristic.mock.calls).toContainEqual([
|
|
301
304
|
platform.Characteristic.TargetHumidifierDehumidifierState,
|
|
302
305
|
0,
|
|
303
306
|
]);
|
|
304
|
-
expect(humidifierService.updateCharacteristic.mock.calls).toContainEqual([
|
|
305
|
-
platform.Characteristic.CurrentHumidifierDehumidifierState,
|
|
306
|
-
2,
|
|
307
|
-
]);
|
|
308
307
|
expect(humidifierService.updateCharacteristic.mock.calls).not.toContainEqual([
|
|
309
308
|
platform.Characteristic.TargetHumidifierDehumidifierState,
|
|
310
309
|
1,
|
|
@@ -313,4 +312,205 @@ describe('HumidifierAccessory write consistency', () => {
|
|
|
313
312
|
expect.stringContaining('did not change to desired mode'),
|
|
314
313
|
);
|
|
315
314
|
});
|
|
315
|
+
|
|
316
|
+
it('maps Dual200S auto mode to HomeKit target state 1 (Humidity/slider)', async () => {
|
|
317
|
+
const { accessory, humidifierService } = createMockAccessory(platform);
|
|
318
|
+
const device = createMockHumidifier({
|
|
319
|
+
deviceStatus: 'on',
|
|
320
|
+
mode: 'manual',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
324
|
+
humidifierService.updateCharacteristic.mockClear();
|
|
325
|
+
|
|
326
|
+
// HomeKit state 1 = HUMIDIFIER ("Humidity" in Home app, shows slider)
|
|
327
|
+
// For Dual200S, this should trigger VeSync auto mode
|
|
328
|
+
await (humidifier as any).setTargetState(1);
|
|
329
|
+
|
|
330
|
+
expect(device.setAutoMode).toHaveBeenCalledTimes(1);
|
|
331
|
+
expect(device.setManualMode).not.toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('maps Dual200S manual mode to HomeKit target state 0 (Auto/no slider)', async () => {
|
|
335
|
+
const { accessory, humidifierService } = createMockAccessory(platform);
|
|
336
|
+
const device = createMockHumidifier({
|
|
337
|
+
deviceStatus: 'on',
|
|
338
|
+
mode: 'auto',
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
342
|
+
humidifierService.updateCharacteristic.mockClear();
|
|
343
|
+
|
|
344
|
+
// HomeKit state 0 = HUMIDIFIER_OR_DEHUMIDIFIER ("Auto" in Home app, no slider)
|
|
345
|
+
// For Dual200S, this should trigger VeSync manual mode
|
|
346
|
+
await (humidifier as any).setTargetState(0);
|
|
347
|
+
|
|
348
|
+
expect(device.setManualMode).toHaveBeenCalledTimes(1);
|
|
349
|
+
expect(device.setAutoMode).not.toHaveBeenCalled();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('reports Dual200S auto mode as HomeKit target state 1 in state sync', async () => {
|
|
353
|
+
const { accessory, humidifierService } = createMockAccessory(platform);
|
|
354
|
+
const device = createMockHumidifier({
|
|
355
|
+
deviceStatus: 'on',
|
|
356
|
+
mode: 'auto',
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
360
|
+
humidifierService.updateCharacteristic.mockClear();
|
|
361
|
+
|
|
362
|
+
(humidifier as any).applyDeviceStatesToHomeKit(device);
|
|
363
|
+
|
|
364
|
+
expect(humidifierService.updateCharacteristic).toHaveBeenCalledWith(
|
|
365
|
+
platform.Characteristic.TargetHumidifierDehumidifierState,
|
|
366
|
+
1,
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('reports Dual200S manual mode as HomeKit target state 0 in state sync', async () => {
|
|
371
|
+
const { accessory, humidifierService } = createMockAccessory(platform);
|
|
372
|
+
const device = createMockHumidifier({
|
|
373
|
+
deviceStatus: 'on',
|
|
374
|
+
mode: 'manual',
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
378
|
+
humidifierService.updateCharacteristic.mockClear();
|
|
379
|
+
|
|
380
|
+
(humidifier as any).applyDeviceStatesToHomeKit(device);
|
|
381
|
+
|
|
382
|
+
expect(humidifierService.updateCharacteristic).toHaveBeenCalledWith(
|
|
383
|
+
platform.Characteristic.TargetHumidifierDehumidifierState,
|
|
384
|
+
0,
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('detects LUH-D301S-WEU as isHumidDual200S and isHumid200300S', () => {
|
|
389
|
+
const { accessory } = createMockAccessory(platform);
|
|
390
|
+
const device = createMockHumidifier({ deviceStatus: 'off', mode: 'manual' });
|
|
391
|
+
(device as any).deviceType = 'LUH-D301S-WEU';
|
|
392
|
+
|
|
393
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
394
|
+
|
|
395
|
+
expect((humidifier as any).isHumidDual200S).toBe(true);
|
|
396
|
+
expect((humidifier as any).isHumid200300S).toBe(true);
|
|
397
|
+
expect((humidifier as any).isHumid200S).toBe(false);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('detects Dual200S as isHumidDual200S, not isHumid200S', () => {
|
|
401
|
+
const { accessory } = createMockAccessory(platform);
|
|
402
|
+
const device = createMockHumidifier({ deviceStatus: 'off', mode: 'manual' });
|
|
403
|
+
|
|
404
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
405
|
+
|
|
406
|
+
expect((humidifier as any).isHumidDual200S).toBe(true);
|
|
407
|
+
expect((humidifier as any).isHumid200300S).toBe(true);
|
|
408
|
+
expect((humidifier as any).isHumid200S).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('maps Dual200S mist level 1 to 50% rotation speed', async () => {
|
|
412
|
+
const { accessory, humidifierService } = createMockAccessory(platform);
|
|
413
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'manual' });
|
|
414
|
+
device.mistLevel = 1;
|
|
415
|
+
|
|
416
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
417
|
+
humidifierService.updateCharacteristic.mockClear();
|
|
418
|
+
|
|
419
|
+
(humidifier as any).applyDeviceStatesToHomeKit(device);
|
|
420
|
+
|
|
421
|
+
expect(humidifierService.updateCharacteristic).toHaveBeenCalledWith(
|
|
422
|
+
platform.Characteristic.RotationSpeed,
|
|
423
|
+
50,
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('maps Dual200S mist level 2 to 100% rotation speed', async () => {
|
|
428
|
+
const { accessory, humidifierService } = createMockAccessory(platform);
|
|
429
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'manual' });
|
|
430
|
+
device.mistLevel = 2;
|
|
431
|
+
|
|
432
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
433
|
+
humidifierService.updateCharacteristic.mockClear();
|
|
434
|
+
|
|
435
|
+
(humidifier as any).applyDeviceStatesToHomeKit(device);
|
|
436
|
+
|
|
437
|
+
expect(humidifierService.updateCharacteristic).toHaveBeenCalledWith(
|
|
438
|
+
platform.Characteristic.RotationSpeed,
|
|
439
|
+
100,
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('converts Dual200S rotation speed 50% to mist level 1', async () => {
|
|
444
|
+
const { accessory } = createMockAccessory(platform);
|
|
445
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'manual' });
|
|
446
|
+
device.setMistLevel = jest.fn().mockResolvedValue(true);
|
|
447
|
+
|
|
448
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
449
|
+
|
|
450
|
+
await (humidifier as any).handleSetRotationSpeed(50);
|
|
451
|
+
|
|
452
|
+
expect(device.setMistLevel).toHaveBeenCalledWith(1);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('converts Dual200S rotation speed 100% to mist level 2', async () => {
|
|
456
|
+
const { accessory } = createMockAccessory(platform);
|
|
457
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'manual' });
|
|
458
|
+
device.setMistLevel = jest.fn().mockResolvedValue(true);
|
|
459
|
+
|
|
460
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
461
|
+
|
|
462
|
+
await (humidifier as any).handleSetRotationSpeed(100);
|
|
463
|
+
|
|
464
|
+
expect(device.setMistLevel).toHaveBeenCalledWith(2);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('snaps Dual200S rotation speed 30% down to mist level 1', async () => {
|
|
468
|
+
const { accessory } = createMockAccessory(platform);
|
|
469
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'manual' });
|
|
470
|
+
device.setMistLevel = jest.fn().mockResolvedValue(true);
|
|
471
|
+
|
|
472
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
473
|
+
|
|
474
|
+
await (humidifier as any).handleSetRotationSpeed(30);
|
|
475
|
+
|
|
476
|
+
expect(device.setMistLevel).toHaveBeenCalledWith(1);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('snaps Dual200S rotation speed 80% up to mist level 2', async () => {
|
|
480
|
+
const { accessory } = createMockAccessory(platform);
|
|
481
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'manual' });
|
|
482
|
+
device.setMistLevel = jest.fn().mockResolvedValue(true);
|
|
483
|
+
|
|
484
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
485
|
+
|
|
486
|
+
await (humidifier as any).handleSetRotationSpeed(80);
|
|
487
|
+
|
|
488
|
+
expect(device.setMistLevel).toHaveBeenCalledWith(2);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('switches Dual200S from auto to manual mode when adjusting rotation speed', async () => {
|
|
492
|
+
const { accessory } = createMockAccessory(platform);
|
|
493
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'auto' });
|
|
494
|
+
device.setMistLevel = jest.fn().mockResolvedValue(true);
|
|
495
|
+
|
|
496
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
497
|
+
|
|
498
|
+
await (humidifier as any).handleSetRotationSpeed(100);
|
|
499
|
+
|
|
500
|
+
expect(device.setManualMode).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(device.setMistLevel).toHaveBeenCalledWith(2);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('does not switch mode when Dual200S is already in manual mode', async () => {
|
|
505
|
+
const { accessory } = createMockAccessory(platform);
|
|
506
|
+
const device = createMockHumidifier({ deviceStatus: 'on', mode: 'manual' });
|
|
507
|
+
device.setMistLevel = jest.fn().mockResolvedValue(true);
|
|
508
|
+
|
|
509
|
+
const humidifier = new HumidifierAccessory(platform, accessory, device as any);
|
|
510
|
+
|
|
511
|
+
await (humidifier as any).handleSetRotationSpeed(50);
|
|
512
|
+
|
|
513
|
+
expect(device.setManualMode).not.toHaveBeenCalled();
|
|
514
|
+
expect(device.setMistLevel).toHaveBeenCalledWith(1);
|
|
515
|
+
});
|
|
316
516
|
});
|
|
@@ -55,6 +55,7 @@ interface ExtendedVeSyncHumidifier extends VeSyncHumidifier {
|
|
|
55
55
|
// For device-specific details
|
|
56
56
|
details?: {
|
|
57
57
|
target_humidity?: number;
|
|
58
|
+
auto_target_humidity?: number;
|
|
58
59
|
night_light_brightness?: number;
|
|
59
60
|
current_humidity?: number;
|
|
60
61
|
water_lacks?: boolean;
|
|
@@ -83,6 +84,7 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
83
84
|
private capabilities: DeviceCapabilities; // Removed readonly to allow re-initialization
|
|
84
85
|
|
|
85
86
|
// Device type flags
|
|
87
|
+
private isHumidDual200S: boolean;
|
|
86
88
|
private isHumid200S: boolean;
|
|
87
89
|
private isHumid200300S: boolean;
|
|
88
90
|
private isHumid1000S: boolean;
|
|
@@ -93,6 +95,11 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
93
95
|
private temperatureService?: Service;
|
|
94
96
|
private filterService?: Service;
|
|
95
97
|
|
|
98
|
+
// Tracks the last mode we commanded, to protect against stale API responses.
|
|
99
|
+
// Expires after 30s so out-of-band changes (VeSync app) are not masked.
|
|
100
|
+
private _lastCommandedMode: string | null = null;
|
|
101
|
+
private _lastCommandedModeTime: number = 0;
|
|
102
|
+
|
|
96
103
|
constructor(
|
|
97
104
|
platform: TSVESyncPlatform,
|
|
98
105
|
accessory: PlatformAccessory,
|
|
@@ -114,14 +121,17 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
114
121
|
hasSwingMode: false,
|
|
115
122
|
};
|
|
116
123
|
|
|
117
|
-
// Detect device type
|
|
124
|
+
// Detect device type — order matters: specific types first
|
|
125
|
+
this.isHumidDual200S = this.detectHumidDual200S();
|
|
118
126
|
this.isHumid200S = this.detectHumid200S();
|
|
119
127
|
this.isHumid200300S = this.detectHumid200300S();
|
|
120
128
|
this.isHumid1000S = this.detectHumid1000S();
|
|
121
129
|
this.isSuperior6000S = this.detectSuperior6000S();
|
|
122
|
-
|
|
130
|
+
|
|
123
131
|
// Log detected device type
|
|
124
|
-
if (this.
|
|
132
|
+
if (this.isHumidDual200S) {
|
|
133
|
+
this.platform.log.debug(`Detected Dual200S device: ${this.device.deviceName}`);
|
|
134
|
+
} else if (this.isHumid200S) {
|
|
125
135
|
this.platform.log.debug(`Detected Humid200S device: ${this.device.deviceName}`);
|
|
126
136
|
} else if (this.isHumid200300S) {
|
|
127
137
|
this.platform.log.debug(`Detected Humid200300S device: ${this.device.deviceName}`);
|
|
@@ -134,6 +144,14 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
134
144
|
}
|
|
135
145
|
}
|
|
136
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Detect if the device is a HumidDual200S (or regional variant LUH-D301S)
|
|
149
|
+
*/
|
|
150
|
+
private detectHumidDual200S(): boolean {
|
|
151
|
+
const deviceType = this.device.deviceType.toUpperCase();
|
|
152
|
+
return deviceType.includes('DUAL200S') || deviceType.includes('LUH-D301S');
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
/**
|
|
138
156
|
* Detect if the device is a Humid200S
|
|
139
157
|
*/
|
|
@@ -145,7 +163,11 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
145
163
|
return true;
|
|
146
164
|
}
|
|
147
165
|
|
|
148
|
-
// Check for mist level range 1-3 which is specific to Humid200S
|
|
166
|
+
// Check for mist level range 1-3 which is specific to Humid200S,
|
|
167
|
+
// but exclude Dual200S which also has low mist levels
|
|
168
|
+
if (this.isHumidDual200S) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
149
171
|
const extendedDevice = this.device as ExtendedVeSyncHumidifier;
|
|
150
172
|
if (extendedDevice.mistLevel !== undefined && extendedDevice.mistLevel <= 3) {
|
|
151
173
|
return true;
|
|
@@ -167,7 +189,7 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
167
189
|
|
|
168
190
|
// Check device type for Classic300S or Dual200S
|
|
169
191
|
const deviceType = this.device.deviceType.toUpperCase();
|
|
170
|
-
if (deviceType.includes('CLASSIC300S') ||
|
|
192
|
+
if (deviceType.includes('CLASSIC300S') || this.isHumidDual200S) {
|
|
171
193
|
return true;
|
|
172
194
|
}
|
|
173
195
|
|
|
@@ -235,6 +257,45 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
235
257
|
return false;
|
|
236
258
|
}
|
|
237
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Convert a VeSync mode string to a HomeKit TargetHumidifierDehumidifierState value.
|
|
262
|
+
*
|
|
263
|
+
* For Dual200S the mapping is swapped so that VeSync "auto" (which needs a
|
|
264
|
+
* humidity target) lands on HomeKit state 1 (HUMIDIFIER / "Humidity" in the
|
|
265
|
+
* Home app) where Apple shows the humidity slider, and VeSync "manual"
|
|
266
|
+
* (fixed mist, no target) lands on state 0 ("Auto", no slider).
|
|
267
|
+
*/
|
|
268
|
+
private modeToTargetState(mode: string | undefined): number {
|
|
269
|
+
// Prefer last commanded mode to protect against stale API responses.
|
|
270
|
+
// Clear the override once the device reports the same mode (caught up).
|
|
271
|
+
let effectiveMode = mode;
|
|
272
|
+
if (this._lastCommandedMode !== null) {
|
|
273
|
+
const elapsed = Date.now() - this._lastCommandedModeTime;
|
|
274
|
+
if (mode === this._lastCommandedMode || elapsed > 30000) {
|
|
275
|
+
this._lastCommandedMode = null;
|
|
276
|
+
} else {
|
|
277
|
+
this.platform.log.debug(`Using commanded mode '${this._lastCommandedMode}' instead of device-reported '${mode}'`);
|
|
278
|
+
effectiveMode = this._lastCommandedMode;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (this.isHumidDual200S) {
|
|
283
|
+
return effectiveMode === 'auto' ? 1 : 0;
|
|
284
|
+
}
|
|
285
|
+
return effectiveMode === 'auto' ? 0 : 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Convert a HomeKit TargetHumidifierDehumidifierState value to a VeSync mode.
|
|
290
|
+
* Inverse of modeToTargetState.
|
|
291
|
+
*/
|
|
292
|
+
private targetStateToMode(value: CharacteristicValue): string {
|
|
293
|
+
if (this.isHumidDual200S) {
|
|
294
|
+
return value === 1 ? 'auto' : 'manual';
|
|
295
|
+
}
|
|
296
|
+
return value === 0 ? 'auto' : 'manual';
|
|
297
|
+
}
|
|
298
|
+
|
|
238
299
|
protected setupService(): void {
|
|
239
300
|
// No need to check for capabilities initialization since we set defaults in constructor
|
|
240
301
|
|
|
@@ -242,12 +303,22 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
242
303
|
this.service = this.accessory.getService(this.platform.Service.HumidifierDehumidifier) ||
|
|
243
304
|
this.accessory.addService(this.platform.Service.HumidifierDehumidifier);
|
|
244
305
|
|
|
245
|
-
// Initialize
|
|
306
|
+
// Initialize characteristics
|
|
246
307
|
this.service.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, 40);
|
|
247
308
|
this.service.updateCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold, 60);
|
|
248
309
|
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHumidifierDehumidifierState, 1);
|
|
249
310
|
this.service.updateCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState, 1);
|
|
250
|
-
|
|
311
|
+
|
|
312
|
+
// Register humidity handlers IMMEDIATELY after updateCharacteristic — no separation.
|
|
313
|
+
// The updateCharacteristic creates the instance; onGet/onSet must be on that same instance.
|
|
314
|
+
this.service
|
|
315
|
+
.getCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold)
|
|
316
|
+
.setProps({ minValue: 30, maxValue: 80, minStep: 1 })
|
|
317
|
+
.onGet(this.getTargetHumidity.bind(this))
|
|
318
|
+
.onSet(async (value: CharacteristicValue) => {
|
|
319
|
+
this.platform.log.info(`Setting target humidity to ${value}% for ${this.device.deviceName}`);
|
|
320
|
+
await this.setTargetHumidity(value);
|
|
321
|
+
});
|
|
251
322
|
|
|
252
323
|
// Set up required characteristics
|
|
253
324
|
this.setupCharacteristic(
|
|
@@ -274,7 +345,7 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
274
345
|
|
|
275
346
|
this.setupCharacteristic(
|
|
276
347
|
this.platform.Characteristic.TargetHumidifierDehumidifierState,
|
|
277
|
-
async () => extendedDevice.mode
|
|
348
|
+
async () => this.modeToTargetState(extendedDevice.mode),
|
|
278
349
|
this.setTargetState.bind(this)
|
|
279
350
|
);
|
|
280
351
|
|
|
@@ -285,49 +356,30 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
285
356
|
this.handleSetRotationSpeed.bind(this)
|
|
286
357
|
);
|
|
287
358
|
|
|
359
|
+
// Constrain rotation speed to 2 steps for Dual200S (Low=50%, High=100%)
|
|
360
|
+
if (this.isHumidDual200S) {
|
|
361
|
+
const rotationSpeedChar = this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed);
|
|
362
|
+
rotationSpeedChar.setProps({
|
|
363
|
+
minValue: 0,
|
|
364
|
+
maxValue: 100,
|
|
365
|
+
minStep: 50,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
288
369
|
// Add Name characteristic
|
|
289
370
|
this.setupCharacteristic(
|
|
290
371
|
this.platform.Characteristic.Name,
|
|
291
372
|
async () => this.device.deviceName
|
|
292
373
|
);
|
|
293
374
|
|
|
294
|
-
//
|
|
375
|
+
// CurrentRelativeHumidity getter
|
|
295
376
|
if (this.capabilities && this.capabilities.hasHumidity) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.service.addCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Configure the target humidity characteristic with proper properties
|
|
306
|
-
const targetHumidityChar = this.service.getCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold);
|
|
307
|
-
targetHumidityChar.setProps({
|
|
308
|
-
minValue: 30,
|
|
309
|
-
maxValue: 80,
|
|
310
|
-
minStep: 1
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Set an initial value to ensure the characteristic is not blank
|
|
314
|
-
targetHumidityChar.updateValue(45);
|
|
315
|
-
this.platform.log.debug(`Configured RelativeHumidityHumidifierThreshold characteristic with initial value 45%`);
|
|
316
|
-
|
|
317
|
-
// Set up handlers for target humidity
|
|
318
|
-
this.setupCharacteristic(
|
|
319
|
-
this.platform.Characteristic.RelativeHumidityHumidifierThreshold,
|
|
320
|
-
this.getTargetHumidity.bind(this),
|
|
321
|
-
this.setTargetHumidity.bind(this)
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
// Immediately get and set the actual target humidity to ensure it's displayed correctly
|
|
325
|
-
this.getTargetHumidity().then(targetHumidity => {
|
|
326
|
-
this.platform.log.debug(`Initial target humidity value: ${targetHumidity}%`);
|
|
327
|
-
targetHumidityChar.updateValue(targetHumidity);
|
|
328
|
-
}).catch(error => {
|
|
329
|
-
this.platform.log.warn(`Failed to get initial target humidity: ${error}`);
|
|
330
|
-
});
|
|
377
|
+
this.service
|
|
378
|
+
.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity)
|
|
379
|
+
.onGet(async () => {
|
|
380
|
+
const extDev = this.device as ExtendedVeSyncHumidifier;
|
|
381
|
+
return extDev.currentHumidity ?? extDev.details?.current_humidity ?? 0;
|
|
382
|
+
});
|
|
331
383
|
}
|
|
332
384
|
|
|
333
385
|
// Add Water Level characteristic if supported (mapping inferred from water_lacks/water_tank_lifted)
|
|
@@ -650,13 +702,16 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
650
702
|
|
|
651
703
|
this.service.updateCharacteristic(
|
|
652
704
|
this.platform.Characteristic.TargetHumidifierDehumidifierState,
|
|
653
|
-
extendedDevice.mode
|
|
705
|
+
this.modeToTargetState(extendedDevice.mode)
|
|
654
706
|
);
|
|
655
707
|
|
|
656
708
|
let rotationSpeed = 0;
|
|
657
709
|
|
|
658
710
|
if (isActive) {
|
|
659
|
-
if (this.
|
|
711
|
+
if (this.isHumidDual200S && extendedDevice.mistLevel !== undefined) {
|
|
712
|
+
// Dual200S has 2 levels: 1 (Low) = 50%, 2 (High) = 100%
|
|
713
|
+
rotationSpeed = extendedDevice.mistLevel === 1 ? 50 : 100;
|
|
714
|
+
} else if (this.isHumid200300S && extendedDevice.mistLevel !== undefined) {
|
|
660
715
|
const mistLevel = extendedDevice.mistLevel;
|
|
661
716
|
this.platform.log.debug(`Device ${this.device.deviceName} mist level: ${mistLevel}`);
|
|
662
717
|
|
|
@@ -911,6 +966,16 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
911
966
|
}
|
|
912
967
|
|
|
913
968
|
private async getRotationSpeed(): Promise<CharacteristicValue> {
|
|
969
|
+
const extendedDevice = this.device as ExtendedVeSyncHumidifier;
|
|
970
|
+
|
|
971
|
+
if (this.isHumidDual200S) {
|
|
972
|
+
const level = extendedDevice.mistLevel;
|
|
973
|
+
if (level === undefined || level === null || level === 0) {
|
|
974
|
+
return 0;
|
|
975
|
+
}
|
|
976
|
+
return level === 1 ? 50 : 100;
|
|
977
|
+
}
|
|
978
|
+
|
|
914
979
|
if (this.device.speed === undefined || this.device.speed === null) {
|
|
915
980
|
return 0;
|
|
916
981
|
}
|
|
@@ -947,23 +1012,16 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
947
1012
|
}
|
|
948
1013
|
}
|
|
949
1014
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
case 2: // Dehumidifier (not supported, but we'll handle it gracefully)
|
|
961
|
-
this.platform.log.warn(`Dehumidifier mode not supported for device: ${this.device.deviceName}, using manual mode instead`);
|
|
962
|
-
mode = 'manual';
|
|
963
|
-
break;
|
|
964
|
-
default:
|
|
965
|
-
this.platform.log.warn(`Unknown target state value: ${value}, using manual mode as default`);
|
|
966
|
-
mode = 'manual';
|
|
1015
|
+
let mode: string;
|
|
1016
|
+
|
|
1017
|
+
if (value === 2) {
|
|
1018
|
+
this.platform.log.warn(`Dehumidifier mode not supported for device: ${this.device.deviceName}, using manual mode instead`);
|
|
1019
|
+
mode = 'manual';
|
|
1020
|
+
} else if (value === 0 || value === 1) {
|
|
1021
|
+
mode = this.targetStateToMode(value);
|
|
1022
|
+
} else {
|
|
1023
|
+
this.platform.log.warn(`Unknown target state value: ${value}, using manual mode as default`);
|
|
1024
|
+
mode = 'manual';
|
|
967
1025
|
}
|
|
968
1026
|
|
|
969
1027
|
// Check current mode first
|
|
@@ -971,7 +1029,7 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
971
1029
|
this.platform.log.debug(`Device ${this.device.deviceName} is already in ${mode} mode, skipping API call`);
|
|
972
1030
|
|
|
973
1031
|
// Update target state to reflect the current mode
|
|
974
|
-
const targetState = mode
|
|
1032
|
+
const targetState = this.modeToTargetState(mode);
|
|
975
1033
|
this.updateCharacteristicValue(
|
|
976
1034
|
this.platform.Characteristic.TargetHumidifierDehumidifierState,
|
|
977
1035
|
targetState
|
|
@@ -1017,6 +1075,10 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
1017
1075
|
throw new Error(`Failed to set device mode to ${mode}`);
|
|
1018
1076
|
}
|
|
1019
1077
|
|
|
1078
|
+
// Track commanded mode so stale getDetails() responses don't revert HomeKit
|
|
1079
|
+
this._lastCommandedMode = mode;
|
|
1080
|
+
this._lastCommandedModeTime = Date.now();
|
|
1081
|
+
|
|
1020
1082
|
this.applyDeviceStatesToHomeKit(this.device);
|
|
1021
1083
|
} catch (error) {
|
|
1022
1084
|
this.handleDeviceError('set target state', error);
|
|
@@ -1038,9 +1100,12 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
1038
1100
|
return;
|
|
1039
1101
|
}
|
|
1040
1102
|
|
|
1041
|
-
// Convert
|
|
1103
|
+
// Convert percentage to mist level based on device type
|
|
1042
1104
|
let speed: number;
|
|
1043
|
-
if (
|
|
1105
|
+
if (this.isHumidDual200S) {
|
|
1106
|
+
// Dual200S: 2 levels — 1-50% = Low (1), 51-100% = High (2)
|
|
1107
|
+
speed = percentage <= 50 ? 1 : 2;
|
|
1108
|
+
} else if (percentage <= 11) {
|
|
1044
1109
|
speed = 1;
|
|
1045
1110
|
} else if (percentage <= 22) {
|
|
1046
1111
|
speed = 2;
|
|
@@ -1063,7 +1128,19 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
1063
1128
|
// Adjust speed based on device type
|
|
1064
1129
|
const extendedDevice = this.device as ExtendedVeSyncHumidifier;
|
|
1065
1130
|
let success = false;
|
|
1066
|
-
|
|
1131
|
+
|
|
1132
|
+
// Dual200S: adjusting speed while in auto mode implies the user wants manual control
|
|
1133
|
+
if (this.isHumidDual200S && extendedDevice.mode === 'auto') {
|
|
1134
|
+
this.platform.log.debug(`Switching Dual200S to manual mode before setting mist level: ${this.device.deviceName}`);
|
|
1135
|
+
if (typeof extendedDevice.setManualMode === 'function') {
|
|
1136
|
+
await extendedDevice.setManualMode();
|
|
1137
|
+
} else if (typeof extendedDevice.setMode === 'function') {
|
|
1138
|
+
await extendedDevice.setMode('manual');
|
|
1139
|
+
}
|
|
1140
|
+
this._lastCommandedMode = 'manual';
|
|
1141
|
+
this._lastCommandedModeTime = Date.now();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1067
1144
|
// For Humid200S devices, limit mist level to 1-3
|
|
1068
1145
|
if (this.isHumid200S) {
|
|
1069
1146
|
// Limit speed to 1-3 for Humid200S devices
|
|
@@ -1154,6 +1231,8 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
1154
1231
|
if (!turnOnSuccess) {
|
|
1155
1232
|
throw new Error('Failed to turn on device before setting humidity');
|
|
1156
1233
|
}
|
|
1234
|
+
// Sync Active state immediately so HomeKit tile reflects power-on
|
|
1235
|
+
this.service.updateCharacteristic(this.platform.Characteristic.Active, 1);
|
|
1157
1236
|
}
|
|
1158
1237
|
|
|
1159
1238
|
let success = false;
|
|
@@ -1176,27 +1255,21 @@ export class HumidifierAccessory extends BaseAccessory {
|
|
|
1176
1255
|
if (!success) {
|
|
1177
1256
|
throw new Error(`Failed to set target humidity to ${targetHumidity}%`);
|
|
1178
1257
|
}
|
|
1179
|
-
|
|
1180
|
-
//
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1258
|
+
|
|
1259
|
+
// Push the commanded value to HomeKit and update in-memory state.
|
|
1260
|
+
// Do NOT call getDetails/updateDeviceSpecificStates here — the API
|
|
1261
|
+
// returns stale target humidity, causing the slider to snap back.
|
|
1262
|
+
// The next polling cycle will eventually sync the actual value.
|
|
1263
|
+
if (extendedDevice.details) {
|
|
1264
|
+
extendedDevice.details.target_humidity = targetHumidity;
|
|
1265
|
+
// Humid200300S's humidity getter reads auto_target_humidity
|
|
1266
|
+
extendedDevice.details.auto_target_humidity = targetHumidity;
|
|
1188
1267
|
}
|
|
1189
|
-
|
|
1190
|
-
// Update the target humidity characteristic
|
|
1191
1268
|
this.service.updateCharacteristic(
|
|
1192
1269
|
this.platform.Characteristic.RelativeHumidityHumidifierThreshold,
|
|
1193
1270
|
targetHumidity
|
|
1194
1271
|
);
|
|
1195
|
-
|
|
1196
|
-
// Update device state and characteristics
|
|
1197
|
-
await this.updateDeviceSpecificStates(this.device);
|
|
1198
|
-
|
|
1199
|
-
// Persist the target humidity
|
|
1272
|
+
|
|
1200
1273
|
await this.persistDeviceState('targetHumidity', targetHumidity);
|
|
1201
1274
|
} catch (error) {
|
|
1202
1275
|
this.handleDeviceError('set target humidity', error);
|
package/tsconfig.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/// <reference types="jest" />
|
|
2
|
-
export declare const TEST_CONFIG: {
|
|
3
|
-
username: string | undefined;
|
|
4
|
-
password: string | undefined;
|
|
5
|
-
apiUrl: string | undefined;
|
|
6
|
-
};
|
|
7
|
-
export declare const canRunIntegrationTests: () => boolean;
|
|
8
|
-
export declare const skipWithoutCredentials: () => jest.It;
|
|
9
|
-
export declare const resetApiUrl: () => void;
|
|
10
|
-
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":";AAwCA,eAAO,MAAM,WAAW;;;;CAIvB,CAAC;AAaF,eAAO,MAAM,sBAAsB,QAAO,OAMzC,CAAC;AAGF,eAAO,MAAM,sBAAsB,eAKlC,CAAC;AAGF,eAAO,MAAM,WAAW,YAKvB,CAAC"}
|