Add twice-daily forecasts to MetOffice (#145472)

This commit is contained in:
avee87 2025-05-26 15:56:15 +01:00 committed by GitHub
parent acbfe54c7b
commit ca50fca738
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 458 additions and 4 deletions

View File

@ -29,6 +29,7 @@ from .const import (
METOFFICE_DAILY_COORDINATOR,
METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME,
METOFFICE_TWICE_DAILY_COORDINATOR,
)
from .helpers import fetch_data
@ -59,6 +60,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
fetch_data, connection, latitude, longitude, "daily"
)
async def async_update_twice_daily() -> datapoint.Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "twice-daily"
)
metoffice_hourly_coordinator = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
@ -77,10 +83,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
update_interval=DEFAULT_SCAN_INTERVAL,
)
metoffice_twice_daily_coordinator = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=f"MetOffice Twice Daily Coordinator for {site_name}",
update_method=async_update_twice_daily,
update_interval=DEFAULT_SCAN_INTERVAL,
)
metoffice_hass_data = hass.data.setdefault(DOMAIN, {})
metoffice_hass_data[entry.entry_id] = {
METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator,
METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator,
METOFFICE_TWICE_DAILY_COORDINATOR: metoffice_twice_daily_coordinator,
METOFFICE_NAME: site_name,
METOFFICE_COORDINATES: coordinates,
}

View File

@ -41,6 +41,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=15)
METOFFICE_COORDINATES = "metoffice_coordinates"
METOFFICE_HOURLY_COORDINATOR = "metoffice_hourly_coordinator"
METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator"
METOFFICE_TWICE_DAILY_COORDINATOR = "metoffice_twice_daily_coordinator"
METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions"
METOFFICE_NAME = "metoffice_name"
@ -92,3 +93,28 @@ DAILY_FORECAST_ATTRIBUTE_MAP: dict[str, str] = {
ATTR_FORECAST_NATIVE_WIND_SPEED: "midday10MWindSpeed",
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midday10MWindGust",
}
DAY_FORECAST_ATTRIBUTE_MAP: dict[str, str] = {
ATTR_FORECAST_CONDITION: "daySignificantWeatherCode",
ATTR_FORECAST_NATIVE_APPARENT_TEMP: "dayMaxFeelsLikeTemp",
ATTR_FORECAST_NATIVE_PRESSURE: "middayMslp",
ATTR_FORECAST_NATIVE_TEMP: "dayUpperBoundMaxTemp",
ATTR_FORECAST_NATIVE_TEMP_LOW: "dayLowerBoundMaxTemp",
ATTR_FORECAST_PRECIPITATION_PROBABILITY: "dayProbabilityOfPrecipitation",
ATTR_FORECAST_UV_INDEX: "maxUvIndex",
ATTR_FORECAST_WIND_BEARING: "midday10MWindDirection",
ATTR_FORECAST_NATIVE_WIND_SPEED: "midday10MWindSpeed",
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midday10MWindGust",
}
NIGHT_FORECAST_ATTRIBUTE_MAP: dict[str, str] = {
ATTR_FORECAST_CONDITION: "nightSignificantWeatherCode",
ATTR_FORECAST_NATIVE_APPARENT_TEMP: "nightMinFeelsLikeTemp",
ATTR_FORECAST_NATIVE_PRESSURE: "midnightMslp",
ATTR_FORECAST_NATIVE_TEMP: "nightUpperBoundMinTemp",
ATTR_FORECAST_NATIVE_TEMP_LOW: "nightLowerBoundMinTemp",
ATTR_FORECAST_PRECIPITATION_PROBABILITY: "nightProbabilityOfPrecipitation",
ATTR_FORECAST_WIND_BEARING: "midnight10MWindDirection",
ATTR_FORECAST_NATIVE_WIND_SPEED: "midnight10MWindSpeed",
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midnight10MWindGust",
}

View File

@ -9,6 +9,7 @@ from datapoint.Forecast import Forecast as ForecastData
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_IS_DAYTIME,
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
ATTR_FORECAST_NATIVE_PRESSURE,
ATTR_FORECAST_NATIVE_TEMP,
@ -41,12 +42,15 @@ from .const import (
ATTRIBUTION,
CONDITION_MAP,
DAILY_FORECAST_ATTRIBUTE_MAP,
DAY_FORECAST_ATTRIBUTE_MAP,
DOMAIN,
HOURLY_FORECAST_ATTRIBUTE_MAP,
METOFFICE_COORDINATES,
METOFFICE_DAILY_COORDINATOR,
METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME,
METOFFICE_TWICE_DAILY_COORDINATOR,
NIGHT_FORECAST_ATTRIBUTE_MAP,
)
from .helpers import get_attribute
@ -73,6 +77,7 @@ async def async_setup_entry(
MetOfficeWeather(
hass_data[METOFFICE_DAILY_COORDINATOR],
hass_data[METOFFICE_HOURLY_COORDINATOR],
hass_data[METOFFICE_TWICE_DAILY_COORDINATOR],
hass_data,
)
],
@ -92,6 +97,19 @@ def _build_daily_forecast_data(timestep: dict[str, Any]) -> Forecast:
return data
def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> Forecast:
data = Forecast(datetime=timestep["time"].isoformat())
# day and night forecasts have slightly different format
if "daySignificantWeatherCode" in timestep:
data[ATTR_FORECAST_IS_DAYTIME] = True
_populate_forecast_data(data, timestep, DAY_FORECAST_ATTRIBUTE_MAP)
else:
data[ATTR_FORECAST_IS_DAYTIME] = False
_populate_forecast_data(data, timestep, NIGHT_FORECAST_ATTRIBUTE_MAP)
return data
def _populate_forecast_data(
forecast: Forecast, timestep: dict[str, Any], mapping: dict[str, str]
) -> None:
@ -152,13 +170,16 @@ class MetOfficeWeather(
_attr_native_visibility_unit = UnitOfLength.METERS
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
_attr_supported_features = (
WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_DAILY
WeatherEntityFeature.FORECAST_HOURLY
| WeatherEntityFeature.FORECAST_TWICE_DAILY
| WeatherEntityFeature.FORECAST_DAILY
)
def __init__(
self,
coordinator_daily: TimestampDataUpdateCoordinator[ForecastData],
coordinator_hourly: TimestampDataUpdateCoordinator[ForecastData],
coordinator_twice_daily: TimestampDataUpdateCoordinator[ForecastData],
hass_data: dict[str, Any],
) -> None:
"""Initialise the platform with a data instance."""
@ -167,6 +188,7 @@ class MetOfficeWeather(
observation_coordinator,
daily_coordinator=coordinator_daily,
hourly_coordinator=coordinator_hourly,
twice_daily_coordinator=coordinator_twice_daily,
)
self._attr_device_info = get_device_info(
@ -268,3 +290,17 @@ class MetOfficeWeather(
for timestep in timesteps
if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo)
]
@callback
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
"""Return the twice daily forecast in native units."""
coordinator = cast(
TimestampDataUpdateCoordinator[ForecastData],
self.forecast_coordinators["twice_daily"],
)
timesteps = coordinator.data.timesteps
return [
_build_twice_daily_forecast_data(timestep)
for timestep in timesteps
if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo)
]

View File

@ -724,6 +724,194 @@
})
# ---
# name: test_forecast_service[get_forecasts].2
dict({
'weather.met_office_wavertree': dict({
'forecast': list([
dict({
'apparent_temperature': 4.8,
'condition': 'cloudy',
'datetime': '2024-11-24T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 23,
'pressure': 987.12,
'temperature': 10.7,
'templow': 7.0,
'uv_index': None,
'wind_bearing': 211,
'wind_gust_speed': 47.2,
'wind_speed': 26.39,
}),
dict({
'apparent_temperature': 9.2,
'condition': 'cloudy',
'datetime': '2024-11-24T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 26,
'pressure': 987.48,
'temperature': 15.2,
'templow': 11.9,
'uv_index': 1,
'wind_bearing': 203,
'wind_gust_speed': 42.66,
'wind_speed': 23.94,
}),
dict({
'apparent_temperature': 4.2,
'condition': 'partlycloudy',
'datetime': '2024-11-25T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 6,
'pressure': 1004.81,
'temperature': 9.3,
'templow': 4.4,
'uv_index': None,
'wind_bearing': 262,
'wind_gust_speed': 47.99,
'wind_speed': 29.23,
}),
dict({
'apparent_temperature': 5.3,
'condition': 'partlycloudy',
'datetime': '2024-11-25T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 5,
'pressure': 994.88,
'temperature': 11.0,
'templow': 8.4,
'uv_index': 1,
'wind_bearing': 251,
'wind_gust_speed': 52.16,
'wind_speed': 30.67,
}),
dict({
'apparent_temperature': 1.3,
'condition': 'cloudy',
'datetime': '2024-11-26T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 44,
'pressure': 1013.9,
'temperature': 7.5,
'templow': -0.4,
'uv_index': None,
'wind_bearing': 74,
'wind_gust_speed': 19.51,
'wind_speed': 11.41,
}),
dict({
'apparent_temperature': 5.9,
'condition': 'partlycloudy',
'datetime': '2024-11-26T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 6,
'pressure': 1012.93,
'temperature': 10.1,
'templow': 6.5,
'uv_index': 1,
'wind_bearing': 265,
'wind_gust_speed': 34.49,
'wind_speed': 20.45,
}),
dict({
'apparent_temperature': 0.2,
'condition': 'clear-night',
'datetime': '2024-11-27T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 9,
'pressure': 1021.75,
'temperature': 7.2,
'templow': -3.0,
'uv_index': None,
'wind_bearing': 31,
'wind_gust_speed': 19.94,
'wind_speed': 11.84,
}),
dict({
'apparent_temperature': 3.3,
'condition': 'rainy',
'datetime': '2024-11-27T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 43,
'pressure': 1014.39,
'temperature': 11.1,
'templow': 3.0,
'uv_index': 1,
'wind_bearing': 8,
'wind_gust_speed': 32.18,
'wind_speed': 18.54,
}),
dict({
'apparent_temperature': 1.6,
'condition': 'cloudy',
'datetime': '2024-11-28T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 9,
'pressure': 1023.82,
'temperature': 8.2,
'templow': -1.9,
'uv_index': None,
'wind_bearing': 131,
'wind_gust_speed': 33.16,
'wind_speed': 20.05,
}),
dict({
'apparent_temperature': 3.0,
'condition': 'cloudy',
'datetime': '2024-11-28T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 9,
'pressure': 1025.12,
'temperature': 9.4,
'templow': 1.3,
'uv_index': 1,
'wind_bearing': 104,
'wind_gust_speed': 22.36,
'wind_speed': 12.64,
}),
dict({
'apparent_temperature': 5.0,
'condition': 'cloudy',
'datetime': '2024-11-29T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 13,
'pressure': 1016.88,
'temperature': 10.8,
'templow': -1.9,
'uv_index': None,
'wind_bearing': 151,
'wind_gust_speed': 33.16,
'wind_speed': 20.12,
}),
dict({
'apparent_temperature': 4.9,
'condition': 'cloudy',
'datetime': '2024-11-29T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 11,
'pressure': 1019.85,
'temperature': 12.6,
'templow': 4.2,
'uv_index': 1,
'wind_bearing': 137,
'wind_gust_speed': 38.59,
'wind_speed': 23.0,
}),
]),
}),
})
# ---
# name: test_forecast_service[get_forecasts].3
dict({
'weather.met_office_wavertree': dict({
'forecast': list([
@ -815,7 +1003,7 @@
}),
})
# ---
# name: test_forecast_service[get_forecasts].3
# name: test_forecast_service[get_forecasts].4
dict({
'weather.met_office_wavertree': dict({
'forecast': list([
@ -1447,6 +1635,194 @@
}),
})
# ---
# name: test_forecast_service[get_forecasts].5
dict({
'weather.met_office_wavertree': dict({
'forecast': list([
dict({
'apparent_temperature': 4.8,
'condition': 'cloudy',
'datetime': '2024-11-24T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 23,
'pressure': 987.12,
'temperature': 10.7,
'templow': 7.0,
'uv_index': None,
'wind_bearing': 211,
'wind_gust_speed': 47.2,
'wind_speed': 26.39,
}),
dict({
'apparent_temperature': 9.2,
'condition': 'cloudy',
'datetime': '2024-11-24T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 26,
'pressure': 987.48,
'temperature': 15.2,
'templow': 11.9,
'uv_index': 1,
'wind_bearing': 203,
'wind_gust_speed': 42.66,
'wind_speed': 23.94,
}),
dict({
'apparent_temperature': 4.2,
'condition': 'partlycloudy',
'datetime': '2024-11-25T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 6,
'pressure': 1004.81,
'temperature': 9.3,
'templow': 4.4,
'uv_index': None,
'wind_bearing': 262,
'wind_gust_speed': 47.99,
'wind_speed': 29.23,
}),
dict({
'apparent_temperature': 5.3,
'condition': 'partlycloudy',
'datetime': '2024-11-25T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 5,
'pressure': 994.88,
'temperature': 11.0,
'templow': 8.4,
'uv_index': 1,
'wind_bearing': 251,
'wind_gust_speed': 52.16,
'wind_speed': 30.67,
}),
dict({
'apparent_temperature': 1.3,
'condition': 'cloudy',
'datetime': '2024-11-26T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 44,
'pressure': 1013.9,
'temperature': 7.5,
'templow': -0.4,
'uv_index': None,
'wind_bearing': 74,
'wind_gust_speed': 19.51,
'wind_speed': 11.41,
}),
dict({
'apparent_temperature': 5.9,
'condition': 'partlycloudy',
'datetime': '2024-11-26T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 6,
'pressure': 1012.93,
'temperature': 10.1,
'templow': 6.5,
'uv_index': 1,
'wind_bearing': 265,
'wind_gust_speed': 34.49,
'wind_speed': 20.45,
}),
dict({
'apparent_temperature': 0.2,
'condition': 'clear-night',
'datetime': '2024-11-27T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 9,
'pressure': 1021.75,
'temperature': 7.2,
'templow': -3.0,
'uv_index': None,
'wind_bearing': 31,
'wind_gust_speed': 19.94,
'wind_speed': 11.84,
}),
dict({
'apparent_temperature': 3.3,
'condition': 'rainy',
'datetime': '2024-11-27T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 43,
'pressure': 1014.39,
'temperature': 11.1,
'templow': 3.0,
'uv_index': 1,
'wind_bearing': 8,
'wind_gust_speed': 32.18,
'wind_speed': 18.54,
}),
dict({
'apparent_temperature': 1.6,
'condition': 'cloudy',
'datetime': '2024-11-28T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 9,
'pressure': 1023.82,
'temperature': 8.2,
'templow': -1.9,
'uv_index': None,
'wind_bearing': 131,
'wind_gust_speed': 33.16,
'wind_speed': 20.05,
}),
dict({
'apparent_temperature': 3.0,
'condition': 'cloudy',
'datetime': '2024-11-28T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 9,
'pressure': 1025.12,
'temperature': 9.4,
'templow': 1.3,
'uv_index': 1,
'wind_bearing': 104,
'wind_gust_speed': 22.36,
'wind_speed': 12.64,
}),
dict({
'apparent_temperature': 5.0,
'condition': 'cloudy',
'datetime': '2024-11-29T00:00:00+00:00',
'is_daytime': False,
'precipitation': None,
'precipitation_probability': 13,
'pressure': 1016.88,
'temperature': 10.8,
'templow': -1.9,
'uv_index': None,
'wind_bearing': 151,
'wind_gust_speed': 33.16,
'wind_speed': 20.12,
}),
dict({
'apparent_temperature': 4.9,
'condition': 'cloudy',
'datetime': '2024-11-29T12:00:00+00:00',
'is_daytime': True,
'precipitation': None,
'precipitation_probability': 11,
'pressure': 1019.85,
'temperature': 12.6,
'templow': 4.2,
'uv_index': 1,
'wind_bearing': 137,
'wind_gust_speed': 38.59,
'wind_speed': 23.0,
}),
]),
}),
})
# ---
# name: test_forecast_subscription
list([
dict({

View File

@ -301,7 +301,7 @@ async def test_forecast_service(
assert wavertree_data["wavertree_daily_mock"].call_count == 1
assert wavertree_data["wavertree_hourly_mock"].call_count == 1
for forecast_type in ("daily", "hourly"):
for forecast_type in ("daily", "hourly", "twice_daily"):
response = await hass.services.async_call(
WEATHER_DOMAIN,
service,
@ -319,7 +319,7 @@ async def test_forecast_service(
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
for forecast_type in ("daily", "hourly"):
for forecast_type in ("daily", "hourly", "twice_daily"):
response = await hass.services.async_call(
WEATHER_DOMAIN,
service,