tsvesync 1.0.71 → 1.0.73

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.
@@ -0,0 +1,466 @@
1
+ import { VeSyncFan } from '../vesyncFan';
2
+ import { VeSync } from '../vesync';
3
+ import { Helpers } from '../helpers';
4
+ import { logger } from '../logger';
5
+
6
+ /**
7
+ * VeSync Superior 6000S Humidifier Class
8
+ * Implementation based on PyVeSync's VeSyncSuperior6000S class
9
+ */
10
+ export class VeSyncSuperior6000S extends VeSyncFan {
11
+ protected readonly modes = ['auto', 'manual', 'sleep', 'humidity'] as const;
12
+ protected readonly mistLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9];
13
+ protected readonly humidityRange = { min: 30, max: 80 };
14
+ protected readonly _apiModes = [
15
+ 'getHumidifierStatus',
16
+ 'setSwitch',
17
+ 'setVirtualLevel',
18
+ 'setTargetHumidity',
19
+ 'setHumidityMode',
20
+ 'setDisplay',
21
+ 'setDryingMode',
22
+ ];
23
+
24
+ // Override details with more specific type
25
+ override details: {
26
+ mode: string;
27
+ speed?: number;
28
+ filter_life?: number;
29
+ screen_status?: 'on' | 'off';
30
+ child_lock?: boolean;
31
+ humidity: number;
32
+ target_humidity: number;
33
+ mist_level: number;
34
+ mist_virtual_level: number;
35
+ water_lacks: boolean;
36
+ water_tank_lifted: boolean;
37
+ display: boolean;
38
+ filter_life_percentage: number;
39
+ temperature: number;
40
+ drying_mode: {
41
+ autoDryingSwitch?: number;
42
+ dryingState?: number;
43
+ dryingLevel?: number;
44
+ dryingRemain?: number;
45
+ };
46
+ configuration: Record<string, any>;
47
+ connection_status: string | null;
48
+ };
49
+
50
+ constructor(details: Record<string, any>, manager: VeSync) {
51
+ super(details, manager);
52
+
53
+ // Initialize details object
54
+ this.details = {
55
+ mode: '',
56
+ speed: 0,
57
+ filter_life: 0,
58
+ screen_status: 'off',
59
+ child_lock: false,
60
+ humidity: 0,
61
+ target_humidity: 0,
62
+ mist_level: 0,
63
+ mist_virtual_level: 0,
64
+ water_lacks: false,
65
+ water_tank_lifted: false,
66
+ display: false,
67
+ filter_life_percentage: 0,
68
+ temperature: 0,
69
+ drying_mode: {},
70
+ configuration: {},
71
+ connection_status: null
72
+ };
73
+
74
+ // Update config for Superior 6000S
75
+ this.config = {
76
+ module: 'VeSyncAirBypass', // This doesn't matter as we're implementing directly
77
+ features: ['display', 'humidity', 'mist', 'timer', 'auto_mode', 'drying'],
78
+ levels: this.mistLevels
79
+ };
80
+
81
+ logger.debug(`Initialized VeSyncSuperior6000S device: ${this.deviceName}`);
82
+ }
83
+
84
+ /**
85
+ * Build API dictionary for humidifier
86
+ */
87
+ protected buildApiDict(method: string): [Record<string, any>, Record<string, any>] {
88
+ if (!this._apiModes.includes(method)) {
89
+ logger.debug(`Invalid mode - ${method}`);
90
+ throw new Error(`Invalid mode - ${method}`);
91
+ }
92
+
93
+ const head = Helpers.reqHeaderBypass();
94
+ const body = {
95
+ ...Helpers.reqBody(this.manager, 'bypassV2'),
96
+ cid: this.cid,
97
+ configModule: this.configModule,
98
+ payload: {
99
+ method: method,
100
+ source: 'APP',
101
+ data: {}
102
+ }
103
+ };
104
+
105
+ return [head, body];
106
+ }
107
+
108
+ /**
109
+ * Get device details
110
+ */
111
+ async getDetails(): Promise<Boolean> {
112
+ logger.debug(`Getting details for device: ${this.deviceName}`);
113
+ const [head, body] = this.buildApiDict('getHumidifierStatus');
114
+
115
+ const [response, status] = await this.callApi(
116
+ '/cloud/v2/deviceManaged/bypassV2',
117
+ 'post',
118
+ body,
119
+ head
120
+ );
121
+
122
+ const success = this.checkResponse([response, status], 'getDetails');
123
+ if (success && response?.result?.result) {
124
+ const result = response.result.result;
125
+
126
+ // Update device status based on powerSwitch field
127
+ this.deviceStatus = result.powerSwitch === 1 ? 'on' : 'off';
128
+
129
+ // Build details object with Superior 6000S specific fields
130
+ this.details = {
131
+ mode: result.workMode === 'autoPro' ? 'auto' : result.workMode || '',
132
+ humidity: result.humidity || 0,
133
+ target_humidity: result.targetHumidity || 0,
134
+ mist_level: result.mistLevel || 0,
135
+ mist_virtual_level: result.virtualLevel || 0,
136
+ water_lacks: result.waterLacksState || false,
137
+ water_tank_lifted: result.waterTankLifted || false,
138
+ display: result.screenSwitch === 1,
139
+ filter_life_percentage: result.filterLifePercent || 0,
140
+ temperature: result.temperature || 0,
141
+ drying_mode: result.dryingMode || {},
142
+ configuration: result.configuration || {},
143
+ connection_status: result.connectionStatus || null
144
+ };
145
+
146
+ logger.debug(`Successfully got details for device: ${this.deviceName}`);
147
+ }
148
+ return success;
149
+ }
150
+
151
+ /**
152
+ * Turn device on
153
+ */
154
+ async turnOn(): Promise<boolean> {
155
+ return this.toggleSwitch(true);
156
+ }
157
+
158
+ /**
159
+ * Turn device off
160
+ */
161
+ async turnOff(): Promise<boolean> {
162
+ return this.toggleSwitch(false);
163
+ }
164
+
165
+ /**
166
+ * Toggle device power
167
+ */
168
+ async toggleSwitch(enabled: boolean): Promise<boolean> {
169
+ logger.info(`Setting device power to ${enabled ? 'on' : 'off'}: ${this.deviceName}`);
170
+ const [head, body] = this.buildApiDict('setSwitch');
171
+
172
+ body.payload.data = {
173
+ powerSwitch: enabled ? 1 : 0,
174
+ switchIdx: 0
175
+ };
176
+
177
+ const [response, status] = await this.callApi(
178
+ '/cloud/v2/deviceManaged/bypassV2',
179
+ 'post',
180
+ body,
181
+ head
182
+ );
183
+
184
+ const success = this.checkResponse([response, status], 'toggleSwitch');
185
+ if (success) {
186
+ this.deviceStatus = enabled ? 'on' : 'off';
187
+ return true;
188
+ }
189
+ logger.error(`Failed to set device power to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
190
+ return false;
191
+ }
192
+
193
+ /**
194
+ * Set mist level
195
+ */
196
+ async setMistLevel(level: number): Promise<boolean> {
197
+ if (!this.mistLevels.includes(level)) {
198
+ const error = `Invalid mist level: ${level}. Must be one of: ${this.mistLevels.join(', ')}`;
199
+ logger.error(`${error} for device: ${this.deviceName}`);
200
+ throw new Error(error);
201
+ }
202
+
203
+ logger.info(`Setting mist level to ${level} for device: ${this.deviceName}`);
204
+ const [head, body] = this.buildApiDict('setVirtualLevel');
205
+
206
+ body.payload.data = {
207
+ levelIdx: 0,
208
+ virtualLevel: level,
209
+ levelType: 'mist'
210
+ };
211
+
212
+ const [response, status] = await this.callApi(
213
+ '/cloud/v2/deviceManaged/bypassV2',
214
+ 'post',
215
+ body,
216
+ head
217
+ );
218
+
219
+ const success = this.checkResponse([response, status], 'setMistLevel');
220
+ if (!success) {
221
+ logger.error(`Failed to set mist level to ${level} for device: ${this.deviceName}`);
222
+ }
223
+ return success;
224
+ }
225
+
226
+ /**
227
+ * Change fan speed - Implemented to satisfy interface but redirects to setMistLevel
228
+ */
229
+ async changeFanSpeed(speed: number): Promise<boolean> {
230
+ logger.debug(`Redirecting fan speed change to mist level for device: ${this.deviceName}`);
231
+ return this.setMistLevel(speed);
232
+ }
233
+
234
+ /**
235
+ * Set device mode
236
+ */
237
+ async setMode(mode: string): Promise<boolean> {
238
+ if (!this.modes.includes(mode as any)) {
239
+ const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
240
+ logger.error(`${error} for device: ${this.deviceName}`);
241
+ throw new Error(error);
242
+ }
243
+
244
+ logger.debug(`Setting mode to ${mode} for device: ${this.deviceName}`);
245
+ const [head, body] = this.buildApiDict('setHumidityMode');
246
+
247
+ body.payload.data = {
248
+ workMode: mode === 'auto' ? 'autoPro' : mode
249
+ };
250
+
251
+ const [response, status] = await this.callApi(
252
+ '/cloud/v2/deviceManaged/bypassV2',
253
+ 'post',
254
+ body,
255
+ head
256
+ );
257
+
258
+ const success = this.checkResponse([response, status], 'setMode');
259
+ if (success) {
260
+ this.details.mode = mode;
261
+ } else {
262
+ logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
263
+ }
264
+ return success;
265
+ }
266
+
267
+ /**
268
+ * Set target humidity
269
+ */
270
+ async setHumidity(humidity: number): Promise<boolean> {
271
+ if (humidity < this.humidityRange.min || humidity > this.humidityRange.max) {
272
+ const error = `Invalid humidity: ${humidity}. Must be between ${this.humidityRange.min} and ${this.humidityRange.max}`;
273
+ logger.error(`${error} for device: ${this.deviceName}`);
274
+ throw new Error(error);
275
+ }
276
+
277
+ logger.debug(`Setting target humidity to ${humidity}% for device: ${this.deviceName}`);
278
+ const [head, body] = this.buildApiDict('setTargetHumidity');
279
+
280
+ body.payload.data = {
281
+ targetHumidity: humidity
282
+ };
283
+
284
+ const [response, status] = await this.callApi(
285
+ '/cloud/v2/deviceManaged/bypassV2',
286
+ 'post',
287
+ body,
288
+ head
289
+ );
290
+
291
+ const success = this.checkResponse([response, status], 'setHumidity');
292
+ if (!success) {
293
+ logger.error(`Failed to set target humidity to ${humidity}% for device: ${this.deviceName}`);
294
+ }
295
+ return success;
296
+ }
297
+
298
+ /**
299
+ * Set display status
300
+ */
301
+ async setDisplay(enabled: boolean): Promise<boolean> {
302
+ logger.debug(`Setting display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
303
+ const [head, body] = this.buildApiDict('setDisplay');
304
+
305
+ body.payload.data = {
306
+ screenSwitch: enabled ? 1 : 0
307
+ };
308
+
309
+ const [response, status] = await this.callApi(
310
+ '/cloud/v2/deviceManaged/bypassV2',
311
+ 'post',
312
+ body,
313
+ head
314
+ );
315
+
316
+ const success = this.checkResponse([response, status], 'setDisplay');
317
+ if (!success) {
318
+ logger.error(`Failed to set display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
319
+ }
320
+ return success;
321
+ }
322
+
323
+ /**
324
+ * Set drying mode enabled
325
+ */
326
+ async setDryingModeEnabled(enabled: boolean): Promise<boolean> {
327
+ logger.debug(`Setting drying mode to ${enabled ? 'enabled' : 'disabled'} for device: ${this.deviceName}`);
328
+ const [head, body] = this.buildApiDict('setDryingMode');
329
+
330
+ body.payload.data = {
331
+ autoDryingSwitch: enabled ? 1 : 0
332
+ };
333
+
334
+ const [response, status] = await this.callApi(
335
+ '/cloud/v2/deviceManaged/bypassV2',
336
+ 'post',
337
+ body,
338
+ head
339
+ );
340
+
341
+ const success = this.checkResponse([response, status], 'setDryingModeEnabled');
342
+ if (!success) {
343
+ logger.error(`Failed to set drying mode to ${enabled ? 'enabled' : 'disabled'} for device: ${this.deviceName}`);
344
+ }
345
+ return success;
346
+ }
347
+
348
+ /**
349
+ * Turn on display
350
+ */
351
+ async turnOnDisplay(): Promise<boolean> {
352
+ logger.debug(`Turning on display for device: ${this.deviceName}`);
353
+ return this.setDisplay(true);
354
+ }
355
+
356
+ /**
357
+ * Turn off display
358
+ */
359
+ async turnOffDisplay(): Promise<boolean> {
360
+ logger.debug(`Turning off display for device: ${this.deviceName}`);
361
+ return this.setDisplay(false);
362
+ }
363
+
364
+ /**
365
+ * Set auto mode
366
+ */
367
+ async setAutoMode(): Promise<boolean> {
368
+ logger.debug(`Setting auto mode for device: ${this.deviceName}`);
369
+ return this.setMode('auto');
370
+ }
371
+
372
+ /**
373
+ * Set manual mode
374
+ */
375
+ async setManualMode(): Promise<boolean> {
376
+ logger.debug(`Setting manual mode for device: ${this.deviceName}`);
377
+ return this.setMode('manual');
378
+ }
379
+
380
+ /**
381
+ * Get current humidity
382
+ * Provides access to the current humidity reading
383
+ */
384
+ get currentHumidity(): number {
385
+ return this.details.humidity || 0;
386
+ }
387
+
388
+ /**
389
+ * Get target humidity
390
+ * Provides access to the target humidity setting
391
+ */
392
+ get targetHumidity(): number {
393
+ return this.details.target_humidity || 0;
394
+ }
395
+
396
+ /**
397
+ * Get current temperature
398
+ * Provides access to the current temperature reading
399
+ */
400
+ get temperature(): number {
401
+ return this.details.temperature || 0;
402
+ }
403
+
404
+ /**
405
+ * Check if water tank is empty
406
+ * Returns true if water tank is empty and needs to be refilled
407
+ */
408
+ get waterLacks(): boolean {
409
+ return this.details.water_lacks || false;
410
+ }
411
+
412
+ /**
413
+ * Check if water tank is lifted
414
+ * Returns true if water tank is lifted/removed from the device
415
+ */
416
+ get waterTankLifted(): boolean {
417
+ return this.details.water_tank_lifted || false;
418
+ }
419
+
420
+ /**
421
+ * Get filter life percentage
422
+ * Returns the percentage of filter life remaining
423
+ */
424
+ get filterLifePercentage(): number {
425
+ return this.details.filter_life_percentage || 0;
426
+ }
427
+
428
+ /**
429
+ * Check if drying mode is enabled
430
+ * Returns true if drying mode is enabled
431
+ */
432
+ get dryingModeEnabled(): boolean {
433
+ const enabled = this.details.drying_mode?.autoDryingSwitch;
434
+ return enabled === 1;
435
+ }
436
+
437
+ /**
438
+ * Get drying mode state
439
+ * Returns 'on' if drying mode is active, 'off' if inactive, null if unknown
440
+ */
441
+ get dryingModeState(): string | null {
442
+ const state = this.details.drying_mode?.dryingState;
443
+ if (state === 1) return 'on';
444
+ if (state === 2) return 'off';
445
+ return null;
446
+ }
447
+
448
+ /**
449
+ * Get drying mode level
450
+ * Returns 'low' if level is 1, 'high' if level is 2, null if unknown
451
+ */
452
+ get dryingModeLevel(): string | null {
453
+ const level = this.details.drying_mode?.dryingLevel;
454
+ if (level === 1) return 'low';
455
+ if (level === 2) return 'high';
456
+ return null;
457
+ }
458
+
459
+ /**
460
+ * Get drying mode seconds remaining
461
+ * Returns the number of seconds remaining in drying mode
462
+ */
463
+ get dryingModeSecondsRemaining(): number {
464
+ return this.details.drying_mode?.dryingRemain || 0;
465
+ }
466
+ }
@@ -8,7 +8,7 @@ import { logger } from './logger';
8
8
 
9
9
  interface FanConfig {
10
10
  [key: string]: {
11
- module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2';
11
+ module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2' | 'VeSyncSuperior6000S' | 'VeSyncHumid1000S' | 'VeSyncHumid200S';
12
12
  features: string[];
13
13
  levels?: number[];
14
14
  };
@@ -203,8 +203,9 @@ export const fanConfig: FanConfig = {
203
203
  features: ['display', 'humidity', 'mist', 'timer', 'auto_mode']
204
204
  },
205
205
  'Classic200S': {
206
- module: 'VeSyncHumidifier',
207
- features: ['display', 'humidity', 'mist', 'timer', 'auto_mode']
206
+ module: 'VeSyncHumid200S',
207
+ features: ['display', 'humidity', 'mist', 'timer', 'auto_mode'],
208
+ levels: [1, 2, 3]
208
209
  },
209
210
 
210
211
  // Dual Series
@@ -279,22 +280,26 @@ export const fanConfig: FanConfig = {
279
280
  features: ['display', 'humidity', 'mist', 'warm', 'timer', 'auto_mode']
280
281
  },
281
282
  'LUH-M101S-WUS': {
282
- module: 'VeSyncWarmHumidifier',
283
- features: ['display', 'humidity', 'mist', 'warm', 'timer', 'auto_mode']
283
+ module: 'VeSyncHumid1000S',
284
+ features: ['display', 'humidity', 'mist', 'timer', 'auto_mode', 'night_light'],
285
+ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9]
284
286
  },
285
287
  'LUH-M101S-WEUR': {
286
- module: 'VeSyncWarmHumidifier',
287
- features: ['display', 'humidity', 'mist', 'warm', 'timer', 'auto_mode']
288
+ module: 'VeSyncHumid1000S',
289
+ features: ['display', 'humidity', 'mist', 'timer', 'auto_mode', 'night_light'],
290
+ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9]
288
291
  },
289
292
 
290
- // LEH Series
293
+ // LEH Series - Superior 6000S
291
294
  'LEH-S601S-WUS': {
292
- module: 'VeSyncWarmHumidifier',
293
- features: ['display', 'humidity', 'mist', 'warm', 'timer', 'auto_mode', 'drying']
295
+ module: 'VeSyncSuperior6000S',
296
+ features: ['display', 'humidity', 'mist', 'timer', 'auto_mode', 'drying', 'temperature'],
297
+ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9]
294
298
  },
295
299
  'LEH-S601S-WUSR': {
296
- module: 'VeSyncWarmHumidifier',
297
- features: ['display', 'humidity', 'mist', 'warm', 'timer', 'auto_mode', 'drying']
300
+ module: 'VeSyncSuperior6000S',
301
+ features: ['display', 'humidity', 'mist', 'timer', 'auto_mode', 'drying', 'temperature'],
302
+ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9]
298
303
  },
299
304
 
300
305
  // LV Series
@@ -331,7 +336,7 @@ export abstract class VeSyncFan extends VeSyncBaseDevice {
331
336
  protected speed_dict: Record<string, any> = {};
332
337
  protected _timer: number | { duration: number; action: string } | null = null;
333
338
  protected config: {
334
- module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2';
339
+ module: 'VeSyncAirBypass' | 'VeSyncHumidifier' | 'VeSyncWarmHumidifier' | 'VeSyncTowerFan' | 'VeSyncAirBaseV2' | 'VeSyncSuperior6000S' | 'VeSyncHumid1000S' | 'VeSyncHumid200S';
335
340
  features: string[];
336
341
  levels?: number[];
337
342
  };
@@ -353,6 +358,47 @@ export abstract class VeSyncFan extends VeSyncBaseDevice {
353
358
  return this.config.features.includes(feature);
354
359
  }
355
360
 
361
+ /**
362
+ * Check if feature is supported in current mode
363
+ */
364
+ isFeatureSupportedInCurrentMode(feature: string): boolean {
365
+ // First check if the feature is supported at all
366
+ if (!this.hasFeature(feature)) {
367
+ return false;
368
+ }
369
+
370
+ // Check mode-specific restrictions
371
+ const currentMode = this.mode;
372
+
373
+ // Some features are not supported in sleep mode
374
+ if (currentMode === 'sleep') {
375
+ // Display and child lock are not supported in sleep mode
376
+ if (feature === 'display' || feature === 'child_lock') {
377
+ return false;
378
+ }
379
+ }
380
+
381
+ // Fan speed is only supported in manual mode for some devices
382
+ if (feature === 'fan_speed' && this.deviceType.startsWith('LV-')) {
383
+ return currentMode === 'manual';
384
+ }
385
+
386
+ // Auto mode is not supported on Core200S
387
+ if (feature === 'auto_mode' && this.deviceType.includes('Core200S')) {
388
+ return false;
389
+ }
390
+
391
+ // Feature is supported in current mode
392
+ return true;
393
+ }
394
+
395
+ /**
396
+ * Get maximum fan speed level
397
+ */
398
+ getMaxFanSpeed(): number {
399
+ return this.config.levels ? Math.max(...this.config.levels) : 3;
400
+ }
401
+
356
402
  /**
357
403
  * Get current mode
358
404
  */