Modernize aemet weather (#97969)

* Modernize aemet weather

* Improve test coverage

* Only create a single entity for new config entries
This commit is contained in:
Erik Montnemery 2023-08-15 20:55:16 +02:00 committed by GitHub
parent 857369625a
commit caeb20f9c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 1445 additions and 15 deletions

View File

@ -1,4 +1,6 @@
"""Support for the AEMET OpenData service.""" """Support for the AEMET OpenData service."""
from typing import cast
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_PRECIPITATION,
@ -8,7 +10,10 @@ from homeassistant.components.weather import (
ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
DOMAIN as WEATHER_DOMAIN,
Forecast,
WeatherEntity, WeatherEntity,
WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -17,7 +22,8 @@ from homeassistant.const import (
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -79,10 +85,28 @@ async def async_setup_entry(
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
entities = [] entities = []
for mode in FORECAST_MODES: entity_registry = er.async_get(hass)
name = f"{domain_data[ENTRY_NAME]} {mode}"
unique_id = f"{config_entry.unique_id} {mode}" # Add daily + hourly entity for legacy config entries, only add daily for new
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) # config entries. This can be removed in HA Core 2024.3
if entity_registry.async_get_entity_id(
WEATHER_DOMAIN,
DOMAIN,
f"{config_entry.unique_id} {FORECAST_MODE_HOURLY}",
):
for mode in FORECAST_MODES:
name = f"{domain_data[ENTRY_NAME]} {mode}"
unique_id = f"{config_entry.unique_id} {mode}"
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
else:
entities.append(
AemetWeather(
domain_data[ENTRY_NAME],
config_entry.unique_id,
weather_coordinator,
FORECAST_MODE_DAILY,
)
)
async_add_entities(entities, False) async_add_entities(entities, False)
@ -95,6 +119,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
_attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_pressure_unit = UnitOfPressure.HPA
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_supported_features = (
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
)
def __init__( def __init__(
self, self,
@ -112,20 +139,44 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
self._attr_name = name self._attr_name = name
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
super()._handle_coordinator_update()
assert self.platform.config_entry
self.platform.config_entry.async_create_task(
self.hass, self.async_update_listeners(("daily", "hourly"))
)
@property @property
def condition(self): def condition(self):
"""Return the current condition.""" """Return the current condition."""
return self.coordinator.data[ATTR_API_CONDITION] return self.coordinator.data[ATTR_API_CONDITION]
@property def _forecast(self, forecast_mode: str) -> list[Forecast]:
def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[forecast_mode]]
forecast_map = FORECAST_MAP[self._forecast_mode] forecast_map = FORECAST_MAP[forecast_mode]
return [ return cast(
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} list[Forecast],
for forecast in forecasts [
] {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
for forecast in forecasts
],
)
@property
def forecast(self) -> list[Forecast]:
"""Return the forecast array."""
return self._forecast(self._forecast_mode)
async def async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast(FORECAST_MODE_DAILY)
async def async_forecast_hourly(self) -> list[Forecast]:
"""Return the hourly forecast in native units."""
return self._forecast(FORECAST_MODE_HOURLY)
@property @property
def humidity(self): def humidity(self):

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,16 @@
"""The sensor tests for the AEMET OpenData platform.""" """The sensor tests for the AEMET OpenData platform."""
import datetime
from unittest.mock import patch from unittest.mock import patch
from homeassistant.components.aemet.const import ATTRIBUTION from freezegun.api import FrozenDateTimeFactory
import pytest
import requests_mock
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.aemet.const import ATTRIBUTION, DOMAIN
from homeassistant.components.aemet.weather_update_coordinator import (
WEATHER_UPDATE_INTERVAL,
)
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_PARTLYCLOUDY, ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_SNOWY, ATTR_CONDITION_SNOWY,
@ -19,17 +28,65 @@ from homeassistant.components.weather import (
ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_TEMPERATURE,
ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_BEARING,
ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED,
DOMAIN as WEATHER_DOMAIN,
SERVICE_GET_FORECAST,
) )
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .util import async_init_integration from .util import aemet_requests_mock, async_init_integration
from tests.typing import WebSocketGenerator
async def test_aemet_weather(hass: HomeAssistant) -> None: async def test_aemet_weather(hass: HomeAssistant) -> None:
"""Test states of the weather.""" """Test states of the weather."""
hass.config.set_time_zone("UTC")
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
):
await async_init_integration(hass)
state = hass.states.get("weather.aemet")
assert state
assert state.state == ATTR_CONDITION_SNOWY
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0
assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4 # 100440.0 Pa -> hPa
assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7
assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0
assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15.0 # 4.17 m/s -> km/h
forecast = state.attributes.get(ATTR_FORECAST)[0]
assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY
assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None
assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30
assert forecast.get(ATTR_FORECAST_TEMP) == 4
assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4
assert (
forecast.get(ATTR_FORECAST_TIME)
== dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat()
)
assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0
assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20.0 # 5.56 m/s -> km/h
state = hass.states.get("weather.aemet_hourly")
assert state is None
async def test_aemet_weather_legacy(hass: HomeAssistant) -> None:
"""Test states of the weather."""
registry = er.async_get(hass)
registry.async_get_or_create(
WEATHER_DOMAIN,
DOMAIN,
"None hourly",
)
hass.config.set_time_zone("UTC") hass.config.set_time_zone("UTC")
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
with patch("homeassistant.util.dt.now", return_value=now), patch( with patch("homeassistant.util.dt.now", return_value=now), patch(
@ -61,3 +118,87 @@ async def test_aemet_weather(hass: HomeAssistant) -> None:
state = hass.states.get("weather.aemet_hourly") state = hass.states.get("weather.aemet_hourly")
assert state is None assert state is None
async def test_forecast_service(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test multiple forecast."""
hass.config.set_time_zone("UTC")
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
):
await async_init_integration(hass)
response = await hass.services.async_call(
WEATHER_DOMAIN,
SERVICE_GET_FORECAST,
{
"entity_id": "weather.aemet",
"type": "daily",
},
blocking=True,
return_response=True,
)
assert response == snapshot
response = await hass.services.async_call(
WEATHER_DOMAIN,
SERVICE_GET_FORECAST,
{
"entity_id": "weather.aemet",
"type": "hourly",
},
blocking=True,
return_response=True,
)
assert response == snapshot
@pytest.mark.parametrize("forecast_type", ["daily", "hourly"])
async def test_forecast_subscription(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
forecast_type: str,
) -> None:
"""Test multiple forecast."""
client = await hass_ws_client(hass)
hass.config.set_time_zone("UTC")
freezer.move_to("2021-01-09 12:00:00+00:00")
await async_init_integration(hass)
await client.send_json_auto_id(
{
"type": "weather/subscribe_forecast",
"forecast_type": forecast_type,
"entity_id": "weather.aemet",
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] is None
subscription_id = msg["id"]
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["type"] == "event"
forecast1 = msg["event"]["forecast"]
assert forecast1 == snapshot
with requests_mock.mock() as _m:
aemet_requests_mock(_m)
freezer.tick(WEATHER_UPDATE_INTERVAL + datetime.timedelta(seconds=1))
await hass.async_block_till_done()
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["type"] == "event"
forecast2 = msg["event"]["forecast"]
assert forecast2 == snapshot