diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 40206a5ccbb..208077a4153 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -214,7 +214,8 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore ), (CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER), ): - if action_config := config.get(action_id): + # Scripts can be an empty list, therefore we need to check for None + if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) self._attr_supported_features |= supported_feature diff --git a/homeassistant/components/template/button.py b/homeassistant/components/template/button.py index 7a205446585..4ee8844d6e7 100644 --- a/homeassistant/components/template/button.py +++ b/homeassistant/components/template/button.py @@ -120,7 +120,8 @@ class TemplateButtonEntity(TemplateEntity, ButtonEntity): """Initialize the button.""" super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None - if action := config.get(CONF_PRESS): + # Scripts can be an empty list, therefore we need to check for None + if (action := config.get(CONF_PRESS)) is not None: self.add_script(CONF_PRESS, action, self._attr_name, DOMAIN) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_state = None diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 7a8e347ee8f..7c9c0ea9d53 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -172,7 +172,8 @@ class CoverTemplate(TemplateEntity, CoverEntity): (POSITION_ACTION, CoverEntityFeature.SET_POSITION), (TILT_ACTION, TILT_FEATURES), ): - if action_config := config.get(action_id): + # Scripts can be an empty list, therefore we need to check for None + if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) self._attr_supported_features |= supported_feature diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 6e0f9fe5e0c..f3bc26391a9 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -157,7 +157,8 @@ class TemplateFan(TemplateEntity, FanEntity): CONF_SET_OSCILLATING_ACTION, CONF_SET_DIRECTION_ACTION, ): - if action_config := config.get(action_id): + # Scripts can be an empty list, therefore we need to check for None + if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) self._state: bool | None = False diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 1cc47c74aa0..c58709eba5e 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -296,7 +296,8 @@ class LightTemplate(TemplateEntity, LightEntity): self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION) for action_id in (CONF_ON_ACTION, CONF_OFF_ACTION, CONF_EFFECT_ACTION): - if action_config := config.get(action_id): + # Scripts can be an empty list, therefore we need to check for None + if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) self._state = False @@ -323,7 +324,8 @@ class LightTemplate(TemplateEntity, LightEntity): (CONF_RGBW_ACTION, ColorMode.RGBW), (CONF_RGBWW_ACTION, ColorMode.RGBWW), ): - if action_config := config.get(action_id): + # Scripts can be an empty list, therefore we need to check for None + if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) color_modes.add(color_mode) self._supported_color_modes = filter_supported_color_modes(color_modes) @@ -333,7 +335,7 @@ class LightTemplate(TemplateEntity, LightEntity): self._color_mode = next(iter(self._supported_color_modes)) self._attr_supported_features = LightEntityFeature(0) - if self._action_scripts.get(CONF_EFFECT_ACTION): + if (self._action_scripts.get(CONF_EFFECT_ACTION)) is not None: self._attr_supported_features |= LightEntityFeature.EFFECT if self._supports_transition is True: self._attr_supported_features |= LightEntityFeature.TRANSITION diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index b19cadff26c..12a3e66cb5e 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -98,7 +98,8 @@ class TemplateLock(TemplateEntity, LockEntity): (CONF_UNLOCK, 0), (CONF_OPEN, LockEntityFeature.OPEN), ): - if action_config := config.get(action_id): + # Scripts can be an empty list, therefore we need to check for None + if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) self._attr_supported_features |= supported_feature self._code_format_template = config.get(CONF_CODE_FORMAT_TEMPLATE) diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index eb60a3dbfe4..74d88ee96c4 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -141,7 +141,8 @@ class TemplateSelect(TemplateEntity, SelectEntity): super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None self._value_template = config[CONF_STATE] - if select_option := config.get(CONF_SELECT_OPTION): + # Scripts can be an empty list, therefore we need to check for None + if (select_option := config.get(CONF_SELECT_OPTION)) is not None: self.add_script(CONF_SELECT_OPTION, select_option, self._attr_name, DOMAIN) self._options_template = config[ATTR_OPTIONS] self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC, False) @@ -197,7 +198,8 @@ class TriggerSelectEntity(TriggerEntity, SelectEntity): ) -> None: """Initialize the entity.""" super().__init__(hass, coordinator, config) - if select_option := config.get(CONF_SELECT_OPTION): + # Scripts can be an empty list, therefore we need to check for None + if (select_option := config.get(CONF_SELECT_OPTION)) is not None: self.add_script( CONF_SELECT_OPTION, select_option, diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index fb3aeb1e42a..1d18ea9d5ca 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -226,9 +226,10 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): assert name is not None self._template = config.get(CONF_STATE) - if on_action := config.get(CONF_TURN_ON): + # Scripts can be an empty list, therefore we need to check for None + if (on_action := config.get(CONF_TURN_ON)) is not None: self.add_script(CONF_TURN_ON, on_action, name, DOMAIN) - if off_action := config.get(CONF_TURN_OFF): + if (off_action := config.get(CONF_TURN_OFF)) is not None: self.add_script(CONF_TURN_OFF, off_action, name, DOMAIN) self._state: bool | None = False diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index c4d41b52f31..1e18b06436a 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -158,7 +158,8 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): (SERVICE_LOCATE, VacuumEntityFeature.LOCATE), (SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED), ): - if action_config := config.get(action_id): + # Scripts can be an empty list, therefore we need to check for None + if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) self._attr_supported_features |= supported_feature diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 4b259fabac2..2a99e00a9ce 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -82,6 +82,15 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "data": {"code": "{{ this.entity_id }}"}, }, } +EMPTY_ACTIONS = { + "arm_away": [], + "arm_home": [], + "arm_night": [], + "arm_vacation": [], + "arm_custom_bypass": [], + "disarm": [], + "trigger": [], +} TEMPLATE_ALARM_CONFIG = { @@ -173,6 +182,12 @@ async def test_setup_config_entry( "panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG}, } }, + { + "alarm_control_panel": { + "platform": "template", + "panels": {"test_template_panel": EMPTY_ACTIONS}, + } + }, ], ) @pytest.mark.usefixtures("start_ha") diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index b201385240c..31239dbaf92 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -93,6 +93,45 @@ async def test_missing_optional_config(hass: HomeAssistant) -> None: _verify(hass, STATE_UNKNOWN) +async def test_missing_emtpy_press_action_config( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test: missing optional template is ok.""" + with assert_setup_component(1, "template"): + assert await setup.async_setup_component( + hass, + "template", + { + "template": { + "button": { + "press": [], + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN) + + now = dt.datetime.now(dt.UTC) + freezer.move_to(now) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {CONF_ENTITY_ID: _TEST_BUTTON}, + blocking=True, + ) + + _verify( + hass, + now.isoformat(), + ) + + async def test_missing_required_keys(hass: HomeAssistant) -> None: """Test: missing required fields will fail.""" with assert_setup_component(0, "template"): diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index c49db59c2ee..668592e388b 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN as COVER_DOMAIN, + CoverEntityFeature, CoverState, ) from homeassistant.const import ( @@ -28,6 +29,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.setup import async_setup_component from tests.common import assert_setup_component @@ -1123,3 +1125,50 @@ async def test_self_referencing_icon_with_no_template_is_not_a_loop( assert len(hass.states.async_all()) == 1 assert "Template loop detected" not in caplog.text + + +@pytest.mark.parametrize( + ("script", "supported_feature"), + [ + ("stop_cover", CoverEntityFeature.STOP), + ("set_cover_position", CoverEntityFeature.SET_POSITION), + ( + "set_cover_tilt_position", + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION, + ), + ], +) +async def test_emtpy_action_config( + hass: HomeAssistant, script: str, supported_feature: CoverEntityFeature +) -> None: + """Test configuration with empty script.""" + with assert_setup_component(1, COVER_DOMAIN): + assert await async_setup_component( + hass, + COVER_DOMAIN, + { + COVER_DOMAIN: { + "platform": "template", + "covers": { + "test_template_cover": { + "open_cover": [], + "close_cover": [], + script: [], + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert ( + state.attributes["supported_features"] + == CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | supported_feature + ) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 1a739b4921e..c0aade84e0f 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -556,6 +556,42 @@ async def setup_single_action_light( ) +@pytest.fixture +async def setup_empty_action_light( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + action: str, + extra_config: dict, +) -> None: + """Do setup of light integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + "test_template_light": { + "turn_on": [], + "turn_off": [], + action: [], + **extra_config, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_new_format( + hass, + count, + { + "name": "test_template_light", + "turn_on": [], + "turn_off": [], + action: [], + **extra_config, + }, + ) + + @pytest.fixture async def setup_light_with_effects( hass: HomeAssistant, @@ -2404,3 +2440,82 @@ async def test_nested_unique_id( entry = entity_registry.async_get("light.test_b") assert entry assert entry.unique_id == "x-b" + + +@pytest.mark.parametrize(("count", "extra_config"), [(1, {})]) +@pytest.mark.parametrize( + "style", + [ + ConfigurationStyle.LEGACY, + ConfigurationStyle.MODERN, + ], +) +@pytest.mark.parametrize( + ("action", "color_mode"), + [ + ("set_level", ColorMode.BRIGHTNESS), + ("set_temperature", ColorMode.COLOR_TEMP), + ("set_hs", ColorMode.HS), + ("set_rgb", ColorMode.RGB), + ("set_rgbw", ColorMode.RGBW), + ("set_rgbww", ColorMode.RGBWW), + ], +) +async def test_empty_color_mode_action_config( + hass: HomeAssistant, + color_mode: ColorMode, + setup_empty_action_light, +) -> None: + """Test empty actions for color mode actions.""" + state = hass.states.get("light.test_template_light") + assert state.attributes["supported_color_modes"] == [color_mode] + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light"}, + blocking=True, + ) + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_template_light"}, + blocking=True, + ) + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_OFF + + +@pytest.mark.parametrize(("count"), [1]) +@pytest.mark.parametrize( + ("style", "extra_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "effect_list_template": "{{ ['a'] }}", + "effect_template": "{{ 'a' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "effect_list": "{{ ['a'] }}", + "effect": "{{ 'a' }}", + }, + ), + ], +) +@pytest.mark.parametrize("action", ["set_effect"]) +async def test_effect_with_empty_action( + hass: HomeAssistant, + setup_empty_action_light, +) -> None: + """Test empty set_effect action.""" + state = hass.states.get("light.test_template_light") + assert state.attributes["supported_features"] == LightEntityFeature.EFFECT diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index d9cb294c41f..50baa11b2d0 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -4,7 +4,7 @@ import pytest from homeassistant import setup from homeassistant.components import lock -from homeassistant.components.lock import LockState +from homeassistant.components.lock import LockEntityFeature, LockState from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -15,6 +15,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall +from tests.common import assert_setup_component + OPTIMISTIC_LOCK_CONFIG = { "platform": "template", "lock": { @@ -718,3 +720,50 @@ async def test_unique_id(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.states.async_all("lock")) == 1 + + +async def test_emtpy_action_config(hass: HomeAssistant) -> None: + """Test configuration with empty script.""" + with assert_setup_component(1, lock.DOMAIN): + assert await setup.async_setup_component( + hass, + lock.DOMAIN, + { + lock.DOMAIN: { + "platform": "template", + "value_template": "{{ 0 == 1 }}", + "lock": [], + "unlock": [], + "open": [], + "name": "test_template_lock", + "optimistic": True, + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("lock.test_template_lock") + 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("lock.test_template_lock") + 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("lock.test_template_lock") + assert state.state == LockState.LOCKED diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index f73a943e752..5201541e2e0 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -1,8 +1,12 @@ """The tests for the Template number platform.""" +from typing import Any + +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import setup +from homeassistant.components import number, template from homeassistant.components.input_number import ( ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE, DOMAIN as INPUT_NUMBER_DOMAIN, @@ -18,6 +22,7 @@ from homeassistant.components.number import ( ) from homeassistant.components.template import DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, CONF_ENTITY_ID, CONF_UNIT_OF_MEASUREMENT, @@ -25,10 +30,14 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import ConfigurationStyle from tests.common import MockConfigEntry, assert_setup_component, async_capture_events -_TEST_NUMBER = "number.template_number" +_TEST_OBJECT_ID = "template_number" +_TEST_NUMBER = f"number.{_TEST_OBJECT_ID}" # Represent for number's value _VALUE_INPUT_NUMBER = "input_number.value" # Represent for number's minimum @@ -50,6 +59,38 @@ _VALUE_INPUT_NUMBER_CONFIG = { } +async def async_setup_modern_format( + hass: HomeAssistant, count: int, number_config: dict[str, Any] +) -> None: + """Do setup of number integration via new format.""" + config = {"template": {"number": number_config}} + + 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 +async def setup_number( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + number_config: dict[str, Any], +) -> None: + """Do setup of number integration.""" + if style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, count, {"name": _TEST_OBJECT_ID, **number_config} + ) + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -565,3 +606,36 @@ async def test_device_id( template_entity = entity_registry.async_get("number.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "state": "{{ 1 }}", + "set_value": [], + "step": "{{ 1 }}", + "optimistic": True, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ + ConfigurationStyle.MODERN, + ], +) +async def test_empty_action_config(hass: HomeAssistant, setup_number) -> None: + """Test configuration with empty script.""" + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: _TEST_NUMBER, "value": 4}, + blocking=True, + ) + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 4 diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 59ab45aeb36..b2bc56af44a 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -1,8 +1,12 @@ """The tests for the Template select platform.""" +from typing import Any + +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import setup +from homeassistant.components import select, template from homeassistant.components.input_select import ( ATTR_OPTION as INPUT_SELECT_ATTR_OPTION, ATTR_OPTIONS as INPUT_SELECT_ATTR_OPTIONS, @@ -17,17 +21,53 @@ from homeassistant.components.select import ( SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION, ) from homeassistant.components.template import DOMAIN -from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import ConfigurationStyle from tests.common import MockConfigEntry, assert_setup_component, async_capture_events -_TEST_SELECT = "select.template_select" +_TEST_OBJECT_ID = "template_select" +_TEST_SELECT = f"select.{_TEST_OBJECT_ID}" # Represent for select's current_option _OPTION_INPUT_SELECT = "input_select.option" +async def async_setup_modern_format( + hass: HomeAssistant, count: int, select_config: dict[str, Any] +) -> None: + """Do setup of select integration via new format.""" + config = {"template": {"select": select_config}} + + 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 +async def setup_select( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + select_config: dict[str, Any], +) -> None: + """Do setup of select integration.""" + if style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, count, {"name": _TEST_OBJECT_ID, **select_config} + ) + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -527,3 +567,36 @@ async def test_device_id( template_entity = entity_registry.async_get("select.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("count", "select_config"), + [ + ( + 1, + { + "state": "{{ 'b' }}", + "select_option": [], + "options": "{{ ['a', 'b'] }}", + "optimistic": True, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ + ConfigurationStyle.MODERN, + ], +) +async def test_empty_action_config(hass: HomeAssistant, setup_select) -> None: + """Test configuration with empty script.""" + await hass.services.async_call( + select.DOMAIN, + select.SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: _TEST_SELECT, "option": "a"}, + blocking=True, + ) + + state = hass.states.get(_TEST_SELECT) + assert state.state == "a" diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index d8877851efe..43db93ac146 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -981,3 +981,49 @@ async def test_device_id( template_entity = entity_registry.async_get("switch.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "switch_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + TEST_OBJECT_ID: { + "turn_on": [], + "turn_off": [], + }, + }, + ), + ( + ConfigurationStyle.MODERN, + { + "name": TEST_OBJECT_ID, + "turn_on": [], + "turn_off": [], + }, + ), + ], +) +async def test_empty_action_config(hass: HomeAssistant, setup_switch) -> None: + """Test configuration with empty script.""" + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 6053a2bd9ec..cc5bc9b39e3 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -1,18 +1,29 @@ """The tests for the Template vacuum platform.""" +from typing import Any + import pytest from homeassistant import setup -from homeassistant.components.vacuum import ATTR_BATTERY_LEVEL, VacuumActivity +from homeassistant.components import vacuum +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, + VacuumActivity, + VacuumEntityFeature, +) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.setup import async_setup_component + +from .conftest import ConfigurationStyle from tests.common import assert_setup_component from tests.components.vacuum import common -_TEST_VACUUM = "vacuum.test_vacuum" +_TEST_OBJECT_ID = "test_vacuum" +_TEST_VACUUM = f"vacuum.{_TEST_OBJECT_ID}" _STATE_INPUT_SELECT = "input_select.state" _SPOT_CLEANING_INPUT_BOOLEAN = "input_boolean.spot_cleaning" _LOCATING_INPUT_BOOLEAN = "input_boolean.locating" @@ -20,6 +31,50 @@ _FAN_SPEED_INPUT_SELECT = "input_select.fan_speed" _BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level" +async def async_setup_legacy_format( + hass: HomeAssistant, count: int, vacuum_config: dict[str, Any] +) -> None: + """Do setup of number integration via new format.""" + config = {"vacuum": {"platform": "template", "vacuums": vacuum_config}} + + with assert_setup_component(count, vacuum.DOMAIN): + assert await async_setup_component( + hass, + vacuum.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +@pytest.fixture +async def setup_vacuum( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + vacuum_config: dict[str, Any], +) -> None: + """Do setup of number integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format(hass, count, vacuum_config) + + +@pytest.fixture +async def setup_test_vacuum_with_extra_config( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + vacuum_config: dict[str, Any], + extra_config: dict[str, Any], +) -> None: + """Do setup of number integration.""" + config = {_TEST_OBJECT_ID: {**vacuum_config, **extra_config}} + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format(hass, count, config) + + @pytest.mark.parametrize(("count", "domain"), [(1, "vacuum")]) @pytest.mark.parametrize( ("parm1", "parm2", "config"), @@ -697,3 +752,71 @@ async def _register_components(hass: HomeAssistant) -> None: await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "vacuum_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "start": [], + }, + ), + ], +) +@pytest.mark.parametrize( + ("extra_config", "supported_features"), + [ + ( + { + "pause": [], + }, + VacuumEntityFeature.PAUSE, + ), + ( + { + "stop": [], + }, + VacuumEntityFeature.STOP, + ), + ( + { + "return_to_base": [], + }, + VacuumEntityFeature.RETURN_HOME, + ), + ( + { + "clean_spot": [], + }, + VacuumEntityFeature.CLEAN_SPOT, + ), + ( + { + "locate": [], + }, + VacuumEntityFeature.LOCATE, + ), + ( + { + "set_fan_speed": [], + }, + VacuumEntityFeature.FAN_SPEED, + ), + ], +) +async def test_empty_action_config( + hass: HomeAssistant, + supported_features: VacuumEntityFeature, + setup_test_vacuum_with_extra_config, +) -> None: + """Test configuration with empty script.""" + await common.async_start(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + state = hass.states.get(_TEST_VACUUM) + assert state.attributes["supported_features"] == ( + VacuumEntityFeature.STATE | VacuumEntityFeature.START | supported_features + )