Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions homeassistant/components/open_meteo/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

import json
from types import SimpleNamespace

from open_meteo import (
DailyParameters,
Forecast,
Expand All @@ -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
Expand All @@ -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."""

Expand All @@ -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,
Expand All @@ -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
70 changes: 57 additions & 13 deletions homeassistant/components/open_meteo/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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(),
)
Expand Down Expand Up @@ -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

Expand All @@ -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