mirror of
https://github.com/home-assistant/core.git
synced 2026-11-06 13:41:27 -04:00
Add additional sun conditions (#174537)
This commit is contained in:
@@ -4,16 +4,24 @@ from datetime import datetime
|
||||
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import trace
|
||||
from homeassistant.helpers.condition import async_validate_condition_config
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
# San Diego (default test location) and Longyearbyen, Svalbard (deep polar).
|
||||
_SAN_DIEGO = (32.87336, -117.22743, "US/Pacific")
|
||||
_SVALBARD = (78.22, 15.65, "Europe/Oslo")
|
||||
|
||||
_TWILIGHT_TYPES = ("any", "civil", "nautical", "astronomical")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def prepare_condition_trace() -> None:
|
||||
@@ -1248,3 +1256,306 @@ async def test_if_action_after_sunset_no_offset_kotzebue(
|
||||
"sun",
|
||||
{"result": True, "wanted_time_after": "2015-07-23T11:22:18.467277+00:00"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "location", "now", "expected"),
|
||||
[
|
||||
# San Diego, just after solar noon (sun high, descending).
|
||||
(
|
||||
"sun.is_up",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 20, tzinfo=dt_util.UTC),
|
||||
True,
|
||||
),
|
||||
(
|
||||
"sun.is_set",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 20, tzinfo=dt_util.UTC),
|
||||
False,
|
||||
),
|
||||
(
|
||||
"sun.is_descending",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 20, tzinfo=dt_util.UTC),
|
||||
True,
|
||||
),
|
||||
(
|
||||
"sun.is_ascending",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 20, tzinfo=dt_util.UTC),
|
||||
False,
|
||||
),
|
||||
(
|
||||
"sun.is_night",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 20, tzinfo=dt_util.UTC),
|
||||
False,
|
||||
),
|
||||
# San Diego, just before solar noon (sun high, rising).
|
||||
(
|
||||
"sun.is_ascending",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 19, tzinfo=dt_util.UTC),
|
||||
True,
|
||||
),
|
||||
(
|
||||
"sun.is_descending",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 19, tzinfo=dt_util.UTC),
|
||||
False,
|
||||
),
|
||||
# San Diego, deep night.
|
||||
(
|
||||
"sun.is_set",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 8, 30, tzinfo=dt_util.UTC),
|
||||
True,
|
||||
),
|
||||
(
|
||||
"sun.is_up",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 8, 30, tzinfo=dt_util.UTC),
|
||||
False,
|
||||
),
|
||||
(
|
||||
"sun.is_night",
|
||||
_SAN_DIEGO,
|
||||
datetime(2015, 9, 15, 8, 30, tzinfo=dt_util.UTC),
|
||||
True,
|
||||
),
|
||||
# Svalbard: above the horizon during midnight sun (June), below during
|
||||
# polar night (December).
|
||||
(
|
||||
"sun.is_up",
|
||||
_SVALBARD,
|
||||
datetime(2015, 6, 15, 12, tzinfo=dt_util.UTC),
|
||||
True,
|
||||
),
|
||||
(
|
||||
"sun.is_set",
|
||||
_SVALBARD,
|
||||
datetime(2015, 12, 15, 12, tzinfo=dt_util.UTC),
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_sun_state_conditions(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
condition_key: str,
|
||||
location: tuple[float, float, str],
|
||||
now: datetime,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""Test the option-less sun state conditions evaluate from the sun position."""
|
||||
latitude, longitude, time_zone = location
|
||||
await hass.config.async_set_time_zone(time_zone)
|
||||
hass.config.latitude = latitude
|
||||
hass.config.longitude = longitude
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {"condition": condition_key},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert bool(service_calls) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition_key",
|
||||
[
|
||||
"sun.is_up",
|
||||
"sun.is_set",
|
||||
"sun.is_ascending",
|
||||
"sun.is_descending",
|
||||
"sun.is_night",
|
||||
],
|
||||
)
|
||||
async def test_sun_state_condition_takes_no_options(
|
||||
hass: HomeAssistant, condition_key: str
|
||||
) -> None:
|
||||
"""Test the sun state conditions accept no target and reject options."""
|
||||
await async_validate_condition_config(hass, {"condition": condition_key})
|
||||
with pytest.raises(vol.Invalid):
|
||||
await async_validate_condition_config(
|
||||
hass, {"condition": condition_key, "options": {"unknown": True}}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("threshold", "elevation", "expected"),
|
||||
[
|
||||
({"type": "above", "value": {"number": 10}}, 15.0, True),
|
||||
({"type": "above", "value": {"number": 10}}, 5.0, False),
|
||||
({"type": "below", "value": {"number": 0}}, -5.0, True),
|
||||
({"type": "below", "value": {"number": 0}}, 5.0, False),
|
||||
# Negative thresholds (sun below the horizon) are valid.
|
||||
({"type": "below", "value": {"number": -6}}, -10.0, True),
|
||||
(
|
||||
{
|
||||
"type": "between",
|
||||
"value_min": {"number": -6},
|
||||
"value_max": {"number": 6},
|
||||
},
|
||||
0.0,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
"type": "between",
|
||||
"value_min": {"number": -6},
|
||||
"value_max": {"number": 6},
|
||||
},
|
||||
10.0,
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_elevation_condition(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
threshold: dict[str, object],
|
||||
elevation: float,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""Test the elevation condition compares the sun's elevation to a threshold."""
|
||||
hass.states.async_set("sun.sun", "above_horizon", {"elevation": elevation})
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {
|
||||
"condition": "sun.elevation",
|
||||
"options": {"threshold": threshold},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert bool(service_calls) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("condition_key", "now", "expected_true"),
|
||||
[
|
||||
# San Diego morning twilight (rising). The twilight bands are mutually
|
||||
# exclusive, so at each elevation exactly one specific band matches (plus
|
||||
# "any" whenever the sun is in any twilight band at all).
|
||||
# 13:15Z ~ -4.4° (civil band).
|
||||
(
|
||||
"sun.is_morning_twilight",
|
||||
datetime(2015, 9, 15, 13, 15, tzinfo=dt_util.UTC),
|
||||
{"any", "civil"},
|
||||
),
|
||||
# 13:00Z ~ -7.6° (nautical band).
|
||||
(
|
||||
"sun.is_morning_twilight",
|
||||
datetime(2015, 9, 15, 13, 0, tzinfo=dt_util.UTC),
|
||||
{"any", "nautical"},
|
||||
),
|
||||
# 12:30Z ~ -13.8° (astronomical band).
|
||||
(
|
||||
"sun.is_morning_twilight",
|
||||
datetime(2015, 9, 15, 12, 30, tzinfo=dt_util.UTC),
|
||||
{"any", "astronomical"},
|
||||
),
|
||||
# 12:00Z ~ -19.8° (night, below all twilight bands).
|
||||
(
|
||||
"sun.is_morning_twilight",
|
||||
datetime(2015, 9, 15, 12, 0, tzinfo=dt_util.UTC),
|
||||
set(),
|
||||
),
|
||||
# Morning twilight requires the sun to be rising; an evening (descending)
|
||||
# time matches no type.
|
||||
(
|
||||
"sun.is_morning_twilight",
|
||||
datetime(2015, 9, 16, 2, 45, tzinfo=dt_util.UTC),
|
||||
set(),
|
||||
),
|
||||
# San Diego evening twilight (descending).
|
||||
# 02:15Z ~ -4.9° (civil band).
|
||||
(
|
||||
"sun.is_evening_twilight",
|
||||
datetime(2015, 9, 16, 2, 15, tzinfo=dt_util.UTC),
|
||||
{"any", "civil"},
|
||||
),
|
||||
# 02:45Z ~ -11.2° (nautical band).
|
||||
(
|
||||
"sun.is_evening_twilight",
|
||||
datetime(2015, 9, 16, 2, 45, tzinfo=dt_util.UTC),
|
||||
{"any", "nautical"},
|
||||
),
|
||||
# 03:15Z ~ -17.3° (astronomical band).
|
||||
(
|
||||
"sun.is_evening_twilight",
|
||||
datetime(2015, 9, 16, 3, 15, tzinfo=dt_util.UTC),
|
||||
{"any", "astronomical"},
|
||||
),
|
||||
# Evening twilight requires the sun to be descending; a morning (rising)
|
||||
# time matches no type.
|
||||
(
|
||||
"sun.is_evening_twilight",
|
||||
datetime(2015, 9, 15, 13, 0, tzinfo=dt_util.UTC),
|
||||
set(),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_twilight_condition_type(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
condition_key: str,
|
||||
now: datetime,
|
||||
expected_true: set[str],
|
||||
) -> None:
|
||||
"""Test the morning/evening twilight conditions honor the twilight type band.
|
||||
|
||||
At a single point in time every twilight type is checked, so the mutually
|
||||
exclusive bands are all asserted together.
|
||||
"""
|
||||
latitude, longitude, time_zone = _SAN_DIEGO
|
||||
await hass.config.async_set_time_zone(time_zone)
|
||||
hass.config.latitude = latitude
|
||||
hass.config.longitude = longitude
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"condition": {
|
||||
"condition": condition_key,
|
||||
"options": {"type": twilight_type},
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data": {"type": twilight_type},
|
||||
},
|
||||
}
|
||||
for twilight_type in _TWILIGHT_TYPES
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
with freeze_time(now):
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert {call.data["type"] for call in service_calls} == expected_true
|
||||
|
||||
@@ -2741,6 +2741,18 @@ async def test_or_condition_with_disabled_condition(hass: HomeAssistant) -> None
|
||||
)
|
||||
|
||||
|
||||
_MODERN_SUN_CONDITIONS = (
|
||||
"sun.elevation",
|
||||
"sun.is_ascending",
|
||||
"sun.is_descending",
|
||||
"sun.is_evening_twilight",
|
||||
"sun.is_morning_twilight",
|
||||
"sun.is_night",
|
||||
"sun.is_set",
|
||||
"sun.is_up",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sun_condition_descriptions",
|
||||
[
|
||||
@@ -2883,7 +2895,9 @@ async def test_async_get_all_descriptions(
|
||||
},
|
||||
"before_offset": {"selector": {"time": {}}},
|
||||
}
|
||||
}
|
||||
},
|
||||
# The modern sun conditions have no entry in the mocked conditions.yaml.
|
||||
**dict.fromkeys(_MODERN_SUN_CONDITIONS),
|
||||
}
|
||||
assert descriptions == expected_descriptions
|
||||
|
||||
@@ -3023,7 +3037,7 @@ async def test_async_get_all_descriptions_with_yaml_error(
|
||||
):
|
||||
descriptions = await condition.async_get_all_descriptions(hass)
|
||||
|
||||
assert descriptions == {SUN_DOMAIN: None}
|
||||
assert descriptions == {SUN_DOMAIN: None, **dict.fromkeys(_MODERN_SUN_CONDITIONS)}
|
||||
|
||||
assert expected_message in caplog.text
|
||||
|
||||
@@ -3056,7 +3070,7 @@ async def test_async_get_all_descriptions_with_bad_description(
|
||||
):
|
||||
descriptions = await condition.async_get_all_descriptions(hass)
|
||||
|
||||
assert descriptions == {"sun": None}
|
||||
assert descriptions == {"sun": None, **dict.fromkeys(_MODERN_SUN_CONDITIONS)}
|
||||
|
||||
assert (
|
||||
"Unable to parse conditions.yaml for the sun integration: "
|
||||
@@ -3119,7 +3133,7 @@ async def test_subscribe_conditions(
|
||||
|
||||
assert await async_setup_component(hass, "sun", {})
|
||||
|
||||
assert condition_events == [{"sun"}]
|
||||
assert condition_events == [{"sun", *_MODERN_SUN_CONDITIONS}]
|
||||
assert "Error while notifying condition platform listener" in caplog.text
|
||||
|
||||
await hass.data["entity_components"][SUN_DOMAIN]._async_reset()
|
||||
|
||||
Reference in New Issue
Block a user