"""The tests for the Template button platform.""" from typing import Any from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.template import DOMAIN from homeassistant.components.template.const import CONF_PICTURE from homeassistant.const import ( ATTR_ENTITY_PICTURE, ATTR_ICON, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_ICON, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util from .conftest import ( ConfigurationStyle, TemplatePlatformSetup, assert_action, async_trigger, make_test_action, setup_and_test_nested_unique_id, setup_and_test_unique_id, setup_entity, ) from tests.common import MockConfigEntry TEST_ATTRIBUTE_ENTITY_ID = "sensor.test_attribute" TEST_AVAILABILITY_ENTITY = "binary_sensor.availability" TEST_BUTTON = TemplatePlatformSetup(BUTTON_DOMAIN, "template_button", {}) PRESS_ACTION = make_test_action("press") @pytest.fixture async def setup_button(hass: HomeAssistant, count: int, config: dict[str, Any]) -> None: """Do setup of button integration.""" await setup_entity(hass, TEST_BUTTON, ConfigurationStyle.MODERN, count, config) @pytest.fixture async def setup_single_attribute_button( hass: HomeAssistant, attribute: str, attribute_template: str, config: dict, ) -> None: """Do setup of button integration with a single attribute.""" await setup_entity( hass, TEST_BUTTON, ConfigurationStyle.MODERN, 1, config, extra_config={attribute: attribute_template} if attribute and attribute_template else {}, ) def _verify( hass: HomeAssistant, expected_value: str, attributes: dict[str, Any] | None = None, entity_id: str = TEST_BUTTON.entity_id, ) -> None: """Verify button's state.""" attributes = attributes or {} if CONF_FRIENDLY_NAME not in attributes: attributes[CONF_FRIENDLY_NAME] = TEST_BUTTON.object_id state = hass.states.get(entity_id) assert state.state == expected_value assert state.attributes == attributes @pytest.mark.parametrize( "config_entry_extra_options", [ {}, { "device_class": "update", }, ], ) async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, config_entry_extra_options: dict[str, str], ) -> None: """Test the config flow.""" template_config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={ "name": "My template", "template_type": "button", "press": [ { "service": "input_boolean.toggle", "metadata": {}, "data": {}, "target": {"entity_id": "input_boolean.test"}, } ], } | config_entry_extra_options, 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("button.my_template") assert state is not None assert state == snapshot @pytest.mark.parametrize(("count", "config"), [(1, PRESS_ACTION)]) @pytest.mark.usefixtures("setup_button") async def test_missing_optional_config(hass: HomeAssistant) -> None: """Test: missing optional template is ok.""" _verify(hass, STATE_UNKNOWN) @pytest.mark.parametrize(("count", "config"), [(1, {"press": []})]) @pytest.mark.usefixtures("setup_button") async def test_missing_emtpy_press_action_config( hass: HomeAssistant, freezer: FrozenDateTimeFactory, ) -> None: """Test: missing optional template is ok.""" _verify(hass, STATE_UNKNOWN) now = dt_util.utcnow() freezer.move_to(now) await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, {CONF_ENTITY_ID: TEST_BUTTON.entity_id}, blocking=True, ) _verify( hass, now.isoformat(), ) @pytest.mark.parametrize(("count", "config"), [(0, {})]) @pytest.mark.usefixtures("setup_button") async def test_missing_required_keys(hass: HomeAssistant) -> None: """Test: missing required fields will fail.""" assert hass.states.async_all("button") == [] @pytest.mark.parametrize( ("count", "config"), [ ( 1, { **PRESS_ACTION, "device_class": "restart", }, ) ], ) @pytest.mark.usefixtures("setup_button") async def test_device_class_option( hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall], ) -> None: """Test optional options is ok.""" _verify( hass, STATE_UNKNOWN, { CONF_DEVICE_CLASS: "restart", CONF_FRIENDLY_NAME: TEST_BUTTON.object_id, }, TEST_BUTTON.entity_id, ) now = dt_util.utcnow() freezer.move_to(now) await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, {CONF_ENTITY_ID: TEST_BUTTON.entity_id}, blocking=True, ) assert_action(TEST_BUTTON, calls, 1, "press") _verify( hass, now.isoformat(), { CONF_DEVICE_CLASS: "restart", CONF_FRIENDLY_NAME: TEST_BUTTON.object_id, }, TEST_BUTTON.entity_id, ) @pytest.mark.parametrize("config", [PRESS_ACTION]) @pytest.mark.parametrize( ("attribute", "attribute_template", "attribute_name", "expected"), [ ( CONF_ICON, "{{ 'mdi:test' if is_state('sensor.test_attribute', 'on') else '' }}", ATTR_ICON, "mdi:test", ), ( CONF_PICTURE, "{{ 'test.jpg' if is_state('sensor.test_attribute', 'on') else '' }}", ATTR_ENTITY_PICTURE, "test.jpg", ), ], ) @pytest.mark.usefixtures("setup_single_attribute_button") async def test_options_that_are_templates( hass: HomeAssistant, attribute_name: str, expected: str, freezer: FrozenDateTimeFactory, calls: list[ServiceCall], ) -> None: """Test button options that are templates.""" expected_attributes = {attribute_name: expected} _verify(hass, STATE_UNKNOWN, {attribute_name: ""}) await async_trigger(hass, TEST_ATTRIBUTE_ENTITY_ID, STATE_ON) _verify(hass, STATE_UNKNOWN, expected_attributes) now = dt_util.utcnow() freezer.move_to(now) await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, {CONF_ENTITY_ID: TEST_BUTTON.entity_id}, blocking=True, ) assert_action(TEST_BUTTON, calls, 1, "press") _verify(hass, now.isoformat(), expected_attributes) @pytest.mark.parametrize("config", [PRESS_ACTION]) @pytest.mark.parametrize( ("attribute", "attribute_template"), [("name", "Button {{ 1 + 1 }}")] ) @pytest.mark.usefixtures("setup_single_attribute_button") async def test_name_template(hass: HomeAssistant) -> None: """Test: name template.""" _verify( hass, STATE_UNKNOWN, { CONF_FRIENDLY_NAME: "Button 2", }, "button.button_2", ) async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option only creates one button per id.""" await setup_and_test_unique_id( hass, TEST_BUTTON, ConfigurationStyle.MODERN, PRESS_ACTION ) async def test_nested_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test a template unique_id propagates to button unique_ids.""" await setup_and_test_nested_unique_id( hass, TEST_BUTTON, ConfigurationStyle.MODERN, entity_registry, PRESS_ACTION ) async def test_device_id( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test for device for button template.""" device_config_entry = MockConfigEntry() device_config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=device_config_entry.entry_id, identifiers={("test", "identifier_test")}, connections={("mac", "30:31:32:33:34:35")}, ) await hass.async_block_till_done() assert device_entry is not None assert device_entry.id is not None template_config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={ "name": "My template", "template_type": "button", "device_id": device_entry.id, "press": [ { "service": "input_boolean.toggle", "metadata": {}, "data": {}, "target": {"entity_id": "input_boolean.test"}, } ], }, 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() template_entity = entity_registry.async_get("button.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id @pytest.mark.parametrize( ("config", "attribute", "attribute_template"), [ ( PRESS_ACTION, "availability", "{{ is_state('binary_sensor.availability', 'on') }}", ) ], ) @pytest.mark.usefixtures("setup_single_attribute_button") async def test_available_template_with_entities(hass: HomeAssistant) -> None: """Test availability templates with values from other entities.""" await async_trigger(hass, TEST_AVAILABILITY_ENTITY, STATE_ON) # Device State should not be unavailable assert hass.states.get(TEST_BUTTON.entity_id).state != STATE_UNAVAILABLE # When Availability template returns false await async_trigger(hass, TEST_AVAILABILITY_ENTITY, STATE_OFF) # device state should be unavailable assert hass.states.get(TEST_BUTTON.entity_id).state == STATE_UNAVAILABLE @pytest.mark.parametrize( ("config", "attribute", "attribute_template"), [(PRESS_ACTION, "availability", "{{ x - 12 }}")], ) @pytest.mark.usefixtures("setup_single_attribute_button") async def test_invalid_availability_template_keeps_component_available( hass: HomeAssistant, caplog_setup_text ) -> None: """Test that an invalid availability keeps the device available.""" assert hass.states.get(TEST_BUTTON.entity_id).state != STATE_UNAVAILABLE assert "UndefinedError: 'x' is undefined" in caplog_setup_text