"""The tests for the Template lock platform.""" from typing import Any import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import setup from homeassistant.components import lock, template from homeassistant.components.lock import ( LockEntityFeature, LockEntityStateAttribute, LockState, ) from homeassistant.components.template.lock import DEFAULT_NAME from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er from homeassistant.helpers.typing import ConfigType from .conftest import ( ConfigurationStyle, TemplatePlatformSetup, assert_extra_template_attributes, assert_state_and_attributes, async_get_flow_preview_state, async_trigger, make_test_action, make_test_trigger, setup_and_test_nested_unique_id, setup_and_test_unique_id, setup_entity, setup_mock_template_entity_restore_state, setup_restore_template_entity, ) from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator TEST_STATE_ENTITY_ID = "sensor.test_state" TEST_AVAILABILITY_ENTITY_ID = "availability_state.state" TEST_LOCK = TemplatePlatformSetup( lock.DOMAIN, "test_template_lock", make_test_trigger( TEST_AVAILABILITY_ENTITY_ID, TEST_STATE_ENTITY_ID, ), ) CODE_DATA = { "code": "{{ code if code is defined else None }}", } LOCK_ACTION = make_test_action("lock", CODE_DATA) UNLOCK_ACTION = make_test_action("unlock", CODE_DATA) OPEN_ACTION = make_test_action("open") OPTIMISTIC_LOCK = { **LOCK_ACTION, **UNLOCK_ACTION, } @pytest.fixture async def setup_lock( hass: HomeAssistant, count: int, style: ConfigurationStyle, config: dict[str, Any], ) -> None: """Do setup of lock integration.""" await setup_entity(hass, TEST_LOCK, style, count, config) @pytest.fixture async def setup_base_lock( hass: HomeAssistant, count: int, style: ConfigurationStyle, state_template: str, extra_config: dict, ): """Do setup of lock integration using a state template.""" await setup_entity(hass, TEST_LOCK, style, count, extra_config, state_template) @pytest.fixture async def setup_state_lock( hass: HomeAssistant, count: int, style: ConfigurationStyle, state_template: str, ): """Do setup of lock integration using a state template.""" await setup_entity(hass, TEST_LOCK, style, count, OPTIMISTIC_LOCK, state_template) @pytest.fixture async def setup_state_lock_with_extra_config( hass: HomeAssistant, count: int, style: ConfigurationStyle, state_template: str, extra_config: dict, ): """Do setup of lock integration using a state template.""" await setup_entity( hass, TEST_LOCK, style, count, OPTIMISTIC_LOCK, state_template, extra_config ) @pytest.fixture async def setup_state_lock_with_attribute( hass: HomeAssistant, count: int, style: ConfigurationStyle, state_template: str, attribute: str, attribute_template: str, ): """Do setup of lock integration using a state template.""" await setup_entity( hass, TEST_LOCK, style, count, OPTIMISTIC_LOCK, state_template, {attribute: attribute_template} if attribute else {}, ) @pytest.mark.parametrize( ("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")] ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock") async def test_template_state(hass: HomeAssistant) -> None: """Test template.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OPEN) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.OPEN hass.states.async_set(TEST_STATE_ENTITY_ID, "None") await hass.async_block_till_done() state = hass.states.get(TEST_LOCK.entity_id) assert state.state == STATE_UNKNOWN @pytest.mark.parametrize( ("count", "state_template", "extra_config"), [(1, "{{ states.sensor.test_state.state }}", {"optimistic": True, **OPEN_ACTION})], ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock_with_extra_config") async def test_open_lock_optimistic( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test optimistic open.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_OPEN, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "open" assert calls[0].data["caller"] == TEST_LOCK.entity_id state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.OPEN @pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock") async def test_template_state_boolean_on(hass: HomeAssistant) -> None: """Test the setting of the state with boolean on.""" # Ensure the trigger executes for trigger configurations await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED @pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 2 }}")]) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock") async def test_template_state_boolean_off(hass: HomeAssistant) -> None: """Test the setting of the state with off.""" # Ensure the trigger executes for trigger configurations await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED @pytest.mark.parametrize("count", [0]) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.parametrize( ("state_template", "extra_config"), [ ("{% if rubbish %}", OPTIMISTIC_LOCK), ("{{ rubbish }", OPTIMISTIC_LOCK), ("Invalid", {}), ( "{{ 1==1 }}", { "not_value_template": "{{ states.sensor.test_state.state }}", **OPTIMISTIC_LOCK, }, ), ], ) @pytest.mark.usefixtures("setup_base_lock") async def test_template_syntax_error(hass: HomeAssistant) -> None: """Test templating syntax errors don't create entities.""" assert hass.states.async_all("lock") == [] @pytest.mark.parametrize(("count", "state_template"), [(0, "{{ 1==1 }}")]) @pytest.mark.parametrize("attribute_template", ["{{ rubbish }", "{% if rubbish %}"]) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "code_format"), (ConfigurationStyle.TRIGGER, "code_format"), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_template_code_template_syntax_error(hass: HomeAssistant) -> None: """Test templating code_format syntax errors don't create entities.""" assert hass.states.async_all("lock") == [] @pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 + 1 }}")]) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock") async def test_template_static(hass: HomeAssistant) -> None: """Test that we allow static templates.""" hass.states.async_set(TEST_LOCK.entity_id, LockState.LOCKED) await hass.async_block_till_done() state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.parametrize( ("state_template", "expected"), [ ("{{ True }}", LockState.LOCKED), ("{{ False }}", LockState.UNLOCKED), ("{{ x - 12 }}", STATE_UNAVAILABLE), ("{{ None }}", STATE_UNKNOWN), ], ) @pytest.mark.usefixtures("setup_state_lock") async def test_state_template(hass: HomeAssistant, expected: str) -> None: """Test state and value_template template.""" # Ensure the trigger executes for trigger configurations await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == expected @pytest.mark.parametrize( ("count", "state_template", "attribute"), [(1, "{{ 1==1 }}", "picture")] ) @pytest.mark.parametrize( "attribute_template", ["{% if states.sensor.test_state.state %}/local/switch.png{% endif %}"], ) @pytest.mark.parametrize( ("style", "initial_state"), [ (ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_picture_template(hass: HomeAssistant, initial_state: str) -> None: """Test entity_picture template.""" state = hass.states.get(TEST_LOCK.entity_id) assert state.attributes.get("entity_picture") == initial_state await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.attributes["entity_picture"] == "/local/switch.png" @pytest.mark.parametrize( ("count", "state_template", "attribute"), [(1, "{{ 1==1 }}", "icon")] ) @pytest.mark.parametrize( "attribute_template", ["{% if states.sensor.test_state.state %}mdi:eye{% endif %}"], ) @pytest.mark.parametrize( ("style", "initial_state"), [ (ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_icon_template(hass: HomeAssistant, initial_state: str) -> None: """Test icon template.""" state = hass.states.get(TEST_LOCK.entity_id) assert state.attributes.get("icon") == initial_state await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.attributes["icon"] == "mdi:eye" @pytest.mark.parametrize( ("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")] ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock") async def test_lock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test lock action.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "lock" assert calls[0].data["caller"] == TEST_LOCK.entity_id @pytest.mark.parametrize( ("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")] ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock") async def test_unlock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test unlock action.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "unlock" assert calls[0].data["caller"] == TEST_LOCK.entity_id @pytest.mark.parametrize( ("count", "state_template", "extra_config"), [(1, "{{ states.sensor.test_state.state }}", OPEN_ACTION)], ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_state_lock_with_extra_config") async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test open action.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_OPEN, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "open" assert calls[0].data["caller"] == TEST_LOCK.entity_id @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ states.sensor.test_state.state }}", "{{ '.+' }}", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "code_format"), (ConfigurationStyle.TRIGGER, "code_format"), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_lock_action_with_code( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test lock action with defined code format and supplied lock code.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id, ATTR_CODE: "LOCK_CODE"}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "lock" assert calls[0].data["caller"] == TEST_LOCK.entity_id assert calls[0].data["code"] == "LOCK_CODE" @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ states.sensor.test_state.state }}", "{{ '.+' }}", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "code_format"), (ConfigurationStyle.TRIGGER, "code_format"), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_unlock_action_with_code( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test unlock action with code format and supplied unlock code.""" await setup.async_setup_component(hass, "switch", {}) await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id, ATTR_CODE: "UNLOCK_CODE"}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "unlock" assert calls[0].data["caller"] == TEST_LOCK.entity_id assert calls[0].data["code"] == "UNLOCK_CODE" @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ 1 == 1 }}", "{{ '\\\\d+' }}", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "code_format"), (ConfigurationStyle.TRIGGER, "code_format"), ], ) @pytest.mark.parametrize( "test_action", [ lock.SERVICE_LOCK, lock.SERVICE_UNLOCK, ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_lock_actions_fail_with_invalid_code( hass: HomeAssistant, calls: list[ServiceCall], test_action ) -> None: """Test invalid lock codes.""" # Ensure trigger entities updated await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) await hass.services.async_call( lock.DOMAIN, test_action, {ATTR_ENTITY_ID: TEST_LOCK.entity_id, ATTR_CODE: "non-number-value"}, ) await hass.services.async_call( lock.DOMAIN, test_action, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, ) await hass.async_block_till_done() assert len(calls) == 0 @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ 1 == 1 }}", "{{ 1/0 }}", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "code_format"), (ConfigurationStyle.TRIGGER, "code_format"), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_lock_actions_dont_execute_with_code_template_rendering_error( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test lock code format rendering fails block lock/unlock actions.""" # Ensure trigger entities updated await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, ) await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id, ATTR_CODE: "any-value"}, ) await hass.async_block_till_done() assert len(calls) == 0 @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ states.sensor.test_state.state }}", "{{ None }}", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "code_format"), (ConfigurationStyle.TRIGGER, "code_format"), ], ) @pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_actions_with_none_as_codeformat_ignores_code( hass: HomeAssistant, action, calls: list[ServiceCall] ) -> None: """Test lock actions with supplied lock code.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, action, {ATTR_ENTITY_ID: TEST_LOCK.entity_id, ATTR_CODE: "any code"}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == action assert calls[0].data["caller"] == TEST_LOCK.entity_id assert calls[0].data["code"] == "any code" @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ states.sensor.test_state.state }}", "[12]{1", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "code_format"), (ConfigurationStyle.TRIGGER, "code_format"), ], ) @pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_actions_with_invalid_regexp_as_codeformat_never_execute( hass: HomeAssistant, action, calls: list[ServiceCall] ) -> None: """Test lock actions don't execute with invalid regexp.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, action, {ATTR_ENTITY_ID: TEST_LOCK.entity_id, ATTR_CODE: "1"}, ) await hass.services.async_call( lock.DOMAIN, action, {ATTR_ENTITY_ID: TEST_LOCK.entity_id, ATTR_CODE: "x"}, ) await hass.services.async_call( lock.DOMAIN, action, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, ) await hass.async_block_till_done() assert len(calls) == 0 @pytest.mark.parametrize( ("count", "state_template"), [(1, "{{ states.sensor.test_state.state }}")] ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.parametrize( "test_state", [ LockState.LOCKED, LockState.UNLOCKED, LockState.OPEN, LockState.UNLOCKING, LockState.LOCKING, LockState.JAMMED, LockState.OPENING, ], ) @pytest.mark.usefixtures("setup_state_lock") async def test_lock_state(hass: HomeAssistant, test_state) -> None: """Test value template.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, test_state) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == test_state @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ states('sensor.test_state') }}", "{{ is_state('availability_state.state', 'on') }}", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "availability"), (ConfigurationStyle.TRIGGER, "availability"), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_available_template_with_entities(hass: HomeAssistant) -> None: """Test availability templates with values from other entities.""" # When template returns true.. hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, STATE_ON) await hass.async_block_till_done() # Device State should not be unavailable assert hass.states.get(TEST_LOCK.entity_id).state != STATE_UNAVAILABLE # When Availability template returns false hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() # device state should be unavailable assert hass.states.get(TEST_LOCK.entity_id).state == STATE_UNAVAILABLE @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ ( 1, "{{ 1 + 1 }}", "{{ x - 12 }}", ) ], ) @pytest.mark.parametrize( ("style", "attribute"), [ (ConfigurationStyle.MODERN, "availability"), (ConfigurationStyle.TRIGGER, "availability"), ], ) @pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_invalid_availability_template_keeps_component_available( hass: HomeAssistant, caplog_setup_text, caplog: pytest.LogCaptureFixture ) -> None: """Test that an invalid availability keeps the device available.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON) assert hass.states.get(TEST_LOCK.entity_id).state != STATE_UNAVAILABLE err = "'x' is undefined" assert err in caplog_setup_text or err in caplog.text @pytest.mark.parametrize("config", [OPTIMISTIC_LOCK]) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) async def test_unique_id( hass: HomeAssistant, style: ConfigurationStyle, config: ConfigType ) -> None: """Test unique_id option only creates one entity per id.""" await setup_and_test_unique_id(hass, TEST_LOCK, style, config, "{{ 'on' }}") @pytest.mark.parametrize("config", [OPTIMISTIC_LOCK]) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] ) async def test_nested_unique_id( hass: HomeAssistant, style: ConfigurationStyle, config: ConfigType, entity_registry: er.EntityRegistry, ) -> None: """Test a template unique_id propagates unique_ids.""" await setup_and_test_nested_unique_id( hass, TEST_LOCK, style, entity_registry, config, "{{ 'on' }}" ) @pytest.mark.parametrize( ("count", "config"), [ ( 1, { "state": "{{ 0 == 1 }}", "lock": [], "unlock": [], "open": [], "optimistic": True, }, ) ], ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] ) @pytest.mark.usefixtures("setup_lock") async def test_empty_action_config(hass: HomeAssistant) -> None: """Test configuration with empty script.""" state = hass.states.get(TEST_LOCK.entity_id) assert state.attributes["supported_features"] == LockEntityFeature.OPEN await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test_template_lock"}, ) await hass.async_block_till_done() state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test_template_lock"}, ) await hass.async_block_till_done() state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED @pytest.mark.parametrize( ("count", "config"), [ ( 1, { "lock": [], "unlock": [], }, ) ], ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_lock") async def test_optimistic(hass: HomeAssistant) -> None: """Test configuration with optimistic state.""" state = hass.states.get(TEST_LOCK.entity_id) assert state.state == STATE_UNKNOWN # Ensure Trigger template entities update. await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything") await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, blocking=True, ) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, blocking=True, ) state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED @pytest.mark.parametrize( ("count", "config"), [ ( 1, { "state": "{{ is_state('sensor.test_state', 'on') }}", "lock": [], "unlock": [], "optimistic": False, }, ) ], ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.usefixtures("setup_lock") async def test_not_optimistic(hass: HomeAssistant) -> None: """Test optimistic yaml option set to false.""" await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, {ATTR_ENTITY_ID: TEST_LOCK.entity_id}, blocking=True, ) # Ensure Trigger template entities update. hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "anything") await hass.async_block_till_done() state = hass.states.get(TEST_LOCK.entity_id) assert state.state == LockState.UNLOCKED async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, ) -> None: """Tests creating a lock from a config entry.""" hass.states.async_set( "sensor.test_state", LockState.LOCKED, {}, ) template_config_entry = MockConfigEntry( data={}, domain=template.DOMAIN, options={ "name": "My template", "state": "{{ states('sensor.test_state') }}", "lock": [], "unlock": [], "template_type": lock.DOMAIN, }, title="My template", ) template_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(template_config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("lock.my_template") assert state is not None assert state == snapshot async def test_flow_preview( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, ) -> None: """Test the config flow preview.""" state = await async_get_flow_preview_state( hass, hass_ws_client, lock.DOMAIN, { "name": "My template", "state": "{{ 'locked' }}", "lock": [], "unlock": [], }, ) assert state["state"] == LockState.LOCKED @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] ) @pytest.mark.parametrize( ( "saved_state", "saved_extra_data", "initial_state", "initial_attributes", ), [ ( LockState.JAMMED, { "code_format": ".+", "is_locked": False, "is_locking": False, "is_open": False, "is_opening": False, "is_unlocking": False, "is_jammed": True, }, LockState.JAMMED, { "code_format": ".+", }, ), ( LockState.LOCKED, { "code_format": ".+", "is_locked": True, "is_locking": False, "is_open": False, "is_opening": False, "is_unlocking": False, "is_jammed": False, }, LockState.LOCKED, { "code_format": ".+", }, ), ( LockState.LOCKING, { "code_format": ".+", "is_locked": False, "is_locking": True, "is_open": False, "is_opening": False, "is_unlocking": False, "is_jammed": False, }, LockState.LOCKING, { "code_format": ".+", }, ), ( LockState.JAMMED, { "code_format": ".+", "is_locked": False, "is_locking": False, "is_opening": False, "is_unlocking": False, "is_jammed": True, }, STATE_UNKNOWN, { "code_format": None, }, ), ( STATE_UNAVAILABLE, { "code_format": ".+", "is_locked": False, "is_locking": False, "is_open": False, "is_opening": False, "is_unlocking": False, "is_jammed": True, }, STATE_UNKNOWN, { "code_format": None, }, ), ( STATE_UNKNOWN, { "code_format": ".+", "is_locked": False, "is_locking": False, "is_open": False, "is_opening": False, "is_unlocking": False, "is_jammed": True, }, STATE_UNKNOWN, { "code_format": None, }, ), ], ) async def test_restore_state( hass: HomeAssistant, style: ConfigurationStyle, saved_state: LockState | str, saved_extra_data: dict | None, initial_state: LockState | str, initial_attributes: ConfigType, ) -> None: """Test restoring state.""" setup_mock_template_entity_restore_state( hass, TEST_LOCK, saved_state, saved_extra_data=saved_extra_data, ) await setup_restore_template_entity( hass, TEST_LOCK, style, { "code_format": "{{ state_attr('sensor.test_state', 'code_format') }}", "state": "{{ state_attr('sensor.test_state', 'lock_state') }}", "lock": [], "open": [], "unlock": [], }, "is_state_attr('sensor.test_state', 'lock_state', 'unlocked')", ) assert_state_and_attributes(hass, TEST_LOCK, initial_state, initial_attributes) await async_trigger( hass, "sensor.test_state", "anything", {"lock_state": LockState.UNLOCKED, "code_format": "\\\\d+"}, ) # The first trigger should replace the restored code_format attribute assert_state_and_attributes( hass, TEST_LOCK, LockState.UNLOCKED, {"code_format": "\\\\d+"} ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] ) async def test_extra_template_attributes( hass: HomeAssistant, style: ConfigurationStyle ) -> None: """Test extra attributes.""" await assert_extra_template_attributes( hass, TEST_LOCK, style, {"state": "{{ 'locked' }}", **LOCK_ACTION, **UNLOCK_ACTION}, ) @pytest.mark.parametrize( "attribute", list(LockEntityStateAttribute), ) @pytest.mark.parametrize( "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] ) async def test_blocked_template_attributes( hass: HomeAssistant, style: ConfigurationStyle, attribute, caplog: pytest.LogCaptureFixture, ) -> None: """Test blocked extra attributes.""" await setup_entity( hass, TEST_LOCK, style, 0, { "state": "{{ 'locked' }}", **LOCK_ACTION, **UNLOCK_ACTION, "attributes": {str(attribute): "{{ 'does not matter' }}"}, }, ) assert ( f"Unsupported attribute(s) found for {DEFAULT_NAME}: {attribute}" in caplog.text )