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.
@@ -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 auto mode when the first Dual200S refresh is stale after setting mode', async () => {
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: 'manual',
288
+ mode: 'auto',
287
289
  staleRefresh: (state) => {
288
290
  state.deviceStatus = 'on';
289
- state.mode = 'manual';
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.setAutoMode).toHaveBeenCalledTimes(1);
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.isHumid200S) {
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') || deviceType.includes('DUAL200S')) {
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 all characteristics with explicit values
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
- this.platform.log.debug('Initialized humidifier characteristics with explicit values');
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 === 'auto' ? 0 : 1,
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
- // Add Current Relative Humidity if supported
375
+ // CurrentRelativeHumidity getter
295
376
  if (this.capabilities && this.capabilities.hasHumidity) {
296
- if (!this.service.testCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity)) {
297
- this.service.addCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity);
298
- }
299
-
300
- // Add Target Relative Humidity characteristic
301
- if (!this.service.testCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold)) {
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 === 'auto' ? 0 : 1
705
+ this.modeToTargetState(extendedDevice.mode)
654
706
  );
655
707
 
656
708
  let rotationSpeed = 0;
657
709
 
658
710
  if (isActive) {
659
- if (this.isHumid200300S && extendedDevice.mistLevel !== undefined) {
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
- // Map HomeKit target states to device modes
951
- let mode: string = 'manual'; // Default to manual mode
952
-
953
- switch (value) {
954
- case 0: // Auto
955
- mode = 'auto';
956
- break;
957
- case 1: // Humidifier
958
- mode = 'manual';
959
- break;
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 === 'auto' ? 0 : 1;
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 HomeKit percentage (0-100) to device speed (1-9)
1103
+ // Convert percentage to mist level based on device type
1042
1104
  let speed: number;
1043
- if (percentage <= 11) {
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
- // Refresh device details to get the latest state
1181
- if (typeof extendedDevice.getDetails === 'function') {
1182
- await extendedDevice.getDetails();
1183
-
1184
- // Log the target humidity after setting
1185
- const currentTarget = extendedDevice.details?.target_humidity || extendedDevice.targetHumidity;
1186
- this.platform.log.debug(`Device ${this.device.deviceName} target humidity after setting: ${currentTarget}%`);
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
@@ -24,6 +24,7 @@
24
24
  ],
25
25
  "exclude": [
26
26
  "**/*.spec.ts",
27
- "**/*.test.ts"
27
+ "**/*.test.ts",
28
+ "src/__tests__"
28
29
  ]
29
30
  }
@@ -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"}