tsvesync 1.0.76 → 1.0.78

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,6 +45,32 @@ 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
  */
@@ -143,6 +169,184 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
143
169
  this.details.auto_preference_type = null;
144
170
  }
145
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
+ }
146
350
  /**
147
351
  * Set auto preference
148
352
  */
@@ -157,9 +361,21 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
157
361
  roomSize: roomSize
158
362
  };
159
363
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
160
- 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');
161
368
  if (success) {
162
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
+ }
163
379
  }
164
380
  return success;
165
381
  }
@@ -172,9 +388,20 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
172
388
  lightDetectionSwitch: enabled ? 1 : 0
173
389
  };
174
390
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
175
- 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');
176
395
  if (success) {
177
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
+ }
178
405
  }
179
406
  return success;
180
407
  }
@@ -242,7 +469,10 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
242
469
  switchIdx: 0
243
470
  };
244
471
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
245
- 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');
246
476
  if (success) {
247
477
  this.details.oscillationState = toggle;
248
478
  this.details.oscillationSwitch = toggle ? 1 : 0;
@@ -4,7 +4,7 @@ import { VeSync } from '../vesync';
4
4
  * VeSync Tower Fan Implementation
5
5
  */
6
6
  export declare class VeSyncTowerFan extends VeSyncAirBaseV2 {
7
- protected readonly towerModes: readonly ["normal", "advancedSleep", "off"];
7
+ protected readonly towerModes: readonly ["normal", "auto", "advancedSleep", "turbo", "off"];
8
8
  constructor(details: Record<string, any>, manager: VeSync);
9
9
  /**
10
10
  * Get device details
@@ -25,7 +25,7 @@ export declare class VeSyncTowerFan extends VeSyncAirBaseV2 {
25
25
  /**
26
26
  * Set tower fan mode
27
27
  */
28
- mode_toggle(mode: string): Promise<boolean>;
28
+ modeToggle(mode: string): Promise<boolean>;
29
29
  /**
30
30
  * Change fan speed
31
31
  */
@@ -38,6 +38,14 @@ export declare class VeSyncTowerFan extends VeSyncAirBaseV2 {
38
38
  * Set manual mode - adapter to set mode to normal
39
39
  */
40
40
  manual_mode(): Promise<boolean>;
41
+ /**
42
+ * Set auto mode
43
+ */
44
+ auto_mode(): Promise<boolean>;
45
+ /**
46
+ * Set turbo mode
47
+ */
48
+ turbo_mode(): Promise<boolean>;
41
49
  /**
42
50
  * Set advanced sleep mode
43
51
  */
@@ -50,6 +58,10 @@ export declare class VeSyncTowerFan extends VeSyncAirBaseV2 {
50
58
  * Override setMode to handle tower fan specific modes
51
59
  */
52
60
  setMode(mode: string): Promise<boolean>;
61
+ /**
62
+ * Alias for modeToggle to match Python naming convention
63
+ */
64
+ mode_toggle(mode: string): Promise<boolean>;
53
65
  /**
54
66
  * Get current speed
55
67
  */
@@ -66,6 +78,10 @@ export declare class VeSyncTowerFan extends VeSyncAirBaseV2 {
66
78
  * Get mute status
67
79
  */
68
80
  get muteState(): boolean;
81
+ /**
82
+ * Get mute switch status
83
+ */
84
+ get muteSwitch(): boolean;
69
85
  /**
70
86
  * Get temperature (in tenths of a degree)
71
87
  */
@@ -90,6 +106,10 @@ export declare class VeSyncTowerFan extends VeSyncAirBaseV2 {
90
106
  * Get error code
91
107
  */
92
108
  get errorCode(): number;
109
+ /**
110
+ * Set oscillation state
111
+ */
112
+ setOscillation(state: boolean): Promise<boolean>;
93
113
  /**
94
114
  * Turn oscillation on
95
115
  */
@@ -9,7 +9,7 @@ const logger_1 = require("../logger");
9
9
  class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
10
10
  constructor(details, manager) {
11
11
  super(details, manager);
12
- this.towerModes = ['normal', 'advancedSleep', 'off'];
12
+ this.towerModes = ['normal', 'auto', 'advancedSleep', 'turbo', 'off'];
13
13
  logger_1.logger.debug(`Initialized VeSyncTowerFan device: ${this.deviceName}`);
14
14
  }
15
15
  /**
@@ -30,26 +30,19 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
30
30
  this.connectionStatus = 'offline';
31
31
  return false;
32
32
  }
33
- const deviceData = innerResponse.result;
34
- this.deviceStatus = deviceData.powerSwitch === 1 ? 'on' : 'off';
35
- this.details = {
36
- powerSwitch: deviceData.powerSwitch,
37
- workMode: deviceData.workMode,
38
- manualSpeedLevel: deviceData.manualSpeedLevel,
39
- fanSpeedLevel: deviceData.fanSpeedLevel,
40
- screenState: deviceData.screenState,
41
- screenSwitch: deviceData.screenSwitch,
42
- oscillationState: deviceData.oscillationState,
43
- oscillationSwitch: deviceData.oscillationSwitch,
44
- timerRemain: deviceData.timerRemain,
45
- temperature: deviceData.temperature || 0,
46
- humidity: deviceData.humidity || 0,
47
- thermalComfort: deviceData.thermalComfort,
48
- sleepPreference: deviceData.sleepPreference || {},
49
- scheduleCount: deviceData.scheduleCount,
50
- displayingType: deviceData.displayingType,
51
- errorCode: deviceData.errorCode
52
- };
33
+ const innerResult = innerResponse.result;
34
+ // Use the same methods as Python version
35
+ if (innerResult) {
36
+ this.buildPurifierDict(innerResult);
37
+ }
38
+ else {
39
+ this.connectionStatus = 'offline';
40
+ logger_1.logger.debug('error in inner result dict from tower fan');
41
+ return false;
42
+ }
43
+ if (innerResult.configuration) {
44
+ this.buildConfigDict(innerResult.configuration);
45
+ }
53
46
  logger_1.logger.debug(`Successfully got details for device: ${this.deviceName}`);
54
47
  return true;
55
48
  }
@@ -93,7 +86,7 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
93
86
  /**
94
87
  * Set tower fan mode
95
88
  */
96
- async mode_toggle(mode) {
89
+ async modeToggle(mode) {
97
90
  if (!this.towerModes.includes(mode)) {
98
91
  logger_1.logger.debug(`Invalid mode: ${mode}. Must be one of: ${this.towerModes.join(', ')}`);
99
92
  return false;
@@ -120,11 +113,12 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
120
113
  * Change fan speed
121
114
  */
122
115
  async changeFanSpeed(speed) {
123
- // const speeds = this.config.levels ?? [];
124
- // if (!speeds.includes(speed)) {
125
- // logger.debug(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')}`);
126
- // return false;
127
- // }
116
+ var _a;
117
+ const speeds = (_a = this.config.levels) !== null && _a !== void 0 ? _a : [];
118
+ if (!speeds.includes(speed)) {
119
+ logger_1.logger.debug(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')}`);
120
+ return false;
121
+ }
128
122
  const [head, body] = this.buildApiDict('setLevel');
129
123
  body.payload.data = {
130
124
  levelIdx: 0,
@@ -146,7 +140,7 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
146
140
  */
147
141
  async normal_mode() {
148
142
  logger_1.logger.debug(`Setting normal mode for device: ${this.deviceName}`);
149
- return this.mode_toggle('normal');
143
+ return this.modeToggle('normal');
150
144
  }
151
145
  /**
152
146
  * Set manual mode - adapter to set mode to normal
@@ -155,12 +149,26 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
155
149
  logger_1.logger.debug(`Setting manual mode for device: ${this.deviceName}`);
156
150
  return this.normal_mode();
157
151
  }
152
+ /**
153
+ * Set auto mode
154
+ */
155
+ async auto_mode() {
156
+ logger_1.logger.debug(`Setting auto mode for device: ${this.deviceName}`);
157
+ return this.modeToggle('auto');
158
+ }
159
+ /**
160
+ * Set turbo mode
161
+ */
162
+ async turbo_mode() {
163
+ logger_1.logger.debug(`Setting turbo mode for device: ${this.deviceName}`);
164
+ return this.modeToggle('turbo');
165
+ }
158
166
  /**
159
167
  * Set advanced sleep mode
160
168
  */
161
169
  async advanced_sleep_mode() {
162
170
  logger_1.logger.debug(`Setting advanced sleep mode for device: ${this.deviceName}`);
163
- return this.mode_toggle('advancedSleep');
171
+ return this.modeToggle('advancedSleep');
164
172
  }
165
173
  /**
166
174
  * Set sleep mode - adapter for advanced sleep mode
@@ -173,7 +181,13 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
173
181
  * Override setMode to handle tower fan specific modes
174
182
  */
175
183
  async setMode(mode) {
176
- return this.mode_toggle(mode);
184
+ return this.modeToggle(mode);
185
+ }
186
+ /**
187
+ * Alias for modeToggle to match Python naming convention
188
+ */
189
+ async mode_toggle(mode) {
190
+ return this.modeToggle(mode);
177
191
  }
178
192
  /**
179
193
  * Get current speed
@@ -199,6 +213,12 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
199
213
  get muteState() {
200
214
  return Boolean(this.details.muteState);
201
215
  }
216
+ /**
217
+ * Get mute switch status
218
+ */
219
+ get muteSwitch() {
220
+ return Boolean(this.details.muteSwitch);
221
+ }
202
222
  /**
203
223
  * Get temperature (in tenths of a degree)
204
224
  */
@@ -235,6 +255,26 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
235
255
  get errorCode() {
236
256
  return this.details.errorCode || 0;
237
257
  }
258
+ /**
259
+ * Set oscillation state
260
+ */
261
+ async setOscillation(state) {
262
+ const [head, body] = this.buildApiDict('setSwitch');
263
+ body.payload.data = {
264
+ oscillationSwitch: state ? 1 : 0,
265
+ switchIdx: 0
266
+ };
267
+ const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
268
+ const success = this.checkResponse([response, status], 'setOscillation');
269
+ if (success) {
270
+ this.details.oscillationSwitch = state ? 1 : 0;
271
+ this.details.oscillationState = state ? 1 : 0;
272
+ }
273
+ else {
274
+ logger_1.logger.debug('Error setting oscillation state');
275
+ }
276
+ return success;
277
+ }
238
278
  /**
239
279
  * Turn oscillation on
240
280
  */
@@ -257,20 +297,43 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
257
297
  ['Speed: ', this.speed],
258
298
  ['Fan Speed Level: ', this.details.fanSpeedLevel || 0],
259
299
  ['Display: ', this.displayState],
260
- ['Display Switch: ', this.details.screenSwitch],
261
300
  ['Oscillation: ', this.oscillationState],
262
- ['Oscillation Switch: ', this.details.oscillationSwitch],
263
- ['Mute: ', this.muteState],
264
- ['Mute Switch: ', this.details.muteSwitch],
265
- ['Timer: ', this.timer],
266
- ['Temperature: ', this.temperature / 10, '°C'],
267
- ['Humidity: ', this.humidity, '%'],
268
- ['Thermal Comfort: ', this.thermalComfort],
269
- ['Sleep Preference: ', JSON.stringify(this.sleepPreference)],
270
- ['Schedule Count: ', this.scheduleCount],
271
- ['Displaying Type: ', this.details.displayingType],
272
- ['Error Code: ', this.errorCode]
301
+ ['Timer: ', this.timer]
273
302
  ];
303
+ // Add optional properties only if they exist
304
+ if (this.details.screenSwitch !== undefined) {
305
+ info.push(['Display Switch: ', this.details.screenSwitch]);
306
+ }
307
+ if (this.details.oscillationSwitch !== undefined) {
308
+ info.push(['Oscillation Switch: ', this.details.oscillationSwitch]);
309
+ }
310
+ if (this.details.muteState !== undefined) {
311
+ info.push(['Mute: ', this.muteState]);
312
+ }
313
+ if (this.details.muteSwitch !== undefined) {
314
+ info.push(['Mute Switch: ', this.muteSwitch]);
315
+ }
316
+ if (this.temperature > 0) {
317
+ info.push(['Temperature: ', this.temperature / 10, '°C']);
318
+ }
319
+ if (this.humidity > 0) {
320
+ info.push(['Humidity: ', this.humidity, '%']);
321
+ }
322
+ if (this.thermalComfort > 0) {
323
+ info.push(['Thermal Comfort: ', this.thermalComfort]);
324
+ }
325
+ if (this.sleepPreference && Object.keys(this.sleepPreference).length > 0) {
326
+ info.push(['Sleep Preference: ', JSON.stringify(this.sleepPreference)]);
327
+ }
328
+ if (this.scheduleCount > 0) {
329
+ info.push(['Schedule Count: ', this.scheduleCount]);
330
+ }
331
+ if (this.details.displayingType !== undefined) {
332
+ info.push(['Displaying Type: ', this.details.displayingType]);
333
+ }
334
+ if (this.errorCode !== 0) {
335
+ info.push(['Error Code: ', this.errorCode]);
336
+ }
274
337
  for (const [key, value, unit = ''] of info) {
275
338
  logger_1.logger.info(`${key.toString().padEnd(30, '.')} ${value}${unit}`);
276
339
  }
@@ -165,25 +165,25 @@ exports.fanConfig = {
165
165
  features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_rotate', 'fan_speed', 'auto_mode'],
166
166
  levels: [1, 2, 3]
167
167
  },
168
- // LTF Series
168
+ // LTF Series (Smart Tower Fan)
169
169
  'LTF-F422S-KEU': {
170
170
  module: 'VeSyncTowerFan',
171
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
171
+ features: ['fan_speed'],
172
172
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
173
173
  },
174
174
  'LTF-F422S-WUSR': {
175
175
  module: 'VeSyncTowerFan',
176
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
176
+ features: ['fan_speed'],
177
177
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
178
178
  },
179
179
  'LTF-F422_WJP': {
180
180
  module: 'VeSyncTowerFan',
181
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
181
+ features: ['fan_speed'],
182
182
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
183
183
  },
184
184
  'LTF-F422S-WUS': {
185
185
  module: 'VeSyncTowerFan',
186
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
186
+ features: ['fan_speed'],
187
187
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
188
188
  },
189
189
  // Classic Series
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.0.76",
3
+ "version": "1.0.78",
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
  */
@@ -165,6 +196,231 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
165
196
  }
166
197
  }
167
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
+
168
424
  /**
169
425
  * Set auto preference
170
426
  */
@@ -187,9 +443,23 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
187
443
  head
188
444
  );
189
445
 
190
- 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
+
191
451
  if (success) {
192
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
+ }
193
463
  }
194
464
  return success;
195
465
  }
@@ -210,9 +480,22 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
210
480
  head
211
481
  );
212
482
 
213
- 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
+
214
488
  if (success) {
215
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
+ }
216
499
  }
217
500
  return success;
218
501
  }
@@ -296,7 +579,11 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
296
579
  head
297
580
  );
298
581
 
299
- 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
+
300
587
  if (success) {
301
588
  this.details.oscillationState = toggle;
302
589
  this.details.oscillationSwitch = toggle ? 1 : 0;
@@ -6,7 +6,7 @@ import { logger } from '../logger';
6
6
  * VeSync Tower Fan Implementation
7
7
  */
8
8
  export class VeSyncTowerFan extends VeSyncAirBaseV2 {
9
- protected readonly towerModes = ['normal', 'advancedSleep', 'off'] as const;
9
+ protected readonly towerModes = ['normal', 'auto', 'advancedSleep', 'turbo', 'off'] as const;
10
10
 
11
11
  constructor(details: Record<string, any>, manager: VeSync) {
12
12
  super(details, manager);
@@ -40,26 +40,20 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
40
40
  return false;
41
41
  }
42
42
 
43
- const deviceData = innerResponse.result;
44
- this.deviceStatus = deviceData.powerSwitch === 1 ? 'on' : 'off';
45
- this.details = {
46
- powerSwitch: deviceData.powerSwitch,
47
- workMode: deviceData.workMode,
48
- manualSpeedLevel: deviceData.manualSpeedLevel,
49
- fanSpeedLevel: deviceData.fanSpeedLevel,
50
- screenState: deviceData.screenState,
51
- screenSwitch: deviceData.screenSwitch,
52
- oscillationState: deviceData.oscillationState,
53
- oscillationSwitch: deviceData.oscillationSwitch,
54
- timerRemain: deviceData.timerRemain,
55
- temperature: deviceData.temperature || 0,
56
- humidity: deviceData.humidity || 0,
57
- thermalComfort: deviceData.thermalComfort,
58
- sleepPreference: deviceData.sleepPreference || {},
59
- scheduleCount: deviceData.scheduleCount,
60
- displayingType: deviceData.displayingType,
61
- errorCode: deviceData.errorCode
62
- };
43
+ const innerResult = innerResponse.result;
44
+
45
+ // Use the same methods as Python version
46
+ if (innerResult) {
47
+ this.buildPurifierDict(innerResult);
48
+ } else {
49
+ this.connectionStatus = 'offline';
50
+ logger.debug('error in inner result dict from tower fan');
51
+ return false;
52
+ }
53
+
54
+ if (innerResult.configuration) {
55
+ this.buildConfigDict(innerResult.configuration);
56
+ }
63
57
 
64
58
  logger.debug(`Successfully got details for device: ${this.deviceName}`);
65
59
  return true;
@@ -115,7 +109,7 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
115
109
  /**
116
110
  * Set tower fan mode
117
111
  */
118
- async mode_toggle(mode: string): Promise<boolean> {
112
+ async modeToggle(mode: string): Promise<boolean> {
119
113
  if (!this.towerModes.includes(mode as any)) {
120
114
  logger.debug(`Invalid mode: ${mode}. Must be one of: ${this.towerModes.join(', ')}`);
121
115
  return false;
@@ -151,11 +145,11 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
151
145
  * Change fan speed
152
146
  */
153
147
  async changeFanSpeed(speed: number): Promise<boolean> {
154
- // const speeds = this.config.levels ?? [];
155
- // if (!speeds.includes(speed)) {
156
- // logger.debug(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')}`);
157
- // return false;
158
- // }
148
+ const speeds = this.config.levels ?? [];
149
+ if (!speeds.includes(speed)) {
150
+ logger.debug(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')}`);
151
+ return false;
152
+ }
159
153
 
160
154
  const [head, body] = this.buildApiDict('setLevel');
161
155
  body.payload.data = {
@@ -185,7 +179,7 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
185
179
  */
186
180
  async normal_mode(): Promise<boolean> {
187
181
  logger.debug(`Setting normal mode for device: ${this.deviceName}`);
188
- return this.mode_toggle('normal');
182
+ return this.modeToggle('normal');
189
183
  }
190
184
 
191
185
  /**
@@ -196,12 +190,28 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
196
190
  return this.normal_mode();
197
191
  }
198
192
 
193
+ /**
194
+ * Set auto mode
195
+ */
196
+ async auto_mode(): Promise<boolean> {
197
+ logger.debug(`Setting auto mode for device: ${this.deviceName}`);
198
+ return this.modeToggle('auto');
199
+ }
200
+
201
+ /**
202
+ * Set turbo mode
203
+ */
204
+ async turbo_mode(): Promise<boolean> {
205
+ logger.debug(`Setting turbo mode for device: ${this.deviceName}`);
206
+ return this.modeToggle('turbo');
207
+ }
208
+
199
209
  /**
200
210
  * Set advanced sleep mode
201
211
  */
202
212
  async advanced_sleep_mode(): Promise<boolean> {
203
213
  logger.debug(`Setting advanced sleep mode for device: ${this.deviceName}`);
204
- return this.mode_toggle('advancedSleep');
214
+ return this.modeToggle('advancedSleep');
205
215
  }
206
216
 
207
217
  /**
@@ -216,7 +226,14 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
216
226
  * Override setMode to handle tower fan specific modes
217
227
  */
218
228
  async setMode(mode: string): Promise<boolean> {
219
- return this.mode_toggle(mode);
229
+ return this.modeToggle(mode);
230
+ }
231
+
232
+ /**
233
+ * Alias for modeToggle to match Python naming convention
234
+ */
235
+ async mode_toggle(mode: string): Promise<boolean> {
236
+ return this.modeToggle(mode);
220
237
  }
221
238
 
222
239
  /**
@@ -247,6 +264,13 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
247
264
  return Boolean(this.details.muteState);
248
265
  }
249
266
 
267
+ /**
268
+ * Get mute switch status
269
+ */
270
+ get muteSwitch(): boolean {
271
+ return Boolean(this.details.muteSwitch);
272
+ }
273
+
250
274
  /**
251
275
  * Get temperature (in tenths of a degree)
252
276
  */
@@ -289,6 +313,33 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
289
313
  return this.details.errorCode || 0;
290
314
  }
291
315
 
316
+ /**
317
+ * Set oscillation state
318
+ */
319
+ async setOscillation(state: boolean): Promise<boolean> {
320
+ const [head, body] = this.buildApiDict('setSwitch');
321
+ body.payload.data = {
322
+ oscillationSwitch: state ? 1 : 0,
323
+ switchIdx: 0
324
+ };
325
+
326
+ const [response, status] = await this.callApi(
327
+ '/cloud/v2/deviceManaged/bypassV2',
328
+ 'post',
329
+ body,
330
+ head
331
+ );
332
+
333
+ const success = this.checkResponse([response, status], 'setOscillation');
334
+ if (success) {
335
+ this.details.oscillationSwitch = state ? 1 : 0;
336
+ this.details.oscillationState = state ? 1 : 0;
337
+ } else {
338
+ logger.debug('Error setting oscillation state');
339
+ }
340
+ return success;
341
+ }
342
+
292
343
  /**
293
344
  * Turn oscillation on
294
345
  */
@@ -308,26 +359,50 @@ export class VeSyncTowerFan extends VeSyncAirBaseV2 {
308
359
  */
309
360
  override display(): void {
310
361
  super.display();
311
- const info = [
362
+ const info: Array<[string, any, string?]> = [
312
363
  ['Mode: ', this.details.workMode || ''],
313
364
  ['Speed: ', this.speed],
314
365
  ['Fan Speed Level: ', this.details.fanSpeedLevel || 0],
315
366
  ['Display: ', this.displayState],
316
- ['Display Switch: ', this.details.screenSwitch],
317
367
  ['Oscillation: ', this.oscillationState],
318
- ['Oscillation Switch: ', this.details.oscillationSwitch],
319
- ['Mute: ', this.muteState],
320
- ['Mute Switch: ', this.details.muteSwitch],
321
- ['Timer: ', this.timer],
322
- ['Temperature: ', this.temperature / 10, '°C'],
323
- ['Humidity: ', this.humidity, '%'],
324
- ['Thermal Comfort: ', this.thermalComfort],
325
- ['Sleep Preference: ', JSON.stringify(this.sleepPreference)],
326
- ['Schedule Count: ', this.scheduleCount],
327
- ['Displaying Type: ', this.details.displayingType],
328
- ['Error Code: ', this.errorCode]
368
+ ['Timer: ', this.timer]
329
369
  ];
330
370
 
371
+ // Add optional properties only if they exist
372
+ if (this.details.screenSwitch !== undefined) {
373
+ info.push(['Display Switch: ', this.details.screenSwitch]);
374
+ }
375
+ if (this.details.oscillationSwitch !== undefined) {
376
+ info.push(['Oscillation Switch: ', this.details.oscillationSwitch]);
377
+ }
378
+ if (this.details.muteState !== undefined) {
379
+ info.push(['Mute: ', this.muteState]);
380
+ }
381
+ if (this.details.muteSwitch !== undefined) {
382
+ info.push(['Mute Switch: ', this.muteSwitch]);
383
+ }
384
+ if (this.temperature > 0) {
385
+ info.push(['Temperature: ', this.temperature / 10, '°C']);
386
+ }
387
+ if (this.humidity > 0) {
388
+ info.push(['Humidity: ', this.humidity, '%']);
389
+ }
390
+ if (this.thermalComfort > 0) {
391
+ info.push(['Thermal Comfort: ', this.thermalComfort]);
392
+ }
393
+ if (this.sleepPreference && Object.keys(this.sleepPreference).length > 0) {
394
+ info.push(['Sleep Preference: ', JSON.stringify(this.sleepPreference)]);
395
+ }
396
+ if (this.scheduleCount > 0) {
397
+ info.push(['Schedule Count: ', this.scheduleCount]);
398
+ }
399
+ if (this.details.displayingType !== undefined) {
400
+ info.push(['Displaying Type: ', this.details.displayingType]);
401
+ }
402
+ if (this.errorCode !== 0) {
403
+ info.push(['Error Code: ', this.errorCode]);
404
+ }
405
+
331
406
  for (const [key, value, unit = ''] of info) {
332
407
  logger.info(`${key.toString().padEnd(30, '.')} ${value}${unit}`);
333
408
  }
@@ -175,25 +175,25 @@ export const fanConfig: FanConfig = {
175
175
  levels: [1, 2, 3]
176
176
  },
177
177
 
178
- // LTF Series
178
+ // LTF Series (Smart Tower Fan)
179
179
  'LTF-F422S-KEU': {
180
180
  module: 'VeSyncTowerFan',
181
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
181
+ features: ['fan_speed'],
182
182
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
183
183
  },
184
184
  'LTF-F422S-WUSR': {
185
185
  module: 'VeSyncTowerFan',
186
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
186
+ features: ['fan_speed'],
187
187
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
188
188
  },
189
189
  'LTF-F422_WJP': {
190
190
  module: 'VeSyncTowerFan',
191
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
191
+ features: ['fan_speed'],
192
192
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
193
193
  },
194
194
  'LTF-F422S-WUS': {
195
195
  module: 'VeSyncTowerFan',
196
- features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
196
+ features: ['fan_speed'],
197
197
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
198
198
  },
199
199