tsvesync 1.3.12 → 1.4.0

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VeSyncAir131 = void 0;
4
4
  const vesyncFan_1 = require("../vesyncFan");
5
+ const helpers_1 = require("../helpers");
5
6
  const logger_1 = require("../logger");
6
7
  /**
7
8
  * VeSync Air Purifier 131 Series (LV-PUR131S, LV-RH131S)
@@ -78,6 +79,7 @@ class VeSyncAir131 extends vesyncFan_1.VeSyncFan {
78
79
  logger_1.logger.debug(`${this.deviceName}: API response structure:`, JSON.stringify(data, null, 2));
79
80
  }
80
81
  }
82
+ const normalizedAirQuality = helpers_1.Helpers.normalizeAirQuality(data.airQuality);
81
83
  this.details = {
82
84
  mode: data.mode || 'manual', // Default to manual if not specified
83
85
  speed: data.level || 0,
@@ -86,6 +88,8 @@ class VeSyncAir131 extends vesyncFan_1.VeSyncFan {
86
88
  child_lock: data.childLock || false,
87
89
  air_quality: data.airQuality || 'unknown',
88
90
  air_quality_value: data.air_quality_value || 0,
91
+ air_quality_level: normalizedAirQuality.level >= 1 ? normalizedAirQuality.level : undefined,
92
+ air_quality_label: normalizedAirQuality.label,
89
93
  active_time: data.activeTime || 0
90
94
  };
91
95
  // Store last known speed when device is on and has a valid speed
@@ -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
  */
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VeSyncAirBaseV2 = void 0;
4
4
  const airBypass_1 = require("./airBypass");
5
5
  const logger_1 = require("../logger");
6
+ const helpers_1 = require("../helpers");
6
7
  /**
7
8
  * VeSync Air Purifier with Bypass V2
8
9
  */
@@ -12,11 +13,11 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
12
13
  this._lightDetection = false;
13
14
  this._lightDetectionState = false;
14
15
  this.setSpeedLevel = null;
15
- this.autoPreferences = ['default', 'efficient', 'quiet'];
16
16
  this.enabled = false;
17
17
  this._mode = '';
18
18
  this._speed = 0;
19
19
  this._timer = null;
20
+ this.autoPreferences = [...this.getConfigAutoPreferences(['default', 'efficient', 'quiet'])];
20
21
  logger_1.logger.debug(`Initialized VeSyncAirBaseV2 device: ${this.deviceName}`);
21
22
  }
22
23
  buildConfigDict(configDict) {
@@ -163,6 +164,11 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
163
164
  if (this.hasFeature('air_quality')) {
164
165
  this.details.air_quality_value = (_l = devDict.PM25) !== null && _l !== void 0 ? _l : 0;
165
166
  this.details.air_quality = (_m = devDict.AQLevel) !== null && _m !== void 0 ? _m : 0;
167
+ const normalizedAirQuality = helpers_1.Helpers.normalizeAirQuality(devDict.AQLevel);
168
+ if (normalizedAirQuality.level >= 1) {
169
+ this.details.air_quality_level = normalizedAirQuality.level;
170
+ }
171
+ this.details.air_quality_label = normalizedAirQuality.label;
166
172
  }
167
173
  if ('PM1' in devDict)
168
174
  this.details.pm1 = devDict.PM1;
@@ -259,28 +265,63 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
259
265
  * Override setMode to send workMode payloads for bypassV2 purifiers.
260
266
  */
261
267
  async setMode(mode) {
262
- 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)) {
263
272
  const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
264
273
  logger_1.logger.error(`${error} for device: ${this.deviceName}`);
265
274
  throw new Error(error);
266
275
  }
267
276
  // Special handling for Core200S stays the same for all models
268
- if (this.deviceType.includes('Core200S') && mode === 'auto') {
277
+ if (this.deviceType.includes('Core200S') && targetMode === 'auto') {
269
278
  logger_1.logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
270
279
  return this.setMode('manual');
271
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
+ }
272
294
  // BypassV2 purifiers need the workMode payload; other models use the base implementation
273
295
  if (this.requiresPowerSwitchPayload()) {
274
296
  logger_1.logger.debug(`Using bypassV2 mode payload for ${this.deviceName}`);
275
297
  const [head, body] = this.buildApiDict('setPurifierMode');
276
298
  body.payload.data = {
277
- workMode: mode
299
+ workMode: targetMode
278
300
  };
279
301
  const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
280
302
  const success = this.checkV2Response(response, status, 'setMode');
281
303
  if (success) {
282
- this.details.mode = mode;
283
- 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
+ }
284
325
  // Force a details update to sync state
285
326
  setTimeout(() => {
286
327
  this.getDetails().catch(e => {
@@ -290,7 +331,7 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
290
331
  return true;
291
332
  }
292
333
  else {
293
- 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}`);
294
335
  return false;
295
336
  }
296
337
  }
@@ -299,6 +340,26 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
299
340
  return super.setMode(mode);
300
341
  }
301
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
+ }
302
363
  /**
303
364
  * Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers.
304
365
  */
@@ -325,6 +386,10 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
325
386
  if (success) {
326
387
  this.speed = speed;
327
388
  this.details.manualSpeedLevel = speed;
389
+ this.details.speed = speed;
390
+ this.details.mode = 'manual';
391
+ this.mode = 'manual';
392
+ this.details.isAutoSentinel = false;
328
393
  // Force a details update to sync state
329
394
  setTimeout(() => {
330
395
  this.getDetails().catch(e => {
@@ -351,6 +416,10 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
351
416
  if (success) {
352
417
  this.speed = speed;
353
418
  this.details.manualSpeedLevel = speed;
419
+ this.details.speed = speed;
420
+ this.details.mode = 'manual';
421
+ this.mode = 'manual';
422
+ this.details.isAutoSentinel = false;
354
423
  return true;
355
424
  }
356
425
  else {
@@ -364,7 +433,7 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
364
433
  */
365
434
  async setAutoPreference(preference = 'default', roomSize = 600) {
366
435
  if (!this.autoPreferences.includes(preference)) {
367
- 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(', ')}`);
368
437
  return false;
369
438
  }
370
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}`);
@@ -78,6 +80,11 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
78
80
  air_quality: result.air_quality || 0,
79
81
  air_quality_value: result.air_quality_value || 0
80
82
  };
83
+ const normalizedAirQuality = helpers_1.Helpers.normalizeAirQuality(result.air_quality);
84
+ if (normalizedAirQuality.level >= 1) {
85
+ this.details.air_quality_level = normalizedAirQuality.level;
86
+ }
87
+ this.details.air_quality_label = normalizedAirQuality.label;
81
88
  // Capture additional air quality properties if available
82
89
  if ('PM1' in result)
83
90
  this.details.pm1 = result.PM1;
@@ -178,14 +185,17 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
178
185
  * Set device mode
179
186
  */
180
187
  async setMode(mode) {
181
- if (!this.modes.includes(mode)) {
188
+ const requested = mode.toLowerCase();
189
+ const supported = this.modes.map(m => m.toLowerCase());
190
+ const allowsCore200AutoFallback = this.deviceType.includes('Core200S') && requested === 'auto';
191
+ if (!supported.includes(requested) && !allowsCore200AutoFallback) {
182
192
  const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
183
193
  logger_1.logger.error(`${error} for device: ${this.deviceName}`);
184
194
  throw new Error(error);
185
195
  }
186
- logger_1.logger.debug(`Setting mode to ${mode} for device: ${this.deviceName}`);
196
+ logger_1.logger.debug(`Setting mode to ${requested} for device: ${this.deviceName}`);
187
197
  // Special handling for Core200S auto mode
188
- if (this.deviceType.includes('Core200S') && mode === 'auto') {
198
+ if (allowsCore200AutoFallback) {
189
199
  // Core200S doesn't support auto mode, so we'll set it to manual mode
190
200
  logger_1.logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
191
201
  return this.setMode('manual');
@@ -197,7 +207,7 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
197
207
  configModule: this.configModule,
198
208
  payload: {
199
209
  data: {
200
- mode
210
+ mode: requested
201
211
  },
202
212
  method: 'setPurifierMode',
203
213
  source: 'APP'
@@ -205,11 +215,11 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
205
215
  }, helpers_1.Helpers.reqHeaderBypass());
206
216
  const success = this.checkResponse([response, status], 'setMode');
207
217
  if (success) {
208
- this.details.mode = mode;
218
+ this.details.mode = requested;
209
219
  return true;
210
220
  }
211
221
  else {
212
- logger_1.logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
222
+ logger_1.logger.error(`Failed to set mode to ${requested} for device: ${this.deviceName}`);
213
223
  return false;
214
224
  }
215
225
  }
@@ -106,6 +106,10 @@ export interface LoginResponse {
106
106
  export declare class Helpers {
107
107
  static shouldRedact: boolean;
108
108
  static generateTraceId(): string;
109
+ static normalizeAirQuality(value: unknown): {
110
+ level: number;
111
+ label: string;
112
+ };
109
113
  /**
110
114
  * Calculate MD5 hash
111
115
  */
@@ -182,6 +182,50 @@ class Helpers {
182
182
  static generateTraceId() {
183
183
  return generateTraceId();
184
184
  }
185
+ static normalizeAirQuality(value) {
186
+ const stringMap = {
187
+ 'excellent': 1,
188
+ 'very good': 1,
189
+ 'good': 2,
190
+ 'moderate': 3,
191
+ 'fair': 3,
192
+ 'inferior': 4,
193
+ 'poor': 4,
194
+ 'bad': 4,
195
+ };
196
+ if (typeof value === 'number') {
197
+ const level = Number.isFinite(value) ? Math.trunc(value) : -1;
198
+ if (level >= 1 && level <= 4) {
199
+ return {
200
+ level,
201
+ label: level === 1
202
+ ? 'excellent'
203
+ : level === 2
204
+ ? 'good'
205
+ : level === 3
206
+ ? 'moderate'
207
+ : 'poor',
208
+ };
209
+ }
210
+ }
211
+ else if (typeof value === 'string') {
212
+ const normalized = value.trim().toLowerCase();
213
+ const level = stringMap[normalized];
214
+ if (level) {
215
+ return {
216
+ level,
217
+ label: level === 1
218
+ ? 'excellent'
219
+ : level === 2
220
+ ? 'good'
221
+ : level === 3
222
+ ? 'moderate'
223
+ : 'poor',
224
+ };
225
+ }
226
+ }
227
+ return { level: -1, label: 'unknown' };
228
+ }
185
229
  /**
186
230
  * Calculate MD5 hash
187
231
  */
@@ -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
  /**
@@ -25,6 +28,8 @@ export declare abstract class VeSyncFan extends VeSyncBaseDevice {
25
28
  child_lock?: boolean;
26
29
  air_quality?: string | number;
27
30
  air_quality_value?: number;
31
+ air_quality_level?: number;
32
+ air_quality_label?: string;
28
33
  pm1?: number;
29
34
  pm10?: number;
30
35
  aq_percent?: number;
@@ -40,16 +45,28 @@ export declare abstract class VeSyncFan extends VeSyncBaseDevice {
40
45
  duration: number;
41
46
  action: string;
42
47
  } | null;
43
- protected config: {
44
- module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2' | 'VeSyncSuperior6000S' | 'VeSyncHumid1000S' | 'VeSyncHumid200S' | 'VeSyncAir131';
45
- features: string[];
46
- levels?: number[];
47
- };
48
+ protected config: FanConfigEntry;
48
49
  constructor(details: Record<string, any>, manager: VeSync);
49
50
  /**
50
51
  * Check if feature is supported
51
52
  */
52
53
  hasFeature(feature: string): boolean;
54
+ /**
55
+ * Get configured modes for the device, falling back to provided defaults.
56
+ */
57
+ protected getConfigModes(defaultModes?: ReadonlyArray<string>): ReadonlyArray<string>;
58
+ /**
59
+ * Get configured auto preferences for the device, falling back to provided defaults.
60
+ */
61
+ protected getConfigAutoPreferences(defaultPreferences?: ReadonlyArray<string>): ReadonlyArray<string>;
62
+ /**
63
+ * Public accessor for supported modes for consumers that need to inspect capability metadata.
64
+ */
65
+ getSupportedModes(defaultModes?: ReadonlyArray<string>): ReadonlyArray<string>;
66
+ /**
67
+ * Public accessor for supported auto preference profiles.
68
+ */
69
+ getSupportedAutoPreferences(defaultPreferences?: ReadonlyArray<string>): ReadonlyArray<string>;
53
70
  /**
54
71
  * Check if feature is supported in current mode
55
72
  */
@@ -90,6 +107,13 @@ export declare abstract class VeSyncFan extends VeSyncBaseDevice {
90
107
  * Get air quality value - PM2.5 in μg/m³ (if supported)
91
108
  */
92
109
  get airQualityValue(): number;
110
+ /**
111
+ * Normalized air quality metadata aligned with pyvesync levels.
112
+ */
113
+ getNormalizedAirQuality(): {
114
+ level: number;
115
+ label: string;
116
+ };
93
117
  /**
94
118
  * Get PM1 value (if supported)
95
119
  */