diff --git a/custom_components/lun_misto_air/__init__.py b/custom_components/lun_misto_air/__init__.py index 1a68ed0..c0c9b11 100644 --- a/custom_components/lun_misto_air/__init__.py +++ b/custom_components/lun_misto_air/__init__.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING from homeassistant.const import Platform +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .api import LUNMistoAirApi from .coordinator import LUNMistoAirCoordinator @@ -23,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a new entry.""" LOGGER.debug("Entry data: %s", entry.data) - api = LUNMistoAirApi() + api = LUNMistoAirApi(session=async_get_clientsession(hass)) coordinator = LUNMistoAirCoordinator(hass, api, entry) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/custom_components/lun_misto_air/api.py b/custom_components/lun_misto_air/api.py index d326dae..7273f43 100644 --- a/custom_components/lun_misto_air/api.py +++ b/custom_components/lun_misto_air/api.py @@ -1,9 +1,11 @@ -"""API for LUN Misto Air.""" +"""Asynchronous API for LUN Misto Air.""" + +from __future__ import annotations from dataclasses import dataclass from typing import Any, Self -import requests +from aiohttp import ClientError, ClientSession, ClientTimeout class LUNMistoAirError(Exception): @@ -23,7 +25,7 @@ class LUNMistoAirStationNotFoundError(LUNMistoAirError): class LUNMistoAirCityNotFoundError(LUNMistoAirError): - """Raised for station errors.""" + """Raised when a city is not found.""" @dataclass(slots=True) @@ -41,7 +43,7 @@ class LUNMistoAirStation: updated: str @classmethod - def from_dict(cls: type[Self], data: dict) -> Self: + def from_dict(cls: type[Self], data: dict[str, Any]) -> Self: """Initialize from a dict.""" return cls( name=data["name"], @@ -57,48 +59,66 @@ def from_dict(cls: type[Self], data: dict) -> Self: class LUNMistoAirApi: - """API for LUN Misto Air.""" + """Asynchronous API for LUN Misto Air.""" base_url = "https://misto.lun.ua/api/v1/air/stations" - timeout = 60 - def _request(self, url: str) -> list[dict[str, Any]]: - """Private method to handle HTTP requests.""" + def __init__( + self, + session: ClientSession | None = None, + timeout: int = 60, + ) -> None: + """Initialize the API.""" + self.session = session or ClientSession() + self.close_session = session is None + self.timeout = ClientTimeout(total=timeout) + + async def close(self) -> None: + """Close the client session if we created it.""" + if self.close_session and not self.session.closed: + await self.session.close() + + async def _request(self, url: str) -> Any: + """Make an asynchronous HTTP request.""" try: - response = requests.get(url, timeout=self.timeout) - response.raise_for_status() # Raise an error for bad status codes - return response.json() # Return JSON data - except requests.exceptions.HTTPError as http_err: - msg = f"HTTP error occurred: {http_err}" - raise LUNMistoAirResponseError(msg) from http_err - except requests.exceptions.Timeout as timeout_err: - msg = f"Request timed out: {timeout_err}" - raise LUNMistoAirConnectionError(msg) from timeout_err - except requests.exceptions.RequestException as req_err: - msg = f"Request error occurred: {req_err}" - raise LUNMistoAirConnectionError(msg) from req_err + async with self.session.get(url, timeout=self.timeout) as response: + http_ok = 200 + if response.status != http_ok: + text = await response.text() + msg = f"HTTP error {response.status}: {text}" + raise LUNMistoAirResponseError(msg) # noqa: TRY301 + return await response.json() + except TimeoutError as err: + msg = "Request timed out" + raise LUNMistoAirConnectionError(msg) from err + except ClientError as err: + msg = f"Connection error: {err}" + raise LUNMistoAirConnectionError(msg) from err except Exception as err: - msg = f"An unexpected error occurred: {err}" + msg = f"Unexpected error: {err}" raise LUNMistoAirError(msg) from err - def get_all_stations(self) -> list[LUNMistoAirStation]: + async def get_all_stations(self) -> list[LUNMistoAirStation]: """Fetch and return data for all stations.""" - if data := self._request(self.base_url): - return [LUNMistoAirStation.from_dict(station) for station in data] - return [] + data = await self._request(self.base_url) + return [LUNMistoAirStation.from_dict(station) for station in data] - def get_by_station_name(self, station_name: str) -> LUNMistoAirStation: + async def get_station_by_name(self, station_name: str) -> LUNMistoAirStation: """Fetch and return data for a specific station by its name.""" - if stations := self.get_all_stations(): - for station in stations: - if station.name == station_name: - return station - raise LUNMistoAirStationNotFoundError - - def get_by_city(self, city: str) -> list[LUNMistoAirStation]: + stations = await self.get_all_stations() + for station in stations: + if station.name == station_name: + return station + msg = f"Station with name '{station_name}' not found." + raise LUNMistoAirStationNotFoundError(msg) + + async def get_stations_by_city(self, city: str) -> list[LUNMistoAirStation]: """Fetch and return data for all stations in a specific city.""" - if stations := self.get_all_stations(): - return [ - station for station in stations if station.city.lower() == city.lower() - ] - raise LUNMistoAirCityNotFoundError + stations = await self.get_all_stations() + matching_stations = [ + station for station in stations if station.city.lower() == city.lower() + ] + if not matching_stations: + msg = f"No stations found in city '{city}'." + raise LUNMistoAirCityNotFoundError(msg) + return matching_stations diff --git a/custom_components/lun_misto_air/config_flow.py b/custom_components/lun_misto_air/config_flow.py index f71a96c..049cb8c 100644 --- a/custom_components/lun_misto_air/config_flow.py +++ b/custom_components/lun_misto_air/config_flow.py @@ -17,6 +17,7 @@ CONF_METHOD, ) from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( LocationSelector, SelectOptionDict, @@ -58,7 +59,7 @@ class LUNMistoAirOptionsFlow(OptionsFlow): def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - self.api = LUNMistoAirApi() + self.api = LUNMistoAirApi(session=async_get_clientsession(self.hass)) async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult: """Handle the station flow.""" @@ -77,9 +78,7 @@ async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowRes }, ) - stations = await self.hass.async_add_executor_job( - self.api.get_all_stations, - ) + stations = await self.api.get_all_stations() return self.async_show_form( step_id="init", @@ -106,7 +105,7 @@ class LUNMistoAirConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" - self.api = LUNMistoAirApi() + self.api = LUNMistoAirApi(session=async_get_clientsession(self.hass)) self.data: dict[str, Any] = {} @staticmethod @@ -148,10 +147,7 @@ async def async_step_map( if user_input is not None: LOGGER.debug("User data: %s", user_input) - stations = await self.hass.async_add_executor_job( - self.api.get_all_stations, - ) - + stations = await self.api.get_all_stations() if len(stations) == 0: errors["base"] = "no_stations" @@ -209,9 +205,7 @@ async def async_step_station_name( }, ) - stations = await self.hass.async_add_executor_job( - self.api.get_all_stations, - ) + stations = await self.api.get_all_stations() return self.async_show_form( step_id=STEP_STATION_NAME, diff --git a/custom_components/lun_misto_air/coordinator.py b/custom_components/lun_misto_air/coordinator.py index 3862b14..4d57fb0 100644 --- a/custom_components/lun_misto_air/coordinator.py +++ b/custom_components/lun_misto_air/coordinator.py @@ -44,10 +44,7 @@ def __init__( async def _async_update_data(self) -> LUNMistoAirStation: try: - return await self.hass.async_add_executor_job( - self._api.get_by_station_name, - self.station_name, - ) + return await self._api.get_station_by_name(self.station_name) except LUNMistoAirStationNotFoundError as exc: msg = f"Station '{self.station_name}' not found" raise UpdateFailed(msg) from exc