22
33import asyncio
44from collections .abc import AsyncIterator
5- from contextlib import asynccontextmanager , suppress
5+ from contextlib import asynccontextmanager
66from dataclasses import dataclass
77from datetime import UTC , datetime , timedelta
88import logging
1515
1616from ..coresys import CoreSys , CoreSysAttributes
1717from ..exceptions import HomeAssistantAPIError , HomeAssistantAuthError
18- from ..jobs .const import JobConcurrency
19- from ..jobs .decorator import Job
20- from ..utils import check_port , version_is_new_enough
18+ from ..utils import version_is_new_enough
2119from .const import LANDINGPAGE
2220
2321_LOGGER : logging .Logger = logging .getLogger (__name__ )
@@ -43,22 +41,35 @@ def __init__(self, coresys: CoreSys):
4341 # We don't persist access tokens. Instead we fetch new ones when needed
4442 self .access_token : str | None = None
4543 self ._access_token_expires : datetime | None = None
44+ self ._token_lock : asyncio .Lock = asyncio .Lock ()
4645
47- @Job (
48- name = "home_assistant_api_ensure_access_token" ,
49- internal = True ,
50- concurrency = JobConcurrency .QUEUE ,
51- )
5246 async def ensure_access_token (self ) -> None :
53- """Ensure there is an access token."""
47+ """Ensure there is a valid access token.
48+
49+ Raises:
50+ HomeAssistantAuthError: When we cannot get a valid token
51+ aiohttp.ClientError: On network or connection errors
52+ TimeoutError: On request timeouts
53+
54+ """
55+ # Fast path check without lock (avoid unnecessary locking
56+ # for the majority of calls).
5457 if (
5558 self .access_token
5659 and self ._access_token_expires
5760 and self ._access_token_expires > datetime .now (tz = UTC )
5861 ):
5962 return
6063
61- with suppress (asyncio .TimeoutError , aiohttp .ClientError ):
64+ async with self ._token_lock :
65+ # Double-check after acquiring lock (avoid race condition)
66+ if (
67+ self .access_token
68+ and self ._access_token_expires
69+ and self ._access_token_expires > datetime .now (tz = UTC )
70+ ):
71+ return
72+
6273 async with self .sys_websession .post (
6374 f"{ self .sys_homeassistant .api_url } /auth/token" ,
6475 timeout = aiohttp .ClientTimeout (total = 30 ),
@@ -92,7 +103,36 @@ async def make_request(
92103 params : MultiMapping [str ] | None = None ,
93104 headers : dict [str , str ] | None = None ,
94105 ) -> AsyncIterator [aiohttp .ClientResponse ]:
95- """Async context manager to make a request with right auth."""
106+ """Async context manager to make authenticated requests to Home Assistant API.
107+
108+ This context manager handles authentication token management automatically,
109+ including token refresh on 401 responses. It yields the HTTP response
110+ for the caller to handle.
111+
112+ Error Handling:
113+ - HTTP error status codes (4xx, 5xx) are preserved in the response
114+ - Authentication is handled transparently with one retry on 401
115+ - Network/connection failures raise HomeAssistantAPIError
116+ - No logging is performed - callers should handle logging as needed
117+
118+ Args:
119+ method: HTTP method (get, post, etc.)
120+ path: API path relative to Home Assistant base URL
121+ json: JSON data to send in request body
122+ content_type: Override content-type header
123+ data: Raw data to send in request body
124+ timeout: Request timeout in seconds
125+ params: URL query parameters
126+ headers: Additional HTTP headers
127+
128+ Yields:
129+ aiohttp.ClientResponse: The HTTP response object
130+
131+ Raises:
132+ HomeAssistantAPIError: When request cannot be completed due to
133+ network errors, timeouts, or connection failures
134+
135+ """
96136 url = f"{ self .sys_homeassistant .api_url } /{ path } "
97137 headers = headers or {}
98138
@@ -101,10 +141,9 @@ async def make_request(
101141 headers [hdrs .CONTENT_TYPE ] = content_type
102142
103143 for _ in (1 , 2 ):
104- await self .ensure_access_token ()
105- headers [hdrs .AUTHORIZATION ] = f"Bearer { self .access_token } "
106-
107144 try :
145+ await self .ensure_access_token ()
146+ headers [hdrs .AUTHORIZATION ] = f"Bearer { self .access_token } "
108147 async with getattr (self .sys_websession , method )(
109148 url ,
110149 data = data ,
@@ -120,23 +159,19 @@ async def make_request(
120159 continue
121160 yield resp
122161 return
123- except TimeoutError :
124- _LOGGER .error ("Timeout on call %s." , url )
125- break
162+ except TimeoutError as err :
163+ _LOGGER .debug ("Timeout on call %s." , url )
164+ raise HomeAssistantAPIError ( str ( err )) from err
126165 except aiohttp .ClientError as err :
127- _LOGGER .error ("Error on call %s: %s" , url , err )
128- break
129-
130- raise HomeAssistantAPIError ()
166+ _LOGGER .debug ("Error on call %s: %s" , url , err )
167+ raise HomeAssistantAPIError (str (err )) from err
131168
132169 async def _get_json (self , path : str ) -> dict [str , Any ]:
133170 """Return Home Assistant get API."""
134171 async with self .make_request ("get" , path ) as resp :
135172 if resp .status in (200 , 201 ):
136173 return await resp .json ()
137- else :
138- _LOGGER .debug ("Home Assistant API return: %d" , resp .status )
139- raise HomeAssistantAPIError ()
174+ raise HomeAssistantAPIError (f"Home Assistant Core API return { resp .status } " )
140175
141176 async def get_config (self ) -> dict [str , Any ]:
142177 """Return Home Assistant config."""
@@ -155,15 +190,8 @@ async def get_api_state(self) -> APIState | None:
155190 ):
156191 return None
157192
158- # Check if port is up
159- if not await check_port (
160- self .sys_homeassistant .ip_address ,
161- self .sys_homeassistant .api_port ,
162- ):
163- return None
164-
165193 # Check if API is up
166- with suppress ( HomeAssistantAPIError ) :
194+ try :
167195 # get_core_state is available since 2023.8.0 and preferred
168196 # since it is significantly faster than get_config because
169197 # it does not require serializing the entire config
@@ -181,6 +209,8 @@ async def get_api_state(self) -> APIState | None:
181209 migrating = recorder_state .get ("migration_in_progress" , False )
182210 live_migration = recorder_state .get ("migration_is_live" , False )
183211 return APIState (state , migrating and not live_migration )
212+ except HomeAssistantAPIError as err :
213+ _LOGGER .debug ("Can't connect to Home Assistant API: %s" , err )
184214
185215 return None
186216
0 commit comments