tsvesync 1.3.13 → 1.4.1

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.
@@ -57,6 +57,14 @@ export declare class VeSyncAirBaseV2 extends VeSyncAirBypass {
57
57
  * Override setMode to send workMode payloads for bypassV2 purifiers.
58
58
  */
59
59
  setMode(mode: string): Promise<boolean>;
60
+ /**
61
+ * Convenience helper to enter turbo mode when supported.
62
+ */
63
+ turboMode(): Promise<boolean>;
64
+ /**
65
+ * Convenience helper to enter pet mode when supported.
66
+ */
67
+ petMode(): Promise<boolean>;
60
68
  /**
61
69
  * Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers.
62
70
  */
@@ -13,11 +13,11 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
13
13
  this._lightDetection = false;
14
14
  this._lightDetectionState = false;
15
15
  this.setSpeedLevel = null;
16
- this.autoPreferences = ['default', 'efficient', 'quiet'];
17
16
  this.enabled = false;
18
17
  this._mode = '';
19
18
  this._speed = 0;
20
19
  this._timer = null;
20
+ this.autoPreferences = [...this.getConfigAutoPreferences(['default', 'efficient', 'quiet'])];
21
21
  logger_1.logger.debug(`Initialized VeSyncAirBaseV2 device: ${this.deviceName}`);
22
22
  }
23
23
  buildConfigDict(configDict) {
@@ -265,28 +265,63 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
265
265
  * Override setMode to send workMode payloads for bypassV2 purifiers.
266
266
  */
267
267
  async setMode(mode) {
268
- if (!this.modes.includes(mode)) {
268
+ var _a, _b;
269
+ const targetMode = mode.toLowerCase();
270
+ const supportedModes = this.modes.map(m => m.toLowerCase());
271
+ if (!supportedModes.includes(targetMode)) {
269
272
  const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
270
273
  logger_1.logger.error(`${error} for device: ${this.deviceName}`);
271
274
  throw new Error(error);
272
275
  }
273
276
  // Special handling for Core200S stays the same for all models
274
- if (this.deviceType.includes('Core200S') && mode === 'auto') {
277
+ if (this.deviceType.includes('Core200S') && targetMode === 'auto') {
275
278
  logger_1.logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
276
279
  return this.setMode('manual');
277
280
  }
281
+ // Manual mode should leverage changeFanSpeed to mirror pyvesync behaviour
282
+ if (targetMode === 'manual') {
283
+ const defaultLevel = this.config.levels && this.config.levels.length > 0
284
+ ? this.config.levels[0]
285
+ : 1;
286
+ const desiredLevel = (_b = (_a = this.details.manualSpeedLevel) !== null && _a !== void 0 ? _a : this.speed) !== null && _b !== void 0 ? _b : defaultLevel;
287
+ const success = await this.changeFanSpeed(desiredLevel);
288
+ if (success) {
289
+ this.details.mode = 'manual';
290
+ this.mode = 'manual';
291
+ }
292
+ return success;
293
+ }
278
294
  // BypassV2 purifiers need the workMode payload; other models use the base implementation
279
295
  if (this.requiresPowerSwitchPayload()) {
280
296
  logger_1.logger.debug(`Using bypassV2 mode payload for ${this.deviceName}`);
281
297
  const [head, body] = this.buildApiDict('setPurifierMode');
282
298
  body.payload.data = {
283
- workMode: mode
299
+ workMode: targetMode
284
300
  };
285
301
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
286
302
  const success = this.checkV2Response(response, status, 'setMode');
287
303
  if (success) {
288
- this.details.mode = mode;
289
- this._mode = mode;
304
+ this.details.mode = targetMode;
305
+ this.mode = targetMode;
306
+ // Update cached speed snapshot for special modes
307
+ if (targetMode === 'sleep') {
308
+ this.speed = 0;
309
+ this.details.speed = 0;
310
+ }
311
+ else if (targetMode === 'turbo') {
312
+ const maxSpeed = this.config.levels && this.config.levels.length > 0
313
+ ? Math.max(...this.config.levels)
314
+ : this.speed;
315
+ if (typeof maxSpeed === 'number' && maxSpeed > 0) {
316
+ this.speed = maxSpeed;
317
+ this.details.speed = maxSpeed;
318
+ }
319
+ }
320
+ else if (targetMode === 'auto') {
321
+ this.speed = 0;
322
+ this.details.speed = 0;
323
+ this.details.isAutoSentinel = true;
324
+ }
290
325
  // Force a details update to sync state
291
326
  setTimeout(() => {
292
327
  this.getDetails().catch(e => {
@@ -296,7 +331,7 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
296
331
  return true;
297
332
  }
298
333
  else {
299
- logger_1.logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
334
+ logger_1.logger.error(`Failed to set mode to ${targetMode} for device: ${this.deviceName}`);
300
335
  return false;
301
336
  }
302
337
  }
@@ -305,6 +340,26 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
305
340
  return super.setMode(mode);
306
341
  }
307
342
  }
343
+ /**
344
+ * Convenience helper to enter turbo mode when supported.
345
+ */
346
+ async turboMode() {
347
+ if (!this.modes.map(m => m.toLowerCase()).includes('turbo')) {
348
+ logger_1.logger.debug(`Turbo mode not supported for device: ${this.deviceName}`);
349
+ return false;
350
+ }
351
+ return this.setMode('turbo');
352
+ }
353
+ /**
354
+ * Convenience helper to enter pet mode when supported.
355
+ */
356
+ async petMode() {
357
+ if (!this.modes.map(m => m.toLowerCase()).includes('pet')) {
358
+ logger_1.logger.debug(`Pet mode not supported for device: ${this.deviceName}`);
359
+ return false;
360
+ }
361
+ return this.setMode('pet');
362
+ }
308
363
  /**
309
364
  * Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers.
310
365
  */
@@ -331,6 +386,10 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
331
386
  if (success) {
332
387
  this.speed = speed;
333
388
  this.details.manualSpeedLevel = speed;
389
+ this.details.speed = speed;
390
+ this.details.mode = 'manual';
391
+ this.mode = 'manual';
392
+ this.details.isAutoSentinel = false;
334
393
  // Force a details update to sync state
335
394
  setTimeout(() => {
336
395
  this.getDetails().catch(e => {
@@ -357,6 +416,10 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
357
416
  if (success) {
358
417
  this.speed = speed;
359
418
  this.details.manualSpeedLevel = speed;
419
+ this.details.speed = speed;
420
+ this.details.mode = 'manual';
421
+ this.mode = 'manual';
422
+ this.details.isAutoSentinel = false;
360
423
  return true;
361
424
  }
362
425
  else {
@@ -370,7 +433,7 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
370
433
  */
371
434
  async setAutoPreference(preference = 'default', roomSize = 600) {
372
435
  if (!this.autoPreferences.includes(preference)) {
373
- logger_1.logger.debug(`Invalid preference: ${preference} - valid preferences are default, efficient, quiet`);
436
+ logger_1.logger.debug(`Invalid preference: ${preference} - valid preferences are ${this.autoPreferences.join(', ')}`);
374
437
  return false;
375
438
  }
376
439
  const [head, body] = this.buildApiDict('setAutoPreference');
@@ -4,7 +4,7 @@ import { VeSync } from '../vesync';
4
4
  * VeSync Air Purifier with Bypass
5
5
  */
6
6
  export declare class VeSyncAirBypass extends VeSyncFan {
7
- protected readonly modes: readonly ["auto", "manual", "sleep"];
7
+ protected get modes(): ReadonlyArray<string>;
8
8
  protected readonly displayModes: readonly ["on", "off"];
9
9
  protected readonly childLockModes: readonly ["on", "off"];
10
10
  constructor(details: Record<string, any>, manager: VeSync);
@@ -8,9 +8,11 @@ const logger_1 = require("../logger");
8
8
  * VeSync Air Purifier with Bypass
9
9
  */
10
10
  class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
11
+ get modes() {
12
+ return this.getConfigModes(['auto', 'manual', 'sleep']);
13
+ }
11
14
  constructor(details, manager) {
12
15
  super(details, manager);
13
- this.modes = ['auto', 'manual', 'sleep'];
14
16
  this.displayModes = ['on', 'off'];
15
17
  this.childLockModes = ['on', 'off'];
16
18
  logger_1.logger.debug(`Initialized VeSyncAirBypass device: ${this.deviceName}`);
@@ -69,16 +71,30 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
69
71
  logger_1.logger.debug(`${this.deviceName}: API response structure:`, JSON.stringify(result, null, 2));
70
72
  }
71
73
  }
74
+ // Parse air quality with fallback support for multiple API response formats
75
+ // Some devices (like Core 400S) may use PascalCase (AQLevel, PM25)
76
+ // while others use snake_case (air_quality, air_quality_value)
77
+ const airQualityLevel = result.air_quality || result.AQLevel || 0;
78
+ const airQualityValue = result.air_quality_value || result.PM25 || 0;
79
+ // Debug logging to diagnose air quality field availability
80
+ logger_1.logger.debug(`${this.deviceName}: Air quality API response fields:`, {
81
+ air_quality: result.air_quality,
82
+ AQLevel: result.AQLevel,
83
+ air_quality_value: result.air_quality_value,
84
+ PM25: result.PM25,
85
+ parsed_level: airQualityLevel,
86
+ parsed_value: airQualityValue
87
+ });
72
88
  this.details = {
73
89
  mode: result.mode || '',
74
90
  speed: result.level || 0,
75
91
  filter_life: filterLife,
76
92
  screen_status: result.display ? 'on' : 'off',
77
93
  child_lock: result.child_lock || false,
78
- air_quality: result.air_quality || 0,
79
- air_quality_value: result.air_quality_value || 0
94
+ air_quality: airQualityLevel,
95
+ air_quality_value: airQualityValue
80
96
  };
81
- const normalizedAirQuality = helpers_1.Helpers.normalizeAirQuality(result.air_quality);
97
+ const normalizedAirQuality = helpers_1.Helpers.normalizeAirQuality(airQualityLevel);
82
98
  if (normalizedAirQuality.level >= 1) {
83
99
  this.details.air_quality_level = normalizedAirQuality.level;
84
100
  }
@@ -90,6 +106,14 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
90
106
  this.details.pm10 = result.PM10;
91
107
  if ('AQPercent' in result)
92
108
  this.details.aq_percent = result.AQPercent;
109
+ // Debug log additional air quality fields for diagnostics
110
+ if (result.PM1 !== undefined || result.PM10 !== undefined || result.AQPercent !== undefined) {
111
+ logger_1.logger.debug(`${this.deviceName}: Additional air quality data:`, {
112
+ PM1: result.PM1,
113
+ PM10: result.PM10,
114
+ AQPercent: result.AQPercent
115
+ });
116
+ }
93
117
  // Don't overwrite config as it contains features array
94
118
  if (result.configuration) {
95
119
  this.details.configuration = result.configuration;
@@ -183,14 +207,17 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
183
207
  * Set device mode
184
208
  */
185
209
  async setMode(mode) {
186
- if (!this.modes.includes(mode)) {
210
+ const requested = mode.toLowerCase();
211
+ const supported = this.modes.map(m => m.toLowerCase());
212
+ const allowsCore200AutoFallback = this.deviceType.includes('Core200S') && requested === 'auto';
213
+ if (!supported.includes(requested) && !allowsCore200AutoFallback) {
187
214
  const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
188
215
  logger_1.logger.error(`${error} for device: ${this.deviceName}`);
189
216
  throw new Error(error);
190
217
  }
191
- logger_1.logger.debug(`Setting mode to ${mode} for device: ${this.deviceName}`);
218
+ logger_1.logger.debug(`Setting mode to ${requested} for device: ${this.deviceName}`);
192
219
  // Special handling for Core200S auto mode
193
- if (this.deviceType.includes('Core200S') && mode === 'auto') {
220
+ if (allowsCore200AutoFallback) {
194
221
  // Core200S doesn't support auto mode, so we'll set it to manual mode
195
222
  logger_1.logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
196
223
  return this.setMode('manual');
@@ -202,7 +229,7 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
202
229
  configModule: this.configModule,
203
230
  payload: {
204
231
  data: {
205
- mode
232
+ mode: requested
206
233
  },
207
234
  method: 'setPurifierMode',
208
235
  source: 'APP'
@@ -210,11 +237,11 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
210
237
  }, helpers_1.Helpers.reqHeaderBypass());
211
238
  const success = this.checkResponse([response, status], 'setMode');
212
239
  if (success) {
213
- this.details.mode = mode;
240
+ this.details.mode = requested;
214
241
  return true;
215
242
  }
216
243
  else {
217
- logger_1.logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
244
+ logger_1.logger.error(`Failed to set mode to ${requested} for device: ${this.deviceName}`);
218
245
  return false;
219
246
  }
220
247
  }
@@ -3,12 +3,15 @@
3
3
  */
4
4
  import { VeSyncBaseDevice } from './vesyncBaseDevice';
5
5
  import { VeSync } from './vesync';
6
+ interface FanConfigEntry {
7
+ module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2' | 'VeSyncSuperior6000S' | 'VeSyncHumid1000S' | 'VeSyncHumid200S' | 'VeSyncAir131';
8
+ features: string[];
9
+ levels?: number[];
10
+ modes?: ReadonlyArray<string>;
11
+ autoPreferences?: ReadonlyArray<string>;
12
+ }
6
13
  interface FanConfig {
7
- [key: string]: {
8
- module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2' | 'VeSyncSuperior6000S' | 'VeSyncHumid1000S' | 'VeSyncHumid200S' | 'VeSyncAir131';
9
- features: string[];
10
- levels?: number[];
11
- };
14
+ [key: string]: FanConfigEntry;
12
15
  }
13
16
  export declare const fanConfig: FanConfig;
14
17
  /**
@@ -42,16 +45,33 @@ export declare abstract class VeSyncFan extends VeSyncBaseDevice {
42
45
  duration: number;
43
46
  action: string;
44
47
  } | null;
45
- protected config: {
46
- module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2' | 'VeSyncSuperior6000S' | 'VeSyncHumid1000S' | 'VeSyncHumid200S' | 'VeSyncAir131';
47
- features: string[];
48
- levels?: number[];
49
- };
48
+ protected config: FanConfigEntry;
50
49
  constructor(details: Record<string, any>, manager: VeSync);
50
+ /**
51
+ * Find config entry by substring matching for variant device types
52
+ * Maps LAP variants to their base model configs (e.g., LAP-C401S-WUSR → Core400S)
53
+ */
54
+ private findConfigBySubstring;
51
55
  /**
52
56
  * Check if feature is supported
53
57
  */
54
58
  hasFeature(feature: string): boolean;
59
+ /**
60
+ * Get configured modes for the device, falling back to provided defaults.
61
+ */
62
+ protected getConfigModes(defaultModes?: ReadonlyArray<string>): ReadonlyArray<string>;
63
+ /**
64
+ * Get configured auto preferences for the device, falling back to provided defaults.
65
+ */
66
+ protected getConfigAutoPreferences(defaultPreferences?: ReadonlyArray<string>): ReadonlyArray<string>;
67
+ /**
68
+ * Public accessor for supported modes for consumers that need to inspect capability metadata.
69
+ */
70
+ getSupportedModes(defaultModes?: ReadonlyArray<string>): ReadonlyArray<string>;
71
+ /**
72
+ * Public accessor for supported auto preference profiles.
73
+ */
74
+ getSupportedAutoPreferences(defaultPreferences?: ReadonlyArray<string>): ReadonlyArray<string>;
55
75
  /**
56
76
  * Check if feature is supported in current mode
57
77
  */