tsvesync 1.3.8 → 1.3.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.
@@ -30,10 +30,9 @@ export declare class VeSyncAirBaseV2 extends VeSyncAirBypass {
30
30
  action: string;
31
31
  } | null);
32
32
  /**
33
- * Check response for Vital series devices
34
- * These devices return success at API level but may have inner errors
33
+ * Check response for bypassV2 purifiers that surface nested result codes.
35
34
  */
36
- protected checkVitalResponse(response: any, status: number, method: string): boolean;
35
+ protected checkV2Response(response: any, status: number, method: string): boolean;
37
36
  /**
38
37
  * Get device details
39
38
  */
@@ -43,24 +42,23 @@ export declare class VeSyncAirBaseV2 extends VeSyncAirBypass {
43
42
  */
44
43
  protected buildPurifierDict(devDict: Record<string, any>): void;
45
44
  /**
46
- * Check if the current device is a Vital series model
47
- * @returns true if this is a LAP-V102S or LAP-V201S device
45
+ * Determine whether this purifier requires the powerSwitch payload and nested response handling.
48
46
  */
49
- private isVitalSeries;
47
+ private requiresPowerSwitchPayload;
50
48
  /**
51
- * Override turn on method to handle Vital series special cases
49
+ * Override turn on to use the powerSwitch payload required by bypassV2 purifiers.
52
50
  */
53
51
  turnOn(): Promise<boolean>;
54
52
  /**
55
- * Override turn off method to handle Vital series special cases
53
+ * Override turn off to use the powerSwitch payload required by bypassV2 purifiers.
56
54
  */
57
55
  turnOff(): Promise<boolean>;
58
56
  /**
59
- * Override setMode method to handle Vital series special cases
57
+ * Override setMode to send workMode payloads for bypassV2 purifiers.
60
58
  */
61
59
  setMode(mode: string): Promise<boolean>;
62
60
  /**
63
- * Override changeFanSpeed method to handle Vital series special cases
61
+ * Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers.
64
62
  */
65
63
  changeFanSpeed(speed: number): Promise<boolean>;
66
64
  /**
@@ -46,26 +46,21 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
46
46
  this._timer = value;
47
47
  }
48
48
  /**
49
- * Check response for Vital series devices
50
- * These devices return success at API level but may have inner errors
49
+ * Check response for bypassV2 purifiers that surface nested result codes.
51
50
  */
52
- checkVitalResponse(response, status, method) {
53
- // First check basic response
51
+ checkV2Response(response, status, method) {
52
+ // First check basic response structure
54
53
  if (!this.checkResponse([response, status], method)) {
55
54
  return false;
56
55
  }
57
- // Check if this is a Vital series device
58
- const isVitalSeries = this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
59
- if (!isVitalSeries) {
60
- return true; // Not a Vital series device, standard check is enough
56
+ // Only apply extended handling to bypassV2 purifiers (Vital/Everest)
57
+ if (!this.requiresPowerSwitchPayload()) {
58
+ return true;
61
59
  }
62
- // For Vital series, we need to check the inner result code
60
+ // These devices frequently succeed with a nested non-zero code
63
61
  if (response.result && response.result.code !== undefined && response.result.code !== 0) {
64
- // Vital series often returns code -1 but still succeeds
65
- // Let's update the state optimistically
66
- logger_1.logger.debug(`Vital series device ${this.deviceName} returned inner code ${response.result.code} for ${method}`);
67
- logger_1.logger.debug('This is normal for Vital series devices - proceeding with operation');
68
- // Keep the device online despite inner error
62
+ logger_1.logger.debug(`${this.deviceName} returned inner code ${response.result.code} for ${method}`);
63
+ logger_1.logger.debug('BypassV2 purifier reports non-zero inner code but request still succeeded');
69
64
  this.connectionStatus = 'online';
70
65
  return true;
71
66
  }
@@ -79,22 +74,20 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
79
74
  logger_1.logger.debug(`Getting details for device: ${this.deviceName}`);
80
75
  const [head, body] = this.buildApiDict('getPurifierStatus');
81
76
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
82
- // Check if this is a Vital series device (LAP-V102S or LAP-V201S)
83
- const isVitalSeries = this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
77
+ const needsV2Handling = this.requiresPowerSwitchPayload();
84
78
  // First check the outer response
85
79
  if (!response || status !== 200) {
86
80
  logger_1.logger.debug(`Error getting purifier details for ${this.deviceName}: Invalid response`);
87
81
  this.connectionStatus = 'offline';
88
82
  return false;
89
83
  }
90
- // For Vital series, we need special handling as they return inner code -1 for details
91
- // but still provide valid status information
92
- if (isVitalSeries && response.result) {
84
+ // For bypassV2 purifier families the API nests the result and may surface non-zero codes
85
+ if (needsV2Handling && response.result) {
93
86
  // Check if we have a valid getPurifierStatus response
94
87
  if (response.code === 0 && ((_a = response.result) === null || _a === void 0 ? void 0 : _a.result)) {
95
88
  const statusResponse = response.result.result;
96
89
  // If we have valid status data, use it regardless of inner result code
97
- logger_1.logger.debug(`Using status data for Vital series device: ${this.deviceName}`);
90
+ logger_1.logger.debug(`Using nested status data for bypassV2 purifier: ${this.deviceName}`);
98
91
  this.buildPurifierDict(statusResponse);
99
92
  if (statusResponse.configuration) {
100
93
  this.buildConfigDict(statusResponse.configuration);
@@ -104,7 +97,7 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
104
97
  return true;
105
98
  }
106
99
  }
107
- // Standard processing for non-Vital devices or when no status data is available
100
+ // Standard processing for non-bypassV2 devices or when no status data is available
108
101
  if (!this.checkResponse([response, status], 'getDetails') || !(response === null || response === void 0 ? void 0 : response.result)) {
109
102
  logger_1.logger.debug('Error getting purifier details');
110
103
  this.connectionStatus = 'offline';
@@ -133,7 +126,9 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
133
126
  this.enabled = powerSwitch;
134
127
  this.deviceStatus = powerSwitch ? 'on' : 'off';
135
128
  this.mode = (_b = devDict.workMode) !== null && _b !== void 0 ? _b : 'manual';
136
- this.speed = (_c = devDict.fanSpeedLevel) !== null && _c !== void 0 ? _c : 0;
129
+ const reportedFanSpeed = (_c = devDict.fanSpeedLevel) !== null && _c !== void 0 ? _c : 0;
130
+ const isAutoSentinel = reportedFanSpeed === 255;
131
+ this.speed = isAutoSentinel ? 0 : reportedFanSpeed;
137
132
  this.setSpeedLevel = (_d = devDict.manualSpeedLevel) !== null && _d !== void 0 ? _d : 1;
138
133
  // Parse filter life with proper fallback handling and logging
139
134
  let filterLife = 0;
@@ -159,7 +154,11 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
159
154
  light_detection_switch: Boolean((_g = devDict.lightDetectionSwitch) !== null && _g !== void 0 ? _g : 0),
160
155
  environment_light_state: Boolean((_h = devDict.environmentLightState) !== null && _h !== void 0 ? _h : 0),
161
156
  screen_switch: Boolean((_j = devDict.screenSwitch) !== null && _j !== void 0 ? _j : 0),
162
- screenStatus: Boolean((_k = devDict.screenSwitch) !== null && _k !== void 0 ? _k : 0) ? 'on' : 'off'
157
+ screenStatus: Boolean((_k = devDict.screenSwitch) !== null && _k !== void 0 ? _k : 0) ? 'on' : 'off',
158
+ manualSpeedLevel: this.setSpeedLevel,
159
+ mode: this.mode,
160
+ speed: this.speed,
161
+ isAutoSentinel
163
162
  };
164
163
  if (this.hasFeature('air_quality')) {
165
164
  this.details.air_quality_value = (_l = devDict.PM25) !== null && _l !== void 0 ? _l : 0;
@@ -186,29 +185,28 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
186
185
  }
187
186
  }
188
187
  /**
189
- * Check if the current device is a Vital series model
190
- * @returns true if this is a LAP-V102S or LAP-V201S device
188
+ * Determine whether this purifier requires the powerSwitch payload and nested response handling.
191
189
  */
192
- isVitalSeries() {
193
- return this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
190
+ requiresPowerSwitchPayload() {
191
+ const type = (this.deviceType || '').toUpperCase();
192
+ return type.startsWith('LAP-V') || type.startsWith('LAP-EL');
194
193
  }
195
194
  /**
196
- * Override turn on method to handle Vital series special cases
195
+ * Override turn on to use the powerSwitch payload required by bypassV2 purifiers.
197
196
  */
198
197
  async turnOn() {
199
198
  logger_1.logger.info(`Turning on device: ${this.deviceName}`);
200
199
  const [head, body] = this.buildApiDict('setSwitch');
201
- // For Vital series, use powerSwitch per YAML spec
202
- // For other models, use the parent class implementation
203
- if (this.isVitalSeries()) {
204
- logger_1.logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
200
+ // BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation
201
+ if (this.requiresPowerSwitchPayload()) {
202
+ logger_1.logger.debug(`Using bypassV2 power payload for ${this.deviceName}`);
205
203
  body.payload.data = {
206
204
  powerSwitch: 1,
207
205
  switchIdx: 0
208
206
  };
209
207
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
210
- // Use Vital-specific response checking
211
- const success = this.checkVitalResponse(response, status, 'turnOn');
208
+ // Use bypassV2 response checking
209
+ const success = this.checkV2Response(response, status, 'turnOn');
212
210
  if (success) {
213
211
  this.deviceStatus = 'on';
214
212
  // Force a details update to sync state
@@ -221,27 +219,26 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
221
219
  return success;
222
220
  }
223
221
  else {
224
- // Use parent implementation for non-Vital models
222
+ // Use parent implementation for non-bypassV2 models
225
223
  return super.turnOn();
226
224
  }
227
225
  }
228
226
  /**
229
- * Override turn off method to handle Vital series special cases
227
+ * Override turn off to use the powerSwitch payload required by bypassV2 purifiers.
230
228
  */
231
229
  async turnOff() {
232
230
  logger_1.logger.info(`Turning off device: ${this.deviceName}`);
233
231
  const [head, body] = this.buildApiDict('setSwitch');
234
- // For Vital series, use powerSwitch per YAML spec
235
- // For other models, use the parent class implementation
236
- if (this.isVitalSeries()) {
237
- logger_1.logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
232
+ // BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation
233
+ if (this.requiresPowerSwitchPayload()) {
234
+ logger_1.logger.debug(`Using bypassV2 power payload for ${this.deviceName}`);
238
235
  body.payload.data = {
239
236
  powerSwitch: 0,
240
237
  switchIdx: 0
241
238
  };
242
239
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
243
- // Use Vital-specific response checking
244
- const success = this.checkVitalResponse(response, status, 'turnOff');
240
+ // Use bypassV2 response checking
241
+ const success = this.checkV2Response(response, status, 'turnOff');
245
242
  if (success) {
246
243
  this.deviceStatus = 'off';
247
244
  // Force a details update to sync state
@@ -254,12 +251,12 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
254
251
  return success;
255
252
  }
256
253
  else {
257
- // Use parent implementation for non-Vital models
254
+ // Use parent implementation for non-bypassV2 models
258
255
  return super.turnOff();
259
256
  }
260
257
  }
261
258
  /**
262
- * Override setMode method to handle Vital series special cases
259
+ * Override setMode to send workMode payloads for bypassV2 purifiers.
263
260
  */
264
261
  async setMode(mode) {
265
262
  if (!this.modes.includes(mode)) {
@@ -272,16 +269,15 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
272
269
  logger_1.logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
273
270
  return this.setMode('manual');
274
271
  }
275
- // For Vital series, use special response handling
276
- // For other models, use the parent implementation
277
- if (this.isVitalSeries()) {
278
- logger_1.logger.debug(`Using Vital series specific response handling for ${this.deviceName}`);
272
+ // BypassV2 purifiers need the workMode payload; other models use the base implementation
273
+ if (this.requiresPowerSwitchPayload()) {
274
+ logger_1.logger.debug(`Using bypassV2 mode payload for ${this.deviceName}`);
279
275
  const [head, body] = this.buildApiDict('setPurifierMode');
280
276
  body.payload.data = {
281
277
  workMode: mode
282
278
  };
283
279
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
284
- const success = this.checkVitalResponse(response, status, 'setMode');
280
+ const success = this.checkV2Response(response, status, 'setMode');
285
281
  if (success) {
286
282
  this.details.mode = mode;
287
283
  this._mode = mode;
@@ -299,12 +295,12 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
299
295
  }
300
296
  }
301
297
  else {
302
- // Use parent implementation for non-Vital models
298
+ // Use parent implementation for non-bypassV2 models
303
299
  return super.setMode(mode);
304
300
  }
305
301
  }
306
302
  /**
307
- * Override changeFanSpeed method to handle Vital series special cases
303
+ * Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers.
308
304
  */
309
305
  async changeFanSpeed(speed) {
310
306
  var _a;
@@ -315,17 +311,17 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
315
311
  }
316
312
  logger_1.logger.info(`Changing fan speed to ${speed} for device: ${this.deviceName}`);
317
313
  const [head, body] = this.buildApiDict('setLevel');
318
- // Specific handling for Vital series devices
319
- if (this.isVitalSeries()) {
320
- logger_1.logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
321
- // For Vital series, use levelIdx, levelType, and manualSpeedLevel per YAML spec
314
+ // Specific handling for bypassV2 devices
315
+ if (this.requiresPowerSwitchPayload()) {
316
+ logger_1.logger.debug(`Using bypassV2 speed payload for ${this.deviceName}`);
317
+ // For these devices, use levelIdx, levelType, and manualSpeedLevel per YAML spec
322
318
  body.payload.data = {
323
319
  levelIdx: 0,
324
320
  levelType: 'wind',
325
321
  manualSpeedLevel: speed
326
322
  };
327
323
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
328
- const success = this.checkVitalResponse(response, status, 'changeFanSpeed');
324
+ const success = this.checkV2Response(response, status, 'changeFanSpeed');
329
325
  if (success) {
330
326
  this.speed = speed;
331
327
  this.details.manualSpeedLevel = speed;
@@ -343,7 +339,7 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
343
339
  }
344
340
  }
345
341
  else {
346
- // For non-Vital models, use id, level, mode, and type
342
+ // For non-bypassV2 models, use id, level, mode, and type
347
343
  body.payload.data = {
348
344
  id: 0,
349
345
  level: speed,
@@ -378,14 +374,14 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
378
374
  };
379
375
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
380
376
  // Use the appropriate response checking method based on device type
381
- const success = this.isVitalSeries()
382
- ? this.checkVitalResponse(response, status, 'setAutoPreference')
377
+ const success = this.requiresPowerSwitchPayload()
378
+ ? this.checkV2Response(response, status, 'setAutoPreference')
383
379
  : this.checkResponse([response, status], 'setAutoPreference');
384
380
  if (success) {
385
381
  this.details.auto_preference = preference;
386
382
  this.details.auto_preference_type = preference;
387
- // Force a details update to sync state for Vital series
388
- if (this.isVitalSeries()) {
383
+ // Force a details update to sync state for bypassV2 purifiers
384
+ if (this.requiresPowerSwitchPayload()) {
389
385
  setTimeout(() => {
390
386
  this.getDetails().catch(e => {
391
387
  logger_1.logger.debug(`Background state refresh failed: ${e}`);
@@ -405,13 +401,13 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
405
401
  };
406
402
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
407
403
  // Use the appropriate response checking method based on device type
408
- const success = this.isVitalSeries()
409
- ? this.checkVitalResponse(response, status, 'setLightDetection')
404
+ const success = this.requiresPowerSwitchPayload()
405
+ ? this.checkV2Response(response, status, 'setLightDetection')
410
406
  : this.checkResponse([response, status], 'setLightDetection');
411
407
  if (success) {
412
408
  this.details.light_detection_switch = enabled;
413
- // Force a details update to sync state for Vital series
414
- if (this.isVitalSeries()) {
409
+ // Force a details update to sync state for bypassV2 purifiers
410
+ if (this.requiresPowerSwitchPayload()) {
415
411
  setTimeout(() => {
416
412
  this.getDetails().catch(e => {
417
413
  logger_1.logger.debug(`Background state refresh failed: ${e}`);
@@ -486,8 +482,8 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
486
482
  };
487
483
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
488
484
  // Use the appropriate response checking method based on device type
489
- const success = this.isVitalSeries()
490
- ? this.checkVitalResponse(response, status, 'setOscillation')
485
+ const success = this.requiresPowerSwitchPayload()
486
+ ? this.checkV2Response(response, status, 'setOscillation')
491
487
  : this.checkResponse([response, status], 'setOscillation');
492
488
  if (success) {
493
489
  this.details.oscillationState = toggle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
4
4
  "description": "A TypeScript library for interacting with VeSync smart home devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,5 @@
1
1
  import { VeSyncAirBypass } from './airBypass';
2
2
  import { VeSync } from '../vesync';
3
- import { Helpers } from '../helpers';
4
3
  import { logger } from '../logger';
5
4
 
6
5
  /**
@@ -55,29 +54,23 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
55
54
  }
56
55
 
57
56
  /**
58
- * Check response for Vital series devices
59
- * These devices return success at API level but may have inner errors
57
+ * Check response for bypassV2 purifiers that surface nested result codes.
60
58
  */
61
- protected checkVitalResponse(response: any, status: number, method: string): boolean {
62
- // First check basic response
59
+ protected checkV2Response(response: any, status: number, method: string): boolean {
60
+ // First check basic response structure
63
61
  if (!this.checkResponse([response, status], method)) {
64
62
  return false;
65
63
  }
66
64
 
67
- // Check if this is a Vital series device
68
- const isVitalSeries = this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
69
- if (!isVitalSeries) {
70
- return true; // Not a Vital series device, standard check is enough
65
+ // Only apply extended handling to bypassV2 purifiers (Vital/Everest)
66
+ if (!this.requiresPowerSwitchPayload()) {
67
+ return true;
71
68
  }
72
69
 
73
- // For Vital series, we need to check the inner result code
70
+ // These devices frequently succeed with a nested non-zero code
74
71
  if (response.result && response.result.code !== undefined && response.result.code !== 0) {
75
- // Vital series often returns code -1 but still succeeds
76
- // Let's update the state optimistically
77
- logger.debug(`Vital series device ${this.deviceName} returned inner code ${response.result.code} for ${method}`);
78
- logger.debug('This is normal for Vital series devices - proceeding with operation');
79
-
80
- // Keep the device online despite inner error
72
+ logger.debug(`${this.deviceName} returned inner code ${response.result.code} for ${method}`);
73
+ logger.debug('BypassV2 purifier reports non-zero inner code but request still succeeded');
81
74
  this.connectionStatus = 'online';
82
75
  return true;
83
76
  }
@@ -99,8 +92,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
99
92
  head
100
93
  );
101
94
 
102
- // Check if this is a Vital series device (LAP-V102S or LAP-V201S)
103
- const isVitalSeries = this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
95
+ const needsV2Handling = this.requiresPowerSwitchPayload();
104
96
 
105
97
  // First check the outer response
106
98
  if (!response || status !== 200) {
@@ -109,15 +101,14 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
109
101
  return false;
110
102
  }
111
103
 
112
- // For Vital series, we need special handling as they return inner code -1 for details
113
- // but still provide valid status information
114
- if (isVitalSeries && response.result) {
104
+ // For bypassV2 purifier families the API nests the result and may surface non-zero codes
105
+ if (needsV2Handling && response.result) {
115
106
  // Check if we have a valid getPurifierStatus response
116
107
  if (response.code === 0 && response.result?.result) {
117
108
  const statusResponse = response.result.result;
118
109
 
119
110
  // If we have valid status data, use it regardless of inner result code
120
- logger.debug(`Using status data for Vital series device: ${this.deviceName}`);
111
+ logger.debug(`Using nested status data for bypassV2 purifier: ${this.deviceName}`);
121
112
  this.buildPurifierDict(statusResponse);
122
113
  if (statusResponse.configuration) {
123
114
  this.buildConfigDict(statusResponse.configuration);
@@ -128,7 +119,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
128
119
  }
129
120
  }
130
121
 
131
- // Standard processing for non-Vital devices or when no status data is available
122
+ // Standard processing for non-bypassV2 devices or when no status data is available
132
123
  if (!this.checkResponse([response, status], 'getDetails') || !response?.result) {
133
124
  logger.debug('Error getting purifier details');
134
125
  this.connectionStatus = 'offline';
@@ -159,10 +150,12 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
159
150
  this.enabled = powerSwitch;
160
151
  this.deviceStatus = powerSwitch ? 'on' : 'off';
161
152
  this.mode = devDict.workMode ?? 'manual';
162
-
163
- this.speed = devDict.fanSpeedLevel ?? 0;
153
+
154
+ const reportedFanSpeed = devDict.fanSpeedLevel ?? 0;
155
+ const isAutoSentinel = reportedFanSpeed === 255;
156
+ this.speed = isAutoSentinel ? 0 : reportedFanSpeed;
164
157
  this.setSpeedLevel = devDict.manualSpeedLevel ?? 1;
165
-
158
+
166
159
  // Parse filter life with proper fallback handling and logging
167
160
  let filterLife = 0;
168
161
  if (devDict.filterLifePercent !== undefined) {
@@ -186,7 +179,11 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
186
179
  light_detection_switch: Boolean(devDict.lightDetectionSwitch ?? 0),
187
180
  environment_light_state: Boolean(devDict.environmentLightState ?? 0),
188
181
  screen_switch: Boolean(devDict.screenSwitch ?? 0),
189
- screenStatus: Boolean(devDict.screenSwitch ?? 0) ? 'on' : 'off'
182
+ screenStatus: Boolean(devDict.screenSwitch ?? 0) ? 'on' : 'off',
183
+ manualSpeedLevel: this.setSpeedLevel,
184
+ mode: this.mode,
185
+ speed: this.speed,
186
+ isAutoSentinel
190
187
  };
191
188
 
192
189
  if (this.hasFeature('air_quality')) {
@@ -212,25 +209,24 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
212
209
  }
213
210
 
214
211
  /**
215
- * Check if the current device is a Vital series model
216
- * @returns true if this is a LAP-V102S or LAP-V201S device
212
+ * Determine whether this purifier requires the powerSwitch payload and nested response handling.
217
213
  */
218
- private isVitalSeries(): boolean {
219
- return this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
214
+ private requiresPowerSwitchPayload(): boolean {
215
+ const type = (this.deviceType || '').toUpperCase();
216
+ return type.startsWith('LAP-V') || type.startsWith('LAP-EL');
220
217
  }
221
218
 
222
219
  /**
223
- * Override turn on method to handle Vital series special cases
220
+ * Override turn on to use the powerSwitch payload required by bypassV2 purifiers.
224
221
  */
225
222
  override async turnOn(): Promise<boolean> {
226
223
  logger.info(`Turning on device: ${this.deviceName}`);
227
224
 
228
225
  const [head, body] = this.buildApiDict('setSwitch');
229
226
 
230
- // For Vital series, use powerSwitch per YAML spec
231
- // For other models, use the parent class implementation
232
- if (this.isVitalSeries()) {
233
- logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
227
+ // BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation
228
+ if (this.requiresPowerSwitchPayload()) {
229
+ logger.debug(`Using bypassV2 power payload for ${this.deviceName}`);
234
230
  body.payload.data = {
235
231
  powerSwitch: 1,
236
232
  switchIdx: 0
@@ -243,8 +239,8 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
243
239
  head
244
240
  );
245
241
 
246
- // Use Vital-specific response checking
247
- const success = this.checkVitalResponse(response, status, 'turnOn');
242
+ // Use bypassV2 response checking
243
+ const success = this.checkV2Response(response, status, 'turnOn');
248
244
  if (success) {
249
245
  this.deviceStatus = 'on';
250
246
 
@@ -257,23 +253,22 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
257
253
  }
258
254
  return success;
259
255
  } else {
260
- // Use parent implementation for non-Vital models
256
+ // Use parent implementation for non-bypassV2 models
261
257
  return super.turnOn();
262
258
  }
263
259
  }
264
260
 
265
261
  /**
266
- * Override turn off method to handle Vital series special cases
262
+ * Override turn off to use the powerSwitch payload required by bypassV2 purifiers.
267
263
  */
268
264
  override async turnOff(): Promise<boolean> {
269
265
  logger.info(`Turning off device: ${this.deviceName}`);
270
266
 
271
267
  const [head, body] = this.buildApiDict('setSwitch');
272
268
 
273
- // For Vital series, use powerSwitch per YAML spec
274
- // For other models, use the parent class implementation
275
- if (this.isVitalSeries()) {
276
- logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
269
+ // BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation
270
+ if (this.requiresPowerSwitchPayload()) {
271
+ logger.debug(`Using bypassV2 power payload for ${this.deviceName}`);
277
272
  body.payload.data = {
278
273
  powerSwitch: 0,
279
274
  switchIdx: 0
@@ -286,8 +281,8 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
286
281
  head
287
282
  );
288
283
 
289
- // Use Vital-specific response checking
290
- const success = this.checkVitalResponse(response, status, 'turnOff');
284
+ // Use bypassV2 response checking
285
+ const success = this.checkV2Response(response, status, 'turnOff');
291
286
  if (success) {
292
287
  this.deviceStatus = 'off';
293
288
 
@@ -300,13 +295,13 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
300
295
  }
301
296
  return success;
302
297
  } else {
303
- // Use parent implementation for non-Vital models
298
+ // Use parent implementation for non-bypassV2 models
304
299
  return super.turnOff();
305
300
  }
306
301
  }
307
302
 
308
303
  /**
309
- * Override setMode method to handle Vital series special cases
304
+ * Override setMode to send workMode payloads for bypassV2 purifiers.
310
305
  */
311
306
  override async setMode(mode: string): Promise<boolean> {
312
307
  if (!this.modes.includes(mode as any)) {
@@ -321,10 +316,9 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
321
316
  return this.setMode('manual');
322
317
  }
323
318
 
324
- // For Vital series, use special response handling
325
- // For other models, use the parent implementation
326
- if (this.isVitalSeries()) {
327
- logger.debug(`Using Vital series specific response handling for ${this.deviceName}`);
319
+ // BypassV2 purifiers need the workMode payload; other models use the base implementation
320
+ if (this.requiresPowerSwitchPayload()) {
321
+ logger.debug(`Using bypassV2 mode payload for ${this.deviceName}`);
328
322
  const [head, body] = this.buildApiDict('setPurifierMode');
329
323
  body.payload.data = {
330
324
  workMode: mode
@@ -337,7 +331,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
337
331
  head
338
332
  );
339
333
 
340
- const success = this.checkVitalResponse(response, status, 'setMode');
334
+ const success = this.checkV2Response(response, status, 'setMode');
341
335
  if (success) {
342
336
  this.details.mode = mode;
343
337
  this._mode = mode;
@@ -355,13 +349,13 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
355
349
  return false;
356
350
  }
357
351
  } else {
358
- // Use parent implementation for non-Vital models
352
+ // Use parent implementation for non-bypassV2 models
359
353
  return super.setMode(mode);
360
354
  }
361
355
  }
362
356
 
363
357
  /**
364
- * Override changeFanSpeed method to handle Vital series special cases
358
+ * Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers.
365
359
  */
366
360
  override async changeFanSpeed(speed: number): Promise<boolean> {
367
361
  const speeds = this.config.levels ?? [];
@@ -374,10 +368,10 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
374
368
 
375
369
  const [head, body] = this.buildApiDict('setLevel');
376
370
 
377
- // Specific handling for Vital series devices
378
- if (this.isVitalSeries()) {
379
- logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
380
- // For Vital series, use levelIdx, levelType, and manualSpeedLevel per YAML spec
371
+ // Specific handling for bypassV2 devices
372
+ if (this.requiresPowerSwitchPayload()) {
373
+ logger.debug(`Using bypassV2 speed payload for ${this.deviceName}`);
374
+ // For these devices, use levelIdx, levelType, and manualSpeedLevel per YAML spec
381
375
  body.payload.data = {
382
376
  levelIdx: 0,
383
377
  levelType: 'wind',
@@ -391,7 +385,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
391
385
  head
392
386
  );
393
387
 
394
- const success = this.checkVitalResponse(response, status, 'changeFanSpeed');
388
+ const success = this.checkV2Response(response, status, 'changeFanSpeed');
395
389
  if (success) {
396
390
  this.speed = speed;
397
391
  this.details.manualSpeedLevel = speed;
@@ -409,7 +403,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
409
403
  return false;
410
404
  }
411
405
  } else {
412
- // For non-Vital models, use id, level, mode, and type
406
+ // For non-bypassV2 models, use id, level, mode, and type
413
407
  body.payload.data = {
414
408
  id: 0,
415
409
  level: speed,
@@ -459,16 +453,16 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
459
453
  );
460
454
 
461
455
  // Use the appropriate response checking method based on device type
462
- const success = this.isVitalSeries()
463
- ? this.checkVitalResponse(response, status, 'setAutoPreference')
456
+ const success = this.requiresPowerSwitchPayload()
457
+ ? this.checkV2Response(response, status, 'setAutoPreference')
464
458
  : this.checkResponse([response, status], 'setAutoPreference');
465
459
 
466
460
  if (success) {
467
461
  this.details.auto_preference = preference;
468
462
  this.details.auto_preference_type = preference;
469
463
 
470
- // Force a details update to sync state for Vital series
471
- if (this.isVitalSeries()) {
464
+ // Force a details update to sync state for bypassV2 purifiers
465
+ if (this.requiresPowerSwitchPayload()) {
472
466
  setTimeout(() => {
473
467
  this.getDetails().catch(e => {
474
468
  logger.debug(`Background state refresh failed: ${e}`);
@@ -496,15 +490,15 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
496
490
  );
497
491
 
498
492
  // Use the appropriate response checking method based on device type
499
- const success = this.isVitalSeries()
500
- ? this.checkVitalResponse(response, status, 'setLightDetection')
493
+ const success = this.requiresPowerSwitchPayload()
494
+ ? this.checkV2Response(response, status, 'setLightDetection')
501
495
  : this.checkResponse([response, status], 'setLightDetection');
502
496
 
503
497
  if (success) {
504
498
  this.details.light_detection_switch = enabled;
505
499
 
506
- // Force a details update to sync state for Vital series
507
- if (this.isVitalSeries()) {
500
+ // Force a details update to sync state for bypassV2 purifiers
501
+ if (this.requiresPowerSwitchPayload()) {
508
502
  setTimeout(() => {
509
503
  this.getDetails().catch(e => {
510
504
  logger.debug(`Background state refresh failed: ${e}`);
@@ -595,8 +589,8 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
595
589
  );
596
590
 
597
591
  // Use the appropriate response checking method based on device type
598
- const success = this.isVitalSeries()
599
- ? this.checkVitalResponse(response, status, 'setOscillation')
592
+ const success = this.requiresPowerSwitchPayload()
593
+ ? this.checkV2Response(response, status, 'setOscillation')
600
594
  : this.checkResponse([response, status], 'setOscillation');
601
595
 
602
596
  if (success) {