Fix empty actions (#142292)

* Apply fix

* Add tests for alarm button cover lock

* update light

* add number tests

* test select

* add switch tests

* test vacuum

* update lock test
This commit is contained in:
Petro31 2025-04-04 16:17:52 -04:00 committed by Franck Nijhof
parent 8d62cb60a6
commit c25f26a290
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
18 changed files with 613 additions and 19 deletions

View File

@ -214,7 +214,8 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
), ),
(CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER), (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.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature self._attr_supported_features |= supported_feature

View File

@ -120,7 +120,8 @@ class TemplateButtonEntity(TemplateEntity, ButtonEntity):
"""Initialize the button.""" """Initialize the button."""
super().__init__(hass, config=config, unique_id=unique_id) super().__init__(hass, config=config, unique_id=unique_id)
assert self._attr_name is not None 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.add_script(CONF_PRESS, action, self._attr_name, DOMAIN)
self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._attr_state = None self._attr_state = None

View File

@ -172,7 +172,8 @@ class CoverTemplate(TemplateEntity, CoverEntity):
(POSITION_ACTION, CoverEntityFeature.SET_POSITION), (POSITION_ACTION, CoverEntityFeature.SET_POSITION),
(TILT_ACTION, TILT_FEATURES), (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.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature self._attr_supported_features |= supported_feature

View File

@ -157,7 +157,8 @@ class TemplateFan(TemplateEntity, FanEntity):
CONF_SET_OSCILLATING_ACTION, CONF_SET_OSCILLATING_ACTION,
CONF_SET_DIRECTION_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.add_script(action_id, action_config, name, DOMAIN)
self._state: bool | None = False self._state: bool | None = False

View File

@ -296,7 +296,8 @@ class LightTemplate(TemplateEntity, LightEntity):
self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION) self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION)
for action_id in (CONF_ON_ACTION, CONF_OFF_ACTION, CONF_EFFECT_ACTION): 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.add_script(action_id, action_config, name, DOMAIN)
self._state = False self._state = False
@ -323,7 +324,8 @@ class LightTemplate(TemplateEntity, LightEntity):
(CONF_RGBW_ACTION, ColorMode.RGBW), (CONF_RGBW_ACTION, ColorMode.RGBW),
(CONF_RGBWW_ACTION, ColorMode.RGBWW), (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) self.add_script(action_id, action_config, name, DOMAIN)
color_modes.add(color_mode) color_modes.add(color_mode)
self._supported_color_modes = filter_supported_color_modes(color_modes) 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._color_mode = next(iter(self._supported_color_modes))
self._attr_supported_features = LightEntityFeature(0) 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 self._attr_supported_features |= LightEntityFeature.EFFECT
if self._supports_transition is True: if self._supports_transition is True:
self._attr_supported_features |= LightEntityFeature.TRANSITION self._attr_supported_features |= LightEntityFeature.TRANSITION

View File

@ -98,7 +98,8 @@ class TemplateLock(TemplateEntity, LockEntity):
(CONF_UNLOCK, 0), (CONF_UNLOCK, 0),
(CONF_OPEN, LockEntityFeature.OPEN), (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.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature self._attr_supported_features |= supported_feature
self._code_format_template = config.get(CONF_CODE_FORMAT_TEMPLATE) self._code_format_template = config.get(CONF_CODE_FORMAT_TEMPLATE)

View File

@ -141,7 +141,8 @@ class TemplateSelect(TemplateEntity, SelectEntity):
super().__init__(hass, config=config, unique_id=unique_id) super().__init__(hass, config=config, unique_id=unique_id)
assert self._attr_name is not None assert self._attr_name is not None
self._value_template = config[CONF_STATE] 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.add_script(CONF_SELECT_OPTION, select_option, self._attr_name, DOMAIN)
self._options_template = config[ATTR_OPTIONS] self._options_template = config[ATTR_OPTIONS]
self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC, False) self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC, False)
@ -197,7 +198,8 @@ class TriggerSelectEntity(TriggerEntity, SelectEntity):
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(hass, coordinator, config) 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( self.add_script(
CONF_SELECT_OPTION, CONF_SELECT_OPTION,
select_option, select_option,

View File

@ -226,9 +226,10 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
assert name is not None assert name is not None
self._template = config.get(CONF_STATE) 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) 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.add_script(CONF_TURN_OFF, off_action, name, DOMAIN)
self._state: bool | None = False self._state: bool | None = False

View File

@ -158,7 +158,8 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
(SERVICE_LOCATE, VacuumEntityFeature.LOCATE), (SERVICE_LOCATE, VacuumEntityFeature.LOCATE),
(SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED), (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.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature self._attr_supported_features |= supported_feature

View File

@ -82,6 +82,15 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = {
"data": {"code": "{{ this.entity_id }}"}, "data": {"code": "{{ this.entity_id }}"},
}, },
} }
EMPTY_ACTIONS = {
"arm_away": [],
"arm_home": [],
"arm_night": [],
"arm_vacation": [],
"arm_custom_bypass": [],
"disarm": [],
"trigger": [],
}
TEMPLATE_ALARM_CONFIG = { TEMPLATE_ALARM_CONFIG = {
@ -173,6 +182,12 @@ async def test_setup_config_entry(
"panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG}, "panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG},
} }
}, },
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": EMPTY_ACTIONS},
}
},
], ],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.usefixtures("start_ha")

View File

@ -93,6 +93,45 @@ async def test_missing_optional_config(hass: HomeAssistant) -> None:
_verify(hass, STATE_UNKNOWN) _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: async def test_missing_required_keys(hass: HomeAssistant) -> None:
"""Test: missing required fields will fail.""" """Test: missing required fields will fail."""
with assert_setup_component(0, "template"): with assert_setup_component(0, "template"):

View File

@ -9,6 +9,7 @@ from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
ATTR_TILT_POSITION, ATTR_TILT_POSITION,
DOMAIN as COVER_DOMAIN, DOMAIN as COVER_DOMAIN,
CoverEntityFeature,
CoverState, CoverState,
) )
from homeassistant.const import ( from homeassistant.const import (
@ -28,6 +29,7 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.setup import async_setup_component
from tests.common import assert_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 len(hass.states.async_all()) == 1
assert "Template loop detected" not in caplog.text 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
)

View File

@ -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 @pytest.fixture
async def setup_light_with_effects( async def setup_light_with_effects(
hass: HomeAssistant, hass: HomeAssistant,
@ -2404,3 +2440,82 @@ async def test_nested_unique_id(
entry = entity_registry.async_get("light.test_b") entry = entity_registry.async_get("light.test_b")
assert entry assert entry
assert entry.unique_id == "x-b" 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

View File

@ -4,7 +4,7 @@ import pytest
from homeassistant import setup from homeassistant import setup
from homeassistant.components import lock from homeassistant.components import lock
from homeassistant.components.lock import LockState from homeassistant.components.lock import LockEntityFeature, LockState
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -15,6 +15,8 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from tests.common import assert_setup_component
OPTIMISTIC_LOCK_CONFIG = { OPTIMISTIC_LOCK_CONFIG = {
"platform": "template", "platform": "template",
"lock": { "lock": {
@ -718,3 +720,50 @@ async def test_unique_id(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all("lock")) == 1 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

View File

@ -1,8 +1,12 @@
"""The tests for the Template number platform.""" """The tests for the Template number platform."""
from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant import setup from homeassistant import setup
from homeassistant.components import number, template
from homeassistant.components.input_number import ( from homeassistant.components.input_number import (
ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE, ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE,
DOMAIN as INPUT_NUMBER_DOMAIN, DOMAIN as INPUT_NUMBER_DOMAIN,
@ -18,6 +22,7 @@ from homeassistant.components.number import (
) )
from homeassistant.components.template import DOMAIN from homeassistant.components.template import DOMAIN
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ICON, ATTR_ICON,
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
@ -25,10 +30,14 @@ from homeassistant.const import (
) )
from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.core import Context, HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er 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 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 # Represent for number's value
_VALUE_INPUT_NUMBER = "input_number.value" _VALUE_INPUT_NUMBER = "input_number.value"
# Represent for number's minimum # 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( async def test_setup_config_entry(
hass: HomeAssistant, hass: HomeAssistant,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
@ -565,3 +606,36 @@ async def test_device_id(
template_entity = entity_registry.async_get("number.my_template") template_entity = entity_registry.async_get("number.my_template")
assert template_entity is not None assert template_entity is not None
assert template_entity.device_id == device_entry.id 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

View File

@ -1,8 +1,12 @@
"""The tests for the Template select platform.""" """The tests for the Template select platform."""
from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant import setup from homeassistant import setup
from homeassistant.components import select, template
from homeassistant.components.input_select import ( from homeassistant.components.input_select import (
ATTR_OPTION as INPUT_SELECT_ATTR_OPTION, ATTR_OPTION as INPUT_SELECT_ATTR_OPTION,
ATTR_OPTIONS as INPUT_SELECT_ATTR_OPTIONS, ATTR_OPTIONS as INPUT_SELECT_ATTR_OPTIONS,
@ -17,17 +21,53 @@ from homeassistant.components.select import (
SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION,
) )
from homeassistant.components.template import DOMAIN 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.core import Context, HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er 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 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 # Represent for select's current_option
_OPTION_INPUT_SELECT = "input_select.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( async def test_setup_config_entry(
hass: HomeAssistant, hass: HomeAssistant,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
@ -527,3 +567,36 @@ async def test_device_id(
template_entity = entity_registry.async_get("select.my_template") template_entity = entity_registry.async_get("select.my_template")
assert template_entity is not None assert template_entity is not None
assert template_entity.device_id == device_entry.id 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"

View File

@ -981,3 +981,49 @@ async def test_device_id(
template_entity = entity_registry.async_get("switch.my_template") template_entity = entity_registry.async_get("switch.my_template")
assert template_entity is not None assert template_entity is not None
assert template_entity.device_id == device_entry.id 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

View File

@ -1,18 +1,29 @@
"""The tests for the Template vacuum platform.""" """The tests for the Template vacuum platform."""
from typing import Any
import pytest import pytest
from homeassistant import setup 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.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_component import async_update_entity 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.common import assert_setup_component
from tests.components.vacuum import common 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" _STATE_INPUT_SELECT = "input_select.state"
_SPOT_CLEANING_INPUT_BOOLEAN = "input_boolean.spot_cleaning" _SPOT_CLEANING_INPUT_BOOLEAN = "input_boolean.spot_cleaning"
_LOCATING_INPUT_BOOLEAN = "input_boolean.locating" _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" _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(("count", "domain"), [(1, "vacuum")])
@pytest.mark.parametrize( @pytest.mark.parametrize(
("parm1", "parm2", "config"), ("parm1", "parm2", "config"),
@ -697,3 +752,71 @@ async def _register_components(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_start() await hass.async_start()
await hass.async_block_till_done() 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
)