"""Test built-in blueprints.""" import asyncio import contextlib from datetime import timedelta import pathlib from unittest.mock import patch import pytest from homeassistant.components import automation from homeassistant.components.blueprint import models from homeassistant.core import HomeAssistant, 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: pytest.fail(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: HomeAssistant) -> None: """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: HomeAssistant) -> None: """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