tsvesync 1.0.100 → 1.0.102

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.
@@ -9,7 +9,7 @@ export declare function setCurrentRegion(region: string): void;
9
9
  export declare function getRegionFromCountryCode(countryCode: string): string;
10
10
  export declare const API_RATE_LIMIT = 30;
11
11
  export declare const API_TIMEOUT = 15000;
12
- export declare const APP_VERSION = "5.6.60";
12
+ export declare const APP_VERSION = "5.7.16";
13
13
  export declare const PHONE_BRAND = "SM N9005";
14
14
  export declare const PHONE_OS = "Android";
15
15
  export declare const CLIENT_INFO = "SM N9005";
@@ -18,7 +18,7 @@ export declare const DEFAULT_TZ = "America/New_York";
18
18
  export declare const DEFAULT_REGION = "US";
19
19
  export declare const MOBILE_ID = "1234567890123456";
20
20
  export declare const BYPASS_HEADER_UA = "okhttp/3.12.1";
21
- export declare const CLIENT_VERSION = "VeSync 5.6.60";
21
+ export declare const CLIENT_VERSION = "VeSync 5.7.16";
22
22
  export declare const REGION_ENDPOINTS: {
23
23
  US: string;
24
24
  CA: string;
@@ -26,8 +26,22 @@ export declare const REGION_ENDPOINTS: {
26
26
  JP: string;
27
27
  EU: string;
28
28
  };
29
- export declare const CROSS_REGION_ERROR_CODE = -11260022;
29
+ export declare const CROSS_REGION_ERROR_CODES: number[];
30
30
  export declare const APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
31
+ export declare const AUTH_ERROR_CODES: {
32
+ ACCOUNT_PASSWORD_INCORRECT: number;
33
+ ILLEGAL_ARGUMENT: number;
34
+ CROSS_REGION: number;
35
+ CROSS_REGION_ALT: number;
36
+ };
37
+ /**
38
+ * Detect user's home region from email domain or country hints
39
+ */
40
+ export declare function detectUserRegion(email: string): string;
41
+ /**
42
+ * Get country code from region
43
+ */
44
+ export declare function getCountryCodeFromRegion(region: string, email?: string): string;
31
45
  export declare function generateAppId(): string;
32
46
  export declare function generateTerminalId(): string;
33
47
  export interface RequestBody {
@@ -113,7 +127,7 @@ export declare class Helpers {
113
127
  /**
114
128
  * Build request body for initial authentication step
115
129
  */
116
- static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string): Record<string, any>;
130
+ static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string, region?: string): Record<string, any>;
117
131
  /**
118
132
  * Build request body for second authentication step (login with authorize code)
119
133
  */
@@ -6,12 +6,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.Helpers = exports.APP_VERSION_TOO_LOW_ERROR_CODE = exports.CROSS_REGION_ERROR_CODE = exports.REGION_ENDPOINTS = exports.CLIENT_VERSION = exports.BYPASS_HEADER_UA = exports.MOBILE_ID = exports.DEFAULT_REGION = exports.DEFAULT_TZ = exports.USER_TYPE = exports.CLIENT_INFO = exports.PHONE_OS = exports.PHONE_BRAND = exports.APP_VERSION = exports.API_TIMEOUT = exports.API_RATE_LIMIT = void 0;
9
+ exports.Helpers = exports.AUTH_ERROR_CODES = exports.APP_VERSION_TOO_LOW_ERROR_CODE = exports.CROSS_REGION_ERROR_CODES = exports.REGION_ENDPOINTS = exports.CLIENT_VERSION = exports.BYPASS_HEADER_UA = exports.MOBILE_ID = exports.DEFAULT_REGION = exports.DEFAULT_TZ = exports.USER_TYPE = exports.CLIENT_INFO = exports.PHONE_OS = exports.PHONE_BRAND = exports.APP_VERSION = exports.API_TIMEOUT = exports.API_RATE_LIMIT = void 0;
10
10
  exports.getApiBaseUrl = getApiBaseUrl;
11
11
  exports.setApiBaseUrl = setApiBaseUrl;
12
12
  exports.getCurrentRegion = getCurrentRegion;
13
13
  exports.setCurrentRegion = setCurrentRegion;
14
14
  exports.getRegionFromCountryCode = getRegionFromCountryCode;
15
+ exports.detectUserRegion = detectUserRegion;
16
+ exports.getCountryCodeFromRegion = getCountryCodeFromRegion;
15
17
  exports.generateAppId = generateAppId;
16
18
  exports.generateTerminalId = generateTerminalId;
17
19
  const axios_1 = __importDefault(require("axios"));
@@ -51,7 +53,7 @@ function getRegionFromCountryCode(countryCode) {
51
53
  }
52
54
  exports.API_RATE_LIMIT = 30;
53
55
  exports.API_TIMEOUT = 15000;
54
- exports.APP_VERSION = '5.6.60';
56
+ exports.APP_VERSION = '5.7.16'; // Updated to newer version
55
57
  exports.PHONE_BRAND = 'SM N9005';
56
58
  exports.PHONE_OS = 'Android';
57
59
  exports.CLIENT_INFO = 'SM N9005';
@@ -60,7 +62,7 @@ exports.DEFAULT_TZ = 'America/New_York';
60
62
  exports.DEFAULT_REGION = 'US';
61
63
  exports.MOBILE_ID = '1234567890123456';
62
64
  exports.BYPASS_HEADER_UA = 'okhttp/3.12.1';
63
- exports.CLIENT_VERSION = 'VeSync 5.6.60';
65
+ exports.CLIENT_VERSION = 'VeSync 5.7.16';
64
66
  // Regional API endpoints
65
67
  exports.REGION_ENDPOINTS = {
66
68
  'US': 'https://smartapi.vesync.com',
@@ -69,12 +71,57 @@ exports.REGION_ENDPOINTS = {
69
71
  'JP': 'https://smartapi.vesync.com',
70
72
  'EU': 'https://smartapi.vesync.eu'
71
73
  };
72
- // Cross-region error code
73
- exports.CROSS_REGION_ERROR_CODE = -11260022;
74
+ // Cross-region error codes - multiple versions exist
75
+ exports.CROSS_REGION_ERROR_CODES = [-11260022, -11261022];
74
76
  exports.APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
77
+ // Authentication error codes
78
+ exports.AUTH_ERROR_CODES = {
79
+ ACCOUNT_PASSWORD_INCORRECT: -11201129,
80
+ ILLEGAL_ARGUMENT: -11000022,
81
+ CROSS_REGION: -11260022,
82
+ CROSS_REGION_ALT: -11261022
83
+ };
84
+ /**
85
+ * Detect user's home region from email domain or country hints
86
+ */
87
+ function detectUserRegion(email) {
88
+ // Check for common EU email domains
89
+ const euDomains = ['.de', '.fr', '.it', '.es', '.nl', '.be', '.at', '.dk', '.se', '.no', '.fi', '.eu'];
90
+ const emailLower = email.toLowerCase();
91
+ for (const domain of euDomains) {
92
+ if (emailLower.includes(domain)) {
93
+ return 'EU';
94
+ }
95
+ }
96
+ // Default to US for unknown domains
97
+ return 'US';
98
+ }
99
+ /**
100
+ * Get country code from region
101
+ */
102
+ function getCountryCodeFromRegion(region, email) {
103
+ if (region === 'EU') {
104
+ // Try to detect specific EU country from email
105
+ if (email) {
106
+ const emailLower = email.toLowerCase();
107
+ if (emailLower.includes('.de') || emailLower.includes('german'))
108
+ return 'DE';
109
+ if (emailLower.includes('.fr'))
110
+ return 'FR';
111
+ if (emailLower.includes('.it'))
112
+ return 'IT';
113
+ if (emailLower.includes('.es'))
114
+ return 'ES';
115
+ if (emailLower.includes('.nl'))
116
+ return 'NL';
117
+ }
118
+ return 'DE'; // Default to Germany for EU
119
+ }
120
+ return 'US'; // Default for non-EU
121
+ }
75
122
  // Generate unique APP_ID for each session
76
123
  function generateAppId() {
77
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
124
+ const chars = 'ABCDEFGHIJKLMNOPqRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
78
125
  let result = '';
79
126
  for (let i = 0; i < 8; i++) {
80
127
  result += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -131,7 +178,9 @@ class Helpers {
131
178
  return {
132
179
  'Content-Type': 'application/json; charset=UTF-8',
133
180
  'User-Agent': exports.BYPASS_HEADER_UA,
134
- 'accept-language': 'en'
181
+ 'accept-language': 'en',
182
+ 'appVersion': exports.APP_VERSION,
183
+ 'clientVersion': exports.CLIENT_VERSION
135
184
  };
136
185
  }
137
186
  /**
@@ -169,7 +218,16 @@ class Helpers {
169
218
  /**
170
219
  * Build request body for initial authentication step
171
220
  */
172
- static reqBodyAuthStep1(manager, appId, terminalId) {
221
+ static reqBodyAuthStep1(manager, appId, terminalId, region = 'US') {
222
+ // Detect user's home region and country code
223
+ const userRegion = detectUserRegion(manager.username);
224
+ const userCountryCode = getCountryCodeFromRegion(region === 'EU' ? 'EU' : userRegion, manager.username);
225
+ logger_1.logger.debug('Authentication Step 1 - Region detection:', {
226
+ email: manager.username,
227
+ detectedRegion: userRegion,
228
+ requestRegion: region,
229
+ userCountryCode: userCountryCode
230
+ });
173
231
  return {
174
232
  'email': manager.username,
175
233
  'method': 'authByPWDOrOTM',
@@ -185,7 +243,7 @@ class Helpers {
185
243
  'terminalId': terminalId,
186
244
  'timeZone': manager.timeZone || exports.DEFAULT_TZ,
187
245
  'token': '',
188
- 'userCountryCode': 'US',
246
+ 'userCountryCode': userCountryCode,
189
247
  'appID': appId,
190
248
  'sourceAppID': appId,
191
249
  'traceId': `APP${appId}${Math.floor(Date.now() / 1000)}`
@@ -402,7 +460,7 @@ class Helpers {
402
460
  // Generate terminal ID to be used in both steps
403
461
  const terminalId = generateTerminalId();
404
462
  // Step 1: Get authorization code
405
- const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId);
463
+ const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId, region);
406
464
  const step1Headers = this.reqHeaderAuth();
407
465
  // Set the correct regional endpoint
408
466
  const originalUrl = getApiBaseUrl();
@@ -422,16 +480,16 @@ class Helpers {
422
480
  }
423
481
  if (authResponse.code !== 0) {
424
482
  logger_1.logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg);
425
- // Handle cross-region error
426
- if (authResponse.code === exports.CROSS_REGION_ERROR_CODE) {
427
- logger_1.logger.debug('Cross-region error detected, will try different region');
483
+ // Handle cross-region error (multiple error codes possible)
484
+ if (exports.CROSS_REGION_ERROR_CODES.includes(authResponse.code)) {
485
+ logger_1.logger.debug('Cross-region error detected:', authResponse.code, 'will try different region');
428
486
  setApiBaseUrl(originalUrl); // Restore original URL
429
487
  return [false, null, null, 'cross_region'];
430
488
  }
431
489
  setApiBaseUrl(originalUrl); // Restore original URL
432
490
  return [false, null, null, null];
433
491
  }
434
- const { authorizeCode, bizToken, userCountryCode } = authResponse.result || {};
492
+ const { authorizeCode, bizToken } = authResponse.result || {};
435
493
  if (!authorizeCode) {
436
494
  logger_1.logger.error('Missing authorization code in step 1 response:', authResponse.result);
437
495
  setApiBaseUrl(originalUrl); // Restore original URL
@@ -439,7 +497,9 @@ class Helpers {
439
497
  }
440
498
  logger_1.logger.debug('Step 1 successful, got authorization code');
441
499
  // Step 2: Login with authorization code
442
- const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, userCountryCode);
500
+ // Use the appropriate country code based on the region we're authenticating with
501
+ const countryCodeForStep2 = region === 'EU' ? 'DE' : 'US';
502
+ const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, countryCodeForStep2);
443
503
  logger_1.logger.debug('Step 2: Logging in with authorization code...');
444
504
  const [loginResponse, loginStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', step2Body, step1Headers, manager);
445
505
  if (!loginResponse || loginStatus !== 200) {
@@ -449,31 +509,44 @@ class Helpers {
449
509
  }
450
510
  if (loginResponse.code !== 0) {
451
511
  // Handle cross-region error with retry (like pyvesync PR #340)
452
- if (loginResponse.code === exports.CROSS_REGION_ERROR_CODE) {
512
+ if (exports.CROSS_REGION_ERROR_CODES.includes(loginResponse.code)) {
453
513
  logger_1.logger.debug('Cross-region error detected in Step 2, retrying with region change token...');
454
514
  const { bizToken: regionChangeToken, countryCode: newCountryCode } = loginResponse.result || {};
455
- if (regionChangeToken) {
456
- // Retry Step 2 with region change token
515
+ if (regionChangeToken && newCountryCode) {
516
+ // Retry Step 2 with region change token on the SAME endpoint
517
+ // The region change token tells the server to handle the cross-region authentication
457
518
  const retryBody = {
458
- ...step2Body,
519
+ method: 'loginByAuthorizeCode4Vesync',
520
+ authorizeCode: authorizeCode,
521
+ acceptLanguage: 'en',
522
+ clientInfo: exports.CLIENT_INFO,
523
+ clientType: 'vesyncApp',
524
+ clientVersion: exports.CLIENT_VERSION,
525
+ debugMode: false,
526
+ emailSubscriptions: false,
527
+ osInfo: exports.PHONE_OS,
528
+ terminalId: terminalId,
529
+ timeZone: exports.DEFAULT_TZ,
530
+ userCountryCode: newCountryCode,
531
+ traceId: `APP${appId}${Math.floor(Date.now() / 1000)}`,
459
532
  bizToken: regionChangeToken,
460
- regionChange: 'last_region',
461
- userCountryCode: newCountryCode || userCountryCode
533
+ regionChange: 'last_region'
462
534
  };
463
535
  logger_1.logger.debug('Retrying Step 2 with region change token...', {
464
536
  newCountryCode,
465
- hasRegionChangeToken: true
537
+ currentEndpoint: getApiBaseUrl()
466
538
  });
467
539
  const [retryResponse, retryStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', retryBody, step1Headers, manager);
468
- setApiBaseUrl(originalUrl); // Restore original URL after retry
469
540
  if (retryResponse && retryResponse.code === 0) {
470
541
  const { token, accountID, countryCode } = retryResponse.result || {};
471
542
  if (token && accountID) {
472
543
  logger_1.logger.debug('Step 2 retry successful with region change');
544
+ setApiBaseUrl(originalUrl); // Restore original URL on success
473
545
  return [true, token, accountID, countryCode || newCountryCode];
474
546
  }
475
547
  }
476
548
  logger_1.logger.error('Step 2 retry failed:', retryResponse);
549
+ setApiBaseUrl(originalUrl); // Restore original URL on failure
477
550
  return [false, null, null, null];
478
551
  }
479
552
  else {
@@ -491,7 +564,7 @@ class Helpers {
491
564
  return [false, null, null, null];
492
565
  }
493
566
  logger_1.logger.debug('Step 2 successful, got token and accountID');
494
- return [true, token, accountID, countryCode || userCountryCode];
567
+ return [true, token, accountID, countryCode || countryCodeForStep2];
495
568
  }
496
569
  catch (error) {
497
570
  logger_1.logger.error('New authentication flow error:', error);
@@ -36,6 +36,8 @@ export declare class VeSync {
36
36
  enabled: boolean;
37
37
  updateInterval: number;
38
38
  timeZone: string;
39
+ authFlowUsed?: 'legacy' | 'new';
40
+ apiBaseUrl?: string;
39
41
  fans: VeSyncBaseDevice[];
40
42
  outlets: VeSyncBaseDevice[];
41
43
  switches: VeSyncBaseDevice[];
@@ -46,13 +48,21 @@ export declare class VeSync {
46
48
  * @param username - VeSync account username
47
49
  * @param password - VeSync account password
48
50
  * @param timeZone - Optional timezone for device operations (defaults to America/New_York)
49
- * @param debug - Optional debug mode flag
50
- * @param redact - Optional redact mode flag
51
- * @param apiUrl - Optional API base URL override
52
- * @param customLogger - Optional custom logger implementation
53
- * @param excludeConfig - Optional device exclusion configuration
54
- */
55
- constructor(username: string, password: string, timeZone?: string, debug?: boolean, redact?: boolean, apiUrl?: string, customLogger?: Logger, excludeConfig?: ExcludeConfig);
51
+ * @param optionsOrDebug - Either options object or debug flag for backward compatibility
52
+ * @param redact - Optional redact mode flag (used when optionsOrDebug is boolean)
53
+ * @param apiUrl - Optional API base URL override (used when optionsOrDebug is boolean)
54
+ * @param customLogger - Optional custom logger implementation (used when optionsOrDebug is boolean)
55
+ * @param excludeConfig - Optional device exclusion configuration (used when optionsOrDebug is boolean)
56
+ */
57
+ constructor(username: string, password: string, timeZone?: string, optionsOrDebug?: boolean | {
58
+ debug?: boolean;
59
+ redact?: boolean;
60
+ apiUrl?: string;
61
+ customLogger?: Logger;
62
+ excludeConfig?: ExcludeConfig;
63
+ region?: string;
64
+ debugMode?: boolean;
65
+ }, redact?: boolean, apiUrl?: string, customLogger?: Logger, excludeConfig?: ExcludeConfig);
56
66
  /**
57
67
  * Get/Set debug mode
58
68
  */
@@ -125,22 +125,37 @@ class VeSync {
125
125
  * @param username - VeSync account username
126
126
  * @param password - VeSync account password
127
127
  * @param timeZone - Optional timezone for device operations (defaults to America/New_York)
128
- * @param debug - Optional debug mode flag
129
- * @param redact - Optional redact mode flag
130
- * @param apiUrl - Optional API base URL override
131
- * @param customLogger - Optional custom logger implementation
132
- * @param excludeConfig - Optional device exclusion configuration
128
+ * @param optionsOrDebug - Either options object or debug flag for backward compatibility
129
+ * @param redact - Optional redact mode flag (used when optionsOrDebug is boolean)
130
+ * @param apiUrl - Optional API base URL override (used when optionsOrDebug is boolean)
131
+ * @param customLogger - Optional custom logger implementation (used when optionsOrDebug is boolean)
132
+ * @param excludeConfig - Optional device exclusion configuration (used when optionsOrDebug is boolean)
133
133
  */
134
- constructor(username, password, timeZone = helpers_1.DEFAULT_TZ, debug = false, redact = true, apiUrl, customLogger, excludeConfig) {
135
- this._debug = debug;
136
- this._redact = redact;
134
+ constructor(username, password, timeZone = helpers_1.DEFAULT_TZ, optionsOrDebug = false, redact = true, apiUrl, customLogger, excludeConfig) {
135
+ // Handle options object or backward compatible parameters
136
+ let options = {};
137
+ if (typeof optionsOrDebug === 'object' && optionsOrDebug !== null) {
138
+ // Using new options pattern
139
+ options = optionsOrDebug;
140
+ this._debug = options.debug || options.debugMode || false;
141
+ this._redact = options.redact !== undefined ? options.redact : true;
142
+ this._excludeConfig = options.excludeConfig || null;
143
+ this._region = options.region || 'US';
144
+ customLogger = options.customLogger;
145
+ apiUrl = options.apiUrl;
146
+ }
147
+ else {
148
+ // Using old parameter pattern for backward compatibility
149
+ this._debug = optionsOrDebug;
150
+ this._redact = redact;
151
+ this._excludeConfig = excludeConfig || null;
152
+ this._region = 'US';
153
+ }
137
154
  this._energyUpdateInterval = DEFAULT_ENERGY_UPDATE_INTERVAL;
138
155
  this._energyCheck = true;
139
156
  this._lastUpdateTs = null;
140
157
  this._inProcess = false;
141
- this._excludeConfig = excludeConfig || null;
142
158
  this._appId = (0, helpers_1.generateAppId)();
143
- this._region = 'US';
144
159
  this.username = username;
145
160
  this.password = password;
146
161
  this.token = null;
@@ -175,25 +190,28 @@ class VeSync {
175
190
  this.timeZone = helpers_1.DEFAULT_TZ;
176
191
  logger_1.logger.debug('Time zone is not a string');
177
192
  }
178
- // Set custom API URL if provided, otherwise use default US endpoint
193
+ // Set custom API URL if provided, otherwise use region-based endpoint
179
194
  if (apiUrl) {
180
195
  (0, helpers_1.setApiBaseUrl)(apiUrl);
181
196
  }
182
197
  else {
183
- // Start with US endpoint as default
184
- (0, helpers_1.setApiBaseUrl)('https://smartapi.vesync.com');
185
- (0, helpers_1.setCurrentRegion)('US');
198
+ // Set endpoint based on region
199
+ const regionEndpoints = {
200
+ 'US': 'https://smartapi.vesync.com',
201
+ 'EU': 'https://smartapi.vesync.eu',
202
+ 'CA': 'https://smartapi.vesync.com',
203
+ 'MX': 'https://smartapi.vesync.com',
204
+ 'JP': 'https://smartapi.vesync.com'
205
+ };
206
+ const endpoint = regionEndpoints[this._region] || 'https://smartapi.vesync.com';
207
+ (0, helpers_1.setApiBaseUrl)(endpoint);
208
+ (0, helpers_1.setCurrentRegion)(this._region);
186
209
  }
187
210
  // Set custom logger if provided
188
211
  if (customLogger) {
189
212
  (0, logger_1.setLogger)(customLogger);
190
213
  }
191
- if (debug) {
192
- this.debug = true;
193
- }
194
- if (redact) {
195
- this.redact = true;
196
- }
214
+ // Debug and redact are already set above in the options handling
197
215
  }
198
216
  /**
199
217
  * Get/Set debug mode
@@ -443,20 +461,38 @@ class VeSync {
443
461
  for (let attempt = 0; attempt < retryAttempts; attempt++) {
444
462
  try {
445
463
  logger_1.logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
446
- // Try new authentication flow first
464
+ // First, try to detect user's region from email
465
+ const detectedRegion = (0, helpers_1.getRegionFromCountryCode)((0, helpers_1.getCountryCodeFromRegion)(this._region, this.username));
466
+ if (detectedRegion !== this._region) {
467
+ logger_1.logger.debug(`Adjusting region from ${this._region} to ${detectedRegion} based on email domain`);
468
+ this._region = detectedRegion;
469
+ (0, helpers_1.setCurrentRegion)(detectedRegion);
470
+ }
471
+ // Try new authentication flow first with detected region
447
472
  let [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, this._region);
473
+ // Track if new flow succeeded
474
+ if (success) {
475
+ this.authFlowUsed = 'new';
476
+ }
448
477
  if (!success) {
449
478
  // Check if we need to try a different region
450
479
  if (countryCode === 'cross_region') {
451
- logger_1.logger.debug('Cross-region error detected, trying EU region...');
452
- this._region = 'EU';
453
- (0, helpers_1.setCurrentRegion)('EU');
454
- [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, 'EU');
480
+ logger_1.logger.debug('Cross-region error detected, trying opposite region...');
481
+ const alternateRegion = this._region === 'US' ? 'EU' : 'US';
482
+ this._region = alternateRegion;
483
+ (0, helpers_1.setCurrentRegion)(alternateRegion);
484
+ [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, alternateRegion);
485
+ if (success) {
486
+ this.authFlowUsed = 'new';
487
+ }
455
488
  }
456
489
  // If new flow still fails, try legacy authentication as fallback
457
490
  if (!success) {
458
491
  logger_1.logger.debug('New authentication flow failed, trying legacy flow...');
459
492
  [success, token, accountId, countryCode] = await helpers_1.Helpers.authLegacyFlow(this);
493
+ if (success) {
494
+ this.authFlowUsed = 'legacy';
495
+ }
460
496
  }
461
497
  }
462
498
  if (success && token && accountId) {
@@ -464,6 +500,7 @@ class VeSync {
464
500
  this.accountId = accountId;
465
501
  this.countryCode = countryCode;
466
502
  this.enabled = true;
503
+ this.apiBaseUrl = (0, helpers_1.getApiBaseUrl)(); // Track the API endpoint being used
467
504
  // Set region based on country code if we got one
468
505
  if (countryCode) {
469
506
  const detectedRegion = (0, helpers_1.getRegionFromCountryCode)(countryCode);
@@ -474,6 +511,7 @@ class VeSync {
474
511
  }
475
512
  }
476
513
  logger_1.logger.debug('Authentication successful!', {
514
+ authFlow: this.authFlowUsed,
477
515
  region: this._region,
478
516
  countryCode: countryCode,
479
517
  endpoint: (0, helpers_1.getApiBaseUrl)()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.0.100",
3
+ "version": "1.0.102",
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",
@@ -51,7 +51,7 @@ export function getRegionFromCountryCode(countryCode: string): string {
51
51
  export const API_RATE_LIMIT = 30;
52
52
  export const API_TIMEOUT = 15000;
53
53
 
54
- export const APP_VERSION = '5.6.60';
54
+ export const APP_VERSION = '5.7.16'; // Updated to newer version
55
55
  export const PHONE_BRAND = 'SM N9005';
56
56
  export const PHONE_OS = 'Android';
57
57
  export const CLIENT_INFO = 'SM N9005';
@@ -60,7 +60,7 @@ export const DEFAULT_TZ = 'America/New_York';
60
60
  export const DEFAULT_REGION = 'US';
61
61
  export const MOBILE_ID = '1234567890123456';
62
62
  export const BYPASS_HEADER_UA = 'okhttp/3.12.1';
63
- export const CLIENT_VERSION = 'VeSync 5.6.60';
63
+ export const CLIENT_VERSION = 'VeSync 5.7.16';
64
64
 
65
65
  // Regional API endpoints
66
66
  export const REGION_ENDPOINTS = {
@@ -71,13 +71,58 @@ export const REGION_ENDPOINTS = {
71
71
  'EU': 'https://smartapi.vesync.eu'
72
72
  };
73
73
 
74
- // Cross-region error code
75
- export const CROSS_REGION_ERROR_CODE = -11260022;
74
+ // Cross-region error codes - multiple versions exist
75
+ export const CROSS_REGION_ERROR_CODES = [-11260022, -11261022];
76
76
  export const APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
77
77
 
78
+ // Authentication error codes
79
+ export const AUTH_ERROR_CODES = {
80
+ ACCOUNT_PASSWORD_INCORRECT: -11201129,
81
+ ILLEGAL_ARGUMENT: -11000022,
82
+ CROSS_REGION: -11260022,
83
+ CROSS_REGION_ALT: -11261022
84
+ };
85
+
86
+ /**
87
+ * Detect user's home region from email domain or country hints
88
+ */
89
+ export function detectUserRegion(email: string): string {
90
+ // Check for common EU email domains
91
+ const euDomains = ['.de', '.fr', '.it', '.es', '.nl', '.be', '.at', '.dk', '.se', '.no', '.fi', '.eu'];
92
+ const emailLower = email.toLowerCase();
93
+
94
+ for (const domain of euDomains) {
95
+ if (emailLower.includes(domain)) {
96
+ return 'EU';
97
+ }
98
+ }
99
+
100
+ // Default to US for unknown domains
101
+ return 'US';
102
+ }
103
+
104
+ /**
105
+ * Get country code from region
106
+ */
107
+ export function getCountryCodeFromRegion(region: string, email?: string): string {
108
+ if (region === 'EU') {
109
+ // Try to detect specific EU country from email
110
+ if (email) {
111
+ const emailLower = email.toLowerCase();
112
+ if (emailLower.includes('.de') || emailLower.includes('german')) return 'DE';
113
+ if (emailLower.includes('.fr')) return 'FR';
114
+ if (emailLower.includes('.it')) return 'IT';
115
+ if (emailLower.includes('.es')) return 'ES';
116
+ if (emailLower.includes('.nl')) return 'NL';
117
+ }
118
+ return 'DE'; // Default to Germany for EU
119
+ }
120
+ return 'US'; // Default for non-EU
121
+ }
122
+
78
123
  // Generate unique APP_ID for each session
79
124
  export function generateAppId(): string {
80
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
125
+ const chars = 'ABCDEFGHIJKLMNOPqRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
81
126
  let result = '';
82
127
  for (let i = 0; i < 8; i++) {
83
128
  result += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -196,7 +241,9 @@ export class Helpers {
196
241
  return {
197
242
  'Content-Type': 'application/json; charset=UTF-8',
198
243
  'User-Agent': BYPASS_HEADER_UA,
199
- 'accept-language': 'en'
244
+ 'accept-language': 'en',
245
+ 'appVersion': APP_VERSION,
246
+ 'clientVersion': CLIENT_VERSION
200
247
  };
201
248
  }
202
249
 
@@ -238,7 +285,18 @@ export class Helpers {
238
285
  /**
239
286
  * Build request body for initial authentication step
240
287
  */
241
- static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string): Record<string, any> {
288
+ static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string, region: string = 'US'): Record<string, any> {
289
+ // Detect user's home region and country code
290
+ const userRegion = detectUserRegion(manager.username);
291
+ const userCountryCode = getCountryCodeFromRegion(region === 'EU' ? 'EU' : userRegion, manager.username);
292
+
293
+ logger.debug('Authentication Step 1 - Region detection:', {
294
+ email: manager.username,
295
+ detectedRegion: userRegion,
296
+ requestRegion: region,
297
+ userCountryCode: userCountryCode
298
+ });
299
+
242
300
  return {
243
301
  'email': manager.username,
244
302
  'method': 'authByPWDOrOTM',
@@ -254,7 +312,7 @@ export class Helpers {
254
312
  'terminalId': terminalId,
255
313
  'timeZone': manager.timeZone || DEFAULT_TZ,
256
314
  'token': '',
257
- 'userCountryCode': 'US',
315
+ 'userCountryCode': userCountryCode,
258
316
  'appID': appId,
259
317
  'sourceAppID': appId,
260
318
  'traceId': `APP${appId}${Math.floor(Date.now() / 1000)}`
@@ -504,7 +562,7 @@ export class Helpers {
504
562
  const terminalId = generateTerminalId();
505
563
 
506
564
  // Step 1: Get authorization code
507
- const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId);
565
+ const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId, region);
508
566
  const step1Headers = this.reqHeaderAuth();
509
567
 
510
568
  // Set the correct regional endpoint
@@ -536,9 +594,9 @@ export class Helpers {
536
594
  if (authResponse.code !== 0) {
537
595
  logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg);
538
596
 
539
- // Handle cross-region error
540
- if (authResponse.code === CROSS_REGION_ERROR_CODE) {
541
- logger.debug('Cross-region error detected, will try different region');
597
+ // Handle cross-region error (multiple error codes possible)
598
+ if (CROSS_REGION_ERROR_CODES.includes(authResponse.code)) {
599
+ logger.debug('Cross-region error detected:', authResponse.code, 'will try different region');
542
600
  setApiBaseUrl(originalUrl); // Restore original URL
543
601
  return [false, null, null, 'cross_region'];
544
602
  }
@@ -547,7 +605,7 @@ export class Helpers {
547
605
  return [false, null, null, null];
548
606
  }
549
607
 
550
- const { authorizeCode, bizToken, userCountryCode } = authResponse.result || {};
608
+ const { authorizeCode, bizToken } = authResponse.result || {};
551
609
 
552
610
  if (!authorizeCode) {
553
611
  logger.error('Missing authorization code in step 1 response:', authResponse.result);
@@ -558,7 +616,9 @@ export class Helpers {
558
616
  logger.debug('Step 1 successful, got authorization code');
559
617
 
560
618
  // Step 2: Login with authorization code
561
- const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, userCountryCode);
619
+ // Use the appropriate country code based on the region we're authenticating with
620
+ const countryCodeForStep2 = region === 'EU' ? 'DE' : 'US';
621
+ const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, countryCodeForStep2);
562
622
 
563
623
  logger.debug('Step 2: Logging in with authorization code...');
564
624
 
@@ -578,23 +638,35 @@ export class Helpers {
578
638
 
579
639
  if (loginResponse.code !== 0) {
580
640
  // Handle cross-region error with retry (like pyvesync PR #340)
581
- if (loginResponse.code === CROSS_REGION_ERROR_CODE) {
641
+ if (CROSS_REGION_ERROR_CODES.includes(loginResponse.code)) {
582
642
  logger.debug('Cross-region error detected in Step 2, retrying with region change token...');
583
643
 
584
644
  const { bizToken: regionChangeToken, countryCode: newCountryCode } = loginResponse.result || {};
585
645
 
586
- if (regionChangeToken) {
587
- // Retry Step 2 with region change token
646
+ if (regionChangeToken && newCountryCode) {
647
+ // Retry Step 2 with region change token on the SAME endpoint
648
+ // The region change token tells the server to handle the cross-region authentication
588
649
  const retryBody = {
589
- ...step2Body,
650
+ method: 'loginByAuthorizeCode4Vesync',
651
+ authorizeCode: authorizeCode,
652
+ acceptLanguage: 'en',
653
+ clientInfo: CLIENT_INFO,
654
+ clientType: 'vesyncApp',
655
+ clientVersion: CLIENT_VERSION,
656
+ debugMode: false,
657
+ emailSubscriptions: false,
658
+ osInfo: PHONE_OS,
659
+ terminalId: terminalId,
660
+ timeZone: DEFAULT_TZ,
661
+ userCountryCode: newCountryCode,
662
+ traceId: `APP${appId}${Math.floor(Date.now() / 1000)}`,
590
663
  bizToken: regionChangeToken,
591
- regionChange: 'last_region',
592
- userCountryCode: newCountryCode || userCountryCode
664
+ regionChange: 'last_region'
593
665
  };
594
666
 
595
667
  logger.debug('Retrying Step 2 with region change token...', {
596
668
  newCountryCode,
597
- hasRegionChangeToken: true
669
+ currentEndpoint: getApiBaseUrl()
598
670
  });
599
671
 
600
672
  const [retryResponse, retryStatus] = await this.callApi(
@@ -605,17 +677,17 @@ export class Helpers {
605
677
  manager
606
678
  );
607
679
 
608
- setApiBaseUrl(originalUrl); // Restore original URL after retry
609
-
610
680
  if (retryResponse && retryResponse.code === 0) {
611
681
  const { token, accountID, countryCode } = retryResponse.result || {};
612
682
  if (token && accountID) {
613
683
  logger.debug('Step 2 retry successful with region change');
684
+ setApiBaseUrl(originalUrl); // Restore original URL on success
614
685
  return [true, token, accountID, countryCode || newCountryCode];
615
686
  }
616
687
  }
617
688
 
618
689
  logger.error('Step 2 retry failed:', retryResponse);
690
+ setApiBaseUrl(originalUrl); // Restore original URL on failure
619
691
  return [false, null, null, null];
620
692
  } else {
621
693
  logger.error('No region change token in cross-region error response');
@@ -637,7 +709,7 @@ export class Helpers {
637
709
  }
638
710
 
639
711
  logger.debug('Step 2 successful, got token and accountID');
640
- return [true, token, accountID, countryCode || userCountryCode];
712
+ return [true, token, accountID, countryCode || countryCodeForStep2];
641
713
 
642
714
  } catch (error) {
643
715
  logger.error('New authentication flow error:', error);
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, setApiBaseUrl, getApiBaseUrl, generateAppId, getRegionFromCountryCode, setCurrentRegion, getCurrentRegion, REGION_ENDPOINTS } from './helpers';
5
+ import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, setApiBaseUrl, getApiBaseUrl, generateAppId, getRegionFromCountryCode, setCurrentRegion, getCurrentRegion, REGION_ENDPOINTS, getCountryCodeFromRegion } from './helpers';
6
6
  import { VeSyncBaseDevice } from './vesyncBaseDevice';
7
7
  import { fanModules } from './vesyncFanImpl';
8
8
  import { outletModules } from './vesyncOutletImpl';
@@ -171,6 +171,8 @@ export class VeSync {
171
171
  enabled: boolean;
172
172
  updateInterval: number;
173
173
  timeZone: string;
174
+ authFlowUsed?: 'legacy' | 'new'; // Track which auth flow was used
175
+ apiBaseUrl?: string; // Track which API endpoint is being used
174
176
 
175
177
  fans: VeSyncBaseDevice[];
176
178
  outlets: VeSyncBaseDevice[];
@@ -183,31 +185,54 @@ export class VeSync {
183
185
  * @param username - VeSync account username
184
186
  * @param password - VeSync account password
185
187
  * @param timeZone - Optional timezone for device operations (defaults to America/New_York)
186
- * @param debug - Optional debug mode flag
187
- * @param redact - Optional redact mode flag
188
- * @param apiUrl - Optional API base URL override
189
- * @param customLogger - Optional custom logger implementation
190
- * @param excludeConfig - Optional device exclusion configuration
188
+ * @param optionsOrDebug - Either options object or debug flag for backward compatibility
189
+ * @param redact - Optional redact mode flag (used when optionsOrDebug is boolean)
190
+ * @param apiUrl - Optional API base URL override (used when optionsOrDebug is boolean)
191
+ * @param customLogger - Optional custom logger implementation (used when optionsOrDebug is boolean)
192
+ * @param excludeConfig - Optional device exclusion configuration (used when optionsOrDebug is boolean)
191
193
  */
192
194
  constructor(
193
195
  username: string,
194
196
  password: string,
195
197
  timeZone: string = DEFAULT_TZ,
196
- debug = false,
198
+ optionsOrDebug: boolean | {
199
+ debug?: boolean;
200
+ redact?: boolean;
201
+ apiUrl?: string;
202
+ customLogger?: Logger;
203
+ excludeConfig?: ExcludeConfig;
204
+ region?: string;
205
+ debugMode?: boolean; // Alias for debug
206
+ } = false,
197
207
  redact = true,
198
208
  apiUrl?: string,
199
209
  customLogger?: Logger,
200
210
  excludeConfig?: ExcludeConfig
201
211
  ) {
202
- this._debug = debug;
203
- this._redact = redact;
212
+ // Handle options object or backward compatible parameters
213
+ let options: any = {};
214
+ if (typeof optionsOrDebug === 'object' && optionsOrDebug !== null) {
215
+ // Using new options pattern
216
+ options = optionsOrDebug;
217
+ this._debug = options.debug || options.debugMode || false;
218
+ this._redact = options.redact !== undefined ? options.redact : true;
219
+ this._excludeConfig = options.excludeConfig || null;
220
+ this._region = options.region || 'US';
221
+ customLogger = options.customLogger;
222
+ apiUrl = options.apiUrl;
223
+ } else {
224
+ // Using old parameter pattern for backward compatibility
225
+ this._debug = optionsOrDebug as boolean;
226
+ this._redact = redact;
227
+ this._excludeConfig = excludeConfig || null;
228
+ this._region = 'US';
229
+ }
230
+
204
231
  this._energyUpdateInterval = DEFAULT_ENERGY_UPDATE_INTERVAL;
205
232
  this._energyCheck = true;
206
233
  this._lastUpdateTs = null;
207
234
  this._inProcess = false;
208
- this._excludeConfig = excludeConfig || null;
209
235
  this._appId = generateAppId();
210
- this._region = 'US';
211
236
 
212
237
  this.username = username;
213
238
  this.password = password;
@@ -245,13 +270,21 @@ export class VeSync {
245
270
  logger.debug('Time zone is not a string');
246
271
  }
247
272
 
248
- // Set custom API URL if provided, otherwise use default US endpoint
273
+ // Set custom API URL if provided, otherwise use region-based endpoint
249
274
  if (apiUrl) {
250
275
  setApiBaseUrl(apiUrl);
251
276
  } else {
252
- // Start with US endpoint as default
253
- setApiBaseUrl('https://smartapi.vesync.com');
254
- setCurrentRegion('US');
277
+ // Set endpoint based on region
278
+ const regionEndpoints: Record<string, string> = {
279
+ 'US': 'https://smartapi.vesync.com',
280
+ 'EU': 'https://smartapi.vesync.eu',
281
+ 'CA': 'https://smartapi.vesync.com',
282
+ 'MX': 'https://smartapi.vesync.com',
283
+ 'JP': 'https://smartapi.vesync.com'
284
+ };
285
+ const endpoint = regionEndpoints[this._region] || 'https://smartapi.vesync.com';
286
+ setApiBaseUrl(endpoint);
287
+ setCurrentRegion(this._region);
255
288
  }
256
289
 
257
290
  // Set custom logger if provided
@@ -259,13 +292,7 @@ export class VeSync {
259
292
  setLogger(customLogger);
260
293
  }
261
294
 
262
- if (debug) {
263
- this.debug = true;
264
- }
265
-
266
- if (redact) {
267
- this.redact = true;
268
- }
295
+ // Debug and redact are already set above in the options handling
269
296
  }
270
297
 
271
298
  /**
@@ -548,22 +575,42 @@ export class VeSync {
548
575
  try {
549
576
  logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
550
577
 
551
- // Try new authentication flow first
578
+ // First, try to detect user's region from email
579
+ const detectedRegion = getRegionFromCountryCode(getCountryCodeFromRegion(this._region, this.username));
580
+ if (detectedRegion !== this._region) {
581
+ logger.debug(`Adjusting region from ${this._region} to ${detectedRegion} based on email domain`);
582
+ this._region = detectedRegion;
583
+ setCurrentRegion(detectedRegion);
584
+ }
585
+
586
+ // Try new authentication flow first with detected region
552
587
  let [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, this._region);
588
+
589
+ // Track if new flow succeeded
590
+ if (success) {
591
+ this.authFlowUsed = 'new';
592
+ }
553
593
 
554
594
  if (!success) {
555
595
  // Check if we need to try a different region
556
596
  if (countryCode === 'cross_region') {
557
- logger.debug('Cross-region error detected, trying EU region...');
558
- this._region = 'EU';
559
- setCurrentRegion('EU');
560
- [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, 'EU');
597
+ logger.debug('Cross-region error detected, trying opposite region...');
598
+ const alternateRegion = this._region === 'US' ? 'EU' : 'US';
599
+ this._region = alternateRegion;
600
+ setCurrentRegion(alternateRegion);
601
+ [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, alternateRegion);
602
+ if (success) {
603
+ this.authFlowUsed = 'new';
604
+ }
561
605
  }
562
606
 
563
607
  // If new flow still fails, try legacy authentication as fallback
564
608
  if (!success) {
565
609
  logger.debug('New authentication flow failed, trying legacy flow...');
566
610
  [success, token, accountId, countryCode] = await Helpers.authLegacyFlow(this);
611
+ if (success) {
612
+ this.authFlowUsed = 'legacy';
613
+ }
567
614
  }
568
615
  }
569
616
 
@@ -572,6 +619,7 @@ export class VeSync {
572
619
  this.accountId = accountId;
573
620
  this.countryCode = countryCode;
574
621
  this.enabled = true;
622
+ this.apiBaseUrl = getApiBaseUrl(); // Track the API endpoint being used
575
623
 
576
624
  // Set region based on country code if we got one
577
625
  if (countryCode) {
@@ -584,6 +632,7 @@ export class VeSync {
584
632
  }
585
633
 
586
634
  logger.debug('Authentication successful!', {
635
+ authFlow: this.authFlowUsed,
587
636
  region: this._region,
588
637
  countryCode: countryCode,
589
638
  endpoint: getApiBaseUrl()