core/tests/components/automation/test_blueprint.py

215 lines
6.4 KiB
Python

"""Test built-in blueprints."""
import asyncio
import contextlib
from datetime import timedelta
import pathlib
from unittest.mock import patch
from homeassistant.components import automation
from homeassistant.components.blueprint import models
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util, yaml
from tests.common import async_fire_time_changed, async_mock_service
BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(automation.__file__).parent / "blueprints"
@contextlib.contextmanager
def patch_blueprint(blueprint_path: str, data_path):
"""Patch blueprint loading from a different source."""
orig_load = models.DomainBlueprints._load_blueprint
@callback
def mock_load_blueprint(self, path):
if path != blueprint_path:
assert False, f"Unexpected blueprint {path}"
return orig_load(self, path)
return models.Blueprint(
yaml.load_yaml(data_path), expected_domain=self.domain, path=path
)
with patch(
"homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint",
mock_load_blueprint,
):
yield
async def test_notify_leaving_zone(hass):
"""Test notifying leaving a zone blueprint."""
def set_person_state(state, extra={}):
hass.states.async_set(
"person.test_person", state, {"friendly_name": "Paulus", **extra}
)
set_person_state("School")
assert await async_setup_component(
hass, "zone", {"zone": {"name": "School", "latitude": 1, "longitude": 2}}
)
with patch_blueprint(
"notify_leaving_zone.yaml",
BUILTIN_BLUEPRINT_FOLDER / "notify_leaving_zone.yaml",
):
assert await async_setup_component(
hass,
"automation",
{
"automation": {
"use_blueprint": {
"path": "notify_leaving_zone.yaml",
"input": {
"person_entity": "person.test_person",
"zone_entity": "zone.school",
"notify_device": "abcdefgh",
},
}
}
},
)
with patch(
"homeassistant.components.mobile_app.device_action.async_call_action_from_config"
) as mock_call_action:
# Leaving zone to no zone
set_person_state("not_home")
await hass.async_block_till_done()
assert len(mock_call_action.mock_calls) == 1
_hass, config, variables, _context = mock_call_action.mock_calls[0][1]
message_tpl = config.pop("message")
assert config == {
"alias": "Notify that a person has left the zone",
"domain": "mobile_app",
"type": "notify",
"device_id": "abcdefgh",
}
message_tpl.hass = hass
assert message_tpl.async_render(variables) == "Paulus has left School"
# Should not increase when we go to another zone
set_person_state("bla")
await hass.async_block_till_done()
assert len(mock_call_action.mock_calls) == 1
# Should not increase when we go into the zone
set_person_state("School")
await hass.async_block_till_done()
assert len(mock_call_action.mock_calls) == 1
# Should not increase when we move in the zone
set_person_state("School", {"extra_key": "triggers change with same state"})
await hass.async_block_till_done()
assert len(mock_call_action.mock_calls) == 1
# Should increase when leaving zone for another zone
set_person_state("Just Outside School")
await hass.async_block_till_done()
assert len(mock_call_action.mock_calls) == 2
# Verify trigger works
await hass.services.async_call(
"automation",
"trigger",
{"entity_id": "automation.automation_0"},
blocking=True,
)
assert len(mock_call_action.mock_calls) == 3
async def test_motion_light(hass):
"""Test motion light blueprint."""
hass.states.async_set("binary_sensor.kitchen", "off")
with patch_blueprint(
"motion_light.yaml",
BUILTIN_BLUEPRINT_FOLDER / "motion_light.yaml",
):
assert await async_setup_component(
hass,
"automation",
{
"automation": {
"use_blueprint": {
"path": "motion_light.yaml",
"input": {
"light_target": {"entity_id": "light.kitchen"},
"motion_entity": "binary_sensor.kitchen",
},
}
}
},
)
turn_on_calls = async_mock_service(hass, "light", "turn_on")
turn_off_calls = async_mock_service(hass, "light", "turn_off")
# Turn on motion
hass.states.async_set("binary_sensor.kitchen", "on")
# Can't block till done because delay is active
# So wait 10 event loop iterations to process script
for _ in range(10):
await asyncio.sleep(0)
assert len(turn_on_calls) == 1
# Test light doesn't turn off if motion stays
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
for _ in range(10):
await asyncio.sleep(0)
assert len(turn_off_calls) == 0
# Test light turns off off 120s after last motion
hass.states.async_set("binary_sensor.kitchen", "off")
for _ in range(10):
await asyncio.sleep(0)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120))
await hass.async_block_till_done()
assert len(turn_off_calls) == 1
# Test restarting the script
hass.states.async_set("binary_sensor.kitchen", "on")
for _ in range(10):
await asyncio.sleep(0)
assert len(turn_on_calls) == 2
assert len(turn_off_calls) == 1
hass.states.async_set("binary_sensor.kitchen", "off")
for _ in range(10):
await asyncio.sleep(0)
hass.states.async_set("binary_sensor.kitchen", "on")
for _ in range(15):
await asyncio.sleep(0)
assert len(turn_on_calls) == 3
assert len(turn_off_calls) == 1
# Verify trigger works
await hass.services.async_call(
"automation",
"trigger",
{"entity_id": "automation.automation_0"},
)
for _ in range(25):
await asyncio.sleep(0)
assert len(turn_on_calls) == 4