tsvesync 1.0.102 → 1.0.104

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 CHANGED
@@ -6,8 +6,10 @@ A TypeScript library for interacting with VeSync smart home devices. This librar
6
6
 
7
7
  - **Enhanced Authentication**: Now supports the new VeSync authentication flow (pyvesync PR #340) with automatic fallback to legacy authentication
8
8
  - **Regional API Support**: Automatic detection and routing for US (`smartapi.vesync.com`) and EU (`smartapi.vesync.eu`) endpoints
9
- - **Improved Error Handling**: Better recovery from API changes and authentication errors
10
- - **Cross-Region Support**: Handles cross-region authentication errors automatically
9
+ - **International Account Support**: Full support for accounts worldwide including Australia, New Zealand, Japan, and all European countries
10
+ - **Country Code Configuration**: Specify your country code for proper authentication with international accounts
11
+ - **Improved Error Handling**: Better recovery from API changes and authentication errors with helpful guidance
12
+ - **Cross-Region Support**: Automatic handling of cross-region authentication errors
11
13
 
12
14
  ## Features
13
15
 
@@ -176,6 +178,68 @@ await manager.login();
176
178
  await manager.getDevices();
177
179
  ```
178
180
 
181
+ ### International Account Support
182
+
183
+ For accounts created outside the United States, you may need to specify your country code:
184
+
185
+ ```typescript
186
+ import { VeSync } from 'tsvesync';
187
+
188
+ // For Australian users
189
+ const manager = new VeSync(username, password, 'America/Sydney', {
190
+ countryCode: 'AU'
191
+ });
192
+
193
+ // For German users
194
+ const manager = new VeSync(username, password, 'Europe/Berlin', {
195
+ countryCode: 'DE'
196
+ });
197
+
198
+ // For UK users
199
+ const manager = new VeSync(username, password, 'Europe/London', {
200
+ countryCode: 'GB'
201
+ });
202
+
203
+ await manager.login();
204
+ ```
205
+
206
+ #### Supported Country Codes
207
+
208
+ The library supports all country codes. Common ones include:
209
+
210
+ **Americas:**
211
+ - `US` - United States (default)
212
+ - `CA` - Canada
213
+ - `MX` - Mexico
214
+
215
+ **Europe:** (uses EU endpoint)
216
+ - `GB` - United Kingdom
217
+ - `DE` - Germany
218
+ - `FR` - France
219
+ - `IT` - Italy
220
+ - `ES` - Spain
221
+ - `NL` - Netherlands
222
+ - `SE` - Sweden
223
+ - `NO` - Norway
224
+ - `DK` - Denmark
225
+ - `FI` - Finland
226
+ - `PL` - Poland
227
+ - And all other European countries
228
+
229
+ **Asia-Pacific:** (uses US endpoint)
230
+ - `AU` - Australia
231
+ - `NZ` - New Zealand
232
+ - `JP` - Japan
233
+ - `SG` - Singapore
234
+ - `CN` - China
235
+ - `KR` - South Korea
236
+
237
+ **Important Notes:**
238
+ - If you get an authentication error about "country code mismatch" or "cross-region error", you need to set your country code
239
+ - The country code should match the country where your VeSync account was created
240
+ - European countries automatically use the EU endpoint (`smartapi.vesync.eu`)
241
+ - All other countries use the US endpoint (`smartapi.vesync.com`)
242
+
179
243
  ### Working with Devices
180
244
 
181
245
  ```typescript
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { VeSyncBulb } from './lib/vesyncBulb';
5
5
  export { VeSyncWallSwitch } from './lib/vesyncSwitchImpl';
6
6
  export { VeSyncBaseDevice } from './lib/vesyncBaseDevice';
7
7
  export { Helpers, setApiBaseUrl } from './lib/helpers';
8
+ export { CREDENTIAL_ERROR_CODES, CROSS_REGION_ERROR_CODES, isCredentialError, isCrossRegionError } from './lib/constants';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setApiBaseUrl = exports.Helpers = exports.VeSyncBaseDevice = exports.VeSyncWallSwitch = exports.VeSyncBulb = exports.VeSyncOutdoorPlug = exports.VeSyncOutlet15A = exports.VeSyncOutlet10A = exports.VeSyncOutlet7A = exports.VeSyncAirBypass = exports.VeSync = void 0;
3
+ exports.isCrossRegionError = exports.isCredentialError = exports.CROSS_REGION_ERROR_CODES = exports.CREDENTIAL_ERROR_CODES = exports.setApiBaseUrl = exports.Helpers = exports.VeSyncBaseDevice = exports.VeSyncWallSwitch = exports.VeSyncBulb = exports.VeSyncOutdoorPlug = exports.VeSyncOutlet15A = exports.VeSyncOutlet10A = exports.VeSyncOutlet7A = exports.VeSyncAirBypass = exports.VeSync = void 0;
4
4
  // Export main VeSync manager class
5
5
  var vesync_1 = require("./lib/vesync");
6
6
  Object.defineProperty(exports, "VeSync", { enumerable: true, get: function () { return vesync_1.VeSync; } });
@@ -23,3 +23,9 @@ Object.defineProperty(exports, "VeSyncBaseDevice", { enumerable: true, get: func
23
23
  var helpers_1 = require("./lib/helpers");
24
24
  Object.defineProperty(exports, "Helpers", { enumerable: true, get: function () { return helpers_1.Helpers; } });
25
25
  Object.defineProperty(exports, "setApiBaseUrl", { enumerable: true, get: function () { return helpers_1.setApiBaseUrl; } });
26
+ // Export constants
27
+ var constants_1 = require("./lib/constants");
28
+ Object.defineProperty(exports, "CREDENTIAL_ERROR_CODES", { enumerable: true, get: function () { return constants_1.CREDENTIAL_ERROR_CODES; } });
29
+ Object.defineProperty(exports, "CROSS_REGION_ERROR_CODES", { enumerable: true, get: function () { return constants_1.CROSS_REGION_ERROR_CODES; } });
30
+ Object.defineProperty(exports, "isCredentialError", { enumerable: true, get: function () { return constants_1.isCredentialError; } });
31
+ Object.defineProperty(exports, "isCrossRegionError", { enumerable: true, get: function () { return constants_1.isCrossRegionError; } });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * VeSync API Error Codes
3
+ */
4
+ export declare const CREDENTIAL_ERROR_CODES: number[];
5
+ export declare const CROSS_REGION_ERROR_CODES: number[];
6
+ export declare const APP_VERSION_ERROR_CODES: number[];
7
+ /**
8
+ * Check if an error code indicates bad credentials
9
+ */
10
+ export declare function isCredentialError(code: number): boolean;
11
+ /**
12
+ * Check if an error code indicates cross-region issue
13
+ */
14
+ export declare function isCrossRegionError(code: number): boolean;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * VeSync API Error Codes
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.APP_VERSION_ERROR_CODES = exports.CROSS_REGION_ERROR_CODES = exports.CREDENTIAL_ERROR_CODES = void 0;
7
+ exports.isCredentialError = isCredentialError;
8
+ exports.isCrossRegionError = isCrossRegionError;
9
+ // Authentication error codes that indicate bad credentials (no point retrying)
10
+ exports.CREDENTIAL_ERROR_CODES = [
11
+ -11201129, // account or password incorrect
12
+ -11202129, // the account does not exist
13
+ -11000129, // illegal argument (empty credentials)
14
+ ];
15
+ // Cross-region error codes that require region switching
16
+ exports.CROSS_REGION_ERROR_CODES = [
17
+ -11260022, // cross region error
18
+ -11261022, // access region conflict error
19
+ ];
20
+ // Other error codes
21
+ exports.APP_VERSION_ERROR_CODES = [
22
+ -11012022, // app version is too low
23
+ ];
24
+ /**
25
+ * Check if an error code indicates bad credentials
26
+ */
27
+ function isCredentialError(code) {
28
+ return exports.CREDENTIAL_ERROR_CODES.includes(code);
29
+ }
30
+ /**
31
+ * Check if an error code indicates cross-region issue
32
+ */
33
+ function isCrossRegionError(code) {
34
+ return exports.CROSS_REGION_ERROR_CODES.includes(code);
35
+ }
@@ -26,7 +26,6 @@ export declare const REGION_ENDPOINTS: {
26
26
  JP: string;
27
27
  EU: string;
28
28
  };
29
- export declare const CROSS_REGION_ERROR_CODES: number[];
30
29
  export declare const APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
31
30
  export declare const AUTH_ERROR_CODES: {
32
31
  ACCOUNT_PASSWORD_INCORRECT: number;
@@ -34,6 +33,15 @@ export declare const AUTH_ERROR_CODES: {
34
33
  CROSS_REGION: number;
35
34
  CROSS_REGION_ALT: number;
36
35
  };
36
+ /**
37
+ * Get list of country codes that use the US endpoint
38
+ * These are tried when we get a cross-region error
39
+ */
40
+ export declare function getUSEndpointCountryCodes(): string[];
41
+ /**
42
+ * Get the appropriate API endpoint based on country code
43
+ */
44
+ export declare function getEndpointForCountryCode(countryCode: string): 'US' | 'EU';
37
45
  /**
38
46
  * Detect user's home region from email domain or country hints
39
47
  */
@@ -159,7 +167,7 @@ export declare class Helpers {
159
167
  /**
160
168
  * Perform new two-step authentication flow
161
169
  */
162
- static authNewFlow(manager: VeSync, appId: string, region?: string): Promise<[boolean, string | null, string | null, string | null]>;
170
+ static authNewFlow(manager: VeSync, appId: string, region?: string, countryCodeOverride?: string): Promise<[boolean, string | null, string | null, string | null]>;
163
171
  /**
164
172
  * Perform legacy authentication (fallback)
165
173
  */
@@ -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.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;
9
+ exports.Helpers = exports.AUTH_ERROR_CODES = exports.APP_VERSION_TOO_LOW_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;
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.getUSEndpointCountryCodes = getUSEndpointCountryCodes;
16
+ exports.getEndpointForCountryCode = getEndpointForCountryCode;
15
17
  exports.detectUserRegion = detectUserRegion;
16
18
  exports.getCountryCodeFromRegion = getCountryCodeFromRegion;
17
19
  exports.generateAppId = generateAppId;
@@ -19,6 +21,7 @@ exports.generateTerminalId = generateTerminalId;
19
21
  const axios_1 = __importDefault(require("axios"));
20
22
  const crypto_1 = __importDefault(require("crypto"));
21
23
  const logger_1 = require("./logger");
24
+ const constants_1 = require("./constants");
22
25
  // API configuration - Support regional endpoints
23
26
  let _apiBaseUrl = 'https://smartapi.vesync.com';
24
27
  let _currentRegion = 'US';
@@ -72,7 +75,7 @@ exports.REGION_ENDPOINTS = {
72
75
  'EU': 'https://smartapi.vesync.eu'
73
76
  };
74
77
  // Cross-region error codes - multiple versions exist
75
- exports.CROSS_REGION_ERROR_CODES = [-11260022, -11261022];
78
+ // CROSS_REGION_ERROR_CODES moved to constants.ts
76
79
  exports.APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
77
80
  // Authentication error codes
78
81
  exports.AUTH_ERROR_CODES = {
@@ -81,6 +84,38 @@ exports.AUTH_ERROR_CODES = {
81
84
  CROSS_REGION: -11260022,
82
85
  CROSS_REGION_ALT: -11261022
83
86
  };
87
+ /**
88
+ * Get list of country codes that use the US endpoint
89
+ * These are tried when we get a cross-region error
90
+ */
91
+ function getUSEndpointCountryCodes() {
92
+ // Countries that use the US endpoint but might need their own country code
93
+ return ['US', 'AU', 'NZ', 'JP', 'CA', 'MX', 'SG'];
94
+ }
95
+ /**
96
+ * Get the appropriate API endpoint based on country code
97
+ */
98
+ function getEndpointForCountryCode(countryCode) {
99
+ // European country codes that should use the EU endpoint
100
+ const euCountryCodes = [
101
+ // Western Europe
102
+ 'GB', 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH', 'IE', 'PT', 'LU',
103
+ // Nordic countries
104
+ 'SE', 'NO', 'DK', 'FI', 'IS',
105
+ // Eastern Europe
106
+ 'PL', 'CZ', 'HU', 'RO', 'BG', 'HR', 'SI', 'SK', 'LT', 'LV', 'EE',
107
+ // Southern Europe
108
+ 'GR', 'CY', 'MT',
109
+ // Other European
110
+ 'AL', 'BA', 'RS', 'ME', 'MK', 'MD', 'UA', 'BY'
111
+ ];
112
+ // Check if country code is in EU list
113
+ if (euCountryCodes.includes(countryCode)) {
114
+ return 'EU';
115
+ }
116
+ // Everything else uses US endpoint (including AU, NZ, JP, CA, MX, etc.)
117
+ return 'US';
118
+ }
84
119
  /**
85
120
  * Detect user's home region from email domain or country hints
86
121
  */
@@ -455,7 +490,7 @@ class Helpers {
455
490
  /**
456
491
  * Perform new two-step authentication flow
457
492
  */
458
- static async authNewFlow(manager, appId, region = 'US') {
493
+ static async authNewFlow(manager, appId, region = 'US', countryCodeOverride) {
459
494
  try {
460
495
  // Generate terminal ID to be used in both steps
461
496
  const terminalId = generateTerminalId();
@@ -480,8 +515,14 @@ class Helpers {
480
515
  }
481
516
  if (authResponse.code !== 0) {
482
517
  logger_1.logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg);
518
+ // Check if it's a credential error (no point retrying)
519
+ if ((0, constants_1.isCredentialError)(authResponse.code)) {
520
+ logger_1.logger.error('Credential error detected - invalid username or password');
521
+ setApiBaseUrl(originalUrl); // Restore original URL
522
+ return [false, null, null, 'credential_error'];
523
+ }
483
524
  // Handle cross-region error (multiple error codes possible)
484
- if (exports.CROSS_REGION_ERROR_CODES.includes(authResponse.code)) {
525
+ if ((0, constants_1.isCrossRegionError)(authResponse.code)) {
485
526
  logger_1.logger.debug('Cross-region error detected:', authResponse.code, 'will try different region');
486
527
  setApiBaseUrl(originalUrl); // Restore original URL
487
528
  return [false, null, null, 'cross_region'];
@@ -497,61 +538,46 @@ class Helpers {
497
538
  }
498
539
  logger_1.logger.debug('Step 1 successful, got authorization code');
499
540
  // Step 2: Login with authorization code
500
- // Use the appropriate country code based on the region we're authenticating with
501
- const countryCodeForStep2 = region === 'EU' ? 'DE' : 'US';
541
+ // Use the override if provided, otherwise default to 'US'
542
+ // Users should specify their actual country code in the configuration
543
+ const countryCodeForStep2 = countryCodeOverride || 'US';
502
544
  const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, countryCodeForStep2);
503
545
  logger_1.logger.debug('Step 2: Logging in with authorization code...');
504
546
  const [loginResponse, loginStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', step2Body, step1Headers, manager);
547
+ logger_1.logger.debug('Step 2 response:', {
548
+ status: loginStatus,
549
+ code: loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.code,
550
+ hasResult: !!(loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.result),
551
+ msg: loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.msg
552
+ });
505
553
  if (!loginResponse || loginStatus !== 200) {
506
554
  logger_1.logger.error('Step 2 failed:', loginResponse);
507
555
  setApiBaseUrl(originalUrl); // Restore original URL on failure
508
556
  return [false, null, null, null];
509
557
  }
510
558
  if (loginResponse.code !== 0) {
511
- // Handle cross-region error with retry (like pyvesync PR #340)
512
- if (exports.CROSS_REGION_ERROR_CODES.includes(loginResponse.code)) {
513
- logger_1.logger.debug('Cross-region error detected in Step 2, retrying with region change token...');
514
- const { bizToken: regionChangeToken, countryCode: newCountryCode } = loginResponse.result || {};
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
518
- const retryBody = {
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)}`,
532
- bizToken: regionChangeToken,
533
- regionChange: 'last_region'
534
- };
535
- logger_1.logger.debug('Retrying Step 2 with region change token...', {
536
- newCountryCode,
537
- currentEndpoint: getApiBaseUrl()
538
- });
539
- const [retryResponse, retryStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', retryBody, step1Headers, manager);
540
- if (retryResponse && retryResponse.code === 0) {
541
- const { token, accountID, countryCode } = retryResponse.result || {};
542
- if (token && accountID) {
543
- logger_1.logger.debug('Step 2 retry successful with region change');
544
- setApiBaseUrl(originalUrl); // Restore original URL on success
545
- return [true, token, accountID, countryCode || newCountryCode];
546
- }
547
- }
548
- logger_1.logger.error('Step 2 retry failed:', retryResponse);
549
- setApiBaseUrl(originalUrl); // Restore original URL on failure
550
- return [false, null, null, null];
551
- }
552
- else {
553
- logger_1.logger.error('No region change token in cross-region error response');
554
- }
559
+ logger_1.logger.debug('Step 2 error, code:', loginResponse.code, 'checking if cross-region...');
560
+ // Handle cross-region error - for EU accounts, we need to switch regions entirely
561
+ if (constants_1.CROSS_REGION_ERROR_CODES.includes(loginResponse.code)) {
562
+ // Log helpful message about country code mismatch
563
+ logger_1.logger.warn('═══════════════════════════════════════════════════════════════');
564
+ logger_1.logger.warn('COUNTRY CODE MISMATCH DETECTED');
565
+ logger_1.logger.warn(`Your account expects a different country code than '${countryCodeForStep2}'`);
566
+ logger_1.logger.warn('This usually means:');
567
+ logger_1.logger.warn(' 1. You need to set the correct country code in your config');
568
+ logger_1.logger.warn(' 2. Your account was created in a different country/region');
569
+ logger_1.logger.warn('');
570
+ logger_1.logger.warn('To fix this:');
571
+ logger_1.logger.warn(' - In Homebridge UI: Select your correct country from the dropdown');
572
+ logger_1.logger.warn(' - In config.json: Add "countryCode": "YOUR_COUNTRY_CODE"');
573
+ logger_1.logger.warn('');
574
+ logger_1.logger.warn('Common country codes: US, CA, GB, DE, FR, AU, NZ, JP');
575
+ logger_1.logger.warn('═══════════════════════════════════════════════════════════════');
576
+ logger_1.logger.debug('Cross-region error detected in Step 2 - will try different endpoint');
577
+ // For cross-region errors, we need to retry the entire flow with a different region
578
+ // The authorization code from Step 1 is tied to the endpoint it was obtained from
579
+ setApiBaseUrl(originalUrl); // Restore original URL
580
+ return [false, null, null, 'cross_region_retry'];
555
581
  }
556
582
  logger_1.logger.error('Step 2 error code:', loginResponse.code, 'message:', loginResponse.msg);
557
583
  setApiBaseUrl(originalUrl); // Restore original URL on error
@@ -588,6 +614,11 @@ class Helpers {
588
614
  }
589
615
  if (response.code && response.code !== 0) {
590
616
  logger_1.logger.error('Legacy login error code:', response.code, 'message:', response.msg);
617
+ // Check if it's a credential error
618
+ if ((0, constants_1.isCredentialError)(response.code)) {
619
+ logger_1.logger.error('Legacy auth: Credential error detected');
620
+ return [false, null, null, 'credential_error'];
621
+ }
591
622
  return [false, null, null, null];
592
623
  }
593
624
  const { token, accountID, countryCode } = response.result || {};
@@ -27,6 +27,7 @@ export declare class VeSync {
27
27
  private _excludeConfig;
28
28
  private _appId;
29
29
  private _region;
30
+ private _countryCodeOverride;
30
31
  username: string;
31
32
  password: string;
32
33
  token: string | null;
@@ -61,6 +62,7 @@ export declare class VeSync {
61
62
  customLogger?: Logger;
62
63
  excludeConfig?: ExcludeConfig;
63
64
  region?: string;
65
+ countryCode?: string;
64
66
  debugMode?: boolean;
65
67
  }, redact?: boolean, apiUrl?: string, customLogger?: Logger, excludeConfig?: ExcludeConfig);
66
68
  /**
@@ -141,6 +141,7 @@ class VeSync {
141
141
  this._redact = options.redact !== undefined ? options.redact : true;
142
142
  this._excludeConfig = options.excludeConfig || null;
143
143
  this._region = options.region || 'US';
144
+ this._countryCodeOverride = options.countryCode || null;
144
145
  customLogger = options.customLogger;
145
146
  apiUrl = options.apiUrl;
146
147
  }
@@ -150,6 +151,7 @@ class VeSync {
150
151
  this._redact = redact;
151
152
  this._excludeConfig = excludeConfig || null;
152
153
  this._region = 'US';
154
+ this._countryCodeOverride = null;
153
155
  }
154
156
  this._energyUpdateInterval = DEFAULT_ENERGY_UPDATE_INTERVAL;
155
157
  this._energyCheck = true;
@@ -195,6 +197,12 @@ class VeSync {
195
197
  (0, helpers_1.setApiBaseUrl)(apiUrl);
196
198
  }
197
199
  else {
200
+ // If country code is provided, use it to determine the endpoint
201
+ if (this._countryCodeOverride) {
202
+ const endpointRegion = (0, helpers_1.getEndpointForCountryCode)(this._countryCodeOverride);
203
+ this._region = endpointRegion;
204
+ logger_1.logger.debug(`Country code ${this._countryCodeOverride} maps to ${endpointRegion} endpoint`);
205
+ }
198
206
  // Set endpoint based on region
199
207
  const regionEndpoints = {
200
208
  'US': 'https://smartapi.vesync.com',
@@ -458,32 +466,82 @@ class VeSync {
458
466
  appId: this._appId,
459
467
  region: this._region
460
468
  });
469
+ // Track which regions have been attempted to prevent infinite loops
470
+ const triedRegions = new Set();
461
471
  for (let attempt = 0; attempt < retryAttempts; attempt++) {
462
472
  try {
463
473
  logger_1.logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
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
- }
474
+ // Skip email-based region detection for now since we'll handle cross-region errors
475
+ // The cross-region error handling below will automatically switch to the correct region
471
476
  // Try new authentication flow first with detected region
472
- let [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, this._region);
477
+ let [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, this._region, this._countryCodeOverride || undefined);
473
478
  // Track if new flow succeeded
474
479
  if (success) {
475
480
  this.authFlowUsed = 'new';
476
481
  }
477
482
  if (!success) {
483
+ // Check if it's a credential error (no point retrying)
484
+ if (countryCode === 'credential_error') {
485
+ logger_1.logger.error('Authentication failed due to invalid credentials');
486
+ return false; // Exit immediately, don't retry
487
+ }
478
488
  // Check if we need to try a different region
479
- if (countryCode === 'cross_region') {
489
+ if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
490
+ // Mark current region as tried
491
+ triedRegions.add(this._region);
480
492
  logger_1.logger.debug('Cross-region error detected, trying opposite region...');
481
493
  const alternateRegion = this._region === 'US' ? 'EU' : 'US';
494
+ // Check if we've already tried this region to prevent infinite loops
495
+ if (triedRegions.has(alternateRegion)) {
496
+ logger_1.logger.error('═══════════════════════════════════════════════════════════════');
497
+ logger_1.logger.error('AUTHENTICATION FAILED: COUNTRY CODE REQUIRED');
498
+ logger_1.logger.error('');
499
+ logger_1.logger.error('Both US and EU endpoints rejected your account.');
500
+ logger_1.logger.error('This means you MUST specify your country code.');
501
+ logger_1.logger.error('');
502
+ logger_1.logger.error('SOLUTION:');
503
+ logger_1.logger.error('1. In Homebridge UI:');
504
+ logger_1.logger.error(' - Go to plugin settings');
505
+ logger_1.logger.error(' - Select your country from the "Country Code" dropdown');
506
+ logger_1.logger.error('');
507
+ logger_1.logger.error('2. In config.json:');
508
+ logger_1.logger.error(' Add: "countryCode": "YOUR_COUNTRY_CODE"');
509
+ logger_1.logger.error('');
510
+ logger_1.logger.error('Common non-US/EU country codes:');
511
+ logger_1.logger.error(' 🇦🇺 Australia: "AU"');
512
+ logger_1.logger.error(' 🇳🇿 New Zealand: "NZ"');
513
+ logger_1.logger.error(' 🇯🇵 Japan: "JP"');
514
+ logger_1.logger.error(' 🇨🇦 Canada: "CA"');
515
+ logger_1.logger.error(' 🇸🇬 Singapore: "SG"');
516
+ logger_1.logger.error(' 🇲🇽 Mexico: "MX"');
517
+ logger_1.logger.error('═══════════════════════════════════════════════════════════════');
518
+ return false;
519
+ }
520
+ logger_1.logger.debug(`Switching from ${this._region} to ${alternateRegion} region`);
482
521
  this._region = alternateRegion;
483
522
  (0, helpers_1.setCurrentRegion)(alternateRegion);
484
- [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, alternateRegion);
523
+ // Update the API endpoint for the new region
524
+ const regionEndpoints = {
525
+ 'US': 'https://smartapi.vesync.com',
526
+ 'EU': 'https://smartapi.vesync.eu'
527
+ };
528
+ if (alternateRegion in regionEndpoints) {
529
+ (0, helpers_1.setApiBaseUrl)(regionEndpoints[alternateRegion]);
530
+ logger_1.logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
531
+ }
532
+ // Retry with the alternate region
533
+ [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, alternateRegion, this._countryCodeOverride || undefined);
485
534
  if (success) {
486
535
  this.authFlowUsed = 'new';
536
+ logger_1.logger.debug(`Authentication successful with ${alternateRegion} region`);
537
+ }
538
+ else if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
539
+ // Both regions failed with cross-region error
540
+ logger_1.logger.error('Authentication failed: Account rejected by both US and EU regions');
541
+ logger_1.logger.error('This may indicate your account was created in a region not yet supported');
542
+ logger_1.logger.error('Please contact VeSync support to determine your account region');
543
+ // Don't retry further attempts as we've exhausted both regions
544
+ return false;
487
545
  }
488
546
  }
489
547
  // If new flow still fails, try legacy authentication as fallback
@@ -493,6 +551,11 @@ class VeSync {
493
551
  if (success) {
494
552
  this.authFlowUsed = 'legacy';
495
553
  }
554
+ else if (countryCode === 'credential_error') {
555
+ // Legacy auth also detected bad credentials
556
+ logger_1.logger.error('Both authentication methods report invalid credentials');
557
+ return false; // Exit immediately
558
+ }
496
559
  }
497
560
  }
498
561
  if (success && token && accountId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
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",
package/src/index.ts CHANGED
@@ -11,4 +11,12 @@ export { VeSyncWallSwitch } from './lib/vesyncSwitchImpl';
11
11
  export { VeSyncBaseDevice } from './lib/vesyncBaseDevice';
12
12
 
13
13
  // Export helper functions
14
- export { Helpers, setApiBaseUrl } from './lib/helpers';
14
+ export { Helpers, setApiBaseUrl } from './lib/helpers';
15
+
16
+ // Export constants
17
+ export {
18
+ CREDENTIAL_ERROR_CODES,
19
+ CROSS_REGION_ERROR_CODES,
20
+ isCredentialError,
21
+ isCrossRegionError
22
+ } from './lib/constants';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * VeSync API Error Codes
3
+ */
4
+
5
+ // Authentication error codes that indicate bad credentials (no point retrying)
6
+ export const CREDENTIAL_ERROR_CODES = [
7
+ -11201129, // account or password incorrect
8
+ -11202129, // the account does not exist
9
+ -11000129, // illegal argument (empty credentials)
10
+ ];
11
+
12
+ // Cross-region error codes that require region switching
13
+ export const CROSS_REGION_ERROR_CODES = [
14
+ -11260022, // cross region error
15
+ -11261022, // access region conflict error
16
+ ];
17
+
18
+ // Other error codes
19
+ export const APP_VERSION_ERROR_CODES = [
20
+ -11012022, // app version is too low
21
+ ];
22
+
23
+ /**
24
+ * Check if an error code indicates bad credentials
25
+ */
26
+ export function isCredentialError(code: number): boolean {
27
+ return CREDENTIAL_ERROR_CODES.includes(code);
28
+ }
29
+
30
+ /**
31
+ * Check if an error code indicates cross-region issue
32
+ */
33
+ export function isCrossRegionError(code: number): boolean {
34
+ return CROSS_REGION_ERROR_CODES.includes(code);
35
+ }
@@ -6,6 +6,7 @@ import axios from 'axios';
6
6
  import crypto from 'crypto';
7
7
  import { VeSync } from './vesync';
8
8
  import { logger } from './logger';
9
+ import { isCredentialError, isCrossRegionError, CROSS_REGION_ERROR_CODES } from './constants';
9
10
 
10
11
  // API configuration - Support regional endpoints
11
12
  let _apiBaseUrl = 'https://smartapi.vesync.com';
@@ -72,7 +73,7 @@ export const REGION_ENDPOINTS = {
72
73
  };
73
74
 
74
75
  // Cross-region error codes - multiple versions exist
75
- export const CROSS_REGION_ERROR_CODES = [-11260022, -11261022];
76
+ // CROSS_REGION_ERROR_CODES moved to constants.ts
76
77
  export const APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
77
78
 
78
79
  // Authentication error codes
@@ -83,6 +84,42 @@ export const AUTH_ERROR_CODES = {
83
84
  CROSS_REGION_ALT: -11261022
84
85
  };
85
86
 
87
+ /**
88
+ * Get list of country codes that use the US endpoint
89
+ * These are tried when we get a cross-region error
90
+ */
91
+ export function getUSEndpointCountryCodes(): string[] {
92
+ // Countries that use the US endpoint but might need their own country code
93
+ return ['US', 'AU', 'NZ', 'JP', 'CA', 'MX', 'SG'];
94
+ }
95
+
96
+ /**
97
+ * Get the appropriate API endpoint based on country code
98
+ */
99
+ export function getEndpointForCountryCode(countryCode: string): 'US' | 'EU' {
100
+ // European country codes that should use the EU endpoint
101
+ const euCountryCodes = [
102
+ // Western Europe
103
+ 'GB', 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH', 'IE', 'PT', 'LU',
104
+ // Nordic countries
105
+ 'SE', 'NO', 'DK', 'FI', 'IS',
106
+ // Eastern Europe
107
+ 'PL', 'CZ', 'HU', 'RO', 'BG', 'HR', 'SI', 'SK', 'LT', 'LV', 'EE',
108
+ // Southern Europe
109
+ 'GR', 'CY', 'MT',
110
+ // Other European
111
+ 'AL', 'BA', 'RS', 'ME', 'MK', 'MD', 'UA', 'BY'
112
+ ];
113
+
114
+ // Check if country code is in EU list
115
+ if (euCountryCodes.includes(countryCode)) {
116
+ return 'EU';
117
+ }
118
+
119
+ // Everything else uses US endpoint (including AU, NZ, JP, CA, MX, etc.)
120
+ return 'US';
121
+ }
122
+
86
123
  /**
87
124
  * Detect user's home region from email domain or country hints
88
125
  */
@@ -556,7 +593,7 @@ export class Helpers {
556
593
  /**
557
594
  * Perform new two-step authentication flow
558
595
  */
559
- static async authNewFlow(manager: VeSync, appId: string, region: string = 'US'): Promise<[boolean, string | null, string | null, string | null]> {
596
+ static async authNewFlow(manager: VeSync, appId: string, region: string = 'US', countryCodeOverride?: string): Promise<[boolean, string | null, string | null, string | null]> {
560
597
  try {
561
598
  // Generate terminal ID to be used in both steps
562
599
  const terminalId = generateTerminalId();
@@ -594,8 +631,15 @@ export class Helpers {
594
631
  if (authResponse.code !== 0) {
595
632
  logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg);
596
633
 
634
+ // Check if it's a credential error (no point retrying)
635
+ if (isCredentialError(authResponse.code)) {
636
+ logger.error('Credential error detected - invalid username or password');
637
+ setApiBaseUrl(originalUrl); // Restore original URL
638
+ return [false, null, null, 'credential_error'];
639
+ }
640
+
597
641
  // Handle cross-region error (multiple error codes possible)
598
- if (CROSS_REGION_ERROR_CODES.includes(authResponse.code)) {
642
+ if (isCrossRegionError(authResponse.code)) {
599
643
  logger.debug('Cross-region error detected:', authResponse.code, 'will try different region');
600
644
  setApiBaseUrl(originalUrl); // Restore original URL
601
645
  return [false, null, null, 'cross_region'];
@@ -616,8 +660,9 @@ export class Helpers {
616
660
  logger.debug('Step 1 successful, got authorization code');
617
661
 
618
662
  // Step 2: Login with authorization code
619
- // Use the appropriate country code based on the region we're authenticating with
620
- const countryCodeForStep2 = region === 'EU' ? 'DE' : 'US';
663
+ // Use the override if provided, otherwise default to 'US'
664
+ // Users should specify their actual country code in the configuration
665
+ const countryCodeForStep2 = countryCodeOverride || 'US';
621
666
  const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, countryCodeForStep2);
622
667
 
623
668
  logger.debug('Step 2: Logging in with authorization code...');
@@ -630,6 +675,13 @@ export class Helpers {
630
675
  manager
631
676
  );
632
677
 
678
+ logger.debug('Step 2 response:', {
679
+ status: loginStatus,
680
+ code: loginResponse?.code,
681
+ hasResult: !!loginResponse?.result,
682
+ msg: loginResponse?.msg
683
+ });
684
+
633
685
  if (!loginResponse || loginStatus !== 200) {
634
686
  logger.error('Step 2 failed:', loginResponse);
635
687
  setApiBaseUrl(originalUrl); // Restore original URL on failure
@@ -637,61 +689,29 @@ export class Helpers {
637
689
  }
638
690
 
639
691
  if (loginResponse.code !== 0) {
640
- // Handle cross-region error with retry (like pyvesync PR #340)
692
+ logger.debug('Step 2 error, code:', loginResponse.code, 'checking if cross-region...');
693
+ // Handle cross-region error - for EU accounts, we need to switch regions entirely
641
694
  if (CROSS_REGION_ERROR_CODES.includes(loginResponse.code)) {
642
- logger.debug('Cross-region error detected in Step 2, retrying with region change token...');
695
+ // Log helpful message about country code mismatch
696
+ logger.warn('═══════════════════════════════════════════════════════════════');
697
+ logger.warn('COUNTRY CODE MISMATCH DETECTED');
698
+ logger.warn(`Your account expects a different country code than '${countryCodeForStep2}'`);
699
+ logger.warn('This usually means:');
700
+ logger.warn(' 1. You need to set the correct country code in your config');
701
+ logger.warn(' 2. Your account was created in a different country/region');
702
+ logger.warn('');
703
+ logger.warn('To fix this:');
704
+ logger.warn(' - In Homebridge UI: Select your correct country from the dropdown');
705
+ logger.warn(' - In config.json: Add "countryCode": "YOUR_COUNTRY_CODE"');
706
+ logger.warn('');
707
+ logger.warn('Common country codes: US, CA, GB, DE, FR, AU, NZ, JP');
708
+ logger.warn('═══════════════════════════════════════════════════════════════');
643
709
 
644
- const { bizToken: regionChangeToken, countryCode: newCountryCode } = loginResponse.result || {};
645
-
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
649
- const retryBody = {
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)}`,
663
- bizToken: regionChangeToken,
664
- regionChange: 'last_region'
665
- };
666
-
667
- logger.debug('Retrying Step 2 with region change token...', {
668
- newCountryCode,
669
- currentEndpoint: getApiBaseUrl()
670
- });
671
-
672
- const [retryResponse, retryStatus] = await this.callApi(
673
- '/user/api/accountManage/v1/loginByAuthorizeCode4Vesync',
674
- 'post',
675
- retryBody,
676
- step1Headers,
677
- manager
678
- );
679
-
680
- if (retryResponse && retryResponse.code === 0) {
681
- const { token, accountID, countryCode } = retryResponse.result || {};
682
- if (token && accountID) {
683
- logger.debug('Step 2 retry successful with region change');
684
- setApiBaseUrl(originalUrl); // Restore original URL on success
685
- return [true, token, accountID, countryCode || newCountryCode];
686
- }
687
- }
688
-
689
- logger.error('Step 2 retry failed:', retryResponse);
690
- setApiBaseUrl(originalUrl); // Restore original URL on failure
691
- return [false, null, null, null];
692
- } else {
693
- logger.error('No region change token in cross-region error response');
694
- }
710
+ logger.debug('Cross-region error detected in Step 2 - will try different endpoint');
711
+ // For cross-region errors, we need to retry the entire flow with a different region
712
+ // The authorization code from Step 1 is tied to the endpoint it was obtained from
713
+ setApiBaseUrl(originalUrl); // Restore original URL
714
+ return [false, null, null, 'cross_region_retry'];
695
715
  }
696
716
 
697
717
  logger.error('Step 2 error code:', loginResponse.code, 'message:', loginResponse.msg);
@@ -744,6 +764,13 @@ export class Helpers {
744
764
 
745
765
  if (response.code && response.code !== 0) {
746
766
  logger.error('Legacy login error code:', response.code, 'message:', response.msg);
767
+
768
+ // Check if it's a credential error
769
+ if (isCredentialError(response.code)) {
770
+ logger.error('Legacy auth: Credential error detected');
771
+ return [false, null, null, 'credential_error'];
772
+ }
773
+
747
774
  return [false, null, null, null];
748
775
  }
749
776
 
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, getCountryCodeFromRegion } from './helpers';
5
+ import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, setApiBaseUrl, getApiBaseUrl, generateAppId, getRegionFromCountryCode, setCurrentRegion, getCurrentRegion, REGION_ENDPOINTS, getCountryCodeFromRegion, getEndpointForCountryCode } from './helpers';
6
6
  import { VeSyncBaseDevice } from './vesyncBaseDevice';
7
7
  import { fanModules } from './vesyncFanImpl';
8
8
  import { outletModules } from './vesyncOutletImpl';
@@ -161,6 +161,7 @@ export class VeSync {
161
161
  private _excludeConfig: ExcludeConfig | null;
162
162
  private _appId: string;
163
163
  private _region: string;
164
+ private _countryCodeOverride: string | null;
164
165
 
165
166
  username: string;
166
167
  password: string;
@@ -202,6 +203,7 @@ export class VeSync {
202
203
  customLogger?: Logger;
203
204
  excludeConfig?: ExcludeConfig;
204
205
  region?: string;
206
+ countryCode?: string; // Override country code for Step 2 authentication
205
207
  debugMode?: boolean; // Alias for debug
206
208
  } = false,
207
209
  redact = true,
@@ -218,6 +220,7 @@ export class VeSync {
218
220
  this._redact = options.redact !== undefined ? options.redact : true;
219
221
  this._excludeConfig = options.excludeConfig || null;
220
222
  this._region = options.region || 'US';
223
+ this._countryCodeOverride = options.countryCode || null;
221
224
  customLogger = options.customLogger;
222
225
  apiUrl = options.apiUrl;
223
226
  } else {
@@ -226,6 +229,7 @@ export class VeSync {
226
229
  this._redact = redact;
227
230
  this._excludeConfig = excludeConfig || null;
228
231
  this._region = 'US';
232
+ this._countryCodeOverride = null;
229
233
  }
230
234
 
231
235
  this._energyUpdateInterval = DEFAULT_ENERGY_UPDATE_INTERVAL;
@@ -274,6 +278,13 @@ export class VeSync {
274
278
  if (apiUrl) {
275
279
  setApiBaseUrl(apiUrl);
276
280
  } else {
281
+ // If country code is provided, use it to determine the endpoint
282
+ if (this._countryCodeOverride) {
283
+ const endpointRegion = getEndpointForCountryCode(this._countryCodeOverride);
284
+ this._region = endpointRegion;
285
+ logger.debug(`Country code ${this._countryCodeOverride} maps to ${endpointRegion} endpoint`);
286
+ }
287
+
277
288
  // Set endpoint based on region
278
289
  const regionEndpoints: Record<string, string> = {
279
290
  'US': 'https://smartapi.vesync.com',
@@ -571,20 +582,18 @@ export class VeSync {
571
582
  region: this._region
572
583
  });
573
584
 
585
+ // Track which regions have been attempted to prevent infinite loops
586
+ const triedRegions = new Set<string>();
587
+
574
588
  for (let attempt = 0; attempt < retryAttempts; attempt++) {
575
589
  try {
576
590
  logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
577
591
 
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
- }
592
+ // Skip email-based region detection for now since we'll handle cross-region errors
593
+ // The cross-region error handling below will automatically switch to the correct region
585
594
 
586
595
  // Try new authentication flow first with detected region
587
- let [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, this._region);
596
+ let [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, this._region, this._countryCodeOverride || undefined);
588
597
 
589
598
  // Track if new flow succeeded
590
599
  if (success) {
@@ -592,15 +601,74 @@ export class VeSync {
592
601
  }
593
602
 
594
603
  if (!success) {
604
+ // Check if it's a credential error (no point retrying)
605
+ if (countryCode === 'credential_error') {
606
+ logger.error('Authentication failed due to invalid credentials');
607
+ return false; // Exit immediately, don't retry
608
+ }
609
+
595
610
  // Check if we need to try a different region
596
- if (countryCode === 'cross_region') {
611
+ if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
612
+ // Mark current region as tried
613
+ triedRegions.add(this._region);
614
+
597
615
  logger.debug('Cross-region error detected, trying opposite region...');
598
616
  const alternateRegion = this._region === 'US' ? 'EU' : 'US';
617
+
618
+ // Check if we've already tried this region to prevent infinite loops
619
+ if (triedRegions.has(alternateRegion)) {
620
+ logger.error('═══════════════════════════════════════════════════════════════');
621
+ logger.error('AUTHENTICATION FAILED: COUNTRY CODE REQUIRED');
622
+ logger.error('');
623
+ logger.error('Both US and EU endpoints rejected your account.');
624
+ logger.error('This means you MUST specify your country code.');
625
+ logger.error('');
626
+ logger.error('SOLUTION:');
627
+ logger.error('1. In Homebridge UI:');
628
+ logger.error(' - Go to plugin settings');
629
+ logger.error(' - Select your country from the "Country Code" dropdown');
630
+ logger.error('');
631
+ logger.error('2. In config.json:');
632
+ logger.error(' Add: "countryCode": "YOUR_COUNTRY_CODE"');
633
+ logger.error('');
634
+ logger.error('Common non-US/EU country codes:');
635
+ logger.error(' 🇦🇺 Australia: "AU"');
636
+ logger.error(' 🇳🇿 New Zealand: "NZ"');
637
+ logger.error(' 🇯🇵 Japan: "JP"');
638
+ logger.error(' 🇨🇦 Canada: "CA"');
639
+ logger.error(' 🇸🇬 Singapore: "SG"');
640
+ logger.error(' 🇲🇽 Mexico: "MX"');
641
+ logger.error('═══════════════════════════════════════════════════════════════');
642
+ return false;
643
+ }
644
+
645
+ logger.debug(`Switching from ${this._region} to ${alternateRegion} region`);
646
+
599
647
  this._region = alternateRegion;
600
648
  setCurrentRegion(alternateRegion);
601
- [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, alternateRegion);
649
+
650
+ // Update the API endpoint for the new region
651
+ const regionEndpoints: Record<string, string> = {
652
+ 'US': 'https://smartapi.vesync.com',
653
+ 'EU': 'https://smartapi.vesync.eu'
654
+ };
655
+ if (alternateRegion in regionEndpoints) {
656
+ setApiBaseUrl(regionEndpoints[alternateRegion]);
657
+ logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
658
+ }
659
+
660
+ // Retry with the alternate region
661
+ [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, alternateRegion, this._countryCodeOverride || undefined);
602
662
  if (success) {
603
663
  this.authFlowUsed = 'new';
664
+ logger.debug(`Authentication successful with ${alternateRegion} region`);
665
+ } else if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
666
+ // Both regions failed with cross-region error
667
+ logger.error('Authentication failed: Account rejected by both US and EU regions');
668
+ logger.error('This may indicate your account was created in a region not yet supported');
669
+ logger.error('Please contact VeSync support to determine your account region');
670
+ // Don't retry further attempts as we've exhausted both regions
671
+ return false;
604
672
  }
605
673
  }
606
674
 
@@ -610,6 +678,10 @@ export class VeSync {
610
678
  [success, token, accountId, countryCode] = await Helpers.authLegacyFlow(this);
611
679
  if (success) {
612
680
  this.authFlowUsed = 'legacy';
681
+ } else if (countryCode === 'credential_error') {
682
+ // Legacy auth also detected bad credentials
683
+ logger.error('Both authentication methods report invalid credentials');
684
+ return false; // Exit immediately
613
685
  }
614
686
  }
615
687
  }