mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 10:47:10 +00:00
Add AEMET Weather Radar images (#131386)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
43a420cf01
commit
5ef12c3993
@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
from homeassistant.helpers.storage import STORAGE_DIR
|
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
|
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -26,6 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
|
|||||||
latitude = entry.data[CONF_LATITUDE]
|
latitude = entry.data[CONF_LATITUDE]
|
||||||
longitude = entry.data[CONF_LONGITUDE]
|
longitude = entry.data[CONF_LONGITUDE]
|
||||||
update_features: int = UpdateFeature.FORECAST
|
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):
|
if entry.options.get(CONF_STATION_UPDATES, True):
|
||||||
update_features |= UpdateFeature.STATION
|
update_features |= UpdateFeature.STATION
|
||||||
|
|
||||||
|
@ -17,10 +17,11 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||||||
SchemaOptionsFlowHandler,
|
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(
|
OPTIONS_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Required(CONF_RADAR_UPDATES, default=False): bool,
|
||||||
vol.Required(CONF_STATION_UPDATES, default=True): bool,
|
vol.Required(CONF_STATION_UPDATES, default=True): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -51,8 +51,9 @@ from homeassistant.components.weather import (
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||||
|
CONF_RADAR_UPDATES = "radar_updates"
|
||||||
CONF_STATION_UPDATES = "station_updates"
|
CONF_STATION_UPDATES = "station_updates"
|
||||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
PLATFORMS = [Platform.IMAGE, Platform.SENSOR, Platform.WEATHER]
|
||||||
DEFAULT_NAME = "AEMET"
|
DEFAULT_NAME = "AEMET"
|
||||||
DOMAIN = "aemet"
|
DOMAIN = "aemet"
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
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.components.diagnostics import async_redact_data
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -26,6 +26,7 @@ TO_REDACT_CONFIG = [
|
|||||||
|
|
||||||
TO_REDACT_COORD = [
|
TO_REDACT_COORD = [
|
||||||
AOD_COORDS,
|
AOD_COORDS,
|
||||||
|
AOD_IMG_BYTES,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
86
homeassistant/components/aemet/image.py
Normal file
86
homeassistant/components/aemet/image.py
Normal 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)
|
@ -18,10 +18,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"entity": {
|
||||||
|
"image": {
|
||||||
|
"weather_radar": {
|
||||||
|
"name": "Weather radar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"radar_updates": "Gather data from AEMET weather radar",
|
||||||
"station_updates": "Gather data from AEMET weather stations"
|
"station_updates": "Gather data from AEMET weather stations"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
'entry_id': '7442b231f139e813fc1939281123f220',
|
'entry_id': '7442b231f139e813fc1939281123f220',
|
||||||
'minor_version': 1,
|
'minor_version': 1,
|
||||||
'options': dict({
|
'options': dict({
|
||||||
|
'radar_updates': True,
|
||||||
}),
|
}),
|
||||||
'pref_disable_new_entities': False,
|
'pref_disable_new_entities': False,
|
||||||
'pref_disable_polling': False,
|
'pref_disable_polling': False,
|
||||||
@ -33,6 +34,12 @@
|
|||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
'lib': dict({
|
'lib': dict({
|
||||||
|
'radar': dict({
|
||||||
|
'datetime': '2021-01-09T11:34:06.448809+00:00',
|
||||||
|
'id': 'national',
|
||||||
|
'image-bytes': '**REDACTED**',
|
||||||
|
'image-type': 'image/gif',
|
||||||
|
}),
|
||||||
'station': dict({
|
'station': dict({
|
||||||
'altitude': 667.0,
|
'altitude': 667.0,
|
||||||
'coordinates': '**REDACTED**',
|
'coordinates': '**REDACTED**',
|
||||||
|
@ -6,7 +6,11 @@ from aemet_opendata.exceptions import AuthError
|
|||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
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.config_entries import SOURCE_USER, ConfigEntryState
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -61,13 +65,20 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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(
|
async def test_form_options(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
user_input: dict[str, bool],
|
user_input: dict[str, bool],
|
||||||
expected: bool,
|
expected: dict[str, bool],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the form options."""
|
"""Test the form options."""
|
||||||
|
|
||||||
@ -98,7 +109,8 @@ async def test_form_options(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert entry.options == {
|
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()
|
await hass.async_block_till_done()
|
||||||
|
22
tests/components/aemet/test_image.py
Normal file
22
tests/components/aemet/test_image.py
Normal 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"
|
@ -3,9 +3,9 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
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.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
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"),
|
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 = {
|
STATION_DATA_MOCK = {
|
||||||
ATTR_DATA: load_json_value_fixture("aemet/station-3195-data.json"),
|
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
|
return FORECAST_DAILY_DATA_MOCK
|
||||||
if cmd == "prediccion/especifica/municipio/horaria/28065":
|
if cmd == "prediccion/especifica/municipio/horaria/28065":
|
||||||
return FORECAST_HOURLY_DATA_MOCK
|
return FORECAST_HOURLY_DATA_MOCK
|
||||||
|
if cmd == "red/radar/nacional":
|
||||||
|
return RADAR_DATA_MOCK
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@ -69,6 +80,9 @@ async def async_init_integration(hass: HomeAssistant):
|
|||||||
},
|
},
|
||||||
entry_id="7442b231f139e813fc1939281123f220",
|
entry_id="7442b231f139e813fc1939281123f220",
|
||||||
unique_id="40.30403754--3.72935236",
|
unique_id="40.30403754--3.72935236",
|
||||||
|
options={
|
||||||
|
CONF_RADAR_UPDATES: True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user