Modernize met_eireann weather (#98030)

This commit is contained in:
Erik Montnemery 2023-08-08 14:21:52 +02:00 committed by GitHub
parent 50ef674902
commit 0d55a7600e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 277 additions and 23 deletions

View File

@ -9,13 +9,11 @@ from homeassistant.components.weather import (
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRESSURE,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
DOMAIN as WEATHER_DOMAIN,
)
@ -29,12 +27,10 @@ HOME_LOCATION_NAME = "Home"
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}"
FORECAST_MAP = {
ATTR_FORECAST_CONDITION: "condition",
ATTR_FORECAST_NATIVE_PRESSURE: "pressure",
ATTR_FORECAST_PRECIPITATION: "precipitation",
ATTR_FORECAST_NATIVE_TEMP: "temperature",
ATTR_FORECAST_NATIVE_TEMP_LOW: "templow",
ATTR_FORECAST_TIME: "datetime",
ATTR_FORECAST_WIND_BEARING: "wind_bearing",
ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed",
}

View File

@ -1,10 +1,13 @@
"""Support for Met Éireann weather service."""
import logging
from typing import cast
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_TIME,
Forecast,
WeatherEntity,
WeatherEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -16,7 +19,7 @@ from homeassistant.const import (
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -28,7 +31,7 @@ from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP
_LOGGER = logging.getLogger(__name__)
def format_condition(condition: str):
def format_condition(condition: str | None) -> str | None:
"""Map the conditions provided by the weather API to those supported by the frontend."""
if condition is not None:
for key, value in CONDITION_MAP.items():
@ -60,6 +63,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
_attr_native_pressure_unit = UnitOfPressure.HPA
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_supported_features = (
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
)
def __init__(self, coordinator, config, hourly):
"""Initialise the platform with a data instance and site."""
@ -67,6 +73,15 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
self._config = config
self._hourly = hourly
@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
def unique_id(self):
"""Return unique ID."""
@ -126,35 +141,51 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
"""Return the wind direction."""
return self.coordinator.data.current_weather_data.get("wind_bearing")
@property
def forecast(self):
def _forecast(self, hourly: bool) -> list[Forecast]:
"""Return the forecast array."""
if self._hourly:
if hourly:
me_forecast = self.coordinator.data.hourly_forecast
else:
me_forecast = self.coordinator.data.daily_forecast
required_keys = {"temperature", "datetime"}
ha_forecast = []
ha_forecast: list[Forecast] = []
for item in me_forecast:
if not set(item).issuperset(required_keys):
continue
ha_item = {
k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None
}
if ha_item.get(ATTR_FORECAST_CONDITION):
ha_item[ATTR_FORECAST_CONDITION] = format_condition(
ha_item[ATTR_FORECAST_CONDITION]
)
# Convert timestamp to UTC
if ha_item.get(ATTR_FORECAST_TIME):
ha_item: Forecast = cast(
Forecast,
{
k: item[v]
for k, v in FORECAST_MAP.items()
if item.get(v) is not None
},
)
# Convert condition
if item.get("condition"):
ha_item[ATTR_FORECAST_CONDITION] = format_condition(item["condition"])
# Convert timestamp to UTC string
if item.get("datetime"):
ha_item[ATTR_FORECAST_TIME] = dt_util.as_utc(
ha_item.get(ATTR_FORECAST_TIME)
item["datetime"]
).isoformat()
ha_forecast.append(ha_item)
return ha_forecast
@property
def forecast(self) -> list[Forecast]:
"""Return the forecast array."""
return self._forecast(self._hourly)
async def async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast(False)
async def async_forecast_hourly(self) -> list[Forecast]:
"""Return the hourly forecast in native units."""
return self._forecast(True)
@property
def device_info(self):
"""Device info."""

View File

@ -0,0 +1,89 @@
# serializer version: 1
# name: test_forecast_service
dict({
'forecast': list([
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-08T12:00:00+00:00',
'temperature': 10.0,
}),
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-09T12:00:00+00:00',
'temperature': 20.0,
}),
]),
})
# ---
# name: test_forecast_service.1
dict({
'forecast': list([
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-08T12:00:00+00:00',
'temperature': 10.0,
}),
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-09T12:00:00+00:00',
'temperature': 20.0,
}),
]),
})
# ---
# name: test_forecast_subscription[daily]
list([
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-08T12:00:00+00:00',
'temperature': 10.0,
}),
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-09T12:00:00+00:00',
'temperature': 20.0,
}),
])
# ---
# name: test_forecast_subscription[daily].1
list([
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-08T12:00:00+00:00',
'temperature': 15.0,
}),
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-09T12:00:00+00:00',
'temperature': 25.0,
}),
])
# ---
# name: test_forecast_subscription[hourly]
list([
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-08T12:00:00+00:00',
'temperature': 10.0,
}),
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-09T12:00:00+00:00',
'temperature': 20.0,
}),
])
# ---
# name: test_forecast_subscription[hourly].1
list([
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-08T12:00:00+00:00',
'temperature': 15.0,
}),
dict({
'condition': 'lightning-rainy',
'datetime': '2023-08-09T12:00:00+00:00',
'temperature': 25.0,
}),
])
# ---

View File

@ -1,13 +1,25 @@
"""Test Met Éireann weather entity."""
import datetime
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.met_eireann import UPDATE_INTERVAL
from homeassistant.components.met_eireann.const import DOMAIN
from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN,
SERVICE_GET_FORECAST,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator
async def test_weather(hass: HomeAssistant, mock_weather) -> None:
"""Test weather entity."""
# Create a mock configuration for testing
async def setup_config_entry(hass: HomeAssistant) -> ConfigEntry:
"""Create a mock configuration for testing."""
mock_data = MockConfigEntry(
domain=DOMAIN,
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
@ -16,6 +28,12 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None:
await hass.config_entries.async_setup(mock_data.entry_id)
await hass.async_block_till_done()
return mock_data
async def test_weather(hass: HomeAssistant, mock_weather) -> None:
"""Test weather entity."""
await setup_config_entry(hass)
assert len(hass.states.async_entity_ids("weather")) == 1
assert len(mock_weather.mock_calls) == 4
@ -29,3 +47,123 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None:
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 0
async def test_forecast_service(
hass: HomeAssistant,
mock_weather,
snapshot: SnapshotAssertion,
) -> None:
"""Test multiple forecast."""
mock_weather.get_forecast.return_value = [
{
"condition": "SleetSunThunder",
"datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC),
"temperature": 10.0,
},
{
"condition": "SleetSunThunder",
"datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC),
"temperature": 20.0,
},
]
await setup_config_entry(hass)
assert len(hass.states.async_entity_ids("weather")) == 1
entity_id = hass.states.async_entity_ids("weather")[0]
response = await hass.services.async_call(
WEATHER_DOMAIN,
SERVICE_GET_FORECAST,
{
"entity_id": entity_id,
"type": "daily",
},
blocking=True,
return_response=True,
)
assert response == snapshot
response = await hass.services.async_call(
WEATHER_DOMAIN,
SERVICE_GET_FORECAST,
{
"entity_id": entity_id,
"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,
mock_weather,
snapshot: SnapshotAssertion,
forecast_type: str,
) -> None:
"""Test multiple forecast."""
client = await hass_ws_client(hass)
mock_weather.get_forecast.return_value = [
{
"condition": "SleetSunThunder",
"datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC),
"temperature": 10.0,
},
{
"condition": "SleetSunThunder",
"datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC),
"temperature": 20.0,
},
]
await setup_config_entry(hass)
assert len(hass.states.async_entity_ids("weather")) == 1
entity_id = hass.states.async_entity_ids("weather")[0]
await client.send_json_auto_id(
{
"type": "weather/subscribe_forecast",
"forecast_type": forecast_type,
"entity_id": entity_id,
}
)
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
mock_weather.get_forecast.return_value = [
{
"condition": "SleetSunThunder",
"datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC),
"temperature": 15.0,
},
{
"condition": "SleetSunThunder",
"datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC),
"temperature": 25.0,
},
]
freezer.tick(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