Home Assistant Unofficial Reference 2024.12.1
aiohttp_client.py
Go to the documentation of this file.
1 """Helper for aiohttp webclient stuff."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable
7 from contextlib import suppress
8 import socket
9 from ssl import SSLContext
10 import sys
11 from types import MappingProxyType
12 from typing import TYPE_CHECKING, Any
13 
14 import aiohttp
15 from aiohttp import web
16 from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT
17 from aiohttp.resolver import AsyncResolver
18 from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout
19 
20 from homeassistant import config_entries
21 from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__
22 from homeassistant.core import Event, HomeAssistant, callback
23 from homeassistant.loader import bind_hass
24 from homeassistant.util import ssl as ssl_util
25 from homeassistant.util.hass_dict import HassKey
26 from homeassistant.util.json import json_loads
27 
28 from .frame import warn_use
29 from .json import json_dumps
30 
31 if TYPE_CHECKING:
32  from aiohttp.typedefs import JSONDecoder
33 
34 
35 DATA_CONNECTOR: HassKey[dict[tuple[bool, int, str], aiohttp.BaseConnector]] = HassKey(
36  "aiohttp_connector"
37 )
38 DATA_CLIENTSESSION: HassKey[dict[tuple[bool, int, str], aiohttp.ClientSession]] = (
39  HassKey("aiohttp_clientsession")
40 )
41 
42 SERVER_SOFTWARE = (
43  f"{APPLICATION_NAME}/{__version__} "
44  f"aiohttp/{aiohttp.__version__} Python/{sys.version_info[0]}.{sys.version_info[1]}"
45 )
46 
47 ENABLE_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < (
48  3,
49  13,
50  1,
51 ) or sys.version_info < (3, 12, 7)
52 # Cleanup closed is no longer needed after https://github.com/python/cpython/pull/118960
53 # which first appeared in Python 3.12.7 and 3.13.1
54 
55 WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session"
56 
57 #
58 # The default connection limit of 100 meant that you could only have
59 # 100 concurrent connections.
60 #
61 # This was effectively a limit of 100 devices and than
62 # the supervisor API would fail as soon as it was hit.
63 #
64 # We now apply the 100 limit per host, so that we can have 100 connections
65 # to a single host, but can have more than 4096 connections in total to
66 # prevent a single host from using all available connections.
67 #
68 MAXIMUM_CONNECTIONS = 4096
69 MAXIMUM_CONNECTIONS_PER_HOST = 100
70 
71 
72 class HassClientResponse(aiohttp.ClientResponse):
73  """aiohttp.ClientResponse with a json method that uses json_loads by default."""
74 
75  async def json(
76  self,
77  *args: Any,
78  loads: JSONDecoder = json_loads,
79  **kwargs: Any,
80  ) -> Any:
81  """Send a json request and parse the json response."""
82  return await super().json(*args, loads=loads, **kwargs)
83 
84 
85 @callback
86 @bind_hass
88  hass: HomeAssistant,
89  verify_ssl: bool = True,
90  family: socket.AddressFamily = socket.AF_UNSPEC,
91  ssl_cipher: ssl_util.SSLCipherList = ssl_util.SSLCipherList.PYTHON_DEFAULT,
92 ) -> aiohttp.ClientSession:
93  """Return default aiohttp ClientSession.
94 
95  This method must be run in the event loop.
96  """
97  session_key = _make_key(verify_ssl, family, ssl_cipher)
98  sessions = hass.data.setdefault(DATA_CLIENTSESSION, {})
99 
100  if session_key not in sessions:
101  session = _async_create_clientsession(
102  hass,
103  verify_ssl,
104  auto_cleanup_method=_async_register_default_clientsession_shutdown,
105  family=family,
106  ssl_cipher=ssl_cipher,
107  )
108  sessions[session_key] = session
109  else:
110  session = sessions[session_key]
111 
112  return session
113 
114 
115 @callback
116 @bind_hass
118  hass: HomeAssistant,
119  verify_ssl: bool = True,
120  auto_cleanup: bool = True,
121  family: socket.AddressFamily = socket.AF_UNSPEC,
122  ssl_cipher: ssl_util.SSLCipherList = ssl_util.SSLCipherList.PYTHON_DEFAULT,
123  **kwargs: Any,
124 ) -> aiohttp.ClientSession:
125  """Create a new ClientSession with kwargs, i.e. for cookies.
126 
127  If auto_cleanup is False, you need to call detach() after the session
128  returned is no longer used. Default is True, the session will be
129  automatically detached on homeassistant_stop or when being created
130  in config entry setup, the config entry is unloaded.
131 
132  This method must be run in the event loop.
133  """
134  auto_cleanup_method = None
135  if auto_cleanup:
136  auto_cleanup_method = _async_register_clientsession_shutdown
137 
139  hass,
140  verify_ssl,
141  auto_cleanup_method=auto_cleanup_method,
142  family=family,
143  ssl_cipher=ssl_cipher,
144  **kwargs,
145  )
146 
147 
148 @callback
150  hass: HomeAssistant,
151  verify_ssl: bool = True,
152  auto_cleanup_method: Callable[[HomeAssistant, aiohttp.ClientSession], None]
153  | None = None,
154  family: socket.AddressFamily = socket.AF_UNSPEC,
155  ssl_cipher: ssl_util.SSLCipherList = ssl_util.SSLCipherList.PYTHON_DEFAULT,
156  **kwargs: Any,
157 ) -> aiohttp.ClientSession:
158  """Create a new ClientSession with kwargs, i.e. for cookies."""
159  clientsession = aiohttp.ClientSession(
160  connector=_async_get_connector(hass, verify_ssl, family, ssl_cipher),
161  json_serialize=json_dumps,
162  response_class=HassClientResponse,
163  **kwargs,
164  )
165  # Prevent packages accidentally overriding our default headers
166  # It's important that we identify as Home Assistant
167  # If a package requires a different user agent, override it by passing a headers
168  # dictionary to the request method.
169  clientsession._default_headers = MappingProxyType( # type: ignore[assignment] # noqa: SLF001
170  {USER_AGENT: SERVER_SOFTWARE},
171  )
172 
173  clientsession.close = warn_use( # type: ignore[method-assign]
174  clientsession.close,
175  WARN_CLOSE_MSG,
176  )
177 
178  if auto_cleanup_method:
179  auto_cleanup_method(hass, clientsession)
180 
181  return clientsession
182 
183 
184 @bind_hass
186  hass: HomeAssistant,
187  request: web.BaseRequest,
188  web_coro: Awaitable[aiohttp.ClientResponse],
189  buffer_size: int = 102400,
190  timeout: int = 10,
191 ) -> web.StreamResponse | None:
192  """Stream websession request to aiohttp web response."""
193  try:
194  async with asyncio.timeout(timeout):
195  req = await web_coro
196 
197  except asyncio.CancelledError:
198  # The user cancelled the request
199  return None
200 
201  except TimeoutError as err:
202  # Timeout trying to start the web request
203  raise HTTPGatewayTimeout from err
204 
205  except aiohttp.ClientError as err:
206  # Something went wrong with the connection
207  raise HTTPBadGateway from err
208 
209  try:
210  return await async_aiohttp_proxy_stream(
211  hass, request, req.content, req.headers.get(CONTENT_TYPE)
212  )
213  finally:
214  req.close()
215 
216 
217 @bind_hass
219  hass: HomeAssistant,
220  request: web.BaseRequest,
221  stream: aiohttp.StreamReader,
222  content_type: str | None,
223  buffer_size: int = 102400,
224  timeout: int = 10,
225 ) -> web.StreamResponse:
226  """Stream a stream to aiohttp web response."""
227  response = web.StreamResponse()
228  if content_type is not None:
229  response.content_type = content_type
230  await response.prepare(request)
231 
232  # Suppressing something went wrong fetching data, closed connection
233  with suppress(TimeoutError, aiohttp.ClientError):
234  while hass.is_running:
235  async with asyncio.timeout(timeout):
236  data = await stream.read(buffer_size)
237 
238  if not data:
239  break
240  await response.write(data)
241 
242  return response
243 
244 
245 @callback
247  hass: HomeAssistant, clientsession: aiohttp.ClientSession
248 ) -> None:
249  """Register ClientSession close on Home Assistant shutdown or config entry unload.
250 
251  This method must be run in the event loop.
252  """
253 
254  @callback
255  def _async_close_websession(*_: Any) -> None:
256  """Close websession."""
257  clientsession.detach()
258 
259  unsub = hass.bus.async_listen_once(
260  EVENT_HOMEASSISTANT_CLOSE, _async_close_websession
261  )
262 
263  if not (config_entry := config_entries.current_entry.get()):
264  return
265 
266  config_entry.async_on_unload(unsub)
267  config_entry.async_on_unload(_async_close_websession)
268 
269 
270 @callback
272  hass: HomeAssistant, clientsession: aiohttp.ClientSession
273 ) -> None:
274  """Register default ClientSession close on Home Assistant shutdown.
275 
276  This method must be run in the event loop.
277  """
278 
279  @callback
280  def _async_close_websession(event: Event) -> None:
281  """Close websession."""
282  clientsession.detach()
283 
284  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_websession)
285 
286 
287 @callback
289  verify_ssl: bool = True,
290  family: socket.AddressFamily = socket.AF_UNSPEC,
291  ssl_cipher: ssl_util.SSLCipherList = ssl_util.SSLCipherList.PYTHON_DEFAULT,
292 ) -> tuple[bool, socket.AddressFamily, ssl_util.SSLCipherList]:
293  """Make a key for connector or session pool."""
294  return (verify_ssl, family, ssl_cipher)
295 
296 
297 class HomeAssistantTCPConnector(aiohttp.TCPConnector):
298  """Home Assistant TCP Connector.
299 
300  Same as aiohttp.TCPConnector but with a longer cleanup_closed timeout.
301 
302  By default the cleanup_closed timeout is 2 seconds. This is too short
303  for Home Assistant since we churn through a lot of connections. We set
304  it to 60 seconds to reduce the overhead of aborting TLS connections
305  that are likely already closed.
306  """
307 
308  # abort transport after 60 seconds (cleanup broken connections)
309  _cleanup_closed_period = 60.0
310 
311 
312 @callback
314  hass: HomeAssistant,
315  verify_ssl: bool = True,
316  family: socket.AddressFamily = socket.AF_UNSPEC,
317  ssl_cipher: ssl_util.SSLCipherList = ssl_util.SSLCipherList.PYTHON_DEFAULT,
318 ) -> aiohttp.BaseConnector:
319  """Return the connector pool for aiohttp.
320 
321  This method must be run in the event loop.
322  """
323  connector_key = _make_key(verify_ssl, family, ssl_cipher)
324  connectors = hass.data.setdefault(DATA_CONNECTOR, {})
325 
326  if connector_key in connectors:
327  return connectors[connector_key]
328 
329  if verify_ssl:
330  ssl_context: SSLContext = ssl_util.client_context(ssl_cipher)
331  else:
332  ssl_context = ssl_util.client_context_no_verify(ssl_cipher)
333 
334  connector = HomeAssistantTCPConnector(
335  family=family,
336  enable_cleanup_closed=ENABLE_CLEANUP_CLOSED,
337  ssl=ssl_context,
338  limit=MAXIMUM_CONNECTIONS,
339  limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST,
340  resolver=AsyncResolver(),
341  )
342  connectors[connector_key] = connector
343 
344  async def _async_close_connector(event: Event) -> None:
345  """Close connector pool."""
346  await connector.close()
347 
348  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_connector)
349 
350  return connector
Any json(self, *Any args, JSONDecoder loads=json_loads, **Any kwargs)
None _async_register_clientsession_shutdown(HomeAssistant hass, aiohttp.ClientSession clientsession)
web.StreamResponse async_aiohttp_proxy_stream(HomeAssistant hass, web.BaseRequest request, aiohttp.StreamReader stream, str|None content_type, int buffer_size=102400, int timeout=10)
aiohttp.ClientSession async_create_clientsession(HomeAssistant hass, bool verify_ssl=True, bool auto_cleanup=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT, **Any kwargs)
aiohttp.BaseConnector _async_get_connector(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
aiohttp.ClientSession _async_create_clientsession(HomeAssistant hass, bool verify_ssl=True, Callable[[HomeAssistant, aiohttp.ClientSession], None]|None auto_cleanup_method=None, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT, **Any kwargs)
tuple[bool, socket.AddressFamily, ssl_util.SSLCipherList] _make_key(bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
web.StreamResponse|None async_aiohttp_proxy_web(HomeAssistant hass, web.BaseRequest request, Awaitable[aiohttp.ClientResponse] web_coro, int buffer_size=102400, int timeout=10)
None _async_register_default_clientsession_shutdown(HomeAssistant hass, aiohttp.ClientSession clientsession)