22
33from __future__ import annotations
44
5- from typing import cast
6-
75import asyncio
86import json
97import logging
10- import ssl
8+ import ssl
119
1210import aiohttp
1311
1917class AirOS8 :
2018 """Set up connection to AirOS."""
2119
22- def __init__ (self , host : str , username : str , password : str ): - > None
20+ def __init__ (self , host : str , username : str , password : str ):
2321 """Initialize AirOS8 class."""
2422 self .username = username
2523 self .password = password
2624 self .base_url = f"https://{ host } "
2725
2826 self ._login_url = f"{ self .base_url } /api/auth" # AirOS 8
2927 self ._status_cgi_url = f"{ self .base_url } /status.cgi" # AirOS 8
28+ self .current_csrf_token = None
3029
3130 self ._use_json_for_login_post = False
3231
@@ -44,20 +43,15 @@ def __init__(self, host: str, username: str, password: str): -> None
4443 "X-Requested-With" : "XMLHttpRequest" ,
4544 }
4645
47- async def login (self ): - > bool
46+ async def login (self ) -> bool :
4847 """Log in to the device assuring cookies and tokens set correctly."""
4948 loop = asyncio .get_running_loop ()
50- ssl_context = await loop .run_in_executor (
51- None ,
52- ssl .create_default_context
53- )
49+ ssl_context = await loop .run_in_executor (None , ssl .create_default_context )
5450 ssl_context .check_hostname = False
5551 ssl_context .verify_mode = ssl .CERT_NONE
5652 connector = aiohttp .TCPConnector (ssl = ssl_context )
5753
5854 async with aiohttp .ClientSession (connector = connector ) as self .session :
59- current_csrf_token = None
60-
6155 # --- Step 0: Pre-inject the 'ok=1' cookie before login POST (mimics curl) ---
6256 self .session .cookie_jar .update_cookies ({"ok" : "1" })
6357
@@ -74,26 +68,43 @@ async def login(self): -> bool
7468 login_request_headers ["Content-Type" ] = "application/json"
7569 post_data = json .dumps (login_payload )
7670 else :
77- login_request_headers ["Content-Type" ] = "application/x-www-form-urlencoded; charset=UTF-8"
71+ login_request_headers ["Content-Type" ] = (
72+ "application/x-www-form-urlencoded; charset=UTF-8"
73+ )
7874 post_data = login_payload
7975
8076 try :
81- async with self .session .post (self ._login_url , data = post_data , headers = login_request_headers ) as response :
77+ async with self .session .post (
78+ self ._login_url , data = post_data , headers = login_request_headers
79+ ) as response :
8280 if not response .cookies :
8381 logger .exception ("Empty cookies after login, bailing out." )
84- raise DataMissingError
82+ raise DataMissingError from None
8583 else :
8684 for _ , morsel in response .cookies .items ():
87-
8885 # If the AIROS_ cookie was parsed but isn't automatically added to the jar, add it manually
89- if morsel .key .startswith ("AIROS_" ) and morsel .key not in self .session .cookie_jar :
86+ if (
87+ morsel .key .startswith ("AIROS_" )
88+ and morsel .key not in self .session .cookie_jar
89+ ):
9090 # `SimpleCookie`'s Morsel objects are designed to be compatible with cookie jars.
9191 # We need to set the domain if it's missing, otherwise the cookie might not be sent.
9292 # For IP addresses, the domain is typically blank.
9393 # aiohttp's jar should handle it, but for explicit control:
9494 if not morsel .get ("domain" ):
95- morsel ["domain" ] = response .url .host # Set to the host that issued it
96- self .session .cookie_jar .update_cookies ({morsel .key : morsel .output (header = "" )[len (morsel .key )+ 1 :].split (";" )[0 ].strip ()}, response .url )
95+ morsel ["domain" ] = (
96+ response .url .host
97+ ) # Set to the host that issued it
98+ self .session .cookie_jar .update_cookies (
99+ {
100+ morsel .key : morsel .output (header = "" )[
101+ len (morsel .key ) + 1 :
102+ ]
103+ .split (";" )[0 ]
104+ .strip ()
105+ },
106+ response .url ,
107+ )
97108 # The update_cookies method can take a SimpleCookie morsel directly or a dict.
98109 # The morsel.output method gives 'NAME=VALUE; Path=...; HttpOnly'
99110 # We just need 'NAME=VALUE' or the morsel object itself.
@@ -104,68 +115,75 @@ async def login(self): -> bool
104115 # Simpler: just ensure the key-value pair is there for simple jar.
105116
106117 # Let's try the direct update of the key-value
107- self .session .cookie_jar .update_cookies ({morsel .key : morsel .value })
118+ self .session .cookie_jar .update_cookies (
119+ {morsel .key : morsel .value }
120+ )
108121
109122 new_csrf_token = response .headers .get ("X-CSRF-ID" )
110123 if new_csrf_token :
111- current_csrf_token = new_csrf_token
124+ self . current_csrf_token = new_csrf_token
112125 else :
113126 return
114127
115128 # Re-check cookies in self.session.cookie_jar AFTER potential manual injection
116129 airos_cookie_found = False
117130 ok_cookie_found = False
118131 if not self .session .cookie_jar :
119- logger .exception ("COOKIE JAR IS EMPTY after login POST. This is a major issue." )
120- raise DataMissingError
132+ logger .exception (
133+ "COOKIE JAR IS EMPTY after login POST. This is a major issue."
134+ )
135+ raise DataMissingError from None
121136 for cookie in self .session .cookie_jar :
122137 if cookie .key .startswith ("AIROS_" ):
123138 airos_cookie_found = True
124139 if cookie .key == "ok" :
125140 ok_cookie_found = True
126141
127142 if not airos_cookie_found and not ok_cookie_found :
128- raise DataMissingError
143+ raise DataMissingError from None
129144
130145 response_text = await response .text ()
131146
132147 if response .status == 200 :
133148 try :
134149 json .loads (response_text )
135150 return True
136- except json .JSONDecodeError :
151+ except json .JSONDecodeError as err :
137152 logger .exception ("JSON Decode Error" )
138- raise DataMissingError
153+ raise DataMissingError from err
139154
140155 else :
141156 log = f"Login failed with status { response .status } . Full Response: { response .text } "
142157 logger .error (log )
143- raise ConnectionFailedError
144- except aiohttp .ClientError :
158+ raise ConnectionFailedError from None
159+ except aiohttp .ClientError as err :
145160 logger .exception ("Error during login" )
146- raise ConnectionFailedError
161+ raise ConnectionFailedError from err
147162
148- async def status (self ): - > dict
163+ async def status (self ) -> dict :
149164 """Retrieve status from the device."""
150- # --- Step 2: Verify authenticated access by fetching status.cgi ---
151- authenticated_get_headers = {** self ._common_headers }
152- if current_csrf_token :
153- authenticated_get_headers ["X-CSRF-ID" ] = current_csrf_token
154-
155- try :
156- async with self .session .get (self ._status_cgi_url , headers = authenticated_get_headers ) as response :
157- status_response_text = await response .text ()
158-
159- if response .status == 200 :
160- try :
161- return json .loads (status_response_text )
162- except json .JSONDecodeError :
163- logger .exception ("JSON Decode Error in authenticated status response" )
164- raise DataMissingError
165- else :
166- log = f"Authenticated status.cgi failed: { response .status } . Response: { status_response_text } "
167- logger .error (log )
168- except aiohttp .ClientError :
169- logger .exception ("Error during authenticated status.cgi call" )
170- raise ConnectionFailedError
171-
165+ # --- Step 2: Verify authenticated access by fetching status.cgi ---
166+ authenticated_get_headers = {** self ._common_headers }
167+ if self .current_csrf_token :
168+ authenticated_get_headers ["X-CSRF-ID" ] = self .current_csrf_token
169+
170+ try :
171+ async with self .session .get (
172+ self ._status_cgi_url , headers = authenticated_get_headers
173+ ) as response :
174+ status_response_text = await response .text ()
175+
176+ if response .status == 200 :
177+ try :
178+ return json .loads (status_response_text )
179+ except json .JSONDecodeError :
180+ logger .exception (
181+ "JSON Decode Error in authenticated status response"
182+ )
183+ raise DataMissingError from None
184+ else :
185+ log = f"Authenticated status.cgi failed: { response .status } . Response: { status_response_text } "
186+ logger .error (log )
187+ except aiohttp .ClientError as err :
188+ logger .exception ("Error during authenticated status.cgi call" )
189+ raise ConnectionFailedError from err
0 commit comments