Check supported features in calls to vacuum services (#95833)

* Check supported features in vacuum services

* Update tests

* Add comment
This commit is contained in:
Erik Montnemery 2023-07-10 13:05:47 +02:00 committed by GitHub
parent 7dc03ef301
commit 96c71b214f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 81 deletions

View File

@ -77,19 +77,19 @@ DEFAULT_NAME = "Vacuum cleaner robot"
class VacuumEntityFeature(IntFlag):
"""Supported features of the vacuum entity."""
TURN_ON = 1
TURN_OFF = 2
TURN_ON = 1 # Deprecated, not supported by StateVacuumEntity
TURN_OFF = 2 # Deprecated, not supported by StateVacuumEntity
PAUSE = 4
STOP = 8
RETURN_HOME = 16
FAN_SPEED = 32
BATTERY = 64
STATUS = 128
STATUS = 128 # Deprecated, not supported by StateVacuumEntity
SEND_COMMAND = 256
LOCATE = 512
CLEAN_SPOT = 1024
MAP = 2048
STATE = 4096
STATE = 4096 # Must be set by vacuum platforms derived from StateVacuumEntity
START = 8192
@ -127,24 +127,73 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await component.async_setup(config)
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
component.async_register_entity_service(
SERVICE_START_PAUSE, {}, "async_start_pause"
SERVICE_TURN_ON,
{},
"async_turn_on",
[VacuumEntityFeature.TURN_ON],
)
component.async_register_entity_service(SERVICE_START, {}, "async_start")
component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause")
component.async_register_entity_service(
SERVICE_RETURN_TO_BASE, {}, "async_return_to_base"
SERVICE_TURN_OFF,
{},
"async_turn_off",
[VacuumEntityFeature.TURN_OFF],
)
component.async_register_entity_service(
SERVICE_TOGGLE,
{},
"async_toggle",
[VacuumEntityFeature.TURN_OFF | VacuumEntityFeature.TURN_ON],
)
# start_pause is a legacy service, only supported by VacuumEntity, and only needs
# VacuumEntityFeature.PAUSE
component.async_register_entity_service(
SERVICE_START_PAUSE,
{},
"async_start_pause",
[VacuumEntityFeature.PAUSE],
)
component.async_register_entity_service(
SERVICE_START,
{},
"async_start",
[VacuumEntityFeature.START],
)
component.async_register_entity_service(
SERVICE_PAUSE,
{},
"async_pause",
[VacuumEntityFeature.PAUSE],
)
component.async_register_entity_service(
SERVICE_RETURN_TO_BASE,
{},
"async_return_to_base",
[VacuumEntityFeature.RETURN_HOME],
)
component.async_register_entity_service(
SERVICE_CLEAN_SPOT,
{},
"async_clean_spot",
[VacuumEntityFeature.CLEAN_SPOT],
)
component.async_register_entity_service(
SERVICE_LOCATE,
{},
"async_locate",
[VacuumEntityFeature.LOCATE],
)
component.async_register_entity_service(
SERVICE_STOP,
{},
"async_stop",
[VacuumEntityFeature.STOP],
)
component.async_register_entity_service(SERVICE_CLEAN_SPOT, {}, "async_clean_spot")
component.async_register_entity_service(SERVICE_LOCATE, {}, "async_locate")
component.async_register_entity_service(SERVICE_STOP, {}, "async_stop")
component.async_register_entity_service(
SERVICE_SET_FAN_SPEED,
{vol.Required(ATTR_FAN_SPEED): cv.string},
"async_set_fan_speed",
[VacuumEntityFeature.FAN_SPEED],
)
component.async_register_entity_service(
SERVICE_SEND_COMMAND,
@ -153,6 +202,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list),
},
"async_send_command",
[VacuumEntityFeature.SEND_COMMAND],
)
return True

View File

@ -37,8 +37,8 @@ VALID_STATES_STATE = {
STATE_CLEANING,
STATE_DOCKED,
STATE_IDLE,
STATE_RETURNING,
STATE_PAUSED,
STATE_RETURNING,
}

View File

@ -37,6 +37,7 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@ -201,42 +202,39 @@ async def test_unsupported_methods(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert vacuum.is_on(hass, ENTITY_VACUUM_NONE)
await common.async_turn_off(hass, ENTITY_VACUUM_NONE)
assert vacuum.is_on(hass, ENTITY_VACUUM_NONE)
with pytest.raises(HomeAssistantError):
await common.async_turn_off(hass, ENTITY_VACUUM_NONE)
await common.async_stop(hass, ENTITY_VACUUM_NONE)
assert vacuum.is_on(hass, ENTITY_VACUUM_NONE)
with pytest.raises(HomeAssistantError):
await common.async_stop(hass, ENTITY_VACUUM_NONE)
hass.states.async_set(ENTITY_VACUUM_NONE, STATE_OFF)
await hass.async_block_till_done()
assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE)
await common.async_turn_on(hass, ENTITY_VACUUM_NONE)
assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE)
with pytest.raises(HomeAssistantError):
await common.async_turn_on(hass, ENTITY_VACUUM_NONE)
await common.async_toggle(hass, ENTITY_VACUUM_NONE)
assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE)
with pytest.raises(HomeAssistantError):
await common.async_toggle(hass, ENTITY_VACUUM_NONE)
# Non supported methods:
await common.async_start_pause(hass, ENTITY_VACUUM_NONE)
assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE)
with pytest.raises(HomeAssistantError):
await common.async_start_pause(hass, ENTITY_VACUUM_NONE)
await common.async_locate(hass, ENTITY_VACUUM_NONE)
state = hass.states.get(ENTITY_VACUUM_NONE)
assert state.attributes.get(ATTR_STATUS) is None
with pytest.raises(HomeAssistantError):
await common.async_locate(hass, ENTITY_VACUUM_NONE)
await common.async_return_to_base(hass, ENTITY_VACUUM_NONE)
state = hass.states.get(ENTITY_VACUUM_NONE)
assert state.attributes.get(ATTR_STATUS) is None
with pytest.raises(HomeAssistantError):
await common.async_return_to_base(hass, ENTITY_VACUUM_NONE)
await common.async_set_fan_speed(hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_NONE)
state = hass.states.get(ENTITY_VACUUM_NONE)
assert state.attributes.get(ATTR_FAN_SPEED) != FAN_SPEEDS[-1]
with pytest.raises(HomeAssistantError):
await common.async_set_fan_speed(
hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_NONE
)
await common.async_clean_spot(hass, entity_id=ENTITY_VACUUM_BASIC)
state = hass.states.get(ENTITY_VACUUM_BASIC)
assert "spot" not in state.attributes.get(ATTR_STATUS)
assert state.state == STATE_OFF
with pytest.raises(HomeAssistantError):
await common.async_clean_spot(hass, entity_id=ENTITY_VACUUM_BASIC)
# VacuumEntity should not support start and pause methods.
hass.states.async_set(ENTITY_VACUUM_COMPLETE, STATE_ON)
@ -250,21 +248,18 @@ async def test_unsupported_methods(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert not vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE)
await common.async_start(hass, ENTITY_VACUUM_COMPLETE)
assert not vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE)
with pytest.raises(HomeAssistantError):
await common.async_start(hass, ENTITY_VACUUM_COMPLETE)
# StateVacuumEntity does not support on/off
await common.async_turn_on(hass, entity_id=ENTITY_VACUUM_STATE)
state = hass.states.get(ENTITY_VACUUM_STATE)
assert state.state != STATE_CLEANING
with pytest.raises(HomeAssistantError):
await common.async_turn_on(hass, entity_id=ENTITY_VACUUM_STATE)
await common.async_turn_off(hass, entity_id=ENTITY_VACUUM_STATE)
state = hass.states.get(ENTITY_VACUUM_STATE)
assert state.state != STATE_RETURNING
with pytest.raises(HomeAssistantError):
await common.async_turn_off(hass, entity_id=ENTITY_VACUUM_STATE)
await common.async_toggle(hass, entity_id=ENTITY_VACUUM_STATE)
state = hass.states.get(ENTITY_VACUUM_STATE)
assert state.state != STATE_CLEANING
with pytest.raises(HomeAssistantError):
await common.async_toggle(hass, entity_id=ENTITY_VACUUM_STATE)
async def test_services(hass: HomeAssistant) -> None:
@ -302,22 +297,15 @@ async def test_services(hass: HomeAssistant) -> None:
async def test_set_fan_speed(hass: HomeAssistant) -> None:
"""Test vacuum service to set the fan speed."""
group_vacuums = ",".join(
[ENTITY_VACUUM_BASIC, ENTITY_VACUUM_COMPLETE, ENTITY_VACUUM_STATE]
)
old_state_basic = hass.states.get(ENTITY_VACUUM_BASIC)
group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE, ENTITY_VACUUM_STATE])
old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE)
old_state_state = hass.states.get(ENTITY_VACUUM_STATE)
await common.async_set_fan_speed(hass, FAN_SPEEDS[0], entity_id=group_vacuums)
new_state_basic = hass.states.get(ENTITY_VACUUM_BASIC)
new_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE)
new_state_state = hass.states.get(ENTITY_VACUUM_STATE)
assert old_state_basic == new_state_basic
assert ATTR_FAN_SPEED not in new_state_basic.attributes
assert old_state_complete != new_state_complete
assert old_state_complete.attributes[ATTR_FAN_SPEED] == FAN_SPEEDS[1]
assert new_state_complete.attributes[ATTR_FAN_SPEED] == FAN_SPEEDS[0]
@ -329,18 +317,15 @@ async def test_set_fan_speed(hass: HomeAssistant) -> None:
async def test_send_command(hass: HomeAssistant) -> None:
"""Test vacuum service to send a command."""
group_vacuums = ",".join([ENTITY_VACUUM_BASIC, ENTITY_VACUUM_COMPLETE])
old_state_basic = hass.states.get(ENTITY_VACUUM_BASIC)
group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE])
old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE)
await common.async_send_command(
hass, "test_command", params={"p1": 3}, entity_id=group_vacuums
)
new_state_basic = hass.states.get(ENTITY_VACUUM_BASIC)
new_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE)
assert old_state_basic == new_state_basic
assert old_state_complete != new_state_complete
assert new_state_complete.state == STATE_ON
assert (

View File

@ -32,6 +32,7 @@ from homeassistant.components.vacuum import (
)
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
from .test_common import (
@ -245,39 +246,48 @@ async def test_commands_without_supported_features(
"""Test commands which are not supported by the vacuum."""
mqtt_mock = await mqtt_mock_entry()
await common.async_turn_on(hass, "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_turn_on(hass, "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_turn_off(hass, "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_turn_off(hass, "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_stop(hass, "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_stop(hass, "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_clean_spot(hass, "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_clean_spot(hass, "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_locate(hass, "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_locate(hass, "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_start_pause(hass, "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_start_pause(hass, "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_return_to_base(hass, "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_return_to_base(hass, "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_set_fan_speed(hass, "high", "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_set_fan_speed(hass, "high", "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_send_command(hass, "44 FE 93", entity_id="vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_send_command(hass, "44 FE 93", entity_id="vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()

View File

@ -29,6 +29,7 @@ from homeassistant.components.vacuum import (
)
from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .test_common import (
help_custom_config,
@ -242,13 +243,15 @@ async def test_commands_without_supported_features(
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_set_fan_speed(hass, "medium", "vacuum.mqtttest")
with pytest.raises(HomeAssistantError):
await common.async_set_fan_speed(hass, "medium", "vacuum.mqtttest")
mqtt_mock.async_publish.assert_not_called()
mqtt_mock.async_publish.reset_mock()
await common.async_send_command(
hass, "44 FE 93", {"key": "value"}, entity_id="vacuum.mqtttest"
)
with pytest.raises(HomeAssistantError):
await common.async_send_command(
hass, "44 FE 93", {"key": "value"}, entity_id="vacuum.mqtttest"
)
mqtt_mock.async_publish.assert_not_called()

View File

@ -12,6 +12,7 @@ from homeassistant.components.vacuum import (
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_component import async_update_entity
from tests.common import assert_setup_component
@ -317,31 +318,37 @@ async def test_unique_id(hass: HomeAssistant, start_ha) -> None:
async def test_unused_services(hass: HomeAssistant) -> None:
"""Test calling unused services should not crash."""
"""Test calling unused services raises."""
await _register_basic_vacuum(hass)
# Pause vacuum
await common.async_pause(hass, _TEST_VACUUM)
with pytest.raises(HomeAssistantError):
await common.async_pause(hass, _TEST_VACUUM)
await hass.async_block_till_done()
# Stop vacuum
await common.async_stop(hass, _TEST_VACUUM)
with pytest.raises(HomeAssistantError):
await common.async_stop(hass, _TEST_VACUUM)
await hass.async_block_till_done()
# Return vacuum to base
await common.async_return_to_base(hass, _TEST_VACUUM)
with pytest.raises(HomeAssistantError):
await common.async_return_to_base(hass, _TEST_VACUUM)
await hass.async_block_till_done()
# Spot cleaning
await common.async_clean_spot(hass, _TEST_VACUUM)
with pytest.raises(HomeAssistantError):
await common.async_clean_spot(hass, _TEST_VACUUM)
await hass.async_block_till_done()
# Locate vacuum
await common.async_locate(hass, _TEST_VACUUM)
with pytest.raises(HomeAssistantError):
await common.async_locate(hass, _TEST_VACUUM)
await hass.async_block_till_done()
# Set fan's speed
await common.async_set_fan_speed(hass, "medium", _TEST_VACUUM)
with pytest.raises(HomeAssistantError):
await common.async_set_fan_speed(hass, "medium", _TEST_VACUUM)
await hass.async_block_till_done()
_verify(hass, STATE_UNKNOWN, None)