mirror of
https://github.com/home-assistant/core.git
synced 2026-05-22 00:35:16 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d9dde7601 |
@@ -3,5 +3,13 @@
|
||||
"reload": {
|
||||
"service": "mdi:reload"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"entered": {
|
||||
"trigger": "mdi:map-marker-plus"
|
||||
},
|
||||
"left": {
|
||||
"trigger": "mdi:map-marker-minus"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads zones from the YAML-configuration.",
|
||||
"name": "Reload zones"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"entered": {
|
||||
"description": "Triggers when one or more persons or device trackers enter a zone.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::zone::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::zone::common::trigger_for_name%]"
|
||||
},
|
||||
"zone": {
|
||||
"description": "The zones to trigger on.",
|
||||
"name": "Zones"
|
||||
}
|
||||
},
|
||||
"name": "Entered zone"
|
||||
},
|
||||
"left": {
|
||||
"description": "Triggers when one or more persons or device trackers leave a zone.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::zone::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::zone::common::trigger_for_name%]"
|
||||
},
|
||||
"zone": {
|
||||
"description": "The zones to trigger on.",
|
||||
"name": "Zones"
|
||||
}
|
||||
},
|
||||
"name": "Left zone"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
"""Offer zone automation rules."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import ATTR_IN_ZONES
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_EVENT,
|
||||
CONF_PLATFORM,
|
||||
CONF_OPTIONS,
|
||||
CONF_ZONE,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HassJob,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
@@ -24,8 +26,18 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
location,
|
||||
)
|
||||
from homeassistant.helpers.automation import (
|
||||
DomainSpec,
|
||||
move_top_level_schema_fields_to_options,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
TriggerActionRunner,
|
||||
TriggerConfig,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import condition
|
||||
@@ -38,93 +50,168 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_EVENT_DESCRIPTION = {EVENT_ENTER: "entering", EVENT_LEAVE: "leaving"}
|
||||
|
||||
_TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||
_LEGACY_OPTIONS_SCHEMA: dict[vol.Marker, Any] = {
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids,
|
||||
vol.Required(CONF_ZONE): cv.entity_id,
|
||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(EVENT_ENTER, EVENT_LEAVE),
|
||||
}
|
||||
|
||||
_LEGACY_TRIGGER_OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "zone",
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids,
|
||||
vol.Required(CONF_ZONE): cv.entity_id,
|
||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(
|
||||
EVENT_ENTER, EVENT_LEAVE
|
||||
),
|
||||
vol.Required(CONF_OPTIONS): _LEGACY_OPTIONS_SCHEMA,
|
||||
},
|
||||
)
|
||||
|
||||
# New-style zone trigger schema
|
||||
_ZONE_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_ZONE): vol.All(
|
||||
cv.ensure_list, [cv.entity_id], vol.Length(min=1)
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate trigger config."""
|
||||
config = _TRIGGER_SCHEMA(config)
|
||||
registry = er.async_get(hass)
|
||||
config[CONF_ENTITY_ID] = er.async_validate_entity_ids(
|
||||
registry, config[CONF_ENTITY_ID]
|
||||
)
|
||||
return config
|
||||
_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
"person": DomainSpec(),
|
||||
"device_tracker": DomainSpec(),
|
||||
}
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
*,
|
||||
platform_type: str = "zone",
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
entity_id: list[str] = config[CONF_ENTITY_ID]
|
||||
zone_entity_id: str = config[CONF_ZONE]
|
||||
event: str = config[CONF_EVENT]
|
||||
job = HassJob(action)
|
||||
class LegacyZoneTrigger(Trigger):
|
||||
"""Legacy zone trigger (platform: zone)."""
|
||||
|
||||
@callback
|
||||
def zone_automation_listener(zone_event: Event[EventStateChangedData]) -> None:
|
||||
"""Listen for state changes and calls action."""
|
||||
entity = zone_event.data["entity_id"]
|
||||
from_s = zone_event.data["old_state"]
|
||||
to_s = zone_event.data["new_state"]
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate complete config, migrating legacy format to options."""
|
||||
complete_config = move_top_level_schema_fields_to_options(
|
||||
complete_config, _LEGACY_OPTIONS_SCHEMA
|
||||
)
|
||||
return await super().async_validate_complete_config(hass, complete_config)
|
||||
|
||||
if (from_s and not location.has_location(from_s)) or (
|
||||
to_s and not location.has_location(to_s)
|
||||
):
|
||||
return
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
config = cast(ConfigType, _LEGACY_TRIGGER_OPTIONS_SCHEMA(config))
|
||||
registry = er.async_get(hass)
|
||||
config[CONF_OPTIONS][CONF_ENTITY_ID] = er.async_validate_entity_ids(
|
||||
registry, config[CONF_OPTIONS][CONF_ENTITY_ID]
|
||||
)
|
||||
return config
|
||||
|
||||
if not (zone_state := hass.states.get(zone_entity_id)):
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Automation '%s' is referencing non-existing zone '%s' in a zone"
|
||||
" trigger"
|
||||
),
|
||||
trigger_info["name"],
|
||||
zone_entity_id,
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
self._options = config.options
|
||||
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id: list[str] = self._options[CONF_ENTITY_ID]
|
||||
zone_entity_id: str = self._options[CONF_ZONE]
|
||||
event: str = self._options[CONF_EVENT]
|
||||
|
||||
@callback
|
||||
def zone_automation_listener(zone_event: Event[EventStateChangedData]) -> None:
|
||||
"""Listen for state changes and calls action."""
|
||||
entity = zone_event.data["entity_id"]
|
||||
from_s = zone_event.data["old_state"]
|
||||
to_s = zone_event.data["new_state"]
|
||||
|
||||
if (from_s and not location.has_location(from_s)) or (
|
||||
to_s and not location.has_location(to_s)
|
||||
):
|
||||
return
|
||||
|
||||
if not (zone_state := self._hass.states.get(zone_entity_id)):
|
||||
_LOGGER.warning(
|
||||
"Non-existing zone '%s' in a zone trigger",
|
||||
zone_entity_id,
|
||||
)
|
||||
return
|
||||
|
||||
from_match = (
|
||||
condition.zone(self._hass, zone_state, from_s) if from_s else False
|
||||
)
|
||||
return
|
||||
to_match = condition.zone(self._hass, zone_state, to_s) if to_s else False
|
||||
|
||||
from_match = condition.zone(hass, zone_state, from_s) if from_s else False
|
||||
to_match = condition.zone(hass, zone_state, to_s) if to_s else False
|
||||
|
||||
if (event == EVENT_ENTER and not from_match and to_match) or (
|
||||
event == EVENT_LEAVE and from_match and not to_match
|
||||
):
|
||||
description = (
|
||||
f"{entity} {_EVENT_DESCRIPTION[event]}"
|
||||
f" {zone_state.attributes[ATTR_FRIENDLY_NAME]}"
|
||||
)
|
||||
hass.async_run_hass_job(
|
||||
job,
|
||||
{
|
||||
"trigger": {
|
||||
**trigger_data,
|
||||
"platform": platform_type,
|
||||
if (event == EVENT_ENTER and not from_match and to_match) or (
|
||||
event == EVENT_LEAVE and from_match and not to_match
|
||||
):
|
||||
description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}"
|
||||
run_action(
|
||||
{
|
||||
"entity_id": entity,
|
||||
"from_state": from_s,
|
||||
"to_state": to_s,
|
||||
"zone": zone_state,
|
||||
"event": event,
|
||||
"description": description,
|
||||
}
|
||||
},
|
||||
to_s.context if to_s else None,
|
||||
)
|
||||
},
|
||||
description,
|
||||
to_s.context if to_s else None,
|
||||
)
|
||||
|
||||
return async_track_state_change_event(hass, entity_id, zone_automation_listener)
|
||||
return async_track_state_change_event(
|
||||
self._hass, entity_id, zone_automation_listener
|
||||
)
|
||||
|
||||
|
||||
class ZoneTriggerBase(EntityTriggerBase):
|
||||
"""Base for zone-based triggers targeting person and device_tracker entities."""
|
||||
|
||||
_domain_specs = _DOMAIN_SPECS
|
||||
_schema = _ZONE_TRIGGER_SCHEMA
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._zones: set[str] = set(self._options[CONF_ZONE])
|
||||
|
||||
def _in_target_zones(self, state: State) -> bool:
|
||||
"""Check if the entity is in any of the selected zones."""
|
||||
in_zones = set(state.attributes.get(ATTR_IN_ZONES) or [])
|
||||
return bool(in_zones.intersection(self._zones))
|
||||
|
||||
|
||||
class EnteredZoneTrigger(ZoneTriggerBase):
|
||||
"""Trigger when an entity enters one of the selected zones."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check that the entity was not already in any of the selected zones."""
|
||||
return not self._in_target_zones(from_state)
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check that the entity is now in at least one of the selected zones."""
|
||||
return self._in_target_zones(state)
|
||||
|
||||
|
||||
class LeftZoneTrigger(ZoneTriggerBase):
|
||||
"""Trigger when an entity leaves all of the selected zones."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check that the entity was previously in at least one of the selected zones."""
|
||||
return self._in_target_zones(from_state)
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check that the entity is no longer in any of the selected zones."""
|
||||
return not self._in_target_zones(state)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"_": LegacyZoneTrigger,
|
||||
"entered": EnteredZoneTrigger,
|
||||
"left": LeftZoneTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for zones."""
|
||||
return TRIGGERS
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.trigger_zone: &trigger_zone
|
||||
target:
|
||||
entity:
|
||||
domain:
|
||||
- person
|
||||
- device_tracker
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
zone:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
domain: zone
|
||||
multiple: true
|
||||
|
||||
entered: *trigger_zone
|
||||
left: *trigger_zone
|
||||
@@ -1,5 +1,7 @@
|
||||
"""The tests for the location automation."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation, zone
|
||||
@@ -9,6 +11,16 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import mock_component
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_options_supported,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -343,10 +355,7 @@ async def test_unknown_zone(
|
||||
},
|
||||
)
|
||||
|
||||
assert (
|
||||
"Automation 'My Automation' is referencing non-existing zone"
|
||||
" 'zone.no_such_zone' in a zone trigger" not in caplog.text
|
||||
)
|
||||
assert "Non-existing zone 'zone.no_such_zone' in a zone trigger" not in caplog.text
|
||||
|
||||
hass.states.async_set(
|
||||
"test.entity",
|
||||
@@ -356,7 +365,206 @@ async def test_unknown_zone(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
"Automation 'My Automation' is referencing non-existing zone"
|
||||
" 'zone.no_such_zone' in a zone trigger" in caplog.text
|
||||
assert "Non-existing zone 'zone.no_such_zone' in a zone trigger" in caplog.text
|
||||
|
||||
|
||||
# --- New-style zone trigger tests ---
|
||||
|
||||
ZONE_HOME = "zone.home"
|
||||
ZONE_WORK = "zone.work"
|
||||
IN_ZONES_HOME = {"in_zones": [ZONE_HOME]}
|
||||
IN_ZONES_WORK = {"in_zones": [ZONE_WORK]}
|
||||
IN_ZONES_NONE: dict[str, list[str]] = {"in_zones": []}
|
||||
TRIGGER_ZONES = [ZONE_HOME, ZONE_WORK]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("zone.entered", {"zone": TRIGGER_ZONES}, True, True),
|
||||
("zone.left", {"zone": TRIGGER_ZONES}, True, True),
|
||||
],
|
||||
)
|
||||
async def test_zone_trigger_options_validation(
|
||||
hass: HomeAssistant,
|
||||
trigger_key: str,
|
||||
base_options: dict[str, Any] | None,
|
||||
supports_behavior: bool,
|
||||
supports_duration: bool,
|
||||
) -> None:
|
||||
"""Test that zone triggers support the expected options."""
|
||||
await assert_trigger_options_supported(
|
||||
hass,
|
||||
trigger_key,
|
||||
base_options,
|
||||
supports_behavior=supports_behavior,
|
||||
supports_duration=supports_duration,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_persons(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple person entities associated with different targets."""
|
||||
return await target_entities(hass, "person", domain_excluded="sensor")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("person"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="zone.entered",
|
||||
trigger_options={"zone": TRIGGER_ZONES},
|
||||
target_states=[
|
||||
("home", IN_ZONES_HOME),
|
||||
("Work", IN_ZONES_WORK),
|
||||
],
|
||||
other_states=[
|
||||
("not_home", IN_ZONES_NONE),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="zone.left",
|
||||
trigger_options={"zone": TRIGGER_ZONES},
|
||||
target_states=[
|
||||
("not_home", IN_ZONES_NONE),
|
||||
],
|
||||
other_states=[
|
||||
("home", IN_ZONES_HOME),
|
||||
("Work", IN_ZONES_WORK),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_zone_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_persons: dict[str, list[str]],
|
||||
trigger_target_config: dict[str, Any],
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test zone triggers fire when any targeted entity changes."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
target_entities=target_persons,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("person"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="zone.entered",
|
||||
trigger_options={"zone": TRIGGER_ZONES},
|
||||
target_states=[
|
||||
("home", IN_ZONES_HOME),
|
||||
("Work", IN_ZONES_WORK),
|
||||
],
|
||||
other_states=[
|
||||
("not_home", IN_ZONES_NONE),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="zone.left",
|
||||
trigger_options={"zone": TRIGGER_ZONES},
|
||||
target_states=[
|
||||
("not_home", IN_ZONES_NONE),
|
||||
],
|
||||
other_states=[
|
||||
("home", IN_ZONES_HOME),
|
||||
("Work", IN_ZONES_WORK),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_zone_trigger_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
target_persons: dict[str, list[str]],
|
||||
trigger_target_config: dict[str, Any],
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test zone triggers fire when first targeted entity changes."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
target_entities=target_persons,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("person"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="zone.entered",
|
||||
trigger_options={"zone": TRIGGER_ZONES},
|
||||
target_states=[
|
||||
("home", IN_ZONES_HOME),
|
||||
("Work", IN_ZONES_WORK),
|
||||
],
|
||||
other_states=[
|
||||
("not_home", IN_ZONES_NONE),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="zone.left",
|
||||
trigger_options={"zone": TRIGGER_ZONES},
|
||||
target_states=[
|
||||
("not_home", IN_ZONES_NONE),
|
||||
],
|
||||
other_states=[
|
||||
("home", IN_ZONES_HOME),
|
||||
("Work", IN_ZONES_WORK),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_zone_trigger_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
target_persons: dict[str, list[str]],
|
||||
trigger_target_config: dict[str, Any],
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test zone triggers fire when last targeted entity changes."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
target_entities=target_persons,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user