Fix swallowed exceptions in roomba action handlers (#181042)

This commit is contained in:
Jason Dillingham
2026-09-02 08:20:07 +02:00
committed by GitHub
parent 13b9ef09f2
commit 8f1391e15f
3 changed files with 149 additions and 24 deletions
@@ -89,6 +89,23 @@
}
}
},
"exceptions": {
"invalid_fan_speed": {
"message": "Invalid fan speed {fan_speed}. Expected one of: {fan_speeds}."
},
"invalid_fan_speed_format": {
"message": "Invalid fan speed {fan_speed}. Expected the format behavior-spray_amount, for example Standard-1."
},
"invalid_mop_behavior": {
"message": "Invalid mop behavior {behavior}. Expected one of: {behaviors}."
},
"invalid_spray_amount": {
"message": "Invalid spray amount {spray_amount}. Expected one of: {spray_amounts}."
},
"spray_amount_not_a_number": {
"message": "Invalid spray amount {spray_amount}. Expected a whole number."
}
},
"options": {
"step": {
"init": {
+36 -22
View File
@@ -11,11 +11,13 @@ from homeassistant.components.vacuum import (
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_system import METRIC_SYSTEM
from . import roomba_reported_state
from .const import DOMAIN
from .entity import IRobotEntity
from .models import RoombaConfigEntry
@@ -317,8 +319,14 @@ class RoombaVacuumCarpetBoost(RoombaVacuum):
high_perf = True
carpet_boost = False
else:
_LOGGER.error("No such fan speed available: %s", fan_speed)
return
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_fan_speed",
translation_placeholders={
"fan_speed": fan_speed,
"fan_speeds": ", ".join(FAN_SPEEDS),
},
)
# The set_preference method does only accept string values
def _set_fan_speed_preferences() -> None:
@@ -371,30 +379,36 @@ class BraavaJet(IRobotVacuum):
spray = int(split[1])
if behavior.capitalize() in BRAAVA_MOP_BEHAVIORS:
behavior = behavior.capitalize()
# pylint: disable-next=home-assistant-action-swallowed-exception
except IndexError:
_LOGGER.error(
"Fan speed error: expected {behavior}-{spray_amount}, got '%s'",
fan_speed,
)
return
except ValueError:
_LOGGER.error("Spray amount error: expected integer, got '%s'", split[1])
return
except IndexError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_fan_speed_format",
translation_placeholders={"fan_speed": fan_speed},
) from err
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="spray_amount_not_a_number",
translation_placeholders={"spray_amount": split[1]},
) from err
if behavior not in BRAAVA_MOP_BEHAVIORS:
_LOGGER.error(
"Mop behavior error: expected one of %s, got '%s'",
str(BRAAVA_MOP_BEHAVIORS),
behavior,
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_mop_behavior",
translation_placeholders={
"behavior": behavior,
"behaviors": ", ".join(BRAAVA_MOP_BEHAVIORS),
},
)
return
if spray not in BRAAVA_SPRAY_AMOUNT:
_LOGGER.error(
"Spray amount error: expected one of %s, got '%d'",
str(BRAAVA_SPRAY_AMOUNT),
spray,
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_spray_amount",
translation_placeholders={
"spray_amount": str(spray),
"spray_amounts": ", ".join(str(s) for s in BRAAVA_SPRAY_AMOUNT),
},
)
return
overlap = 0
if behavior == MOP_STANDARD:
+96 -2
View File
@@ -4,15 +4,29 @@ from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.vacuum import VacuumActivity
from homeassistant.const import Platform
from homeassistant.components.vacuum import (
ATTR_FAN_SPEED,
DOMAIN as VACUUM_DOMAIN,
SERVICE_SET_FAN_SPEED,
VacuumActivity,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from tests.common import MockConfigEntry
ENTITY_ID = "vacuum.test_roomba"
async def _setup(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None:
"""Set up the vacuum platform only."""
with patch("homeassistant.components.roomba.PLATFORMS", [Platform.VACUUM]):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
@pytest.mark.parametrize(
("phase", "cycle", "expected"),
[
@@ -52,3 +66,83 @@ async def test_vacuum_activity(
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == expected
@pytest.mark.parametrize(
("fan_speed", "translation_key"),
[
# Missing the "-<spray amount>" half entirely.
("Standard", "invalid_fan_speed_format"),
# Spray amount present but not a number.
("Standard-x", "spray_amount_not_a_number"),
# Well-formed, but the behavior is not one we support.
("Bogus-1", "invalid_mop_behavior"),
# Well-formed, but the spray amount is out of range.
("Standard-9", "invalid_spray_amount"),
],
)
async def test_braava_set_fan_speed_invalid(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_roomba: AsyncMock,
fan_speed: str,
translation_key: str,
) -> None:
"""Test that invalid Braava fan speeds raise instead of being swallowed."""
mock_roomba.master_state["state"]["reported"]["detectedPad"] = "reusableWet"
await _setup(hass, mock_config_entry)
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
VACUUM_DOMAIN,
SERVICE_SET_FAN_SPEED,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: fan_speed},
blocking=True,
)
assert err.value.translation_domain == "roomba"
assert err.value.translation_key == translation_key
async def test_carpet_boost_set_fan_speed_invalid(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_roomba: AsyncMock,
) -> None:
"""Test that an unknown carpet-boost fan speed raises instead of being swallowed."""
mock_roomba.master_state["state"]["reported"]["cap"]["carpetBoost"] = 1
await _setup(hass, mock_config_entry)
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
VACUUM_DOMAIN,
SERVICE_SET_FAN_SPEED,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: "Turbo"},
blocking=True,
)
assert err.value.translation_domain == "roomba"
assert err.value.translation_key == "invalid_fan_speed"
async def test_carpet_boost_set_fan_speed_valid(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_roomba: AsyncMock,
) -> None:
"""Test that a valid fan speed still sets the preferences."""
mock_roomba.master_state["state"]["reported"]["cap"]["carpetBoost"] = 1
await _setup(hass, mock_config_entry)
await hass.services.async_call(
VACUUM_DOMAIN,
SERVICE_SET_FAN_SPEED,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: "eco"},
blocking=True,
)
mock_roomba.set_preference.assert_any_call("carpetBoost", "False")
mock_roomba.set_preference.assert_any_call("vacHigh", "False")