homebridge-tsvesync 1.0.72 → 1.0.76

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,14 +1,14 @@
1
- import { CharacteristicValue, PlatformAccessory } from 'homebridge';
1
+ import { CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
2
2
  import { BaseAccessory } from './base.accessory';
3
3
  import { TSVESyncPlatform } from '../platform';
4
4
  import { DeviceCapabilities, VeSyncAirPurifier } from '../types/device.types';
5
5
 
6
- // Extended interface to include LV-series specific methods and properties
6
+ // Extended interface to include device-specific methods and properties
7
7
  interface ExtendedVeSyncAirPurifier extends VeSyncAirPurifier {
8
8
  deviceType: string;
9
9
  // Override setMode from base interface to make it more specific
10
10
  setMode(mode: string): Promise<boolean>;
11
- // Add additional methods specific to LV-series
11
+ // Add additional methods
12
12
  autoMode?(): Promise<boolean>;
13
13
  manualMode?(): Promise<boolean>;
14
14
  sleepMode?(): Promise<boolean>;
@@ -18,19 +18,28 @@ interface ExtendedVeSyncAirPurifier extends VeSyncAirPurifier {
18
18
  setChildLock?(enabled: boolean): Promise<boolean>;
19
19
  setOscillation?(enabled: boolean): Promise<boolean>;
20
20
  hasFeature?(feature: string): boolean;
21
+ isFeatureSupportedInCurrentMode?(feature: string): boolean;
22
+ getMaxFanSpeed?(): number;
21
23
  details?: {
22
24
  filter_life?: number;
23
25
  child_lock?: boolean;
24
26
  air_quality_value?: number;
25
27
  air_quality?: string;
26
28
  screen_status?: 'on' | 'off';
29
+ pm25?: number;
27
30
  };
28
31
  }
29
32
 
30
33
  export class AirPurifierAccessory extends BaseAccessory {
31
34
  private readonly capabilities: DeviceCapabilities;
32
35
  protected readonly device: VeSyncAirPurifier;
33
- private isLVSeries: boolean;
36
+ private isAirBypassDevice: boolean;
37
+ private isAirBaseV2Device: boolean;
38
+ private isAir131Device: boolean;
39
+ private airQualityService?: Service;
40
+ private lastSetSpeed: number = 0; // Track the last speed we set
41
+ private lastSetPercentage: number = 0; // Track the last percentage we set
42
+ private skipNextUpdate: boolean = false; // Flag to skip the next update
34
43
 
35
44
  constructor(
36
45
  platform: TSVESyncPlatform,
@@ -41,13 +50,204 @@ export class AirPurifierAccessory extends BaseAccessory {
41
50
  this.device = device;
42
51
  this.capabilities = this.getDeviceCapabilities();
43
52
 
44
- // Detect if this is an LV-series device (LV-PUR131S or LV-RH131S)
53
+ // Detect device class based on model number patterns
45
54
  const extendedDevice = this.device as ExtendedVeSyncAirPurifier;
46
- this.isLVSeries = extendedDevice.deviceType?.startsWith('LV-') || false;
55
+ const deviceType = extendedDevice.deviceType || '';
47
56
 
48
- if (this.isLVSeries) {
49
- this.platform.log.debug(`Detected LV-series device: ${this.device.deviceName} (${extendedDevice.deviceType})`);
57
+ // AirBypass Devices: Core series, LAP-C series, Vital series
58
+ this.isAirBypassDevice = deviceType.includes('CORE') ||
59
+ deviceType.startsWith('LAP-C') ||
60
+ deviceType.includes('VITAL');
61
+
62
+ // AirBaseV2 Devices: LAP-V series, LAP-EL series, EverestAir series
63
+ this.isAirBaseV2Device = deviceType.startsWith('LAP-V') ||
64
+ deviceType.startsWith('LAP-EL') ||
65
+ deviceType.includes('EVERESTAIR');
66
+
67
+ // Air131 Devices: LV-PUR131S, LV-RH131S
68
+ this.isAir131Device = deviceType.startsWith('LV-');
69
+
70
+ // Log device class detection
71
+ if (this.isAirBypassDevice) {
72
+ this.platform.log.debug(`Detected AirBypass device: ${this.device.deviceName} (${deviceType})`);
73
+ } else if (this.isAirBaseV2Device) {
74
+ this.platform.log.debug(`Detected AirBaseV2 device: ${this.device.deviceName} (${deviceType})`);
75
+ } else if (this.isAir131Device) {
76
+ this.platform.log.debug(`Detected Air131 device: ${this.device.deviceName} (${deviceType})`);
77
+ } else {
78
+ this.platform.log.debug(`Unknown air purifier type: ${this.device.deviceName} (${deviceType})`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Feature detection system
84
+ */
85
+ private hasFeature(feature: string): boolean {
86
+ const extendedDevice = this.device as unknown as ExtendedVeSyncAirPurifier;
87
+
88
+ // Use device's native hasFeature method if available
89
+ if (typeof extendedDevice.hasFeature === 'function') {
90
+ return extendedDevice.hasFeature(feature);
50
91
  }
92
+
93
+ // Fallback feature detection based on device type
94
+ switch (feature) {
95
+ case 'air_quality':
96
+ // Explicitly disable air quality features
97
+ return false;
98
+
99
+ case 'child_lock':
100
+ // Explicitly disable child lock features
101
+ return false;
102
+
103
+ case 'display':
104
+ // Explicitly disable display control features
105
+ return false;
106
+
107
+ case 'filter_life':
108
+ // Explicitly disable filter life reporting
109
+ return false;
110
+
111
+ case 'fan_speed':
112
+ return true;
113
+
114
+ default:
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if a feature is supported in the current device mode
121
+ */
122
+ private isFeatureSupportedInCurrentMode(feature: string): boolean {
123
+ const extendedDevice = this.device as unknown as ExtendedVeSyncAirPurifier;
124
+
125
+ // Use device's native method if available
126
+ if (typeof extendedDevice.isFeatureSupportedInCurrentMode === 'function') {
127
+ return extendedDevice.isFeatureSupportedInCurrentMode(feature);
128
+ }
129
+
130
+ // Fallback logic
131
+ const mode = extendedDevice.mode || 'manual';
132
+
133
+ // Air131 only supports fan speed in manual mode
134
+ if (feature === 'fan_speed' && this.isAir131Device && mode !== 'manual') {
135
+ return false;
136
+ }
137
+
138
+ return true;
139
+ }
140
+
141
+ /**
142
+ * Get the maximum fan speed for the device
143
+ */
144
+ private getMaxFanSpeed(): number {
145
+ const extendedDevice = this.device as unknown as ExtendedVeSyncAirPurifier;
146
+
147
+ // Use device's native method if available
148
+ if (typeof extendedDevice.getMaxFanSpeed === 'function') {
149
+ const maxSpeed = extendedDevice.getMaxFanSpeed();
150
+ if (typeof maxSpeed === 'number' && maxSpeed > 0) {
151
+ return maxSpeed;
152
+ }
153
+ }
154
+
155
+ // Use maxSpeed property if available
156
+ if (typeof extendedDevice.maxSpeed === 'number' && extendedDevice.maxSpeed > 0) {
157
+ return extendedDevice.maxSpeed;
158
+ }
159
+
160
+ // Fallback logic based on device type
161
+ if (this.isAir131Device) {
162
+ return 3; // LV-series devices have 3 speed levels
163
+ } else if (this.isAirBypassDevice) {
164
+ return 3; // All Core series have 3 speed levels
165
+ } else if (this.isAirBaseV2Device) {
166
+ if (this.device.deviceType.includes('LAP-EL551S')) {
167
+ return 3; // LAP-EL551S has 3 speed levels
168
+ } else {
169
+ return 4; // LAP-V series have 4 speed levels
170
+ }
171
+ }
172
+
173
+ // Default to 3 for unknown devices
174
+ return 3;
175
+ }
176
+
177
+ /**
178
+ * Convert device speed to percentage
179
+ */
180
+ private speedToPercentage(speed: number): number {
181
+ const maxSpeed = this.getMaxFanSpeed();
182
+
183
+ // Ensure we have valid numbers
184
+ if (maxSpeed <= 0 || typeof speed !== 'number' || speed <= 0) {
185
+ return 0;
186
+ }
187
+
188
+ // For devices with 3 speed levels
189
+ if (maxSpeed === 3) {
190
+ switch (speed) {
191
+ case 1: return 33; // Low -> 33%
192
+ case 2: return 67; // Medium -> 67%
193
+ case 3: return 100; // High -> 100%
194
+ default: return 0;
195
+ }
196
+ }
197
+ // For devices with 4 speed levels
198
+ else if (maxSpeed === 4) {
199
+ switch (speed) {
200
+ case 1: return 25; // Low -> 25%
201
+ case 2: return 50; // Medium-Low -> 50%
202
+ case 3: return 75; // Medium-High -> 75%
203
+ case 4: return 100; // High -> 100%
204
+ default: return 0;
205
+ }
206
+ }
207
+
208
+ // Default calculation
209
+ return Math.min(100, Math.max(0, Math.round((speed / maxSpeed) * 100)));
210
+ }
211
+
212
+ /**
213
+ * Convert percentage to device speed
214
+ */
215
+ private percentageToSpeed(percentage: number): number {
216
+ const maxSpeed = this.getMaxFanSpeed();
217
+
218
+ // Ensure we have valid numbers
219
+ if (maxSpeed <= 0 || typeof percentage !== 'number') {
220
+ return 1; // Default to lowest speed
221
+ }
222
+
223
+ // Ensure percentage is between 0 and 100
224
+ percentage = Math.min(100, Math.max(0, percentage));
225
+
226
+ // For devices with 3 speed levels
227
+ if (maxSpeed === 3) {
228
+ if (percentage <= 33) {
229
+ return 1; // Low
230
+ } else if (percentage <= 67) {
231
+ return 2; // Medium
232
+ } else {
233
+ return 3; // High
234
+ }
235
+ }
236
+ // For devices with 4 speed levels
237
+ else if (maxSpeed === 4) {
238
+ if (percentage <= 25) {
239
+ return 1; // Low
240
+ } else if (percentage <= 50) {
241
+ return 2; // Medium-Low
242
+ } else if (percentage <= 75) {
243
+ return 3; // Medium-High
244
+ } else {
245
+ return 4; // High
246
+ }
247
+ }
248
+
249
+ // Default calculation
250
+ return Math.max(1, Math.min(Math.round((percentage / 100) * maxSpeed), maxSpeed));
51
251
  }
52
252
 
53
253
  /**
@@ -66,24 +266,137 @@ export class AirPurifierAccessory extends BaseAccessory {
66
266
  );
67
267
 
68
268
  // Set up target state characteristic for mode mapping
69
- this.setupCharacteristic(
70
- this.platform.Characteristic.TargetAirPurifierState,
71
- this.getTargetState.bind(this),
72
- this.setTargetState.bind(this)
73
- );
269
+ const targetStateChar = this.service.getCharacteristic(this.platform.Characteristic.TargetAirPurifierState);
270
+
271
+ // For Core200S, only allow manual mode
272
+ if (this.device.deviceType.includes('Core200S')) {
273
+ targetStateChar.setProps({
274
+ validValues: [0] // MANUAL only
275
+ });
276
+ } else if (this.device.deviceType.includes('Core300S')) {
277
+ // Explicitly ensure Core300S has both AUTO and MANUAL modes
278
+ // Force the mode toggle to be visible for Core300S devices
279
+ this.platform.log.debug(`Explicitly enabling mode toggle for Core300S device: ${this.device.deviceName}`);
280
+
281
+ // First remove any existing characteristic to ensure clean setup
282
+ if (this.service.testCharacteristic(this.platform.Characteristic.TargetAirPurifierState)) {
283
+ this.service.removeCharacteristic(
284
+ this.service.getCharacteristic(this.platform.Characteristic.TargetAirPurifierState)
285
+ );
286
+ }
287
+
288
+ // Re-add the characteristic with proper properties
289
+ const newTargetStateChar = this.service.addCharacteristic(this.platform.Characteristic.TargetAirPurifierState);
290
+ newTargetStateChar.setProps({
291
+ validValues: [0, 1], // MANUAL and AUTO
292
+ perms: [this.platform.Characteristic.Perms.PAIRED_READ, this.platform.Characteristic.Perms.PAIRED_WRITE, this.platform.Characteristic.Perms.NOTIFY]
293
+ });
294
+
295
+ // Set up the characteristic handlers
296
+ newTargetStateChar.onGet(async () => {
297
+ try {
298
+ const value = await this.getTargetState();
299
+ return value;
300
+ } catch (error) {
301
+ this.platform.log.error(`Error getting target state: ${error}`);
302
+ throw error;
303
+ }
304
+ });
305
+
306
+ newTargetStateChar.onSet(async (value) => {
307
+ try {
308
+ await this.setTargetState(value);
309
+ } catch (error) {
310
+ this.platform.log.error(`Error setting target state: ${error}`);
311
+ throw error;
312
+ }
313
+ });
314
+ } else {
315
+ targetStateChar.setProps({
316
+ validValues: [0, 1] // MANUAL and AUTO
317
+ });
318
+
319
+ // Set up the characteristic handlers for non-Core300S devices
320
+ this.setupCharacteristic(
321
+ this.platform.Characteristic.TargetAirPurifierState,
322
+ this.getTargetState.bind(this),
323
+ this.setTargetState.bind(this)
324
+ );
325
+ }
74
326
 
75
- // Set up speed control
327
+ // Set up current state
76
328
  this.setupCharacteristic(
77
- this.platform.Characteristic.RotationSpeed,
78
- this.getRotationSpeed.bind(this),
79
- this.handleSetRotationSpeed.bind(this)
329
+ this.platform.Characteristic.CurrentAirPurifierState,
330
+ this.getCurrentState.bind(this)
80
331
  );
81
332
 
333
+ // Set up speed control with special handling for Core200S and Core300S
334
+ if (this.device.deviceType.includes('Core200S') || this.device.deviceType.includes('Core300S')) {
335
+ // For Core200S and Core300S, set up rotation speed characteristic manually
336
+ this.platform.log.debug(`Explicitly setting up rotation speed for ${this.device.deviceName}`);
337
+
338
+ // First remove any existing characteristic to ensure clean setup
339
+ if (this.service.testCharacteristic(this.platform.Characteristic.RotationSpeed)) {
340
+ this.service.removeCharacteristic(
341
+ this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed)
342
+ );
343
+ }
344
+
345
+ // Re-add the characteristic with proper properties
346
+ const rotationSpeedChar = this.service.addCharacteristic(this.platform.Characteristic.RotationSpeed);
347
+ rotationSpeedChar.setProps({
348
+ minValue: 0,
349
+ maxValue: 100,
350
+ minStep: 1,
351
+ perms: [this.platform.Characteristic.Perms.PAIRED_READ, this.platform.Characteristic.Perms.PAIRED_WRITE, this.platform.Characteristic.Perms.NOTIFY]
352
+ });
353
+
354
+ // Set up the characteristic handlers
355
+ rotationSpeedChar.onGet(async () => {
356
+ try {
357
+ const value = await this.getRotationSpeed();
358
+ this.platform.log.debug(`${this.device.deviceName} getRotationSpeed returned: ${value}`);
359
+ return value;
360
+ } catch (error) {
361
+ this.platform.log.error(`Error getting rotation speed: ${error}`);
362
+ throw error;
363
+ }
364
+ });
365
+
366
+ rotationSpeedChar.onSet(async (value) => {
367
+ try {
368
+ this.platform.log.debug(`${this.device.deviceName} setRotationSpeed called with: ${value}`);
369
+ await this.setRotationSpeed(value);
370
+ } catch (error) {
371
+ this.platform.log.error(`Error setting rotation speed: ${error}`);
372
+ throw error;
373
+ }
374
+ });
375
+ } else {
376
+ // For other devices, use the standard setup
377
+ this.setupCharacteristic(
378
+ this.platform.Characteristic.RotationSpeed,
379
+ this.getRotationSpeed.bind(this),
380
+ this.setRotationSpeed.bind(this)
381
+ );
382
+ }
383
+
82
384
  // Add Name characteristic
83
385
  this.setupCharacteristic(
84
386
  this.platform.Characteristic.Name,
85
387
  async () => this.device.deviceName
86
388
  );
389
+
390
+ // Air quality sensor service has been disabled for all devices
391
+ }
392
+
393
+ // Air quality sensor service has been removed
394
+
395
+ /**
396
+ * Get current state (INACTIVE = 0, IDLE = 1, PURIFYING_AIR = 2)
397
+ */
398
+ private async getCurrentState(): Promise<CharacteristicValue> {
399
+ return this.device.deviceStatus === 'on' ? 2 : 0;
87
400
  }
88
401
 
89
402
  /**
@@ -92,8 +405,8 @@ export class AirPurifierAccessory extends BaseAccessory {
92
405
  protected getDeviceCapabilities(): DeviceCapabilities {
93
406
  return {
94
407
  hasSpeed: true,
95
- hasAirQuality: true,
96
- hasChildLock: true,
408
+ hasAirQuality: this.hasFeature('air_quality'),
409
+ hasChildLock: false, // Explicitly disable child lock
97
410
  hasBrightness: false,
98
411
  hasColorTemp: false,
99
412
  hasColor: false,
@@ -120,11 +433,16 @@ export class AirPurifierAccessory extends BaseAccessory {
120
433
  // Get the extended device to access mode information
121
434
  const extendedDevice = this.device as ExtendedVeSyncAirPurifier;
122
435
 
123
- // Update target state (AUTO = 0, MANUAL = 1)
124
- // If mode is available, use it to determine target state
125
- let targetState = 1; // Default to MANUAL
126
- if (extendedDevice.mode) {
127
- targetState = extendedDevice.mode === 'auto' ? 0 : 1;
436
+ // Update target state (MANUAL = 0, AUTO = 1)
437
+ // Special handling for Core200S
438
+ let targetState = 0; // Default to MANUAL
439
+
440
+ if (this.device.deviceType.includes('Core200S')) {
441
+ // Core200S always reports MANUAL mode
442
+ targetState = 0; // MANUAL
443
+ } else if (extendedDevice.mode) {
444
+ // For other devices, including Core300S, use the reported mode
445
+ targetState = extendedDevice.mode === 'auto' ? 1 : 0;
128
446
  }
129
447
 
130
448
  this.service.updateCharacteristic(
@@ -133,37 +451,79 @@ export class AirPurifierAccessory extends BaseAccessory {
133
451
  );
134
452
 
135
453
  // Update rotation speed
136
- let rotationSpeed = 0;
137
- if (isOn && details.speed !== undefined) {
138
- if (this.isLVSeries) {
139
- // For LV-series devices, speed is limited to 1-3
140
- // Convert device speed (1-3) to HomeKit percentage (0-100)
141
- switch (details.speed) {
142
- case 1: rotationSpeed = 33; break; // Low
143
- case 2: rotationSpeed = 66; break; // Medium
144
- case 3: rotationSpeed = 100; break; // High
454
+ if (isOn && details.speed !== undefined && details.speed !== null) {
455
+ // Check if we should skip this update
456
+ if (this.skipNextUpdate) {
457
+ this.platform.log.debug(`Skipping update for ${this.device.deviceName} as requested`);
458
+ this.skipNextUpdate = false;
459
+ return;
460
+ }
461
+
462
+ // If we've recently set the speed, use the percentage we calculated
463
+ if (this.lastSetSpeed > 0 && this.lastSetPercentage > 0) {
464
+ this.platform.log.debug(`Using last set percentage for ${this.device.deviceName}: speed=${this.lastSetSpeed}, percentage=${this.lastSetPercentage}`);
465
+
466
+ // Only update if the device speed matches what we set
467
+ if (this.lastSetSpeed === details.speed) {
468
+ this.platform.log.debug(`Device speed matches last set speed: ${details.speed}`);
469
+
470
+ // Force update the characteristic to our last set percentage
471
+ this.service.updateCharacteristic(
472
+ this.platform.Characteristic.RotationSpeed,
473
+ this.lastSetPercentage
474
+ );
475
+ } else {
476
+ // Device speed doesn't match what we set, so convert from device speed
477
+ const percentage = this.speedToPercentage(details.speed);
478
+ this.platform.log.debug(`Device speed doesn't match last set speed. Device: ${details.speed}, Last set: ${this.lastSetSpeed}, Calculated percentage: ${percentage}`);
479
+
480
+ // Update our tracking variables
481
+ this.lastSetSpeed = details.speed;
482
+ this.lastSetPercentage = percentage;
483
+
484
+ // Update the characteristic
485
+ this.service.updateCharacteristic(
486
+ this.platform.Characteristic.RotationSpeed,
487
+ percentage
488
+ );
145
489
  }
146
490
  } else {
147
- // For standard devices, speed can be 1-5
148
- // Convert device speed (1-5) to HomeKit percentage (0-100)
149
- switch (details.speed) {
150
- case 1: rotationSpeed = 20; break; // Sleep
151
- case 2: rotationSpeed = 40; break; // Low
152
- case 3: rotationSpeed = 60; break; // Medium
153
- case 4: rotationSpeed = 80; break; // High
154
- case 5: rotationSpeed = 100; break; // Turbo
155
- }
491
+ // No recent set speed, so convert from device speed
492
+ const percentage = this.speedToPercentage(details.speed);
493
+ this.platform.log.debug(`No recent set speed for ${this.device.deviceName}. Device speed: ${details.speed}, Calculated percentage: ${percentage}`);
494
+
495
+ // Update the characteristic
496
+ this.service.updateCharacteristic(
497
+ this.platform.Characteristic.RotationSpeed,
498
+ percentage
499
+ );
156
500
  }
501
+ } else {
502
+ this.service.updateCharacteristic(
503
+ this.platform.Characteristic.RotationSpeed,
504
+ 0
505
+ );
506
+ // Reset tracking variables when device is off
507
+ this.lastSetSpeed = 0;
508
+ this.lastSetPercentage = 0;
157
509
  }
158
- this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, rotationSpeed);
510
+
511
+ // Air quality updates have been removed
159
512
  }
160
513
 
161
514
  /**
162
- * Get target state (AUTO = 0, MANUAL = 1)
515
+ * Get target state (MANUAL = 0, AUTO = 1)
163
516
  */
164
517
  private async getTargetState(): Promise<CharacteristicValue> {
518
+ // For Core200S, always return MANUAL
519
+ if (this.device.deviceType.includes('Core200S')) {
520
+ return 0; // MANUAL
521
+ }
522
+
165
523
  const extendedDevice = this.device as ExtendedVeSyncAirPurifier;
166
- return extendedDevice.mode === 'auto' ? 0 : 1;
524
+ const targetState = extendedDevice.mode === 'auto' ? 1 : 0;
525
+
526
+ return targetState;
167
527
  }
168
528
 
169
529
  /**
@@ -174,16 +534,24 @@ export class AirPurifierAccessory extends BaseAccessory {
174
534
  const targetState = value as number;
175
535
  const extendedDevice = this.device as ExtendedVeSyncAirPurifier;
176
536
 
177
- // Map HomeKit target states to device modes
178
- const mode = targetState === 0 ? 'auto' : 'manual';
537
+ // For Core200S, don't allow mode changes
538
+ if (this.device.deviceType.includes('Core200S')) {
539
+ this.platform.log.warn(`Mode changes not supported on ${this.device.deviceName}, only fan speed control is available`);
540
+ // Update the characteristic back to MANUAL (0)
541
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetAirPurifierState, 0);
542
+ return;
543
+ }
544
+
545
+ // For all other devices, including Core300S, normal mode handling
546
+ const mode = targetState === 1 ? 'auto' : 'manual';
179
547
  this.platform.log.debug(`Setting mode to ${mode} for device: ${this.device.deviceName}`);
180
548
 
181
549
  let success = false;
182
550
 
183
551
  // Use the appropriate method to set the mode
184
- if (targetState === 0 && typeof extendedDevice.autoMode === 'function') {
552
+ if (targetState === 1 && typeof extendedDevice.autoMode === 'function') {
185
553
  success = await extendedDevice.autoMode();
186
- } else if (targetState === 1 && typeof extendedDevice.manualMode === 'function') {
554
+ } else if (targetState === 0 && typeof extendedDevice.manualMode === 'function') {
187
555
  success = await extendedDevice.manualMode();
188
556
  } else if (typeof extendedDevice.setMode === 'function') {
189
557
  success = await extendedDevice.setMode(mode as 'auto' | 'manual' | 'sleep');
@@ -205,11 +573,44 @@ export class AirPurifierAccessory extends BaseAccessory {
205
573
  }
206
574
 
207
575
  /**
208
- * Handle set rotation speed
576
+ * Get rotation speed
209
577
  */
210
- private async handleSetRotationSpeed(value: CharacteristicValue): Promise<void> {
578
+ private async getRotationSpeed(): Promise<CharacteristicValue> {
579
+ // If device is off or speed is not defined, return 0
580
+ if (this.device.deviceStatus !== 'on' ||
581
+ this.device.speed === undefined ||
582
+ this.device.speed === null) {
583
+ return 0;
584
+ }
585
+
586
+ // If we have a last set percentage, use that instead of calculating from device speed
587
+ if (this.lastSetPercentage > 0 && this.lastSetSpeed === this.device.speed) {
588
+ this.platform.log.debug(`getRotationSpeed returning last set percentage: ${this.lastSetPercentage} for speed: ${this.device.speed}`);
589
+ return this.lastSetPercentage;
590
+ }
591
+
592
+ // Convert device speed to percentage using our consistent conversion method
593
+ const percentage = this.speedToPercentage(this.device.speed);
594
+ this.platform.log.debug(`getRotationSpeed calculated percentage: ${percentage} for speed: ${this.device.speed}`);
595
+
596
+ return percentage;
597
+ }
598
+
599
+ /**
600
+ * Set rotation speed
601
+ */
602
+ private async setRotationSpeed(value: CharacteristicValue): Promise<void> {
211
603
  try {
212
- const percentage = value as number;
604
+ // Ensure value is a valid number
605
+ let percentage = value as number;
606
+ if (isNaN(percentage) || percentage === null || percentage === undefined) {
607
+ this.platform.log.warn(`Invalid rotation speed value: ${value} for device: ${this.device.deviceName}`);
608
+ return;
609
+ }
610
+
611
+ // Ensure percentage is between 0 and 100
612
+ percentage = Math.min(100, Math.max(0, percentage));
613
+
213
614
  this.platform.log.debug(`Setting rotation speed to ${percentage}% for device: ${this.device.deviceName}`);
214
615
 
215
616
  if (percentage === 0) {
@@ -219,62 +620,72 @@ export class AirPurifierAccessory extends BaseAccessory {
219
620
  if (!success) {
220
621
  throw new Error('Failed to turn off device');
221
622
  }
623
+ // Reset tracking variables when turning off
624
+ this.lastSetSpeed = 0;
625
+ this.lastSetPercentage = 0;
222
626
  return;
223
627
  }
224
628
 
225
629
  const extendedDevice = this.device as ExtendedVeSyncAirPurifier;
226
630
 
227
- // For LV-series devices, ensure the device is in manual mode before changing fan speed
228
- if (this.isLVSeries && extendedDevice.mode !== 'manual') {
229
- this.platform.log.debug(`Setting device to manual mode before changing fan speed: ${this.device.deviceName}`);
631
+ // Check if fan speed control is supported in current mode
632
+ if (!this.isFeatureSupportedInCurrentMode('fan_speed')) {
633
+ this.platform.log.warn(`Fan speed control not supported in ${extendedDevice.mode} mode for device: ${this.device.deviceName}`);
230
634
 
231
- // Use the appropriate method to set manual mode
232
- if (typeof extendedDevice.manualMode === 'function') {
233
- const modeSuccess = await extendedDevice.manualMode();
234
- this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`);
235
- } else if (typeof extendedDevice.setMode === 'function') {
236
- const modeSuccess = await extendedDevice.setMode('manual');
237
- this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`);
635
+ // For Air131 devices, switch to manual mode first
636
+ if (this.isAir131Device && extendedDevice.mode !== 'manual') {
637
+ this.platform.log.debug(`Setting device to manual mode before changing fan speed: ${this.device.deviceName}`);
638
+
639
+ // Use the appropriate method to set manual mode
640
+ if (typeof extendedDevice.manualMode === 'function') {
641
+ const modeSuccess = await extendedDevice.manualMode();
642
+ this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`);
643
+ } else if (typeof extendedDevice.setMode === 'function') {
644
+ const modeSuccess = await extendedDevice.setMode('manual');
645
+ this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`);
646
+ }
238
647
  }
239
648
  }
240
649
 
241
- let speed: number;
650
+ // Convert percentage to device speed using our consistent conversion method
651
+ const speed = this.percentageToSpeed(percentage);
242
652
 
243
- if (this.isLVSeries) {
244
- // For LV-series devices, speed is limited to 1-3
245
- // Convert HomeKit percentage (0-100) to device speed (1-3)
246
- if (percentage <= 33) {
247
- speed = 1; // Low
248
- } else if (percentage <= 66) {
249
- speed = 2; // Medium
250
- } else {
251
- speed = 3; // High
252
- }
253
- } else {
254
- // For standard devices, speed can be 1-5
255
- // Convert HomeKit percentage (0-100) to device speed (1-5)
256
- if (percentage <= 20) {
257
- speed = 1; // Sleep
258
- } else if (percentage <= 40) {
259
- speed = 2; // Low
260
- } else if (percentage <= 60) {
261
- speed = 3; // Medium
262
- } else if (percentage <= 80) {
263
- speed = 4; // High
264
- } else {
265
- speed = 5; // Turbo
266
- }
653
+ // Final validation to ensure speed is a valid number
654
+ if (isNaN(speed) || speed === null || speed === undefined) {
655
+ this.platform.log.warn(`Calculated invalid speed: ${speed} from percentage: ${percentage} for device: ${this.device.deviceName}`);
656
+ return;
267
657
  }
268
658
 
269
659
  this.platform.log.debug(`Setting fan speed to ${speed} for device: ${this.device.deviceName}`);
660
+
661
+ // Store the values we're setting before making the API call
662
+ this.lastSetSpeed = speed;
663
+ this.lastSetPercentage = percentage;
664
+
665
+ // Set the flag to skip the next update
666
+ this.skipNextUpdate = true;
667
+
668
+ // Immediately update the characteristic to show the correct percentage
669
+ // This ensures the slider shows the value we set, not what the device reports
670
+ this.service.updateCharacteristic(
671
+ this.platform.Characteristic.RotationSpeed,
672
+ percentage
673
+ );
674
+
270
675
  const success = await this.device.changeFanSpeed(speed);
271
676
 
272
677
  if (!success) {
678
+ // If the API call failed, reset our tracking variables
679
+ this.lastSetSpeed = 0;
680
+ this.lastSetPercentage = 0;
273
681
  throw new Error(`Failed to set speed to ${speed}`);
274
682
  }
275
683
 
684
+ this.platform.log.debug(`Successfully set fan speed to ${speed} (${percentage}%) for device: ${this.device.deviceName}`);
685
+
276
686
  // Update device state and characteristics
277
- await this.updateDeviceSpecificStates(this.device);
687
+ // Skip this to avoid overriding our UI update
688
+ // await this.updateDeviceSpecificStates(this.device);
278
689
  } catch (error) {
279
690
  this.handleDeviceError('set rotation speed', error);
280
691
  }
@@ -295,6 +706,12 @@ export class AirPurifierAccessory extends BaseAccessory {
295
706
  throw new Error(`Failed to turn ${isOn ? 'on' : 'off'} device`);
296
707
  }
297
708
 
709
+ // Reset tracking variables when turning off
710
+ if (!isOn) {
711
+ this.lastSetSpeed = 0;
712
+ this.lastSetPercentage = 0;
713
+ }
714
+
298
715
  // Update device state and characteristics
299
716
  await this.updateDeviceSpecificStates(this.device);
300
717
  await this.persistDeviceState('deviceStatus', isOn ? 'on' : 'off');
@@ -302,34 +719,4 @@ export class AirPurifierAccessory extends BaseAccessory {
302
719
  this.handleDeviceError('set active state', error);
303
720
  }
304
721
  }
305
-
306
- private async getRotationSpeed(): Promise<CharacteristicValue> {
307
- if (this.device.speed === undefined || this.device.speed === null) {
308
- return 0;
309
- }
310
-
311
- if (this.isLVSeries) {
312
- // For LV-series devices, speed is limited to 1-3
313
- // Convert device speed (1-3) to HomeKit percentage (0-100)
314
- switch (this.device.speed) {
315
- case 0: return 0; // Off
316
- case 1: return 33; // Low
317
- case 2: return 66; // Medium
318
- case 3: return 100; // High
319
- default: return 0;
320
- }
321
- } else {
322
- // For standard devices, speed can be 1-5
323
- // Convert device speed (1-5) to HomeKit percentage (0-100)
324
- switch (this.device.speed) {
325
- case 0: return 0; // Off
326
- case 1: return 20; // Sleep
327
- case 2: return 40; // Low
328
- case 3: return 60; // Medium
329
- case 4: return 80; // High
330
- case 5: return 100; // Turbo
331
- default: return 0;
332
- }
333
- }
334
- }
335
722
  }