1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-16 21:41:44 +01:00
Files
core/tests/components/template/test_fan.py
T

1455 lines
45 KiB
Python

"""The tests for the Template fan platform."""
from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion
import voluptuous as vol
from homeassistant.components import fan, template
from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import ConfigType
from .conftest import (
ConfigurationStyle,
TemplatePlatformSetup,
assert_action,
async_get_flow_preview_state,
async_trigger,
make_test_action,
make_test_trigger,
setup_and_test_nested_unique_id,
setup_and_test_unique_id,
setup_entity,
)
from tests.common import MockConfigEntry
from tests.components.fan import common
from tests.typing import WebSocketGenerator
TEST_INPUT_BOOLEAN = "input_boolean.state"
TEST_STATE_ENTITY_ID = "sensor.test_sensor"
TEST_AVAILABILITY_ENTITY = "binary_sensor.availability"
TEST_FAN = TemplatePlatformSetup(
fan.DOMAIN,
"fans",
"test_fan",
make_test_trigger(
TEST_INPUT_BOOLEAN, TEST_STATE_ENTITY_ID, TEST_AVAILABILITY_ENTITY
),
)
ON_ACTION = make_test_action("turn_on")
OFF_ACTION = make_test_action("turn_off")
OPTIMISTIC_ON_OFF_ACTIONS = {
**ON_ACTION,
**OFF_ACTION,
}
PERCENTAGE_ACTION = make_test_action(
"set_percentage", {"percentage": "{{ percentage }}"}
)
OPTIMISTIC_PERCENTAGE_CONFIG = {
**OPTIMISTIC_ON_OFF_ACTIONS,
**PERCENTAGE_ACTION,
}
PRESET_MODE_ACTION = make_test_action(
"set_preset_mode", {"preset_mode": "{{ preset_mode }}"}
)
OPTIMISTIC_PRESET_MODE_CONFIG = {
**OPTIMISTIC_ON_OFF_ACTIONS,
**PRESET_MODE_ACTION,
}
OPTIMISTIC_PRESET_MODE_CONFIG2 = {
**OPTIMISTIC_PRESET_MODE_CONFIG,
"preset_modes": ["auto", "low", "medium", "high"],
}
OSCILLATE_ACTION = make_test_action(
"set_oscillating", {"oscillating": "{{ oscillating }}"}
)
OPTIMISTIC_OSCILLATE_CONFIG = {
**OPTIMISTIC_ON_OFF_ACTIONS,
**OSCILLATE_ACTION,
}
DIRECTION_ACTION = make_test_action("set_direction", {"direction": "{{ direction }}"})
OPTIMISTIC_DIRECTION_CONFIG = {
**OPTIMISTIC_ON_OFF_ACTIONS,
**DIRECTION_ACTION,
}
def _verify(
hass: HomeAssistant,
expected_state: str,
expected_percentage: int | None = None,
expected_oscillating: bool | None = None,
expected_direction: str | None = None,
expected_preset_mode: str | None = None,
) -> None:
"""Verify fan's state, speed and osc."""
state = hass.states.get(TEST_FAN.entity_id)
attributes = state.attributes
assert state.state == str(expected_state)
assert attributes.get(ATTR_PERCENTAGE) == expected_percentage
assert attributes.get(ATTR_OSCILLATING) == expected_oscillating
assert attributes.get(ATTR_DIRECTION) == expected_direction
assert attributes.get(ATTR_PRESET_MODE) == expected_preset_mode
@pytest.fixture
async def setup_fan(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
config: ConfigType,
extra_config: ConfigType,
) -> None:
"""Do setup of fan integration."""
await setup_entity(hass, TEST_FAN, style, count, config, extra_config=extra_config)
@pytest.fixture
async def setup_state_fan(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str,
extra_config: ConfigType,
):
"""Do setup of fan integration using a state template."""
await setup_entity(hass, TEST_FAN, style, count, extra_config, state_template)
@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."""
await setup_entity(
hass, TEST_FAN, style, count, {}, "{{ 1==1 }}", extra_config=extra_config
)
@pytest.fixture
async def setup_single_attribute_state_fan(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
attribute: str,
attribute_template: str,
state_template: str,
extra_config: dict,
) -> None:
"""Do setup of fan integration testing a single attribute."""
await setup_entity(
hass,
TEST_FAN,
style,
count,
{attribute: attribute_template} if attribute and attribute_template else {},
state_template,
{**OPTIMISTIC_ON_OFF_ACTIONS, **extra_config},
)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", OPTIMISTIC_ON_OFF_ACTIONS)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_missing_optional_config(hass: HomeAssistant) -> None:
"""Test: missing optional template is ok."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
_verify(hass, STATE_ON, None, None, None, None)
@pytest.mark.parametrize("count", [0])
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
"extra_config",
[OFF_ACTION, ON_ACTION],
)
@pytest.mark.usefixtures("setup_optimistic_fan_attribute")
async def test_wrong_template_config(hass: HomeAssistant) -> None:
"""Test: missing 'turn_on' or 'turn_off' will fail."""
assert hass.states.async_all("fan") == []
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ is_state('input_boolean.state', 'on') }}", OPTIMISTIC_ON_OFF_ACTIONS)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_state_template(hass: HomeAssistant) -> None:
"""Test state template."""
await async_trigger(hass, TEST_INPUT_BOOLEAN, STATE_OFF)
_verify(hass, STATE_OFF, None, None, None, None)
await async_trigger(hass, TEST_INPUT_BOOLEAN, STATE_ON)
_verify(hass, STATE_ON, None, None, None, None)
await async_trigger(hass, TEST_INPUT_BOOLEAN, STATE_OFF)
_verify(hass, STATE_OFF, None, None, None, None)
@pytest.mark.parametrize(("count", "extra_config"), [(1, OPTIMISTIC_ON_OFF_ACTIONS)])
@pytest.mark.parametrize(
("state_template", "expected"),
[
("{{ True }}", STATE_ON),
("{{ False }}", STATE_OFF),
("{{ x - 1 }}", STATE_UNAVAILABLE),
("{{ 1 }}", STATE_ON),
("{{ 'true' }}", STATE_ON),
("{{ 'yes' }}", STATE_ON),
("{{ 'on' }}", STATE_ON),
("{{ 'enable' }}", STATE_ON),
("{{ 0 }}", STATE_OFF),
("{{ 'false' }}", STATE_OFF),
("{{ 'no' }}", STATE_OFF),
("{{ 'off' }}", STATE_OFF),
("{{ 'disable' }}", STATE_OFF),
("{{ None }}", STATE_UNKNOWN),
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_state_template_states(hass: HomeAssistant, expected: str) -> None:
"""Test state template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
_verify(hass, expected, None, None, None, None)
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config", "attribute"),
[
(
1,
"{{ 1 == 1}}",
"{% if is_state('sensor.test_sensor', 'on') %}/local/switch.png{% endif %}",
{},
"picture",
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_picture_template(hass: HomeAssistant) -> None:
"""Test picture template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
state = hass.states.get(TEST_FAN.entity_id)
assert state.attributes.get("entity_picture") == ""
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON)
state = hass.states.get(TEST_FAN.entity_id)
assert state.attributes["entity_picture"] == "/local/switch.png"
@pytest.mark.parametrize(
("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",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_icon_template(hass: HomeAssistant) -> None:
"""Test icon template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
state = hass.states.get(TEST_FAN.entity_id)
assert state.attributes.get("icon") == ""
await async_trigger(hass, TEST_INPUT_BOOLEAN, STATE_ON)
state = hass.states.get(TEST_FAN.entity_id)
assert state.attributes["icon"] == "mdi:eye"
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config"),
[
(
1,
"{{ 1 == 1 }}",
"{{ states('sensor.test_sensor') }}",
PERCENTAGE_ACTION,
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "percentage_template"),
(ConfigurationStyle.MODERN, "percentage"),
(ConfigurationStyle.TRIGGER, "percentage"),
],
)
@pytest.mark.parametrize(
("percent", "expected"),
[
("0", 0),
("33", 33),
("invalid", None),
("5000", None),
("100", 100),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_percentage_template(
hass: HomeAssistant, percent: str, expected: int
) -> None:
"""Test templates with fan percentages from other entities."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, percent)
_verify(hass, STATE_ON, expected, None, None, None)
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config"),
[
(
1,
"{{ 1 == 1 }}",
"{{ states('sensor.test_sensor') }}",
{"preset_modes": ["auto", "smart"], **PRESET_MODE_ACTION},
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "preset_mode_template"),
(ConfigurationStyle.MODERN, "preset_mode"),
(ConfigurationStyle.TRIGGER, "preset_mode"),
],
)
@pytest.mark.parametrize(
("preset_mode", "expected"),
[
("0", None),
("invalid", None),
("auto", "auto"),
("smart", "smart"),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_preset_mode_template(
hass: HomeAssistant, preset_mode: str, expected: int
) -> None:
"""Test preset_mode template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, preset_mode)
_verify(hass, STATE_ON, None, None, None, expected)
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config"),
[
(
1,
"{{ 1 == 1 }}",
"{{ is_state('sensor.test_sensor', 'on') }}",
OSCILLATE_ACTION,
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "oscillating_template"),
(ConfigurationStyle.MODERN, "oscillating"),
(ConfigurationStyle.TRIGGER, "oscillating"),
],
)
@pytest.mark.parametrize(
("oscillating", "expected"),
[
(STATE_ON, True),
(STATE_OFF, False),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_oscillating_template(
hass: HomeAssistant, oscillating: str, expected: bool | None
) -> None:
"""Test oscillating template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, oscillating)
_verify(hass, STATE_ON, None, expected, None, None)
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config"),
[
(
1,
"{{ 1 == 1 }}",
"{{ states('sensor.test_sensor') }}",
DIRECTION_ACTION,
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "direction_template"),
(ConfigurationStyle.MODERN, "direction"),
(ConfigurationStyle.TRIGGER, "direction"),
],
)
@pytest.mark.parametrize(
("direction", "expected"),
[
(DIRECTION_FORWARD, DIRECTION_FORWARD),
(DIRECTION_REVERSE, DIRECTION_REVERSE),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_fan")
async def test_direction_template(
hass: HomeAssistant, direction: str, expected: bool | None
) -> None:
"""Test direction template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, direction)
_verify(hass, STATE_ON, None, None, expected, None)
@pytest.mark.parametrize(("count", "extra_config"), [(1, {})])
@pytest.mark.parametrize(
("style", "config"),
[
(
ConfigurationStyle.LEGACY,
{
"availability_template": (
"{{ is_state('binary_sensor.availability', 'on') }}"
),
"value_template": "{{ 'on' }}",
"oscillating_template": "{{ 1 == 1 }}",
"direction_template": "{{ 'forward' }}",
"turn_on": {"service": "script.fan_on"},
"turn_off": {"service": "script.fan_off"},
},
),
(
ConfigurationStyle.MODERN,
{
"availability": ("{{ is_state('binary_sensor.availability', 'on') }}"),
"state": "{{ 'on' }}",
"oscillating": "{{ 1 == 1 }}",
"direction": "{{ 'forward' }}",
"turn_on": {"service": "script.fan_on"},
"turn_off": {"service": "script.fan_off"},
},
),
(
ConfigurationStyle.TRIGGER,
{
"availability": ("{{ is_state('binary_sensor.availability', 'on') }}"),
"state": "{{ 'on' }}",
"oscillating": "{{ 1 == 1 }}",
"direction": "{{ 'forward' }}",
"turn_on": {"service": "script.fan_on"},
"turn_off": {"service": "script.fan_off"},
},
),
],
)
@pytest.mark.usefixtures("setup_fan")
async def test_availability_template_with_entities(hass: HomeAssistant) -> None:
"""Test availability tempalates with values from other entities."""
for state, test_assert in ((STATE_ON, True), (STATE_OFF, False)):
await async_trigger(hass, TEST_AVAILABILITY_ENTITY, state)
assert (
hass.states.get(TEST_FAN.entity_id).state != STATE_UNAVAILABLE
) == test_assert
@pytest.mark.parametrize(("count", "extra_config"), [(1, {})])
@pytest.mark.parametrize(
("style", "config", "states"),
[
(
ConfigurationStyle.LEGACY,
{
"value_template": "{{ 'unavailable' }}",
**OPTIMISTIC_ON_OFF_ACTIONS,
},
[STATE_UNKNOWN, None, None, None],
),
(
ConfigurationStyle.MODERN,
{
"state": "{{ 'unavailable' }}",
**OPTIMISTIC_ON_OFF_ACTIONS,
},
[STATE_UNKNOWN, None, None, None],
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'unavailable' }}",
**OPTIMISTIC_ON_OFF_ACTIONS,
},
[STATE_UNKNOWN, None, None, None],
),
(
ConfigurationStyle.LEGACY,
{
"value_template": "{{ 'on' }}",
"percentage_template": "{{ 0 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating_template": "{{ 'unavailable' }}",
**OSCILLATE_ACTION,
"direction_template": "{{ 'unavailable' }}",
**DIRECTION_ACTION,
},
[STATE_ON, 0, None, None],
),
(
ConfigurationStyle.MODERN,
{
"state": "{{ 'on' }}",
"percentage": "{{ 0 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating": "{{ 'unavailable' }}",
**OSCILLATE_ACTION,
"direction": "{{ 'unavailable' }}",
**DIRECTION_ACTION,
},
[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,
{
"value_template": "{{ 'on' }}",
"percentage_template": "{{ 66 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating_template": "{{ 1 == 1 }}",
**OSCILLATE_ACTION,
"direction_template": "{{ 'forward' }}",
**DIRECTION_ACTION,
},
[STATE_ON, 66, True, DIRECTION_FORWARD],
),
(
ConfigurationStyle.MODERN,
{
"state": "{{ 'on' }}",
"percentage": "{{ 66 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating": "{{ 1 == 1 }}",
**OSCILLATE_ACTION,
"direction": "{{ 'forward' }}",
**DIRECTION_ACTION,
},
[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,
{
"value_template": "{{ 'abc' }}",
"percentage_template": "{{ 0 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating_template": "{{ 'xyz' }}",
**OSCILLATE_ACTION,
"direction_template": "{{ 'right' }}",
**DIRECTION_ACTION,
},
[STATE_UNKNOWN, 0, None, None],
),
(
ConfigurationStyle.MODERN,
{
"state": "{{ 'abc' }}",
"percentage": "{{ 0 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating": "{{ 'xyz' }}",
**OSCILLATE_ACTION,
"direction": "{{ 'right' }}",
**DIRECTION_ACTION,
},
[STATE_UNKNOWN, 0, None, None],
),
(
ConfigurationStyle.TRIGGER,
{
"state": "{{ 'abc' }}",
"percentage": "{{ 0 }}",
**OPTIMISTIC_PERCENTAGE_CONFIG,
"oscillating": "{{ 'xyz' }}",
**OSCILLATE_ACTION,
"direction": "{{ 'right' }}",
**DIRECTION_ACTION,
},
[STATE_UNKNOWN, 0, None, None],
),
],
)
@pytest.mark.usefixtures("setup_fan")
async def test_template_with_unavailable_entities(hass: HomeAssistant, states) -> None:
"""Test unavailability with value_template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
_verify(hass, states[0], states[1], states[2], states[3], None)
@pytest.mark.parametrize(("count", "extra_config"), [(1, {})])
@pytest.mark.parametrize(
("style", "config"),
[
(
ConfigurationStyle.LEGACY,
{
"value_template": "{{ 'on' }}",
"availability_template": "{{ x - 12 }}",
"preset_mode_template": ("{{ states('input_select.preset_mode') }}"),
"oscillating_template": "{{ states('input_select.osc') }}",
"direction_template": "{{ states('input_select.direction') }}",
"turn_on": {"service": "script.fan_on"},
"turn_off": {"service": "script.fan_off"},
},
),
(
ConfigurationStyle.MODERN,
{
"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"},
},
),
(
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_fan")
async def test_invalid_availability_template_keeps_component_available(
hass: HomeAssistant, caplog_setup_text, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that an invalid availability keeps the device available."""
# Ensure trigger entities update.
await async_trigger(hass, TEST_INPUT_BOOLEAN, STATE_ON)
assert hass.states.get(TEST_FAN.entity_id).state != STATE_UNAVAILABLE
err = "'x' is undefined"
assert err in caplog_setup_text or err in caplog.text
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'off' }}", OPTIMISTIC_ON_OFF_ACTIONS)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_on_off(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test turn on and turn off."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
state = hass.states.get(TEST_FAN.entity_id)
assert state.state == STATE_OFF
for expected_calls, (func, action) in enumerate(
[
(common.async_turn_on, "turn_on"),
(common.async_turn_off, "turn_off"),
]
):
await func(hass, TEST_FAN.entity_id)
assert_action(TEST_FAN, calls, expected_calls + 1, action)
@pytest.mark.parametrize(
("count", "extra_config", "state_template"),
[
(
1,
{
**OPTIMISTIC_ON_OFF_ACTIONS,
**OPTIMISTIC_PRESET_MODE_CONFIG2,
**OPTIMISTIC_PERCENTAGE_CONFIG,
},
"{{ 'off' }}",
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_on_with_extra_attributes(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test turn on and turn off."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
state = hass.states.get(TEST_FAN.entity_id)
assert state.state == STATE_OFF
await common.async_turn_on(hass, TEST_FAN.entity_id, 100)
assert_action(TEST_FAN, calls, 2, "turn_on", index=-2)
assert_action(TEST_FAN, calls, 2, "set_percentage", percentage=100)
await common.async_turn_off(hass, TEST_FAN.entity_id)
assert_action(TEST_FAN, calls, 3, "turn_off")
await common.async_turn_on(hass, TEST_FAN.entity_id, None, "auto")
assert_action(TEST_FAN, calls, 5, "turn_on", index=-2)
assert_action(TEST_FAN, calls, 5, "set_preset_mode", preset_mode="auto")
await common.async_turn_off(hass, TEST_FAN.entity_id)
assert_action(TEST_FAN, calls, 6, "turn_off")
await common.async_turn_on(hass, TEST_FAN.entity_id, 50, "high")
assert_action(TEST_FAN, calls, 9, "turn_on", index=-3)
assert_action(TEST_FAN, calls, 9, "set_preset_mode", index=-2, preset_mode="high")
assert_action(TEST_FAN, calls, 9, "set_percentage", percentage=50)
await common.async_turn_off(hass, TEST_FAN.entity_id)
assert_action(TEST_FAN, calls, 10, "turn_off")
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", {**OPTIMISTIC_ON_OFF_ACTIONS, **DIRECTION_ACTION})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_set_invalid_direction_from_initial_stage(hass: HomeAssistant) -> None:
"""Test set invalid direction when fan is in initial state."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_set_direction(hass, TEST_FAN.entity_id, "invalid")
_verify(hass, STATE_ON, None, None, None, None)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", {**OPTIMISTIC_ON_OFF_ACTIONS, **OSCILLATE_ACTION})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_set_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test set oscillating."""
expected_calls = 0
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_turn_on(hass, TEST_FAN.entity_id)
expected_calls += 1
for state in (True, False):
await common.async_oscillate(hass, TEST_FAN.entity_id, state)
_verify(hass, STATE_ON, None, state, None, None)
expected_calls += 1
assert_action(
TEST_FAN, calls, expected_calls, "set_oscillating", oscillating=state
)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", {**OPTIMISTIC_ON_OFF_ACTIONS, **DIRECTION_ACTION})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_set_direction(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test set valid direction."""
expected_calls = 0
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_turn_on(hass, TEST_FAN.entity_id)
expected_calls += 1
for direction in (DIRECTION_FORWARD, DIRECTION_REVERSE):
await common.async_set_direction(hass, TEST_FAN.entity_id, direction)
_verify(hass, STATE_ON, None, None, direction, None)
expected_calls += 1
assert_action(
TEST_FAN, calls, expected_calls, "set_direction", direction=direction
)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", {**OPTIMISTIC_ON_OFF_ACTIONS, **DIRECTION_ACTION})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_set_invalid_direction(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test set invalid direction when fan has valid direction."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
expected_calls = 1
for direction in (DIRECTION_FORWARD, "invalid"):
await common.async_set_direction(hass, TEST_FAN.entity_id, direction)
_verify(hass, STATE_ON, None, None, DIRECTION_FORWARD, None)
assert_action(
TEST_FAN,
calls,
expected_calls,
"set_direction",
direction=DIRECTION_FORWARD,
)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", OPTIMISTIC_PRESET_MODE_CONFIG2)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_preset_modes(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test preset_modes."""
for cnt, mode in enumerate(("auto", "low", "medium", "high")):
await common.async_set_preset_mode(hass, TEST_FAN.entity_id, mode)
assert_action(TEST_FAN, calls, cnt + 1, "set_preset_mode", preset_mode=mode)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", OPTIMISTIC_PRESET_MODE_CONFIG2)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_invalid_preset_modes(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test invalid preset_modes."""
for mode in ("invalid", "smart"):
with pytest.raises(NotValidPresetModeError):
await common.async_set_preset_mode(hass, TEST_FAN.entity_id, mode)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", OPTIMISTIC_PERCENTAGE_CONFIG)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_set_percentage(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test set valid speed percentage."""
expected_calls = 0
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_turn_on(hass, TEST_FAN.entity_id)
expected_calls += 1
for state, value in (
(STATE_ON, 100),
(STATE_ON, 66),
(STATE_ON, 0),
):
await common.async_set_percentage(hass, TEST_FAN.entity_id, value)
_verify(hass, state, value, None, None, None)
expected_calls += 1
assert_action(
TEST_FAN, calls, expected_calls, "set_percentage", percentage=value
)
await common.async_turn_on(hass, TEST_FAN.entity_id, percentage=50)
_verify(hass, STATE_ON, 50, None, None, None)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", {"speed_count": 3, **OPTIMISTIC_PERCENTAGE_CONFIG})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_increase_decrease_speed(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test set valid increase and decrease speed."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_turn_on(hass, TEST_FAN.entity_id)
for func, extra, state, value in (
(common.async_set_percentage, 100, STATE_ON, 100),
(common.async_decrease_speed, None, STATE_ON, 66),
(common.async_decrease_speed, None, STATE_ON, 33),
(common.async_decrease_speed, None, STATE_ON, 0),
(common.async_increase_speed, None, STATE_ON, 33),
):
await func(hass, TEST_FAN.entity_id, extra)
_verify(hass, state, value, None, None, None)
@pytest.mark.parametrize(
("count", "config", "extra_config"),
[
(
1,
{
**OPTIMISTIC_ON_OFF_ACTIONS,
"preset_modes": ["auto"],
**PRESET_MODE_ACTION,
**PERCENTAGE_ACTION,
**OSCILLATE_ACTION,
**DIRECTION_ACTION,
},
{},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_fan")
async def test_optimistic_state(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test a fan without a value_template."""
await common.async_turn_on(hass, TEST_FAN.entity_id)
_verify(hass, STATE_ON)
assert_action(TEST_FAN, calls, 1, "turn_on")
await common.async_turn_off(hass, TEST_FAN.entity_id)
_verify(hass, STATE_OFF)
assert_action(TEST_FAN, calls, 2, "turn_off")
percent = 100
await common.async_set_percentage(hass, TEST_FAN.entity_id, percent)
_verify(hass, STATE_ON, percent)
assert_action(TEST_FAN, calls, 3, "set_percentage", percentage=percent)
await common.async_turn_off(hass, TEST_FAN.entity_id)
_verify(hass, STATE_OFF, percent)
assert_action(TEST_FAN, calls, 4, "turn_off")
preset = "auto"
await common.async_set_preset_mode(hass, TEST_FAN.entity_id, preset)
_verify(hass, STATE_ON, percent, None, None, preset)
assert_action(TEST_FAN, calls, 5, "set_preset_mode", preset_mode=preset)
await common.async_turn_off(hass, TEST_FAN.entity_id)
_verify(hass, STATE_OFF, percent, None, None, preset)
assert_action(TEST_FAN, calls, 6, "turn_off")
await common.async_set_direction(hass, TEST_FAN.entity_id, DIRECTION_FORWARD)
_verify(hass, STATE_OFF, percent, None, DIRECTION_FORWARD, preset)
assert_action(TEST_FAN, calls, 7, "set_direction", direction=DIRECTION_FORWARD)
await common.async_oscillate(hass, TEST_FAN.entity_id, True)
_verify(hass, STATE_OFF, percent, True, DIRECTION_FORWARD, preset)
assert_action(TEST_FAN, calls, 8, "set_oscillating", oscillating=True)
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@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,
),
],
)
@pytest.mark.usefixtures("setup_optimistic_fan_attribute")
async def test_optimistic_attributes(
hass: HomeAssistant,
attribute: str,
action: str,
verify_attr: str,
coro,
value: Any,
calls: list[ServiceCall],
) -> None:
"""Test setting percentage with optimistic template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await coro(hass, TEST_FAN.entity_id, value)
_verify(hass, STATE_ON, **{verify_attr: value})
assert_action(TEST_FAN, calls, 1, action, **{attribute: value})
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", OPTIMISTIC_PERCENTAGE_CONFIG)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_increase_decrease_speed_default_speed_count(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test set valid increase and decrease speed."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_turn_on(hass, TEST_FAN.entity_id)
for func, extra, state, value in (
(common.async_set_percentage, 100, STATE_ON, 100),
(common.async_decrease_speed, None, STATE_ON, 99),
(common.async_decrease_speed, None, STATE_ON, 98),
(common.async_decrease_speed, 31, STATE_ON, 67),
(common.async_decrease_speed, None, STATE_ON, 66),
):
await func(hass, TEST_FAN.entity_id, extra)
_verify(hass, state, value, None, None, None)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", {**OPTIMISTIC_ON_OFF_ACTIONS, **OSCILLATE_ACTION})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_set_invalid_osc_from_initial_state(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test set invalid oscillating when fan is in initial state."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_turn_on(hass, TEST_FAN.entity_id)
with pytest.raises(vol.Invalid):
await common.async_oscillate(hass, TEST_FAN.entity_id, "invalid")
_verify(hass, STATE_ON, None, None, None, None)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ 'on' }}", {**OPTIMISTIC_ON_OFF_ACTIONS, **OSCILLATE_ACTION})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_state_fan")
async def test_set_invalid_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test set invalid oscillating when fan has valid osc."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
await common.async_turn_on(hass, TEST_FAN.entity_id)
await common.async_oscillate(hass, TEST_FAN.entity_id, True)
_verify(hass, STATE_ON, None, True, None, None)
await common.async_oscillate(hass, TEST_FAN.entity_id, False)
_verify(hass, STATE_ON, None, False, None, None)
with pytest.raises(vol.Invalid):
await common.async_oscillate(hass, TEST_FAN.entity_id, None)
_verify(hass, STATE_ON, None, False, None, None)
@pytest.mark.parametrize("config", [OPTIMISTIC_ON_OFF_ACTIONS])
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
async def test_unique_id(
hass: HomeAssistant, style: ConfigurationStyle, config: ConfigType
) -> None:
"""Test unique_id option only creates one fan per id."""
await setup_and_test_unique_id(hass, TEST_FAN, style, config)
@pytest.mark.parametrize("config", [OPTIMISTIC_ON_OFF_ACTIONS])
@pytest.mark.parametrize(
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
async def test_nested_unique_id(
hass: HomeAssistant,
style: ConfigurationStyle,
config: ConfigType,
entity_registry: er.EntityRegistry,
) -> None:
"""Test a template unique_id propagates to fan unique_ids."""
await setup_and_test_nested_unique_id(
hass, TEST_FAN, style, entity_registry, config
)
@pytest.mark.parametrize(
("count", "extra_config"),
[(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **OPTIMISTIC_PERCENTAGE_CONFIG})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("config", "percentage_step"),
[({"speed_count": 0}, 1), ({"speed_count": 100}, 1), ({"speed_count": 3}, 100 / 3)],
)
@pytest.mark.usefixtures("setup_fan")
async def test_speed_percentage_step(hass: HomeAssistant, percentage_step) -> None:
"""Test a fan that implements percentage."""
assert len(hass.states.async_all()) == 1
state = hass.states.get(TEST_FAN.entity_id)
attributes = state.attributes
assert attributes["percentage_step"] == percentage_step
assert attributes.get("supported_features") & FanEntityFeature.SET_SPEED
@pytest.mark.parametrize(
("count", "config", "extra_config"),
[(1, OPTIMISTIC_ON_OFF_ACTIONS, OPTIMISTIC_PRESET_MODE_CONFIG2)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_fan")
async def test_preset_mode_supported_features(hass: HomeAssistant) -> None:
"""Test a fan that implements preset_mode."""
assert len(hass.states.async_all()) == 1
state = hass.states.get(TEST_FAN.entity_id)
attributes = state.attributes
assert attributes.get("supported_features") & FanEntityFeature.PRESET_MODE
@pytest.mark.parametrize(
("count", "config"),
[(1, {"turn_on": [], "turn_off": []})],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@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,
),
],
)
@pytest.mark.usefixtures("setup_fan")
async def test_empty_action_config(
hass: HomeAssistant,
supported_features: FanEntityFeature,
) -> None:
"""Test configuration with empty script."""
state = hass.states.get(TEST_FAN.entity_id)
assert state.attributes["supported_features"] == (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON | supported_features
)
@pytest.mark.parametrize(
("count", "config", "extra_config"),
[
(
1,
{
"state": "{{ is_state('sensor.test_sensor', 'on') }}",
"turn_on": [],
"turn_off": [],
"optimistic": True,
},
{},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_fan")
async def test_optimistic_option(hass: HomeAssistant) -> None:
"""Test optimistic yaml option."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF)
state = hass.states.get(TEST_FAN.entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
"turn_on",
{"entity_id": TEST_FAN.entity_id},
blocking=True,
)
state = hass.states.get(TEST_FAN.entity_id)
assert state.state == STATE_ON
# The double trigger is needed because the state machine
# suppresses 'off' -> 'off' state changes for TEST_STATE_ENTITY_ID
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON)
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF)
state = hass.states.get(TEST_FAN.entity_id)
assert state.state == STATE_OFF
@pytest.mark.parametrize(
("count", "config", "extra_config"),
[
(
1,
{
"state": "{{ is_state('sensor.test_sensor', 'on') }}",
"turn_on": [],
"turn_off": [],
"optimistic": False,
},
{},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_fan")
async def test_not_optimistic(hass: HomeAssistant) -> None:
"""Test optimistic yaml option set to false."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_OFF)
await hass.services.async_call(
fan.DOMAIN,
"turn_on",
{"entity_id": TEST_FAN.entity_id},
blocking=True,
)
state = hass.states.get(TEST_FAN.entity_id)
assert state.state == STATE_OFF
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Tests creating a fan from a config entry."""
hass.states.async_set(
"sensor.test_sensor",
"on",
{},
)
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": "My template",
"state": "{{ states('sensor.test_sensor') }}",
"turn_on": [],
"turn_off": [],
"template_type": fan.DOMAIN,
},
title="My template",
)
template_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(template_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("fan.my_template")
assert state is not None
assert state == snapshot
async def test_flow_preview(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the config flow preview."""
state = await async_get_flow_preview_state(
hass,
hass_ws_client,
fan.DOMAIN,
{
"name": "My template",
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
)
assert state["state"] == STATE_ON