33from __future__ import annotations
44
55import asyncio
6+ from http .cookies import SimpleCookie
67import json
78import logging
8- from typing import Any
9+ from typing import Any , NamedTuple
910from urllib .parse import urlparse
1011
1112import aiohttp
1213from mashumaro .exceptions import InvalidFieldValue , MissingField
14+ from yarl import URL
1315
1416from .data import (
1517 AirOS8Data as AirOSData ,
2830_LOGGER = logging .getLogger (__name__ )
2931
3032
33+ class ApiResponse (NamedTuple ):
34+ """Define API call structure."""
35+
36+ status : int
37+ headers : dict [str , Any ]
38+ cookies : SimpleCookie
39+ url : URL
40+ text : str
41+
42+
3143class AirOS :
3244 """AirOS 8 connection class."""
3345
@@ -159,7 +171,7 @@ def _get_authenticated_headers(
159171
160172 async def _api_call (
161173 self , method : str , url : str , headers : dict [str , Any ], ** kwargs : Any
162- ) -> dict [ str , Any ] :
174+ ) -> ApiResponse :
163175 """Make API call."""
164176 if url != self ._login_url and not self .connected :
165177 _LOGGER .error ("Not connected, login first" )
@@ -170,8 +182,13 @@ async def _api_call(
170182 method , url , headers = headers , ** kwargs
171183 ) as response :
172184 response_text = await response .text ()
173- result = {"response" : response , "response_text" : response_text }
174- return result
185+ return ApiResponse (
186+ status = response .status ,
187+ headers = dict (response .headers ),
188+ cookies = response .cookies ,
189+ url = response .url ,
190+ text = response_text ,
191+ )
175192 except (TimeoutError , aiohttp .ClientError ) as err :
176193 _LOGGER .exception ("Error during API call to %s: %s" , url , err )
177194 raise AirOSDeviceConnectionError from err
@@ -183,9 +200,7 @@ async def _request_json(
183200 self , method : str , url : str , headers : dict [str , Any ], ** kwargs : Any
184201 ) -> dict [str , Any ] | Any :
185202 """Return JSON from API call."""
186- result = await self ._api_call (method , url , headers = headers , ** kwargs )
187- response = result .get ("response" , {})
188- response_text = result .get ("response_text" , "" )
203+ response = await self ._api_call (method , url , headers = headers , ** kwargs )
189204
190205 match response .status :
191206 case 200 :
@@ -198,12 +213,12 @@ async def _request_json(
198213 "API call to %s failed with status %d: %s" ,
199214 url ,
200215 response .status ,
201- response_text ,
216+ response . text ,
202217 )
203218 raise AirOSDeviceConnectionError from None
204219
205220 try :
206- return json .loads (await response .text () )
221+ return json .loads (response .text )
207222 except json .JSONDecodeError as err :
208223 _LOGGER .exception ("JSON Decode Error in API response from %s" , url )
209224 raise AirOSDataMissingError from err
@@ -222,26 +237,23 @@ async def login(self) -> bool:
222237 request_headers = self ._get_authenticated_headers (ct_form = True )
223238 if self ._use_json_for_login_post :
224239 request_headers = self ._get_authenticated_headers (ct_json = True )
225- result = await self ._api_call (
240+ response = await self ._api_call (
226241 "POST" , self ._login_url , headers = request_headers , json = payload
227242 )
228243 else :
229- result = await self ._api_call (
244+ response = await self ._api_call (
230245 "POST" , self ._login_url , headers = request_headers , data = payload
231246 )
232- response = result .get ("response" , {})
233- response_text = result .get ("response_text" , "" )
234247
235248 if response .status == 403 :
236249 _LOGGER .error ("Authentication denied." )
237250 raise AirOSConnectionAuthenticationError from None
238251
239252 for _ , morsel in response .cookies .items ():
240253 # If the AIROS_ cookie was parsed but isn't automatically added to the jar, add it manually
241- if (
242- morsel .key .startswith ("AIROS_" )
243- and morsel .key not in self .session .cookie_jar
244- ):
254+ if morsel .key .startswith ("AIROS_" ) and morsel .key not in [
255+ cookie .key for cookie in self .session .cookie_jar
256+ ]:
245257 # `SimpleCookie`'s Morsel objects are designed to be compatible with cookie jars.
246258 # We need to set the domain if it's missing, otherwise the cookie might not be sent.
247259 # For IP addresses, the domain is typically blank.
@@ -299,7 +311,7 @@ async def login(self) -> bool:
299311 raise AirOSConnectionAuthenticationError from None
300312
301313 try :
302- json .loads (response_text )
314+ json .loads (response . text )
303315 self .connected = True
304316 return True
305317 except json .JSONDecodeError as err :
@@ -344,15 +356,13 @@ async def stakick(self, mac_address: str | None = None) -> bool:
344356 request_headers = self ._get_authenticated_headers (ct_form = True )
345357 payload = {"staif" : "ath0" , "staid" : mac_address .upper ()}
346358
347- result = await self ._api_call (
359+ response = await self ._api_call (
348360 "POST" , self ._stakick_cgi_url , headers = request_headers , data = payload
349361 )
350- response = result .get ("response" , {})
351362 if response .status == 200 :
352363 return True
353364
354- response_text = result .get ("response_text" , "" )
355- log = f"Unable to restart connection response status { response .status } with { response_text } "
365+ log = f"Unable to restart connection response status { response .status } with { response .text } "
356366 _LOGGER .error (log )
357367 return False
358368
@@ -365,15 +375,13 @@ async def provmode(self, active: bool = False) -> bool:
365375 action = "start"
366376
367377 payload = {"action" : action }
368- result = await self ._api_call (
378+ response = await self ._api_call (
369379 "POST" , self ._provmode_url , headers = request_headers , data = payload
370380 )
371- response = result .get ("response" , {})
372381 if response .status == 200 :
373382 return True
374383
375- response_text = result .get ("response_text" , "" )
376- log = f"Unable to change provisioning mode response status { response .status } with { response_text } "
384+ log = f"Unable to change provisioning mode response status { response .status } with { response .text } "
377385 _LOGGER .error (log )
378386 return False
379387
@@ -392,6 +400,10 @@ async def update_check(self, force: bool = False) -> dict[str, Any]:
392400 if force :
393401 payload = {"force" : "yes" }
394402 request_headers = self ._get_authenticated_headers (ct_form = True )
403+ return await self ._request_json (
404+ "POST" , self ._update_check_url , headers = request_headers , data = payload
405+ )
406+
395407 return await self ._request_json (
396408 "POST" , self ._update_check_url , headers = request_headers , json = payload
397409 )
@@ -402,7 +414,7 @@ async def progress(self) -> dict[str, Any]:
402414 payload : dict [str , Any ] = {}
403415
404416 return await self ._request_json (
405- "POST" , self ._download_progress_url , headers = request_headers , json = payload
417+ "POST" , self ._download_progress_url , headers = request_headers , data = payload
406418 )
407419
408420 async def download (self ) -> dict [str , Any ]:
0 commit comments