tsvesync 1.4.2 → 1.4.4
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/README.md +15 -34
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +13 -1
- package/dist/lib/helpers.d.ts +4 -3
- package/dist/lib/helpers.js +151 -97
- package/dist/lib/vesync.d.ts +5 -0
- package/dist/lib/vesync.js +173 -45
- package/package.json +1 -1
- package/src/lib/constants.ts +14 -1
- package/src/lib/helpers.ts +178 -108
- package/src/lib/vesync.ts +201 -51
package/dist/lib/vesync.js
CHANGED
|
@@ -11,7 +11,20 @@ const vesyncOutletImpl_1 = require("./vesyncOutletImpl");
|
|
|
11
11
|
const vesyncSwitchImpl_1 = require("./vesyncSwitchImpl");
|
|
12
12
|
const vesyncBulbImpl_1 = require("./vesyncBulbImpl");
|
|
13
13
|
const logger_1 = require("./logger");
|
|
14
|
+
const constants_1 = require("./constants");
|
|
14
15
|
const DEFAULT_ENERGY_UPDATE_INTERVAL = 21600;
|
|
16
|
+
function normalizeApiBaseUrl(apiBaseUrl) {
|
|
17
|
+
return apiBaseUrl.trim().replace(/\/+$/, '');
|
|
18
|
+
}
|
|
19
|
+
const KNOWN_API_BASE_URLS = new Set(Object.values(helpers_1.REGION_ENDPOINTS).map(normalizeApiBaseUrl));
|
|
20
|
+
function inferRegionFromApiBaseUrl(apiBaseUrl) {
|
|
21
|
+
const normalized = normalizeApiBaseUrl(apiBaseUrl).toLowerCase();
|
|
22
|
+
if (normalized.includes('vesync.eu'))
|
|
23
|
+
return 'EU';
|
|
24
|
+
if (normalized.includes('vesync.com'))
|
|
25
|
+
return 'US';
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
15
28
|
/**
|
|
16
29
|
* Create device instance based on type
|
|
17
30
|
*/
|
|
@@ -157,6 +170,7 @@ class VeSync {
|
|
|
157
170
|
this._region = 'US';
|
|
158
171
|
this._countryCodeOverride = null;
|
|
159
172
|
}
|
|
173
|
+
this._apiUrlOverride = apiUrl || null;
|
|
160
174
|
this._energyUpdateInterval = DEFAULT_ENERGY_UPDATE_INTERVAL;
|
|
161
175
|
this._energyCheck = true;
|
|
162
176
|
this._lastUpdateTs = null;
|
|
@@ -198,7 +212,7 @@ class VeSync {
|
|
|
198
212
|
}
|
|
199
213
|
// Set custom API URL if provided, otherwise use region-based endpoint
|
|
200
214
|
if (apiUrl) {
|
|
201
|
-
|
|
215
|
+
this.apiBaseUrl = apiUrl;
|
|
202
216
|
}
|
|
203
217
|
else {
|
|
204
218
|
// If country code is provided, use it to determine the endpoint
|
|
@@ -208,16 +222,8 @@ class VeSync {
|
|
|
208
222
|
logger_1.logger.debug(`Country code ${this._countryCodeOverride} maps to ${endpointRegion} endpoint`);
|
|
209
223
|
}
|
|
210
224
|
// Set endpoint based on region
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
'EU': 'https://smartapi.vesync.eu',
|
|
214
|
-
'CA': 'https://smartapi.vesync.com',
|
|
215
|
-
'MX': 'https://smartapi.vesync.com',
|
|
216
|
-
'JP': 'https://smartapi.vesync.com'
|
|
217
|
-
};
|
|
218
|
-
const endpoint = regionEndpoints[this._region] || 'https://smartapi.vesync.com';
|
|
219
|
-
(0, helpers_1.setApiBaseUrl)(endpoint);
|
|
220
|
-
(0, helpers_1.setCurrentRegion)(this._region);
|
|
225
|
+
const endpoint = helpers_1.REGION_ENDPOINTS[this._region] || helpers_1.REGION_ENDPOINTS.US;
|
|
226
|
+
this.apiBaseUrl = endpoint;
|
|
221
227
|
}
|
|
222
228
|
// Set custom logger if provided
|
|
223
229
|
if (customLogger) {
|
|
@@ -229,18 +235,56 @@ class VeSync {
|
|
|
229
235
|
* Hydrate manager from a previously persisted session
|
|
230
236
|
*/
|
|
231
237
|
hydrateSession(session) {
|
|
238
|
+
var _a;
|
|
232
239
|
try {
|
|
233
|
-
if (session.
|
|
234
|
-
(
|
|
235
|
-
this.apiBaseUrl = session.apiBaseUrl;
|
|
236
|
-
}
|
|
237
|
-
if (session.region) {
|
|
238
|
-
this._region = session.region;
|
|
239
|
-
(0, helpers_1.setCurrentRegion)(session.region);
|
|
240
|
+
if (!(session === null || session === void 0 ? void 0 : session.token) || !(session === null || session === void 0 ? void 0 : session.accountId)) {
|
|
241
|
+
throw new Error('Session missing token/accountId');
|
|
240
242
|
}
|
|
243
|
+
// Always hydrate core credentials first.
|
|
241
244
|
this.token = session.token;
|
|
242
245
|
this.accountId = session.accountId;
|
|
243
|
-
this.countryCode = session.countryCode
|
|
246
|
+
this.countryCode = (_a = session.countryCode) !== null && _a !== void 0 ? _a : null;
|
|
247
|
+
this.authFlowUsed = session.authFlowUsed;
|
|
248
|
+
// pyvesync parity:
|
|
249
|
+
// pyvesync persists `current_region` and derives the API base URL per-request from that region.
|
|
250
|
+
// It does NOT treat a stored base URL as authoritative. For backwards compatibility with older
|
|
251
|
+
// sessions, we normalize the fields here:
|
|
252
|
+
// - Prefer stored `region` when present
|
|
253
|
+
// - Otherwise derive region from stored `countryCode` (same mapping as pyvesync)
|
|
254
|
+
// - Otherwise infer region from stored apiBaseUrl (best-effort)
|
|
255
|
+
const sessionBaseUrl = session.apiBaseUrl ? normalizeApiBaseUrl(session.apiBaseUrl) : null;
|
|
256
|
+
const sessionBaseUrlIsKnown = !!sessionBaseUrl && KNOWN_API_BASE_URLS.has(sessionBaseUrl);
|
|
257
|
+
// If the session contains a custom base URL (not a known regional URL) and the caller did not
|
|
258
|
+
// explicitly set an API override, treat it as an override so region switching doesn't clobber it.
|
|
259
|
+
if (!this._apiUrlOverride && sessionBaseUrl && !sessionBaseUrlIsKnown) {
|
|
260
|
+
this._apiUrlOverride = sessionBaseUrl;
|
|
261
|
+
}
|
|
262
|
+
// Apply base URL override if present (pyvesync API_BASE_URL-style override).
|
|
263
|
+
if (this._apiUrlOverride) {
|
|
264
|
+
this.apiBaseUrl = this._apiUrlOverride;
|
|
265
|
+
}
|
|
266
|
+
let hydratedRegion = null;
|
|
267
|
+
if (typeof session.region === 'string' && session.region in helpers_1.REGION_ENDPOINTS) {
|
|
268
|
+
hydratedRegion = session.region;
|
|
269
|
+
}
|
|
270
|
+
else if (typeof session.countryCode === 'string' && session.countryCode.trim()) {
|
|
271
|
+
hydratedRegion = (0, helpers_1.getRegionFromCountryCode)(session.countryCode);
|
|
272
|
+
}
|
|
273
|
+
else if (sessionBaseUrl) {
|
|
274
|
+
hydratedRegion = inferRegionFromApiBaseUrl(sessionBaseUrl);
|
|
275
|
+
}
|
|
276
|
+
if (hydratedRegion && hydratedRegion in helpers_1.REGION_ENDPOINTS) {
|
|
277
|
+
this._region = hydratedRegion;
|
|
278
|
+
}
|
|
279
|
+
// Derive the API base URL from region unless explicitly overridden.
|
|
280
|
+
if (!this._apiUrlOverride) {
|
|
281
|
+
const endpoint = helpers_1.REGION_ENDPOINTS[this._region] || helpers_1.REGION_ENDPOINTS.US;
|
|
282
|
+
this.apiBaseUrl = endpoint;
|
|
283
|
+
}
|
|
284
|
+
else if (!this.apiBaseUrl && sessionBaseUrl) {
|
|
285
|
+
// Fallback: keep the persisted base URL if we have no other source.
|
|
286
|
+
this.apiBaseUrl = sessionBaseUrl;
|
|
287
|
+
}
|
|
244
288
|
this.enabled = true;
|
|
245
289
|
}
|
|
246
290
|
catch (e) {
|
|
@@ -312,13 +356,21 @@ class VeSync {
|
|
|
312
356
|
get region() {
|
|
313
357
|
return this._region;
|
|
314
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Return the configured API URL override, if any.
|
|
361
|
+
*/
|
|
362
|
+
get apiUrlOverride() {
|
|
363
|
+
return this._apiUrlOverride;
|
|
364
|
+
}
|
|
315
365
|
/**
|
|
316
366
|
* Set region and update API endpoint
|
|
317
367
|
*/
|
|
318
368
|
set region(region) {
|
|
319
369
|
if (region in helpers_1.REGION_ENDPOINTS) {
|
|
320
370
|
this._region = region;
|
|
321
|
-
(
|
|
371
|
+
if (!this._apiUrlOverride) {
|
|
372
|
+
this.apiBaseUrl = helpers_1.REGION_ENDPOINTS[region];
|
|
373
|
+
}
|
|
322
374
|
}
|
|
323
375
|
}
|
|
324
376
|
/**
|
|
@@ -437,7 +489,7 @@ class VeSync {
|
|
|
437
489
|
* Get list of VeSync devices
|
|
438
490
|
*/
|
|
439
491
|
async getDevices() {
|
|
440
|
-
var _a, _b;
|
|
492
|
+
var _a, _b, _c;
|
|
441
493
|
if (!this.enabled) {
|
|
442
494
|
logger_1.logger.error('Not logged in to VeSync');
|
|
443
495
|
return false;
|
|
@@ -445,16 +497,96 @@ class VeSync {
|
|
|
445
497
|
this._inProcess = true;
|
|
446
498
|
let success = false;
|
|
447
499
|
try {
|
|
448
|
-
const
|
|
500
|
+
const fetchDeviceList = async () => {
|
|
501
|
+
return await helpers_1.Helpers.callApi('/cloud/v1/deviceManaged/devices', 'post', helpers_1.Helpers.reqBody(this, 'devicelist'), helpers_1.Helpers.reqHeaderBypass(), this);
|
|
502
|
+
};
|
|
503
|
+
const originalRegion = this._region;
|
|
504
|
+
let didRetryToken = false;
|
|
505
|
+
let didRetryCrossRegion = false;
|
|
506
|
+
let response;
|
|
507
|
+
let status;
|
|
508
|
+
[response, status] = await fetchDeviceList();
|
|
509
|
+
// Handle VeSync API codes similarly to pyvesync:
|
|
510
|
+
// - TOKEN_ERROR triggers a reauth + retry
|
|
511
|
+
// - CROSS_REGION triggers a region correction + retry
|
|
512
|
+
//
|
|
513
|
+
// pyvesync reference:
|
|
514
|
+
// - Token error retry: `pyvesync/vesync.py:async_call_api` (ErrorTypes.TOKEN_ERROR)
|
|
515
|
+
// - Region correction: `pyvesync/auth.py:_exchange_authorization_code` (ErrorTypes.CROSS_REGION)
|
|
516
|
+
while (true) {
|
|
517
|
+
if (!response || status !== 200) {
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
const code = response === null || response === void 0 ? void 0 : response.code;
|
|
521
|
+
if (typeof code === 'number' && code !== 0) {
|
|
522
|
+
if (!didRetryToken && (0, constants_1.isTokenError)(code)) {
|
|
523
|
+
didRetryToken = true;
|
|
524
|
+
logger_1.logger.warn('Device list request returned token error; re-authenticating and retrying (pyvesync parity)', {
|
|
525
|
+
code,
|
|
526
|
+
msg: response === null || response === void 0 ? void 0 : response.msg,
|
|
527
|
+
region: this._region,
|
|
528
|
+
endpoint: this.apiBaseUrl
|
|
529
|
+
});
|
|
530
|
+
const reloginOk = await this.login();
|
|
531
|
+
if (!reloginOk) {
|
|
532
|
+
this.enabled = false;
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
[response, status] = await fetchDeviceList();
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (!didRetryCrossRegion && (0, constants_1.isCrossRegionError)(code) && !this.apiUrlOverride) {
|
|
539
|
+
didRetryCrossRegion = true;
|
|
540
|
+
const serverRegion = (_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.currentRegion;
|
|
541
|
+
const normalizedServerRegion = typeof serverRegion === 'string' ? serverRegion.trim().toUpperCase() : null;
|
|
542
|
+
const nextRegion = normalizedServerRegion && normalizedServerRegion in helpers_1.REGION_ENDPOINTS
|
|
543
|
+
? normalizedServerRegion
|
|
544
|
+
: (this._region === 'US' ? 'EU' : 'US');
|
|
545
|
+
logger_1.logger.warn('Device list request returned cross-region error; retrying against corrected endpoint (pyvesync-style)', {
|
|
546
|
+
code,
|
|
547
|
+
msg: response === null || response === void 0 ? void 0 : response.msg,
|
|
548
|
+
region: this._region,
|
|
549
|
+
endpoint: this.apiBaseUrl,
|
|
550
|
+
serverRegion: normalizedServerRegion,
|
|
551
|
+
nextRegion
|
|
552
|
+
});
|
|
553
|
+
this.region = nextRegion;
|
|
554
|
+
[response, status] = await fetchDeviceList();
|
|
555
|
+
// If this was a best-effort alternate-region guess (no serverRegion provided) and it
|
|
556
|
+
// didn't help, revert so we don't leave the manager stuck on the wrong endpoint.
|
|
557
|
+
if (!normalizedServerRegion && (!response || response.code !== 0)) {
|
|
558
|
+
this.region = originalRegion;
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
449
565
|
if (!response) {
|
|
450
566
|
logger_1.logger.error('No response received from VeSync API');
|
|
451
567
|
return false;
|
|
452
568
|
}
|
|
453
|
-
if (
|
|
454
|
-
logger_1.logger.error('
|
|
569
|
+
if (status !== 200) {
|
|
570
|
+
logger_1.logger.error('Device list request failed with non-200 status:', {
|
|
571
|
+
status,
|
|
572
|
+
code: response === null || response === void 0 ? void 0 : response.code,
|
|
573
|
+
msg: response === null || response === void 0 ? void 0 : response.msg,
|
|
574
|
+
region: this._region,
|
|
575
|
+
endpoint: this.apiBaseUrl
|
|
576
|
+
});
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
if (typeof response.code === 'number' && response.code !== 0) {
|
|
580
|
+
logger_1.logger.error('Device list request returned error code:', {
|
|
581
|
+
status,
|
|
582
|
+
code: response.code,
|
|
583
|
+
msg: response === null || response === void 0 ? void 0 : response.msg,
|
|
584
|
+
region: this._region,
|
|
585
|
+
endpoint: this.apiBaseUrl
|
|
586
|
+
});
|
|
455
587
|
return false;
|
|
456
588
|
}
|
|
457
|
-
if (!((
|
|
589
|
+
if (!((_b = response.result) === null || _b === void 0 ? void 0 : _b.list)) {
|
|
458
590
|
logger_1.logger.error('No device list found in response');
|
|
459
591
|
return false;
|
|
460
592
|
}
|
|
@@ -484,7 +616,7 @@ class VeSync {
|
|
|
484
616
|
// Log summary statistics
|
|
485
617
|
logger_1.logger.debug('\nSummary Statistics:');
|
|
486
618
|
logger_1.logger.debug('-------------------');
|
|
487
|
-
logger_1.logger.debug(`Total Devices: ${((
|
|
619
|
+
logger_1.logger.debug(`Total Devices: ${((_c = this.devices) === null || _c === void 0 ? void 0 : _c.length) || 0}`);
|
|
488
620
|
for (const [category, devices] of Object.entries(this._devList)) {
|
|
489
621
|
logger_1.logger.debug(`${category}: ${devices.length} devices`);
|
|
490
622
|
}
|
|
@@ -524,8 +656,12 @@ class VeSync {
|
|
|
524
656
|
for (let attempt = 0; attempt < retryAttempts; attempt++) {
|
|
525
657
|
try {
|
|
526
658
|
logger_1.logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
|
|
527
|
-
// Try new authentication flow first with detected region
|
|
528
|
-
|
|
659
|
+
// Try new authentication flow first with detected region.
|
|
660
|
+
// NOTE: authNewFlow may temporarily change `this.region` during pyvesync-style
|
|
661
|
+
// cross-region Step 2 retry. Capture the attempted region so our fallback logic
|
|
662
|
+
// (try opposite endpoint) doesn't get confused by that internal mutation.
|
|
663
|
+
const attemptedRegion = this._region;
|
|
664
|
+
let [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, attemptedRegion, this._countryCodeOverride || undefined);
|
|
529
665
|
// Track if new flow succeeded
|
|
530
666
|
if (success) {
|
|
531
667
|
this.authFlowUsed = 'new';
|
|
@@ -538,10 +674,11 @@ class VeSync {
|
|
|
538
674
|
}
|
|
539
675
|
// Check if we need to try a different region
|
|
540
676
|
if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
|
|
541
|
-
// Mark
|
|
542
|
-
|
|
677
|
+
// Mark attempted region as tried (do not rely on `this._region`, which may have been
|
|
678
|
+
// mutated by authNewFlow during pyvesync-style cross-region handling).
|
|
679
|
+
triedRegions.add(attemptedRegion);
|
|
543
680
|
logger_1.logger.debug('Cross-region error detected, trying opposite region...');
|
|
544
|
-
const alternateRegion =
|
|
681
|
+
const alternateRegion = attemptedRegion === 'US' ? 'EU' : 'US';
|
|
545
682
|
// Check if we've already tried this region to prevent infinite loops
|
|
546
683
|
if (triedRegions.has(alternateRegion)) {
|
|
547
684
|
logger_1.logger.error('═══════════════════════════════════════════════════════════════');
|
|
@@ -568,18 +705,9 @@ class VeSync {
|
|
|
568
705
|
logger_1.logger.error('═══════════════════════════════════════════════════════════════');
|
|
569
706
|
return false;
|
|
570
707
|
}
|
|
571
|
-
logger_1.logger.debug(`Switching from ${
|
|
572
|
-
this.
|
|
573
|
-
(
|
|
574
|
-
// Update the API endpoint for the new region
|
|
575
|
-
const regionEndpoints = {
|
|
576
|
-
'US': 'https://smartapi.vesync.com',
|
|
577
|
-
'EU': 'https://smartapi.vesync.eu'
|
|
578
|
-
};
|
|
579
|
-
if (alternateRegion in regionEndpoints) {
|
|
580
|
-
(0, helpers_1.setApiBaseUrl)(regionEndpoints[alternateRegion]);
|
|
581
|
-
logger_1.logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
|
|
582
|
-
}
|
|
708
|
+
logger_1.logger.debug(`Switching from ${attemptedRegion} to ${alternateRegion} region`);
|
|
709
|
+
this.region = alternateRegion;
|
|
710
|
+
logger_1.logger.debug(`API endpoint set to ${this.apiBaseUrl}`);
|
|
583
711
|
// Retry with the alternate region
|
|
584
712
|
[success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, alternateRegion, this._countryCodeOverride || undefined);
|
|
585
713
|
if (success) {
|
|
@@ -614,7 +742,7 @@ class VeSync {
|
|
|
614
742
|
this.accountId = accountId;
|
|
615
743
|
this.countryCode = countryCode;
|
|
616
744
|
this.enabled = true;
|
|
617
|
-
this.apiBaseUrl =
|
|
745
|
+
this.apiBaseUrl = this.apiBaseUrl || 'https://smartapi.vesync.com'; // Track the API endpoint being used
|
|
618
746
|
// Persist session details for reuse across restarts
|
|
619
747
|
this.emitTokenChange();
|
|
620
748
|
// DO NOT change the region/endpoint after successful authentication!
|
|
@@ -634,7 +762,7 @@ class VeSync {
|
|
|
634
762
|
authFlow: this.authFlowUsed,
|
|
635
763
|
region: this._region,
|
|
636
764
|
countryCode: countryCode,
|
|
637
|
-
endpoint:
|
|
765
|
+
endpoint: this.apiBaseUrl
|
|
638
766
|
});
|
|
639
767
|
return true;
|
|
640
768
|
}
|
package/package.json
CHANGED
package/src/lib/constants.ts
CHANGED
|
@@ -15,6 +15,12 @@ export const CROSS_REGION_ERROR_CODES = [
|
|
|
15
15
|
-11261022, // access region conflict error
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
+
// Token/auth invalidation error codes
|
|
19
|
+
// pyvesync parity: `-11001000` maps to TOKEN_EXPIRED (invalid token)
|
|
20
|
+
export const TOKEN_ERROR_CODES = [
|
|
21
|
+
-11001000, // token expired/invalid
|
|
22
|
+
];
|
|
23
|
+
|
|
18
24
|
// Other error codes
|
|
19
25
|
export const APP_VERSION_ERROR_CODES = [
|
|
20
26
|
-11012022, // app version is too low
|
|
@@ -32,4 +38,11 @@ export function isCredentialError(code: number): boolean {
|
|
|
32
38
|
*/
|
|
33
39
|
export function isCrossRegionError(code: number): boolean {
|
|
34
40
|
return CROSS_REGION_ERROR_CODES.includes(code);
|
|
35
|
-
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if an error code indicates the token is invalid/expired.
|
|
45
|
+
*/
|
|
46
|
+
export function isTokenError(code: number): boolean {
|
|
47
|
+
return TOKEN_ERROR_CODES.includes(code);
|
|
48
|
+
}
|