From 317ecf86d0d46e9c48777a6a186b32f48acd8f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 21 Aug 2023 14:48:53 +0200 Subject: [PATCH 1/2] Update AEMET-OpenData to v0.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/__init__.py | 3 +- homeassistant/components/aemet/config_flow.py | 19 ++- homeassistant/components/aemet/manifest.json | 2 +- .../aemet/weather_update_coordinator.py | 22 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../aemet/fixtures/station-3195.json | 6 - .../aemet/fixtures/station-list.json | 6 - .../fixtures/town-28065-forecast-daily.json | 6 - .../fixtures/town-28065-forecast-hourly.json | 6 - tests/components/aemet/test_config_flow.py | 32 +++-- tests/components/aemet/test_init.py | 11 +- tests/components/aemet/test_weather.py | 9 +- tests/components/aemet/util.py | 133 ++++++++---------- 14 files changed, 109 insertions(+), 150 deletions(-) delete mode 100644 tests/components/aemet/fixtures/station-3195.json delete mode 100644 tests/components/aemet/fixtures/station-list.json delete mode 100644 tests/components/aemet/fixtures/town-28065-forecast-daily.json delete mode 100644 tests/components/aemet/fixtures/town-28065-forecast-hourly.json diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 032e0a3a9f6d31..68e7bb6c5e063b 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -6,6 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client from .const import ( CONF_STATION_UPDATES, @@ -27,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: longitude = entry.data[CONF_LONGITUDE] station_updates = entry.options.get(CONF_STATION_UPDATES, True) - aemet = AEMET(api_key) + aemet = AEMET(aiohttp_client.async_get_clientsession(hass), api_key) weather_coordinator = WeatherUpdateCoordinator( hass, aemet, latitude, longitude, station_updates ) diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index 9db0c6f7db1f91..129f513025abfc 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -2,12 +2,13 @@ from __future__ import annotations from aemet_opendata import AEMET +from aemet_opendata.exceptions import AuthError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, SchemaOptionsFlowHandler, @@ -39,8 +40,13 @@ async def async_step_user(self, user_input=None): await self.async_set_unique_id(f"{latitude}-{longitude}") self._abort_if_unique_id_configured() - api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY]) - if not api_online: + aemet = AEMET( + aiohttp_client.async_get_clientsession(self.hass), + user_input[CONF_API_KEY], + ) + try: + await aemet.get_conventional_observation_stations(False) + except AuthError: errors["base"] = "invalid_api_key" if not errors: @@ -70,10 +76,3 @@ def async_get_options_flow( ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) - - -async def _is_aemet_api_online(hass, api_key): - aemet = AEMET(api_key) - return await hass.async_add_executor_job( - aemet.get_conventional_observation_stations, False - ) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index f9f1129f3b0d77..a460d9e16bca35 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.2.2"] + "requirements": ["AEMET-OpenData==0.3.0"] } diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 5e9ce6af67758a..d0957507a0d447 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -146,13 +146,13 @@ async def _async_update_data(self): async def _get_aemet_weather(self): """Poll weather data from AEMET OpenData.""" - weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast) + weather = await self._get_weather_and_forecast() return weather - def _get_weather_station(self): + async def _get_weather_station(self): if not self._station: self._station = ( - self._aemet.get_conventional_observation_station_by_coordinates( + await self._aemet.get_conventional_observation_station_by_coordinates( self._latitude, self._longitude ) ) @@ -171,9 +171,9 @@ def _get_weather_station(self): ) return self._station - def _get_weather_town(self): + async def _get_weather_town(self): if not self._town: - self._town = self._aemet.get_town_by_coordinates( + self._town = await self._aemet.get_town_by_coordinates( self._latitude, self._longitude ) if self._town: @@ -192,18 +192,18 @@ def _get_weather_town(self): raise TownNotFound return self._town - def _get_weather_and_forecast(self): + async def _get_weather_and_forecast(self): """Get weather and forecast data from AEMET OpenData.""" - self._get_weather_town() + await self._get_weather_town() - daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID]) + daily = await self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID]) if not daily: _LOGGER.error( 'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID] ) - hourly = self._aemet.get_specific_forecast_town_hourly( + hourly = await self._aemet.get_specific_forecast_town_hourly( self._town[AEMET_ATTR_ID] ) if not hourly: @@ -212,8 +212,8 @@ def _get_weather_and_forecast(self): ) station = None - if self._station_updates and self._get_weather_station(): - station = self._aemet.get_conventional_observation_station_data( + if self._station_updates and await self._get_weather_station(): + station = await self._aemet.get_conventional_observation_station_data( self._station[AEMET_ATTR_IDEMA] ) if not station: diff --git a/requirements_all.txt b/requirements_all.txt index a88b5a6423c4e0..7fed024ad14831 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.2.2 +AEMET-OpenData==0.3.0 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36cb0cb766aae4..7451a2ea2f251e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.2.2 +AEMET-OpenData==0.3.0 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/tests/components/aemet/fixtures/station-3195.json b/tests/components/aemet/fixtures/station-3195.json deleted file mode 100644 index cfd8c59a7eec4a..00000000000000 --- a/tests/components/aemet/fixtures/station-3195.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/208c3ca3", - "metadatos": "https://opendata.aemet.es/opendata/sh/55c2971b" -} diff --git a/tests/components/aemet/fixtures/station-list.json b/tests/components/aemet/fixtures/station-list.json deleted file mode 100644 index 86f79727e7fd4a..00000000000000 --- a/tests/components/aemet/fixtures/station-list.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/2c55192f", - "metadatos": "https://opendata.aemet.es/opendata/sh/55c2971b" -} diff --git a/tests/components/aemet/fixtures/town-28065-forecast-daily.json b/tests/components/aemet/fixtures/town-28065-forecast-daily.json deleted file mode 100644 index 41103c1033f0dd..00000000000000 --- a/tests/components/aemet/fixtures/town-28065-forecast-daily.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/64e29abb", - "metadatos": "https://opendata.aemet.es/opendata/sh/dfd88b22" -} diff --git a/tests/components/aemet/fixtures/town-28065-forecast-hourly.json b/tests/components/aemet/fixtures/town-28065-forecast-hourly.json deleted file mode 100644 index cdcacfcb6a534e..00000000000000 --- a/tests/components/aemet/fixtures/town-28065-forecast-hourly.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/18ca1886", - "metadatos": "https://opendata.aemet.es/opendata/sh/93a7c63d" -} diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index 59a6993903fcc7..b311cfd4a54873 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -1,8 +1,8 @@ """Define tests for the AEMET OpenData config flow.""" from unittest.mock import AsyncMock, MagicMock, patch +from aemet_opendata.exceptions import AuthError import pytest -import requests_mock from homeassistant import data_entry_flow from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .util import aemet_requests_mock +from .util import mock_api_call from tests.common import MockConfigEntry @@ -28,9 +28,10 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test that the form is served with valid input.""" - with requests_mock.mock() as _m: - aemet_requests_mock(_m) - + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -64,9 +65,10 @@ async def test_form_options(hass: HomeAssistant) -> None: now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - + ), patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): entry = MockConfigEntry( domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG ) @@ -120,9 +122,10 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - + ), patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): entry = MockConfigEntry( domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG ) @@ -136,11 +139,10 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_form_api_offline(hass: HomeAssistant) -> None: - """Test setting up with api call error.""" +async def test_form_auth_error(hass: HomeAssistant) -> None: + """Test setting up with api auth error.""" mocked_aemet = MagicMock() - - mocked_aemet.get_conventional_observation_stations.return_value = None + mocked_aemet.get_conventional_observation_stations.side_effect = AuthError with patch( "homeassistant.components.aemet.config_flow.AEMET", diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 9db0ffb2bcf603..24c16ba3ef3260 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -1,15 +1,13 @@ """Define tests for the AEMET OpenData init.""" from unittest.mock import patch -import requests_mock - from homeassistant.components.aemet.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .util import aemet_requests_mock +from .util import mock_api_call from tests.common import MockConfigEntry @@ -27,9 +25,10 @@ async def test_unload_entry(hass: HomeAssistant) -> None: now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - + ), patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): config_entry = MockConfigEntry( domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG ) diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index c64e824e18dfd5..703ef4348f86c8 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -4,7 +4,6 @@ from freezegun.api import FrozenDateTimeFactory import pytest -import requests_mock from syrupy.assertion import SnapshotAssertion from homeassistant.components.aemet.const import ATTRIBUTION, DOMAIN @@ -36,7 +35,7 @@ from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from .util import aemet_requests_mock, async_init_integration +from .util import async_init_integration, mock_api_call from tests.typing import WebSocketGenerator @@ -191,8 +190,10 @@ async def test_forecast_subscription( assert forecast1 == snapshot - with requests_mock.mock() as _m: - aemet_requests_mock(_m) + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): freezer.tick(WEATHER_UPDATE_INTERVAL + datetime.timedelta(seconds=1)) await hass.async_block_till_done() msg = await client.receive_json() diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py index 991e7459bf6d95..05417563e2ff72 100644 --- a/tests/components/aemet/util.py +++ b/tests/components/aemet/util.py @@ -1,93 +1,74 @@ """Tests for the AEMET OpenData integration.""" +from typing import Any +from unittest.mock import patch -import requests_mock +from aemet_opendata.const import ATTR_DATA from homeassistant.components.aemet import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_value_fixture +FORECAST_DAILY_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-28065-forecast-daily-data.json"), +} -def aemet_requests_mock(mock): - """Mock requests performed to AEMET OpenData API.""" +FORECAST_HOURLY_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-28065-forecast-hourly-data.json"), +} - station_3195_fixture = "aemet/station-3195.json" - station_3195_data_fixture = "aemet/station-3195-data.json" - station_list_fixture = "aemet/station-list.json" - station_list_data_fixture = "aemet/station-list-data.json" +STATION_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/station-3195-data.json"), +} - town_28065_forecast_daily_fixture = "aemet/town-28065-forecast-daily.json" - town_28065_forecast_daily_data_fixture = "aemet/town-28065-forecast-daily-data.json" - town_28065_forecast_hourly_fixture = "aemet/town-28065-forecast-hourly.json" - town_28065_forecast_hourly_data_fixture = ( - "aemet/town-28065-forecast-hourly-data.json" - ) - town_id28065_fixture = "aemet/town-id28065.json" - town_list_fixture = "aemet/town-list.json" +STATIONS_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/station-list-data.json"), +} - mock.get( - "https://opendata.aemet.es/opendata/api/observacion/convencional/datos/estacion/3195", - text=load_fixture(station_3195_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/208c3ca3", - text=load_fixture(station_3195_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/observacion/convencional/todas", - text=load_fixture(station_list_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/2c55192f", - text=load_fixture(station_list_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/diaria/28065", - text=load_fixture(town_28065_forecast_daily_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/64e29abb", - text=load_fixture(town_28065_forecast_daily_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/horaria/28065", - text=load_fixture(town_28065_forecast_hourly_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/18ca1886", - text=load_fixture(town_28065_forecast_hourly_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/maestro/municipio/id28065", - text=load_fixture(town_id28065_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/maestro/municipios", - text=load_fixture(town_list_fixture), - ) +TOWN_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-id28065.json"), +} +TOWNS_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-list.json"), +} -async def async_init_integration( - hass: HomeAssistant, - skip_setup: bool = False, -): - """Set up the AEMET OpenData integration in Home Assistant.""" - with requests_mock.mock() as _m: - aemet_requests_mock(_m) +def mock_api_call(cmd: str, fetch_data: bool = False) -> dict[str, Any]: + """Mock AEMET OpenData API calls.""" + if cmd == "maestro/municipio/id28065": + return TOWN_DATA_MOCK + if cmd == "maestro/municipios": + return TOWNS_DATA_MOCK + if cmd == "observacion/convencional/datos/estacion/3195": + return STATION_DATA_MOCK + if cmd == "observacion/convencional/todas": + return STATIONS_DATA_MOCK + if cmd == "prediccion/especifica/municipio/diaria/28065": + return FORECAST_DAILY_DATA_MOCK + if cmd == "prediccion/especifica/municipio/horaria/28065": + return FORECAST_HOURLY_DATA_MOCK + return {} - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "mock", - CONF_LATITUDE: "40.30403754", - CONF_LONGITUDE: "-3.72935236", - CONF_NAME: "AEMET", - }, - ) - entry.add_to_hass(hass) - if not skip_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() +async def async_init_integration(hass: HomeAssistant): + """Set up the AEMET OpenData integration in Home Assistant.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "api-key", + CONF_LATITUDE: "40.30403754", + CONF_LONGITUDE: "-3.72935236", + CONF_NAME: "AEMET", + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() From 7bb3b1c97ae4ed6a37c49015d8c6ae464210ac2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 22 Aug 2023 13:03:43 +0200 Subject: [PATCH 2/2] aemet: fix black MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/weather_update_coordinator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index d0957507a0d447..d44160116f242e 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -197,7 +197,9 @@ async def _get_weather_and_forecast(self): await self._get_weather_town() - daily = await self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID]) + daily = await self._aemet.get_specific_forecast_town_daily( + self._town[AEMET_ATTR_ID] + ) if not daily: _LOGGER.error( 'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID]