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_DAILY_COORDINATOR,
METOFFICE_HOURLY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME, METOFFICE_NAME,
METOFFICE_TWICE_DAILY_COORDINATOR,
) )
from .helpers import fetch_data 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" 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( metoffice_hourly_coordinator = TimestampDataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
@ -77,10 +83,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
update_interval=DEFAULT_SCAN_INTERVAL, 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 = hass.data.setdefault(DOMAIN, {})
metoffice_hass_data[entry.entry_id] = { metoffice_hass_data[entry.entry_id] = {
METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator, METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator,
METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator, METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator,
METOFFICE_TWICE_DAILY_COORDINATOR: metoffice_twice_daily_coordinator,
METOFFICE_NAME: site_name, METOFFICE_NAME: site_name,
METOFFICE_COORDINATES: coordinates, METOFFICE_COORDINATES: coordinates,
} }

View File

@ -41,6 +41,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=15)
METOFFICE_COORDINATES = "metoffice_coordinates" METOFFICE_COORDINATES = "metoffice_coordinates"
METOFFICE_HOURLY_COORDINATOR = "metoffice_hourly_coordinator" METOFFICE_HOURLY_COORDINATOR = "metoffice_hourly_coordinator"
METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator" METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator"
METOFFICE_TWICE_DAILY_COORDINATOR = "metoffice_twice_daily_coordinator"
METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions" METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions"
METOFFICE_NAME = "metoffice_name" 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_SPEED: "midday10MWindSpeed",
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midday10MWindGust", 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 ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_IS_DAYTIME,
ATTR_FORECAST_NATIVE_APPARENT_TEMP, ATTR_FORECAST_NATIVE_APPARENT_TEMP,
ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_PRESSURE,
ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP,
@ -41,12 +42,15 @@ from .const import (
ATTRIBUTION, ATTRIBUTION,
CONDITION_MAP, CONDITION_MAP,
DAILY_FORECAST_ATTRIBUTE_MAP, DAILY_FORECAST_ATTRIBUTE_MAP,
DAY_FORECAST_ATTRIBUTE_MAP,
DOMAIN, DOMAIN,
HOURLY_FORECAST_ATTRIBUTE_MAP, HOURLY_FORECAST_ATTRIBUTE_MAP,
METOFFICE_COORDINATES, METOFFICE_COORDINATES,
METOFFICE_DAILY_COORDINATOR, METOFFICE_DAILY_COORDINATOR,
METOFFICE_HOURLY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME, METOFFICE_NAME,
METOFFICE_TWICE_DAILY_COORDINATOR,
NIGHT_FORECAST_ATTRIBUTE_MAP,
) )
from .helpers import get_attribute from .helpers import get_attribute
@ -73,6 +77,7 @@ async def async_setup_entry(
MetOfficeWeather( MetOfficeWeather(
hass_data[METOFFICE_DAILY_COORDINATOR], hass_data[METOFFICE_DAILY_COORDINATOR],
hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data[METOFFICE_HOURLY_COORDINATOR],
hass_data[METOFFICE_TWICE_DAILY_COORDINATOR],
hass_data, hass_data,
) )
], ],
@ -92,6 +97,19 @@ def _build_daily_forecast_data(timestep: dict[str, Any]) -> Forecast:
return data 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( def _populate_forecast_data(
forecast: Forecast, timestep: dict[str, Any], mapping: dict[str, str] forecast: Forecast, timestep: dict[str, Any], mapping: dict[str, str]
) -> None: ) -> None:
@ -152,13 +170,16 @@ class MetOfficeWeather(
_attr_native_visibility_unit = UnitOfLength.METERS _attr_native_visibility_unit = UnitOfLength.METERS
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
_attr_supported_features = ( _attr_supported_features = (
WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_DAILY WeatherEntityFeature.FORECAST_HOURLY
| WeatherEntityFeature.FORECAST_TWICE_DAILY
| WeatherEntityFeature.FORECAST_DAILY
) )
def __init__( def __init__(
self, self,
coordinator_daily: TimestampDataUpdateCoordinator[ForecastData], coordinator_daily: TimestampDataUpdateCoordinator[ForecastData],
coordinator_hourly: TimestampDataUpdateCoordinator[ForecastData], coordinator_hourly: TimestampDataUpdateCoordinator[ForecastData],
coordinator_twice_daily: TimestampDataUpdateCoordinator[ForecastData],
hass_data: dict[str, Any], hass_data: dict[str, Any],
) -> None: ) -> None:
"""Initialise the platform with a data instance.""" """Initialise the platform with a data instance."""
@ -167,6 +188,7 @@ class MetOfficeWeather(
observation_coordinator, observation_coordinator,
daily_coordinator=coordinator_daily, daily_coordinator=coordinator_daily,
hourly_coordinator=coordinator_hourly, hourly_coordinator=coordinator_hourly,
twice_daily_coordinator=coordinator_twice_daily,
) )
self._attr_device_info = get_device_info( self._attr_device_info = get_device_info(
@ -268,3 +290,17 @@ class MetOfficeWeather(
for timestep in timesteps for timestep in timesteps
if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo) 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 # 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({ dict({
'weather.met_office_wavertree': dict({ 'weather.met_office_wavertree': dict({
'forecast': list([ 'forecast': list([
@ -815,7 +1003,7 @@
}), }),
}) })
# --- # ---
# name: test_forecast_service[get_forecasts].3 # name: test_forecast_service[get_forecasts].4
dict({ dict({
'weather.met_office_wavertree': dict({ 'weather.met_office_wavertree': dict({
'forecast': list([ '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 # name: test_forecast_subscription
list([ list([
dict({ 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_daily_mock"].call_count == 1
assert wavertree_data["wavertree_hourly_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( response = await hass.services.async_call(
WEATHER_DOMAIN, WEATHER_DOMAIN,
service, service,
@ -319,7 +319,7 @@ async def test_forecast_service(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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( response = await hass.services.async_call(
WEATHER_DOMAIN, WEATHER_DOMAIN,
service, service,