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:
parent
eeeb5b2725
commit
f99ec87338
3 changed files with 75 additions and 2 deletions
|
@ -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"),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue