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,
ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY, ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_PRESSURE,
ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
DOMAIN as WEATHER_DOMAIN, 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}" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}"
FORECAST_MAP = { FORECAST_MAP = {
ATTR_FORECAST_CONDITION: "condition",
ATTR_FORECAST_NATIVE_PRESSURE: "pressure", ATTR_FORECAST_NATIVE_PRESSURE: "pressure",
ATTR_FORECAST_PRECIPITATION: "precipitation", ATTR_FORECAST_PRECIPITATION: "precipitation",
ATTR_FORECAST_NATIVE_TEMP: "temperature", ATTR_FORECAST_NATIVE_TEMP: "temperature",
ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_NATIVE_TEMP_LOW: "templow",
ATTR_FORECAST_TIME: "datetime",
ATTR_FORECAST_WIND_BEARING: "wind_bearing", ATTR_FORECAST_WIND_BEARING: "wind_bearing",
ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed",
} }

View File

@ -1,10 +1,13 @@
"""Support for Met Éireann weather service.""" """Support for Met Éireann weather service."""
import logging import logging
from typing import cast
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
Forecast,
WeatherEntity, WeatherEntity,
WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -16,7 +19,7 @@ from homeassistant.const import (
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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__) _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.""" """Map the conditions provided by the weather API to those supported by the frontend."""
if condition is not None: if condition is not None:
for key, value in CONDITION_MAP.items(): for key, value in CONDITION_MAP.items():
@ -60,6 +63,9 @@ class MetEireannWeather(CoordinatorEntity, 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__(self, coordinator, config, hourly): def __init__(self, coordinator, config, hourly):
"""Initialise the platform with a data instance and site.""" """Initialise the platform with a data instance and site."""
@ -67,6 +73,15 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
self._config = config self._config = config
self._hourly = hourly 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 @property
def unique_id(self): def unique_id(self):
"""Return unique ID.""" """Return unique ID."""
@ -126,35 +141,51 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
"""Return the wind direction.""" """Return the wind direction."""
return self.coordinator.data.current_weather_data.get("wind_bearing") return self.coordinator.data.current_weather_data.get("wind_bearing")
@property def _forecast(self, hourly: bool) -> list[Forecast]:
def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
if self._hourly: if hourly:
me_forecast = self.coordinator.data.hourly_forecast me_forecast = self.coordinator.data.hourly_forecast
else: else:
me_forecast = self.coordinator.data.daily_forecast me_forecast = self.coordinator.data.daily_forecast
required_keys = {"temperature", "datetime"} required_keys = {"temperature", "datetime"}
ha_forecast = [] ha_forecast: list[Forecast] = []
for item in me_forecast: for item in me_forecast:
if not set(item).issuperset(required_keys): if not set(item).issuperset(required_keys):
continue continue
ha_item = { ha_item: Forecast = cast(
k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None Forecast,
} {
if ha_item.get(ATTR_FORECAST_CONDITION): k: item[v]
ha_item[ATTR_FORECAST_CONDITION] = format_condition( for k, v in FORECAST_MAP.items()
ha_item[ATTR_FORECAST_CONDITION] if item.get(v) is not None
) },
# Convert timestamp to UTC )
if ha_item.get(ATTR_FORECAST_TIME): # 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[ATTR_FORECAST_TIME] = dt_util.as_utc(
ha_item.get(ATTR_FORECAST_TIME) item["datetime"]
).isoformat() ).isoformat()
ha_forecast.append(ha_item) ha_forecast.append(ha_item)
return ha_forecast 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 @property
def device_info(self): def device_info(self):
"""Device info.""" """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.""" """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.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 homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator
async def test_weather(hass: HomeAssistant, mock_weather) -> None: async def setup_config_entry(hass: HomeAssistant) -> ConfigEntry:
"""Test weather entity.""" """Create a mock configuration for testing."""
# Create a mock configuration for testing
mock_data = MockConfigEntry( mock_data = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, 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.config_entries.async_setup(mock_data.entry_id)
await hass.async_block_till_done() 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(hass.states.async_entity_ids("weather")) == 1
assert len(mock_weather.mock_calls) == 4 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.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 0 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