Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AEMET Weather Radar images #131386

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion homeassistant/components/aemet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client

from .const import CONF_STATION_UPDATES, PLATFORMS
from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, PLATFORMS
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator

_LOGGER = logging.getLogger(__name__)
Expand All @@ -24,6 +24,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

Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/aemet/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
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,
}
)
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/aemet/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@
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"

Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/aemet/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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 (
Expand All @@ -26,6 +26,7 @@

TO_REDACT_COORD = [
AOD_COORDS,
AOD_IMG_BYTES,
]


Expand Down
93 changes: 93 additions & 0 deletions homeassistant/components/aemet/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Support for the AEMET OpenData images."""

from __future__ import annotations

from dataclasses import dataclass
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


@dataclass(frozen=True, kw_only=True)
class AemetImageEntityDescription(ImageEntityDescription):
"""A class that describes AEMET OpenData image entities."""


AEMET_IMAGES: Final[tuple[AemetImageEntityDescription, ...]] = (
AemetImageEntityDescription(
key=AOD_RADAR,
name="Weather Radar image",
),
)


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: AemetImageEntityDescription

def __init__(
self,
hass: HomeAssistant,
name: str,
coordinator: WeatherUpdateCoordinator,
description: AemetImageEntityDescription,
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)
1 change: 1 addition & 0 deletions homeassistant/components/aemet/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"step": {
"init": {
"data": {
"radar_updates": "Gather data from AEMET weather radar",
"station_updates": "Gather data from AEMET weather stations"
}
}
Expand Down
7 changes: 7 additions & 0 deletions tests/components/aemet/snapshots/test_diagnostics.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'entry_id': '7442b231f139e813fc1939281123f220',
'minor_version': 1,
'options': dict({
'radar_updates': True,
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
Expand All @@ -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**',
Expand Down
20 changes: 16 additions & 4 deletions tests/components/aemet/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
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
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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()
Expand Down
22 changes: 22 additions & 0 deletions tests/components/aemet/test_image.py
Original file line number Diff line number Diff line change
@@ -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_image")
assert state is not None
assert state.state == "2021-01-09T11:34:06.448809+00:00"
18 changes: 16 additions & 2 deletions tests/components/aemet/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -19,6 +19,14 @@
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"),
}
Expand Down Expand Up @@ -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 {}


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

Expand Down