diff --git a/homeassistant/components/open_meteo/coordinator.py b/homeassistant/components/open_meteo/coordinator.py index 9e2f262db782af..ee76c1e72771c1 100644 --- a/homeassistant/components/open_meteo/coordinator.py +++ b/homeassistant/components/open_meteo/coordinator.py @@ -2,6 +2,9 @@ from __future__ import annotations +import json +from types import SimpleNamespace + from open_meteo import ( DailyParameters, Forecast, @@ -10,8 +13,10 @@ OpenMeteoError, PrecipitationUnit, TemperatureUnit, + TimeFormat, WindSpeedUnit, ) +from yarl import URL from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE @@ -24,6 +29,51 @@ type OpenMeteoConfigEntry = ConfigEntry[OpenMeteoDataUpdateCoordinator] +class OpenMeteoWithCurrent(OpenMeteo): + """Subclass of OpenMeteo to support current parameter.""" + + async def forecast( + self, + *, + latitude: float, + longitude: float, + timezone: str = "UTC", + current_weather: bool = False, + current: list[str] | None = None, + daily: list[DailyParameters] | None = None, + hourly: list[HourlyParameters] | None = None, + past_days: int = 0, + precipitation_unit: PrecipitationUnit = PrecipitationUnit.MILLIMETERS, + temperature_unit: TemperatureUnit = TemperatureUnit.CELSIUS, + timeformat: TimeFormat = TimeFormat.ISO_8601, + wind_speed_unit: WindSpeedUnit = WindSpeedUnit.KILOMETERS_PER_HOUR, + ) -> Forecast: + """Get weather forecast with support for current parameter.""" + url = URL("https://api.open-meteo.com/v1/forecast").with_query( + current_weather="true" if current_weather else "false", + current=",".join(current) if current is not None else [], + daily=",".join(daily) if daily is not None else [], + hourly=",".join(hourly) if hourly is not None else [], + latitude=latitude, + longitude=longitude, + past_days=past_days, + precipitation_unit=precipitation_unit, + temperature_unit=temperature_unit, + timeformat=timeformat, + timezone=timezone, + windspeed_unit=wind_speed_unit, + ) + data = await self._request(url=url) + data_dict = json.loads(data) + forecast = Forecast.from_dict(data_dict) + + if "current" in data_dict: + # Attach the current data as a SimpleNamespace to allow dot access + forecast.current = SimpleNamespace(**data_dict["current"]) + + return forecast + + class OpenMeteoDataUpdateCoordinator(DataUpdateCoordinator[Forecast]): """A Open-Meteo Data Update Coordinator.""" @@ -39,18 +89,28 @@ def __init__(self, hass: HomeAssistant, config_entry: OpenMeteoConfigEntry) -> N update_interval=SCAN_INTERVAL, ) session = async_get_clientsession(hass) - self.open_meteo = OpenMeteo(session=session) + # Use our patched client + self.open_meteo = OpenMeteoWithCurrent(session=session) async def _async_update_data(self) -> Forecast: - """Fetch data from Sensibo.""" + """Fetch data from Open-Meteo.""" if (zone := self.hass.states.get(self.config_entry.data[CONF_ZONE])) is None: raise UpdateFailed(f"Zone '{self.config_entry.data[CONF_ZONE]}' not found") + current = [ + HourlyParameters.TEMPERATURE_2M, + HourlyParameters.WIND_SPEED_10M, + HourlyParameters.WIND_DIRECTION_10M, + HourlyParameters.WEATHER_CODE, + HourlyParameters.CLOUD_COVER, + HourlyParameters.WIND_GUSTS_10M, + ] + try: return await self.open_meteo.forecast( latitude=zone.attributes[ATTR_LATITUDE], longitude=zone.attributes[ATTR_LONGITUDE], - current_weather=True, + current=current, daily=[ DailyParameters.PRECIPITATION_SUM, DailyParameters.TEMPERATURE_2M_MAX, @@ -63,11 +123,18 @@ async def _async_update_data(self) -> Forecast: HourlyParameters.PRECIPITATION, HourlyParameters.TEMPERATURE_2M, HourlyParameters.WEATHER_CODE, + HourlyParameters.WIND_DIRECTION_10M, + HourlyParameters.WIND_SPEED_10M, + HourlyParameters.RELATIVE_HUMIDITY_2M, + HourlyParameters.CLOUD_COVER, + HourlyParameters.PRESSURE_MSL, + HourlyParameters.WIND_GUSTS_10M, ], precipitation_unit=PrecipitationUnit.MILLIMETERS, temperature_unit=TemperatureUnit.CELSIUS, - timezone="UTC", + timezone="auto", wind_speed_unit=WindSpeedUnit.KILOMETERS_PER_HOUR, + # forecast_days=14, ) except OpenMeteoError as err: raise UpdateFailed("Open-Meteo API communication error") from err diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 9782051ab225c4..fb91b47005653b 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -2,17 +2,21 @@ from __future__ import annotations -from datetime import datetime, time +from datetime import datetime, time, timedelta, timezone from open_meteo import Forecast as OpenMeteoForecast from homeassistant.components.weather import ( + ATTR_FORECAST_CLOUD_COVERAGE, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_GUST_SPEED, Forecast, SingleCoordinatorWeatherEntity, WeatherEntityFeature, @@ -72,32 +76,52 @@ def __init__( @property def condition(self) -> str | None: """Return the current condition.""" - if not self.coordinator.data.current_weather: + if ( + not hasattr(self.coordinator.data, "current") + or not self.coordinator.data.current + ): return None - return WMO_TO_HA_CONDITION_MAP.get( - self.coordinator.data.current_weather.weather_code - ) + return WMO_TO_HA_CONDITION_MAP.get(self.coordinator.data.current.weathercode) @property def native_temperature(self) -> float | None: """Return the platform temperature.""" - if not self.coordinator.data.current_weather: + if ( + not hasattr(self.coordinator.data, "current") + or not self.coordinator.data.current + ): return None - return self.coordinator.data.current_weather.temperature + return self.coordinator.data.current.temperature_2m @property def native_wind_speed(self) -> float | None: """Return the wind speed.""" - if not self.coordinator.data.current_weather: + if ( + not hasattr(self.coordinator.data, "current") + or not self.coordinator.data.current + ): return None - return self.coordinator.data.current_weather.wind_speed + return self.coordinator.data.current.windspeed_10m @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" - if not self.coordinator.data.current_weather: + if ( + not hasattr(self.coordinator.data, "current") + or not self.coordinator.data.current + ): return None - return self.coordinator.data.current_weather.wind_direction + return self.coordinator.data.current.winddirection_10m + + @property + def native_wind_gust_speed(self) -> float | None: + """Return the current wind gust speed.""" + if ( + not hasattr(self.coordinator.data, "current") + or not self.coordinator.data.current + ): + return None + return self.coordinator.data.current.windgusts_10m @callback def _async_forecast_daily(self) -> list[Forecast] | None: @@ -106,10 +130,11 @@ def _async_forecast_daily(self) -> list[Forecast] | None: return None forecasts: list[Forecast] = [] + tz = timezone(timedelta(seconds=self.coordinator.data.utc_offset_seconds)) daily = self.coordinator.data.daily for index, date in enumerate(self.coordinator.data.daily.time): - _datetime = datetime.combine(date=date, time=time(0), tzinfo=dt_util.UTC) + _datetime = datetime.combine(date=date, time=time(0), tzinfo=tz) forecast = Forecast( datetime=_datetime.isoformat(), ) @@ -156,11 +181,12 @@ def _async_forecast_hourly(self) -> list[Forecast] | None: # Can have data in the past: https://github.com/open-meteo/open-meteo/issues/699 today = dt_util.utcnow() + tz = timezone(timedelta(seconds=self.coordinator.data.utc_offset_seconds)) hourly = self.coordinator.data.hourly for index, _datetime in enumerate(self.coordinator.data.hourly.time): if _datetime.tzinfo is None: - _datetime = _datetime.replace(tzinfo=dt_util.UTC) + _datetime = _datetime.replace(tzinfo=tz) if _datetime < today: continue @@ -181,6 +207,24 @@ def _async_forecast_hourly(self) -> list[Forecast] | None: if hourly.temperature_2m is not None: forecast[ATTR_FORECAST_NATIVE_TEMP] = hourly.temperature_2m[index] + if hourly.wind_speed_10m is not None: + forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = hourly.wind_speed_10m[index] + + if hourly.wind_direction_10m is not None: + forecast[ATTR_FORECAST_WIND_BEARING] = hourly.wind_direction_10m[index] + + if hourly.relative_humidity_2m is not None: + forecast[ATTR_FORECAST_HUMIDITY] = hourly.relative_humidity_2m[index] + + if hourly.cloud_cover is not None: + forecast[ATTR_FORECAST_CLOUD_COVERAGE] = hourly.cloud_cover[index] + + if hourly.pressure_msl is not None: + forecast[ATTR_FORECAST_PRESSURE] = hourly.pressure_msl[index] + + if hourly.wind_gusts_10m is not None: + forecast[ATTR_FORECAST_WIND_GUST_SPEED] = hourly.wind_gusts_10m[index] + forecasts.append(forecast) return forecasts