mirror of
https://github.com/home-assistant/core.git
synced 2026-02-28 04:51:41 +01:00
Compare commits
7 Commits
github_mer
...
schedule/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53ce3bdf40 | ||
|
|
da3c362c1f | ||
|
|
e4289f8f24 | ||
|
|
b9acada665 | ||
|
|
c0b1d02448 | ||
|
|
37cf3a785b | ||
|
|
51ff1ca2d8 |
@@ -150,6 +150,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"media_player",
|
||||
"person",
|
||||
"scene",
|
||||
"schedule",
|
||||
"siren",
|
||||
"switch",
|
||||
"text",
|
||||
|
||||
@@ -6,5 +6,13 @@
|
||||
"reload": {
|
||||
"service": "mdi:reload"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"trigger": "mdi:calendar-blank"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:calendar-clock"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted schedules to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::schedule::title%]",
|
||||
@@ -20,6 +24,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_schedule": {
|
||||
"description": "Retrieves the configured time ranges of one or multiple schedules.",
|
||||
@@ -30,5 +43,27 @@
|
||||
"name": "[%key:common::action::reload%]"
|
||||
}
|
||||
},
|
||||
"title": "Schedule"
|
||||
"title": "Schedule",
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"description": "Triggers when a schedule block ends.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::schedule::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::schedule::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Schedule block ended"
|
||||
},
|
||||
"turned_on": {
|
||||
"description": "Triggers when a schedule block starts.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::schedule::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::schedule::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Schedule block started"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
homeassistant/components/schedule/trigger.py
Normal file
43
homeassistant/components/schedule/trigger.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Provides triggers for schedulers."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityTransitionTriggerBase,
|
||||
Trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
from . import DOMAIN
|
||||
from .const import ATTR_NEXT_EVENT
|
||||
|
||||
|
||||
class ScheduleBackToBackTrigger(EntityTransitionTriggerBase):
|
||||
"""Trigger for back-to-back schedule blocks."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_from_states = {STATE_OFF, STATE_ON}
|
||||
_to_states = {STATE_ON}
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state matches the expected ones."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
|
||||
from_next_event = from_state.attributes.get(ATTR_NEXT_EVENT)
|
||||
to_next_event = to_state.attributes.get(ATTR_NEXT_EVENT)
|
||||
|
||||
return (
|
||||
from_state.state in self._from_states and from_next_event != to_next_event
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": ScheduleBackToBackTrigger,
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for schedulers."""
|
||||
return TRIGGERS
|
||||
18
homeassistant/components/schedule/triggers.yaml
Normal file
18
homeassistant/components/schedule/triggers.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: schedule
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
108
tests/components/schedule/conftest.py
Normal file
108
tests/components/schedule/conftest.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Test for the Schedule integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.schedule import STORAGE_VERSION, STORAGE_VERSION_MINOR
|
||||
from homeassistant.components.schedule.const import (
|
||||
CONF_DATA,
|
||||
CONF_FRIDAY,
|
||||
CONF_FROM,
|
||||
CONF_MONDAY,
|
||||
CONF_SATURDAY,
|
||||
CONF_SUNDAY,
|
||||
CONF_THURSDAY,
|
||||
CONF_TO,
|
||||
CONF_TUESDAY,
|
||||
CONF_WEDNESDAY,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_ICON, CONF_ID, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schedule_setup(
|
||||
hass: HomeAssistant, hass_storage: dict[str, Any]
|
||||
) -> Callable[..., Coroutine[Any, Any, bool]]:
|
||||
"""Schedule setup."""
|
||||
|
||||
async def _schedule_setup(
|
||||
items: dict[str, Any] | None = None,
|
||||
config: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
if items is None:
|
||||
hass_storage[DOMAIN] = {
|
||||
"key": DOMAIN,
|
||||
"version": STORAGE_VERSION,
|
||||
"minor_version": STORAGE_VERSION_MINOR,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
CONF_ID: "from_storage",
|
||||
CONF_NAME: "from storage",
|
||||
CONF_ICON: "mdi:party-popper",
|
||||
CONF_FRIDAY: [
|
||||
{
|
||||
CONF_FROM: "17:00:00",
|
||||
CONF_TO: "23:59:59",
|
||||
CONF_DATA: {"party_level": "epic"},
|
||||
},
|
||||
],
|
||||
CONF_SATURDAY: [
|
||||
{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"},
|
||||
],
|
||||
CONF_SUNDAY: [
|
||||
{
|
||||
CONF_FROM: "00:00:00",
|
||||
CONF_TO: "24:00:00",
|
||||
CONF_DATA: {"entry": "VIPs only"},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
else:
|
||||
hass_storage[DOMAIN] = {
|
||||
"key": DOMAIN,
|
||||
"version": 1,
|
||||
"minor_version": STORAGE_VERSION_MINOR,
|
||||
"data": {"items": items},
|
||||
}
|
||||
if config is None:
|
||||
config = {
|
||||
DOMAIN: {
|
||||
"from_yaml": {
|
||||
CONF_NAME: "from yaml",
|
||||
CONF_ICON: "mdi:party-pooper",
|
||||
CONF_MONDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_TUESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_WEDNESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_THURSDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_FRIDAY: [
|
||||
{
|
||||
CONF_FROM: "00:00:00",
|
||||
CONF_TO: "23:59:59",
|
||||
CONF_DATA: {"party_level": "epic"},
|
||||
}
|
||||
],
|
||||
CONF_SATURDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_SUNDAY: [
|
||||
{
|
||||
CONF_FROM: "00:00:00",
|
||||
CONF_TO: "23:59:59",
|
||||
CONF_DATA: {"entry": "VIPs only"},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
return await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
return _schedule_setup
|
||||
@@ -10,7 +10,6 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.schedule import STORAGE_VERSION, STORAGE_VERSION_MINOR
|
||||
from homeassistant.components.schedule.const import (
|
||||
ATTR_NEXT_EVENT,
|
||||
CONF_ALL_DAYS,
|
||||
@@ -34,7 +33,6 @@ from homeassistant.const import (
|
||||
ATTR_NAME,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
EVENT_STATE_CHANGED,
|
||||
SERVICE_RELOAD,
|
||||
@@ -49,88 +47,6 @@ from tests.common import MockUser, async_capture_events, async_fire_time_changed
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schedule_setup(
|
||||
hass: HomeAssistant, hass_storage: dict[str, Any]
|
||||
) -> Callable[..., Coroutine[Any, Any, bool]]:
|
||||
"""Schedule setup."""
|
||||
|
||||
async def _schedule_setup(
|
||||
items: dict[str, Any] | None = None,
|
||||
config: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
if items is None:
|
||||
hass_storage[DOMAIN] = {
|
||||
"key": DOMAIN,
|
||||
"version": STORAGE_VERSION,
|
||||
"minor_version": STORAGE_VERSION_MINOR,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
CONF_ID: "from_storage",
|
||||
CONF_NAME: "from storage",
|
||||
CONF_ICON: "mdi:party-popper",
|
||||
CONF_FRIDAY: [
|
||||
{
|
||||
CONF_FROM: "17:00:00",
|
||||
CONF_TO: "23:59:59",
|
||||
CONF_DATA: {"party_level": "epic"},
|
||||
},
|
||||
],
|
||||
CONF_SATURDAY: [
|
||||
{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"},
|
||||
],
|
||||
CONF_SUNDAY: [
|
||||
{
|
||||
CONF_FROM: "00:00:00",
|
||||
CONF_TO: "24:00:00",
|
||||
CONF_DATA: {"entry": "VIPs only"},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
else:
|
||||
hass_storage[DOMAIN] = {
|
||||
"key": DOMAIN,
|
||||
"version": 1,
|
||||
"minor_version": STORAGE_VERSION_MINOR,
|
||||
"data": {"items": items},
|
||||
}
|
||||
if config is None:
|
||||
config = {
|
||||
DOMAIN: {
|
||||
"from_yaml": {
|
||||
CONF_NAME: "from yaml",
|
||||
CONF_ICON: "mdi:party-pooper",
|
||||
CONF_MONDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_TUESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_WEDNESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_THURSDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_FRIDAY: [
|
||||
{
|
||||
CONF_FROM: "00:00:00",
|
||||
CONF_TO: "23:59:59",
|
||||
CONF_DATA: {"party_level": "epic"},
|
||||
}
|
||||
],
|
||||
CONF_SATURDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}],
|
||||
CONF_SUNDAY: [
|
||||
{
|
||||
CONF_FROM: "00:00:00",
|
||||
CONF_TO: "23:59:59",
|
||||
CONF_DATA: {"entry": "VIPs only"},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
return await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
return _schedule_setup
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid_config", [None, {"name with space": None}])
|
||||
async def test_invalid_config(hass: HomeAssistant, invalid_config) -> None:
|
||||
"""Test invalid configs."""
|
||||
|
||||
299
tests/components/schedule/test_trigger.py
Normal file
299
tests/components/schedule/test_trigger.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""Test schedule triggers."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.schedule.const import (
|
||||
ATTR_NEXT_EVENT,
|
||||
CONF_FROM,
|
||||
CONF_SUNDAY,
|
||||
CONF_TO,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_LABEL_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.components import (
|
||||
TriggerStateDescription,
|
||||
arm_trigger,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_schedules(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple schedule entities associated with different targets."""
|
||||
return (await target_entities(hass, DOMAIN))["included"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"schedule.turned_off",
|
||||
"schedule.turned_on",
|
||||
],
|
||||
)
|
||||
async def test_schedule_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the schedule triggers are gated by the labs flag."""
|
||||
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
|
||||
assert (
|
||||
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
|
||||
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
|
||||
"feature to be enabled in Home Assistant Labs settings (feature flag: "
|
||||
"'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="schedule.turned_off",
|
||||
target_states=[(STATE_OFF, {ATTR_NEXT_EVENT: "2022-08-30T13:20:00-07:00"})],
|
||||
other_states=[(STATE_ON, {ATTR_NEXT_EVENT: "2022-08-30T13:30:00-07:00"})],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="schedule.turned_on",
|
||||
target_states=[(STATE_ON, {ATTR_NEXT_EVENT: "2022-08-30T13:20:00-07:00"})],
|
||||
other_states=[(STATE_OFF, {ATTR_NEXT_EVENT: "2022-08-30T13:30:00-07:00"})],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_schedule_state_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_schedules: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test that the schedule state trigger fires when any schedule state changes to a specific state."""
|
||||
other_entity_ids = set(target_schedules) - {entity_id}
|
||||
|
||||
# Set all schedules, including the tested one, to the initial state
|
||||
for eid in target_schedules:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Check if changing other schedules also triggers
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == (entities_in_target - 1) * state["count"]
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="schedule.turned_off",
|
||||
target_states=[(STATE_OFF, {ATTR_NEXT_EVENT: "2022-08-30T13:20:00-07:00"})],
|
||||
other_states=[(STATE_ON, {ATTR_NEXT_EVENT: "2022-08-30T13:30:00-07:00"})],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="schedule.turned_on",
|
||||
target_states=[(STATE_ON, {ATTR_NEXT_EVENT: "2022-08-30T13:20:00-07:00"})],
|
||||
other_states=[(STATE_OFF, {ATTR_NEXT_EVENT: "2022-08-30T13:30:00-07:00"})],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_schedule_state_trigger_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_schedules: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test that the schedule state trigger fires when the first schedule changes to a specific state."""
|
||||
other_entity_ids = set(target_schedules) - {entity_id}
|
||||
|
||||
# Set all schedules, including the tested one, to the initial state
|
||||
for eid in target_schedules:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Triggering other schedules should not cause the trigger to fire again
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="schedule.turned_off",
|
||||
target_states=[(STATE_OFF, {ATTR_NEXT_EVENT: "2022-08-30T13:20:00-07:00"})],
|
||||
other_states=[(STATE_ON, {ATTR_NEXT_EVENT: "2022-08-30T13:30:00-07:00"})],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="schedule.turned_on",
|
||||
target_states=[(STATE_ON, {ATTR_NEXT_EVENT: "2022-08-30T13:20:00-07:00"})],
|
||||
other_states=[(STATE_OFF, {ATTR_NEXT_EVENT: "2022-08-30T13:30:00-07:00"})],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_schedule_state_trigger_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_schedules: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test that the schedule state trigger fires when the last schedule changes to a specific state."""
|
||||
other_entity_ids = set(target_schedules) - {entity_id}
|
||||
|
||||
# Set all schedules, including the tested one, to the initial state
|
||||
for eid in target_schedules:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
async def test_schedule_state_trigger_back_to_back(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
schedule_setup: Callable[..., Coroutine[Any, Any, bool]],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that the schedule state trigger fires when transitioning between two back-to-back schedule blocks."""
|
||||
freezer.move_to("2022-08-30 13:20:00-07:00")
|
||||
entity_id = "schedule.from_yaml"
|
||||
|
||||
assert await schedule_setup(
|
||||
config={
|
||||
DOMAIN: {
|
||||
"from_yaml": {
|
||||
CONF_NAME: "from yaml",
|
||||
CONF_ICON: "mdi:party-popper",
|
||||
CONF_SUNDAY: [
|
||||
{CONF_FROM: "22:00:00", CONF_TO: "22:30:00"},
|
||||
{CONF_FROM: "22:30:00", CONF_TO: "23:00:00"},
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
items=[],
|
||||
)
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
"schedule.turned_on",
|
||||
{},
|
||||
{"entity_id": [entity_id]},
|
||||
)
|
||||
|
||||
# initial state
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:00:00-07:00"
|
||||
|
||||
# move time into first block
|
||||
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:30:00-07:00"
|
||||
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# move time into second block (back-to-back)
|
||||
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T23:00:00-07:00"
|
||||
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
Reference in New Issue
Block a user