Fail if targeting all devices in the house in service intent handler (#117930)

* Fail if targeting all devices in the house

* Update homeassistant/helpers/intent.py

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Michael Hansen 2024-05-22 12:53:31 -05:00 committed by GitHub
parent eeeb5b2725
commit f99ec87338
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 75 additions and 2 deletions

View file

@ -243,6 +243,19 @@ class MatchTargetsConstraints:
allow_duplicate_names: bool = False allow_duplicate_names: bool = False
"""True if entities with duplicate names are allowed in result.""" """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 @dataclass
class MatchTargetsPreferences: class MatchTargetsPreferences:
@ -766,6 +779,15 @@ class IntentHandler:
return f"<{self.__class__.__name__} - {self.intent_type}>" 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): class DynamicServiceIntentHandler(IntentHandler):
"""Service Intent handler registration (dynamic). """Service Intent handler registration (dynamic).
@ -817,7 +839,7 @@ class DynamicServiceIntentHandler(IntentHandler):
def slot_schema(self) -> dict: def slot_schema(self) -> dict:
"""Return a slot schema.""" """Return a slot schema."""
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("domain"): vol.All(cv.ensure_list, [cv.string]),
vol.Optional("device_class"): 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, vol.Optional("preferred_area_id"): cv.string,
@ -892,6 +914,10 @@ class DynamicServiceIntentHandler(IntentHandler):
features=self.required_features, features=self.required_features,
states=self.required_states, 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( match_preferences = MatchTargetsPreferences(
area_id=slots.get("preferred_area_id", {}).get("value"), area_id=slots.get("preferred_area_id", {}).get("value"),
floor_id=slots.get("preferred_floor_id", {}).get("value"), floor_id=slots.get("preferred_floor_id", {}).get("value"),

View file

@ -236,7 +236,12 @@ async def test_turn_on_all(hass: HomeAssistant) -> None:
hass.states.async_set("light.test_light_2", "off") hass.states.async_set("light.test_light_2", "off")
calls = async_mock_service(hass, "light", SERVICE_TURN_ON) 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() await hass.async_block_till_done()
# All lights should be on now # All lights should be on now

View file

@ -771,3 +771,45 @@ async def test_service_intent_handler_required_domains(hass: HomeAssistant) -> N
"TestType", "TestType",
slots={"name": {"value": "bedroom"}, "domain": {"value": "switch"}}, 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",
)