Add twice_daily forecast to SMHI

This commit is contained in:
G Johansson 2025-07-16 12:28:18 +00:00
parent cd94685b7d
commit 918e5430a3
4 changed files with 333 additions and 7 deletions

View File

@ -24,6 +24,7 @@ class SMHIForecastData:
daily: list[SMHIForecast]
hourly: list[SMHIForecast]
twice_daily: list[SMHIForecast]
class SMHIDataUpdateCoordinator(DataUpdateCoordinator[SMHIForecastData]):
@ -52,6 +53,9 @@ class SMHIDataUpdateCoordinator(DataUpdateCoordinator[SMHIForecastData]):
async with asyncio.timeout(TIMEOUT):
_forecast_daily = await self._smhi_api.async_get_daily_forecast()
_forecast_hourly = await self._smhi_api.async_get_hourly_forecast()
_forecast_twice_daily = (
await self._smhi_api.async_get_twice_daily_forecast()
)
except SmhiForecastException as ex:
raise UpdateFailed(
"Failed to retrieve the forecast from the SMHI API"
@ -60,4 +64,5 @@ class SMHIDataUpdateCoordinator(DataUpdateCoordinator[SMHIForecastData]):
return SMHIForecastData(
daily=_forecast_daily,
hourly=_forecast_hourly,
twice_daily=_forecast_twice_daily,
)

View File

@ -26,6 +26,7 @@ from homeassistant.components.weather import (
ATTR_FORECAST_CLOUD_COVERAGE,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_HUMIDITY,
ATTR_FORECAST_IS_DAYTIME,
ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_PRESSURE,
ATTR_FORECAST_NATIVE_TEMP,
@ -109,7 +110,9 @@ class SmhiWeather(SmhiWeatherBaseEntity, SingleCoordinatorWeatherEntity):
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
_attr_native_pressure_unit = UnitOfPressure.HPA
_attr_supported_features = (
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
WeatherEntityFeature.FORECAST_DAILY
| WeatherEntityFeature.FORECAST_HOURLY
| WeatherEntityFeature.FORECAST_TWICE_DAILY
)
def update_entity_data(self) -> None:
@ -145,7 +148,7 @@ class SmhiWeather(SmhiWeatherBaseEntity, SingleCoordinatorWeatherEntity):
super()._handle_coordinator_update()
def _get_forecast_data(
self, forecast_data: list[SMHIForecast] | None
self, forecast_data: list[SMHIForecast] | None, forecast_type: str
) -> list[Forecast] | None:
"""Get forecast data."""
if forecast_data is None or len(forecast_data) < 3:
@ -160,7 +163,7 @@ class SmhiWeather(SmhiWeatherBaseEntity, SingleCoordinatorWeatherEntity):
):
condition = ATTR_CONDITION_CLEAR_NIGHT
data.append(
new_forecast = Forecast(
{
ATTR_FORECAST_TIME: forecast["valid_time"].isoformat(),
ATTR_FORECAST_NATIVE_TEMP: forecast["temperature_max"],
@ -178,13 +181,23 @@ class SmhiWeather(SmhiWeatherBaseEntity, SingleCoordinatorWeatherEntity):
ATTR_FORECAST_CLOUD_COVERAGE: forecast["total_cloud"],
}
)
if forecast_type == "twice_daily":
new_forecast[ATTR_FORECAST_IS_DAYTIME] = False
if forecast["valid_time"].hour == 12:
new_forecast[ATTR_FORECAST_IS_DAYTIME] = True
data.append(new_forecast)
return data
def _async_forecast_daily(self) -> list[Forecast] | None:
"""Service to retrieve the daily forecast."""
return self._get_forecast_data(self.coordinator.data.daily)
return self._get_forecast_data(self.coordinator.data.daily, "daily")
def _async_forecast_hourly(self) -> list[Forecast] | None:
"""Service to retrieve the hourly forecast."""
return self._get_forecast_data(self.coordinator.data.hourly)
return self._get_forecast_data(self.coordinator.data.hourly, "hourly")
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
"""Service to retrieve the twice daily forecast."""
return self._get_forecast_data(self.coordinator.data.twice_daily, "twice_daily")

View File

@ -68,7 +68,7 @@
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
'pressure': 992.4,
'pressure_unit': <UnitOfPressure.HPA: 'hPa'>,
'supported_features': <WeatherEntityFeature: 3>,
'supported_features': <WeatherEntityFeature: 7>,
'temperature': 18.4,
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
'thunder_probability': 37,
@ -287,7 +287,7 @@
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
'pressure': 992.4,
'pressure_unit': <UnitOfPressure.HPA: 'hPa'>,
'supported_features': <WeatherEntityFeature: 3>,
'supported_features': <WeatherEntityFeature: 7>,
'temperature': 18.4,
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
'thunder_probability': 37,
@ -299,3 +299,291 @@
'wind_speed_unit': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
})
# ---
# name: test_twice_daily_forecast_service[load_platforms0]
dict({
'weather.smhi_test': dict({
'forecast': list([
dict({
'cloud_coverage': 100,
'condition': 'fog',
'datetime': '2023-08-07T08:00:00+00:00',
'humidity': 100,
'is_daytime': False,
'precipitation': 0.0,
'pressure': 992.4,
'temperature': 18.4,
'templow': 18.4,
'wind_bearing': 93,
'wind_gust_speed': 22.32,
'wind_speed': 9.0,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-07T12:00:00+00:00',
'humidity': 96,
'is_daytime': True,
'precipitation': 0.0,
'pressure': 991.7,
'temperature': 18.4,
'templow': 17.1,
'wind_bearing': 114,
'wind_gust_speed': 32.76,
'wind_speed': 10.08,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-08T00:00:00+00:00',
'humidity': 99,
'is_daytime': False,
'precipitation': 0.1,
'pressure': 987.5,
'temperature': 18.4,
'templow': 14.8,
'wind_bearing': 357,
'wind_gust_speed': 10.44,
'wind_speed': 3.96,
}),
dict({
'cloud_coverage': 100,
'condition': 'rainy',
'datetime': '2023-08-08T12:00:00+00:00',
'humidity': 97,
'is_daytime': True,
'precipitation': 0.3,
'pressure': 984.1,
'temperature': 18.4,
'templow': 12.8,
'wind_bearing': 183,
'wind_gust_speed': 27.36,
'wind_speed': 11.16,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-09T00:00:00+00:00',
'humidity': 85,
'is_daytime': False,
'precipitation': 0.1,
'pressure': 995.6,
'temperature': 18.4,
'templow': 11.2,
'wind_bearing': 193,
'wind_gust_speed': 48.6,
'wind_speed': 19.8,
}),
dict({
'cloud_coverage': 100,
'condition': 'rainy',
'datetime': '2023-08-09T12:00:00+00:00',
'humidity': 95,
'is_daytime': True,
'precipitation': 1.1,
'pressure': 1001.4,
'temperature': 18.4,
'templow': 11.1,
'wind_bearing': 166,
'wind_gust_speed': 48.24,
'wind_speed': 18.0,
}),
dict({
'cloud_coverage': 100,
'condition': 'rainy',
'datetime': '2023-08-10T00:00:00+00:00',
'humidity': 99,
'is_daytime': False,
'precipitation': 3.6,
'pressure': 1007.8,
'temperature': 18.4,
'templow': 10.4,
'wind_bearing': 200,
'wind_gust_speed': 28.08,
'wind_speed': 14.4,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-10T12:00:00+00:00',
'humidity': 75,
'is_daytime': True,
'precipitation': 0.0,
'pressure': 1011.1,
'temperature': 18.4,
'templow': 13.9,
'wind_bearing': 174,
'wind_gust_speed': 29.16,
'wind_speed': 11.16,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-11T00:00:00+00:00',
'humidity': 98,
'is_daytime': False,
'precipitation': 0.0,
'pressure': 1012.3,
'temperature': 18.4,
'templow': 11.7,
'wind_bearing': 169,
'wind_gust_speed': 16.56,
'wind_speed': 7.56,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-11T12:00:00+00:00',
'humidity': 69,
'is_daytime': True,
'precipitation': 0.0,
'pressure': 1015.3,
'temperature': 18.4,
'templow': 17.6,
'wind_bearing': 197,
'wind_gust_speed': 27.36,
'wind_speed': 10.08,
}),
dict({
'cloud_coverage': 0,
'condition': 'clear-night',
'datetime': '2023-08-12T00:00:00+00:00',
'humidity': 97,
'is_daytime': False,
'precipitation': 0.0,
'pressure': 1015.8,
'temperature': 18.4,
'templow': 12.3,
'wind_bearing': 191,
'wind_gust_speed': 18.0,
'wind_speed': 8.64,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-12T12:00:00+00:00',
'humidity': 82,
'is_daytime': True,
'precipitation': 0.0,
'pressure': 1014.0,
'temperature': 18.4,
'templow': 17.0,
'wind_bearing': 225,
'wind_gust_speed': 28.08,
'wind_speed': 8.64,
}),
dict({
'cloud_coverage': 12,
'condition': 'clear-night',
'datetime': '2023-08-13T00:00:00+00:00',
'humidity': 92,
'is_daytime': False,
'precipitation': 0.0,
'pressure': 1013.9,
'temperature': 18.4,
'templow': 13.6,
'wind_bearing': 233,
'wind_gust_speed': 20.16,
'wind_speed': 10.08,
}),
dict({
'cloud_coverage': 75,
'condition': 'partlycloudy',
'datetime': '2023-08-13T12:00:00+00:00',
'humidity': 59,
'is_daytime': True,
'precipitation': 0.0,
'pressure': 1013.6,
'temperature': 20.0,
'templow': 18.4,
'wind_bearing': 234,
'wind_gust_speed': 35.64,
'wind_speed': 14.76,
}),
dict({
'cloud_coverage': 50,
'condition': 'partlycloudy',
'datetime': '2023-08-14T00:00:00+00:00',
'humidity': 91,
'is_daytime': False,
'precipitation': 0.0,
'pressure': 1015.2,
'temperature': 18.4,
'templow': 13.5,
'wind_bearing': 227,
'wind_gust_speed': 23.4,
'wind_speed': 10.8,
}),
dict({
'cloud_coverage': 100,
'condition': 'partlycloudy',
'datetime': '2023-08-14T12:00:00+00:00',
'humidity': 56,
'is_daytime': True,
'precipitation': 0.0,
'pressure': 1015.3,
'temperature': 20.8,
'templow': 18.4,
'wind_bearing': 216,
'wind_gust_speed': 33.12,
'wind_speed': 13.68,
}),
dict({
'cloud_coverage': 100,
'condition': 'cloudy',
'datetime': '2023-08-15T00:00:00+00:00',
'humidity': 93,
'is_daytime': False,
'precipitation': 1.2,
'pressure': 1014.9,
'temperature': 18.4,
'templow': 14.3,
'wind_bearing': 196,
'wind_gust_speed': 22.32,
'wind_speed': 10.08,
}),
dict({
'cloud_coverage': 88,
'condition': 'partlycloudy',
'datetime': '2023-08-15T12:00:00+00:00',
'humidity': 64,
'is_daytime': True,
'precipitation': 2.4,
'pressure': 1014.3,
'temperature': 20.4,
'templow': 18.4,
'wind_bearing': 226,
'wind_gust_speed': 33.12,
'wind_speed': 13.68,
}),
dict({
'cloud_coverage': 38,
'condition': 'clear-night',
'datetime': '2023-08-16T00:00:00+00:00',
'humidity': 93,
'is_daytime': False,
'precipitation': 1.2,
'pressure': 1014.9,
'temperature': 18.4,
'templow': 13.8,
'wind_bearing': 228,
'wind_gust_speed': 21.24,
'wind_speed': 10.08,
}),
dict({
'cloud_coverage': 75,
'condition': 'partlycloudy',
'datetime': '2023-08-16T12:00:00+00:00',
'humidity': 61,
'is_daytime': True,
'precipitation': 1.2,
'pressure': 1014.0,
'temperature': 20.2,
'templow': 18.4,
'wind_bearing': 233,
'wind_gust_speed': 33.48,
'wind_speed': 14.04,
}),
]),
}),
})
# ---

View File

@ -473,3 +473,23 @@ async def test_forecast_service(
return_response=True,
)
assert response == snapshot
@pytest.mark.parametrize(
"load_platforms",
[[Platform.WEATHER]],
)
async def test_twice_daily_forecast_service(
hass: HomeAssistant,
load_int: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test forecast service."""
response = await hass.services.async_call(
WEATHER_DOMAIN,
SERVICE_GET_FORECASTS,
{"entity_id": ENTITY_ID, "type": "twice_daily"},
blocking=True,
return_response=True,
)
assert response == snapshot