tsvesync 1.0.75 → 1.0.77

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.
@@ -29,6 +29,11 @@ export declare class VeSyncAirBaseV2 extends VeSyncAirBypass {
29
29
  duration: number;
30
30
  action: string;
31
31
  } | null);
32
+ /**
33
+ * Check response for Vital series devices
34
+ * These devices return success at API level but may have inner errors
35
+ */
36
+ protected checkVitalResponse(response: any, status: number, method: string): boolean;
32
37
  /**
33
38
  * Get device details
34
39
  */
@@ -37,6 +42,27 @@ export declare class VeSyncAirBaseV2 extends VeSyncAirBypass {
37
42
  * Build purifier dictionary
38
43
  */
39
44
  protected buildPurifierDict(devDict: Record<string, any>): void;
45
+ /**
46
+ * Check if the current device is a Vital series model
47
+ * @returns true if this is a LAP-V102S or LAP-V201S device
48
+ */
49
+ private isVitalSeries;
50
+ /**
51
+ * Override turn on method to handle Vital series special cases
52
+ */
53
+ turnOn(): Promise<boolean>;
54
+ /**
55
+ * Override turn off method to handle Vital series special cases
56
+ */
57
+ turnOff(): Promise<boolean>;
58
+ /**
59
+ * Override setMode method to handle Vital series special cases
60
+ */
61
+ setMode(mode: string): Promise<boolean>;
62
+ /**
63
+ * Override changeFanSpeed method to handle Vital series special cases
64
+ */
65
+ changeFanSpeed(speed: number): Promise<boolean>;
40
66
  /**
41
67
  * Set auto preference
42
68
  */
@@ -45,13 +45,66 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
45
45
  set timer(value) {
46
46
  this._timer = value;
47
47
  }
48
+ /**
49
+ * Check response for Vital series devices
50
+ * These devices return success at API level but may have inner errors
51
+ */
52
+ checkVitalResponse(response, status, method) {
53
+ // First check basic response
54
+ if (!this.checkResponse([response, status], method)) {
55
+ return false;
56
+ }
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
61
+ }
62
+ // For Vital series, we need to check the inner result code
63
+ 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
69
+ this.connectionStatus = 'online';
70
+ return true;
71
+ }
72
+ return true;
73
+ }
48
74
  /**
49
75
  * Get device details
50
76
  */
51
77
  async getDetails() {
78
+ var _a;
52
79
  logger_1.logger.debug(`Getting details for device: ${this.deviceName}`);
53
80
  const [head, body] = this.buildApiDict('getPurifierStatus');
54
81
  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');
84
+ // First check the outer response
85
+ if (!response || status !== 200) {
86
+ logger_1.logger.debug(`Error getting purifier details for ${this.deviceName}: Invalid response`);
87
+ this.connectionStatus = 'offline';
88
+ return false;
89
+ }
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) {
93
+ // Check if we have a valid getPurifierStatus response
94
+ if (response.code === 0 && ((_a = response.result) === null || _a === void 0 ? void 0 : _a.result)) {
95
+ const statusResponse = response.result.result;
96
+ // 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}`);
98
+ this.buildPurifierDict(statusResponse);
99
+ if (statusResponse.configuration) {
100
+ this.buildConfigDict(statusResponse.configuration);
101
+ }
102
+ // Keep device online since we have valid status
103
+ this.connectionStatus = 'online';
104
+ return true;
105
+ }
106
+ }
107
+ // Standard processing for non-Vital devices or when no status data is available
55
108
  if (!this.checkResponse([response, status], 'getDetails') || !(response === null || response === void 0 ? void 0 : response.result)) {
56
109
  logger_1.logger.debug('Error getting purifier details');
57
110
  this.connectionStatus = 'offline';
@@ -116,6 +169,184 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
116
169
  this.details.auto_preference_type = null;
117
170
  }
118
171
  }
172
+ /**
173
+ * Check if the current device is a Vital series model
174
+ * @returns true if this is a LAP-V102S or LAP-V201S device
175
+ */
176
+ isVitalSeries() {
177
+ return this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
178
+ }
179
+ /**
180
+ * Override turn on method to handle Vital series special cases
181
+ */
182
+ async turnOn() {
183
+ logger_1.logger.info(`Turning on device: ${this.deviceName}`);
184
+ const [head, body] = this.buildApiDict('setSwitch');
185
+ // For Vital series, use powerSwitch per YAML spec
186
+ // For other models, use the parent class implementation
187
+ if (this.isVitalSeries()) {
188
+ logger_1.logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
189
+ body.payload.data = {
190
+ powerSwitch: 1,
191
+ switchIdx: 0
192
+ };
193
+ const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
194
+ // Use Vital-specific response checking
195
+ const success = this.checkVitalResponse(response, status, 'turnOn');
196
+ if (success) {
197
+ this.deviceStatus = 'on';
198
+ // Force a details update to sync state
199
+ setTimeout(() => {
200
+ this.getDetails().catch(e => {
201
+ logger_1.logger.debug(`Background state refresh failed: ${e}`);
202
+ });
203
+ }, 1000);
204
+ }
205
+ return success;
206
+ }
207
+ else {
208
+ // Use parent implementation for non-Vital models
209
+ return super.turnOn();
210
+ }
211
+ }
212
+ /**
213
+ * Override turn off method to handle Vital series special cases
214
+ */
215
+ async turnOff() {
216
+ logger_1.logger.info(`Turning off device: ${this.deviceName}`);
217
+ const [head, body] = this.buildApiDict('setSwitch');
218
+ // For Vital series, use powerSwitch per YAML spec
219
+ // For other models, use the parent class implementation
220
+ if (this.isVitalSeries()) {
221
+ logger_1.logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
222
+ body.payload.data = {
223
+ powerSwitch: 0,
224
+ switchIdx: 0
225
+ };
226
+ const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
227
+ // Use Vital-specific response checking
228
+ const success = this.checkVitalResponse(response, status, 'turnOff');
229
+ if (success) {
230
+ this.deviceStatus = 'off';
231
+ // Force a details update to sync state
232
+ setTimeout(() => {
233
+ this.getDetails().catch(e => {
234
+ logger_1.logger.debug(`Background state refresh failed: ${e}`);
235
+ });
236
+ }, 1000);
237
+ }
238
+ return success;
239
+ }
240
+ else {
241
+ // Use parent implementation for non-Vital models
242
+ return super.turnOff();
243
+ }
244
+ }
245
+ /**
246
+ * Override setMode method to handle Vital series special cases
247
+ */
248
+ async setMode(mode) {
249
+ if (!this.modes.includes(mode)) {
250
+ const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
251
+ logger_1.logger.error(`${error} for device: ${this.deviceName}`);
252
+ throw new Error(error);
253
+ }
254
+ // Special handling for Core200S stays the same for all models
255
+ if (this.deviceType.includes('Core200S') && mode === 'auto') {
256
+ logger_1.logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
257
+ return this.setMode('manual');
258
+ }
259
+ // For Vital series, use special response handling
260
+ // For other models, use the parent implementation
261
+ if (this.isVitalSeries()) {
262
+ logger_1.logger.debug(`Using Vital series specific response handling for ${this.deviceName}`);
263
+ const [head, body] = this.buildApiDict('setPurifierMode');
264
+ body.payload.data = {
265
+ workMode: mode
266
+ };
267
+ const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
268
+ const success = this.checkVitalResponse(response, status, 'setMode');
269
+ if (success) {
270
+ this.details.mode = mode;
271
+ this._mode = mode;
272
+ // Force a details update to sync state
273
+ setTimeout(() => {
274
+ this.getDetails().catch(e => {
275
+ logger_1.logger.debug(`Background state refresh failed: ${e}`);
276
+ });
277
+ }, 1000);
278
+ return true;
279
+ }
280
+ else {
281
+ logger_1.logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
282
+ return false;
283
+ }
284
+ }
285
+ else {
286
+ // Use parent implementation for non-Vital models
287
+ return super.setMode(mode);
288
+ }
289
+ }
290
+ /**
291
+ * Override changeFanSpeed method to handle Vital series special cases
292
+ */
293
+ async changeFanSpeed(speed) {
294
+ var _a;
295
+ const speeds = (_a = this.config.levels) !== null && _a !== void 0 ? _a : [];
296
+ if (speeds.length > 0 && !speeds.includes(speed)) {
297
+ logger_1.logger.error(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')} for device: ${this.deviceName}`);
298
+ return false;
299
+ }
300
+ logger_1.logger.info(`Changing fan speed to ${speed} for device: ${this.deviceName}`);
301
+ const [head, body] = this.buildApiDict('setLevel');
302
+ // Specific handling for Vital series devices
303
+ if (this.isVitalSeries()) {
304
+ logger_1.logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
305
+ // For Vital series, use levelIdx, levelType, and manualSpeedLevel per YAML spec
306
+ body.payload.data = {
307
+ levelIdx: 0,
308
+ levelType: 'wind',
309
+ manualSpeedLevel: speed
310
+ };
311
+ const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
312
+ const success = this.checkVitalResponse(response, status, 'changeFanSpeed');
313
+ if (success) {
314
+ this.speed = speed;
315
+ this.details.manualSpeedLevel = speed;
316
+ // Force a details update to sync state
317
+ setTimeout(() => {
318
+ this.getDetails().catch(e => {
319
+ logger_1.logger.debug(`Background state refresh failed: ${e}`);
320
+ });
321
+ }, 1000);
322
+ return true;
323
+ }
324
+ else {
325
+ logger_1.logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
326
+ return false;
327
+ }
328
+ }
329
+ else {
330
+ // For non-Vital models, use id, level, mode, and type
331
+ body.payload.data = {
332
+ id: 0,
333
+ level: speed,
334
+ mode: 'manual',
335
+ type: 'wind'
336
+ };
337
+ const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
338
+ const success = this.checkResponse([response, status], 'changeFanSpeed');
339
+ if (success) {
340
+ this.speed = speed;
341
+ this.details.manualSpeedLevel = speed;
342
+ return true;
343
+ }
344
+ else {
345
+ logger_1.logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
346
+ return false;
347
+ }
348
+ }
349
+ }
119
350
  /**
120
351
  * Set auto preference
121
352
  */
@@ -130,9 +361,21 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
130
361
  roomSize: roomSize
131
362
  };
132
363
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
133
- const success = this.checkResponse([response, status], 'setAutoPreference');
364
+ // Use the appropriate response checking method based on device type
365
+ const success = this.isVitalSeries()
366
+ ? this.checkVitalResponse(response, status, 'setAutoPreference')
367
+ : this.checkResponse([response, status], 'setAutoPreference');
134
368
  if (success) {
135
369
  this.details.auto_preference = preference;
370
+ this.details.auto_preference_type = preference;
371
+ // Force a details update to sync state for Vital series
372
+ if (this.isVitalSeries()) {
373
+ setTimeout(() => {
374
+ this.getDetails().catch(e => {
375
+ logger_1.logger.debug(`Background state refresh failed: ${e}`);
376
+ });
377
+ }, 1000);
378
+ }
136
379
  }
137
380
  return success;
138
381
  }
@@ -145,9 +388,20 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
145
388
  lightDetectionSwitch: enabled ? 1 : 0
146
389
  };
147
390
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
148
- const success = this.checkResponse([response, status], 'setLightDetection');
391
+ // Use the appropriate response checking method based on device type
392
+ const success = this.isVitalSeries()
393
+ ? this.checkVitalResponse(response, status, 'setLightDetection')
394
+ : this.checkResponse([response, status], 'setLightDetection');
149
395
  if (success) {
150
396
  this.details.light_detection_switch = enabled;
397
+ // Force a details update to sync state for Vital series
398
+ if (this.isVitalSeries()) {
399
+ setTimeout(() => {
400
+ this.getDetails().catch(e => {
401
+ logger_1.logger.debug(`Background state refresh failed: ${e}`);
402
+ });
403
+ }, 1000);
404
+ }
151
405
  }
152
406
  return success;
153
407
  }
@@ -215,7 +469,10 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
215
469
  switchIdx: 0
216
470
  };
217
471
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
218
- const success = this.checkResponse([response, status], 'setOscillation');
472
+ // Use the appropriate response checking method based on device type
473
+ const success = this.isVitalSeries()
474
+ ? this.checkVitalResponse(response, status, 'setOscillation')
475
+ : this.checkResponse([response, status], 'setOscillation');
219
476
  if (success) {
220
477
  this.details.oscillationState = toggle;
221
478
  this.details.oscillationSwitch = toggle ? 1 : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.0.75",
3
+ "version": "1.0.77",
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",
@@ -54,6 +54,37 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
54
54
  this._timer = value;
55
55
  }
56
56
 
57
+ /**
58
+ * Check response for Vital series devices
59
+ * These devices return success at API level but may have inner errors
60
+ */
61
+ protected checkVitalResponse(response: any, status: number, method: string): boolean {
62
+ // First check basic response
63
+ if (!this.checkResponse([response, status], method)) {
64
+ return false;
65
+ }
66
+
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
71
+ }
72
+
73
+ // For Vital series, we need to check the inner result code
74
+ 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
81
+ this.connectionStatus = 'online';
82
+ return true;
83
+ }
84
+
85
+ return true;
86
+ }
87
+
57
88
  /**
58
89
  * Get device details
59
90
  */
@@ -68,6 +99,36 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
68
99
  head
69
100
  );
70
101
 
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');
104
+
105
+ // First check the outer response
106
+ if (!response || status !== 200) {
107
+ logger.debug(`Error getting purifier details for ${this.deviceName}: Invalid response`);
108
+ this.connectionStatus = 'offline';
109
+ return false;
110
+ }
111
+
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) {
115
+ // Check if we have a valid getPurifierStatus response
116
+ if (response.code === 0 && response.result?.result) {
117
+ const statusResponse = response.result.result;
118
+
119
+ // 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}`);
121
+ this.buildPurifierDict(statusResponse);
122
+ if (statusResponse.configuration) {
123
+ this.buildConfigDict(statusResponse.configuration);
124
+ }
125
+ // Keep device online since we have valid status
126
+ this.connectionStatus = 'online';
127
+ return true;
128
+ }
129
+ }
130
+
131
+ // Standard processing for non-Vital devices or when no status data is available
71
132
  if (!this.checkResponse([response, status], 'getDetails') || !response?.result) {
72
133
  logger.debug('Error getting purifier details');
73
134
  this.connectionStatus = 'offline';
@@ -135,6 +196,231 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
135
196
  }
136
197
  }
137
198
 
199
+ /**
200
+ * Check if the current device is a Vital series model
201
+ * @returns true if this is a LAP-V102S or LAP-V201S device
202
+ */
203
+ private isVitalSeries(): boolean {
204
+ return this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
205
+ }
206
+
207
+ /**
208
+ * Override turn on method to handle Vital series special cases
209
+ */
210
+ override async turnOn(): Promise<boolean> {
211
+ logger.info(`Turning on device: ${this.deviceName}`);
212
+
213
+ const [head, body] = this.buildApiDict('setSwitch');
214
+
215
+ // For Vital series, use powerSwitch per YAML spec
216
+ // For other models, use the parent class implementation
217
+ if (this.isVitalSeries()) {
218
+ logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
219
+ body.payload.data = {
220
+ powerSwitch: 1,
221
+ switchIdx: 0
222
+ };
223
+
224
+ const [response, status] = await this.callApi(
225
+ '/cloud/v2/deviceManaged/bypassV2',
226
+ 'post',
227
+ body,
228
+ head
229
+ );
230
+
231
+ // Use Vital-specific response checking
232
+ const success = this.checkVitalResponse(response, status, 'turnOn');
233
+ if (success) {
234
+ this.deviceStatus = 'on';
235
+
236
+ // Force a details update to sync state
237
+ setTimeout(() => {
238
+ this.getDetails().catch(e => {
239
+ logger.debug(`Background state refresh failed: ${e}`);
240
+ });
241
+ }, 1000);
242
+ }
243
+ return success;
244
+ } else {
245
+ // Use parent implementation for non-Vital models
246
+ return super.turnOn();
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Override turn off method to handle Vital series special cases
252
+ */
253
+ override async turnOff(): Promise<boolean> {
254
+ logger.info(`Turning off device: ${this.deviceName}`);
255
+
256
+ const [head, body] = this.buildApiDict('setSwitch');
257
+
258
+ // For Vital series, use powerSwitch per YAML spec
259
+ // For other models, use the parent class implementation
260
+ if (this.isVitalSeries()) {
261
+ logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
262
+ body.payload.data = {
263
+ powerSwitch: 0,
264
+ switchIdx: 0
265
+ };
266
+
267
+ const [response, status] = await this.callApi(
268
+ '/cloud/v2/deviceManaged/bypassV2',
269
+ 'post',
270
+ body,
271
+ head
272
+ );
273
+
274
+ // Use Vital-specific response checking
275
+ const success = this.checkVitalResponse(response, status, 'turnOff');
276
+ if (success) {
277
+ this.deviceStatus = 'off';
278
+
279
+ // Force a details update to sync state
280
+ setTimeout(() => {
281
+ this.getDetails().catch(e => {
282
+ logger.debug(`Background state refresh failed: ${e}`);
283
+ });
284
+ }, 1000);
285
+ }
286
+ return success;
287
+ } else {
288
+ // Use parent implementation for non-Vital models
289
+ return super.turnOff();
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Override setMode method to handle Vital series special cases
295
+ */
296
+ override async setMode(mode: string): Promise<boolean> {
297
+ if (!this.modes.includes(mode as any)) {
298
+ const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
299
+ logger.error(`${error} for device: ${this.deviceName}`);
300
+ throw new Error(error);
301
+ }
302
+
303
+ // Special handling for Core200S stays the same for all models
304
+ if (this.deviceType.includes('Core200S') && mode === 'auto') {
305
+ logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
306
+ return this.setMode('manual');
307
+ }
308
+
309
+ // For Vital series, use special response handling
310
+ // For other models, use the parent implementation
311
+ if (this.isVitalSeries()) {
312
+ logger.debug(`Using Vital series specific response handling for ${this.deviceName}`);
313
+ const [head, body] = this.buildApiDict('setPurifierMode');
314
+ body.payload.data = {
315
+ workMode: mode
316
+ };
317
+
318
+ const [response, status] = await this.callApi(
319
+ '/cloud/v2/deviceManaged/bypassV2',
320
+ 'post',
321
+ body,
322
+ head
323
+ );
324
+
325
+ const success = this.checkVitalResponse(response, status, 'setMode');
326
+ if (success) {
327
+ this.details.mode = mode;
328
+ this._mode = mode;
329
+
330
+ // Force a details update to sync state
331
+ setTimeout(() => {
332
+ this.getDetails().catch(e => {
333
+ logger.debug(`Background state refresh failed: ${e}`);
334
+ });
335
+ }, 1000);
336
+
337
+ return true;
338
+ } else {
339
+ logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
340
+ return false;
341
+ }
342
+ } else {
343
+ // Use parent implementation for non-Vital models
344
+ return super.setMode(mode);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Override changeFanSpeed method to handle Vital series special cases
350
+ */
351
+ override async changeFanSpeed(speed: number): Promise<boolean> {
352
+ const speeds = this.config.levels ?? [];
353
+ if (speeds.length > 0 && !speeds.includes(speed)) {
354
+ logger.error(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')} for device: ${this.deviceName}`);
355
+ return false;
356
+ }
357
+
358
+ logger.info(`Changing fan speed to ${speed} for device: ${this.deviceName}`);
359
+
360
+ const [head, body] = this.buildApiDict('setLevel');
361
+
362
+ // Specific handling for Vital series devices
363
+ if (this.isVitalSeries()) {
364
+ logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
365
+ // For Vital series, use levelIdx, levelType, and manualSpeedLevel per YAML spec
366
+ body.payload.data = {
367
+ levelIdx: 0,
368
+ levelType: 'wind',
369
+ manualSpeedLevel: speed
370
+ };
371
+
372
+ const [response, status] = await this.callApi(
373
+ '/cloud/v2/deviceManaged/bypassV2',
374
+ 'post',
375
+ body,
376
+ head
377
+ );
378
+
379
+ const success = this.checkVitalResponse(response, status, 'changeFanSpeed');
380
+ if (success) {
381
+ this.speed = speed;
382
+ this.details.manualSpeedLevel = speed;
383
+
384
+ // Force a details update to sync state
385
+ setTimeout(() => {
386
+ this.getDetails().catch(e => {
387
+ logger.debug(`Background state refresh failed: ${e}`);
388
+ });
389
+ }, 1000);
390
+
391
+ return true;
392
+ } else {
393
+ logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
394
+ return false;
395
+ }
396
+ } else {
397
+ // For non-Vital models, use id, level, mode, and type
398
+ body.payload.data = {
399
+ id: 0,
400
+ level: speed,
401
+ mode: 'manual',
402
+ type: 'wind'
403
+ };
404
+
405
+ const [response, status] = await this.callApi(
406
+ '/cloud/v2/deviceManaged/bypassV2',
407
+ 'post',
408
+ body,
409
+ head
410
+ );
411
+
412
+ const success = this.checkResponse([response, status], 'changeFanSpeed');
413
+ if (success) {
414
+ this.speed = speed;
415
+ this.details.manualSpeedLevel = speed;
416
+ return true;
417
+ } else {
418
+ logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
419
+ return false;
420
+ }
421
+ }
422
+ }
423
+
138
424
  /**
139
425
  * Set auto preference
140
426
  */
@@ -157,9 +443,23 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
157
443
  head
158
444
  );
159
445
 
160
- const success = this.checkResponse([response, status], 'setAutoPreference');
446
+ // Use the appropriate response checking method based on device type
447
+ const success = this.isVitalSeries()
448
+ ? this.checkVitalResponse(response, status, 'setAutoPreference')
449
+ : this.checkResponse([response, status], 'setAutoPreference');
450
+
161
451
  if (success) {
162
452
  this.details.auto_preference = preference;
453
+ this.details.auto_preference_type = preference;
454
+
455
+ // Force a details update to sync state for Vital series
456
+ if (this.isVitalSeries()) {
457
+ setTimeout(() => {
458
+ this.getDetails().catch(e => {
459
+ logger.debug(`Background state refresh failed: ${e}`);
460
+ });
461
+ }, 1000);
462
+ }
163
463
  }
164
464
  return success;
165
465
  }
@@ -180,9 +480,22 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
180
480
  head
181
481
  );
182
482
 
183
- const success = this.checkResponse([response, status], 'setLightDetection');
483
+ // Use the appropriate response checking method based on device type
484
+ const success = this.isVitalSeries()
485
+ ? this.checkVitalResponse(response, status, 'setLightDetection')
486
+ : this.checkResponse([response, status], 'setLightDetection');
487
+
184
488
  if (success) {
185
489
  this.details.light_detection_switch = enabled;
490
+
491
+ // Force a details update to sync state for Vital series
492
+ if (this.isVitalSeries()) {
493
+ setTimeout(() => {
494
+ this.getDetails().catch(e => {
495
+ logger.debug(`Background state refresh failed: ${e}`);
496
+ });
497
+ }, 1000);
498
+ }
186
499
  }
187
500
  return success;
188
501
  }
@@ -266,7 +579,11 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
266
579
  head
267
580
  );
268
581
 
269
- const success = this.checkResponse([response, status], 'setOscillation');
582
+ // Use the appropriate response checking method based on device type
583
+ const success = this.isVitalSeries()
584
+ ? this.checkVitalResponse(response, status, 'setOscillation')
585
+ : this.checkResponse([response, status], 'setOscillation');
586
+
270
587
  if (success) {
271
588
  this.details.oscillationState = toggle;
272
589
  this.details.oscillationSwitch = toggle ? 1 : 0;