diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 8f5ace63be8..6f9c221b1ca 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -243,6 +243,19 @@ class MatchTargetsConstraints: allow_duplicate_names: bool = False """True if entities with duplicate names are allowed in result.""" + @property + def has_constraints(self) -> bool: + """Returns True if at least one constraint is set (ignores assistant).""" + return bool( + self.name + or self.area_name + or self.floor_name + or self.domains + or self.device_classes + or self.features + or self.states + ) + @dataclass class MatchTargetsPreferences: @@ -766,6 +779,15 @@ class IntentHandler: return f"<{self.__class__.__name__} - {self.intent_type}>" +def non_empty_string(value: Any) -> str: + """Coerce value to string and fail if string is empty or whitespace.""" + value_str = cv.string(value) + if not value_str.strip(): + raise vol.Invalid("string value is empty") + + return value_str + + class DynamicServiceIntentHandler(IntentHandler): """Service Intent handler registration (dynamic). @@ -817,7 +839,7 @@ class DynamicServiceIntentHandler(IntentHandler): def slot_schema(self) -> dict: """Return a slot schema.""" slot_schema = { - vol.Any("name", "area", "floor"): cv.string, + vol.Any("name", "area", "floor"): non_empty_string, vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), vol.Optional("preferred_area_id"): cv.string, @@ -892,6 +914,10 @@ class DynamicServiceIntentHandler(IntentHandler): features=self.required_features, states=self.required_states, ) + if not match_constraints.has_constraints: + # Fail if attempting to target all devices in the house + raise IntentHandleError("Service handler cannot target all devices") + match_preferences = MatchTargetsPreferences( area_id=slots.get("preferred_area_id", {}).get("value"), floor_id=slots.get("preferred_floor_id", {}).get("value"), diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 586ea7dd8a2..95d1ee78538 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -236,7 +236,12 @@ async def test_turn_on_all(hass: HomeAssistant) -> None: hass.states.async_set("light.test_light_2", "off") calls = async_mock_service(hass, "light", SERVICE_TURN_ON) - await intent.async_handle(hass, "test", "HassTurnOn", {"name": {"value": "all"}}) + await intent.async_handle( + hass, + "test", + "HassTurnOn", + {"name": {"value": "all"}, "domain": {"value": "light"}}, + ) await hass.async_block_till_done() # All lights should be on now diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index f9efd52d727..9f62e76ebc0 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -771,3 +771,45 @@ async def test_service_intent_handler_required_domains(hass: HomeAssistant) -> N "TestType", slots={"name": {"value": "bedroom"}, "domain": {"value": "switch"}}, ) + + +async def test_service_handler_empty_strings(hass: HomeAssistant) -> None: + """Test that passing empty strings for filters fails in ServiceIntentHandler.""" + handler = intent.ServiceIntentHandler( + "TestType", "light", "turn_on", "Turned {} on" + ) + intent.async_register(hass, handler) + + for slot_name in ("name", "area", "floor"): + # Empty string + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + "TestType", + slots={slot_name: {"value": ""}}, + ) + + # Whitespace + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + "TestType", + slots={slot_name: {"value": " "}}, + ) + + +async def test_service_handler_no_filter(hass: HomeAssistant) -> None: + """Test that targeting all devices in the house fails.""" + handler = intent.ServiceIntentHandler( + "TestType", "light", "turn_on", "Turned {} on" + ) + intent.async_register(hass, handler) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + "TestType", + )