Correct template fan optimistic mode and supported features (#142414)

This commit is contained in:
Petro31 2025-04-14 09:40:29 -04:00 committed by GitHub
parent aeca2842fe
commit 9b274a0bc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 398 additions and 145 deletions

View File

@ -149,17 +149,21 @@ class TemplateFan(TemplateEntity, FanEntity):
self._oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE) self._oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE)
self._direction_template = config.get(CONF_DIRECTION_TEMPLATE) self._direction_template = config.get(CONF_DIRECTION_TEMPLATE)
for action_id in ( self._attr_supported_features |= (
CONF_ON_ACTION, FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
CONF_OFF_ACTION, )
CONF_SET_PERCENTAGE_ACTION, for action_id, supported_feature in (
CONF_SET_PRESET_MODE_ACTION, (CONF_ON_ACTION, 0),
CONF_SET_OSCILLATING_ACTION, (CONF_OFF_ACTION, 0),
CONF_SET_DIRECTION_ACTION, (CONF_SET_PERCENTAGE_ACTION, FanEntityFeature.SET_SPEED),
(CONF_SET_PRESET_MODE_ACTION, FanEntityFeature.PRESET_MODE),
(CONF_SET_OSCILLATING_ACTION, FanEntityFeature.OSCILLATE),
(CONF_SET_DIRECTION_ACTION, FanEntityFeature.DIRECTION),
): ):
# Scripts can be an empty list, therefore we need to check for None # Scripts can be an empty list, therefore we need to check for None
if (action_config := config.get(action_id)) is not 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._state: bool | None = False self._state: bool | None = False
self._percentage: int | None = None self._percentage: int | None = None
@ -172,19 +176,6 @@ class TemplateFan(TemplateEntity, FanEntity):
# List of valid preset modes # List of valid preset modes
self._preset_modes: list[str] | None = config.get(CONF_PRESET_MODES) self._preset_modes: list[str] | None = config.get(CONF_PRESET_MODES)
if self._percentage_template:
self._attr_supported_features |= FanEntityFeature.SET_SPEED
if self._preset_mode_template and self._preset_modes:
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
if self._oscillating_template:
self._attr_supported_features |= FanEntityFeature.OSCILLATE
if self._direction_template:
self._attr_supported_features |= FanEntityFeature.DIRECTION
self._attr_supported_features |= (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
)
self._attr_assumed_state = self._template is None self._attr_assumed_state = self._template is None
@property @property
@ -270,6 +261,8 @@ class TemplateFan(TemplateEntity, FanEntity):
if self._template is None: if self._template is None:
self._state = percentage != 0 self._state = percentage != 0
if self._template is None or self._percentage_template is None:
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
@ -285,32 +278,39 @@ class TemplateFan(TemplateEntity, FanEntity):
if self._template is None: if self._template is None:
self._state = True self._state = True
if self._template is None or self._preset_mode_template is None:
self.async_write_ha_state() self.async_write_ha_state()
async def async_oscillate(self, oscillating: bool) -> None: async def async_oscillate(self, oscillating: bool) -> None:
"""Set oscillation of the fan.""" """Set oscillation of the fan."""
if (script := self._action_scripts.get(CONF_SET_OSCILLATING_ACTION)) is None:
return
self._oscillating = oscillating self._oscillating = oscillating
await self.async_run_script( if (
script, script := self._action_scripts.get(CONF_SET_OSCILLATING_ACTION)
run_variables={ATTR_OSCILLATING: self.oscillating}, ) is not None:
context=self._context, await self.async_run_script(
) script,
run_variables={ATTR_OSCILLATING: self.oscillating},
context=self._context,
)
if self._oscillating_template is None:
self.async_write_ha_state()
async def async_set_direction(self, direction: str) -> None: async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan.""" """Set the direction of the fan."""
if (script := self._action_scripts.get(CONF_SET_DIRECTION_ACTION)) is None:
return
if direction in _VALID_DIRECTIONS: if direction in _VALID_DIRECTIONS:
self._direction = direction self._direction = direction
await self.async_run_script( if (
script, script := self._action_scripts.get(CONF_SET_DIRECTION_ACTION)
run_variables={ATTR_DIRECTION: direction}, ) is not None:
context=self._context, await self.async_run_script(
) script,
run_variables={ATTR_DIRECTION: direction},
context=self._context,
)
if self._direction_template is None:
self.async_write_ha_state()
else: else:
_LOGGER.error( _LOGGER.error(
"Received invalid direction: %s for entity %s. Expected: %s", "Received invalid direction: %s for entity %s. Expected: %s",

View File

@ -1,9 +1,12 @@
"""The tests for the Template fan platform.""" """The tests for the Template fan platform."""
from typing import Any
import pytest import pytest
import voluptuous as vol import voluptuous as vol
from homeassistant import setup from homeassistant import setup
from homeassistant.components import fan
from homeassistant.components.fan import ( from homeassistant.components.fan import (
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_OSCILLATING, ATTR_OSCILLATING,
@ -17,11 +20,15 @@ from homeassistant.components.fan import (
) )
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
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.fan import common from tests.components.fan import common
_TEST_FAN = "fan.test_fan" _TEST_OBJECT_ID = "test_fan"
_TEST_FAN = f"fan.{_TEST_OBJECT_ID}"
# Represent for fan's state # Represent for fan's state
_STATE_INPUT_BOOLEAN = "input_boolean.state" _STATE_INPUT_BOOLEAN = "input_boolean.state"
# Represent for fan's state # Represent for fan's state
@ -36,6 +43,169 @@ _OSC_INPUT = "input_select.osc"
_DIRECTION_INPUT_SELECT = "input_select.direction" _DIRECTION_INPUT_SELECT = "input_select.direction"
OPTIMISTIC_ON_OFF_CONFIG = {
"turn_on": {
"service": "test.automation",
"data": {
"action": "turn_on",
"caller": "{{ this.entity_id }}",
},
},
"turn_off": {
"service": "test.automation",
"data": {
"action": "turn_off",
"caller": "{{ this.entity_id }}",
},
},
}
PERCENTAGE_ACTION = {
"set_percentage": {
"action": "test.automation",
"data": {
"action": "set_percentage",
"percentage": "{{ percentage }}",
"caller": "{{ this.entity_id }}",
},
},
}
OPTIMISTIC_PERCENTAGE_CONFIG = {
**OPTIMISTIC_ON_OFF_CONFIG,
**PERCENTAGE_ACTION,
}
PRESET_MODE_ACTION = {
"set_preset_mode": {
"action": "test.automation",
"data": {
"action": "set_preset_mode",
"preset_mode": "{{ preset_mode }}",
"caller": "{{ this.entity_id }}",
},
},
}
OPTIMISTIC_PRESET_MODE_CONFIG = {
**OPTIMISTIC_ON_OFF_CONFIG,
**PRESET_MODE_ACTION,
}
OPTIMISTIC_PRESET_MODE_CONFIG2 = {
**OPTIMISTIC_PRESET_MODE_CONFIG,
"preset_modes": ["auto", "low", "medium", "high"],
}
OSCILLATE_ACTION = {
"set_oscillating": {
"action": "test.automation",
"data": {
"action": "set_oscillating",
"oscillating": "{{ oscillating }}",
"caller": "{{ this.entity_id }}",
},
},
}
OPTIMISTIC_OSCILLATE_CONFIG = {
**OPTIMISTIC_ON_OFF_CONFIG,
**OSCILLATE_ACTION,
}
DIRECTION_ACTION = {
"set_direction": {
"action": "test.automation",
"data": {
"action": "set_direction",
"direction": "{{ direction }}",
"caller": "{{ this.entity_id }}",
},
},
}
OPTIMISTIC_DIRECTION_CONFIG = {
**OPTIMISTIC_ON_OFF_CONFIG,
**DIRECTION_ACTION,
}
async def async_setup_legacy_format(
hass: HomeAssistant, count: int, light_config: dict[str, Any]
) -> None:
"""Do setup of fan integration via legacy format."""
config = {"fan": {"platform": "template", "fans": light_config}}
with assert_setup_component(count, fan.DOMAIN):
assert await async_setup_component(
hass,
fan.DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
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,
}
},
)
@pytest.fixture
async def setup_fan(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
light_config: dict[str, Any],
) -> None:
"""Do setup of fan integration."""
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_format(hass, count, light_config)
@pytest.fixture
async def setup_test_fan_with_extra_config(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
fan_config: dict[str, Any],
extra_config: dict[str, Any],
) -> None:
"""Do setup of fan integration."""
config = {_TEST_OBJECT_ID: {**fan_config, **extra_config}}
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_format(hass, count, config)
@pytest.fixture
async def setup_optimistic_fan_attribute(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
extra_config: dict,
) -> 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
)
@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) @pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", "config",
@ -123,28 +293,21 @@ async def test_wrong_template_config(hass: HomeAssistant) -> None:
"platform": "template", "platform": "template",
"fans": { "fans": {
"test_fan": { "test_fan": {
"value_template": """ "value_template": "{{ is_state('input_boolean.state', 'True') }}",
{% if is_state('input_boolean.state', 'True') %}
{{ 'on' }}
{% else %}
{{ 'off' }}
{% endif %}
""",
"percentage_template": ( "percentage_template": (
"{{ states('input_number.percentage') }}" "{{ states('input_number.percentage') }}"
), ),
**OPTIMISTIC_ON_OFF_CONFIG,
**PERCENTAGE_ACTION,
"preset_mode_template": ( "preset_mode_template": (
"{{ states('input_select.preset_mode') }}" "{{ states('input_select.preset_mode') }}"
), ),
**PRESET_MODE_ACTION,
"oscillating_template": "{{ states('input_select.osc') }}", "oscillating_template": "{{ states('input_select.osc') }}",
**OSCILLATE_ACTION,
"direction_template": "{{ states('input_select.direction') }}", "direction_template": "{{ states('input_select.direction') }}",
**DIRECTION_ACTION,
"speed_count": "3", "speed_count": "3",
"set_percentage": {
"service": "script.fans_set_speed",
"data_template": {"percentage": "{{ percentage }}"},
},
"turn_on": {"service": "script.fan_on"},
"turn_off": {"service": "script.fan_off"},
} }
}, },
} }
@ -188,8 +351,7 @@ async def test_templates_with_entities(hass: HomeAssistant) -> None:
"test_fan": { "test_fan": {
"value_template": "{{ 'on' }}", "value_template": "{{ 'on' }}",
"percentage_template": "{{ states('sensor.percentage') }}", "percentage_template": "{{ states('sensor.percentage') }}",
"turn_on": {"service": "script.fan_on"}, **OPTIMISTIC_PERCENTAGE_CONFIG,
"turn_off": {"service": "script.fan_off"},
}, },
}, },
} }
@ -215,8 +377,7 @@ async def test_templates_with_entities(hass: HomeAssistant) -> None:
"preset_mode_template": ( "preset_mode_template": (
"{{ states('sensor.preset_mode') }}" "{{ states('sensor.preset_mode') }}"
), ),
"turn_on": {"service": "script.fan_on"}, **OPTIMISTIC_PRESET_MODE_CONFIG,
"turn_off": {"service": "script.fan_off"},
}, },
}, },
} }
@ -284,8 +445,7 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
"fans": { "fans": {
"test_fan": { "test_fan": {
"value_template": "{{ 'unavailable' }}", "value_template": "{{ 'unavailable' }}",
"turn_on": {"service": "script.fan_on"}, **OPTIMISTIC_ON_OFF_CONFIG,
"turn_off": {"service": "script.fan_off"},
} }
}, },
} }
@ -299,11 +459,12 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
"fans": { "fans": {
"test_fan": { "test_fan": {
"value_template": "{{ 'on' }}", "value_template": "{{ 'on' }}",
"oscillating_template": "{{ 'unavailable' }}",
"direction_template": "{{ 'unavailable' }}",
"percentage_template": "{{ 0 }}", "percentage_template": "{{ 0 }}",
"turn_on": {"service": "script.fan_on"}, **OPTIMISTIC_PERCENTAGE_CONFIG,
"turn_off": {"service": "script.fan_off"}, "oscillating_template": "{{ 'unavailable' }}",
**OSCILLATE_ACTION,
"direction_template": "{{ 'unavailable' }}",
**DIRECTION_ACTION,
} }
}, },
} }
@ -317,11 +478,12 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
"fans": { "fans": {
"test_fan": { "test_fan": {
"value_template": "{{ 'on' }}", "value_template": "{{ 'on' }}",
"oscillating_template": "{{ 1 == 1 }}",
"direction_template": "{{ 'forward' }}",
"percentage_template": "{{ 66 }}", "percentage_template": "{{ 66 }}",
"turn_on": {"service": "script.fan_on"}, **OPTIMISTIC_PERCENTAGE_CONFIG,
"turn_off": {"service": "script.fan_off"}, "oscillating_template": "{{ 1 == 1 }}",
**OSCILLATE_ACTION,
"direction_template": "{{ 'forward' }}",
**DIRECTION_ACTION,
} }
}, },
} }
@ -335,11 +497,12 @@ async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
"fans": { "fans": {
"test_fan": { "test_fan": {
"value_template": "{{ 'abc' }}", "value_template": "{{ 'abc' }}",
"oscillating_template": "{{ 'xyz' }}",
"direction_template": "{{ 'right' }}",
"percentage_template": "{{ 0 }}", "percentage_template": "{{ 0 }}",
"turn_on": {"service": "script.fan_on"}, **OPTIMISTIC_PERCENTAGE_CONFIG,
"turn_off": {"service": "script.fan_off"}, "oscillating_template": "{{ 'xyz' }}",
**OSCILLATE_ACTION,
"direction_template": "{{ 'right' }}",
**DIRECTION_ACTION,
} }
}, },
} }
@ -541,77 +704,18 @@ async def test_increase_decrease_speed(
_verify(hass, state, value, None, None, None) _verify(hass, state, value, None, None, None)
async def test_no_value_template(hass: HomeAssistant, calls: list[ServiceCall]) -> None: async def test_optimistic_state(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test a fan without a value_template.""" """Test a fan without a value_template."""
await _register_fan_sources(hass) await _register_fan_sources(hass)
with assert_setup_component(1, "fan"): with assert_setup_component(1, "fan"):
test_fan_config = { test_fan_config = {
"preset_mode_template": "{{ states('input_select.preset_mode') }}", **OPTIMISTIC_ON_OFF_CONFIG,
"preset_modes": ["auto"], "preset_modes": ["auto"],
"percentage_template": "{{ states('input_number.percentage') }}", **PRESET_MODE_ACTION,
"oscillating_template": "{{ states('input_select.osc') }}", **PERCENTAGE_ACTION,
"direction_template": "{{ states('input_select.direction') }}", **OSCILLATE_ACTION,
"turn_on": [ **DIRECTION_ACTION,
{
"service": "input_boolean.turn_on",
"entity_id": _STATE_INPUT_BOOLEAN,
},
{
"service": "test.automation",
"data_template": {
"action": "turn_on",
"caller": "{{ this.entity_id }}",
},
},
],
"turn_off": [
{
"service": "input_boolean.turn_off",
"entity_id": _STATE_INPUT_BOOLEAN,
},
{
"service": "test.automation",
"data_template": {
"action": "turn_off",
"caller": "{{ this.entity_id }}",
},
},
],
"set_preset_mode": [
{
"service": "input_select.select_option",
"data_template": {
"entity_id": _PRESET_MODE_INPUT_SELECT,
"option": "{{ preset_mode }}",
},
},
{
"service": "test.automation",
"data_template": {
"action": "set_preset_mode",
"caller": "{{ this.entity_id }}",
"option": "{{ preset_mode }}",
},
},
],
"set_percentage": [
{
"service": "input_number.set_value",
"data_template": {
"entity_id": _PERCENTAGE_INPUT_NUMBER,
"value": "{{ percentage }}",
},
},
{
"service": "test.automation",
"data_template": {
"action": "set_value",
"caller": "{{ this.entity_id }}",
"value": "{{ percentage }}",
},
},
],
} }
assert await setup.async_setup_component( assert await setup.async_setup_component(
hass, hass,
@ -624,32 +728,127 @@ async def test_no_value_template(hass: HomeAssistant, calls: list[ServiceCall])
await hass.async_block_till_done() await hass.async_block_till_done()
await common.async_turn_on(hass, _TEST_FAN) await common.async_turn_on(hass, _TEST_FAN)
_verify(hass, STATE_ON, 0, None, None, "auto") _verify(hass, STATE_ON)
assert len(calls) == 1
assert calls[-1].data["action"] == "turn_on"
assert calls[-1].data["caller"] == _TEST_FAN
await common.async_turn_off(hass, _TEST_FAN) await common.async_turn_off(hass, _TEST_FAN)
_verify(hass, STATE_OFF, 0, None, None, "auto") _verify(hass, STATE_OFF)
assert len(calls) == 2
assert calls[-1].data["action"] == "turn_off"
assert calls[-1].data["caller"] == _TEST_FAN
percent = 100 percent = 100
await common.async_set_percentage(hass, _TEST_FAN, percent) await common.async_set_percentage(hass, _TEST_FAN, percent)
assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == percent _verify(hass, STATE_ON, percent)
_verify(hass, STATE_ON, percent, None, None, "auto")
assert len(calls) == 3
assert calls[-1].data["action"] == "set_percentage"
assert calls[-1].data["percentage"] == 100
assert calls[-1].data["caller"] == _TEST_FAN
await common.async_turn_off(hass, _TEST_FAN) await common.async_turn_off(hass, _TEST_FAN)
_verify(hass, STATE_OFF, percent, None, None, "auto") _verify(hass, STATE_OFF, percent)
assert len(calls) == 4
assert calls[-1].data["action"] == "turn_off"
assert calls[-1].data["caller"] == _TEST_FAN
preset = "auto" preset = "auto"
await common.async_set_preset_mode(hass, _TEST_FAN, preset) await common.async_set_preset_mode(hass, _TEST_FAN, preset)
assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == preset assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == preset
_verify(hass, STATE_ON, percent, None, None, preset) _verify(hass, STATE_ON, percent, None, None, preset)
assert len(calls) == 5
assert calls[-1].data["action"] == "set_preset_mode"
assert calls[-1].data["preset_mode"] == preset
assert calls[-1].data["caller"] == _TEST_FAN
await common.async_turn_off(hass, _TEST_FAN) await common.async_turn_off(hass, _TEST_FAN)
_verify(hass, STATE_OFF, percent, None, None, preset) _verify(hass, STATE_OFF, percent, None, None, preset)
await common.async_set_direction(hass, _TEST_FAN, True) assert len(calls) == 6
_verify(hass, STATE_OFF, percent, None, None, preset) assert calls[-1].data["action"] == "turn_off"
assert calls[-1].data["caller"] == _TEST_FAN
await common.async_set_direction(hass, _TEST_FAN, DIRECTION_FORWARD)
_verify(hass, STATE_OFF, percent, None, DIRECTION_FORWARD, preset)
assert len(calls) == 7
assert calls[-1].data["action"] == "set_direction"
assert calls[-1].data["direction"] == DIRECTION_FORWARD
assert calls[-1].data["caller"] == _TEST_FAN
await common.async_oscillate(hass, _TEST_FAN, True) await common.async_oscillate(hass, _TEST_FAN, True)
_verify(hass, STATE_OFF, percent, None, None, preset) _verify(hass, STATE_OFF, percent, True, DIRECTION_FORWARD, preset)
assert len(calls) == 8
assert calls[-1].data["action"] == "set_oscillating"
assert calls[-1].data["oscillating"] is True
assert calls[-1].data["caller"] == _TEST_FAN
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize("style", [ConfigurationStyle.LEGACY])
@pytest.mark.parametrize(
("extra_config", "attribute", "action", "verify_attr", "coro", "value"),
[
(
OPTIMISTIC_PERCENTAGE_CONFIG,
"percentage",
"set_percentage",
"expected_percentage",
common.async_set_percentage,
50,
),
(
OPTIMISTIC_PRESET_MODE_CONFIG2,
"preset_mode",
"set_preset_mode",
"expected_preset_mode",
common.async_set_preset_mode,
"auto",
),
(
OPTIMISTIC_OSCILLATE_CONFIG,
"oscillating",
"set_oscillating",
"expected_oscillating",
common.async_oscillate,
True,
),
(
OPTIMISTIC_DIRECTION_CONFIG,
"direction",
"set_direction",
"expected_direction",
common.async_set_direction,
DIRECTION_FORWARD,
),
],
)
async def test_optimistic_attributes(
hass: HomeAssistant,
attribute: str,
action: str,
verify_attr: str,
coro,
value: Any,
setup_optimistic_fan_attribute,
calls: list[ServiceCall],
) -> None:
"""Test setting percentage with optimistic template."""
await coro(hass, _TEST_FAN, value)
_verify(hass, STATE_ON, **{verify_attr: value})
assert len(calls) == 1
assert calls[-1].data["action"] == action
assert calls[-1].data[attribute] == value
assert calls[-1].data["caller"] == _TEST_FAN
async def test_increase_decrease_speed_default_speed_count( async def test_increase_decrease_speed_default_speed_count(
@ -702,10 +901,10 @@ async def test_set_invalid_osc(hass: HomeAssistant, calls: list[ServiceCall]) ->
def _verify( def _verify(
hass: HomeAssistant, hass: HomeAssistant,
expected_state: str, expected_state: str,
expected_percentage: int | None, expected_percentage: int | None = None,
expected_oscillating: bool | None, expected_oscillating: bool | None = None,
expected_direction: str | None, expected_direction: str | None = None,
expected_preset_mode: str | None, expected_preset_mode: str | None = None,
) -> None: ) -> None:
"""Verify fan's state, speed and osc.""" """Verify fan's state, speed and osc."""
state = hass.states.get(_TEST_FAN) state = hass.states.get(_TEST_FAN)
@ -1093,3 +1292,57 @@ async def test_implemented_preset_mode(hass: HomeAssistant) -> None:
attributes = state.attributes attributes = state.attributes
assert attributes.get("percentage") is None assert attributes.get("percentage") is None
assert attributes.get("supported_features") & FanEntityFeature.PRESET_MODE 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": [],
},
),
],
)
@pytest.mark.parametrize(
("extra_config", "supported_features"),
[
(
{
"set_percentage": [],
},
FanEntityFeature.SET_SPEED,
),
(
{
"set_preset_mode": [],
},
FanEntityFeature.PRESET_MODE,
),
(
{
"set_oscillating": [],
},
FanEntityFeature.OSCILLATE,
),
(
{
"set_direction": [],
},
FanEntityFeature.DIRECTION,
),
],
)
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_FAN)
assert state.attributes["supported_features"] == (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON | supported_features
)