mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Add device trigger support for device trackers (#42324)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
5f09addb74
commit
bc4bbaf6ef
105
homeassistant/components/device_tracker/device_trigger.py
Normal file
105
homeassistant/components/device_tracker/device_trigger.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""Provides device automations for Device Tracker."""
|
||||
from typing import List
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.automation import AutomationActionType
|
||||
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.components.zone import DOMAIN as DOMAIN_ZONE, trigger as zone
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_EVENT,
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
CONF_ZONE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
TRIGGER_TYPES = {"enters", "leaves"}
|
||||
|
||||
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
|
||||
vol.Required(CONF_ZONE): cv.entity_domain(DOMAIN_ZONE),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
|
||||
"""List device triggers for Device Tracker devices."""
|
||||
registry = await entity_registry.async_get_registry(hass)
|
||||
triggers = []
|
||||
|
||||
# Get all the integrations entities for this device
|
||||
for entry in entity_registry.async_entries_for_device(registry, device_id):
|
||||
if entry.domain != DOMAIN:
|
||||
continue
|
||||
|
||||
triggers.append(
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "enters",
|
||||
}
|
||||
)
|
||||
triggers.append(
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "leaves",
|
||||
}
|
||||
)
|
||||
|
||||
return triggers
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: AutomationActionType,
|
||||
automation_info: dict,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
|
||||
if config[CONF_TYPE] == "enters":
|
||||
event = zone.EVENT_ENTER
|
||||
else:
|
||||
event = zone.EVENT_LEAVE
|
||||
|
||||
zone_config = {
|
||||
CONF_PLATFORM: DOMAIN_ZONE,
|
||||
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||
CONF_ZONE: config[CONF_ZONE],
|
||||
CONF_EVENT: event,
|
||||
}
|
||||
zone_config = zone.TRIGGER_SCHEMA(zone_config)
|
||||
return await zone.async_attach_trigger(
|
||||
hass, zone_config, action, automation_info, platform_type="device"
|
||||
)
|
||||
|
||||
|
||||
async def async_get_trigger_capabilities(hass: HomeAssistant, config: ConfigType):
|
||||
"""List trigger capabilities."""
|
||||
zones = {
|
||||
ent.entity_id: ent.name
|
||||
for ent in sorted(hass.states.async_all(DOMAIN_ZONE), key=lambda ent: ent.name)
|
||||
}
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ZONE): vol.In(zones),
|
||||
}
|
||||
)
|
||||
}
|
@ -4,6 +4,10 @@
|
||||
"condition_type": {
|
||||
"is_home": "{entity_name} is home",
|
||||
"is_not_home": "{entity_name} is not home"
|
||||
},
|
||||
"trigger_type": {
|
||||
"enters": "{entity_name} enters a zone",
|
||||
"leaves": "{entity_name} leaves a zone"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
@ -12,4 +16,4 @@
|
||||
"not_home": "[%key:common::state::not_home%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,10 @@
|
||||
"condition_type": {
|
||||
"is_home": "{entity_name} is home",
|
||||
"is_not_home": "{entity_name} is not home"
|
||||
},
|
||||
"trigger_type": {
|
||||
"enters": "{entity_name} enters a zone",
|
||||
"leaves": "{entity_name} leaves a zone"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
|
@ -8,11 +8,12 @@ from homeassistant.const import (
|
||||
CONF_PLATFORM,
|
||||
CONF_ZONE,
|
||||
)
|
||||
from homeassistant.core import HassJob, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJob, callback
|
||||
from homeassistant.helpers import condition, config_validation as cv, location
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||
# mypy: allow-incomplete-defs, allow-untyped-defs
|
||||
# mypy: no-check-untyped-defs
|
||||
|
||||
EVENT_ENTER = "enter"
|
||||
EVENT_LEAVE = "leave"
|
||||
@ -32,7 +33,9 @@ TRIGGER_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def async_attach_trigger(hass, config, action, automation_info):
|
||||
async def async_attach_trigger(
|
||||
hass, config, action, automation_info, *, platform_type: str = "zone"
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
@ -70,7 +73,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
|
||||
job,
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "zone",
|
||||
"platform": platform_type,
|
||||
"entity_id": entity,
|
||||
"from_state": from_s,
|
||||
"to_state": to_s,
|
||||
|
205
tests/components/device_tracker/test_device_trigger.py
Normal file
205
tests/components/device_tracker/test_device_trigger.py
Normal file
@ -0,0 +1,205 @@
|
||||
"""The tests for Device Tracker device triggers."""
|
||||
import pytest
|
||||
import voluptuous_serialize
|
||||
|
||||
import homeassistant.components.automation as automation
|
||||
from homeassistant.components.device_tracker import DOMAIN, device_trigger
|
||||
import homeassistant.components.zone as zone
|
||||
from homeassistant.helpers import config_validation as cv, device_registry
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
assert_lists_same,
|
||||
async_get_device_automations,
|
||||
async_mock_service,
|
||||
mock_device_registry,
|
||||
mock_registry,
|
||||
)
|
||||
|
||||
AWAY_LATITUDE = 32.881011
|
||||
AWAY_LONGITUDE = -117.234758
|
||||
|
||||
HOME_LATITUDE = 32.880837
|
||||
HOME_LONGITUDE = -117.237561
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_reg(hass):
|
||||
"""Return an empty, loaded, registry."""
|
||||
return mock_device_registry(hass)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entity_reg(hass):
|
||||
"""Return an empty, loaded, registry."""
|
||||
return mock_registry(hass)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calls(hass):
|
||||
"""Track calls to a mock service."""
|
||||
return async_mock_service(hass, "test", "automation")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_zone(hass):
|
||||
"""Create test zone."""
|
||||
hass.loop.run_until_complete(
|
||||
async_setup_component(
|
||||
hass,
|
||||
zone.DOMAIN,
|
||||
{
|
||||
"zone": {
|
||||
"name": "test",
|
||||
"latitude": HOME_LATITUDE,
|
||||
"longitude": HOME_LONGITUDE,
|
||||
"radius": 250,
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def test_get_triggers(hass, device_reg, entity_reg):
|
||||
"""Test we get the expected triggers from a device_tracker."""
|
||||
config_entry = MockConfigEntry(domain="test", data={})
|
||||
config_entry.add_to_hass(hass)
|
||||
device_entry = device_reg.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
)
|
||||
entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
|
||||
expected_triggers = [
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"type": "leaves",
|
||||
"device_id": device_entry.id,
|
||||
"entity_id": f"{DOMAIN}.test_5678",
|
||||
},
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"type": "enters",
|
||||
"device_id": device_entry.id,
|
||||
"entity_id": f"{DOMAIN}.test_5678",
|
||||
},
|
||||
]
|
||||
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
|
||||
assert_lists_same(triggers, expected_triggers)
|
||||
|
||||
|
||||
async def test_if_fires_on_zone_change(hass, calls):
|
||||
"""Test for enter and leave triggers firing."""
|
||||
hass.states.async_set(
|
||||
"device_tracker.entity",
|
||||
"state",
|
||||
{"latitude": AWAY_LATITUDE, "longitude": AWAY_LONGITUDE},
|
||||
)
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": "",
|
||||
"entity_id": "device_tracker.entity",
|
||||
"type": "enters",
|
||||
"zone": "zone.test",
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": (
|
||||
"enter - {{ trigger.platform}} - "
|
||||
"{{ trigger.entity_id}} - {{ trigger.from_state.attributes.longitude|round(3)}} - "
|
||||
"{{ trigger.to_state.attributes.longitude|round(3)}}"
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": "",
|
||||
"entity_id": "device_tracker.entity",
|
||||
"type": "leaves",
|
||||
"zone": "zone.test",
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": (
|
||||
"leave - {{ trigger.platform}} - "
|
||||
"{{ trigger.entity_id}} - {{ trigger.from_state.attributes.longitude|round(3)}} - "
|
||||
"{{ trigger.to_state.attributes.longitude|round(3)}}"
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Fake that the entity is entering.
|
||||
hass.states.async_set(
|
||||
"device_tracker.entity",
|
||||
"state",
|
||||
{"latitude": HOME_LATITUDE, "longitude": HOME_LONGITUDE},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data["some"] == "enter - device - {} - -117.235 - -117.238".format(
|
||||
"device_tracker.entity"
|
||||
)
|
||||
|
||||
# Fake that the entity is leaving.
|
||||
hass.states.async_set(
|
||||
"device_tracker.entity",
|
||||
"state",
|
||||
{"latitude": AWAY_LATITUDE, "longitude": AWAY_LONGITUDE},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
assert calls[1].data["some"] == "leave - device - {} - -117.238 - -117.235".format(
|
||||
"device_tracker.entity"
|
||||
)
|
||||
|
||||
|
||||
async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
|
||||
"""Test we get the expected capabilities from a device_tracker trigger."""
|
||||
config_entry = MockConfigEntry(domain="test", data={})
|
||||
config_entry.add_to_hass(hass)
|
||||
device_entry = device_reg.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
)
|
||||
entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
|
||||
capabilities = await device_trigger.async_get_trigger_capabilities(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"type": "enters",
|
||||
"device_id": device_entry.id,
|
||||
"entity_id": f"{DOMAIN}.test_5678",
|
||||
},
|
||||
)
|
||||
assert capabilities and "extra_fields" in capabilities
|
||||
|
||||
assert voluptuous_serialize.convert(
|
||||
capabilities["extra_fields"], custom_serializer=cv.custom_serializer
|
||||
) == [
|
||||
{
|
||||
"name": "zone",
|
||||
"required": True,
|
||||
"type": "select",
|
||||
"options": [("zone.test", "test"), ("zone.home", "test home")],
|
||||
}
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user