Files
core/tests/components/template/test_vacuum.py
T

1398 lines
40 KiB
Python

"""The tests for the Template vacuum platform."""
from dataclasses import asdict
from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import template, vacuum
from homeassistant.components.template.vacuum import CONF_CLEAN_SEGMENTS, CONF_SEGMENTS
from homeassistant.components.vacuum import (
ATTR_BATTERY_LEVEL,
ATTR_FAN_SPEED,
Segment,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.const import (
CONF_UNIQUE_ID,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.entity_component import async_update_entity
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.vacuum import common
from tests.typing import WebSocketGenerator
TEST_STATE_ENTITY_ID = "sensor.test_state"
TEST_ATTRIBUTE_ENTITY_ID = "sensor.test_attribute"
TEST_AVAILABILITY_ENTITY = "binary_sensor.availability"
TEST_VACUUM = TemplatePlatformSetup(
vacuum.DOMAIN,
"vacuums",
"test_vacuum",
make_test_trigger(
TEST_STATE_ENTITY_ID,
TEST_ATTRIBUTE_ENTITY_ID,
TEST_AVAILABILITY_ENTITY,
),
)
CLEAN_SPOT_ACTION = make_test_action("clean_spot")
LOCATE_ACTION = make_test_action("locate")
PAUSE_ACTION = make_test_action("pause")
RETURN_TO_BASE_ACTION = make_test_action("return_to_base")
SET_FAN_SPEED_ACTION = make_test_action(
"set_fan_speed", {"fan_speed": "{{ fan_speed }}"}
)
START_ACTION = make_test_action("start")
STOP_ACTION = make_test_action("stop")
CLEAN_SEGMENTS_ACTION = make_test_action(
"clean_segments", {"segment_ids": "{{ segment_ids }}"}
)
TEMPLATE_VACUUM_ACTIONS = {
**START_ACTION,
**PAUSE_ACTION,
**STOP_ACTION,
**RETURN_TO_BASE_ACTION,
**CLEAN_SPOT_ACTION,
**LOCATE_ACTION,
**SET_FAN_SPEED_ACTION,
}
def _verify(
hass: HomeAssistant,
expected_state: str,
expected_battery_level: int | None = None,
expected_fan_speed: str | None = None,
) -> None:
"""Verify vacuum's state and speed."""
state = hass.states.get(TEST_VACUUM.entity_id)
attributes = state.attributes
assert state.state == expected_state
assert attributes.get(ATTR_BATTERY_LEVEL) == expected_battery_level
assert attributes.get(ATTR_FAN_SPEED) == expected_fan_speed
@pytest.fixture
async def setup_vacuum(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
vacuum_config: dict[str, Any],
) -> None:
"""Do setup of number integration."""
await setup_entity(hass, TEST_VACUUM, style, 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."""
await setup_entity(
hass, TEST_VACUUM, style, count, vacuum_config, extra_config=extra_config
)
@pytest.fixture
async def setup_state_vacuum(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str,
):
"""Do setup of vacuum integration using a state template."""
await setup_entity(
hass, TEST_VACUUM, style, count, TEMPLATE_VACUUM_ACTIONS, state_template
)
@pytest.fixture
async def setup_base_vacuum(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str | None,
extra_config: dict,
):
"""Do setup of vacuum integration using a state template."""
await setup_entity(hass, TEST_VACUUM, style, count, extra_config, state_template)
@pytest.fixture
async def setup_single_attribute_state_vacuum(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str | None,
attribute: str,
attribute_template: str,
extra_config: dict,
) -> None:
"""Do setup of vacuum integration testing a single attribute."""
config = {attribute: attribute_template} if attribute and attribute_template else {}
await setup_entity(
hass,
TEST_VACUUM,
style,
count,
{**config, **TEMPLATE_VACUUM_ACTIONS},
state_template,
extra_config,
)
@pytest.fixture
async def setup_attributes_state_vacuum(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str | None,
attributes: dict,
) -> None:
"""Do setup of vacuum integration testing a single attribute."""
await setup_entity(
hass,
TEST_VACUUM,
style,
count,
TEMPLATE_VACUUM_ACTIONS,
state_template,
attributes=attributes,
)
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("style", "state_template", "extra_config", "parm1", "parm2"),
[
(
ConfigurationStyle.LEGACY,
None,
{"start": {"service": "script.vacuum_start"}},
STATE_UNKNOWN,
None,
),
(
ConfigurationStyle.MODERN,
None,
{"start": {"service": "script.vacuum_start"}},
STATE_UNKNOWN,
None,
),
(
ConfigurationStyle.TRIGGER,
None,
{"start": {"service": "script.vacuum_start"}},
STATE_UNKNOWN,
None,
),
(
ConfigurationStyle.LEGACY,
"{{ 'cleaning' }}",
{
"battery_level_template": "{{ 100 }}",
"start": {"service": "script.vacuum_start"},
},
VacuumActivity.CLEANING,
100,
),
(
ConfigurationStyle.MODERN,
"{{ 'cleaning' }}",
{
"battery_level": "{{ 100 }}",
"start": {"service": "script.vacuum_start"},
},
VacuumActivity.CLEANING,
100,
),
(
ConfigurationStyle.TRIGGER,
"{{ 'cleaning' }}",
{
"battery_level": "{{ 100 }}",
"start": {"service": "script.vacuum_start"},
},
VacuumActivity.CLEANING,
100,
),
(
ConfigurationStyle.LEGACY,
"{{ 'abc' }}",
{
"battery_level_template": "{{ 101 }}",
"start": {"service": "script.vacuum_start"},
},
STATE_UNKNOWN,
None,
),
(
ConfigurationStyle.MODERN,
"{{ 'abc' }}",
{
"battery_level": "{{ 101 }}",
"start": {"service": "script.vacuum_start"},
},
STATE_UNKNOWN,
None,
),
(
ConfigurationStyle.TRIGGER,
"{{ 'abc' }}",
{
"battery_level": "{{ 101 }}",
"start": {"service": "script.vacuum_start"},
},
STATE_UNKNOWN,
None,
),
(
ConfigurationStyle.LEGACY,
"{{ this_function_does_not_exist() }}",
{
"battery_level_template": "{{ this_function_does_not_exist() }}",
"fan_speed_template": "{{ this_function_does_not_exist() }}",
"start": {"service": "script.vacuum_start"},
},
STATE_UNAVAILABLE,
None,
),
(
ConfigurationStyle.MODERN,
"{{ this_function_does_not_exist() }}",
{
"battery_level": "{{ this_function_does_not_exist() }}",
"fan_speed": "{{ this_function_does_not_exist() }}",
"start": {"service": "script.vacuum_start"},
},
STATE_UNAVAILABLE,
None,
),
(
ConfigurationStyle.TRIGGER,
"{{ this_function_does_not_exist() }}",
{
"battery_level": "{{ this_function_does_not_exist() }}",
"fan_speed": "{{ this_function_does_not_exist() }}",
"start": {"service": "script.vacuum_start"},
},
STATE_UNAVAILABLE,
None,
),
],
)
@pytest.mark.usefixtures("setup_base_vacuum")
async def test_valid_legacy_configs(hass: HomeAssistant, count, parm1, parm2) -> None:
"""Test: configs."""
# Ensure trigger entity templates are rendered
hass.states.async_set(TEST_STATE_ENTITY_ID, None)
await hass.async_block_till_done()
assert len(hass.states.async_all("vacuum")) == count
_verify(hass, parm1, parm2)
@pytest.mark.parametrize("count", [0])
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("state_template", "extra_config"),
[
("{{ 'on' }}", {}),
(None, {"nothingburger": {"service": "script.vacuum_start"}}),
],
)
@pytest.mark.usefixtures("setup_base_vacuum")
async def test_invalid_configs(hass: HomeAssistant, count) -> None:
"""Test: configs."""
assert len(hass.states.async_all("vacuum")) == count
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[(1, "{{ states('sensor.test_state') }}", {})],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "battery_level_template"),
(ConfigurationStyle.MODERN, "battery_level"),
(ConfigurationStyle.TRIGGER, "battery_level"),
],
)
@pytest.mark.parametrize(
("attribute_template", "expected"),
[
("{{ '0' }}", 0),
("{{ 100 }}", 100),
("{{ 101 }}", None),
("{{ -1 }}", None),
("{{ 'foo' }}", None),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_battery_level_template(
hass: HomeAssistant, expected: int | None
) -> None:
"""Test templates with values from other entities."""
await async_trigger(hass, TEST_STATE_ENTITY_ID)
_verify(hass, STATE_UNKNOWN, expected)
@pytest.mark.parametrize(
("count", "state_template", "extra_config", "attribute_template"),
[(1, "{{ states('sensor.test_state') }}", {}, "{{ 50 }}")],
)
@pytest.mark.parametrize(
("style", "attribute", "issue_count"),
[
(ConfigurationStyle.LEGACY, "battery_level_template", 2),
(ConfigurationStyle.MODERN, "battery_level", 1),
(ConfigurationStyle.TRIGGER, "battery_level", 1),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_battery_level_template_repair(
hass: HomeAssistant,
issue_count: int,
issue_registry: ir.IssueRegistry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test battery_level template raises issue."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, VacuumActivity.DOCKED)
assert len(issue_registry.issues) == issue_count
issue = issue_registry.async_get_issue(
"template", f"deprecated_battery_level_{TEST_VACUUM.entity_id}"
)
assert issue.domain == "template"
assert issue.severity == ir.IssueSeverity.WARNING
assert issue.translation_placeholders["entity_name"] == TEST_VACUUM.object_id
assert issue.translation_placeholders["entity_id"] == TEST_VACUUM.entity_id
assert "Detected that integration 'template' is setting the" not in caplog.text
@pytest.mark.parametrize(
("count", "state_template", "extra_config"),
[
(
1,
"{{ states('sensor.test_state') }}",
{
"fan_speeds": ["low", "medium", "high"],
},
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "fan_speed_template"),
(ConfigurationStyle.MODERN, "fan_speed"),
(ConfigurationStyle.TRIGGER, "fan_speed"),
],
)
@pytest.mark.parametrize(
("attribute_template", "expected"),
[
("{{ 'low' }}", "low"),
("{{ 'medium' }}", "medium"),
("{{ 'high' }}", "high"),
("{{ 'invalid' }}", None),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_fan_speed_template(hass: HomeAssistant, expected: str | None) -> None:
"""Test templates with values from other entities."""
await async_trigger(hass, TEST_STATE_ENTITY_ID)
_verify(hass, STATE_UNKNOWN, None, expected)
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config", "attribute"),
[
(
1,
"{{ 'on' }}",
"{% if states.sensor.test_state.state %}mdi:check{% endif %}",
{},
"icon",
)
],
)
@pytest.mark.parametrize(
("style", "expected"),
[
(ConfigurationStyle.MODERN, ""),
(ConfigurationStyle.TRIGGER, None),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_icon_template(hass: HomeAssistant, expected: int) -> None:
"""Test icon template."""
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.attributes.get("icon") == expected
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.attributes["icon"] == "mdi:check"
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config", "attribute"),
[
(
1,
"{{ 'on' }}",
"{% if states.sensor.test_state.state %}local/vacuum.png{% endif %}",
{},
"picture",
)
],
)
@pytest.mark.parametrize(
("style", "expected"),
[
(ConfigurationStyle.MODERN, ""),
(ConfigurationStyle.TRIGGER, None),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_picture_template(hass: HomeAssistant, expected: int) -> None:
"""Test picture template."""
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.attributes.get("entity_picture") == expected
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.attributes["entity_picture"] == "local/vacuum.png"
@pytest.mark.parametrize("extra_config", [{}])
@pytest.mark.parametrize(
("count", "state_template", "attribute_template"),
[
(
1,
None,
"{{ is_state('binary_sensor.availability', 'on') }}",
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "availability_template"),
(ConfigurationStyle.MODERN, "availability"),
(ConfigurationStyle.TRIGGER, "availability"),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_available_template_with_entities(hass: HomeAssistant) -> None:
"""Test availability templates with values from other entities."""
# When template returns true..
hass.states.async_set(TEST_AVAILABILITY_ENTITY, STATE_ON)
await hass.async_block_till_done()
# Device State should not be unavailable
assert hass.states.get(TEST_VACUUM.entity_id).state != STATE_UNAVAILABLE
# When Availability template returns false
hass.states.async_set(TEST_AVAILABILITY_ENTITY, STATE_OFF)
await hass.async_block_till_done()
# device state should be unavailable
assert hass.states.get(TEST_VACUUM.entity_id).state == STATE_UNAVAILABLE
@pytest.mark.parametrize("extra_config", [{}])
@pytest.mark.parametrize(
("count", "state_template", "attribute_template"),
[
(
1,
None,
"{{ x - 12 }}",
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "availability_template"),
(ConfigurationStyle.MODERN, "availability"),
(ConfigurationStyle.TRIGGER, "availability"),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
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."""
await async_trigger(hass, TEST_STATE_ENTITY_ID)
assert hass.states.get(TEST_VACUUM.entity_id) != STATE_UNAVAILABLE
err = "'x' is undefined"
assert err in caplog_setup_text or err in caplog.text
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@pytest.mark.parametrize(
("count", "state_template", "attributes"),
[
(
1,
"{{ 'cleaning' }}",
{"test_attribute": "It {{ states.sensor.test_state.state }}."},
)
],
)
@pytest.mark.usefixtures("setup_attributes_state_vacuum")
async def test_attribute_templates(hass: HomeAssistant) -> None:
"""Test attribute_templates template."""
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.attributes["test_attribute"] == "It ."
hass.states.async_set(TEST_STATE_ENTITY_ID, "Works")
await hass.async_block_till_done()
await async_update_entity(hass, TEST_VACUUM.entity_id)
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.attributes["test_attribute"] == "It Works."
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("count", "state_template", "attributes"),
[
(
1,
"{{ states('sensor.test_state') }}",
{"test_attribute": "{{ this_function_does_not_exist() }}"},
)
],
)
@pytest.mark.usefixtures("setup_attributes_state_vacuum")
async def test_invalid_attribute_template(
hass: HomeAssistant, caplog_setup_text, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that errors are logged if rendering template fails."""
hass.states.async_set(TEST_STATE_ENTITY_ID, "Works")
await hass.async_block_till_done()
assert len(hass.states.async_all("vacuum")) == 1
err = "'this_function_does_not_exist' is undefined"
assert err in caplog_setup_text or err in caplog.text
@pytest.mark.parametrize("config", [TEMPLATE_VACUUM_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 vacuum per id."""
await setup_and_test_unique_id(hass, TEST_VACUUM, style, config)
@pytest.mark.parametrize("config", [TEMPLATE_VACUUM_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 vacuum unique_ids."""
await setup_and_test_nested_unique_id(
hass, TEST_VACUUM, style, entity_registry, config
)
@pytest.mark.parametrize(
("count", "state_template", "extra_config"), [(1, None, START_ACTION)]
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_base_vacuum")
async def test_unused_services(hass: HomeAssistant) -> None:
"""Test calling unused services raises."""
# Pause vacuum
with pytest.raises(HomeAssistantError):
await common.async_pause(hass, TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# Stop vacuum
with pytest.raises(HomeAssistantError):
await common.async_stop(hass, TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# Return vacuum to base
with pytest.raises(HomeAssistantError):
await common.async_return_to_base(hass, TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# Spot cleaning
with pytest.raises(HomeAssistantError):
await common.async_clean_spot(hass, TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# Locate vacuum
with pytest.raises(HomeAssistantError):
await common.async_locate(hass, TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# Set fan's speed
with pytest.raises(HomeAssistantError):
await common.async_set_fan_speed(hass, "medium", TEST_VACUUM.entity_id)
await hass.async_block_till_done()
_verify(hass, STATE_UNKNOWN, None)
@pytest.mark.parametrize(
("count", "state_template"),
[(1, "{{ states('sensor.test_state') }}")],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
"action",
[
"start",
"pause",
"stop",
"clean_spot",
"return_to_base",
"locate",
],
)
@pytest.mark.usefixtures("setup_state_vacuum")
async def test_state_services(
hass: HomeAssistant, action: str, calls: list[ServiceCall]
) -> None:
"""Test locate service."""
await hass.services.async_call(
"vacuum",
action,
{"entity_id": TEST_VACUUM.entity_id},
blocking=True,
)
await hass.async_block_till_done()
# verify
assert_action(TEST_VACUUM, calls, 1, action)
@pytest.mark.parametrize(
("count", "state_template", "attribute_template", "extra_config"),
[
(
1,
"{{ states('sensor.test_state') }}",
"{{ states('sensor.test_attribute') }}",
{
"fan_speeds": ["low", "medium", "high"],
},
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "fan_speed_template"),
(ConfigurationStyle.MODERN, "fan_speed"),
(ConfigurationStyle.TRIGGER, "fan_speed"),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_set_fan_speed(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
"""Test set valid fan speed."""
# Set vacuum's fan speed to high
await common.async_set_fan_speed(hass, "high", TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# verify
assert_action(TEST_VACUUM, calls, 1, "set_fan_speed", fan_speed="high")
# Set fan's speed to medium
await common.async_set_fan_speed(hass, "medium", TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# verify
assert_action(TEST_VACUUM, calls, 2, "set_fan_speed", fan_speed="medium")
@pytest.mark.parametrize(
"extra_config",
[
{
"fan_speeds": ["low", "medium", "high"],
}
],
)
@pytest.mark.parametrize(
("count", "state_template", "attribute_template"),
[
(
1,
"{{ states('sensor.test_state') }}",
"{{ states('sensor.test_attribute') }}",
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.LEGACY, "fan_speed_template"),
(ConfigurationStyle.MODERN, "fan_speed"),
(ConfigurationStyle.TRIGGER, "fan_speed"),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_vacuum")
async def test_set_invalid_fan_speed(
hass: HomeAssistant, calls: list[ServiceCall]
) -> None:
"""Test set invalid fan speed when fan has valid speed."""
# Set vacuum's fan speed to high
await common.async_set_fan_speed(hass, "high", TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# verify
assert_action(TEST_VACUUM, calls, 1, "set_fan_speed", fan_speed="high")
# Set vacuum's fan speed to 'invalid'
await common.async_set_fan_speed(hass, "invalid", TEST_VACUUM.entity_id)
await hass.async_block_till_done()
# verify fan speed is unchanged
assert_action(TEST_VACUUM, calls, 1, "set_fan_speed", fan_speed="high")
@pytest.mark.parametrize(("count", "vacuum_config"), [(1, {"start": []})])
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@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,
),
],
)
@pytest.mark.usefixtures("setup_test_vacuum_with_extra_config")
async def test_empty_action_config(
hass: HomeAssistant,
supported_features: VacuumEntityFeature,
) -> None:
"""Test configuration with empty script."""
await common.async_start(hass, TEST_VACUUM.entity_id)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.attributes["supported_features"] == (
VacuumEntityFeature.STATE | VacuumEntityFeature.START | supported_features
)
@pytest.mark.parametrize(
("count", "vacuum_config"),
[
(
1,
{"name": TEST_VACUUM.object_id, "start": [], **TEMPLATE_VACUUM_ACTIONS},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("service", "expected"),
[
(vacuum.SERVICE_START, VacuumActivity.CLEANING),
(vacuum.SERVICE_PAUSE, VacuumActivity.PAUSED),
(vacuum.SERVICE_STOP, VacuumActivity.IDLE),
(vacuum.SERVICE_RETURN_TO_BASE, VacuumActivity.RETURNING),
(vacuum.SERVICE_CLEAN_SPOT, VacuumActivity.CLEANING),
],
)
@pytest.mark.usefixtures("setup_vacuum")
async def test_assumed_optimistic(
hass: HomeAssistant,
service: str,
expected: VacuumActivity,
calls: list[ServiceCall],
) -> None:
"""Test assumed optimistic."""
await hass.services.async_call(
vacuum.DOMAIN,
service,
{"entity_id": TEST_VACUUM.entity_id},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.state == expected
@pytest.mark.parametrize(
("count", "vacuum_config"),
[
(
1,
{
"name": TEST_VACUUM.object_id,
"state": "{{ states('sensor.test_state') }}",
"start": [],
**TEMPLATE_VACUUM_ACTIONS,
"optimistic": True,
},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("service", "expected"),
[
(vacuum.SERVICE_START, VacuumActivity.CLEANING),
(vacuum.SERVICE_PAUSE, VacuumActivity.PAUSED),
(vacuum.SERVICE_STOP, VacuumActivity.IDLE),
(vacuum.SERVICE_RETURN_TO_BASE, VacuumActivity.RETURNING),
(vacuum.SERVICE_CLEAN_SPOT, VacuumActivity.CLEANING),
],
)
@pytest.mark.usefixtures("setup_vacuum")
async def test_optimistic_option(
hass: HomeAssistant,
service: str,
expected: VacuumActivity,
calls: list[ServiceCall],
) -> None:
"""Test optimistic yaml option."""
hass.states.async_set(TEST_STATE_ENTITY_ID, VacuumActivity.DOCKED)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.state == VacuumActivity.DOCKED
await hass.services.async_call(
vacuum.DOMAIN,
service,
{"entity_id": TEST_VACUUM.entity_id},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.state == expected
hass.states.async_set(TEST_STATE_ENTITY_ID, VacuumActivity.RETURNING)
await hass.async_block_till_done()
hass.states.async_set(TEST_STATE_ENTITY_ID, VacuumActivity.DOCKED)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.state == VacuumActivity.DOCKED
@pytest.mark.parametrize(
("count", "vacuum_config"),
[
(
1,
{
"name": TEST_VACUUM.object_id,
"state": "{{ states('sensor.test_state') }}",
"start": [],
**TEMPLATE_VACUUM_ACTIONS,
"optimistic": False,
},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
"service",
[
vacuum.SERVICE_START,
vacuum.SERVICE_PAUSE,
vacuum.SERVICE_STOP,
vacuum.SERVICE_RETURN_TO_BASE,
vacuum.SERVICE_CLEAN_SPOT,
],
)
@pytest.mark.usefixtures("setup_vacuum")
async def test_not_optimistic(
hass: HomeAssistant,
service: str,
calls: list[ServiceCall],
) -> None:
"""Test optimistic yaml option set to false."""
await hass.services.async_call(
vacuum.DOMAIN,
service,
{"entity_id": TEST_VACUUM.entity_id},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(TEST_VACUUM.entity_id)
assert state.state == STATE_UNKNOWN
@pytest.mark.parametrize(
("count", "vacuum_config"),
[
(
1,
{
"unique_id": TEST_VACUUM.entity_id,
"start": [],
**CLEAN_SEGMENTS_ACTION,
"segments": "{{ [{'id': '1', 'name': 'Livingroom'}, {'id': '2', 'name': 'Kitchen'}] }}",
},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_vacuum")
async def test_clean_area(
hass: HomeAssistant,
calls: list[ServiceCall],
entity_registry: er.EntityRegistry,
) -> None:
"""Test clean area passes segment IDs to action."""
entity_registry.async_update_entity_options(
TEST_VACUUM.entity_id,
vacuum.DOMAIN,
{
"area_mapping": {"area_1": ["1", "2"]},
"last_seen_segments": [
{"id": "1", "name": "Livingroom"},
{"id": "2", "name": "Kitchen"},
],
},
)
await common.async_clean_area(hass, ["area_1"], TEST_VACUUM.entity_id)
await hass.async_block_till_done()
assert_action(TEST_VACUUM, calls, 1, "clean_segments", segment_ids=["1", "2"])
state = hass.states.get(TEST_VACUUM.entity_id)
assert state is not None
assert state.attributes["supported_features"] & VacuumEntityFeature.CLEAN_AREA
@pytest.mark.parametrize(
("count", "vacuum_config"),
[
(
1,
{
"start": [],
**CLEAN_SEGMENTS_ACTION,
},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("extra_config", "expected_segments"),
[
(
{
"unique_id": TEST_VACUUM.entity_id,
"segments": "{{ ["
"{'id': '1', 'name': 'Kitchen'}, "
"{'id': '2', 'name': 'Bedroom', 'group': 'Upstairs'}"
"] }}",
},
[
Segment(id="1", name="Kitchen"),
Segment(id="2", name="Bedroom", group="Upstairs"),
],
),
],
)
@pytest.mark.usefixtures("setup_test_vacuum_with_extra_config")
async def test_get_segments(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
expected_segments: list[Segment],
) -> None:
"""Test get_segments returns segments from template."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, VacuumActivity.DOCKED)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": TEST_VACUUM.entity_id}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {
"segments": [asdict(segment) for segment in expected_segments]
}
@pytest.mark.parametrize(
("count", "vacuum_config"),
[
(
1,
{
"start": [],
**CLEAN_SEGMENTS_ACTION,
},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("extra_config", "err_msg"),
[
(
{
"unique_id": TEST_VACUUM.entity_id,
"segments": "{{ [ {'id': '1'} ] }}",
},
"expected dictionary with keys id, name and optional group and string values",
),
(
{
"unique_id": TEST_VACUUM.entity_id,
"segments": "{{ [ {'name': 'kitchen'} ] }}",
},
"expected dictionary with keys id, name and optional group and string values",
),
(
{
"unique_id": TEST_VACUUM.entity_id,
"segments": "{{ [ {} ] }}",
},
"expected dictionary with keys id, name and optional group and string values",
),
(
{
"unique_id": TEST_VACUUM.entity_id,
"segments": "{{ [ {'id': '1', 'name': 'Kitchen', 'extra_key': 'value'} ] }}",
},
"expected dictionary with keys id, name and optional group and string values",
),
(
{"unique_id": TEST_VACUUM.entity_id, "segments": "{{ [[]] }}"},
"expected dictionary with keys id, name and optional group and string values",
),
(
{
"unique_id": TEST_VACUUM.entity_id,
"segments": "{{ [ {'id': '1', 'name': 'Kitchen'}, [] ] }}",
},
"expected dictionary with keys id, name and optional group and string values",
),
],
)
@pytest.mark.usefixtures("setup_test_vacuum_with_extra_config")
async def test_invalid_segments(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog_setup_text,
caplog: pytest.LogCaptureFixture,
err_msg: str,
) -> None:
"""Test that errors are logged if parsing segment template fails."""
hass.states.async_set(TEST_STATE_ENTITY_ID, "Works")
await hass.async_block_till_done()
assert err_msg in caplog_setup_text or err_msg in caplog.text
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": TEST_VACUUM.entity_id}
)
msg = await client.receive_json()
assert msg["result"] == {"segments": []}
@pytest.mark.parametrize(
("count", "vacuum_config"),
[
(
1,
{
"unique_id": TEST_VACUUM.entity_id,
"start": [],
**CLEAN_SEGMENTS_ACTION,
"segments": "{{ [ {'id': '1', 'name': 'Kitchen'}, {'id': '2', 'name': states('sensor.test_attribute')}] }}",
},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_vacuum")
async def test_raise_segments_changed_issue(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test that issue is raised on segments change."""
hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, "Bedroom")
await hass.async_block_till_done()
entity_registry.async_update_entity_options(
TEST_VACUUM.entity_id,
vacuum.DOMAIN,
{
"last_seen_segments": [
{"id": "1", "name": "Kitchen"},
{"id": "2", "name": "Bedroom"},
],
},
)
hass.states.async_set(TEST_ATTRIBUTE_ENTITY_ID, "Bathroom")
await hass.async_block_till_done()
issue_registry = ir.async_get(hass)
assert len(issue_registry.issues) != 0
@pytest.mark.parametrize(
("vacuum_config", "err_msg"),
[
(
{**START_ACTION},
f"Options `{CONF_SEGMENTS}` and `{CONF_CLEAN_SEGMENTS}` must both exist",
),
],
)
@pytest.mark.parametrize(
("count", "extra_config"),
[
(
0,
{
"segments": "{{ [{'id': '1', 'name': 'Kitchen'}] }}",
},
),
(
0,
{**CLEAN_SEGMENTS_ACTION},
),
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_test_vacuum_with_extra_config")
async def test_segments_part_config(
hass: HomeAssistant,
caplog_setup_text,
caplog: pytest.LogCaptureFixture,
count: int,
err_msg: str,
) -> None:
"""Test creating vacuum with segments, missing required options."""
assert len(hass.states.async_all(vacuum.DOMAIN)) == count
assert err_msg in caplog_setup_text or err_msg in caplog.text
@pytest.mark.parametrize(
("vacuum_config"),
[
{
**START_ACTION,
**CLEAN_SEGMENTS_ACTION,
"segments": "{{ [{'id': '1', 'name': 'Kitchen'}] }}",
},
],
)
@pytest.mark.parametrize(
("count", "extra_config", "err_msg"),
[
(
0,
{},
f'key "{CONF_SEGMENTS}" requires key "{CONF_UNIQUE_ID}" to exist',
),
(1, {"unique_id": TEST_VACUUM.entity_id}, ""),
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_test_vacuum_with_extra_config")
async def test_segments_unique_id(
hass: HomeAssistant,
caplog_setup_text,
caplog: pytest.LogCaptureFixture,
count: int,
err_msg: str,
) -> None:
"""Test creating vacuum with segments, missing required options."""
assert len(hass.states.async_all(vacuum.DOMAIN)) == count
assert err_msg in caplog_setup_text or err_msg in caplog.text
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Tests creating a vacuum from a config entry."""
hass.states.async_set(
"sensor.test_sensor",
"docked",
{},
)
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": "My template",
"state": "{{ states('sensor.test_sensor') }}",
"start": [],
"template_type": vacuum.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("vacuum.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,
vacuum.DOMAIN,
{
"name": "My template",
"state": "{{ 'cleaning' }}",
"start": [],
},
)
assert state["state"] == VacuumActivity.CLEANING