From 5ef12c39934c30f2b28c1ce0752d9dee911ea3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 23 Dec 2024 15:51:21 +0100 Subject: [PATCH] Add AEMET Weather Radar images (#131386) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/aemet/__init__.py | 4 +- homeassistant/components/aemet/config_flow.py | 3 +- homeassistant/components/aemet/const.py | 3 +- homeassistant/components/aemet/diagnostics.py | 3 +- homeassistant/components/aemet/image.py | 86 +++++++++++++++++++ homeassistant/components/aemet/strings.json | 8 ++ .../aemet/snapshots/test_diagnostics.ambr | 7 ++ tests/components/aemet/test_config_flow.py | 20 ++++- tests/components/aemet/test_image.py | 22 +++++ tests/components/aemet/util.py | 18 +++- 10 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/aemet/image.py create mode 100644 tests/components/aemet/test_image.py diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 79dc3cc55ce..4bd9dd03eea 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.storage import STORAGE_DIR -from .const import CONF_STATION_UPDATES, DOMAIN, PLATFORMS +from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DOMAIN, PLATFORMS from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -26,6 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo latitude = entry.data[CONF_LATITUDE] longitude = entry.data[CONF_LONGITUDE] update_features: int = UpdateFeature.FORECAST + if entry.options.get(CONF_RADAR_UPDATES, False): + update_features |= UpdateFeature.RADAR if entry.options.get(CONF_STATION_UPDATES, True): update_features |= UpdateFeature.STATION diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index e2b0b436c8c..80b5c07e6bd 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -17,10 +17,11 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaOptionsFlowHandler, ) -from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN +from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN OPTIONS_SCHEMA = vol.Schema( { + vol.Required(CONF_RADAR_UPDATES, default=False): bool, vol.Required(CONF_STATION_UPDATES, default=True): bool, } ) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 665075c4093..b79a94d209d 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -51,8 +51,9 @@ from homeassistant.components.weather import ( from homeassistant.const import Platform ATTRIBUTION = "Powered by AEMET OpenData" +CONF_RADAR_UPDATES = "radar_updates" CONF_STATION_UPDATES = "station_updates" -PLATFORMS = [Platform.SENSOR, Platform.WEATHER] +PLATFORMS = [Platform.IMAGE, Platform.SENSOR, Platform.WEATHER] DEFAULT_NAME = "AEMET" DOMAIN = "aemet" diff --git a/homeassistant/components/aemet/diagnostics.py b/homeassistant/components/aemet/diagnostics.py index bc366fc6d44..b072309d4b8 100644 --- a/homeassistant/components/aemet/diagnostics.py +++ b/homeassistant/components/aemet/diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from aemet_opendata.const import AOD_COORDS +from aemet_opendata.const import AOD_COORDS, AOD_IMG_BYTES from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import ( @@ -26,6 +26,7 @@ TO_REDACT_CONFIG = [ TO_REDACT_COORD = [ AOD_COORDS, + AOD_IMG_BYTES, ] diff --git a/homeassistant/components/aemet/image.py b/homeassistant/components/aemet/image.py new file mode 100644 index 00000000000..ffc53022e4c --- /dev/null +++ b/homeassistant/components/aemet/image.py @@ -0,0 +1,86 @@ +"""Support for the AEMET OpenData images.""" + +from __future__ import annotations + +from typing import Final + +from aemet_opendata.const import AOD_DATETIME, AOD_IMG_BYTES, AOD_IMG_TYPE, AOD_RADAR +from aemet_opendata.helpers import dict_nested_value + +from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator +from .entity import AemetEntity + +AEMET_IMAGES: Final[tuple[ImageEntityDescription, ...]] = ( + ImageEntityDescription( + key=AOD_RADAR, + translation_key="weather_radar", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AemetConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AEMET OpenData image entities based on a config entry.""" + domain_data = config_entry.runtime_data + name = domain_data.name + coordinator = domain_data.coordinator + + unique_id = config_entry.unique_id + assert unique_id is not None + + async_add_entities( + AemetImage( + hass, + name, + coordinator, + description, + unique_id, + ) + for description in AEMET_IMAGES + if dict_nested_value(coordinator.data["lib"], [description.key]) is not None + ) + + +class AemetImage(AemetEntity, ImageEntity): + """Implementation of an AEMET OpenData image.""" + + entity_description: ImageEntityDescription + + def __init__( + self, + hass: HomeAssistant, + name: str, + coordinator: WeatherUpdateCoordinator, + description: ImageEntityDescription, + unique_id: str, + ) -> None: + """Initialize the image.""" + super().__init__(coordinator, name, unique_id) + ImageEntity.__init__(self, hass) + self.entity_description = description + self._attr_unique_id = f"{unique_id}-{description.key}" + + self._async_update_attrs() + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update image attributes.""" + image_data = self.get_aemet_value([self.entity_description.key]) + self._cached_image = Image( + content_type=image_data.get(AOD_IMG_TYPE), + content=image_data.get(AOD_IMG_BYTES), + ) + self._attr_image_last_updated = image_data.get(AOD_DATETIME) diff --git a/homeassistant/components/aemet/strings.json b/homeassistant/components/aemet/strings.json index 75c810978ad..d65c546b050 100644 --- a/homeassistant/components/aemet/strings.json +++ b/homeassistant/components/aemet/strings.json @@ -18,10 +18,18 @@ } } }, + "entity": { + "image": { + "weather_radar": { + "name": "Weather radar" + } + } + }, "options": { "step": { "init": { "data": { + "radar_updates": "Gather data from AEMET weather radar", "station_updates": "Gather data from AEMET weather stations" } } diff --git a/tests/components/aemet/snapshots/test_diagnostics.ambr b/tests/components/aemet/snapshots/test_diagnostics.ambr index 54546507dfa..0e40cce1b86 100644 --- a/tests/components/aemet/snapshots/test_diagnostics.ambr +++ b/tests/components/aemet/snapshots/test_diagnostics.ambr @@ -17,6 +17,7 @@ 'entry_id': '7442b231f139e813fc1939281123f220', 'minor_version': 1, 'options': dict({ + 'radar_updates': True, }), 'pref_disable_new_entities': False, 'pref_disable_polling': False, @@ -33,6 +34,12 @@ ]), }), 'lib': dict({ + 'radar': dict({ + 'datetime': '2021-01-09T11:34:06.448809+00:00', + 'id': 'national', + 'image-bytes': '**REDACTED**', + 'image-type': 'image/gif', + }), 'station': dict({ 'altitude': 667.0, 'coordinates': '**REDACTED**', diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index 0f3491b1c43..3dd8303c8cb 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -6,7 +6,11 @@ from aemet_opendata.exceptions import AuthError from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN +from homeassistant.components.aemet.const import ( + CONF_RADAR_UPDATES, + CONF_STATION_UPDATES, + DOMAIN, +) from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant @@ -61,13 +65,20 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @pytest.mark.parametrize( - ("user_input", "expected"), [({}, True), ({CONF_STATION_UPDATES: False}, False)] + ("user_input", "expected"), + [ + ({}, {CONF_RADAR_UPDATES: False, CONF_STATION_UPDATES: True}), + ( + {CONF_RADAR_UPDATES: False, CONF_STATION_UPDATES: False}, + {CONF_RADAR_UPDATES: False, CONF_STATION_UPDATES: False}, + ), + ], ) async def test_form_options( hass: HomeAssistant, freezer: FrozenDateTimeFactory, user_input: dict[str, bool], - expected: bool, + expected: dict[str, bool], ) -> None: """Test the form options.""" @@ -98,7 +109,8 @@ async def test_form_options( assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { - CONF_STATION_UPDATES: expected, + CONF_RADAR_UPDATES: expected[CONF_RADAR_UPDATES], + CONF_STATION_UPDATES: expected[CONF_STATION_UPDATES], } await hass.async_block_till_done() diff --git a/tests/components/aemet/test_image.py b/tests/components/aemet/test_image.py new file mode 100644 index 00000000000..4321daac883 --- /dev/null +++ b/tests/components/aemet/test_image.py @@ -0,0 +1,22 @@ +"""The image tests for the AEMET OpenData platform.""" + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + + +async def test_aemet_create_images( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test creation of AEMET images.""" + + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) + + state = hass.states.get("image.aemet_weather_radar") + assert state is not None + assert state.state == "2021-01-09T11:34:06.448809+00:00" diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py index 162ee657513..0361ca9e6d8 100644 --- a/tests/components/aemet/util.py +++ b/tests/components/aemet/util.py @@ -3,9 +3,9 @@ from typing import Any from unittest.mock import patch -from aemet_opendata.const import ATTR_DATA +from aemet_opendata.const import ATTR_BYTES, ATTR_DATA, ATTR_TIMESTAMP, ATTR_TYPE -from homeassistant.components.aemet.const import DOMAIN +from homeassistant.components.aemet.const import CONF_RADAR_UPDATES, DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant @@ -19,6 +19,14 @@ FORECAST_HOURLY_DATA_MOCK = { ATTR_DATA: load_json_value_fixture("aemet/town-28065-forecast-hourly-data.json"), } +RADAR_DATA_MOCK = { + ATTR_DATA: { + ATTR_TYPE: "image/gif", + ATTR_BYTES: bytes([0]), + }, + ATTR_TIMESTAMP: "2021-01-09T11:34:06.448809+00:00", +} + STATION_DATA_MOCK = { ATTR_DATA: load_json_value_fixture("aemet/station-3195-data.json"), } @@ -53,6 +61,9 @@ def mock_api_call(cmd: str, fetch_data: bool = False) -> dict[str, Any]: return FORECAST_DAILY_DATA_MOCK if cmd == "prediccion/especifica/municipio/horaria/28065": return FORECAST_HOURLY_DATA_MOCK + if cmd == "red/radar/nacional": + return RADAR_DATA_MOCK + return {} @@ -69,6 +80,9 @@ async def async_init_integration(hass: HomeAssistant): }, entry_id="7442b231f139e813fc1939281123f220", unique_id="40.30403754--3.72935236", + options={ + CONF_RADAR_UPDATES: True, + }, ) config_entry.add_to_hass(hass)