Add trigger based fan entities to template integration (#145497)

* Add trigger based fan entities to template integration

* more changes

* add tests

* update doc strings

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Petro31 2025-06-23 15:20:55 -04:00 committed by GitHub
parent c29879274a
commit b4fe6f3843
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 384 additions and 121 deletions

View File

@ -158,7 +158,6 @@ CONFIG_SECTION_SCHEMA = vol.All(
),
ensure_domains_do_not_have_trigger_or_action(
DOMAIN_BUTTON,
DOMAIN_FAN,
),
)

View File

@ -15,6 +15,7 @@ from homeassistant.components.fan import (
ATTR_PRESET_MODE,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
DOMAIN as FAN_DOMAIN,
ENTITY_ID_FORMAT,
FanEntity,
FanEntityFeature,
@ -38,6 +39,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_OBJECT_ID, DOMAIN
from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity
from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
@ -46,6 +48,7 @@ from .template_entity import (
make_template_entity_common_modern_schema,
rewrite_common_legacy_to_modern_conf,
)
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
@ -193,6 +196,13 @@ async def async_setup_platform(
)
return
if "coordinator" in discovery_info:
async_add_entities(
TriggerFanEntity(hass, discovery_info["coordinator"], config)
for config in discovery_info["entities"]
)
return
_async_create_template_tracking_entities(
async_add_entities,
hass,
@ -228,7 +238,11 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
self._preset_modes: list[str] | None = config.get(CONF_PRESET_MODES)
self._attr_assumed_state = self._template is None
def _register_scripts(
self._attr_supported_features |= (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
)
def _iterate_scripts(
self, config: dict[str, Any]
) -> Generator[tuple[str, Sequence[dict[str, Any]], FanEntityFeature | int]]:
for action_id, supported_feature in (
@ -492,10 +506,7 @@ class TemplateFan(TemplateEntity, AbstractTemplateFan):
if TYPE_CHECKING:
assert name is not None
self._attr_supported_features |= (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
)
for action_id, action_config, supported_feature in self._register_scripts(
for action_id, action_config, supported_feature in self._iterate_scripts(
config
):
self.add_script(action_id, action_config, name, DOMAIN)
@ -551,3 +562,67 @@ class TemplateFan(TemplateEntity, AbstractTemplateFan):
none_on_template_error=True,
)
super()._async_setup_templates()
class TriggerFanEntity(TriggerEntity, AbstractTemplateFan):
"""Fan entity based on trigger data."""
domain = FAN_DOMAIN
def __init__(
self,
hass: HomeAssistant,
coordinator: TriggerUpdateCoordinator,
config: ConfigType,
) -> None:
"""Initialize the entity."""
TriggerEntity.__init__(self, hass, coordinator, config)
AbstractTemplateFan.__init__(self, config)
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
for action_id, action_config, supported_feature in self._iterate_scripts(
config
):
self.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature
for key in (
CONF_STATE,
CONF_PRESET_MODE,
CONF_PERCENTAGE,
CONF_OSCILLATING,
CONF_DIRECTION,
):
if isinstance(config.get(key), template.Template):
self._to_render_simple.append(key)
self._parse_result.add(key)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update of the data."""
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False
for key, updater in (
(CONF_STATE, self._handle_state),
(CONF_PRESET_MODE, self._update_preset_mode),
(CONF_PERCENTAGE, self._update_percentage),
(CONF_OSCILLATING, self._update_oscillating),
(CONF_DIRECTION, self._update_direction),
):
if (rendered := self._rendered.get(key)) is not None:
updater(rendered)
write_ha_state = True
if len(self._rendered) > 0:
# In case any non optimistic template
write_ha_state = True
if write_ha_state:
self.async_set_context(self.coordinator.data["context"])
self.async_write_ha_state()

View File

@ -28,11 +28,30 @@ from tests.components.fan import common
TEST_OBJECT_ID = "test_fan"
TEST_ENTITY_ID = f"fan.{TEST_OBJECT_ID}"
# Represent for fan's state
_STATE_INPUT_BOOLEAN = "input_boolean.state"
# Represent for fan's state
# Represent for fan's percent
_STATE_TEST_SENSOR = "sensor.test_sensor"
# Represent for fan's availability
_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state"
TEST_STATE_TRIGGER = {
"trigger": {
"trigger": "state",
"entity_id": [
TEST_ENTITY_ID,
_STATE_INPUT_BOOLEAN,
_STATE_AVAILABILITY_BOOLEAN,
_STATE_TEST_SENSOR,
],
},
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
"action": [
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
],
}
OPTIMISTIC_ON_OFF_ACTIONS = {
"turn_on": {
"service": "test.automation",
@ -177,61 +196,22 @@ async def async_setup_modern_format(
await hass.async_block_till_done()
async def async_setup_legacy_named_fan(
async def async_setup_trigger_format(
hass: HomeAssistant, count: int, fan_config: dict[str, Any]
):
"""Do setup of a named fan via legacy format."""
await async_setup_legacy_format(hass, count, {TEST_OBJECT_ID: fan_config})
async def async_setup_modern_named_fan(
hass: HomeAssistant, count: int, fan_config: dict[str, Any]
):
"""Do setup of a named fan via legacy format."""
await async_setup_modern_format(hass, count, {"name": TEST_OBJECT_ID, **fan_config})
async def async_setup_legacy_format_with_attribute(
hass: HomeAssistant,
count: int,
attribute: str,
attribute_template: str,
extra_config: dict,
) -> None:
"""Do setup of a legacy fan that has a single templated attribute."""
extra = {attribute: attribute_template} if attribute and attribute_template else {}
await async_setup_legacy_format(
hass,
count,
{
TEST_OBJECT_ID: {
**extra_config,
"value_template": "{{ 1 == 1 }}",
**extra,
}
},
)
"""Do setup of fan integration via trigger format."""
config = {"template": {"fan": fan_config, **TEST_STATE_TRIGGER}}
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
config,
)
async def async_setup_modern_format_with_attribute(
hass: HomeAssistant,
count: int,
attribute: str,
attribute_template: str,
extra_config: dict,
) -> None:
"""Do setup of a modern fan that has a single templated attribute."""
extra = {attribute: attribute_template} if attribute and attribute_template else {}
await async_setup_modern_format(
hass,
count,
{
"name": TEST_OBJECT_ID,
**extra_config,
"state": "{{ 1 == 1 }}",
**extra,
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
@pytest.fixture
@ -246,6 +226,8 @@ async def setup_fan(
await async_setup_legacy_format(hass, count, fan_config)
elif style == ConfigurationStyle.MODERN:
await async_setup_modern_format(hass, count, fan_config)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(hass, count, fan_config)
@pytest.fixture
@ -257,9 +239,15 @@ async def setup_named_fan(
) -> None:
"""Do setup of fan integration."""
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_named_fan(hass, count, fan_config)
await async_setup_legacy_format(hass, count, {TEST_OBJECT_ID: fan_config})
elif style == ConfigurationStyle.MODERN:
await async_setup_modern_named_fan(hass, count, fan_config)
await async_setup_modern_format(
hass, count, {"name": TEST_OBJECT_ID, **fan_config}
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass, count, {"name": TEST_OBJECT_ID, **fan_config}
)
@pytest.fixture
@ -290,6 +278,15 @@ async def setup_state_fan(
"state": state_template,
},
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass,
count,
{
**NAMED_ON_OFF_ACTIONS,
"state": state_template,
},
)
@pytest.fixture
@ -309,6 +306,10 @@ async def setup_test_fan_with_extra_config(
await async_setup_modern_format(
hass, count, {"name": TEST_OBJECT_ID, **fan_config, **extra_config}
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass, count, {"name": TEST_OBJECT_ID, **fan_config, **extra_config}
)
@pytest.fixture
@ -320,12 +321,35 @@ async def setup_optimistic_fan_attribute(
) -> None:
"""Do setup of a non-optimistic fan with an optimistic attribute."""
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_format_with_attribute(
hass, count, "", "", extra_config
await async_setup_legacy_format(
hass,
count,
{
TEST_OBJECT_ID: {
**extra_config,
"value_template": "{{ 1 == 1 }}",
}
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_modern_format_with_attribute(
hass, count, "", "", extra_config
await async_setup_modern_format(
hass,
count,
{
"name": TEST_OBJECT_ID,
**extra_config,
"state": "{{ 1 == 1 }}",
},
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass,
count,
{
"name": TEST_OBJECT_ID,
**extra_config,
"state": "{{ 1 == 1 }}",
},
)
@ -365,11 +389,23 @@ async def setup_single_attribute_state_fan(
**extra_config,
},
)
elif style == ConfigurationStyle.TRIGGER:
await async_setup_trigger_format(
hass,
count,
{
**NAMED_ON_OFF_ACTIONS,
"state": state_template,
**extra,
**extra_config,
},
)
@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 'on' }}")])
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_missing_optional_config(hass: HomeAssistant) -> None:
@ -379,7 +415,8 @@ async def test_missing_optional_config(hass: HomeAssistant) -> None:
@pytest.mark.parametrize("count", [0])
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
"fan_config",
@ -404,7 +441,8 @@ async def test_wrong_template_config(hass: HomeAssistant) -> None:
("count", "state_template"), [(1, "{{ is_state('input_boolean.state', 'on') }}")]
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_state_template(hass: HomeAssistant) -> None:
@ -433,7 +471,8 @@ async def test_state_template(hass: HomeAssistant) -> None:
],
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_state_template_states(hass: HomeAssistant, expected: str) -> None:
@ -442,29 +481,28 @@ async def test_state_template_states(hass: HomeAssistant, expected: str) -> None
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config"),
("count", "state_template", "attribute_template", "extra_config", "attribute"),
[
(
1,
"{{ 1 == 1}}",
"{% if states.input_boolean.state.state %}/local/switch.png{% endif %}",
"{% if is_state('sensor.test_sensor', 'on') %}/local/switch.png{% endif %}",
{},
"picture",
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.MODERN, "picture"),
],
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_picture_template(hass: HomeAssistant) -> None:
"""Test picture template."""
state = hass.states.get(TEST_ENTITY_ID)
assert state.attributes.get("entity_picture") in ("", None)
assert state.attributes.get("entity_picture") == ""
hass.states.async_set(_STATE_INPUT_BOOLEAN, STATE_ON)
hass.states.async_set(_STATE_TEST_SENSOR, STATE_ON)
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
@ -472,27 +510,26 @@ async def test_picture_template(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config"),
("count", "state_template", "attribute_template", "extra_config", "attribute"),
[
(
1,
"{{ 1 == 1}}",
"{% if states.input_boolean.state.state %}mdi:eye{% endif %}",
{},
"icon",
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.MODERN, "icon"),
],
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_icon_template(hass: HomeAssistant) -> None:
"""Test icon template."""
state = hass.states.get(TEST_ENTITY_ID)
assert state.attributes.get("icon") in ("", None)
assert state.attributes.get("icon") == ""
hass.states.async_set(_STATE_INPUT_BOOLEAN, STATE_ON)
await hass.async_block_till_done()
@ -507,7 +544,7 @@ async def test_icon_template(hass: HomeAssistant) -> None:
(
1,
"{{ 1 == 1 }}",
"{{ states('sensor.percentage') }}",
"{{ states('sensor.test_sensor') }}",
PERCENTAGE_ACTION,
)
],
@ -517,6 +554,7 @@ async def test_icon_template(hass: HomeAssistant) -> None:
[
(ConfigurationStyle.LEGACY, "percentage_template"),
(ConfigurationStyle.MODERN, "percentage"),
(ConfigurationStyle.TRIGGER, "percentage"),
],
)
@pytest.mark.parametrize(
@ -534,7 +572,7 @@ async def test_percentage_template(
hass: HomeAssistant, percent: str, expected: int, calls: list[ServiceCall]
) -> None:
"""Test templates with fan percentages from other entities."""
hass.states.async_set("sensor.percentage", percent)
hass.states.async_set(_STATE_TEST_SENSOR, percent)
await hass.async_block_till_done()
_verify(hass, STATE_ON, expected, None, None, None)
@ -545,7 +583,7 @@ async def test_percentage_template(
(
1,
"{{ 1 == 1 }}",
"{{ states('sensor.preset_mode') }}",
"{{ states('sensor.test_sensor') }}",
{"preset_modes": ["auto", "smart"], **PRESET_MODE_ACTION},
)
],
@ -555,6 +593,7 @@ async def test_percentage_template(
[
(ConfigurationStyle.LEGACY, "preset_mode_template"),
(ConfigurationStyle.MODERN, "preset_mode"),
(ConfigurationStyle.TRIGGER, "preset_mode"),
],
)
@pytest.mark.parametrize(
@ -571,7 +610,7 @@ async def test_preset_mode_template(
hass: HomeAssistant, preset_mode: str, expected: int
) -> None:
"""Test preset_mode template."""
hass.states.async_set("sensor.preset_mode", preset_mode)
hass.states.async_set(_STATE_TEST_SENSOR, preset_mode)
await hass.async_block_till_done()
_verify(hass, STATE_ON, None, None, None, expected)
@ -582,7 +621,7 @@ async def test_preset_mode_template(
(
1,
"{{ 1 == 1 }}",
"{{ is_state('binary_sensor.oscillating', 'on') }}",
"{{ is_state('sensor.test_sensor', 'on') }}",
OSCILLATE_ACTION,
)
],
@ -592,6 +631,7 @@ async def test_preset_mode_template(
[
(ConfigurationStyle.LEGACY, "oscillating_template"),
(ConfigurationStyle.MODERN, "oscillating"),
(ConfigurationStyle.TRIGGER, "oscillating"),
],
)
@pytest.mark.parametrize(
@ -606,7 +646,7 @@ async def test_oscillating_template(
hass: HomeAssistant, oscillating: str, expected: bool | None
) -> None:
"""Test oscillating template."""
hass.states.async_set("binary_sensor.oscillating", oscillating)
hass.states.async_set(_STATE_TEST_SENSOR, oscillating)
await hass.async_block_till_done()
_verify(hass, STATE_ON, None, expected, None, None)
@ -617,7 +657,7 @@ async def test_oscillating_template(
(
1,
"{{ 1 == 1 }}",
"{{ states('sensor.direction') }}",
"{{ states('sensor.test_sensor') }}",
DIRECTION_ACTION,
)
],
@ -627,6 +667,7 @@ async def test_oscillating_template(
[
(ConfigurationStyle.LEGACY, "direction_template"),
(ConfigurationStyle.MODERN, "direction"),
(ConfigurationStyle.TRIGGER, "direction"),
],
)
@pytest.mark.parametrize(
@ -641,7 +682,7 @@ async def test_direction_template(
hass: HomeAssistant, direction: str, expected: bool | None
) -> None:
"""Test direction template."""
hass.states.async_set("sensor.direction", direction)
hass.states.async_set(_STATE_TEST_SENSOR, direction)
await hass.async_block_till_done()
_verify(hass, STATE_ON, None, None, expected, None)
@ -674,6 +715,17 @@ async def test_direction_template(
"turn_off": {"service": "script.fan_off"},
},
),
(
ConfigurationStyle.TRIGGER,
{
"availability": ("{{ is_state('availability_boolean.state', 'on') }}"),
"state": "{{ 'on' }}",
"oscillating": "{{ 1 == 1 }}",
"direction": "{{ 'forward' }}",
"turn_on": {"service": "script.fan_on"},
"turn_off": {"service": "script.fan_off"},
},
),
],
)
@pytest.mark.usefixtures("setup_named_fan")
@ -707,6 +759,14 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
},
[STATE_OFF, None, None, None],
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'unavailable' }}",
**OPTIMISTIC_ON_OFF_ACTIONS,
},
[STATE_OFF, None, None, None],
),
(
ConfigurationStyle.LEGACY,
{
@ -733,6 +793,19 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
},
[STATE_ON, 0, None, None],
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
"percentage": "{{ 0 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating": "{{ 'unavailable' }}",
**OSCILLATE_ACTION,
"direction": "{{ 'unavailable' }}",
**DIRECTION_ACTION,
},
[STATE_ON, 0, None, None],
),
(
ConfigurationStyle.LEGACY,
{
@ -759,6 +832,19 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
},
[STATE_ON, 66, True, DIRECTION_FORWARD],
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
"percentage": "{{ 66 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating": "{{ 1 == 1 }}",
**OSCILLATE_ACTION,
"direction": "{{ 'forward' }}",
**DIRECTION_ACTION,
},
[STATE_ON, 66, True, DIRECTION_FORWARD],
),
(
ConfigurationStyle.LEGACY,
{
@ -785,6 +871,19 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
},
[STATE_OFF, 0, None, None],
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'abc' }}",
"percentage": "{{ 0 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating": "{{ 'xyz' }}",
**OSCILLATE_ACTION,
"direction": "{{ 'right' }}",
**DIRECTION_ACTION,
},
[STATE_OFF, 0, None, None],
),
],
)
@pytest.mark.usefixtures("setup_named_fan")
@ -821,16 +920,33 @@ async def test_template_with_unavailable_entities(hass: HomeAssistant, states) -
"turn_off": {"service": "script.fan_off"},
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
"availability": "{{ x - 12 }}",
"preset_mode": ("{{ states('input_select.preset_mode') }}"),
"oscillating": "{{ states('input_select.osc') }}",
"direction": "{{ states('input_select.direction') }}",
"turn_on": {"service": "script.fan_on"},
"turn_off": {"service": "script.fan_off"},
},
),
],
)
@pytest.mark.usefixtures("setup_named_fan")
async def test_invalid_availability_template_keeps_component_available(
hass: HomeAssistant, caplog_setup_text
hass: HomeAssistant, caplog_setup_text, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that an invalid availability keeps the device available."""
assert hass.states.get("fan.test_fan").state != STATE_UNAVAILABLE
assert "TemplateError" in caplog_setup_text
assert "x" in caplog_setup_text
# Ensure trigger entities update.
hass.states.async_set(_STATE_INPUT_BOOLEAN, STATE_ON)
await hass.async_block_till_done()
assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE
err = "'x' is undefined"
assert err in caplog_setup_text or err in caplog.text
@pytest.mark.parametrize(("count", "extra_config"), [(1, OPTIMISTIC_ON_OFF_ACTIONS)])
@ -849,6 +965,12 @@ async def test_invalid_availability_template_keeps_component_available(
"state": "{{ 'off' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'off' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -899,6 +1021,12 @@ async def test_on_off(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"state": "{{ 'off' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'off' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -981,6 +1109,12 @@ async def test_on_with_extra_attributes(
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1008,6 +1142,12 @@ async def test_set_invalid_direction_from_initial_stage(hass: HomeAssistant) ->
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1045,6 +1185,12 @@ async def test_set_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1082,6 +1228,12 @@ async def test_set_direction(hass: HomeAssistant, calls: list[ServiceCall]) -> N
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1117,6 +1269,12 @@ async def test_set_invalid_direction(
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1154,6 +1312,12 @@ async def test_preset_modes(hass: HomeAssistant, calls: list[ServiceCall]) -> No
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1198,6 +1362,12 @@ async def test_set_percentage(hass: HomeAssistant, calls: list[ServiceCall]) ->
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1236,7 +1406,7 @@ async def test_increase_decrease_speed(
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN],
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_named_fan")
async def test_optimistic_state(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
@ -1307,7 +1477,8 @@ async def test_optimistic_state(hass: HomeAssistant, calls: list[ServiceCall]) -
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("extra_config", "attribute", "action", "verify_attr", "coro", "value"),
@ -1383,6 +1554,12 @@ async def test_optimistic_attributes(
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1420,6 +1597,12 @@ async def test_increase_decrease_speed_default_speed_count(
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1451,6 +1634,12 @@ async def test_set_invalid_osc_from_initial_state(
"state": "{{ 'on' }}",
},
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'on' }}",
},
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
@ -1474,24 +1663,37 @@ async def test_set_invalid_osc(hass: HomeAssistant, calls: list[ServiceCall]) ->
[
(
{
"test_template_cover_01": UNIQUE_ID_CONFIG,
"test_template_cover_02": UNIQUE_ID_CONFIG,
"test_template_fan_01": UNIQUE_ID_CONFIG,
"test_template_fan_02": UNIQUE_ID_CONFIG,
},
ConfigurationStyle.LEGACY,
),
(
[
{
"name": "test_template_cover_01",
"name": "test_template_fan_01",
**UNIQUE_ID_CONFIG,
},
{
"name": "test_template_cover_02",
"name": "test_template_fan_02",
**UNIQUE_ID_CONFIG,
},
],
ConfigurationStyle.MODERN,
),
(
[
{
"name": "test_template_fan_01",
**UNIQUE_ID_CONFIG,
},
{
"name": "test_template_fan_02",
**UNIQUE_ID_CONFIG,
},
],
ConfigurationStyle.TRIGGER,
),
],
)
@pytest.mark.usefixtures("setup_fan")
@ -1506,7 +1708,7 @@ async def test_unique_id(hass: HomeAssistant) -> None:
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN],
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("fan_config", "percentage_step"),
@ -1529,7 +1731,7 @@ async def test_speed_percentage_step(hass: HomeAssistant, percentage_step) -> No
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN],
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_named_fan")
async def test_preset_mode_supported_features(hass: HomeAssistant) -> None:
@ -1541,25 +1743,12 @@ async def test_preset_mode_supported_features(hass: HomeAssistant) -> None:
assert attributes.get("supported_features") & FanEntityFeature.PRESET_MODE
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("style", "fan_config"),
[
(
ConfigurationStyle.LEGACY,
{
"turn_on": [],
"turn_off": [],
},
),
(
ConfigurationStyle.MODERN,
{
"turn_on": [],
"turn_off": [],
},
),
],
("count", "fan_config"), [(1, {"turn_on": [], "turn_off": []})]
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("extra_config", "supported_features"),
@ -1590,10 +1779,10 @@ async def test_preset_mode_supported_features(hass: HomeAssistant) -> None:
),
],
)
@pytest.mark.usefixtures("setup_test_fan_with_extra_config")
async def test_empty_action_config(
hass: HomeAssistant,
supported_features: FanEntityFeature,
setup_test_fan_with_extra_config,
) -> None:
"""Test configuration with empty script."""
state = hass.states.get(TEST_ENTITY_ID)