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:
G Johansson 2023-09-27 10:11:57 +02:00 committed by GitHub
parent fd53e116bb
commit 43954d660b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 768 additions and 36 deletions

View File

@ -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.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_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.config import async_log_exception, config_without_domain
from homeassistant.const import CONF_BINARY_SENSORS, CONF_SENSORS, CONF_UNIQUE_ID from homeassistant.const import CONF_BINARY_SENSORS, CONF_SENSORS, CONF_UNIQUE_ID
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -21,6 +22,7 @@ from . import (
number as number_platform, number as number_platform,
select as select_platform, select as select_platform,
sensor as sensor_platform, sensor as sensor_platform,
weather as weather_platform,
) )
from .const import CONF_ACTION, CONF_TRIGGER, DOMAIN from .const import CONF_ACTION, CONF_TRIGGER, DOMAIN
@ -55,6 +57,9 @@ CONFIG_SECTION_SCHEMA = vol.Schema(
vol.Optional(IMAGE_DOMAIN): vol.All( vol.Optional(IMAGE_DOMAIN): vol.All(
cv.ensure_list, [image_platform.IMAGE_SCHEMA] cv.ensure_list, [image_platform.IMAGE_SCHEMA]
), ),
vol.Optional(WEATHER_DOMAIN): vol.All(
cv.ensure_list, [weather_platform.WEATHER_SCHEMA]
),
} }
) )

View File

@ -1,8 +1,9 @@
"""Template platform that aggregates meteorological data.""" """Template platform that aggregates meteorological data."""
from __future__ import annotations from __future__ import annotations
from dataclasses import asdict, dataclass
from functools import partial from functools import partial
from typing import Any, Literal from typing import Any, Literal, Self
import voluptuous as vol import voluptuous as vol
@ -22,18 +23,27 @@ from homeassistant.components.weather import (
ATTR_CONDITION_SUNNY, ATTR_CONDITION_SUNNY,
ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY,
ATTR_CONDITION_WINDY_VARIANT, ATTR_CONDITION_WINDY_VARIANT,
DOMAIN as WEATHER_DOMAIN,
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
Forecast, Forecast,
WeatherEntity, WeatherEntity,
WeatherEntityFeature, 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.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.unit_conversion import ( from homeassistant.util.unit_conversion import (
DistanceConverter, DistanceConverter,
@ -42,7 +52,9 @@ from homeassistant.util.unit_conversion import (
TemperatureConverter, TemperatureConverter,
) )
from .coordinator import TriggerUpdateCoordinator
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
from .trigger_entity import TriggerEntity
CHECK_FORECAST_KEYS = ( CHECK_FORECAST_KEYS = (
set().union(Forecast.__annotations__.keys()) set().union(Forecast.__annotations__.keys())
@ -92,11 +104,9 @@ CONF_CLOUD_COVERAGE_TEMPLATE = "cloud_coverage_template"
CONF_DEW_POINT_TEMPLATE = "dew_point_template" CONF_DEW_POINT_TEMPLATE = "dew_point_template"
CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template" CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
PLATFORM_SCHEMA = vol.All( WEATHER_SCHEMA = vol.Schema(
cv.deprecated(CONF_FORECAST_TEMPLATE),
PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.template,
vol.Required(CONF_CONDITION_TEMPLATE): cv.template, vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template, vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
@ -111,21 +121,21 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In( vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
TemperatureConverter.VALID_UNITS
),
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.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_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS), vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_PRECIPITATION_UNIT): vol.In( vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(DistanceConverter.VALID_UNITS),
DistanceConverter.VALID_UNITS
),
vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template, vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template, vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
} }
), )
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_FORECAST_TEMPLATE),
PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema),
) )
@ -136,6 +146,12 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Template weather.""" """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) config = rewrite_common_legacy_to_modern_conf(config)
unique_id = config.get(CONF_UNIQUE_ID) unique_id = config.get(CONF_UNIQUE_ID)
@ -452,3 +468,248 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
) )
continue continue
return result 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())

View File

@ -1,4 +1,6 @@
"""The tests for the Template Weather platform.""" """The tests for the Template Weather platform."""
from typing import Any
import pytest import pytest
from homeassistant.components.weather import ( from homeassistant.components.weather import (
@ -18,8 +20,18 @@ from homeassistant.components.weather import (
SERVICE_GET_FORECAST, SERVICE_GET_FORECAST,
Forecast, Forecast,
) )
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant 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)]) @pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
@ -493,3 +505,457 @@ async def test_forecast_format_error(
return_response=True, return_response=True,
) )
assert "Forecast in list is not a dict, see Weather documentation" in caplog.text 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