mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
Modernize template weather (#98064)
* Modernize template weather * mods * adds templates * Fixes * review comments * more comments * Fix validator * Tests * Mods * Fix ruff
This commit is contained in:
parent
d9906b63b7
commit
207e3f90a6
@ -1,6 +1,9 @@
|
||||
"""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 (
|
||||
@ -22,9 +25,11 @@ from homeassistant.components.weather import (
|
||||
ENTITY_ID_FORMAT,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
@ -39,6 +44,8 @@ from homeassistant.util.unit_conversion import (
|
||||
|
||||
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,
|
||||
@ -68,6 +75,9 @@ 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"
|
||||
@ -77,30 +87,40 @@ CONF_CLOUD_COVERAGE_TEMPLATE = "cloud_coverage_template"
|
||||
CONF_DEW_POINT_TEMPLATE = "dew_point_template"
|
||||
CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
|
||||
|
||||
PLATFORM_SCHEMA = 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_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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -151,6 +171,11 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
|
||||
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)
|
||||
@ -180,6 +205,17 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
|
||||
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:
|
||||
@ -246,6 +282,18 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
|
||||
"""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."""
|
||||
@ -327,4 +375,73 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
|
||||
"_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
|
||||
|
@ -2,6 +2,7 @@
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST,
|
||||
ATTR_WEATHER_APPARENT_TEMPERATURE,
|
||||
ATTR_WEATHER_CLOUD_COVERAGE,
|
||||
ATTR_WEATHER_DEW_POINT,
|
||||
@ -13,13 +14,15 @@ from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_WIND_BEARING,
|
||||
ATTR_WEATHER_WIND_GUST_SPEED,
|
||||
ATTR_WEATHER_WIND_SPEED,
|
||||
DOMAIN,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
Forecast,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, DOMAIN)])
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
@ -74,3 +77,419 @@ async def test_template_state_text(hass: HomeAssistant, start_ha) -> None:
|
||||
assert state is not None
|
||||
assert state.state == "sunny"
|
||||
assert state.attributes.get(v_attr) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"weather": [
|
||||
{
|
||||
"platform": "template",
|
||||
"name": "forecast",
|
||||
"condition_template": "sunny",
|
||||
"forecast_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_daily_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_hourly_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}",
|
||||
"temperature_template": "{{ states('sensor.temperature') | float }}",
|
||||
"humidity_template": "{{ states('sensor.humidity') | int }}",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_forecasts(hass: HomeAssistant, start_ha) -> None:
|
||||
"""Test forecast service."""
|
||||
for attr, _v_attr, value in [
|
||||
("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3),
|
||||
("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60),
|
||||
]:
|
||||
hass.states.async_set(attr, value)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(
|
||||
"weather.forecast",
|
||||
"sunny",
|
||||
{
|
||||
ATTR_FORECAST: [
|
||||
Forecast(
|
||||
condition="cloudy",
|
||||
datetime="2023-02-17T14:00:00+00:00",
|
||||
temperature=14.2,
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"weather.forecast_twice_daily",
|
||||
"fog",
|
||||
{
|
||||
ATTR_FORECAST: [
|
||||
Forecast(
|
||||
condition="fog",
|
||||
datetime="2023-02-17T14:00:00+00:00",
|
||||
temperature=14.2,
|
||||
is_daytime=True,
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("weather.forecast")
|
||||
assert state is not None
|
||||
assert state.state == "sunny"
|
||||
state2 = hass.states.get("weather.forecast_twice_daily")
|
||||
assert state2 is not None
|
||||
assert state2.state == "fog"
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "daily"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"forecast": [
|
||||
{
|
||||
"condition": "cloudy",
|
||||
"datetime": "2023-02-17T14:00:00+00:00",
|
||||
"temperature": 14.2,
|
||||
}
|
||||
]
|
||||
}
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "hourly"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"forecast": [
|
||||
{
|
||||
"condition": "cloudy",
|
||||
"datetime": "2023-02-17T14:00:00+00:00",
|
||||
"temperature": 14.2,
|
||||
}
|
||||
]
|
||||
}
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "twice_daily"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"forecast": [
|
||||
{
|
||||
"condition": "fog",
|
||||
"datetime": "2023-02-17T14:00:00+00:00",
|
||||
"temperature": 14.2,
|
||||
"is_daytime": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
hass.states.async_set(
|
||||
"weather.forecast",
|
||||
"sunny",
|
||||
{
|
||||
ATTR_FORECAST: [
|
||||
Forecast(
|
||||
condition="cloudy",
|
||||
datetime="2023-02-17T14:00:00+00:00",
|
||||
temperature=16.9,
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("weather.forecast")
|
||||
assert state is not None
|
||||
assert state.state == "sunny"
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "daily"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"forecast": [
|
||||
{
|
||||
"condition": "cloudy",
|
||||
"datetime": "2023-02-17T14:00:00+00:00",
|
||||
"temperature": 16.9,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"weather": [
|
||||
{
|
||||
"platform": "template",
|
||||
"name": "forecast",
|
||||
"condition_template": "sunny",
|
||||
"forecast_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_daily_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_hourly_template": "{{ states.weather.forecast_hourly.attributes.forecast }}",
|
||||
"temperature_template": "{{ states('sensor.temperature') | float }}",
|
||||
"humidity_template": "{{ states('sensor.humidity') | int }}",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_forecast_invalid(
|
||||
hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test invalid forecasts."""
|
||||
for attr, _v_attr, value in [
|
||||
("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3),
|
||||
("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60),
|
||||
]:
|
||||
hass.states.async_set(attr, value)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(
|
||||
"weather.forecast",
|
||||
"sunny",
|
||||
{
|
||||
ATTR_FORECAST: [
|
||||
Forecast(
|
||||
condition="cloudy",
|
||||
datetime="2023-02-17T14:00:00+00:00",
|
||||
temperature=14.2,
|
||||
not_correct=1,
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"weather.forecast_hourly",
|
||||
"sunny",
|
||||
{ATTR_FORECAST: None},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("weather.forecast_hourly")
|
||||
assert state is not None
|
||||
assert state.state == "sunny"
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "daily"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {"forecast": []}
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "hourly"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {"forecast": []}
|
||||
assert "Only valid keys in Forecast are allowed" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"weather": [
|
||||
{
|
||||
"platform": "template",
|
||||
"name": "forecast",
|
||||
"condition_template": "sunny",
|
||||
"forecast_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}",
|
||||
"temperature_template": "{{ states('sensor.temperature') | float }}",
|
||||
"humidity_template": "{{ states('sensor.humidity') | int }}",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_forecast_invalid_is_daytime_missing_in_twice_daily(
|
||||
hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test forecast service invalid when is_daytime missing in twice_daily forecast."""
|
||||
for attr, _v_attr, value in [
|
||||
("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3),
|
||||
("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60),
|
||||
]:
|
||||
hass.states.async_set(attr, value)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(
|
||||
"weather.forecast_twice_daily",
|
||||
"sunny",
|
||||
{
|
||||
ATTR_FORECAST: [
|
||||
Forecast(
|
||||
condition="cloudy",
|
||||
datetime="2023-02-17T14:00:00+00:00",
|
||||
temperature=14.2,
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("weather.forecast_twice_daily")
|
||||
assert state is not None
|
||||
assert state.state == "sunny"
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "twice_daily"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {"forecast": []}
|
||||
assert "`is_daytime` is missing in twice_daily forecast" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"weather": [
|
||||
{
|
||||
"platform": "template",
|
||||
"name": "forecast",
|
||||
"condition_template": "sunny",
|
||||
"forecast_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}",
|
||||
"temperature_template": "{{ states('sensor.temperature') | float }}",
|
||||
"humidity_template": "{{ states('sensor.humidity') | int }}",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_forecast_invalid_datetime_missing(
|
||||
hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test forecast service invalid when datetime missing."""
|
||||
for attr, _v_attr, value in [
|
||||
("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3),
|
||||
("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60),
|
||||
]:
|
||||
hass.states.async_set(attr, value)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(
|
||||
"weather.forecast_twice_daily",
|
||||
"sunny",
|
||||
{
|
||||
ATTR_FORECAST: [
|
||||
Forecast(
|
||||
condition="cloudy",
|
||||
temperature=14.2,
|
||||
is_daytime=True,
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("weather.forecast_twice_daily")
|
||||
assert state is not None
|
||||
assert state.state == "sunny"
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "twice_daily"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {"forecast": []}
|
||||
assert "`datetime` is required in forecasts" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{
|
||||
"weather": [
|
||||
{
|
||||
"platform": "template",
|
||||
"name": "forecast",
|
||||
"condition_template": "sunny",
|
||||
"forecast_template": "{{ states.weather.forecast.attributes.forecast }}",
|
||||
"forecast_daily_template": "{{ states.weather.forecast_daily.attributes.forecast }}",
|
||||
"forecast_hourly_template": "{{ states.weather.forecast_hourly.attributes.forecast }}",
|
||||
"temperature_template": "{{ states('sensor.temperature') | float }}",
|
||||
"humidity_template": "{{ states('sensor.humidity') | int }}",
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_forecast_format_error(
|
||||
hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test forecast service invalid on incorrect format."""
|
||||
for attr, _v_attr, value in [
|
||||
("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3),
|
||||
("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60),
|
||||
]:
|
||||
hass.states.async_set(attr, value)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(
|
||||
"weather.forecast_daily",
|
||||
"sunny",
|
||||
{
|
||||
ATTR_FORECAST: [
|
||||
"cloudy",
|
||||
"2023-02-17T14:00:00+00:00",
|
||||
14.2,
|
||||
1,
|
||||
]
|
||||
},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"weather.forecast_hourly",
|
||||
"sunny",
|
||||
{
|
||||
ATTR_FORECAST: {
|
||||
"condition": "cloudy",
|
||||
"temperature": 14.2,
|
||||
"is_daytime": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "daily"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert "Forecasts is not a list, see Weather documentation" in caplog.text
|
||||
await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{"entity_id": "weather.forecast", "type": "hourly"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert "Forecast in list is not a dict, see Weather documentation" in caplog.text
|
||||
|
Loading…
x
Reference in New Issue
Block a user