diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index a6eebcc8f246..3428b3416451 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -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": { diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index ee4f9858798a..a608c837d5ca 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -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: diff --git a/tests/components/roomba/test_vacuum.py b/tests/components/roomba/test_vacuum.py index c352adaba3c6..1533ab445927 100644 --- a/tests/components/roomba/test_vacuum.py +++ b/tests/components/roomba/test_vacuum.py @@ -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 "-" 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")