tsvesync 1.5.1 → 1.6.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.
- package/dist/lib/fans/air131.js +3 -2
- package/dist/lib/fans/airBaseV2.d.ts +8 -0
- package/dist/lib/fans/airBaseV2.js +41 -0
- package/dist/lib/fans/airBypass.js +11 -3
- package/dist/lib/vesyncFan.js +13 -9
- package/package.json +6 -2
- package/src/lib/fans/air131.ts +3 -2
- package/src/lib/fans/airBaseV2.ts +52 -0
- package/src/lib/fans/airBypass.ts +10 -3
- package/src/lib/vesyncFan.ts +13 -9
- package/src/lib/fans/airBypass.ts.bak +0 -890
package/dist/lib/fans/air131.js
CHANGED
|
@@ -85,7 +85,8 @@ class VeSyncAir131 extends vesyncFan_1.VeSyncFan {
|
|
|
85
85
|
speed: data.level || 0,
|
|
86
86
|
filter_life: filterLife,
|
|
87
87
|
screen_status: data.screenStatus || 'off',
|
|
88
|
-
|
|
88
|
+
// The 131 API reports childLock as the string 'on'/'off'
|
|
89
|
+
child_lock: data.childLock === 'on' || data.childLock === true,
|
|
89
90
|
air_quality: data.airQuality || 'unknown',
|
|
90
91
|
air_quality_value: data.air_quality_value || 0,
|
|
91
92
|
air_quality_level: normalizedAirQuality.level >= 1 ? normalizedAirQuality.level : undefined,
|
|
@@ -343,7 +344,7 @@ class VeSyncAir131 extends vesyncFan_1.VeSyncFan {
|
|
|
343
344
|
});
|
|
344
345
|
const success = this.checkResponse([response, status], 'setChildLock');
|
|
345
346
|
if (success) {
|
|
346
|
-
this.details.
|
|
347
|
+
this.details.child_lock = enabled;
|
|
347
348
|
return true;
|
|
348
349
|
}
|
|
349
350
|
else {
|
|
@@ -54,6 +54,14 @@ export declare class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
54
54
|
* Override turn off to use the powerSwitch payload required by bypassV2 purifiers.
|
|
55
55
|
*/
|
|
56
56
|
turnOff(): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Override setChildLock to send the childLockSwitch payload required by
|
|
59
|
+
* bypassV2 purifiers (Vital 100S/200S, Everest Air). These devices report
|
|
60
|
+
* child lock as childLockSwitch 0|1 and reject the legacy child_lock
|
|
61
|
+
* boolean payload used by the Core series (matches pyvesync, which sends
|
|
62
|
+
* {'childLockSwitch': int} for this family).
|
|
63
|
+
*/
|
|
64
|
+
setChildLock(enabled: boolean): Promise<boolean>;
|
|
57
65
|
/**
|
|
58
66
|
* Override setMode to send workMode payloads for bypassV2 purifiers.
|
|
59
67
|
*/
|
|
@@ -264,6 +264,47 @@ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass {
|
|
|
264
264
|
return super.turnOff();
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Override setChildLock to send the childLockSwitch payload required by
|
|
269
|
+
* bypassV2 purifiers (Vital 100S/200S, Everest Air). These devices report
|
|
270
|
+
* child lock as childLockSwitch 0|1 and reject the legacy child_lock
|
|
271
|
+
* boolean payload used by the Core series (matches pyvesync, which sends
|
|
272
|
+
* {'childLockSwitch': int} for this family).
|
|
273
|
+
*/
|
|
274
|
+
async setChildLock(enabled) {
|
|
275
|
+
var _a;
|
|
276
|
+
if (!this.requiresPowerSwitchPayload()) {
|
|
277
|
+
// Use parent implementation for non-bypassV2 models
|
|
278
|
+
return super.setChildLock(enabled);
|
|
279
|
+
}
|
|
280
|
+
if (!this.hasFeature('child_lock')) {
|
|
281
|
+
const error = 'Child lock not supported';
|
|
282
|
+
logger_1.logger.error(`${error} for device: ${this.deviceName}`);
|
|
283
|
+
throw new Error(error);
|
|
284
|
+
}
|
|
285
|
+
logger_1.logger.debug(`Setting child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
286
|
+
const [head, body] = this.buildApiDict('setChildLock');
|
|
287
|
+
body.payload.data = {
|
|
288
|
+
childLockSwitch: enabled ? 1 : 0
|
|
289
|
+
};
|
|
290
|
+
const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
|
|
291
|
+
const success = this.checkV2Response(response, status, 'setChildLock');
|
|
292
|
+
// Code 11000000 marks the feature unsupported even when the envelope
|
|
293
|
+
// succeeds, so it must override the lenient inner-code handling
|
|
294
|
+
if ((response === null || response === void 0 ? void 0 : response.code) === 11000000 || (((_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.code) === 11000000)) {
|
|
295
|
+
logger_1.logger.warn(`Child lock control not supported via API for device: ${this.deviceName}`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (success) {
|
|
299
|
+
// No deferred details refresh here: pyvesync and the Core path
|
|
300
|
+
// both rely on the optimistic update plus the next regular poll
|
|
301
|
+
this.details.child_lock = enabled;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
logger_1.logger.error(`Failed to set child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
305
|
+
}
|
|
306
|
+
return success;
|
|
307
|
+
}
|
|
267
308
|
/**
|
|
268
309
|
* Override setMode to send workMode payloads for bypassV2 purifiers.
|
|
269
310
|
*/
|
|
@@ -198,7 +198,15 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
|
|
|
198
198
|
}
|
|
199
199
|
}, helpers_1.Helpers.reqHeaderBypass());
|
|
200
200
|
const success = this.checkResponse([response, status], 'changeFanSpeed');
|
|
201
|
-
if (
|
|
201
|
+
if (success) {
|
|
202
|
+
// Reflect the change locally so callers don't read a stale speed
|
|
203
|
+
// until the next getDetails(). setLevel also puts the device into
|
|
204
|
+
// manual mode and turns it on, mirroring pyvesync's set_fan_speed.
|
|
205
|
+
this.details.speed = speed;
|
|
206
|
+
this.details.mode = 'manual';
|
|
207
|
+
this.deviceStatus = 'on';
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
202
210
|
logger_1.logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
|
|
203
211
|
}
|
|
204
212
|
return success;
|
|
@@ -308,7 +316,7 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
|
|
|
308
316
|
configModule: this.configModule,
|
|
309
317
|
payload: {
|
|
310
318
|
data: {
|
|
311
|
-
|
|
319
|
+
child_lock: enabled
|
|
312
320
|
},
|
|
313
321
|
method: 'setChildLock',
|
|
314
322
|
source: 'APP'
|
|
@@ -321,7 +329,7 @@ class VeSyncAirBypass extends vesyncFan_1.VeSyncFan {
|
|
|
321
329
|
return false;
|
|
322
330
|
}
|
|
323
331
|
if (success) {
|
|
324
|
-
this.details.
|
|
332
|
+
this.details.child_lock = enabled;
|
|
325
333
|
return true;
|
|
326
334
|
}
|
|
327
335
|
else {
|
package/dist/lib/vesyncFan.js
CHANGED
|
@@ -13,13 +13,13 @@ exports.fanConfig = {
|
|
|
13
13
|
'Core200S': {
|
|
14
14
|
module: 'VeSyncAirBypass',
|
|
15
15
|
features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speed', 'sleep_mode', 'filter_life'],
|
|
16
|
-
levels: [1, 2, 3
|
|
16
|
+
levels: [1, 2, 3], // Low/Med/High; sleep is a mode, not a level
|
|
17
17
|
modes: ['sleep', 'manual']
|
|
18
18
|
},
|
|
19
19
|
'Core300S': {
|
|
20
20
|
module: 'VeSyncAirBypass',
|
|
21
21
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
22
|
-
levels: [1, 2, 3
|
|
22
|
+
levels: [1, 2, 3],
|
|
23
23
|
modes: ['sleep', 'manual', 'auto'],
|
|
24
24
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
25
25
|
},
|
|
@@ -53,21 +53,21 @@ exports.fanConfig = {
|
|
|
53
53
|
'LAP-C301S-WJP': {
|
|
54
54
|
module: 'VeSyncAirBypass',
|
|
55
55
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
56
|
-
levels: [1, 2, 3
|
|
56
|
+
levels: [1, 2, 3],
|
|
57
57
|
modes: ['sleep', 'manual', 'auto'],
|
|
58
58
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
59
59
|
},
|
|
60
60
|
'LAP-C302S-WUSB': {
|
|
61
61
|
module: 'VeSyncAirBypass',
|
|
62
62
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
63
|
-
levels: [1, 2, 3
|
|
63
|
+
levels: [1, 2, 3],
|
|
64
64
|
modes: ['sleep', 'manual', 'auto'],
|
|
65
65
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
66
66
|
},
|
|
67
67
|
'LAP-C301S-WAAA': {
|
|
68
68
|
module: 'VeSyncAirBypass',
|
|
69
69
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
70
|
-
levels: [1, 2, 3
|
|
70
|
+
levels: [1, 2, 3],
|
|
71
71
|
modes: ['sleep', 'manual', 'auto'],
|
|
72
72
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
73
73
|
},
|
|
@@ -228,22 +228,26 @@ exports.fanConfig = {
|
|
|
228
228
|
// LTF Series
|
|
229
229
|
'LTF-F422S-KEU': {
|
|
230
230
|
module: 'VeSyncTowerFan',
|
|
231
|
-
|
|
231
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
232
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
232
233
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
233
234
|
},
|
|
234
235
|
'LTF-F422S-WUSR': {
|
|
235
236
|
module: 'VeSyncTowerFan',
|
|
236
|
-
|
|
237
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
238
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
237
239
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
238
240
|
},
|
|
239
241
|
'LTF-F422_WJP': {
|
|
240
242
|
module: 'VeSyncTowerFan',
|
|
241
|
-
|
|
243
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
244
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
242
245
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
243
246
|
},
|
|
244
247
|
'LTF-F422S-WUS': {
|
|
245
248
|
module: 'VeSyncTowerFan',
|
|
246
|
-
|
|
249
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
250
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
247
251
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
248
252
|
},
|
|
249
253
|
// Classic Series
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsvesync",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
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",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"clean": "rm -rf dist",
|
|
10
10
|
"test:everest": "ts-node test/everest-air-parity.test.ts",
|
|
11
|
+
"test:child-lock": "ts-node test/child-lock-parity.test.ts",
|
|
11
12
|
"prepare": "npm run build",
|
|
12
13
|
"version": "git add -A src",
|
|
13
14
|
"postversion": "git push && git push --tags",
|
|
@@ -52,6 +53,9 @@
|
|
|
52
53
|
"files": [
|
|
53
54
|
"dist/**/*",
|
|
54
55
|
"src/**/*",
|
|
55
|
-
"api/**/*"
|
|
56
|
+
"api/**/*",
|
|
57
|
+
"!**/*.bak",
|
|
58
|
+
"!**/*.orig",
|
|
59
|
+
"!**/*.rej"
|
|
56
60
|
]
|
|
57
61
|
}
|
package/src/lib/fans/air131.ts
CHANGED
|
@@ -95,7 +95,8 @@ export class VeSyncAir131 extends VeSyncFan {
|
|
|
95
95
|
speed: data.level || 0,
|
|
96
96
|
filter_life: filterLife,
|
|
97
97
|
screen_status: data.screenStatus || 'off',
|
|
98
|
-
|
|
98
|
+
// The 131 API reports childLock as the string 'on'/'off'
|
|
99
|
+
child_lock: data.childLock === 'on' || data.childLock === true,
|
|
99
100
|
air_quality: data.airQuality || 'unknown',
|
|
100
101
|
air_quality_value: data.air_quality_value || 0,
|
|
101
102
|
air_quality_level: normalizedAirQuality.level >= 1 ? normalizedAirQuality.level : undefined,
|
|
@@ -413,7 +414,7 @@ export class VeSyncAir131 extends VeSyncFan {
|
|
|
413
414
|
|
|
414
415
|
const success = this.checkResponse([response, status], 'setChildLock');
|
|
415
416
|
if (success) {
|
|
416
|
-
this.details.
|
|
417
|
+
this.details.child_lock = enabled;
|
|
417
418
|
return true;
|
|
418
419
|
} else {
|
|
419
420
|
// Check for error code 11000000 (feature not supported)
|
|
@@ -311,6 +311,58 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Override setChildLock to send the childLockSwitch payload required by
|
|
316
|
+
* bypassV2 purifiers (Vital 100S/200S, Everest Air). These devices report
|
|
317
|
+
* child lock as childLockSwitch 0|1 and reject the legacy child_lock
|
|
318
|
+
* boolean payload used by the Core series (matches pyvesync, which sends
|
|
319
|
+
* {'childLockSwitch': int} for this family).
|
|
320
|
+
*/
|
|
321
|
+
override async setChildLock(enabled: boolean): Promise<boolean> {
|
|
322
|
+
if (!this.requiresPowerSwitchPayload()) {
|
|
323
|
+
// Use parent implementation for non-bypassV2 models
|
|
324
|
+
return super.setChildLock(enabled);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!this.hasFeature('child_lock')) {
|
|
328
|
+
const error = 'Child lock not supported';
|
|
329
|
+
logger.error(`${error} for device: ${this.deviceName}`);
|
|
330
|
+
throw new Error(error);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
logger.debug(`Setting child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
334
|
+
|
|
335
|
+
const [head, body] = this.buildApiDict('setChildLock');
|
|
336
|
+
body.payload.data = {
|
|
337
|
+
childLockSwitch: enabled ? 1 : 0
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const [response, status] = await this.callApi(
|
|
341
|
+
'/cloud/v2/deviceManaged/bypassV2',
|
|
342
|
+
'post',
|
|
343
|
+
body,
|
|
344
|
+
head
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const success = this.checkV2Response(response, status, 'setChildLock');
|
|
348
|
+
|
|
349
|
+
// Code 11000000 marks the feature unsupported even when the envelope
|
|
350
|
+
// succeeds, so it must override the lenient inner-code handling
|
|
351
|
+
if (response?.code === 11000000 || (response?.result?.code === 11000000)) {
|
|
352
|
+
logger.warn(`Child lock control not supported via API for device: ${this.deviceName}`);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (success) {
|
|
357
|
+
// No deferred details refresh here: pyvesync and the Core path
|
|
358
|
+
// both rely on the optimistic update plus the next regular poll
|
|
359
|
+
this.details.child_lock = enabled;
|
|
360
|
+
} else {
|
|
361
|
+
logger.error(`Failed to set child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
362
|
+
}
|
|
363
|
+
return success;
|
|
364
|
+
}
|
|
365
|
+
|
|
314
366
|
/**
|
|
315
367
|
* Override setMode to send workMode payloads for bypassV2 purifiers.
|
|
316
368
|
*/
|
|
@@ -233,7 +233,14 @@ export class VeSyncAirBypass extends VeSyncFan {
|
|
|
233
233
|
);
|
|
234
234
|
|
|
235
235
|
const success = this.checkResponse([response, status], 'changeFanSpeed');
|
|
236
|
-
if (
|
|
236
|
+
if (success) {
|
|
237
|
+
// Reflect the change locally so callers don't read a stale speed
|
|
238
|
+
// until the next getDetails(). setLevel also puts the device into
|
|
239
|
+
// manual mode and turns it on, mirroring pyvesync's set_fan_speed.
|
|
240
|
+
this.details.speed = speed;
|
|
241
|
+
this.details.mode = 'manual';
|
|
242
|
+
this.deviceStatus = 'on';
|
|
243
|
+
} else {
|
|
237
244
|
logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
|
|
238
245
|
}
|
|
239
246
|
return success;
|
|
@@ -369,7 +376,7 @@ export class VeSyncAirBypass extends VeSyncFan {
|
|
|
369
376
|
configModule: this.configModule,
|
|
370
377
|
payload: {
|
|
371
378
|
data: {
|
|
372
|
-
|
|
379
|
+
child_lock: enabled
|
|
373
380
|
},
|
|
374
381
|
method: 'setChildLock',
|
|
375
382
|
source: 'APP'
|
|
@@ -387,7 +394,7 @@ export class VeSyncAirBypass extends VeSyncFan {
|
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
if (success) {
|
|
390
|
-
this.details.
|
|
397
|
+
this.details.child_lock = enabled;
|
|
391
398
|
return true;
|
|
392
399
|
} else {
|
|
393
400
|
logger.error(`Failed to set child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
package/src/lib/vesyncFan.ts
CHANGED
|
@@ -25,13 +25,13 @@ export const fanConfig: FanConfig = {
|
|
|
25
25
|
'Core200S': {
|
|
26
26
|
module: 'VeSyncAirBypass',
|
|
27
27
|
features: ['display', 'child_lock', 'night_light', 'timer', 'fan_speed', 'sleep_mode', 'filter_life'],
|
|
28
|
-
levels: [1, 2, 3
|
|
28
|
+
levels: [1, 2, 3], // Low/Med/High; sleep is a mode, not a level
|
|
29
29
|
modes: ['sleep', 'manual']
|
|
30
30
|
},
|
|
31
31
|
'Core300S': {
|
|
32
32
|
module: 'VeSyncAirBypass',
|
|
33
33
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
34
|
-
levels: [1, 2, 3
|
|
34
|
+
levels: [1, 2, 3],
|
|
35
35
|
modes: ['sleep', 'manual', 'auto'],
|
|
36
36
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
37
37
|
},
|
|
@@ -66,21 +66,21 @@ export const fanConfig: FanConfig = {
|
|
|
66
66
|
'LAP-C301S-WJP': {
|
|
67
67
|
module: 'VeSyncAirBypass',
|
|
68
68
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
69
|
-
levels: [1, 2, 3
|
|
69
|
+
levels: [1, 2, 3],
|
|
70
70
|
modes: ['sleep', 'manual', 'auto'],
|
|
71
71
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
72
72
|
},
|
|
73
73
|
'LAP-C302S-WUSB': {
|
|
74
74
|
module: 'VeSyncAirBypass',
|
|
75
75
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
76
|
-
levels: [1, 2, 3
|
|
76
|
+
levels: [1, 2, 3],
|
|
77
77
|
modes: ['sleep', 'manual', 'auto'],
|
|
78
78
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
79
79
|
},
|
|
80
80
|
'LAP-C301S-WAAA': {
|
|
81
81
|
module: 'VeSyncAirBypass',
|
|
82
82
|
features: ['display', 'child_lock', 'night_light', 'air_quality', 'timer', 'fan_speed', 'auto_mode', 'sleep_mode', 'filter_life'],
|
|
83
|
-
levels: [1, 2, 3
|
|
83
|
+
levels: [1, 2, 3],
|
|
84
84
|
modes: ['sleep', 'manual', 'auto'],
|
|
85
85
|
autoPreferences: ['default', 'efficient', 'quiet']
|
|
86
86
|
},
|
|
@@ -242,22 +242,26 @@ export const fanConfig: FanConfig = {
|
|
|
242
242
|
// LTF Series
|
|
243
243
|
'LTF-F422S-KEU': {
|
|
244
244
|
module: 'VeSyncTowerFan',
|
|
245
|
-
|
|
245
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
246
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
246
247
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
247
248
|
},
|
|
248
249
|
'LTF-F422S-WUSR': {
|
|
249
250
|
module: 'VeSyncTowerFan',
|
|
250
|
-
|
|
251
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
252
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
251
253
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
252
254
|
},
|
|
253
255
|
'LTF-F422_WJP': {
|
|
254
256
|
module: 'VeSyncTowerFan',
|
|
255
|
-
|
|
257
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
258
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
256
259
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
257
260
|
},
|
|
258
261
|
'LTF-F422S-WUS': {
|
|
259
262
|
module: 'VeSyncTowerFan',
|
|
260
|
-
|
|
263
|
+
// No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
|
|
264
|
+
features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
|
|
261
265
|
levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
|
262
266
|
},
|
|
263
267
|
|
|
@@ -1,890 +0,0 @@
|
|
|
1
|
-
import { VeSyncFan } from '../vesyncFan';
|
|
2
|
-
import { VeSync } from '../vesync';
|
|
3
|
-
import { Helpers } from '../helpers';
|
|
4
|
-
import { logger } from '../logger';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* VeSync Air Purifier with Bypass
|
|
8
|
-
*/
|
|
9
|
-
export class VeSyncAirBypass extends VeSyncFan {
|
|
10
|
-
protected readonly modes = ['auto', 'manual', 'sleep'] as const;
|
|
11
|
-
protected readonly displayModes = ['on', 'off'] as const;
|
|
12
|
-
protected readonly childLockModes = ['on', 'off'] as const;
|
|
13
|
-
|
|
14
|
-
constructor(details: Record<string, any>, manager: VeSync) {
|
|
15
|
-
super(details, manager);
|
|
16
|
-
logger.debug(`Initialized VeSyncAirBypass device: ${this.deviceName}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Build API dictionary
|
|
21
|
-
*/
|
|
22
|
-
protected buildApiDict(method: string): [Record<string, any>, Record<string, any>] {
|
|
23
|
-
const head = Helpers.reqHeaderBypass();
|
|
24
|
-
const body = {
|
|
25
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
26
|
-
cid: this.cid,
|
|
27
|
-
configModule: this.configModule,
|
|
28
|
-
payload: {
|
|
29
|
-
method,
|
|
30
|
-
source: 'APP',
|
|
31
|
-
data: {}
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
return [head, body];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get device details
|
|
39
|
-
*/
|
|
40
|
-
async getDetails(): Promise<Boolean> {
|
|
41
|
-
logger.debug(`Getting details for device: ${this.deviceName}`);
|
|
42
|
-
|
|
43
|
-
// Special handling for LV series
|
|
44
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
45
|
-
const [response, status] = await this.callApi(
|
|
46
|
-
'/131airPurifier/v1/device/devicedetail',
|
|
47
|
-
'post',
|
|
48
|
-
{
|
|
49
|
-
acceptLanguage: 'en',
|
|
50
|
-
accountID: this.manager.accountId!,
|
|
51
|
-
appVersion: '2.8.6',
|
|
52
|
-
method: 'devicedetail',
|
|
53
|
-
mobileId: '1234567890123456',
|
|
54
|
-
phoneBrand: 'SM N9005',
|
|
55
|
-
phoneOS: 'Android',
|
|
56
|
-
timeZone: this.manager.timeZone!,
|
|
57
|
-
token: this.manager.token!,
|
|
58
|
-
uuid: this.uuid
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
'accept-language': 'en',
|
|
62
|
-
'accountId': this.manager.accountId!,
|
|
63
|
-
'appVersion': '2.8.6',
|
|
64
|
-
'content-type': 'application/json',
|
|
65
|
-
'tk': this.manager.token!,
|
|
66
|
-
'tz': this.manager.timeZone!
|
|
67
|
-
}
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const success = this.checkResponse([response, status], 'getDetails');
|
|
71
|
-
if (success && response) {
|
|
72
|
-
this.deviceStatus = response.result?.result?.deviceStatus === 'on' ? 'on' : 'off';
|
|
73
|
-
this.details = {
|
|
74
|
-
mode: response.result?.result?.mode || '',
|
|
75
|
-
speed: response.result?.result?.level || 0,
|
|
76
|
-
filterLife: response.result?.result?.filterLife || 0,
|
|
77
|
-
screenStatus: response.result?.result?.display ? 'on' : 'off',
|
|
78
|
-
childLock: response.result?.result?.childLock || false,
|
|
79
|
-
airQuality: response.result?.result?.airQuality || 0
|
|
80
|
-
};
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Original implementation for other devices
|
|
87
|
-
const [response, status] = await this.callApi(
|
|
88
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
89
|
-
'post',
|
|
90
|
-
{
|
|
91
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
92
|
-
cid: this.cid,
|
|
93
|
-
configModule: this.configModule,
|
|
94
|
-
payload: {
|
|
95
|
-
data: {},
|
|
96
|
-
method: 'getPurifierStatus',
|
|
97
|
-
source: 'APP'
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
Helpers.reqHeaderBypass()
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const success = this.checkResponse([response, status], 'getDetails');
|
|
104
|
-
if (success) {
|
|
105
|
-
const result = response?.result?.result;
|
|
106
|
-
if (result) {
|
|
107
|
-
this.deviceStatus = result.enabled ? 'on' : 'off';
|
|
108
|
-
this.details = {
|
|
109
|
-
mode: result.mode || '',
|
|
110
|
-
speed: result.level || 0,
|
|
111
|
-
filterLife: result.filter_life || 0,
|
|
112
|
-
screenStatus: result.display ? 'on' : 'off',
|
|
113
|
-
childLock: result.child_lock || false,
|
|
114
|
-
airQuality: result.air_quality || 0
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Don't overwrite config as it contains features array
|
|
118
|
-
if (result.configuration) {
|
|
119
|
-
this.details.configuration = result.configuration;
|
|
120
|
-
}
|
|
121
|
-
logger.debug(`Successfully got details for device: ${this.deviceName}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return success;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Turn device on
|
|
129
|
-
*/
|
|
130
|
-
async turnOn(): Promise<boolean> {
|
|
131
|
-
logger.info(`Turning on device: ${this.deviceName}`);
|
|
132
|
-
|
|
133
|
-
// Special handling for LV series
|
|
134
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
135
|
-
const [response, status] = await this.callApi(
|
|
136
|
-
'/131airPurifier/v1/device/deviceStatus',
|
|
137
|
-
'put',
|
|
138
|
-
{
|
|
139
|
-
acceptLanguage: 'en',
|
|
140
|
-
accountID: this.manager.accountId!,
|
|
141
|
-
status: 'on',
|
|
142
|
-
timeZone: this.manager.timeZone!,
|
|
143
|
-
token: this.manager.token!,
|
|
144
|
-
uuid: this.uuid
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
'accept-language': 'en',
|
|
148
|
-
'accountId': this.manager.accountId!,
|
|
149
|
-
'appVersion': '2.8.6',
|
|
150
|
-
'content-type': 'application/json',
|
|
151
|
-
'tk': this.manager.token!,
|
|
152
|
-
'tz': this.manager.timeZone!
|
|
153
|
-
}
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const success = this.checkResponse([response, status], 'turnOn');
|
|
157
|
-
if (!success) {
|
|
158
|
-
logger.error(`Failed to turn on device: ${this.deviceName}`);
|
|
159
|
-
}
|
|
160
|
-
return success;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Original implementation for other devices
|
|
164
|
-
const [response, status] = await this.callApi(
|
|
165
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
166
|
-
'post',
|
|
167
|
-
{
|
|
168
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
169
|
-
cid: this.cid,
|
|
170
|
-
configModule: this.configModule,
|
|
171
|
-
payload: {
|
|
172
|
-
data: {
|
|
173
|
-
enabled: true,
|
|
174
|
-
id: 0
|
|
175
|
-
},
|
|
176
|
-
method: 'setSwitch',
|
|
177
|
-
source: 'APP'
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
Helpers.reqHeaderBypass()
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const success = this.checkResponse([response, status], 'turnOn');
|
|
184
|
-
if (!success) {
|
|
185
|
-
logger.error(`Failed to turn on device: ${this.deviceName}`);
|
|
186
|
-
}
|
|
187
|
-
return success;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Turn device off
|
|
192
|
-
*/
|
|
193
|
-
async turnOff(): Promise<boolean> {
|
|
194
|
-
logger.info(`Turning off device: ${this.deviceName}`);
|
|
195
|
-
|
|
196
|
-
// Special handling for LV series
|
|
197
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
198
|
-
const [response, status] = await this.callApi(
|
|
199
|
-
'/131airPurifier/v1/device/deviceStatus',
|
|
200
|
-
'put',
|
|
201
|
-
{
|
|
202
|
-
acceptLanguage: 'en',
|
|
203
|
-
accountID: this.manager.accountId!,
|
|
204
|
-
status: 'off',
|
|
205
|
-
timeZone: this.manager.timeZone!,
|
|
206
|
-
token: this.manager.token!,
|
|
207
|
-
uuid: this.uuid
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
'accept-language': 'en',
|
|
211
|
-
'accountId': this.manager.accountId!,
|
|
212
|
-
'appVersion': '2.8.6',
|
|
213
|
-
'content-type': 'application/json',
|
|
214
|
-
'tk': this.manager.token!,
|
|
215
|
-
'tz': this.manager.timeZone!
|
|
216
|
-
}
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
const success = this.checkResponse([response, status], 'turnOff');
|
|
220
|
-
if (!success) {
|
|
221
|
-
logger.error(`Failed to turn off device: ${this.deviceName}`);
|
|
222
|
-
}
|
|
223
|
-
return success;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Original implementation for other devices
|
|
227
|
-
const [response, status] = await this.callApi(
|
|
228
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
229
|
-
'post',
|
|
230
|
-
{
|
|
231
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
232
|
-
cid: this.cid,
|
|
233
|
-
configModule: this.configModule,
|
|
234
|
-
payload: {
|
|
235
|
-
data: {
|
|
236
|
-
enabled: false,
|
|
237
|
-
id: 0
|
|
238
|
-
},
|
|
239
|
-
method: 'setSwitch',
|
|
240
|
-
source: 'APP'
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
Helpers.reqHeaderBypass()
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
const success = this.checkResponse([response, status], 'turnOff');
|
|
247
|
-
if (!success) {
|
|
248
|
-
logger.error(`Failed to turn off device: ${this.deviceName}`);
|
|
249
|
-
}
|
|
250
|
-
return success;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Change fan speed
|
|
255
|
-
*/
|
|
256
|
-
async changeFanSpeed(speed: number): Promise<boolean> {
|
|
257
|
-
// const speeds = this.config.levels ?? [];
|
|
258
|
-
// if (!speeds.includes(speed)) {
|
|
259
|
-
// logger.error(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')} for device: ${this.deviceName}`);
|
|
260
|
-
// return false;
|
|
261
|
-
// }
|
|
262
|
-
|
|
263
|
-
logger.info(`Changing fan speed to ${speed} for device: ${this.deviceName}`);
|
|
264
|
-
|
|
265
|
-
// Special handling for LV series
|
|
266
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
267
|
-
// Check if device is in manual mode
|
|
268
|
-
if (this.details.mode !== 'manual') {
|
|
269
|
-
logger.debug(`${this.deviceName} not in manual mode, cannot change speed`);
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Validate speed for LV series (1-3)
|
|
274
|
-
if (speed < 1 || speed > 3) {
|
|
275
|
-
logger.error(`Invalid fan speed: ${speed}. Must be between 1 and 3 for device: ${this.deviceName}`);
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const [response, status] = await this.callApi(
|
|
280
|
-
'/131airPurifier/v1/device/updateSpeed',
|
|
281
|
-
'put',
|
|
282
|
-
{
|
|
283
|
-
acceptLanguage: 'en',
|
|
284
|
-
accountID: this.manager.accountId!,
|
|
285
|
-
level: speed,
|
|
286
|
-
timeZone: this.manager.timeZone!,
|
|
287
|
-
token: this.manager.token!,
|
|
288
|
-
uuid: this.uuid
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
'accept-language': 'en',
|
|
292
|
-
'accountId': this.manager.accountId!,
|
|
293
|
-
'appVersion': '2.8.6',
|
|
294
|
-
'content-type': 'application/json',
|
|
295
|
-
'tk': this.manager.token!,
|
|
296
|
-
'tz': this.manager.timeZone!
|
|
297
|
-
}
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const success = this.checkResponse([response, status], 'changeFanSpeed');
|
|
301
|
-
if (success) {
|
|
302
|
-
this.details.speed = speed;
|
|
303
|
-
return true;
|
|
304
|
-
} else {
|
|
305
|
-
logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Standard implementation for other devices
|
|
311
|
-
const [response, status] = await this.callApi(
|
|
312
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
313
|
-
'post',
|
|
314
|
-
{
|
|
315
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
316
|
-
cid: this.cid,
|
|
317
|
-
configModule: this.configModule,
|
|
318
|
-
payload: {
|
|
319
|
-
data: {
|
|
320
|
-
id: 0,
|
|
321
|
-
level: speed,
|
|
322
|
-
mode: 'manual',
|
|
323
|
-
type: 'wind'
|
|
324
|
-
},
|
|
325
|
-
method: 'setLevel',
|
|
326
|
-
source: 'APP'
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
Helpers.reqHeaderBypass()
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
const success = this.checkResponse([response, status], 'changeFanSpeed');
|
|
333
|
-
if (!success) {
|
|
334
|
-
logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`);
|
|
335
|
-
}
|
|
336
|
-
return success;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Set device mode
|
|
341
|
-
*/
|
|
342
|
-
async setMode(mode: string): Promise<boolean> {
|
|
343
|
-
if (!this.modes.includes(mode as any)) {
|
|
344
|
-
const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`;
|
|
345
|
-
logger.error(`${error} for device: ${this.deviceName}`);
|
|
346
|
-
throw new Error(error);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
logger.debug(`Setting mode to ${mode} for device: ${this.deviceName}`);
|
|
350
|
-
|
|
351
|
-
// Special handling for LV series
|
|
352
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
353
|
-
const payload: Record<string, any> = {
|
|
354
|
-
acceptLanguage: 'en',
|
|
355
|
-
accountID: this.manager.accountId!,
|
|
356
|
-
mode: mode,
|
|
357
|
-
timeZone: this.manager.timeZone!,
|
|
358
|
-
token: this.manager.token!,
|
|
359
|
-
uuid: this.uuid
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
// For manual mode, we need to set the level
|
|
363
|
-
if (mode === 'manual') {
|
|
364
|
-
payload.level = this.details.speed || 1;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const [response, status] = await this.callApi(
|
|
368
|
-
'/131airPurifier/v1/device/updateMode',
|
|
369
|
-
'put',
|
|
370
|
-
payload,
|
|
371
|
-
{
|
|
372
|
-
'accept-language': 'en',
|
|
373
|
-
'accountId': this.manager.accountId!,
|
|
374
|
-
'appVersion': '2.8.6',
|
|
375
|
-
'content-type': 'application/json',
|
|
376
|
-
'tk': this.manager.token!,
|
|
377
|
-
'tz': this.manager.timeZone!
|
|
378
|
-
}
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
const success = this.checkResponse([response, status], 'setMode');
|
|
382
|
-
if (success) {
|
|
383
|
-
this.details.mode = mode;
|
|
384
|
-
return true;
|
|
385
|
-
} else {
|
|
386
|
-
logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Special handling for Core200S auto mode
|
|
392
|
-
if (this.deviceType.includes('Core200S') && mode === 'auto') {
|
|
393
|
-
// Core200S doesn't support auto mode, so we'll set it to manual mode
|
|
394
|
-
logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`);
|
|
395
|
-
return this.setMode('manual');
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Standard implementation for other devices
|
|
399
|
-
const [response, status] = await this.callApi(
|
|
400
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
401
|
-
'post',
|
|
402
|
-
{
|
|
403
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
404
|
-
cid: this.cid,
|
|
405
|
-
configModule: this.configModule,
|
|
406
|
-
payload: {
|
|
407
|
-
data: {
|
|
408
|
-
mode
|
|
409
|
-
},
|
|
410
|
-
method: 'setPurifierMode',
|
|
411
|
-
source: 'APP'
|
|
412
|
-
}
|
|
413
|
-
},
|
|
414
|
-
Helpers.reqHeaderBypass()
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const success = this.checkResponse([response, status], 'setMode');
|
|
418
|
-
if (success) {
|
|
419
|
-
this.details.mode = mode;
|
|
420
|
-
return true;
|
|
421
|
-
} else {
|
|
422
|
-
logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`);
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Set display status
|
|
429
|
-
*/
|
|
430
|
-
async setDisplay(enabled: boolean): Promise<boolean> {
|
|
431
|
-
if (!this.hasFeature('display')) {
|
|
432
|
-
const error = 'Display control not supported';
|
|
433
|
-
logger.error(`${error} for device: ${this.deviceName}`);
|
|
434
|
-
throw new Error(error);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Check if device is in sleep mode - display control may not work in sleep mode
|
|
438
|
-
if (this.details.mode === 'sleep') {
|
|
439
|
-
logger.warn(`Device ${this.deviceName} is in sleep mode, display control may not work`);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
logger.debug(`Setting display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
443
|
-
|
|
444
|
-
// Special handling for LV series
|
|
445
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
446
|
-
const [response, status] = await this.callApi(
|
|
447
|
-
'/131airPurifier/v1/device/updateScreen',
|
|
448
|
-
'put',
|
|
449
|
-
{
|
|
450
|
-
acceptLanguage: 'en',
|
|
451
|
-
accountID: this.manager.accountId!,
|
|
452
|
-
status: enabled ? 'on' : 'off',
|
|
453
|
-
timeZone: this.manager.timeZone!,
|
|
454
|
-
token: this.manager.token!,
|
|
455
|
-
uuid: this.uuid
|
|
456
|
-
},
|
|
457
|
-
{
|
|
458
|
-
'accept-language': 'en',
|
|
459
|
-
'accountId': this.manager.accountId!,
|
|
460
|
-
'appVersion': '2.8.6',
|
|
461
|
-
'content-type': 'application/json',
|
|
462
|
-
'tk': this.manager.token!,
|
|
463
|
-
'tz': this.manager.timeZone!
|
|
464
|
-
}
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
const success = this.checkResponse([response, status], 'setDisplay');
|
|
468
|
-
if (success) {
|
|
469
|
-
this.details.screenStatus = enabled ? 'on' : 'off';
|
|
470
|
-
return true;
|
|
471
|
-
} else {
|
|
472
|
-
logger.error(`Failed to set display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Standard implementation for other devices
|
|
478
|
-
const [response, status] = await this.callApi(
|
|
479
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
480
|
-
'post',
|
|
481
|
-
{
|
|
482
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
483
|
-
cid: this.cid,
|
|
484
|
-
configModule: this.configModule,
|
|
485
|
-
payload: {
|
|
486
|
-
data: {
|
|
487
|
-
state: enabled
|
|
488
|
-
},
|
|
489
|
-
method: 'setDisplay',
|
|
490
|
-
source: 'APP'
|
|
491
|
-
}
|
|
492
|
-
},
|
|
493
|
-
Helpers.reqHeaderBypass()
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
const success = this.checkResponse([response, status], 'setDisplay');
|
|
497
|
-
|
|
498
|
-
// Check for error code 11018000 (operation not supported in current mode)
|
|
499
|
-
if (response?.result?.code === 11018000) {
|
|
500
|
-
logger.warn(`Display control not supported in current mode for device: ${this.deviceName}`);
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (success) {
|
|
505
|
-
this.details.screenStatus = enabled ? 'on' : 'off';
|
|
506
|
-
return true;
|
|
507
|
-
} else {
|
|
508
|
-
logger.error(`Failed to set display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Set child lock
|
|
515
|
-
*/
|
|
516
|
-
async setChildLock(enabled: boolean): Promise<boolean> {
|
|
517
|
-
if (!this.hasFeature('child_lock')) {
|
|
518
|
-
const error = 'Child lock not supported';
|
|
519
|
-
logger.error(`${error} for device: ${this.deviceName}`);
|
|
520
|
-
throw new Error(error);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Check if device is in sleep mode - child lock may not work in sleep mode
|
|
524
|
-
if (this.details.mode === 'sleep') {
|
|
525
|
-
logger.warn(`Device ${this.deviceName} is in sleep mode, child lock control may not work`);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
logger.debug(`Setting child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
529
|
-
|
|
530
|
-
// Special handling for LV series
|
|
531
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
532
|
-
const [response, status] = await this.callApi(
|
|
533
|
-
'/131airPurifier/v1/device/updateChildLock',
|
|
534
|
-
'put',
|
|
535
|
-
{
|
|
536
|
-
acceptLanguage: 'en',
|
|
537
|
-
accountID: this.manager.accountId!,
|
|
538
|
-
status: enabled ? 'on' : 'off',
|
|
539
|
-
timeZone: this.manager.timeZone!,
|
|
540
|
-
token: this.manager.token!,
|
|
541
|
-
uuid: this.uuid
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
'accept-language': 'en',
|
|
545
|
-
'accountId': this.manager.accountId!,
|
|
546
|
-
'appVersion': '2.8.6',
|
|
547
|
-
'content-type': 'application/json',
|
|
548
|
-
'tk': this.manager.token!,
|
|
549
|
-
'tz': this.manager.timeZone!
|
|
550
|
-
}
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
const success = this.checkResponse([response, status], 'setChildLock');
|
|
554
|
-
if (success) {
|
|
555
|
-
this.details.childLock = enabled;
|
|
556
|
-
return true;
|
|
557
|
-
} else {
|
|
558
|
-
// Check for error code 11000000 (feature not supported)
|
|
559
|
-
if (response?.code === 11000000) {
|
|
560
|
-
logger.warn(`Child lock control not supported via API for device: ${this.deviceName}`);
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
logger.error(`Failed to set child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Standard implementation for other devices
|
|
569
|
-
const [response, status] = await this.callApi(
|
|
570
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
571
|
-
'post',
|
|
572
|
-
{
|
|
573
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
574
|
-
cid: this.cid,
|
|
575
|
-
configModule: this.configModule,
|
|
576
|
-
payload: {
|
|
577
|
-
data: {
|
|
578
|
-
state: enabled
|
|
579
|
-
},
|
|
580
|
-
method: 'setChildLock',
|
|
581
|
-
source: 'APP'
|
|
582
|
-
}
|
|
583
|
-
},
|
|
584
|
-
Helpers.reqHeaderBypass()
|
|
585
|
-
);
|
|
586
|
-
|
|
587
|
-
const success = this.checkResponse([response, status], 'setChildLock');
|
|
588
|
-
|
|
589
|
-
// Check for error code 11000000 (feature not supported)
|
|
590
|
-
if (response?.code === 11000000 || (response?.result?.code === 11000000)) {
|
|
591
|
-
logger.warn(`Child lock control not supported via API for device: ${this.deviceName}`);
|
|
592
|
-
return false;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (success) {
|
|
596
|
-
this.details.childLock = enabled;
|
|
597
|
-
return true;
|
|
598
|
-
} else {
|
|
599
|
-
logger.error(`Failed to set child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`);
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Set timer
|
|
606
|
-
*/
|
|
607
|
-
async setTimer(hours: number): Promise<boolean> {
|
|
608
|
-
if (!this.hasFeature('timer')) {
|
|
609
|
-
const error = 'Timer not supported';
|
|
610
|
-
logger.error(`${error} for device: ${this.deviceName}`);
|
|
611
|
-
throw new Error(error);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
logger.debug(`Setting timer to ${hours} hours for device: ${this.deviceName}`);
|
|
615
|
-
|
|
616
|
-
// Special handling for LV series
|
|
617
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
618
|
-
const [response, status] = await this.callApi(
|
|
619
|
-
'/131airPurifier/v1/device/updateTimer',
|
|
620
|
-
'put',
|
|
621
|
-
{
|
|
622
|
-
acceptLanguage: 'en',
|
|
623
|
-
accountID: this.manager.accountId!,
|
|
624
|
-
action: 'off',
|
|
625
|
-
duration: hours,
|
|
626
|
-
timeZone: this.manager.timeZone!,
|
|
627
|
-
token: this.manager.token!,
|
|
628
|
-
uuid: this.uuid
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
'accept-language': 'en',
|
|
632
|
-
'accountId': this.manager.accountId!,
|
|
633
|
-
'appVersion': '2.8.6',
|
|
634
|
-
'content-type': 'application/json',
|
|
635
|
-
'tk': this.manager.token!,
|
|
636
|
-
'tz': this.manager.timeZone!
|
|
637
|
-
}
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
const success = this.checkResponse([response, status], 'setTimer');
|
|
641
|
-
if (success) {
|
|
642
|
-
this.timer = { duration: hours * 3600, action: 'off' };
|
|
643
|
-
return true;
|
|
644
|
-
} else {
|
|
645
|
-
logger.error(`Failed to set timer to ${hours} hours for device: ${this.deviceName}`);
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Standard implementation for other devices
|
|
651
|
-
const [response, status] = await this.callApi(
|
|
652
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
653
|
-
'post',
|
|
654
|
-
{
|
|
655
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
656
|
-
cid: this.cid,
|
|
657
|
-
configModule: this.configModule,
|
|
658
|
-
payload: {
|
|
659
|
-
data: {
|
|
660
|
-
action: 'off',
|
|
661
|
-
total: hours * 3600
|
|
662
|
-
},
|
|
663
|
-
method: 'addTimer',
|
|
664
|
-
source: 'APP'
|
|
665
|
-
}
|
|
666
|
-
},
|
|
667
|
-
Helpers.reqHeaderBypass()
|
|
668
|
-
);
|
|
669
|
-
|
|
670
|
-
const success = this.checkResponse([response, status], 'setTimer');
|
|
671
|
-
|
|
672
|
-
// Check for successful response with timer ID
|
|
673
|
-
if (success && response?.result?.result?.id) {
|
|
674
|
-
this.timer = { duration: hours * 3600, action: 'off' };
|
|
675
|
-
return true;
|
|
676
|
-
} else if (success) {
|
|
677
|
-
// API call succeeded but no timer ID returned
|
|
678
|
-
logger.warn(`Timer API call succeeded but no timer ID returned for device: ${this.deviceName}`);
|
|
679
|
-
this.timer = { duration: hours * 3600, action: 'off' };
|
|
680
|
-
return true;
|
|
681
|
-
} else {
|
|
682
|
-
logger.error(`Failed to set timer to ${hours} hours for device: ${this.deviceName}`);
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Clear timer
|
|
689
|
-
*/
|
|
690
|
-
async clearTimer(): Promise<boolean> {
|
|
691
|
-
if (!this.hasFeature('timer')) {
|
|
692
|
-
const error = 'Timer not supported';
|
|
693
|
-
logger.error(`${error} for device: ${this.deviceName}`);
|
|
694
|
-
throw new Error(error);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// If no timer is set, return success
|
|
698
|
-
if (!this.timer) {
|
|
699
|
-
logger.debug(`No timer to clear for device: ${this.deviceName}`);
|
|
700
|
-
return true;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
logger.debug(`Clearing timer for device: ${this.deviceName}`);
|
|
704
|
-
|
|
705
|
-
// Special handling for LV series
|
|
706
|
-
if (this.deviceType.startsWith('LV-')) {
|
|
707
|
-
const [response, status] = await this.callApi(
|
|
708
|
-
'/131airPurifier/v1/device/cancelTimer',
|
|
709
|
-
'put',
|
|
710
|
-
{
|
|
711
|
-
acceptLanguage: 'en',
|
|
712
|
-
accountID: this.manager.accountId!,
|
|
713
|
-
timeZone: this.manager.timeZone!,
|
|
714
|
-
token: this.manager.token!,
|
|
715
|
-
uuid: this.uuid
|
|
716
|
-
},
|
|
717
|
-
{
|
|
718
|
-
'accept-language': 'en',
|
|
719
|
-
'accountId': this.manager.accountId!,
|
|
720
|
-
'appVersion': '2.8.6',
|
|
721
|
-
'content-type': 'application/json',
|
|
722
|
-
'tk': this.manager.token!,
|
|
723
|
-
'tz': this.manager.timeZone!
|
|
724
|
-
}
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
const success = this.checkResponse([response, status], 'clearTimer');
|
|
728
|
-
if (success) {
|
|
729
|
-
this.timer = null;
|
|
730
|
-
return true;
|
|
731
|
-
} else {
|
|
732
|
-
logger.error(`Failed to clear timer for device: ${this.deviceName}`);
|
|
733
|
-
return false;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Standard implementation for other devices
|
|
738
|
-
const [response, status] = await this.callApi(
|
|
739
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
740
|
-
'post',
|
|
741
|
-
{
|
|
742
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
743
|
-
cid: this.cid,
|
|
744
|
-
configModule: this.configModule,
|
|
745
|
-
payload: {
|
|
746
|
-
data: {},
|
|
747
|
-
method: 'deleteTimer',
|
|
748
|
-
source: 'APP'
|
|
749
|
-
}
|
|
750
|
-
},
|
|
751
|
-
Helpers.reqHeaderBypass()
|
|
752
|
-
);
|
|
753
|
-
|
|
754
|
-
const success = this.checkResponse([response, status], 'clearTimer');
|
|
755
|
-
|
|
756
|
-
// Check for error code 11000000 (feature not supported or no timer to clear)
|
|
757
|
-
if (response?.code === 11000000 || (response?.result?.code === 11000000)) {
|
|
758
|
-
logger.warn(`No timer to clear or timer control not supported via API for device: ${this.deviceName}`);
|
|
759
|
-
this.timer = null;
|
|
760
|
-
return true;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (success) {
|
|
764
|
-
this.timer = null;
|
|
765
|
-
return true;
|
|
766
|
-
} else {
|
|
767
|
-
logger.error(`Failed to clear timer for device: ${this.deviceName}`);
|
|
768
|
-
return false;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Turn automatic stop on
|
|
774
|
-
*/
|
|
775
|
-
async automaticStopOn(): Promise<boolean> {
|
|
776
|
-
logger.debug(`Setting automatic stop on for device: ${this.deviceName}`);
|
|
777
|
-
const [response, status] = await this.callApi(
|
|
778
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
779
|
-
'post',
|
|
780
|
-
{
|
|
781
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
782
|
-
cid: this.cid,
|
|
783
|
-
configModule: this.configModule,
|
|
784
|
-
payload: {
|
|
785
|
-
data: {
|
|
786
|
-
enabled: true
|
|
787
|
-
},
|
|
788
|
-
method: 'setAutomaticStop',
|
|
789
|
-
source: 'APP'
|
|
790
|
-
}
|
|
791
|
-
},
|
|
792
|
-
Helpers.reqHeaderBypass()
|
|
793
|
-
);
|
|
794
|
-
|
|
795
|
-
const success = this.checkResponse([response, status], 'automaticStopOn');
|
|
796
|
-
if (!success) {
|
|
797
|
-
logger.error(`Failed to set automatic stop on for device: ${this.deviceName}`);
|
|
798
|
-
}
|
|
799
|
-
return success;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Turn automatic stop off
|
|
804
|
-
*/
|
|
805
|
-
async automaticStopOff(): Promise<boolean> {
|
|
806
|
-
logger.debug(`Setting automatic stop off for device: ${this.deviceName}`);
|
|
807
|
-
const [response, status] = await this.callApi(
|
|
808
|
-
'/cloud/v2/deviceManaged/bypassV2',
|
|
809
|
-
'post',
|
|
810
|
-
{
|
|
811
|
-
...Helpers.reqBody(this.manager, 'bypassV2'),
|
|
812
|
-
cid: this.cid,
|
|
813
|
-
configModule: this.configModule,
|
|
814
|
-
payload: {
|
|
815
|
-
data: {
|
|
816
|
-
enabled: false
|
|
817
|
-
},
|
|
818
|
-
method: 'setAutomaticStop',
|
|
819
|
-
source: 'APP'
|
|
820
|
-
}
|
|
821
|
-
},
|
|
822
|
-
Helpers.reqHeaderBypass()
|
|
823
|
-
);
|
|
824
|
-
|
|
825
|
-
const success = this.checkResponse([response, status], 'automaticStopOff');
|
|
826
|
-
if (!success) {
|
|
827
|
-
logger.error(`Failed to set automatic stop off for device: ${this.deviceName}`);
|
|
828
|
-
}
|
|
829
|
-
return success;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Set auto mode
|
|
834
|
-
*/
|
|
835
|
-
async autoMode(): Promise<boolean> {
|
|
836
|
-
logger.debug(`Setting auto mode for device: ${this.deviceName}`);
|
|
837
|
-
const success = await this.setMode('auto');
|
|
838
|
-
if (!success) {
|
|
839
|
-
logger.error(`Failed to set auto mode for device: ${this.deviceName}`);
|
|
840
|
-
}
|
|
841
|
-
return success;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* Set manual mode
|
|
846
|
-
*/
|
|
847
|
-
async manualMode(): Promise<boolean> {
|
|
848
|
-
logger.debug(`Setting manual mode for device: ${this.deviceName}`);
|
|
849
|
-
const success = await this.setMode('manual');
|
|
850
|
-
if (!success) {
|
|
851
|
-
logger.error(`Failed to set manual mode for device: ${this.deviceName}`);
|
|
852
|
-
}
|
|
853
|
-
return success;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Set sleep mode
|
|
858
|
-
*/
|
|
859
|
-
async sleepMode(): Promise<boolean> {
|
|
860
|
-
logger.debug(`Setting sleep mode for device: ${this.deviceName}`);
|
|
861
|
-
const success = await this.setMode('sleep');
|
|
862
|
-
if (!success) {
|
|
863
|
-
logger.error(`Failed to set sleep mode for device: ${this.deviceName}`);
|
|
864
|
-
}
|
|
865
|
-
return success;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* Turn off display
|
|
870
|
-
*/
|
|
871
|
-
async turnOffDisplay(): Promise<boolean> {
|
|
872
|
-
logger.debug(`Turning off display for device: ${this.deviceName}`);
|
|
873
|
-
const success = await this.setDisplay(false);
|
|
874
|
-
if (!success) {
|
|
875
|
-
logger.error(`Failed to turn off display for device: ${this.deviceName}`);
|
|
876
|
-
}
|
|
877
|
-
return success;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Turn on display
|
|
882
|
-
*/
|
|
883
|
-
async turnOnDisplay(): Promise<boolean> {
|
|
884
|
-
logger.debug(`Turning on display for device: ${this.deviceName}`);
|
|
885
|
-
const success = await this.setDisplay(true);
|
|
886
|
-
if (!success) {
|
|
887
|
-
logger.error(`Failed to turn on display for device: ${this.deviceName}`);
|
|
888
|
-
}
|
|
889
|
-
return success;
|
|
890
|
-
}
|