1- import { Buffer } from 'node:buffer' ;
2-
31import axios , { type AxiosError } from 'axios' ;
42
53import type { GoveeHTTPDeviceInfo , GoveeLogging , GoveePluginConfig , HTTPLoginResult } from '../types.js' ;
64import platformConsts from '../utils/constants.js' ;
75import { parseError , sleep } from '../utils/functions.js' ;
6+ import { GOVEE_API_URLS , goveeHeaders } from '../utils/govee-api.js' ;
87import platformLang from '../utils/lang-en.js' ;
98
109interface HTTPPlatformRef {
@@ -21,16 +20,16 @@ interface HTTPPlatformRef {
2120 } ;
2221}
2322
23+ const MAX_RETRIES = 3 ;
24+ const RETRY_DELAY_MS = 30000 ;
25+
2426export default class HTTPClient {
2527 private log : GoveeLogging ;
2628 private password : string ;
2729 private token ?: string ;
2830 private tokenTTR ?: string ;
2931 private username : string ;
30- private appVersion : string ;
31- private userAgent : string ;
3232 private clientId : string ;
33- private base64Tried = false ;
3433
3534 constructor ( platform : HTTPPlatformRef ) {
3635 this . log = platform . log ;
@@ -39,14 +38,15 @@ export default class HTTPClient {
3938 this . tokenTTR = platform . accountTokenTTR ;
4039 this . username = platform . config . username || '' ;
4140
42- this . appVersion = '5.6.01' ;
43- this . userAgent = `GoveeHome/${ this . appVersion } (com.ihoment.GoVeeSensor; build:2; iOS 16.5.0) Alamofire/5.6.4` ;
44-
4541 let clientSuffix = platform . api . hap . uuid . generate ( this . username ) . replace ( / - / g, '' ) ;
4642 clientSuffix = clientSuffix . substring ( 0 , clientSuffix . length - 2 ) ;
4743 this . clientId = `hb${ clientSuffix } ` ;
4844 }
4945
46+ private headers ( token ?: string ) : Record < string , string | number > {
47+ return goveeHeaders ( token || this . token ! , this . clientId ) ;
48+ }
49+
5050 /**
5151 * Set the token from cached credentials
5252 */
@@ -57,12 +57,12 @@ export default class HTTPClient {
5757 }
5858 }
5959
60- async login ( ) : Promise < HTTPLoginResult > {
60+ async login ( retryCount = 0 ) : Promise < HTTPLoginResult > {
6161 try {
6262 this . log . debug ( '[HTTP] Attempting login for user: %s' , this . username ) ;
6363
6464 const res = await axios ( {
65- url : 'https://app2.govee.com/account/rest/account/v1/ login' ,
65+ url : GOVEE_API_URLS . login ,
6666 method : 'post' ,
6767 data : {
6868 email : this . username ,
@@ -81,26 +81,13 @@ export default class HTTPClient {
8181
8282 if ( ! res . data . client || ! res . data . client . token ) {
8383 this . log . debug ( '[HTTP] Login response missing client/token. Message: %s' , res . data . message || 'none' ) ;
84- if ( res . data . message && res . data . message . replace ( / \s + / g, '' ) === 'Incorrectpassword' ) {
85- if ( this . base64Tried ) {
86- throw new Error ( res . data . message || platformLang . noToken ) ;
87- } else {
88- this . log . debug ( '[HTTP] Trying base64 decoded password' ) ;
89- this . base64Tried = true ;
90- this . password = Buffer . from ( this . password , 'base64' )
91- . toString ( 'utf8' )
92- . replace ( / \r \n | \n | \r / g, '' )
93- . trim ( ) ;
94- return await this . login ( ) ;
95- }
96- }
9784 throw new Error ( res . data . message || platformLang . noToken ) ;
9885 }
9986
10087 this . log . debug ( '[HTTP] Primary login successful, fetching TTR token...' ) ;
10188
10289 const ttrRes = await axios ( {
103- url : 'https://community-api.govee.com/os/v1/login' ,
90+ url : GOVEE_API_URLS . loginTTR ,
10491 method : 'post' ,
10592 data : {
10693 email : this . username ,
@@ -117,17 +104,9 @@ export default class HTTPClient {
117104 this . log . debug ( '[HTTP] Fetching IoT credentials...' ) ;
118105
119106 const iotRes = await axios ( {
120- url : 'https://app2.govee.com/app/v1/account/iot/key' ,
107+ url : GOVEE_API_URLS . iotKey ,
121108 method : 'get' ,
122- headers : {
123- 'Authorization' : `Bearer ${ this . token } ` ,
124- 'appVersion' : this . appVersion ,
125- 'clientId' : this . clientId ,
126- 'clientType' : 1 ,
127- 'iotVersion' : 0 ,
128- 'timestamp' : Date . now ( ) ,
129- 'User-Agent' : this . userAgent ,
130- } ,
109+ headers : this . headers ( ) ,
131110 } ) ;
132111
133112 this . log . debug ( '[HTTP] IoT credentials received. Endpoint: %s' , iotRes . data . data . endpoint ) ;
@@ -145,9 +124,13 @@ export default class HTTPClient {
145124 } catch ( err ) {
146125 const axiosErr = err as AxiosError ;
147126 if ( axiosErr . code && platformConsts . httpRetryCodes . includes ( axiosErr . code ) ) {
148- this . log . warn ( '[HTTP] %s [login() - %s].' , platformLang . httpRetry , axiosErr . code ) ;
149- await sleep ( 30000 ) ;
150- return this . login ( ) ;
127+ if ( retryCount >= MAX_RETRIES ) {
128+ this . log . warn ( '[HTTP] login() failed after %d retries [%s].' , MAX_RETRIES , axiosErr . code ) ;
129+ throw err ;
130+ }
131+ this . log . warn ( '[HTTP] %s [login() - %s] (attempt %d/%d).' , platformLang . httpRetry , axiosErr . code , retryCount + 1 , MAX_RETRIES ) ;
132+ await sleep ( RETRY_DELAY_MS ) ;
133+ return this . login ( retryCount + 1 ) ;
151134 }
152135 throw err ;
153136 }
@@ -156,24 +139,16 @@ export default class HTTPClient {
156139 async logout ( ) : Promise < void > {
157140 try {
158141 await axios ( {
159- url : 'https://app2.govee.com/account/rest/account/v1/ logout' ,
142+ url : GOVEE_API_URLS . logout ,
160143 method : 'post' ,
161- headers : {
162- 'Authorization' : `Bearer ${ this . token } ` ,
163- 'appVersion' : this . appVersion ,
164- 'clientId' : this . clientId ,
165- 'clientType' : 1 ,
166- 'iotVersion' : 0 ,
167- 'timestamp' : Date . now ( ) ,
168- 'User-Agent' : this . userAgent ,
169- } ,
144+ headers : this . headers ( ) ,
170145 } ) ;
171146 } catch ( err ) {
172147 this . log . warn ( '[HTTP] %s %s.' , platformLang . logoutFail , parseError ( err as Error ) ) ;
173148 }
174149 }
175150
176- async getDevices ( isSync = true ) : Promise < GoveeHTTPDeviceInfo [ ] > {
151+ async getDevices ( isSync = true , retryCount = 0 ) : Promise < GoveeHTTPDeviceInfo [ ] > {
177152 try {
178153 if ( ! this . token ) {
179154 this . log . debug ( '[HTTP] getDevices called but no token exists' ) ;
@@ -183,17 +158,9 @@ export default class HTTPClient {
183158 this . log . debug ( '[HTTP] Fetching device list...' ) ;
184159
185160 const res = await axios ( {
186- url : 'https://app2.govee.com/device/rest/ devices/v1/list' ,
161+ url : GOVEE_API_URLS . devices ,
187162 method : 'post' ,
188- headers : {
189- 'Authorization' : `Bearer ${ this . token } ` ,
190- 'appVersion' : this . appVersion ,
191- 'clientId' : this . clientId ,
192- 'clientType' : 1 ,
193- 'iotVersion' : 0 ,
194- 'timestamp' : Date . now ( ) ,
195- 'User-Agent' : this . userAgent ,
196- } ,
163+ headers : this . headers ( ) ,
197164 timeout : 30000 ,
198165 } ) ;
199166
@@ -217,54 +184,27 @@ export default class HTTPClient {
217184 } catch ( err ) {
218185 const axiosErr = err as AxiosError ;
219186 if ( ! isSync && axiosErr . code && platformConsts . httpRetryCodes . includes ( axiosErr . code ) ) {
220- this . log . warn ( '[HTTP] %s [getDevices() - %s].' , platformLang . httpRetry , axiosErr . code ) ;
221- await sleep ( 30000 ) ;
222- return this . getDevices ( ) ;
187+ if ( retryCount >= MAX_RETRIES ) {
188+ this . log . warn ( '[HTTP] getDevices() failed after %d retries [%s].' , MAX_RETRIES , axiosErr . code ) ;
189+ throw err ;
190+ }
191+ this . log . warn ( '[HTTP] %s [getDevices() - %s] (attempt %d/%d).' , platformLang . httpRetry , axiosErr . code , retryCount + 1 , MAX_RETRIES ) ;
192+ await sleep ( RETRY_DELAY_MS ) ;
193+ return this . getDevices ( isSync , retryCount + 1 ) ;
223194 }
224195 throw err ;
225196 }
226197 }
227198
228- async getTapToRuns ( ) : Promise < unknown [ ] > {
229- const res = await axios ( {
230- url : 'https://app2.govee.com/bff-app/v1/exec-plat/home' ,
231- method : 'get' ,
232- headers : {
233- 'Authorization' : `Bearer ${ this . tokenTTR } ` ,
234- 'appVersion' : this . appVersion ,
235- 'clientId' : this . clientId ,
236- 'clientType' : 1 ,
237- 'iotVersion' : 0 ,
238- 'timestamp' : Date . now ( ) ,
239- 'User-Agent' : this . userAgent ,
240- } ,
241- timeout : 10000 ,
242- } ) ;
243-
244- if ( ! res ?. data ?. data ?. components ) {
245- throw new Error ( 'not a valid response' ) ;
246- }
247-
248- return res . data . data . components ;
249- }
250-
251199 async getLeakDeviceWarning ( deviceId : string , deviceSku : string ) : Promise < unknown [ ] > {
252200 if ( ! this . token ) {
253201 throw new Error ( platformLang . noTokenExists ) ;
254202 }
255203
256204 const res = await axios ( {
257- url : 'https://app2.govee.com/leak/rest/device/v1/warnMessage' ,
205+ url : GOVEE_API_URLS . leakWarning ,
258206 method : 'post' ,
259- headers : {
260- 'Authorization' : `Bearer ${ this . token } ` ,
261- 'appVersion' : this . appVersion ,
262- 'clientId' : this . clientId ,
263- 'clientType' : 1 ,
264- 'iotVersion' : 0 ,
265- 'timestamp' : Date . now ( ) ,
266- 'User-Agent' : this . userAgent ,
267- } ,
207+ headers : this . headers ( ) ,
268208 data : {
269209 device : deviceId . replaceAll ( ':' , '' ) ,
270210 limit : 50 ,
0 commit comments