From 1433cdaa128106252b187adac97a557c3206eeeb Mon Sep 17 00:00:00 2001 From: CtrlZvi Date: Wed, 27 Jan 2021 03:16:19 -0800 Subject: [PATCH] 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. --- homeassistant/helpers/intent.py | 9 ++++++--- tests/helpers/test_intent.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index df5e40911ff..f8c8b2c6d8c 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -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): diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index bbb6e394ee0..e328d30ab7a 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -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