99from http .cookies import SimpleCookie
1010import json
1111import logging
12+ import time
1213from typing import Any , Generic , TypeVar
1314from urllib .parse import urlparse
1415
@@ -68,6 +69,8 @@ def __init__(
6869
6970 self .session = session
7071
72+ self .api_version : int = 8
73+
7174 self ._use_json_for_login_post = False
7275 self ._auth_cookie : str | None = None
7376 self ._csrf_id : str | None = None
@@ -202,11 +205,15 @@ def _get_authenticated_headers(
202205 headers ["Content-Type" ] = "application/x-www-form-urlencoded"
203206
204207 if self ._csrf_id : # pragma: no cover
205- _LOGGER .error ("TESTv6 - CSRF ID found %s" , self ._csrf_id )
208+ _LOGGER .error ("TESTv%s - CSRF ID found %s" , self . api_version , self ._csrf_id )
206209 headers ["X-CSRF-ID" ] = self ._csrf_id
207210
208211 if self ._auth_cookie : # pragma: no cover
209- _LOGGER .error ("TESTv6 - auth_cookie found: AIROS_%s" , self ._auth_cookie )
212+ _LOGGER .error (
213+ "TESTv%s - auth_cookie found: AIROS_%s" ,
214+ self .api_version ,
215+ self ._auth_cookie ,
216+ )
210217 headers ["Cookie" ] = f"AIROS_{ self ._auth_cookie } "
211218
212219 return headers
@@ -218,11 +225,16 @@ def _store_auth_data(self, response: aiohttp.ClientResponse) -> None:
218225 # Parse all Set-Cookie headers to ensure we don't miss AIROS_* cookie
219226 cookie = SimpleCookie ()
220227 for set_cookie in response .headers .getall ("Set-Cookie" , []):
221- _LOGGER .error ("TESTv6 - regular cookie handling: %s" , set_cookie )
228+ _LOGGER .error (
229+ "TESTv%s - regular cookie handling: %s" , self .api_version , set_cookie
230+ )
222231 cookie .load (set_cookie )
223232 for key , morsel in cookie .items ():
224233 _LOGGER .error (
225- "TESTv6 - AIROS_cookie handling: %s with %s" , key , morsel .value
234+ "TESTv%s - AIROS_cookie handling: %s with %s" ,
235+ self .api_version ,
236+ key ,
237+ morsel .value ,
226238 )
227239 if key .startswith ("AIROS_" ):
228240 self ._auth_cookie = morsel .key [6 :] + "=" + morsel .value
@@ -251,21 +263,36 @@ async def _request_json(
251263 request_headers .update (headers )
252264
253265 # Potential XM fix - not sure, might have been login issue
254- if url == self ._status_cgi_url :
255- request_headers ["Referrer" ] = f"{ self .base_url } /login.cgi"
266+ if self .api_version == 6 and url .startswith (self ._status_cgi_url ):
267+ # Modified from login.cgi to index.cgi
268+ request_headers ["Referrer" ] = f"{ self .base_url } /index.cgi"
256269 request_headers ["Accept" ] = "application/json, text/javascript, */*; q=0.01"
257270 request_headers ["X-Requested-With" ] = "XMLHttpRequest"
271+ # Added AJAX / UA
272+ request_headers ["User-Agent" ] = (
273+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
274+ )
275+ request_headers ["Sec-Fetch-Dest" ] = "empty"
276+ request_headers ["Sec-Fetch-Mode" ] = "cors"
277+ request_headers ["Sec-Fetch-Site" ] = "same-origin"
258278
259279 try :
260280 if (
261281 url not in self ._login_urls .values ()
262- and url != " /"
282+ and url != f" { self . base_url } /"
263283 and not self .connected
264284 ):
265285 _LOGGER .error ("Not connected, login first" )
266286 raise AirOSDeviceConnectionError from None
267287
268- _LOGGER .error ("TESTv6 - Trying with URL: %s" , url )
288+ if self .api_version == 6 and url .startswith (self ._status_cgi_url ):
289+ _LOGGER .error (
290+ "TESTv%s - adding timestamp to status url!" , self .api_version
291+ )
292+ timestamp = int (time .time () * 1000 )
293+ url = f"{ self ._status_cgi_url } ?_={ timestamp } "
294+
295+ _LOGGER .error ("TESTv%s - Trying with URL: %s" , self .api_version , url )
269296 async with self .session .request (
270297 method ,
271298 url ,
@@ -274,10 +301,13 @@ async def _request_json(
274301 headers = request_headers , # Pass the constructed headers
275302 allow_redirects = allow_redirects ,
276303 ) as response :
277- _LOGGER .error ("TESTv6 - Response code: %s" , response .status )
304+ _LOGGER .error (
305+ "TESTv%s - Response code: %s" , self .api_version , response .status
306+ )
278307
279308 # v6 responds with a 302 redirect and empty body
280309 if url != self ._login_urls ["v6_login" ]:
310+ self .api_version = 6
281311 response .raise_for_status ()
282312
283313 response_text = await response .text ()
@@ -288,9 +318,9 @@ async def _request_json(
288318 self ._store_auth_data (response )
289319 self .connected = True
290320
291- _LOGGER .error ("TESTv6 - response: %s" , response_text )
321+ _LOGGER .error ("TESTv%s - response: %s" , self . api_version , response_text )
292322 # V6 responds with empty body on login, not JSON
293- if url == self ._login_urls ["v6_login" ]:
323+ if url . startswith ( self ._login_urls ["v6_login" ]) :
294324 self ._store_auth_data (response )
295325 self .connected = True
296326 return {}
@@ -319,27 +349,32 @@ async def login(self) -> None:
319349 """Login to AirOS device."""
320350 payload = {"username" : self .username , "password" : self .password }
321351 try :
322- _LOGGER .error ("TESTv6 - Trying default v8 login URL" )
352+ _LOGGER .error ("TESTv%s - Trying default v8 login URL" , self . api_version )
323353 await self ._request_json (
324354 "POST" , self ._login_urls ["default" ], json_data = payload
325355 )
326356 except AirOSUrlNotFoundError :
327- _LOGGER .error ("TESTv6 - gives URL not found, trying alternative v6 URL" )
357+ _LOGGER .error (
358+ "TESTv%s - gives URL not found, trying alternative v6 URL" ,
359+ self .api_version ,
360+ )
328361 # Try next URL
329362 except AirOSConnectionSetupError as err :
330- _LOGGER .error ("TESTv6 - failed to login to v8 URL" )
363+ _LOGGER .error ("TESTv%s - failed to login to v8 URL" , self . api_version )
331364 raise AirOSConnectionSetupError ("Failed to login to AirOS device" ) from err
332365 else :
333- _LOGGER .error ("TESTv6 - returning from v8 login" )
366+ _LOGGER .error ("TESTv%s - returning from v8 login" , self . api_version )
334367 return
335368
336369 # Start of v6, go for cookies
337- _LOGGER .error ("TESTv6 - Trying to get / first for cookies" )
370+ _LOGGER .error ("TESTv%s - Trying to get / first for cookies" , self . api_version )
338371 with contextlib .suppress (Exception ):
339372 cookieresponse = await self ._request_json (
340373 "GET" , f"{ self .base_url } /" , authenticated = True
341374 )
342- _LOGGER .error ("TESTv6 - Cookie response: %s" , cookieresponse )
375+ _LOGGER .error (
376+ "TESTv%s - Cookie response: %s" , self .api_version , cookieresponse
377+ )
343378
344379 v6_simple_multipart_form_data = aiohttp .FormData ()
345380 v6_simple_multipart_form_data .add_field ("uri" , "/index.cgi" )
@@ -350,11 +385,12 @@ async def login(self) -> None:
350385 "Referer" : self ._login_urls ["v6_login" ],
351386 }
352387
353- _LOGGER .error ("TESTv6 - start v6 attempts" )
388+ _LOGGER .error ("TESTv%s - start v6 attempts" , self . api_version )
354389 # --- ATTEMPT B: Simple Payload (multipart/form-data) ---
355390 try :
356391 _LOGGER .error (
357- "TESTv6 - Trying V6 POST to %s with SIMPLE multipart/form-data" ,
392+ "TESTv%s - Trying V6 POST to %s with SIMPLE multipart/form-data" ,
393+ self .api_version ,
358394 self ._login_urls ["v6_login" ],
359395 )
360396 await self ._request_json (
@@ -367,16 +403,19 @@ async def login(self) -> None:
367403 )
368404 except (AirOSUrlNotFoundError , AirOSConnectionSetupError ) as err :
369405 _LOGGER .error (
370- "TESTv6 - V6 simple multipart failed (%s) on %s. Error: %s" ,
406+ "TESTv%s - V6 simple multipart failed (%s) on %s. Error: %s" ,
407+ self .api_version ,
371408 type (err ).__name__ ,
372409 self ._login_urls ["v6_login" ],
373410 err ,
374411 )
375412 except AirOSConnectionAuthenticationError :
376- _LOGGER .error ("TESTv6 - autherror during extended multipart" )
413+ _LOGGER .error (
414+ "TESTv%s - autherror during extended multipart" , self .api_version
415+ )
377416 raise
378417 else :
379- _LOGGER .error ("TESTv6 - returning from simple multipart" )
418+ _LOGGER .error ("TESTv%s - returning from simple multipart" , self . api_version )
380419 return # Success
381420
382421 async def status (self ) -> AirOSDataModel :
0 commit comments