mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Add trigger weather template (#100824)
* Add trigger weather template * Add tests * mod dataclass * Remove legacy forecast * Fix test failure * sorting * add hourly test * Add tests * Add and fix tests * Improve tests --------- Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
parent
fd53e116bb
commit
43954d660b
@ -9,6 +9,7 @@ from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
|
||||
from homeassistant.config import async_log_exception, config_without_domain
|
||||
from homeassistant.const import CONF_BINARY_SENSORS, CONF_SENSORS, CONF_UNIQUE_ID
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@ -21,6 +22,7 @@ from . import (
|
||||
number as number_platform,
|
||||
select as select_platform,
|
||||
sensor as sensor_platform,
|
||||
weather as weather_platform,
|
||||
)
|
||||
from .const import CONF_ACTION, CONF_TRIGGER, DOMAIN
|
||||
|
||||
@ -55,6 +57,9 @@ CONFIG_SECTION_SCHEMA = vol.Schema(
|
||||
vol.Optional(IMAGE_DOMAIN): vol.All(
|
||||
cv.ensure_list, [image_platform.IMAGE_SCHEMA]
|
||||
),
|
||||
vol.Optional(WEATHER_DOMAIN): vol.All(
|
||||
cv.ensure_list, [weather_platform.WEATHER_SCHEMA]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""Template platform that aggregates meteorological data."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, Self
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -22,18 +23,27 @@ from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_CONDITION_WINDY,
|
||||
ATTR_CONDITION_WINDY_VARIANT,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
CONF_UNIQUE_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
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.restore_state import ExtraStoredData, RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.unit_conversion import (
|
||||
DistanceConverter,
|
||||
@ -42,7 +52,9 @@ from homeassistant.util.unit_conversion import (
|
||||
TemperatureConverter,
|
||||
)
|
||||
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
CHECK_FORECAST_KEYS = (
|
||||
set().union(Forecast.__annotations__.keys())
|
||||
@ -92,40 +104,38 @@ CONF_CLOUD_COVERAGE_TEMPLATE = "cloud_coverage_template"
|
||||
CONF_DEW_POINT_TEMPLATE = "dew_point_template"
|
||||
CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
|
||||
|
||||
WEATHER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
),
|
||||
PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema),
|
||||
)
|
||||
|
||||
|
||||
@ -136,6 +146,12 @@ async def async_setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Template weather."""
|
||||
if discovery_info and "coordinator" in discovery_info:
|
||||
async_add_entities(
|
||||
TriggerWeatherEntity(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
)
|
||||
return
|
||||
|
||||
config = rewrite_common_legacy_to_modern_conf(config)
|
||||
unique_id = config.get(CONF_UNIQUE_ID)
|
||||
@ -452,3 +468,248 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
|
||||
)
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class WeatherExtraStoredData(ExtraStoredData):
|
||||
"""Object to hold extra stored data."""
|
||||
|
||||
last_apparent_temperature: float | None
|
||||
last_cloud_coverage: int | None
|
||||
last_dew_point: float | None
|
||||
last_humidity: float | None
|
||||
last_ozone: float | None
|
||||
last_pressure: float | None
|
||||
last_temperature: float | None
|
||||
last_visibility: float | None
|
||||
last_wind_bearing: float | str | None
|
||||
last_wind_gust_speed: float | None
|
||||
last_wind_speed: float | None
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of the event data."""
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
|
||||
"""Initialize a stored event state from a dict."""
|
||||
try:
|
||||
return cls(
|
||||
last_apparent_temperature=restored["last_apparent_temperature"],
|
||||
last_cloud_coverage=restored["last_cloud_coverage"],
|
||||
last_dew_point=restored["last_dew_point"],
|
||||
last_humidity=restored["last_humidity"],
|
||||
last_ozone=restored["last_ozone"],
|
||||
last_pressure=restored["last_pressure"],
|
||||
last_temperature=restored["last_temperature"],
|
||||
last_visibility=restored["last_visibility"],
|
||||
last_wind_bearing=restored["last_wind_bearing"],
|
||||
last_wind_gust_speed=restored["last_wind_gust_speed"],
|
||||
last_wind_speed=restored["last_wind_speed"],
|
||||
)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
class TriggerWeatherEntity(TriggerEntity, WeatherEntity, RestoreEntity):
|
||||
"""Sensor entity based on trigger data."""
|
||||
|
||||
domain = WEATHER_DOMAIN
|
||||
extra_template_keys = (
|
||||
CONF_CONDITION_TEMPLATE,
|
||||
CONF_TEMPERATURE_TEMPLATE,
|
||||
CONF_HUMIDITY_TEMPLATE,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(hass, coordinator, config)
|
||||
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._attr_supported_features = 0
|
||||
if config.get(CONF_FORECAST_DAILY_TEMPLATE):
|
||||
self._attr_supported_features |= WeatherEntityFeature.FORECAST_DAILY
|
||||
if config.get(CONF_FORECAST_HOURLY_TEMPLATE):
|
||||
self._attr_supported_features |= WeatherEntityFeature.FORECAST_HOURLY
|
||||
if config.get(CONF_FORECAST_TWICE_DAILY_TEMPLATE):
|
||||
self._attr_supported_features |= WeatherEntityFeature.FORECAST_TWICE_DAILY
|
||||
|
||||
for key in (
|
||||
CONF_APPARENT_TEMPERATURE_TEMPLATE,
|
||||
CONF_CLOUD_COVERAGE_TEMPLATE,
|
||||
CONF_DEW_POINT_TEMPLATE,
|
||||
CONF_FORECAST_DAILY_TEMPLATE,
|
||||
CONF_FORECAST_HOURLY_TEMPLATE,
|
||||
CONF_FORECAST_TWICE_DAILY_TEMPLATE,
|
||||
CONF_OZONE_TEMPLATE,
|
||||
CONF_PRESSURE_TEMPLATE,
|
||||
CONF_VISIBILITY_TEMPLATE,
|
||||
CONF_WIND_BEARING_TEMPLATE,
|
||||
CONF_WIND_GUST_SPEED_TEMPLATE,
|
||||
CONF_WIND_SPEED_TEMPLATE,
|
||||
):
|
||||
if isinstance(config.get(key), template.Template):
|
||||
self._to_render_simple.append(key)
|
||||
self._parse_result.add(key)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
if (
|
||||
(state := await self.async_get_last_state())
|
||||
and state.state is not None
|
||||
and state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
and (weather_data := await self.async_get_last_weather_data())
|
||||
):
|
||||
self._rendered[
|
||||
CONF_APPARENT_TEMPERATURE_TEMPLATE
|
||||
] = weather_data.last_apparent_temperature
|
||||
self._rendered[
|
||||
CONF_CLOUD_COVERAGE_TEMPLATE
|
||||
] = weather_data.last_cloud_coverage
|
||||
self._rendered[CONF_CONDITION_TEMPLATE] = state.state
|
||||
self._rendered[CONF_DEW_POINT_TEMPLATE] = weather_data.last_dew_point
|
||||
self._rendered[CONF_HUMIDITY_TEMPLATE] = weather_data.last_humidity
|
||||
self._rendered[CONF_OZONE_TEMPLATE] = weather_data.last_ozone
|
||||
self._rendered[CONF_PRESSURE_TEMPLATE] = weather_data.last_pressure
|
||||
self._rendered[CONF_TEMPERATURE_TEMPLATE] = weather_data.last_temperature
|
||||
self._rendered[CONF_VISIBILITY_TEMPLATE] = weather_data.last_visibility
|
||||
self._rendered[CONF_WIND_BEARING_TEMPLATE] = weather_data.last_wind_bearing
|
||||
self._rendered[
|
||||
CONF_WIND_GUST_SPEED_TEMPLATE
|
||||
] = weather_data.last_wind_gust_speed
|
||||
self._rendered[CONF_WIND_SPEED_TEMPLATE] = weather_data.last_wind_speed
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
return self._rendered.get(CONF_CONDITION_TEMPLATE)
|
||||
|
||||
@property
|
||||
def native_temperature(self) -> float | None:
|
||||
"""Return the temperature."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_TEMPERATURE_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def humidity(self) -> float | None:
|
||||
"""Return the humidity."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_HUMIDITY_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def native_wind_speed(self) -> float | None:
|
||||
"""Return the wind speed."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_WIND_SPEED_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> float | str | None:
|
||||
"""Return the wind bearing."""
|
||||
return vol.Any(vol.Coerce(float), vol.Coerce(str), None)(
|
||||
self._rendered.get(CONF_WIND_BEARING_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def ozone(self) -> float | None:
|
||||
"""Return the ozone level."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_OZONE_TEMPLATE),
|
||||
)
|
||||
|
||||
@property
|
||||
def native_visibility(self) -> float | None:
|
||||
"""Return the visibility."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_VISIBILITY_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def native_pressure(self) -> float | None:
|
||||
"""Return the air pressure."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_PRESSURE_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def native_wind_gust_speed(self) -> float | None:
|
||||
"""Return the wind gust speed."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_WIND_GUST_SPEED_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def cloud_coverage(self) -> float | None:
|
||||
"""Return the cloud coverage."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_CLOUD_COVERAGE_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def native_dew_point(self) -> float | None:
|
||||
"""Return the dew point."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_DEW_POINT_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def native_apparent_temperature(self) -> float | None:
|
||||
"""Return the apparent temperature."""
|
||||
return vol.Any(vol.Coerce(float), None)(
|
||||
self._rendered.get(CONF_APPARENT_TEMPERATURE_TEMPLATE)
|
||||
)
|
||||
|
||||
async def async_forecast_daily(self) -> list[Forecast]:
|
||||
"""Return the daily forecast in native units."""
|
||||
return vol.Any(vol.Coerce(list), None)(
|
||||
self._rendered.get(CONF_FORECAST_DAILY_TEMPLATE)
|
||||
)
|
||||
|
||||
async def async_forecast_hourly(self) -> list[Forecast]:
|
||||
"""Return the daily forecast in native units."""
|
||||
return vol.Any(vol.Coerce(list), None)(
|
||||
self._rendered.get(CONF_FORECAST_HOURLY_TEMPLATE)
|
||||
)
|
||||
|
||||
async def async_forecast_twice_daily(self) -> list[Forecast]:
|
||||
"""Return the daily forecast in native units."""
|
||||
return vol.Any(vol.Coerce(list), None)(
|
||||
self._rendered.get(CONF_FORECAST_TWICE_DAILY_TEMPLATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_restore_state_data(self) -> WeatherExtraStoredData:
|
||||
"""Return weather specific state data to be restored."""
|
||||
return WeatherExtraStoredData(
|
||||
last_apparent_temperature=self._rendered.get(
|
||||
CONF_APPARENT_TEMPERATURE_TEMPLATE
|
||||
),
|
||||
last_cloud_coverage=self._rendered.get(CONF_CLOUD_COVERAGE_TEMPLATE),
|
||||
last_dew_point=self._rendered.get(CONF_DEW_POINT_TEMPLATE),
|
||||
last_humidity=self._rendered.get(CONF_HUMIDITY_TEMPLATE),
|
||||
last_ozone=self._rendered.get(CONF_OZONE_TEMPLATE),
|
||||
last_pressure=self._rendered.get(CONF_PRESSURE_TEMPLATE),
|
||||
last_temperature=self._rendered.get(CONF_TEMPERATURE_TEMPLATE),
|
||||
last_visibility=self._rendered.get(CONF_VISIBILITY_TEMPLATE),
|
||||
last_wind_bearing=self._rendered.get(CONF_WIND_BEARING_TEMPLATE),
|
||||
last_wind_gust_speed=self._rendered.get(CONF_WIND_GUST_SPEED_TEMPLATE),
|
||||
last_wind_speed=self._rendered.get(CONF_WIND_SPEED_TEMPLATE),
|
||||
)
|
||||
|
||||
async def async_get_last_weather_data(self) -> WeatherExtraStoredData | None:
|
||||
"""Restore weather specific state data."""
|
||||
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
|
||||
return None
|
||||
return WeatherExtraStoredData.from_dict(restored_last_extra_data.as_dict())
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""The tests for the Template Weather platform."""
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
@ -18,8 +20,18 @@ from homeassistant.components.weather import (
|
||||
SERVICE_GET_FORECAST,
|
||||
Forecast,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import Context, HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import (
|
||||
assert_setup_component,
|
||||
async_mock_restore_state_shutdown_restart,
|
||||
mock_restore_cache_with_extra_data,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@ -493,3 +505,457 @@ async def test_forecast_format_error(
|
||||
return_response=True,
|
||||
)
|
||||
assert "Forecast in list is not a dict, see Weather documentation" in caplog.text
|
||||
|
||||
|
||||
SAVED_EXTRA_DATA = {
|
||||
"last_apparent_temperature": None,
|
||||
"last_cloud_coverage": None,
|
||||
"last_dew_point": None,
|
||||
"last_forecast": None,
|
||||
"last_humidity": 10,
|
||||
"last_ozone": None,
|
||||
"last_pressure": None,
|
||||
"last_temperature": 20,
|
||||
"last_visibility": None,
|
||||
"last_wind_bearing": None,
|
||||
"last_wind_gust_speed": None,
|
||||
"last_wind_speed": None,
|
||||
}
|
||||
|
||||
SAVED_EXTRA_DATA_WITH_FUTURE_KEY = {
|
||||
"last_apparent_temperature": None,
|
||||
"last_cloud_coverage": None,
|
||||
"last_dew_point": None,
|
||||
"last_forecast": None,
|
||||
"last_humidity": 10,
|
||||
"last_ozone": None,
|
||||
"last_pressure": None,
|
||||
"last_temperature": 20,
|
||||
"last_visibility": None,
|
||||
"last_wind_bearing": None,
|
||||
"last_wind_gust_speed": None,
|
||||
"last_wind_speed": None,
|
||||
"some_key_added_in_the_future": 123,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"weather": {
|
||||
"name": "test",
|
||||
"condition_template": "{{ trigger.event.data.condition }}",
|
||||
"temperature_template": "{{ trigger.event.data.temperature | float }}",
|
||||
"temperature_unit": "°C",
|
||||
"humidity_template": "{{ trigger.event.data.humidity | float }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("saved_state", "saved_extra_data", "initial_state"),
|
||||
[
|
||||
("sunny", SAVED_EXTRA_DATA, "sunny"),
|
||||
("sunny", SAVED_EXTRA_DATA_WITH_FUTURE_KEY, "sunny"),
|
||||
(STATE_UNAVAILABLE, SAVED_EXTRA_DATA, STATE_UNKNOWN),
|
||||
(STATE_UNKNOWN, SAVED_EXTRA_DATA, STATE_UNKNOWN),
|
||||
],
|
||||
)
|
||||
async def test_trigger_entity_restore_state(
|
||||
hass: HomeAssistant,
|
||||
count: int,
|
||||
domain: str,
|
||||
config: dict,
|
||||
saved_state: str,
|
||||
saved_extra_data: dict | None,
|
||||
initial_state: str,
|
||||
) -> None:
|
||||
"""Test restoring trigger template weather."""
|
||||
|
||||
restored_attributes = { # These should be ignored
|
||||
"temperature": -10,
|
||||
"humidity": 50,
|
||||
}
|
||||
|
||||
fake_state = State(
|
||||
"weather.test",
|
||||
saved_state,
|
||||
restored_attributes,
|
||||
)
|
||||
mock_restore_cache_with_extra_data(hass, ((fake_state, saved_extra_data),))
|
||||
with assert_setup_component(count, domain):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
domain,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.test")
|
||||
assert state.state == initial_state
|
||||
|
||||
hass.bus.async_fire(
|
||||
"test_event", {"condition": "cloudy", "temperature": 15, "humidity": 25}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("weather.test")
|
||||
|
||||
state = hass.states.get("weather.test")
|
||||
assert state.state == "cloudy"
|
||||
assert state.attributes["temperature"] == 15.0
|
||||
assert state.attributes["humidity"] == 25.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"template": [
|
||||
{
|
||||
"unique_id": "listening-test-event",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
{
|
||||
"variables": {
|
||||
"my_variable": "{{ trigger.event.data.temperature + 1 }}"
|
||||
},
|
||||
},
|
||||
],
|
||||
"weather": [
|
||||
{
|
||||
"name": "Hello Name",
|
||||
"condition_template": "sunny",
|
||||
"temperature_unit": "°C",
|
||||
"humidity_template": "{{ 20 }}",
|
||||
"temperature_template": "{{ my_variable + 1 }}",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_trigger_action(
|
||||
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test trigger entity with an action works."""
|
||||
state = hass.states.get("weather.hello_name")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
context = Context()
|
||||
hass.bus.async_fire("test_event", {"temperature": 1}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.hello_name")
|
||||
assert state.state == "sunny"
|
||||
assert state.attributes["temperature"] == 3.0
|
||||
assert state.context is context
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"template": [
|
||||
{
|
||||
"unique_id": "listening-test-event",
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"action": [
|
||||
{
|
||||
"variables": {
|
||||
"my_variable": "{{ trigger.event.data.information + 1 }}",
|
||||
"var_forecast_daily": "{{ trigger.event.data.forecast_daily }}",
|
||||
"var_forecast_hourly": "{{ trigger.event.data.forecast_hourly }}",
|
||||
"var_forecast_twice_daily": "{{ trigger.event.data.forecast_twice_daily }}",
|
||||
},
|
||||
},
|
||||
],
|
||||
"weather": [
|
||||
{
|
||||
"name": "Test",
|
||||
"condition_template": "sunny",
|
||||
"precipitation_unit": "mm",
|
||||
"pressure_unit": "hPa",
|
||||
"visibility_unit": "km",
|
||||
"wind_speed_unit": "km/h",
|
||||
"temperature_unit": "°C",
|
||||
"temperature_template": "{{ my_variable + 1 }}",
|
||||
"humidity_template": "{{ my_variable + 1 }}",
|
||||
"wind_speed_template": "{{ my_variable + 1 }}",
|
||||
"wind_bearing_template": "{{ my_variable + 1 }}",
|
||||
"ozone_template": "{{ my_variable + 1 }}",
|
||||
"visibility_template": "{{ my_variable + 1 }}",
|
||||
"pressure_template": "{{ my_variable + 1 }}",
|
||||
"wind_gust_speed_template": "{{ my_variable + 1 }}",
|
||||
"cloud_coverage_template": "{{ my_variable + 1 }}",
|
||||
"dew_point_template": "{{ my_variable + 1 }}",
|
||||
"apparent_temperature_template": "{{ my_variable + 1 }}",
|
||||
"forecast_template": "{{ var_forecast_daily }}",
|
||||
"forecast_daily_template": "{{ var_forecast_daily }}",
|
||||
"forecast_hourly_template": "{{ var_forecast_hourly }}",
|
||||
"forecast_twice_daily_template": "{{ var_forecast_twice_daily }}",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_trigger_weather_services(
|
||||
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test trigger weather entity with services."""
|
||||
state = hass.states.get("weather.test")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
context = Context()
|
||||
now = dt_util.now().isoformat()
|
||||
hass.bus.async_fire(
|
||||
"test_event",
|
||||
{
|
||||
"information": 1,
|
||||
"forecast_daily": [
|
||||
{
|
||||
"datetime": now,
|
||||
"condition": "sunny",
|
||||
"precipitation": 20,
|
||||
"temperature": 20,
|
||||
"templow": 15,
|
||||
}
|
||||
],
|
||||
"forecast_hourly": [
|
||||
{
|
||||
"datetime": now,
|
||||
"condition": "sunny",
|
||||
"precipitation": 20,
|
||||
"temperature": 20,
|
||||
"templow": 15,
|
||||
}
|
||||
],
|
||||
"forecast_twice_daily": [
|
||||
{
|
||||
"datetime": now,
|
||||
"condition": "sunny",
|
||||
"precipitation": 20,
|
||||
"temperature": 20,
|
||||
"templow": 15,
|
||||
"is_daytime": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.test")
|
||||
assert state.state == "sunny"
|
||||
assert state.attributes["temperature"] == 3.0
|
||||
assert state.attributes["humidity"] == 3.0
|
||||
assert state.attributes["wind_speed"] == 3.0
|
||||
assert state.attributes["wind_bearing"] == 3.0
|
||||
assert state.attributes["ozone"] == 3.0
|
||||
assert state.attributes["visibility"] == 3.0
|
||||
assert state.attributes["pressure"] == 3.0
|
||||
assert state.attributes["wind_gust_speed"] == 3.0
|
||||
assert state.attributes["cloud_coverage"] == 3.0
|
||||
assert state.attributes["dew_point"] == 3.0
|
||||
assert state.attributes["apparent_temperature"] == 3.0
|
||||
assert state.context is context
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": state.entity_id,
|
||||
"type": "daily",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"forecast": [
|
||||
{
|
||||
"datetime": now,
|
||||
"condition": "sunny",
|
||||
"precipitation": 20.0,
|
||||
"temperature": 20.0,
|
||||
"templow": 15.0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": state.entity_id,
|
||||
"type": "hourly",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"forecast": [
|
||||
{
|
||||
"datetime": now,
|
||||
"condition": "sunny",
|
||||
"precipitation": 20.0,
|
||||
"temperature": 20.0,
|
||||
"templow": 15.0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": state.entity_id,
|
||||
"type": "twice_daily",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"forecast": [
|
||||
{
|
||||
"datetime": now,
|
||||
"condition": "sunny",
|
||||
"precipitation": 20.0,
|
||||
"temperature": 20.0,
|
||||
"templow": 15.0,
|
||||
"is_daytime": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_restore_weather_save_state(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test Restore saved state for Weather trigger template."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"weather": {
|
||||
"name": "test",
|
||||
"condition_template": "{{ trigger.event.data.condition }}",
|
||||
"temperature_template": "{{ trigger.event.data.temperature | float }}",
|
||||
"temperature_unit": "°C",
|
||||
"humidity_template": "{{ trigger.event.data.humidity | float }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.bus.async_fire(
|
||||
"test_event", {"condition": "cloudy", "temperature": 15, "humidity": 25}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
entity = hass.states.get("weather.test")
|
||||
|
||||
# Trigger saving state
|
||||
await async_mock_restore_state_shutdown_restart(hass)
|
||||
|
||||
assert len(hass_storage[RESTORE_STATE_KEY]["data"]) == 1
|
||||
state = hass_storage[RESTORE_STATE_KEY]["data"][0]["state"]
|
||||
assert state["entity_id"] == entity.entity_id
|
||||
extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"]
|
||||
assert extra_data == {
|
||||
"last_apparent_temperature": None,
|
||||
"last_cloud_coverage": None,
|
||||
"last_dew_point": None,
|
||||
"last_humidity": "25.0",
|
||||
"last_ozone": None,
|
||||
"last_pressure": None,
|
||||
"last_temperature": "15.0",
|
||||
"last_visibility": None,
|
||||
"last_wind_bearing": None,
|
||||
"last_wind_gust_speed": None,
|
||||
"last_wind_speed": None,
|
||||
}
|
||||
|
||||
|
||||
SAVED_ATTRIBUTES_1 = {
|
||||
"humidity": 20,
|
||||
"temperature": 10,
|
||||
}
|
||||
|
||||
SAVED_EXTRA_DATA_MISSING_KEY = {
|
||||
"last_cloud_coverage": None,
|
||||
"last_dew_point": None,
|
||||
"last_humidity": 20,
|
||||
"last_ozone": None,
|
||||
"last_pressure": None,
|
||||
"last_temperature": 20,
|
||||
"last_visibility": None,
|
||||
"last_wind_bearing": None,
|
||||
"last_wind_gust_speed": None,
|
||||
"last_wind_speed": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("saved_attributes", "saved_extra_data"),
|
||||
[
|
||||
(SAVED_ATTRIBUTES_1, SAVED_EXTRA_DATA_MISSING_KEY),
|
||||
(SAVED_ATTRIBUTES_1, None),
|
||||
],
|
||||
)
|
||||
async def test_trigger_entity_restore_state_fail(
|
||||
hass: HomeAssistant,
|
||||
saved_attributes: dict,
|
||||
saved_extra_data: dict | None,
|
||||
) -> None:
|
||||
"""Test restoring trigger template weather fails due to missing attribute."""
|
||||
|
||||
saved_state = State(
|
||||
"weather.test",
|
||||
None,
|
||||
saved_attributes,
|
||||
)
|
||||
mock_restore_cache_with_extra_data(hass, ((saved_state, saved_extra_data),))
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"weather": {
|
||||
"name": "test",
|
||||
"condition_template": "{{ trigger.event.data.condition }}",
|
||||
"temperature_template": "{{ trigger.event.data.temperature | float }}",
|
||||
"temperature_unit": "°C",
|
||||
"humidity_template": "{{ trigger.event.data.humidity | float }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.test")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert state.attributes.get("temperature") is None
|
||||
|
Loading…
x
Reference in New Issue
Block a user