core/homeassistant/components/template/weather.py

448 lines
16 KiB
Python

"""Template platform that aggregates meteorological data."""
from __future__ import annotations
from functools import partial
from typing import Any, Literal
import voluptuous as vol
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_EXCEPTIONAL,
ATTR_CONDITION_FOG,
ATTR_CONDITION_HAIL,
ATTR_CONDITION_LIGHTNING,
ATTR_CONDITION_LIGHTNING_RAINY,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_POURING,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
ATTR_CONDITION_WINDY,
ATTR_CONDITION_WINDY_VARIANT,
ENTITY_ID_FORMAT,
Forecast,
WeatherEntity,
WeatherEntityFeature,
)
from homeassistant.const import CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.unit_conversion import (
DistanceConverter,
PressureConverter,
SpeedConverter,
TemperatureConverter,
)
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
CHECK_FORECAST_KEYS = set().union(Forecast.__annotations__.keys())
CONDITION_CLASSES = {
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_FOG,
ATTR_CONDITION_HAIL,
ATTR_CONDITION_LIGHTNING,
ATTR_CONDITION_LIGHTNING_RAINY,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_POURING,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
ATTR_CONDITION_WINDY,
ATTR_CONDITION_WINDY_VARIANT,
ATTR_CONDITION_EXCEPTIONAL,
}
CONF_WEATHER = "weather"
CONF_TEMPERATURE_TEMPLATE = "temperature_template"
CONF_HUMIDITY_TEMPLATE = "humidity_template"
CONF_CONDITION_TEMPLATE = "condition_template"
CONF_ATTRIBUTION_TEMPLATE = "attribution_template"
CONF_PRESSURE_TEMPLATE = "pressure_template"
CONF_WIND_SPEED_TEMPLATE = "wind_speed_template"
CONF_WIND_BEARING_TEMPLATE = "wind_bearing_template"
CONF_OZONE_TEMPLATE = "ozone_template"
CONF_VISIBILITY_TEMPLATE = "visibility_template"
CONF_FORECAST_TEMPLATE = "forecast_template"
CONF_FORECAST_DAILY_TEMPLATE = "forecast_daily_template"
CONF_FORECAST_HOURLY_TEMPLATE = "forecast_hourly_template"
CONF_FORECAST_TWICE_DAILY_TEMPLATE = "forecast_twice_daily_template"
CONF_PRESSURE_UNIT = "pressure_unit"
CONF_WIND_SPEED_UNIT = "wind_speed_unit"
CONF_VISIBILITY_UNIT = "visibility_unit"
CONF_PRECIPITATION_UNIT = "precipitation_unit"
CONF_WIND_GUST_SPEED_TEMPLATE = "wind_gust_speed_template"
CONF_CLOUD_COVERAGE_TEMPLATE = "cloud_coverage_template"
CONF_DEW_POINT_TEMPLATE = "dew_point_template"
CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_FORECAST_TEMPLATE),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_DAILY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(
TemperatureConverter.VALID_UNITS
),
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(
DistanceConverter.VALID_UNITS
),
vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
}
),
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Template weather."""
config = rewrite_common_legacy_to_modern_conf(config)
unique_id = config.get(CONF_UNIQUE_ID)
async_add_entities(
[
WeatherTemplate(
hass,
config,
unique_id,
)
]
)
class WeatherTemplate(TemplateEntity, WeatherEntity):
"""Representation of a weather condition."""
_attr_should_poll = False
def __init__(
self,
hass: HomeAssistant,
config: ConfigType,
unique_id: str | None,
) -> None:
"""Initialize the Template weather."""
super().__init__(hass, config=config, unique_id=unique_id)
name = self._attr_name
self._condition_template = config[CONF_CONDITION_TEMPLATE]
self._temperature_template = config[CONF_TEMPERATURE_TEMPLATE]
self._humidity_template = config[CONF_HUMIDITY_TEMPLATE]
self._attribution_template = config.get(CONF_ATTRIBUTION_TEMPLATE)
self._pressure_template = config.get(CONF_PRESSURE_TEMPLATE)
self._wind_speed_template = config.get(CONF_WIND_SPEED_TEMPLATE)
self._wind_bearing_template = config.get(CONF_WIND_BEARING_TEMPLATE)
self._ozone_template = config.get(CONF_OZONE_TEMPLATE)
self._visibility_template = config.get(CONF_VISIBILITY_TEMPLATE)
self._forecast_template = config.get(CONF_FORECAST_TEMPLATE)
self._forecast_daily_template = config.get(CONF_FORECAST_DAILY_TEMPLATE)
self._forecast_hourly_template = config.get(CONF_FORECAST_HOURLY_TEMPLATE)
self._forecast_twice_daily_template = config.get(
CONF_FORECAST_TWICE_DAILY_TEMPLATE
)
self._wind_gust_speed_template = config.get(CONF_WIND_GUST_SPEED_TEMPLATE)
self._cloud_coverage_template = config.get(CONF_CLOUD_COVERAGE_TEMPLATE)
self._dew_point_template = config.get(CONF_DEW_POINT_TEMPLATE)
self._apparent_temperature_template = config.get(
CONF_APPARENT_TEMPERATURE_TEMPLATE
)
self._attr_native_precipitation_unit = config.get(CONF_PRECIPITATION_UNIT)
self._attr_native_pressure_unit = config.get(CONF_PRESSURE_UNIT)
self._attr_native_temperature_unit = config.get(CONF_TEMPERATURE_UNIT)
self._attr_native_visibility_unit = config.get(CONF_VISIBILITY_UNIT)
self._attr_native_wind_speed_unit = config.get(CONF_WIND_SPEED_UNIT)
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
self._condition = None
self._temperature = None
self._humidity = None
self._attribution = None
self._pressure = None
self._wind_speed = None
self._wind_bearing = None
self._ozone = None
self._visibility = None
self._wind_gust_speed = None
self._cloud_coverage = None
self._dew_point = None
self._apparent_temperature = None
self._forecast: list[Forecast] = []
self._forecast_daily: list[Forecast] = []
self._forecast_hourly: list[Forecast] = []
self._forecast_twice_daily: list[Forecast] = []
self._attr_supported_features = 0
if self._forecast_daily_template:
self._attr_supported_features |= WeatherEntityFeature.FORECAST_DAILY
if self._forecast_hourly_template:
self._attr_supported_features |= WeatherEntityFeature.FORECAST_HOURLY
if self._forecast_twice_daily_template:
self._attr_supported_features |= WeatherEntityFeature.FORECAST_TWICE_DAILY
@property
def condition(self) -> str | None:
"""Return the current condition."""
return self._condition
@property
def native_temperature(self) -> float | None:
"""Return the temperature."""
return self._temperature
@property
def humidity(self) -> float | None:
"""Return the humidity."""
return self._humidity
@property
def native_wind_speed(self) -> float | None:
"""Return the wind speed."""
return self._wind_speed
@property
def wind_bearing(self) -> float | str | None:
"""Return the wind bearing."""
return self._wind_bearing
@property
def ozone(self) -> float | None:
"""Return the ozone level."""
return self._ozone
@property
def native_visibility(self) -> float | None:
"""Return the visibility."""
return self._visibility
@property
def native_pressure(self) -> float | None:
"""Return the air pressure."""
return self._pressure
@property
def native_wind_gust_speed(self) -> float | None:
"""Return the wind gust speed."""
return self._wind_gust_speed
@property
def cloud_coverage(self) -> float | None:
"""Return the cloud coverage."""
return self._cloud_coverage
@property
def native_dew_point(self) -> float | None:
"""Return the dew point."""
return self._dew_point
@property
def native_apparent_temperature(self) -> float | None:
"""Return the apparent temperature."""
return self._apparent_temperature
@property
def forecast(self) -> list[Forecast]:
"""Return the forecast."""
return self._forecast
async def async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast_daily
async def async_forecast_hourly(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast_hourly
async def async_forecast_twice_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast_twice_daily
@property
def attribution(self) -> str | None:
"""Return the attribution."""
if self._attribution is None:
return "Powered by Home Assistant"
return self._attribution
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
if self._condition_template:
self.add_template_attribute(
"_condition",
self._condition_template,
lambda condition: condition if condition in CONDITION_CLASSES else None,
)
if self._temperature_template:
self.add_template_attribute(
"_temperature",
self._temperature_template,
)
if self._humidity_template:
self.add_template_attribute(
"_humidity",
self._humidity_template,
)
if self._attribution_template:
self.add_template_attribute(
"_attribution",
self._attribution_template,
)
if self._pressure_template:
self.add_template_attribute(
"_pressure",
self._pressure_template,
)
if self._wind_speed_template:
self.add_template_attribute(
"_wind_speed",
self._wind_speed_template,
)
if self._wind_bearing_template:
self.add_template_attribute(
"_wind_bearing",
self._wind_bearing_template,
)
if self._ozone_template:
self.add_template_attribute(
"_ozone",
self._ozone_template,
)
if self._visibility_template:
self.add_template_attribute(
"_visibility",
self._visibility_template,
)
if self._wind_gust_speed_template:
self.add_template_attribute(
"_wind_gust_speed",
self._wind_gust_speed_template,
)
if self._cloud_coverage_template:
self.add_template_attribute(
"_cloud_coverage",
self._cloud_coverage_template,
)
if self._dew_point_template:
self.add_template_attribute(
"_dew_point",
self._dew_point_template,
)
if self._apparent_temperature_template:
self.add_template_attribute(
"_apparent_temperature",
self._apparent_temperature_template,
)
if self._forecast_template:
self.add_template_attribute(
"_forecast",
self._forecast_template,
)
if self._forecast_daily_template:
self.add_template_attribute(
"_forecast_daily",
self._forecast_daily_template,
on_update=partial(self._update_forecast, "daily"),
validator=partial(self._validate_forecast, "daily"),
)
if self._forecast_hourly_template:
self.add_template_attribute(
"_forecast_hourly",
self._forecast_hourly_template,
on_update=partial(self._update_forecast, "hourly"),
validator=partial(self._validate_forecast, "hourly"),
)
if self._forecast_twice_daily_template:
self.add_template_attribute(
"_forecast_twice_daily",
self._forecast_twice_daily_template,
on_update=partial(self._update_forecast, "twice_daily"),
validator=partial(self._validate_forecast, "twice_daily"),
)
await super().async_added_to_hass()
@callback
def _update_forecast(
self,
forecast_type: Literal["daily", "hourly", "twice_daily"],
result: list[Forecast] | TemplateError,
) -> None:
"""Save template result and trigger forecast listener."""
attr_result = None if isinstance(result, TemplateError) else result
setattr(self, f"_forecast_{forecast_type}", attr_result)
self.hass.create_task(self.async_update_listeners([forecast_type]))
@callback
def _validate_forecast(
self,
forecast_type: Literal["daily", "hourly", "twice_daily"],
result: Any,
) -> list[Forecast] | None:
"""Validate the forecasts."""
if result is None:
return None
if not isinstance(result, list):
raise vol.Invalid(
"Forecasts is not a list, see Weather documentation https://www.home-assistant.io/integrations/weather/"
)
for forecast in result:
if not isinstance(forecast, dict):
raise vol.Invalid(
"Forecast in list is not a dict, see Weather documentation https://www.home-assistant.io/integrations/weather/"
)
diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS)
if diff_result:
raise vol.Invalid(
"Only valid keys in Forecast are allowed, see Weather documentation https://www.home-assistant.io/integrations/weather/"
)
if forecast_type == "twice_daily" and "is_daytime" not in forecast:
raise vol.Invalid(
"`is_daytime` is missing in twice_daily forecast, see Weather documentation https://www.home-assistant.io/integrations/weather/"
)
if "datetime" not in forecast:
raise vol.Invalid(
"`datetime` is required in forecasts, see Weather documentation https://www.home-assistant.io/integrations/weather/"
)
continue
return result