mirror of
https://github.com/home-assistant/core.git
synced 2026-08-06 13:26:29 +01:00
Add sun event trigger offset (#176967)
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
"common": {
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_for_name": "For at least",
|
||||
"trigger_offset_description": "Time to offset the trigger from the solar event.",
|
||||
"trigger_offset_name": "Offset",
|
||||
"trigger_offset_type_description": "Whether to trigger before or after the solar event.",
|
||||
"trigger_offset_type_name": "Offset type",
|
||||
"trigger_threshold_name": "Threshold type",
|
||||
"twilight_type_description": "The phase of twilight.",
|
||||
"twilight_type_name": "Twilight type"
|
||||
@@ -95,6 +99,12 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_offset_type": {
|
||||
"options": {
|
||||
"after": "After",
|
||||
"before": "Before"
|
||||
}
|
||||
},
|
||||
"twilight_type": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
@@ -109,6 +119,14 @@
|
||||
"dawn": {
|
||||
"description": "Triggers at dawn, when civil, nautical, or astronomical twilight begins.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_name%]"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_type_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_type_name%]"
|
||||
},
|
||||
"type": {
|
||||
"description": "[%key:component::sun::common::twilight_type_description%]",
|
||||
"name": "[%key:component::sun::common::twilight_type_name%]"
|
||||
@@ -119,6 +137,14 @@
|
||||
"dusk": {
|
||||
"description": "Triggers at dusk, when civil, nautical, or astronomical twilight ends.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_name%]"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_type_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_type_name%]"
|
||||
},
|
||||
"type": {
|
||||
"description": "[%key:component::sun::common::twilight_type_description%]",
|
||||
"name": "[%key:component::sun::common::twilight_type_name%]"
|
||||
@@ -149,18 +175,58 @@
|
||||
},
|
||||
"solar_midnight": {
|
||||
"description": "Triggers when the sun reaches its lowest point.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_name%]"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_type_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_type_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Solar midnight"
|
||||
},
|
||||
"solar_noon": {
|
||||
"description": "Triggers when the sun reaches its highest point.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_name%]"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_type_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_type_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Solar noon"
|
||||
},
|
||||
"sunrise": {
|
||||
"description": "Triggers when the sun rises.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_name%]"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_type_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_type_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sunrise"
|
||||
},
|
||||
"sunset": {
|
||||
"description": "Triggers when the sun sets.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_name%]"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "[%key:component::sun::common::trigger_offset_type_description%]",
|
||||
"name": "[%key:component::sun::common::trigger_offset_type_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sunset"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,19 @@ _TWILIGHT_CIVIL = "civil"
|
||||
_TWILIGHT_NAUTICAL = "nautical"
|
||||
_TWILIGHT_ASTRONOMICAL = "astronomical"
|
||||
|
||||
CONF_OFFSET_TYPE = "offset_type"
|
||||
OFFSET_TYPE_BEFORE = "before"
|
||||
OFFSET_TYPE_AFTER = "after"
|
||||
|
||||
# Offset options shared by the solar event triggers. A positive offset combined
|
||||
# with an offset type of "before" fires earlier than the event; "after" later.
|
||||
_OFFSET_OPTIONS: dict[vol.Marker, Any] = {
|
||||
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
|
||||
vol.Required(CONF_OFFSET_TYPE, default=OFFSET_TYPE_BEFORE): vol.In(
|
||||
{OFFSET_TYPE_BEFORE, OFFSET_TYPE_AFTER}
|
||||
),
|
||||
}
|
||||
|
||||
# Sun elevation at each twilight boundary.
|
||||
_TWILIGHT_ELEVATIONS = {
|
||||
_TWILIGHT_CIVIL: ELEVATION_CIVIL,
|
||||
@@ -134,7 +147,9 @@ class SunElevationCrossedTrigger(
|
||||
_schema = _ELEVATION_CROSSED_TRIGGER_SCHEMA
|
||||
|
||||
|
||||
_EVENT_TRIGGER_SCHEMA = vol.Schema({vol.Required(CONF_OPTIONS, default=dict): {}})
|
||||
_EVENT_TRIGGER_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_OPTIONS, default=dict): {**_OFFSET_OPTIONS}}
|
||||
)
|
||||
|
||||
|
||||
class SunEventTrigger(Trigger):
|
||||
@@ -155,10 +170,16 @@ class SunEventTrigger(Trigger):
|
||||
"""Initialize the trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._options = config.options or {}
|
||||
offset = self._options.get(CONF_OFFSET) or timedelta(0)
|
||||
if self._options.get(CONF_OFFSET_TYPE) == OFFSET_TYPE_BEFORE:
|
||||
offset = -offset
|
||||
self._offset = offset
|
||||
|
||||
def _get_next_event(self, utc_point_in_time: datetime) -> datetime:
|
||||
"""Return the next time this solar event occurs."""
|
||||
return get_astral_event_next(self._hass, self._event, utc_point_in_time)
|
||||
return get_astral_event_next(
|
||||
self._hass, self._event, utc_point_in_time, self._offset
|
||||
)
|
||||
|
||||
def _action_payload(self) -> dict[str, Any]:
|
||||
"""Return extra trigger payload passed to the action."""
|
||||
@@ -235,6 +256,7 @@ _DAWN_DUSK_TRIGGER_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_TYPE, default=_TWILIGHT_CIVIL): vol.In(
|
||||
_TWILIGHT_ELEVATIONS
|
||||
),
|
||||
**_OFFSET_OPTIONS,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -257,6 +279,7 @@ class SunDawnDuskTrigger(SunEventTrigger):
|
||||
get_astral_observer(self._hass),
|
||||
self._event,
|
||||
utc_point_in_time,
|
||||
self._offset,
|
||||
# astral takes a depression (degrees below the horizon), i.e. the
|
||||
# negated elevation.
|
||||
depression=-self._elevation,
|
||||
@@ -305,13 +328,6 @@ class LegacySunTrigger(SunEventTrigger):
|
||||
"""Initialize the trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._event = self._options[CONF_EVENT]
|
||||
self._offset: timedelta = self._options[CONF_OFFSET]
|
||||
|
||||
@override
|
||||
def _get_next_event(self, utc_point_in_time: datetime) -> datetime:
|
||||
return get_astral_event_next(
|
||||
self._hass, self._event, utc_point_in_time, self._offset
|
||||
)
|
||||
|
||||
@override
|
||||
def _action_payload(self) -> dict[str, Any]:
|
||||
|
||||
@@ -15,6 +15,27 @@
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.offset: &trigger_offset
|
||||
offset:
|
||||
required: true
|
||||
default:
|
||||
days: 0
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 0
|
||||
selector:
|
||||
duration:
|
||||
enable_day: true
|
||||
offset_type:
|
||||
required: true
|
||||
default: before
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_offset_type
|
||||
options:
|
||||
- before
|
||||
- after
|
||||
|
||||
.elevation_threshold_entity: &trigger_elevation_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "°"
|
||||
@@ -29,18 +50,31 @@
|
||||
mode: box
|
||||
unit_of_measurement: "°"
|
||||
|
||||
sunrise: {}
|
||||
sunset: {}
|
||||
solar_noon: {}
|
||||
solar_midnight: {}
|
||||
sunrise:
|
||||
fields:
|
||||
<<: *trigger_offset
|
||||
|
||||
sunset:
|
||||
fields:
|
||||
<<: *trigger_offset
|
||||
|
||||
solar_noon:
|
||||
fields:
|
||||
<<: *trigger_offset
|
||||
|
||||
solar_midnight:
|
||||
fields:
|
||||
<<: *trigger_offset
|
||||
|
||||
dawn:
|
||||
fields:
|
||||
type: *twilight_type
|
||||
<<: *trigger_offset
|
||||
|
||||
dusk:
|
||||
fields:
|
||||
type: *twilight_type
|
||||
<<: *trigger_offset
|
||||
|
||||
elevation_changed:
|
||||
fields:
|
||||
|
||||
@@ -338,6 +338,84 @@ async def test_dawn_defaults_to_civil(
|
||||
assert service_calls[0].data["type"] == "civil"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "astral_event"),
|
||||
[
|
||||
("sun.sunrise", SUN_EVENT_SUNRISE),
|
||||
("sun.sunset", SUN_EVENT_SUNSET),
|
||||
("sun.solar_noon", "noon"),
|
||||
("sun.solar_midnight", "midnight"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("offset_type", "sign"),
|
||||
[("before", -1), ("after", 1)],
|
||||
ids=["before", "after"],
|
||||
)
|
||||
async def test_event_trigger_offset(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
trigger_key: str,
|
||||
astral_event: str,
|
||||
offset_type: str,
|
||||
sign: int,
|
||||
) -> None:
|
||||
"""Test the solar event triggers apply a before/after time offset."""
|
||||
offset = timedelta(hours=1)
|
||||
with freeze_time(_TEST_DATETIME):
|
||||
await _arm_automation(
|
||||
hass,
|
||||
{
|
||||
"platform": trigger_key,
|
||||
"options": {"offset": {"hours": 1}, "offset_type": offset_type},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expected = get_astral_event_next(
|
||||
hass, astral_event, _TEST_DATETIME, sign * offset
|
||||
)
|
||||
# The offset shifts the fire time away from the bare event time.
|
||||
assert expected != get_astral_event_next(hass, astral_event, _TEST_DATETIME)
|
||||
|
||||
async_fire_time_changed(hass, expected + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("trigger_key", ["sun.dawn", "sun.dusk"])
|
||||
@pytest.mark.parametrize(
|
||||
("offset_type", "sign"),
|
||||
[("before", -1), ("after", 1)],
|
||||
ids=["before", "after"],
|
||||
)
|
||||
async def test_dawn_dusk_trigger_offset(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
trigger_key: str,
|
||||
offset_type: str,
|
||||
sign: int,
|
||||
) -> None:
|
||||
"""Test the dawn and dusk triggers apply a before/after time offset."""
|
||||
event = trigger_key.split(".")[1]
|
||||
offset = timedelta(hours=1)
|
||||
with freeze_time(_TEST_DATETIME):
|
||||
await _arm_automation(
|
||||
hass,
|
||||
{
|
||||
"platform": trigger_key,
|
||||
"options": {"offset": {"hours": 1}, "offset_type": offset_type},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expected = _DAWN_DUSK[event, "civil"] + sign * offset
|
||||
|
||||
async_fire_time_changed(hass, expected + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
# --- Edge cases: no matching solar event on the following day ----------------
|
||||
|
||||
# Longyearbyen, Svalbard (deep polar latitude) and Kotzebue, Alaska (above the
|
||||
|
||||
Reference in New Issue
Block a user