mirror of
https://github.com/home-assistant/core.git
synced 2026-06-30 11:16:05 +01:00
559 lines
16 KiB
Python
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,
|
|
)
|