From 3045f67ae5f5ba4f6cd8f8e2a7e20154b8c9540e Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:49:28 -0400 Subject: [PATCH] Modernize binary sensor template tests (#148367) --- tests/components/template/conftest.py | 82 + .../components/template/test_binary_sensor.py | 1802 ++++++++--------- 2 files changed, 963 insertions(+), 921 deletions(-) diff --git a/tests/components/template/conftest.py b/tests/components/template/conftest.py index 6d1776f24cd..c57d1dcbfab 100644 --- a/tests/components/template/conftest.py +++ b/tests/components/template/conftest.py @@ -23,6 +23,88 @@ class ConfigurationStyle(Enum): TRIGGER = "Trigger" +def make_test_trigger(*entities: str) -> dict: + """Make a test state trigger.""" + return { + "trigger": [ + { + "trigger": "state", + "entity_id": list(entities), + }, + {"platform": "event", "event_type": "test_event"}, + ], + "variables": {"triggering_entity": "{{ trigger.entity_id }}"}, + "action": [ + {"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}} + ], + } + + +async def async_setup_legacy_platforms( + hass: HomeAssistant, + domain: str, + slug: str, + count: int, + config: ConfigType, +) -> None: + """Do setup of any legacy platform that supports a keyed dictionary of template entities.""" + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + {domain: {"platform": "template", slug: config}}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_modern_state_format( + hass: HomeAssistant, + domain: str, + count: int, + config: ConfigType, + extra_config: ConfigType | None = None, +) -> None: + """Do setup of template integration via modern format.""" + extra = extra_config or {} + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + {"template": {domain: config, **extra}}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_modern_trigger_format( + hass: HomeAssistant, + domain: str, + trigger: dict, + count: int, + config: ConfigType, + extra_config: ConfigType | None = None, +) -> None: + """Do setup of template integration via trigger format.""" + extra = extra_config or {} + config = {"template": {domain: config, **trigger, **extra}} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + @pytest.fixture def calls(hass: HomeAssistant) -> list[ServiceCall]: """Track calls to a mock service.""" diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index a3b7edea919..75a9e2c9689 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1,9 +1,9 @@ """The tests for the Template Binary sensor platform.""" from collections.abc import Generator -from copy import deepcopy from datetime import UTC, datetime, timedelta import logging +from typing import Any from unittest.mock import Mock, patch from freezegun.api import FrozenDateTimeFactory @@ -23,16 +23,26 @@ from homeassistant.const import ( from homeassistant.core import Context, CoreState, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.helpers.typing import ConfigType -from homeassistant.setup import async_setup_component + +from .conftest import ( + ConfigurationStyle, + async_get_flow_preview_state, + async_setup_legacy_platforms, + async_setup_modern_state_format, + async_setup_modern_trigger_format, + make_test_trigger, +) from tests.common import ( MockConfigEntry, - assert_setup_component, async_fire_time_changed, + async_mock_restore_state_shutdown_restart, mock_restore_cache, mock_restore_cache_with_extra_data, ) +from tests.typing import WebSocketGenerator _BEER_TRIGGER_VALUE_TEMPLATE = ( "{% if trigger.event.data.beer < 0 %}" @@ -45,94 +55,202 @@ _BEER_TRIGGER_VALUE_TEMPLATE = ( ) -@pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - ("config", "domain", "entity_id", "name", "attributes"), - [ - ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "value_template": "{{ True }}", - } - }, - }, - }, - binary_sensor.DOMAIN, - "binary_sensor.test", - "test", - {"friendly_name": "test"}, - ), - ( - { - "template": { - "binary_sensor": { - "state": "{{ True }}", - } - }, - }, - template.DOMAIN, - "binary_sensor.unnamed_device", - "unnamed device", - {}, - ), - ], +TEST_OBJECT_ID = "test_binary_sensor" +TEST_ENTITY_ID = f"binary_sensor.{TEST_OBJECT_ID}" +TEST_STATE_ENTITY_ID = "binary_sensor.test_state" +TEST_ATTRIBUTE_ENTITY_ID = "sensor.test_attribute" +TEST_AVAILABILITY_ENTITY_ID = "binary_sensor.test_availability" +TEST_STATE_TRIGGER = make_test_trigger( + TEST_STATE_ENTITY_ID, TEST_AVAILABILITY_ENTITY_ID, TEST_ATTRIBUTE_ENTITY_ID ) -@pytest.mark.usefixtures("start_ha") -async def test_setup_minimal( - hass: HomeAssistant, entity_id: str, name: str, attributes: dict[str, str] +UNIQUE_ID_CONFIG = { + "unique_id": "not-so-unique-anymore", +} + + +async def async_setup_legacy_format( + hass: HomeAssistant, count: int, config: ConfigType ) -> None: - """Test the setup.""" - state = hass.states.get(entity_id) - assert state is not None - assert state.name == name - assert state.state == STATE_ON - assert state.attributes == attributes + """Do setup of binary sensor integration via legacy format.""" + await async_setup_legacy_platforms( + hass, binary_sensor.DOMAIN, "sensors", count, config + ) + + +async def async_setup_modern_format( + hass: HomeAssistant, + count: int, + config: ConfigType, + extra_config: ConfigType | None = None, +) -> None: + """Do setup of binary sensor integration via modern format.""" + await async_setup_modern_state_format( + hass, binary_sensor.DOMAIN, count, config, extra_config + ) + + +async def async_setup_trigger_format( + hass: HomeAssistant, + count: int, + config: ConfigType, + extra_config: ConfigType | None = None, +) -> None: + """Do setup of binary sensor integration via trigger format.""" + await async_setup_modern_trigger_format( + hass, binary_sensor.DOMAIN, TEST_STATE_TRIGGER, count, config, extra_config + ) + + +@pytest.fixture +async def setup_base_binary_sensor( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + config: ConfigType | list[dict], + extra_template_options: ConfigType, +) -> None: + """Do setup of binary sensor integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format(hass, count, config) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, config, extra_template_options) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format(hass, count, config, extra_template_options) + + +async def async_setup_binary_sensor( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, + extra_config: ConfigType, +) -> None: + """Do setup of binary sensor integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + {TEST_OBJECT_ID: {"value_template": state_template, **extra_config}}, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + {"name": TEST_OBJECT_ID, "state": state_template, **extra_config}, + ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + {"name": TEST_OBJECT_ID, "state": state_template, **extra_config}, + ) + + +@pytest.fixture +async def setup_binary_sensor( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, + extra_config: dict[str, Any], +) -> None: + """Do setup of binary sensor integration.""" + await async_setup_binary_sensor(hass, count, style, state_template, extra_config) + + +@pytest.fixture +async def setup_single_attribute_binary_sensor( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + attribute: str, + attribute_value: str | dict, + state_template: str, + extra_config: dict, +) -> None: + """Do setup of binary sensor integration testing a single attribute.""" + extra = {attribute: attribute_value} if attribute and attribute_value else {} + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + "value_template": state_template, + **extra, + **extra_config, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + "state": state_template, + **extra, + **extra_config, + }, + ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + "state": state_template, + **extra, + **extra_config, + }, + ) -@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "domain", "entity_id"), + ("count", "state_template", "extra_config"), [(1, "{{ True }}", {})] +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_binary_sensor") +async def test_setup_minimal(hass: HomeAssistant) -> None: + """Test the setup.""" + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.name == TEST_OBJECT_ID + assert state.state == STATE_ON + assert state.attributes == {"friendly_name": TEST_OBJECT_ID} + + +@pytest.mark.parametrize( + ("count", "state_template", "extra_config"), [ ( + 1, + "{{ True }}", { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "{{ True }}", - "device_class": "motion", - } - }, - }, + "device_class": "motion", }, - binary_sensor.DOMAIN, - "binary_sensor.test", - ), - ( - { - "template": { - "binary_sensor": { - "name": "virtual thingy", - "state": "{{ True }}", - "device_class": "motion", - } - }, - }, - template.DOMAIN, - "binary_sensor.virtual_thingy", - ), + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_setup(hass: HomeAssistant, entity_id: str) -> None: +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_binary_sensor") +async def test_setup(hass: HomeAssistant) -> None: """Test the setup.""" - state = hass.states.get(entity_id) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) assert state is not None - assert state.name == "virtual thingy" + assert state.name == TEST_OBJECT_ID assert state.state == STATE_ON assert state.attributes["device_class"] == "motion" @@ -298,162 +416,144 @@ async def test_state( assert state.state == expected_result -@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "domain", "entity_id"), + ("count", "state_template", "attribute_value", "extra_config"), [ ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_template_sensor": { - "value_template": "{{ states.sensor.xyz.state }}", - "icon_template": "{% if " - "states.binary_sensor.test_state.state == " - "'on' %}" - "mdi:check" - "{% endif %}", - }, - }, - }, - }, - binary_sensor.DOMAIN, - "binary_sensor.test_template_sensor", - ), - ( - { - "template": { - "binary_sensor": { - "state": "{{ states.sensor.xyz.state }}", - "icon": "{% if " - "states.binary_sensor.test_state.state == " - "'on' %}" - "mdi:check" - "{% endif %}", - }, - }, - }, - template.DOMAIN, - "binary_sensor.unnamed_device", - ), + 1, + "{{ 1 == 1 }}", + "{% if is_state('binary_sensor.test_state', 'on') %}mdi:check{% endif %}", + {}, + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_icon_template(hass: HomeAssistant, entity_id: str) -> None: +@pytest.mark.parametrize( + ("style", "attribute", "initial_state"), + [ + (ConfigurationStyle.LEGACY, "icon_template", ""), + (ConfigurationStyle.MODERN, "icon", ""), + (ConfigurationStyle.TRIGGER, "icon", None), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_icon_template(hass: HomeAssistant, initial_state: str | None) -> None: """Test icon template.""" - state = hass.states.get(entity_id) - assert state.attributes.get("icon") == "" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("icon") == initial_state - hass.states.async_set("binary_sensor.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get(entity_id) + + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["icon"] == "mdi:check" -@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "domain", "entity_id"), + ("count", "state_template", "attribute_value", "extra_config"), [ ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_template_sensor": { - "value_template": "{{ states.sensor.xyz.state }}", - "entity_picture_template": "{% if " - "states.binary_sensor.test_state.state == " - "'on' %}" - "/local/sensor.png" - "{% endif %}", - }, - }, - }, - }, - binary_sensor.DOMAIN, - "binary_sensor.test_template_sensor", - ), - ( - { - "template": { - "binary_sensor": { - "state": "{{ states.sensor.xyz.state }}", - "picture": "{% if " - "states.binary_sensor.test_state.state == " - "'on' %}" - "/local/sensor.png" - "{% endif %}", - }, - }, - }, - template.DOMAIN, - "binary_sensor.unnamed_device", - ), + 1, + "{{ 1 == 1 }}", + "{% if is_state('binary_sensor.test_state', 'on') %}/local/sensor.png{% endif %}", + {}, + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_entity_picture_template(hass: HomeAssistant, entity_id: str) -> None: +@pytest.mark.parametrize( + ("style", "attribute", "initial_state"), + [ + (ConfigurationStyle.LEGACY, "entity_picture_template", ""), + (ConfigurationStyle.MODERN, "picture", ""), + (ConfigurationStyle.TRIGGER, "picture", None), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_entity_picture_template( + hass: HomeAssistant, initial_state: str | None +) -> None: """Test entity_picture template.""" - state = hass.states.get(entity_id) - assert state.attributes.get("entity_picture") == "" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("entity_picture") == initial_state - hass.states.async_set("binary_sensor.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get(entity_id) + + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["entity_picture"] == "/local/sensor.png" -@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "domain", "entity_id"), + ("count", "state_template", "attribute_value", "extra_config"), [ ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_template_sensor": { - "value_template": "{{ states.sensor.xyz.state }}", - "attribute_templates": { - "test_attribute": "It {{ states.sensor.test_state.state }}." - }, - }, - }, - }, - }, - binary_sensor.DOMAIN, - "binary_sensor.test_template_sensor", - ), - ( - { - "template": { - "binary_sensor": { - "state": "{{ states.sensor.xyz.state }}", - "attributes": { - "test_attribute": "It {{ states.sensor.test_state.state }}." - }, - }, - }, - }, - template.DOMAIN, - "binary_sensor.unnamed_device", - ), + 1, + "{{ True }}", + {"test_attribute": "It {{ states.sensor.test_attribute.state }}."}, + {}, + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_attribute_templates(hass: HomeAssistant, entity_id: str) -> None: +@pytest.mark.parametrize( + ("style", "attribute", "initial_value"), + [ + (ConfigurationStyle.LEGACY, "attribute_templates", "It ."), + (ConfigurationStyle.MODERN, "attributes", "It ."), + (ConfigurationStyle.TRIGGER, "attributes", None), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_attribute_templates( + hass: HomeAssistant, initial_value: str | None +) -> None: """Test attribute_templates template.""" - state = hass.states.get(entity_id) - assert state.attributes.get("test_attribute") == "It ." - hass.states.async_set("sensor.test_state", "Works2") + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("test_attribute") == initial_value + + hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, "Works2") await hass.async_block_till_done() - hass.states.async_set("sensor.test_state", "Works") + hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, "Works") await hass.async_block_till_done() - state = hass.states.get(entity_id) + + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["test_attribute"] == "It Works." +@pytest.mark.parametrize( + ("count", "state_template", "attribute_value", "extra_config"), + [ + ( + 1, + "{{ states.binary_sensor.test_sensor }}", + {"test_attribute": "{{ states.binary_sensor.unknown.attributes.picture }}"}, + {}, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "attribute_templates"), + (ConfigurationStyle.MODERN, "attributes"), + (ConfigurationStyle.TRIGGER, "attributes"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_invalid_attribute_template( + hass: HomeAssistant, + style: ConfigurationStyle, + caplog_setup_text: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that errors are logged if rendering template fails.""" + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + text = ( + "Template variable error: 'None' has no attribute 'attributes' when rendering" + ) + assert text in caplog_setup_text or text in caplog.text + + @pytest.fixture def setup_mock() -> Generator[Mock]: """Do setup of sensor mock.""" @@ -496,338 +596,261 @@ async def test_match_all(hass: HomeAssistant, setup_mock: Mock) -> None: assert len(setup_mock.mock_calls) == init_calls -@pytest.mark.parametrize(("count", "domain"), [(1, binary_sensor.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "extra_config"), [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - }, - }, - }, - }, + ( + 1, + "{{ is_state('binary_sensor.test_state', 'on') }}", + {"device_class": "motion"}, + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_event(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("style", "initial_state"), + [ + (ConfigurationStyle.LEGACY, STATE_OFF), + (ConfigurationStyle.MODERN, STATE_OFF), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.usefixtures("setup_binary_sensor") +async def test_binary_sensor_state(hass: HomeAssistant, initial_state: str) -> None: """Test the event.""" - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == initial_state - hass.states.async_set("sensor.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_ON @pytest.mark.parametrize( - ("config", "count", "domain"), + ("count", "state_template", "extra_config", "attribute"), [ ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_on": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_on": 5, - }, - "test_off": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_off": 5, - }, - }, - }, - }, 1, - binary_sensor.DOMAIN, - ), - ( - { - "template": [ - { - "binary_sensor": { - "name": "test on", - "state": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_on": 5, - }, - }, - { - "binary_sensor": { - "name": "test off", - "state": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_off": 5, - }, - }, - ] - }, - 2, - template.DOMAIN, - ), - ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_on": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_on": '{{ ({ "seconds": 10 / 2 }) }}', - }, - "test_off": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_off": '{{ ({ "seconds": 10 / 2 }) }}', - }, - }, - }, - }, - 1, - binary_sensor.DOMAIN, - ), - ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_on": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_on": '{{ ({ "seconds": states("input_number.delay")|int }) }}', - }, - "test_off": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_off": '{{ ({ "seconds": states("input_number.delay")|int }) }}', - }, - }, - }, - }, - 1, - binary_sensor.DOMAIN, - ), + "{{ is_state('binary_sensor.test_state', 'on') }}", + {"device_class": "motion"}, + "delay_on", + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_template_delay_on_off( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize( + ("style", "initial_state"), + [ + (ConfigurationStyle.LEGACY, STATE_OFF), + (ConfigurationStyle.MODERN, STATE_OFF), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.parametrize( + "attribute_value", + [ + 5, + "{{ dict(seconds=10 / 2) }}", + '{{ dict(seconds=states("sensor.test_attribute") | int(0)) }}', + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_delay_on( + hass: HomeAssistant, initial_state: str, freezer: FrozenDateTimeFactory ) -> None: """Test binary sensor template delay on.""" # Ensure the initial state is not on - assert hass.states.get("binary_sensor.test_on").state != STATE_ON - assert hass.states.get("binary_sensor.test_off").state != STATE_ON + assert hass.states.get(TEST_ENTITY_ID).state == initial_state - hass.states.async_set("input_number.delay", 5) - hass.states.async_set("sensor.test_state", STATE_ON) + hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, 5) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test_on").state == STATE_OFF - assert hass.states.get("binary_sensor.test_off").state == STATE_ON + + assert hass.states.get(TEST_ENTITY_ID).state == initial_state freezer.tick(timedelta(seconds=5)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test_on").state == STATE_ON - assert hass.states.get("binary_sensor.test_off").state == STATE_ON - # check with time changes - hass.states.async_set("sensor.test_state", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test_on").state == STATE_OFF - assert hass.states.get("binary_sensor.test_off").state == STATE_ON + assert hass.states.get(TEST_ENTITY_ID).state == STATE_ON - hass.states.async_set("sensor.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test_on").state == STATE_OFF - assert hass.states.get("binary_sensor.test_off").state == STATE_ON - hass.states.async_set("sensor.test_state", STATE_OFF) + assert hass.states.get(TEST_ENTITY_ID).state == STATE_OFF + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test_on").state == STATE_OFF - assert hass.states.get("binary_sensor.test_off").state == STATE_ON + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_OFF + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_OFF freezer.tick(timedelta(seconds=5)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test_on").state == STATE_OFF - assert hass.states.get("binary_sensor.test_off").state == STATE_OFF + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_OFF -@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "domain", "entity_id"), + ("count", "state_template", "extra_config", "attribute"), [ ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "true", - "device_class": "motion", - "delay_off": 5, - }, - }, - }, - }, - binary_sensor.DOMAIN, - "binary_sensor.test", - ), - ( - { - "template": { - "binary_sensor": { - "name": "virtual thingy", - "state": "true", - "device_class": "motion", - "delay_off": 5, - }, - }, - }, - template.DOMAIN, - "binary_sensor.virtual_thingy", - ), + 1, + "{{ is_state('binary_sensor.test_state', 'on') }}", + {"device_class": "motion"}, + "delay_off", + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_available_without_availability_template( - hass: HomeAssistant, entity_id: str -) -> None: +@pytest.mark.parametrize( + "style", + [ + ConfigurationStyle.LEGACY, + ConfigurationStyle.MODERN, + ConfigurationStyle.TRIGGER, + ], +) +@pytest.mark.parametrize( + "attribute_value", + [ + 5, + "{{ dict(seconds=10 / 2) }}", + '{{ dict(seconds=states("sensor.test_attribute") | int(0)) }}', + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_delay_off(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: + """Test binary sensor template delay off.""" + assert hass.states.get(TEST_ENTITY_ID).state != STATE_ON + + hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, 5) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_ON + + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_ON + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_ON + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_ON + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_ON + + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state == STATE_OFF + + +@pytest.mark.parametrize( + ("count", "state_template", "extra_config"), + [ + ( + 1, + "{{ True }}", + { + "device_class": "motion", + "delay_off": 5, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_binary_sensor") +async def test_available_without_availability_template(hass: HomeAssistant) -> None: """Ensure availability is true without an availability_template.""" - state = hass.states.get(entity_id) + state = hass.states.get(TEST_ENTITY_ID) assert state.state != STATE_UNAVAILABLE assert state.attributes[ATTR_DEVICE_CLASS] == "motion" -@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "domain", "entity_id"), + ("count", "state_template", "attribute_value", "extra_config"), [ ( + 1, + "{{ True }}", + "{{ is_state('binary_sensor.test_availability','on') }}", { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "true", - "device_class": "motion", - "delay_off": 5, - "availability_template": "{{ is_state('sensor.test_state','on') }}", - }, - }, - }, + "device_class": "motion", + "delay_off": 5, }, - binary_sensor.DOMAIN, - "binary_sensor.test", - ), - ( - { - "template": { - "binary_sensor": { - "name": "virtual thingy", - "state": "true", - "device_class": "motion", - "delay_off": 5, - "availability": "{{ is_state('sensor.test_state','on') }}", - }, - }, - }, - template.DOMAIN, - "binary_sensor.virtual_thingy", - ), + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_availability_template(hass: HomeAssistant, entity_id: str) -> None: +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "availability_template"), + (ConfigurationStyle.MODERN, "availability"), + (ConfigurationStyle.TRIGGER, "availability"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_availability_template(hass: HomeAssistant) -> None: """Test availability template.""" - hass.states.async_set("sensor.test_state", STATE_OFF) + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state == STATE_UNAVAILABLE - hass.states.async_set("sensor.test_state", STATE_ON) + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get(entity_id) + state = hass.states.get(TEST_ENTITY_ID) assert state.state != STATE_UNAVAILABLE assert state.attributes[ATTR_DEVICE_CLASS] == "motion" -@pytest.mark.parametrize(("count", "domain"), [(1, binary_sensor.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_value", "extra_config"), + [(1, "{{ True }}", "{{ x - 12 }}", {})], +) +@pytest.mark.parametrize( + ("style", "attribute"), [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "invalid_template": { - "value_template": "{{ states.binary_sensor.test_sensor }}", - "attribute_templates": { - "test_attribute": "{{ states.binary_sensor.unknown.attributes.picture }}" - }, - } - }, - }, - }, + (ConfigurationStyle.LEGACY, "availability_template"), + (ConfigurationStyle.MODERN, "availability"), + (ConfigurationStyle.TRIGGER, "availability"), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_invalid_attribute_template( - hass: HomeAssistant, caplog_setup_text: str -) -> None: - """Test that errors are logged if rendering template fails.""" - hass.states.async_set("binary_sensor.test_sensor", STATE_ON) - assert len(hass.states.async_all()) == 2 - assert ("test_attribute") in caplog_setup_text - assert ("TemplateError") in caplog_setup_text - - -@pytest.mark.parametrize(("count", "domain"), [(1, binary_sensor.DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "my_sensor": { - "value_template": "{{ states.binary_sensor.test_sensor }}", - "availability_template": "{{ x - 12 }}", - }, - }, - }, - }, - ], -) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") async def test_invalid_availability_template_keeps_component_available( - hass: HomeAssistant, caplog_setup_text: str + hass: HomeAssistant, caplog_setup_text: str, caplog: pytest.LogCaptureFixture ) -> None: """Test that an invalid availability keeps the device available.""" - assert hass.states.get("binary_sensor.my_sensor").state != STATE_UNAVAILABLE - assert "UndefinedError: 'x' is undefined" in caplog_setup_text + hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE + text = "UndefinedError: 'x' is undefined" + assert text in caplog_setup_text or text in caplog.text async def test_no_update_template_match_all(hass: HomeAssistant) -> None: @@ -896,172 +919,145 @@ async def test_no_update_template_match_all(hass: HomeAssistant) -> None: assert hass.states.get("binary_sensor.all_attribute").state == STATE_OFF -@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize(("count", "extra_template_options"), [(1, {})]) @pytest.mark.parametrize( - "config", + ("config", "style"), [ - { - "template": { - "unique_id": "group-id", - "binary_sensor": { - "name": "top-level", - "unique_id": "sensor-id", - "state": STATE_ON, + ( + { + "test_template_01": { + "value_template": "{{ True }}", + **UNIQUE_ID_CONFIG, + }, + "test_template_02": { + "value_template": "{{ True }}", + **UNIQUE_ID_CONFIG, }, }, - "binary_sensor": { - "platform": "template", - "sensors": { - "test_template_cover_01": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ true }}", - }, - "test_template_cover_02": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ false }}", - }, + ConfigurationStyle.LEGACY, + ), + ( + [ + { + "name": "test_template_01", + "state": "{{ True }}", + **UNIQUE_ID_CONFIG, }, - }, - }, + { + "name": "test_template_02", + "state": "{{ True }}", + **UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.MODERN, + ), + ( + [ + { + "name": "test_template_01", + "state": "{{ True }}", + **UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_02", + "state": "{{ True }}", + **UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.TRIGGER, + ), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_unique_id( +@pytest.mark.usefixtures("setup_base_binary_sensor") +async def test_unique_id(hass: HomeAssistant) -> None: + """Test unique_id option only creates one fan per id.""" + assert len(hass.states.async_all()) == 1 + + +@pytest.mark.parametrize( + ("count", "config", "extra_template_options"), + [ + ( + 1, + [ + { + "name": "test_a", + "state": "{{ True }}", + "unique_id": "a", + }, + { + "name": "test_b", + "state": "{{ True }}", + "unique_id": "b", + }, + ], + {"unique_id": "x"}, + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_base_binary_sensor") +async def test_nested_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: - """Test unique_id option only creates one binary sensor per id.""" - assert len(hass.states.async_all()) == 2 + """Test a template unique_id propagates to switch unique_ids.""" + assert len(hass.states.async_all("binary_sensor")) == 2 - assert len(entity_registry.entities) == 2 - assert entity_registry.async_get_entity_id( - "binary_sensor", "template", "group-id-sensor-id" - ) - assert entity_registry.async_get_entity_id( - "binary_sensor", "template", "not-so-unique-anymore" - ) + entry = entity_registry.async_get("binary_sensor.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("binary_sensor.test_b") + assert entry + assert entry.unique_id == "x-b" -@pytest.mark.parametrize(("count", "domain"), [(1, binary_sensor.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_value", "extra_config"), + [(1, "{{ 1 == 1 }}", "{{ states.sensor.test_attribute.state }}", {})], +) +@pytest.mark.parametrize( + ("style", "attribute", "initial_state"), [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "True", - "icon_template": "{{ states.sensor.test_state.state }}", - "device_class": "motion", - "delay_on": 5, - }, - }, - }, - }, + (ConfigurationStyle.LEGACY, "icon_template", ""), + (ConfigurationStyle.MODERN, "icon", ""), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_template_validation_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_template_icon_validation_error( + hass: HomeAssistant, initial_state: str, caplog: pytest.LogCaptureFixture ) -> None: """Test binary sensor template delay on.""" caplog.set_level(logging.ERROR) - state = hass.states.get("binary_sensor.test") - assert state.attributes.get("icon") == "" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("icon") == initial_state - hass.states.async_set("sensor.test_state", "mdi:check") + hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, "mdi:check") await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") - assert state.attributes.get("icon") == "mdi:check" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["icon"] == "mdi:check" - hass.states.async_set("sensor.test_state", "invalid_icon") + hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, "invalid_icon") await hass.async_block_till_done() + assert len(caplog.records) == 1 assert caplog.records[0].message.startswith( "Error validating template result 'invalid_icon' from template" ) - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("icon") is None -@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "domain", "entity_id"), - [ - ( - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "availability_template": "{{ is_state('sensor.bla', 'available') }}", - "entity_picture_template": "{{ 'blib' + 'blub' }}", - "icon_template": "mdi:{{ 1+2 }}", - "friendly_name": "{{ 'My custom ' + 'sensor' }}", - "value_template": "{{ true }}", - }, - }, - }, - }, - binary_sensor.DOMAIN, - "binary_sensor.test", - ), - ( - { - "template": { - "binary_sensor": { - "availability": "{{ is_state('sensor.bla', 'available') }}", - "picture": "{{ 'blib' + 'blub' }}", - "icon": "mdi:{{ 1+2 }}", - "name": "{{ 'My custom ' + 'sensor' }}", - "state": "{{ true }}", - }, - }, - }, - template.DOMAIN, - "binary_sensor.my_custom_sensor", - ), - ], + ("count", "state_template"), [(1, "{{ states.binary_sensor.test_state.state }}")] ) -@pytest.mark.usefixtures("start_ha") -async def test_availability_icon_picture(hass: HomeAssistant, entity_id: str) -> None: - """Test name, icon and picture templates are rendered at setup.""" - state = hass.states.get(entity_id) - assert state.state == "unavailable" - assert state.attributes == { - "entity_picture": "blibblub", - "friendly_name": "My custom sensor", - "icon": "mdi:3", - } - - hass.states.async_set("sensor.bla", "available") - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == "on" - assert state.attributes == { - "entity_picture": "blibblub", - "friendly_name": "My custom sensor", - "icon": "mdi:3", - } - - -@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) @pytest.mark.parametrize( - "config", - [ - { - "template": { - "binary_sensor": { - "name": "test", - "state": "{{ states.sensor.test_state.state }}", - }, - }, - }, - ], + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], ) @pytest.mark.parametrize( ("extra_config", "source_state", "restored_state", "initial_state"), @@ -1107,8 +1103,8 @@ async def test_availability_icon_picture(hass: HomeAssistant, entity_id: str) -> async def test_restore_state( hass: HomeAssistant, count: int, - domain: str, - config: ConfigType, + style: ConfigurationStyle, + state_template: str, extra_config: ConfigType, source_state: str | None, restored_state: str, @@ -1116,199 +1112,33 @@ async def test_restore_state( ) -> None: """Test restoring template binary sensor.""" - hass.states.async_set("sensor.test_state", source_state) - fake_state = State( - "binary_sensor.test", - restored_state, - {}, - ) + hass.states.async_set(TEST_STATE_ENTITY_ID, source_state) + await hass.async_block_till_done() + + fake_state = State(TEST_ENTITY_ID, restored_state, {}) mock_restore_cache(hass, (fake_state,)) - config = deepcopy(config) - config["template"]["binary_sensor"].update(**extra_config) - with assert_setup_component(count, domain): - assert await async_setup_component( - hass, - domain, - config, - ) - await hass.async_block_till_done() + await async_setup_binary_sensor(hass, count, style, state_template, extra_config) - context = Context() - hass.bus.async_fire("test_event", {"beer": 2}, context=context) - await hass.async_block_till_done() - - await hass.async_start() - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == initial_state -@pytest.mark.parametrize(("count", "domain"), [(2, "template")]) @pytest.mark.parametrize( - "config", + ("count", "style", "state_template", "extra_config"), [ - { - "template": [ - {"invalid": "config"}, - # Config after invalid should still be set up - { - "unique_id": "listening-test-event", - "trigger": {"platform": "event", "event_type": "test_event"}, - "binary_sensors": { - "hello": { - "friendly_name": "Hello Name", - "unique_id": "hello_name-id", - "device_class": "battery", - "value_template": _BEER_TRIGGER_VALUE_TEMPLATE, - "entity_picture_template": "{{ '/local/dogs.png' }}", - "icon_template": "{{ 'mdi:pirate' }}", - "attribute_templates": { - "plus_one": "{{ trigger.event.data.beer + 1 }}" - }, - }, - }, - "binary_sensor": [ - { - "name": "via list", - "unique_id": "via_list-id", - "device_class": "battery", - "state": _BEER_TRIGGER_VALUE_TEMPLATE, - "picture": "{{ '/local/dogs.png' }}", - "icon": "{{ 'mdi:pirate' }}", - "attributes": { - "plus_one": "{{ trigger.event.data.beer + 1 }}", - "another": "{{ trigger.event.data.uno_mas or 1 }}", - }, - } - ], - }, - { - "trigger": [], - "binary_sensors": { - "bare_minimum": { - "value_template": "{{ trigger.event.data.beer == 1 }}" - }, - }, - }, - ], - }, - ], -) -@pytest.mark.parametrize( - ( - "beer_count", - "final_state", - "icon_attr", - "entity_picture_attr", - "plus_one_attr", - "another_attr", - "another_attr_update", - ), - [ - (2, STATE_ON, "mdi:pirate", "/local/dogs.png", 3, 1, "si"), - (1, STATE_OFF, "mdi:pirate", "/local/dogs.png", 2, 1, "si"), - (0, STATE_UNKNOWN, "mdi:pirate", "/local/dogs.png", 1, 1, "si"), - (-1, STATE_UNAVAILABLE, None, None, None, None, None), - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_trigger_entity( - hass: HomeAssistant, - beer_count: int, - final_state: str, - icon_attr: str | None, - entity_picture_attr: str | None, - plus_one_attr: int | None, - another_attr: int | None, - another_attr_update: str | None, - entity_registry: er.EntityRegistry, -) -> None: - """Test trigger entity works.""" - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.hello_name") - assert state is not None - assert state.state == STATE_UNKNOWN - - state = hass.states.get("binary_sensor.bare_minimum") - assert state is not None - assert state.state == STATE_UNKNOWN - - context = Context() - hass.bus.async_fire("test_event", {"beer": beer_count}, context=context) - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.hello_name") - assert state.state == final_state - assert state.attributes.get("device_class") == "battery" - assert state.attributes.get("icon") == icon_attr - assert state.attributes.get("entity_picture") == entity_picture_attr - assert state.attributes.get("plus_one") == plus_one_attr - assert state.context is context - - assert len(entity_registry.entities) == 2 - assert ( - entity_registry.entities["binary_sensor.hello_name"].unique_id - == "listening-test-event-hello_name-id" - ) - assert ( - entity_registry.entities["binary_sensor.via_list"].unique_id - == "listening-test-event-via_list-id" - ) - - state = hass.states.get("binary_sensor.via_list") - assert state.state == final_state - assert state.attributes.get("device_class") == "battery" - assert state.attributes.get("icon") == icon_attr - assert state.attributes.get("entity_picture") == entity_picture_attr - assert state.attributes.get("plus_one") == plus_one_attr - assert state.attributes.get("another") == another_attr - assert state.context is context - - # Even if state itself didn't change, attributes might have changed - hass.bus.async_fire("test_event", {"beer": beer_count, "uno_mas": "si"}) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.via_list") - assert state.state == final_state - assert state.attributes.get("another") == another_attr_update - - # Check None values - hass.bus.async_fire("test_event", {"beer": 0}) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.hello_name") - assert state.state == STATE_UNKNOWN - state = hass.states.get("binary_sensor.via_list") - assert state.state == STATE_UNKNOWN - - # Check impossible values - hass.bus.async_fire("test_event", {"beer": -1}) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.hello_name") - assert state.state == STATE_UNAVAILABLE - state = hass.states.get("binary_sensor.via_list") - assert state.state == STATE_UNAVAILABLE - - -@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) -@pytest.mark.parametrize( - "config", - [ - { - "template": { - "trigger": {"platform": "event", "event_type": "test_event"}, - "binary_sensor": { - "name": "test", - "state": _BEER_TRIGGER_VALUE_TEMPLATE, - "device_class": "motion", - "delay_on": '{{ ({ "seconds": 6 / 2 }) }}', - "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', - }, + ( + 1, + ConfigurationStyle.TRIGGER, + _BEER_TRIGGER_VALUE_TEMPLATE, + { + "device_class": "motion", + "delay_on": '{{ ({ "seconds": 6 / 2 }) }}', + "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', }, - }, + ) ], ) -@pytest.mark.usefixtures("start_ha") @pytest.mark.parametrize( ("beer_count", "first_state", "second_state", "final_state"), [ @@ -1318,7 +1148,8 @@ async def test_trigger_entity( (-1, STATE_UNAVAILABLE, STATE_UNAVAILABLE, STATE_UNAVAILABLE), ], ) -async def test_template_with_trigger_templated_delay_on( +@pytest.mark.usefixtures("setup_binary_sensor") +async def test_template_with_trigger_templated_auto_off( hass: HomeAssistant, beer_count: int, first_state: str, @@ -1326,8 +1157,8 @@ async def test_template_with_trigger_templated_delay_on( final_state: str, freezer: FrozenDateTimeFactory, ) -> None: - """Test binary sensor template with template delay on.""" - state = hass.states.get("binary_sensor.test") + """Test binary sensor template with template auto off.""" + state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN context = Context() @@ -1335,7 +1166,7 @@ async def test_template_with_trigger_templated_delay_on( await hass.async_block_till_done() # State should still be unknown - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == first_state # Now wait for the on delay @@ -1343,7 +1174,7 @@ async def test_template_with_trigger_templated_delay_on( async_fire_time_changed(hass) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == second_state # Now wait for the auto-off @@ -1351,52 +1182,128 @@ async def test_template_with_trigger_templated_delay_on( async_fire_time_changed(hass) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == final_state -@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) @pytest.mark.parametrize( - ("config", "delay_state"), + ("count", "style", "state_template", "extra_config"), [ ( + 1, + ConfigurationStyle.TRIGGER, + "{{ True }}", { - "template": { - "trigger": {"platform": "event", "event_type": "test_event"}, - "binary_sensor": { - "name": "test", - "state": "{{ trigger.event.data.beer == 2 }}", - "device_class": "motion", - "delay_on": '{{ ({ "seconds": 10 }) }}', - }, - }, + "device_class": "motion", + "auto_off": '{{ ({ "seconds": 5 }) }}', }, - STATE_ON, - ), - ( - { - "template": { - "trigger": {"platform": "event", "event_type": "test_event"}, - "binary_sensor": { - "name": "test", - "state": "{{ trigger.event.data.beer != 2 }}", - "device_class": "motion", - "delay_off": '{{ ({ "seconds": 10 }) }}', - }, - }, - }, - STATE_OFF, - ), + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_binary_sensor") +async def test_template_with_trigger_auto_off_cancel( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor template with template auto off.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + context = Context() + hass.bus.async_fire("test_event", {}, context=context) + await hass.async_block_till_done() + + # State should still be unknown + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + + # Now wait for the on delay + freezer.tick(timedelta(seconds=4)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + + hass.bus.async_fire("test_event", {}, context=context) + await hass.async_block_till_done() + + # Now wait for the on delay + freezer.tick(timedelta(seconds=4)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + + # Now wait for the auto-off + freezer.tick(timedelta(seconds=2)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + ("count", "style", "extra_config", "attribute_value"), + [ + ( + 1, + ConfigurationStyle.TRIGGER, + {"device_class": "motion"}, + "{{ states('sensor.test_attribute') }}", + ) + ], +) +@pytest.mark.parametrize( + ("state_template", "attribute"), + [ + ("{{ True }}", "delay_on"), + ("{{ False }}", "delay_off"), + ("{{ True }}", "auto_off"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") +async def test_trigger_with_negative_time_periods( + hass: HomeAssistant, attribute: str, caplog: pytest.LogCaptureFixture +) -> None: + """Test binary sensor template with template negative time periods.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, "-5") + await hass.async_block_till_done() + + assert f"Error rendering {attribute} template: " in caplog.text + + +@pytest.mark.parametrize( + ("count", "style", "extra_config", "attribute_value"), + [ + ( + 1, + ConfigurationStyle.TRIGGER, + {"device_class": "motion"}, + "{{ ({ 'seconds': 10 }) }}", + ) + ], +) +@pytest.mark.parametrize( + ("state_template", "attribute", "delay_state"), + [ + ("{{ trigger.event.data.beer == 2 }}", "delay_on", STATE_ON), + ("{{ trigger.event.data.beer != 2 }}", "delay_off", STATE_OFF), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_binary_sensor") async def test_trigger_template_delay_with_multiple_triggers( hass: HomeAssistant, delay_state: str, freezer: FrozenDateTimeFactory ) -> None: """Test trigger based binary sensor with multiple triggers occurring during the delay.""" for _ in range(10): # State should still be unknown - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN hass.bus.async_fire("test_event", {"beer": 2}, context=Context()) @@ -1406,32 +1313,10 @@ async def test_trigger_template_delay_with_multiple_triggers( async_fire_time_changed(hass) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == delay_state -@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) -@pytest.mark.parametrize( - "config", - [ - { - "template": { - "trigger": {"platform": "event", "event_type": "test_event"}, - "binary_sensor": { - "name": "test", - "state": _BEER_TRIGGER_VALUE_TEMPLATE, - "device_class": "motion", - "picture": "{{ '/local/dogs.png' }}", - "icon": "{{ 'mdi:pirate' }}", - "attributes": { - "plus_one": "{{ trigger.event.data.beer + 1 }}", - "another": "{{ trigger.event.data.uno_mas or 1 }}", - }, - }, - }, - }, - ], -) @pytest.mark.parametrize( ("restored_state", "initial_state", "initial_attributes"), [ @@ -1443,9 +1328,6 @@ async def test_trigger_template_delay_with_multiple_triggers( ) async def test_trigger_entity_restore_state( hass: HomeAssistant, - count: int, - domain: str, - config: ConfigType, restored_state: str, initial_state: str, initial_attributes: list[str], @@ -1459,7 +1341,7 @@ async def test_trigger_entity_restore_state( } fake_state = State( - "binary_sensor.test", + TEST_ENTITY_ID, restored_state, restored_attributes, ) @@ -1467,18 +1349,23 @@ async def test_trigger_entity_restore_state( "auto_off_time": None, } mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) - with assert_setup_component(count, domain): - assert await async_setup_component( - hass, - domain, - config, - ) + await async_setup_binary_sensor( + hass, + 1, + ConfigurationStyle.TRIGGER, + _BEER_TRIGGER_VALUE_TEMPLATE, + { + "device_class": "motion", + "picture": "{{ '/local/dogs.png' }}", + "icon": "{{ 'mdi:pirate' }}", + "attributes": { + "plus_one": "{{ trigger.event.data.beer + 1 }}", + "another": "{{ trigger.event.data.uno_mas or 1 }}", + }, + }, + ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == initial_state for attr, value in restored_attributes.items(): if attr in initial_attributes: @@ -1490,7 +1377,7 @@ async def test_trigger_entity_restore_state( hass.bus.async_fire("test_event", {"beer": 2}) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_ON assert state.attributes["icon"] == "mdi:pirate" assert state.attributes["entity_picture"] == "/local/dogs.png" @@ -1498,40 +1385,16 @@ async def test_trigger_entity_restore_state( assert state.attributes["another"] == 1 -@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) -@pytest.mark.parametrize( - "config", - [ - { - "template": { - "trigger": {"platform": "event", "event_type": "test_event"}, - "binary_sensor": { - "name": "test", - "state": _BEER_TRIGGER_VALUE_TEMPLATE, - "device_class": "motion", - "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', - }, - }, - }, - ], -) @pytest.mark.parametrize("restored_state", [STATE_ON, STATE_OFF]) async def test_trigger_entity_restore_state_auto_off( hass: HomeAssistant, - count: int, - domain: str, - config: ConfigType, restored_state: str, freezer: FrozenDateTimeFactory, ) -> None: """Test restoring trigger template binary sensor.""" freezer.move_to("2022-02-02 12:02:00+00:00") - fake_state = State( - "binary_sensor.test", - restored_state, - {}, - ) + fake_state = State(TEST_ENTITY_ID, restored_state, {}) fake_extra_data = { "auto_off_time": { "__type": "", @@ -1539,18 +1402,15 @@ async def test_trigger_entity_restore_state_auto_off( }, } mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) - with assert_setup_component(count, domain): - assert await async_setup_component( - hass, - domain, - config, - ) + await async_setup_binary_sensor( + hass, + 1, + ConfigurationStyle.TRIGGER, + _BEER_TRIGGER_VALUE_TEMPLATE, + {"device_class": "motion", "auto_off": '{{ ({ "seconds": 1 + 1 }) }}'}, + ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == restored_state # Now wait for the auto-off @@ -1558,42 +1418,18 @@ async def test_trigger_entity_restore_state_auto_off( await hass.async_block_till_done() await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF -@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) -@pytest.mark.parametrize( - "config", - [ - { - "template": { - "trigger": {"platform": "event", "event_type": "test_event"}, - "binary_sensor": { - "name": "test", - "state": _BEER_TRIGGER_VALUE_TEMPLATE, - "device_class": "motion", - "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', - }, - }, - }, - ], -) async def test_trigger_entity_restore_state_auto_off_expired( hass: HomeAssistant, - count: int, - domain: str, - config: ConfigType, freezer: FrozenDateTimeFactory, ) -> None: """Test restoring trigger template binary sensor.""" freezer.move_to("2022-02-02 12:02:00+00:00") - fake_state = State( - "binary_sensor.test", - STATE_ON, - {}, - ) + fake_state = State(TEST_ENTITY_ID, STATE_ON, {}) fake_extra_data = { "auto_off_time": { "__type": "", @@ -1601,21 +1437,132 @@ async def test_trigger_entity_restore_state_auto_off_expired( }, } mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) - with assert_setup_component(count, domain): - assert await async_setup_component( - hass, - domain, - config, - ) + await async_setup_binary_sensor( + hass, + 1, + ConfigurationStyle.TRIGGER, + _BEER_TRIGGER_VALUE_TEMPLATE, + {"device_class": "motion", "auto_off": '{{ ({ "seconds": 1 + 1 }) }}'}, + ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.test") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF +async def test_saving_auto_off( + hass: HomeAssistant, + hass_storage: dict[str, Any], + freezer: FrozenDateTimeFactory, +) -> None: + """Test we restore state integration.""" + restored_attributes = { + "entity_picture": "/local/cats.png", + "icon": "mdi:ship", + "plus_one": 55, + } + + freezer.move_to("2022-02-02 02:02:00+00:00") + fake_extra_data = { + "auto_off_time": { + "__type": "", + "isoformat": "2022-02-02T02:02:02+00:00", + }, + } + await async_setup_binary_sensor( + hass, + 1, + ConfigurationStyle.TRIGGER, + "{{ True }}", + { + "device_class": "motion", + "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', + "attributes": restored_attributes, + }, + ) + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + + await async_mock_restore_state_shutdown_restart(hass) + + assert len(hass_storage[RESTORE_STATE_KEY]["data"]) == 1 + state = hass_storage[RESTORE_STATE_KEY]["data"][0]["state"] + assert state["entity_id"] == TEST_ENTITY_ID + + for attr, value in restored_attributes.items(): + assert state["attributes"][attr] == value + + extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] + assert extra_data == fake_extra_data + + +async def test_trigger_entity_restore_invalid_auto_off_time_data( + hass: HomeAssistant, + hass_storage: dict[str, Any], + freezer: FrozenDateTimeFactory, +) -> None: + """Test restoring trigger template binary sensor.""" + + freezer.move_to("2022-02-02 12:02:00+00:00") + fake_state = State(TEST_ENTITY_ID, STATE_ON, {}) + fake_extra_data = { + "auto_off_time": { + "_type": "", + "isoformat": datetime(2022, 2, 2, 12, 2, 0, tzinfo=UTC).isoformat(), + }, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + await async_mock_restore_state_shutdown_restart(hass) + + extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] + assert extra_data == fake_extra_data + + await async_setup_binary_sensor( + hass, + 1, + ConfigurationStyle.TRIGGER, + _BEER_TRIGGER_VALUE_TEMPLATE, + {"device_class": "motion", "auto_off": '{{ ({ "seconds": 1 + 1 }) }}'}, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + +async def test_trigger_entity_restore_invalid_auto_off_time_key( + hass: HomeAssistant, + hass_storage: dict[str, Any], + freezer: FrozenDateTimeFactory, +) -> None: + """Test restoring trigger template binary sensor.""" + + freezer.move_to("2022-02-02 12:02:00+00:00") + fake_state = State(TEST_ENTITY_ID, STATE_ON, {}) + fake_extra_data = { + "auto_off_timex": { + "__type": "", + "isoformat": datetime(2022, 2, 2, 12, 2, 0, tzinfo=UTC).isoformat(), + }, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + await async_mock_restore_state_shutdown_restart(hass) + + extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] + assert "auto_off_timex" in extra_data + assert extra_data == fake_extra_data + + await async_setup_binary_sensor( + hass, + 1, + ConfigurationStyle.TRIGGER, + _BEER_TRIGGER_VALUE_TEMPLATE, + {"device_class": "motion", "auto_off": '{{ ({ "seconds": 1 + 1 }) }}'}, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + async def test_device_id( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -1653,3 +1600,16 @@ async def test_device_id( template_entity = entity_registry.async_get("binary_sensor.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +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, + binary_sensor.DOMAIN, + {"name": "My template", "state": "{{ 'on' }}"}, + ) + assert state["state"] == "on"