Prefer shorter keys for intent matching (#43672)

When using fuzzy matching to match entity names for intents, whichever
entity is first is preferred in the case of equal matches. This leads
to situations where entities with similar names (such as entities named
for their area and then specific area location) may be used when the
whole area is wanted.

I ran into this with the my Phillips Hue lights. I have each individual
light named such that its room is the first part of the name, and its
location within the room after. So my living room has:
Living Room West
Living Room Northwest
Living Room North
Living Room Northeast

I then have a group for the whole room:
Living Room

Because the group is the last of the entities, trying to adjust the
whole room only activates one light, because all of the lights match
equally well.

By preferring the shortest of equal matches, we prefer keys that have
the least amount of extra information, causing "Living Room" to match
the group instead of an individual light.
This commit is contained in:
CtrlZvi 2021-01-27 03:16:19 -08:00 committed by GitHub
parent 3841f0e42d
commit 1433cdaa12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 24 additions and 3 deletions

View File

@ -169,10 +169,13 @@ def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> Optio
for idx, item in enumerate(items):
match = regex.search(key(item))
if match:
# Add index so we pick first match in case same group and start
matches.append((len(match.group()), match.start(), idx, item))
# Add key length so we prefer shorter keys with the same group and start.
# Add index so we pick first match in case same group, start, and key length.
matches.append(
(len(match.group()), match.start(), len(key(item)), idx, item)
)
return sorted(matches)[0][3] if matches else None
return sorted(matches)[0][4] if matches else None
class ServiceIntentHandler(IntentHandler):

View File

@ -38,3 +38,21 @@ def test_async_validate_slots():
handler1.async_validate_slots(
{"name": {"value": "kitchen"}, "probability": {"value": "0.5"}}
)
def test_fuzzy_match():
"""Test _fuzzymatch."""
state1 = State("light.living_room_northwest", "off")
state2 = State("light.living_room_north", "off")
state3 = State("light.living_room_northeast", "off")
state4 = State("light.living_room_west", "off")
state5 = State("light.living_room", "off")
states = [state1, state2, state3, state4, state5]
state = intent._fuzzymatch("Living Room", states, lambda state: state.name)
assert state == state5
state = intent._fuzzymatch(
"Living Room Northwest", states, lambda state: state.name
)
assert state == state1