tsvesync 1.5.0 → 1.5.2
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/airBypass.js +9 -1
- package/dist/lib/helpers.d.ts +8 -0
- package/dist/lib/helpers.js +63 -13
- package/dist/lib/session.d.ts +8 -0
- package/dist/lib/vesync.d.ts +23 -0
- package/dist/lib/vesync.js +75 -1
- package/dist/lib/vesyncFan.js +5 -5
- package/package.json +7 -3
- package/src/lib/fans/airBypass.ts +8 -1
- package/src/lib/helpers.ts +87 -14
- package/src/lib/session.ts +8 -0
- package/src/lib/vesync.ts +79 -2
- package/src/lib/vesyncFan.ts +5 -5
- package/src/lib/fans/airBypass.ts.bak +0 -890
package/src/lib/session.ts
CHANGED
|
@@ -5,6 +5,14 @@ export interface Session {
|
|
|
5
5
|
region: string;
|
|
6
6
|
apiBaseUrl: string;
|
|
7
7
|
authFlowUsed?: 'legacy' | 'new';
|
|
8
|
+
/**
|
|
9
|
+
* Identifies this installation to VeSync. Issued tokens are bound to it (the JWT payload carries a
|
|
10
|
+
* `terminalId` claim), so it must stay stable across logins and restarts — a fresh id makes every login
|
|
11
|
+
* look like a brand-new device to VeSync.
|
|
12
|
+
*/
|
|
13
|
+
terminalId?: string;
|
|
14
|
+
/** Client app id, kept alongside `terminalId` so the client identity is stable too. */
|
|
15
|
+
appId?: string;
|
|
8
16
|
issuedAt?: number | null; // epoch seconds
|
|
9
17
|
expiresAt?: number | null; // epoch seconds
|
|
10
18
|
lastValidatedAt?: number | null; // epoch ms
|
package/src/lib/vesync.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* VeSync API Device Library
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, generateAppId, getRegionFromCountryCode, REGION_ENDPOINTS, getEndpointForCountryCode } from './helpers';
|
|
5
|
+
import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, generateAppId, generateTerminalId, getRegionFromCountryCode, REGION_ENDPOINTS, getEndpointForCountryCode } from './helpers';
|
|
6
6
|
import { Session, SessionStore, decodeJwtTimestamps } from './session';
|
|
7
7
|
import { VeSyncBaseDevice } from './vesyncBaseDevice';
|
|
8
8
|
import { fanModules } from './vesyncFanImpl';
|
|
@@ -175,6 +175,7 @@ export class VeSync {
|
|
|
175
175
|
private _inProcess: boolean;
|
|
176
176
|
private _excludeConfig: ExcludeConfig | null;
|
|
177
177
|
private _appId: string;
|
|
178
|
+
private _terminalId: string;
|
|
178
179
|
private _region: string;
|
|
179
180
|
private _countryCodeOverride: string | null;
|
|
180
181
|
private _apiUrlOverride: string | null;
|
|
@@ -261,6 +262,9 @@ export class VeSync {
|
|
|
261
262
|
this._lastUpdateTs = null;
|
|
262
263
|
this._inProcess = false;
|
|
263
264
|
this._appId = generateAppId();
|
|
265
|
+
// Stable per-installation identity. Overwritten by `hydrateSession` when a previous session is
|
|
266
|
+
// restored, so logins keep presenting the same terminal to VeSync.
|
|
267
|
+
this._terminalId = generateTerminalId();
|
|
264
268
|
|
|
265
269
|
this.username = username;
|
|
266
270
|
this.password = password;
|
|
@@ -322,6 +326,21 @@ export class VeSync {
|
|
|
322
326
|
// Debug and redact are already set above in the options handling
|
|
323
327
|
}
|
|
324
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Adopt a previously persisted client identity without restoring credentials.
|
|
331
|
+
*
|
|
332
|
+
* Callers that decide not to reuse a stored token (because it has expired, say) should still keep the
|
|
333
|
+
* terminal/app id from that session, so the fresh login presents the same client to VeSync.
|
|
334
|
+
*/
|
|
335
|
+
restoreClientIdentity(identity: { terminalId?: string | null; appId?: string | null }): void {
|
|
336
|
+
if (typeof identity?.terminalId === 'string' && identity.terminalId) {
|
|
337
|
+
this._terminalId = identity.terminalId;
|
|
338
|
+
}
|
|
339
|
+
if (typeof identity?.appId === 'string' && identity.appId) {
|
|
340
|
+
this._appId = identity.appId;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
325
344
|
/**
|
|
326
345
|
* Hydrate manager from a previously persisted session
|
|
327
346
|
*/
|
|
@@ -337,6 +356,14 @@ export class VeSync {
|
|
|
337
356
|
this.countryCode = session.countryCode ?? null;
|
|
338
357
|
this.authFlowUsed = session.authFlowUsed;
|
|
339
358
|
|
|
359
|
+
// Keep the persisted client identity so re-logins present the same terminal to VeSync.
|
|
360
|
+
if (typeof session.terminalId === 'string' && session.terminalId) {
|
|
361
|
+
this._terminalId = session.terminalId;
|
|
362
|
+
}
|
|
363
|
+
if (typeof session.appId === 'string' && session.appId) {
|
|
364
|
+
this._appId = session.appId;
|
|
365
|
+
}
|
|
366
|
+
|
|
340
367
|
// pyvesync parity:
|
|
341
368
|
// pyvesync persists `current_region` and derives the API base URL per-request from that region.
|
|
342
369
|
// It does NOT treat a stored base URL as authoritative. For backwards compatibility with older
|
|
@@ -396,6 +423,8 @@ export class VeSync {
|
|
|
396
423
|
region: this._region,
|
|
397
424
|
apiBaseUrl: this.apiBaseUrl,
|
|
398
425
|
authFlowUsed: this.authFlowUsed,
|
|
426
|
+
terminalId: this._terminalId,
|
|
427
|
+
appId: this._appId,
|
|
399
428
|
issuedAt: timestamps?.iat ?? null,
|
|
400
429
|
expiresAt: timestamps?.exp ?? null,
|
|
401
430
|
lastValidatedAt: Date.now(),
|
|
@@ -448,6 +477,14 @@ export class VeSync {
|
|
|
448
477
|
return this._appId;
|
|
449
478
|
}
|
|
450
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Terminal id presented to VeSync during authentication. Issued tokens carry it as a JWT claim, so it
|
|
482
|
+
* is persisted with the session and reused across logins and restarts.
|
|
483
|
+
*/
|
|
484
|
+
get terminalId(): string {
|
|
485
|
+
return this._terminalId;
|
|
486
|
+
}
|
|
487
|
+
|
|
451
488
|
/**
|
|
452
489
|
* Get current region
|
|
453
490
|
*/
|
|
@@ -605,6 +642,17 @@ export class VeSync {
|
|
|
605
642
|
let success = false;
|
|
606
643
|
|
|
607
644
|
try {
|
|
645
|
+
// A token whose `exp` has already passed can only come back as a token error, so refresh it
|
|
646
|
+
// up front instead of spending a request to be told so.
|
|
647
|
+
if (this.isTokenExpired()) {
|
|
648
|
+
logger.debug('Stored VeSync token is past its expiry; authenticating before requesting the device list');
|
|
649
|
+
if (!await this.login()) {
|
|
650
|
+
logger.error('VeSync token had expired and re-authentication failed. Check the configured username/password (and country code if your account is not in the US).');
|
|
651
|
+
this.enabled = false;
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
608
656
|
const fetchDeviceList = async (): Promise<[any, number]> => {
|
|
609
657
|
return await Helpers.callApi(
|
|
610
658
|
'/cloud/v1/deviceManaged/devices',
|
|
@@ -640,7 +688,9 @@ export class VeSync {
|
|
|
640
688
|
if (typeof code === 'number' && code !== 0) {
|
|
641
689
|
if (!didRetryToken && isTokenError(code)) {
|
|
642
690
|
didRetryToken = true;
|
|
643
|
-
|
|
691
|
+
// Recoverable: re-authenticate and retry. Logged at debug because the retry below
|
|
692
|
+
// normally succeeds; the outcome is reported after the loop.
|
|
693
|
+
logger.debug('Device list request returned a token error; re-authenticating and retrying', {
|
|
644
694
|
code,
|
|
645
695
|
msg: response?.msg,
|
|
646
696
|
region: this._region,
|
|
@@ -648,6 +698,7 @@ export class VeSync {
|
|
|
648
698
|
});
|
|
649
699
|
const reloginOk = await this.login();
|
|
650
700
|
if (!reloginOk) {
|
|
701
|
+
logger.error('VeSync rejected the stored session and re-authentication failed. Check the configured username/password (and country code if your account is not in the US).');
|
|
651
702
|
this.enabled = false;
|
|
652
703
|
return false;
|
|
653
704
|
}
|
|
@@ -688,6 +739,19 @@ export class VeSync {
|
|
|
688
739
|
break;
|
|
689
740
|
}
|
|
690
741
|
|
|
742
|
+
if (didRetryToken) {
|
|
743
|
+
if (response && status === 200 && response.code === 0) {
|
|
744
|
+
logger.info('VeSync session had expired; re-authenticated and recovered automatically.');
|
|
745
|
+
} else {
|
|
746
|
+
logger.warn('VeSync session had expired and the request still failed after re-authenticating.', {
|
|
747
|
+
code: response?.code,
|
|
748
|
+
msg: response?.msg,
|
|
749
|
+
region: this._region,
|
|
750
|
+
endpoint: this.apiBaseUrl
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
691
755
|
if (!response) {
|
|
692
756
|
logger.error('No response received from VeSync API');
|
|
693
757
|
return false;
|
|
@@ -950,6 +1014,19 @@ export class VeSync {
|
|
|
950
1014
|
}
|
|
951
1015
|
}
|
|
952
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* True when the current token carries an `exp` claim that has already passed.
|
|
1019
|
+
*
|
|
1020
|
+
* Deliberately conservative: a token that cannot be decoded, or that has no `exp`, is treated as
|
|
1021
|
+
* usable and left to the API to accept or reject.
|
|
1022
|
+
*/
|
|
1023
|
+
isTokenExpired(skewSeconds: number = 60): boolean {
|
|
1024
|
+
if (!this.token) return false;
|
|
1025
|
+
const exp = decodeJwtTimestamps(this.token)?.exp;
|
|
1026
|
+
if (!exp) return false;
|
|
1027
|
+
return exp * 1000 <= Date.now() + skewSeconds * 1000;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
953
1030
|
/**
|
|
954
1031
|
* Test if update interval has been exceeded
|
|
955
1032
|
*/
|
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
|
},
|