Add AEMET Weather Radar images (#131386)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Álvaro Fernández Rojas 2024-12-23 15:51:21 +01:00 committed by GitHub
parent 43a420cf01
commit 5ef12c3993
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 164 additions and 10 deletions

View File

@ -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

View File

@ -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,
}
)

View File

@ -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"

View File

@ -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,
]

View File

@ -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)

View File

@ -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"
}
}

View File

@ -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**',

View File

@ -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()

View File

@ -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"

View File

@ -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)