Fix intents targeting unexposed entities and non-light entities in HassLightSet (#180026)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michael Hansen
2026-08-24 19:26:31 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 404dd9267a
commit f7ca2fcc46
4 changed files with 88 additions and 1 deletions
+1
View File
@@ -45,6 +45,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
),
},
description="Sets the brightness percentage or color of a light",
required_domains={DOMAIN},
platforms={DOMAIN},
),
)
@@ -272,6 +272,7 @@ class WyomingConversationEntity(
intent_slots,
text_input=user_input.text,
language=user_input.language,
assistant=conversation.DOMAIN,
satellite_id=user_input.satellite_id,
device_id=user_input.device_id,
)
+84 -1
View File
@@ -1,13 +1,20 @@
"""Tests for the light intents."""
import pytest
from homeassistant.components import light
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode, intent
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.intent import async_handle
from homeassistant.helpers import area_registry as ar, entity_registry as er
from homeassistant.helpers.intent import MatchFailedError, async_handle
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
ASSISTANT = "conversation"
async def test_intent_set_color(hass: HomeAssistant) -> None:
"""Test the set color intent."""
@@ -89,3 +96,79 @@ async def test_intent_set_temperature(hass: HomeAssistant) -> None:
assert call.service == SERVICE_TURN_ON
assert call.data.get(ATTR_ENTITY_ID) == "light.test"
assert call.data.get(light.ATTR_COLOR_TEMP_KELVIN) == 2000
async def test_intent_set_area_only_targets_lights(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that only lights are targeted when an area is used without a domain slot."""
kitchen = area_registry.async_create("Kitchen")
for domain, unique_id in (("light", "l1"), ("switch", "s1"), ("sensor", "t1")):
entry = entity_registry.async_get_or_create(domain, "test", unique_id)
entity_registry.async_update_entity(entry.entity_id, area_id=kitchen.id)
hass.states.async_set(entry.entity_id, "off")
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await intent.async_setup_intents(hass)
await async_handle(
hass,
"test",
intent.INTENT_SET,
{"area": {"value": "Kitchen"}, "brightness": {"value": "20"}},
)
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data.get(ATTR_ENTITY_ID) == "light.test_l1"
assert calls[0].data.get(light.ATTR_BRIGHTNESS_PCT) == 20
async def test_intent_set_without_name_or_area_skips_unexposed(
hass: HomeAssistant,
) -> None:
"""Test that unexposed lights are not targeted when no name or area is given."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.exposed", "off")
hass.states.async_set("light.hidden", "off")
async_expose_entity(hass, ASSISTANT, "light.hidden", False)
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await intent.async_setup_intents(hass)
await async_handle(
hass,
"test",
intent.INTENT_SET,
{"brightness": {"value": "20"}},
assistant=ASSISTANT,
)
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data.get(ATTR_ENTITY_ID) == "light.exposed"
async def test_intent_set_without_name_or_area_all_unexposed(
hass: HomeAssistant,
) -> None:
"""Test that no lights are targeted when none are exposed."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.hidden", "off")
async_expose_entity(hass, ASSISTANT, "light.hidden", False)
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await intent.async_setup_intents(hass)
with pytest.raises(MatchFailedError):
await async_handle(
hass,
"test",
intent.INTENT_SET,
{"brightness": {"value": "20"}},
assistant=ASSISTANT,
)
assert not calls
@@ -64,6 +64,8 @@ async def test_intent(
assert intent_obj.slots.get("entity", {}).get("value") == "value"
assert intent_obj.satellite_id == satellite_id
assert intent_obj.device_id == device_id
# Entities must be filtered by exposure to the conversation assistant
assert intent_obj.assistant == conversation.DOMAIN
response = intent_obj.create_response()
# Add parts to test response rendering