1
0
mirror of https://github.com/home-assistant/core.git synced 2026-06-30 11:16:05 +01:00
Files
core/tests/components/zone/test_trigger.py
T
2026-05-28 13:22:38 +02:00

559 lines
16 KiB
Python

"""The tests for the location automation."""
from typing import Any
import pytest
import voluptuous as vol
from homeassistant.components import automation, zone
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import Context, HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.trigger import async_validate_trigger_config
from homeassistant.setup import async_setup_component
from tests.common import mock_component
from tests.components.common import (
TriggerStateDescription,
assert_trigger_behavior_all,
assert_trigger_behavior_each,
assert_trigger_behavior_first,
assert_trigger_options_supported,
parametrize_target_entities,
parametrize_trigger_states,
target_entities,
)
@pytest.fixture(autouse=True)
async def setup_comp(hass: HomeAssistant) -> None:
"""Initialize components."""
mock_component(hass, "group")
await async_setup_component(
hass,
zone.DOMAIN,
{
"zone": {
"name": "test",
"latitude": 32.880837,
"longitude": -117.237561,
"radius": 250,
}
},
)
async def test_if_fires_on_zone_enter(
hass: HomeAssistant, service_calls: list[ServiceCall]
) -> None:
"""Test for firing on zone enter."""
context = Context()
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "zone",
"entity_id": "test.entity",
"zone": "zone.test",
"event": "enter",
},
"action": {
"service": "test.automation",
"data_template": {
"some": (
"{{ trigger.platform }}"
" - {{ trigger.entity_id }}"
" - {{ trigger.from_state.state }}"
" - {{ trigger.to_state.state }}"
" - {{ trigger.zone.name }}"
" - {{ trigger.id }}"
)
},
},
}
},
)
hass.states.async_set(
"test.entity",
"hello",
{"latitude": 32.880586, "longitude": -117.237564},
context=context,
)
await hass.async_block_till_done()
assert len(service_calls) == 1
assert service_calls[0].context.parent_id == context.id
assert (
service_calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0"
)
# Set out of zone again so we can trigger call
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
await hass.services.async_call(
automation.DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
blocking=True,
)
assert len(service_calls) == 2
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564}
)
await hass.async_block_till_done()
assert len(service_calls) == 2
async def test_if_fires_on_zone_enter_uuid(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
service_calls: list[ServiceCall],
) -> None:
"""Test for firing on zone enter when device is specified by entity registry id."""
context = Context()
entry = entity_registry.async_get_or_create(
"test", "hue", "1234", suggested_object_id="entity"
)
assert entry.entity_id == "test.entity"
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "zone",
"entity_id": entry.id,
"zone": "zone.test",
"event": "enter",
},
"action": {
"service": "test.automation",
"data_template": {
"some": (
"{{ trigger.platform }}"
" - {{ trigger.entity_id }}"
" - {{ trigger.from_state.state }}"
" - {{ trigger.to_state.state }}"
" - {{ trigger.zone.name }}"
" - {{ trigger.id }}"
)
},
},
}
},
)
hass.states.async_set(
"test.entity",
"hello",
{"latitude": 32.880586, "longitude": -117.237564},
context=context,
)
await hass.async_block_till_done()
assert len(service_calls) == 1
assert service_calls[0].context.parent_id == context.id
assert (
service_calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0"
)
# Set out of zone again so we can trigger call
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
await hass.services.async_call(
automation.DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
blocking=True,
)
assert len(service_calls) == 2
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564}
)
await hass.async_block_till_done()
assert len(service_calls) == 2
async def test_if_not_fires_for_enter_on_zone_leave(
hass: HomeAssistant, service_calls: list[ServiceCall]
) -> None:
"""Test for not firing on zone leave."""
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564}
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "zone",
"entity_id": "test.entity",
"zone": "zone.test",
"event": "enter",
},
"action": {"service": "test.automation"},
}
},
)
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
assert len(service_calls) == 0
async def test_if_fires_on_zone_leave(
hass: HomeAssistant, service_calls: list[ServiceCall]
) -> None:
"""Test for firing on zone leave."""
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564}
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "zone",
"entity_id": "test.entity",
"zone": "zone.test",
"event": "leave",
},
"action": {"service": "test.automation"},
}
},
)
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
assert len(service_calls) == 1
async def test_if_not_fires_for_leave_on_zone_enter(
hass: HomeAssistant, service_calls: list[ServiceCall]
) -> None:
"""Test for not firing on zone enter."""
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "zone",
"entity_id": "test.entity",
"zone": "zone.test",
"event": "leave",
},
"action": {"service": "test.automation"},
}
},
)
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564}
)
await hass.async_block_till_done()
assert len(service_calls) == 0
async def test_zone_condition(
hass: HomeAssistant, service_calls: list[ServiceCall]
) -> None:
"""Test for zone condition."""
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564}
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {"platform": "event", "event_type": "test_event"},
"condition": {
"condition": "zone",
"entity_id": "test.entity",
"zone": "zone.test",
},
"action": {"service": "test.automation"},
}
},
)
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(service_calls) == 1
async def test_unknown_zone(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test for firing on zone enter."""
context = Context()
hass.states.async_set(
"test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758}
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"alias": "My Automation",
"trigger": {
"platform": "zone",
"entity_id": "test.entity",
"zone": "zone.no_such_zone",
"event": "enter",
},
"action": {
"service": "test.automation",
},
}
},
)
assert "Non-existing zone 'zone.no_such_zone' in a zone trigger" not in caplog.text
hass.states.async_set(
"test.entity",
"hello",
{"latitude": 32.880586, "longitude": -117.237564},
context=context,
)
await hass.async_block_till_done()
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_ZONE = ZONE_HOME
@pytest.mark.parametrize(
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
[
("zone.entered", {"zone": TRIGGER_ZONE}, True, True),
("zone.left", {"zone": TRIGGER_ZONE}, 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.mark.parametrize("trigger_key", ["zone.entered", "zone.left"])
async def test_zone_trigger_rejects_non_zone_entity_id(
hass: HomeAssistant, trigger_key: str
) -> None:
"""Test that the zone option must reference entities in the zone domain."""
with pytest.raises(vol.Invalid):
await async_validate_trigger_config(
hass,
[
{
"platform": trigger_key,
"target": {"entity_id": "person.alice"},
"options": {"zone": "person.alice"},
}
],
)
@pytest.fixture
async def target_zone_entities(
hass: HomeAssistant, domain: str
) -> dict[str, list[str]]:
"""Create multiple zone-trackable entities associated with different targets."""
return await target_entities(hass, domain, domain_excluded="sensor")
_ZONE_TRIGGER_STATES = [
*parametrize_trigger_states(
trigger="zone.entered",
trigger_options={"zone": TRIGGER_ZONE},
target_states=[
("home", IN_ZONES_HOME),
],
other_states=[
("not_home", IN_ZONES_NONE),
("Work", IN_ZONES_WORK),
],
),
*parametrize_trigger_states(
trigger="zone.left",
trigger_options={"zone": TRIGGER_ZONE},
target_states=[
("not_home", IN_ZONES_NONE),
("Work", IN_ZONES_WORK),
],
other_states=[
("home", IN_ZONES_HOME),
],
),
]
def _parametrize_zone_target_entities() -> list[tuple[dict[str, Any], str, int, str]]:
"""Parametrize target entities for all supported zone trigger domains."""
return [
(*params, domain)
for domain in ("person", "device_tracker")
for params in parametrize_target_entities(domain)
]
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target", "domain"),
_parametrize_zone_target_entities(),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
_ZONE_TRIGGER_STATES,
)
async def test_zone_trigger_behavior_each(
hass: HomeAssistant,
target_zone_entities: 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_each(
hass,
target_entities=target_zone_entities,
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", "domain"),
_parametrize_zone_target_entities(),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
_ZONE_TRIGGER_STATES,
)
async def test_zone_trigger_behavior_first(
hass: HomeAssistant,
target_zone_entities: 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_zone_entities,
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", "domain"),
_parametrize_zone_target_entities(),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
_ZONE_TRIGGER_STATES,
)
async def test_zone_trigger_behavior_all(
hass: HomeAssistant,
target_zone_entities: 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_all(
hass,
target_entities=target_zone_entities,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)