diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index f7eab3bc2f2..2a660435e17 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -11,11 +11,10 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from .const import CONF_SERIAL_NUMBER -from .coordinator import RainbirdData +from .coordinator import RainbirdData, async_create_clientsession _LOGGER = logging.getLogger(__name__) @@ -36,9 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) + clientsession = async_create_clientsession() + entry.async_on_unload(clientsession.close) controller = AsyncRainbirdController( AsyncRainbirdClient( - async_get_clientsession(hass), + clientsession, entry.data[CONF_HOST], entry.data[CONF_PASSWORD], ) diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index c4f76c5c9c5..44576db8a33 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -23,7 +23,6 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, selector -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -33,6 +32,7 @@ from .const import ( DOMAIN, TIMEOUT_SECONDS, ) +from .coordinator import async_create_clientsession _LOGGER = logging.getLogger(__name__) @@ -104,9 +104,10 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN): Raises a ConfigFlowError on failure. """ + clientsession = async_create_clientsession() controller = AsyncRainbirdController( AsyncRainbirdClient( - async_get_clientsession(self.hass), + clientsession, host, password, ) @@ -127,6 +128,8 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN): f"Error connecting to Rain Bird controller: {str(err)}", "cannot_connect", ) from err + finally: + await clientsession.close() async def async_finish( self, diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 9f1ea95b333..22aaf2d11a0 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -9,6 +9,7 @@ from functools import cached_property import logging from typing import TypeVar +import aiohttp from pyrainbird.async_client import ( AsyncRainbirdController, RainbirdApiException, @@ -28,6 +29,9 @@ UPDATE_INTERVAL = datetime.timedelta(minutes=1) # changes, so we refresh it less often. CALENDAR_UPDATE_INTERVAL = datetime.timedelta(minutes=15) +# Rainbird devices can only accept a single request at a time +CONECTION_LIMIT = 1 + _LOGGER = logging.getLogger(__name__) _T = TypeVar("_T") @@ -43,6 +47,13 @@ class RainbirdDeviceState: rain_delay: int +def async_create_clientsession() -> aiohttp.ClientSession: + """Create a rainbird async_create_clientsession with a connection limit.""" + return aiohttp.ClientSession( + connector=aiohttp.TCPConnector(limit=CONECTION_LIMIT), + ) + + class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): """Coordinator for rainbird API calls.""" diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 52b98e5c6b6..53df24264df 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from http import HTTPStatus import json from typing import Any @@ -15,7 +16,7 @@ from homeassistant.components.rainbird.const import ( ATTR_DURATION, DEFAULT_TRIGGER_TIME_MINUTES, ) -from homeassistant.const import Platform +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, Platform from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -155,6 +156,31 @@ def setup_platforms( yield +@pytest.fixture(autouse=True) +def aioclient_mock(hass: HomeAssistant) -> Generator[AiohttpClientMocker, None, None]: + """Context manager to mock aiohttp client.""" + mocker = AiohttpClientMocker() + + def create_session(): + session = mocker.create_session(hass.loop) + + async def close_session(event): + """Close session.""" + await session.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, close_session) + return session + + with patch( + "homeassistant.components.rainbird.async_create_clientsession", + side_effect=create_session, + ), patch( + "homeassistant.components.rainbird.config_flow.async_create_clientsession", + side_effect=create_session, + ): + yield mocker + + def rainbird_json_response(result: dict[str, str]) -> bytes: """Create a fake API response.""" return encryption.encrypt(