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.
- package/dist/accessories/air-purifier.accessory.d.ts +38 -5
- package/dist/accessories/air-purifier.accessory.d.ts.map +1 -1
- package/dist/accessories/air-purifier.accessory.js +421 -132
- package/dist/accessories/air-purifier.accessory.js.map +1 -1
- package/dist/platform.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/accessories/air-purifier.accessory.ts +505 -118
- package/src/platform.ts +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
53
|
+
// Detect device class based on model number patterns
|
|
45
54
|
const extendedDevice = this.device as ExtendedVeSyncAirPurifier;
|
|
46
|
-
|
|
55
|
+
const deviceType = extendedDevice.deviceType || '';
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
327
|
+
// Set up current state
|
|
76
328
|
this.setupCharacteristic(
|
|
77
|
-
this.platform.Characteristic.
|
|
78
|
-
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:
|
|
96
|
-
hasChildLock:
|
|
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 (
|
|
124
|
-
//
|
|
125
|
-
let targetState =
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
if (this.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
510
|
+
|
|
511
|
+
// Air quality updates have been removed
|
|
159
512
|
}
|
|
160
513
|
|
|
161
514
|
/**
|
|
162
|
-
* Get target state (
|
|
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
|
-
|
|
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
|
-
//
|
|
178
|
-
|
|
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 ===
|
|
552
|
+
if (targetState === 1 && typeof extendedDevice.autoMode === 'function') {
|
|
185
553
|
success = await extendedDevice.autoMode();
|
|
186
|
-
} else if (targetState ===
|
|
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
|
-
*
|
|
576
|
+
* Get rotation speed
|
|
209
577
|
*/
|
|
210
|
-
private async
|
|
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
|
-
|
|
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
|
-
//
|
|
228
|
-
if (this.
|
|
229
|
-
this.platform.log.
|
|
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
|
-
//
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
650
|
+
// Convert percentage to device speed using our consistent conversion method
|
|
651
|
+
const speed = this.percentageToSpeed(percentage);
|
|
242
652
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
}
|