mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Check area temperature sensors in get temperature intent (#139221)
* Check area temperature sensors in get temperature intent * Fix candidate check * Add new code back in * Remove cruft from climate
This commit is contained in:
parent
e9bb4625d8
commit
49c27ae7bc
@ -2,13 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.components import http, sensor
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
@ -40,7 +41,12 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv, integration_platform, intent
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
config_validation as cv,
|
||||
integration_platform,
|
||||
intent,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@ -454,6 +460,9 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
slot_schema = {
|
||||
vol.Optional("area"): intent.non_empty_string,
|
||||
vol.Optional("name"): intent.non_empty_string,
|
||||
vol.Optional("floor"): intent.non_empty_string,
|
||||
vol.Optional("preferred_area_id"): cv.string,
|
||||
vol.Optional("preferred_floor_id"): cv.string,
|
||||
}
|
||||
platforms = {CLIMATE_DOMAIN}
|
||||
|
||||
@ -470,13 +479,71 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
if "area" in slots:
|
||||
area = slots["area"]["value"]
|
||||
|
||||
floor_name: str | None = None
|
||||
if "floor" in slots:
|
||||
floor_name = slots["floor"]["value"]
|
||||
|
||||
match_preferences = intent.MatchTargetsPreferences(
|
||||
area_id=slots.get("preferred_area_id", {}).get("value"),
|
||||
floor_id=slots.get("preferred_floor_id", {}).get("value"),
|
||||
)
|
||||
|
||||
if (not name) and (area or match_preferences.area_id):
|
||||
# Look for temperature sensors assigned to an area
|
||||
area_registry = ar.async_get(hass)
|
||||
area_temperature_ids: dict[str, str] = {}
|
||||
|
||||
# Keep candidates that are registered as area temperature sensors
|
||||
def area_candidate_filter(
|
||||
candidate: intent.MatchTargetsCandidate,
|
||||
possible_area_ids: Collection[str],
|
||||
) -> bool:
|
||||
for area_id in possible_area_ids:
|
||||
temperature_id = area_temperature_ids.get(area_id)
|
||||
if (temperature_id is None) and (
|
||||
area_entry := area_registry.async_get_area(area_id)
|
||||
):
|
||||
temperature_id = area_entry.temperature_entity_id or ""
|
||||
area_temperature_ids[area_id] = temperature_id
|
||||
|
||||
if candidate.state.entity_id == temperature_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
area_name=area,
|
||||
floor_name=floor_name,
|
||||
domains=[sensor.DOMAIN],
|
||||
device_classes=[sensor.SensorDeviceClass.TEMPERATURE],
|
||||
assistant=intent_obj.assistant,
|
||||
single_target=True,
|
||||
)
|
||||
match_result = intent.async_match_targets(
|
||||
hass,
|
||||
match_constraints,
|
||||
match_preferences,
|
||||
area_candidate_filter=area_candidate_filter,
|
||||
)
|
||||
if match_result.is_match:
|
||||
# Found temperature sensor
|
||||
response = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.QUERY_ANSWER
|
||||
response.async_set_states(matched_states=match_result.states)
|
||||
return response
|
||||
|
||||
# Look for climate devices
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=name,
|
||||
area_name=area,
|
||||
floor_name=floor_name,
|
||||
domains=[CLIMATE_DOMAIN],
|
||||
assistant=intent_obj.assistant,
|
||||
single_target=True,
|
||||
)
|
||||
match_result = intent.async_match_targets(
|
||||
hass, match_constraints, match_preferences
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
|
@ -507,12 +507,22 @@ def _add_areas(
|
||||
candidate.area = areas.async_get_area(candidate.device.area_id)
|
||||
|
||||
|
||||
def _default_area_candidate_filter(
|
||||
candidate: MatchTargetsCandidate, possible_area_ids: Collection[str]
|
||||
) -> bool:
|
||||
"""Keep candidates in the possible areas."""
|
||||
return (candidate.area is not None) and (candidate.area.id in possible_area_ids)
|
||||
|
||||
|
||||
@callback
|
||||
def async_match_targets( # noqa: C901
|
||||
hass: HomeAssistant,
|
||||
constraints: MatchTargetsConstraints,
|
||||
preferences: MatchTargetsPreferences | None = None,
|
||||
states: list[State] | None = None,
|
||||
area_candidate_filter: Callable[
|
||||
[MatchTargetsCandidate, Collection[str]], bool
|
||||
] = _default_area_candidate_filter,
|
||||
) -> MatchTargetsResult:
|
||||
"""Match entities based on constraints in order to handle an intent."""
|
||||
preferences = preferences or MatchTargetsPreferences()
|
||||
@ -623,9 +633,7 @@ def async_match_targets( # noqa: C901
|
||||
}
|
||||
|
||||
candidates = [
|
||||
c
|
||||
for c in candidates
|
||||
if (c.area is not None) and (c.area.id in possible_area_ids)
|
||||
c for c in candidates if area_candidate_filter(c, possible_area_ids)
|
||||
]
|
||||
if not candidates:
|
||||
return MatchTargetsResult(
|
||||
@ -649,9 +657,7 @@ def async_match_targets( # noqa: C901
|
||||
# May be constrained by floors above
|
||||
possible_area_ids.intersection_update(matching_area_ids)
|
||||
candidates = [
|
||||
c
|
||||
for c in candidates
|
||||
if (c.area is not None) and (c.area.id in possible_area_ids)
|
||||
c for c in candidates if area_candidate_filter(c, possible_area_ids)
|
||||
]
|
||||
if not candidates:
|
||||
return MatchTargetsResult(
|
||||
@ -701,7 +707,7 @@ def async_match_targets( # noqa: C901
|
||||
group_candidates = [
|
||||
c
|
||||
for c in group_candidates
|
||||
if (c.area is not None) and (c.area.id == preferences.area_id)
|
||||
if area_candidate_filter(c, {preferences.area_id})
|
||||
]
|
||||
if len(group_candidates) < 2:
|
||||
# Disambiguated by area
|
||||
@ -747,7 +753,7 @@ def async_match_targets( # noqa: C901
|
||||
if preferences.area_id:
|
||||
# Filter by area
|
||||
filtered_candidates = [
|
||||
c for c in candidates if c.area and (c.area.id == preferences.area_id)
|
||||
c for c in candidates if area_candidate_filter(c, {preferences.area_id})
|
||||
]
|
||||
|
||||
if (len(filtered_candidates) > 1) and preferences.floor_id:
|
||||
|
@ -14,10 +14,16 @@ from homeassistant.components.climate import (
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.const import Platform, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, Platform, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import area_registry as ar, entity_registry as er, intent
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@ -131,6 +137,7 @@ async def test_get_temperature(
|
||||
hass: HomeAssistant,
|
||||
area_registry: ar.AreaRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test HassClimateGetTemperature intent."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
@ -157,29 +164,133 @@ async def test_get_temperature(
|
||||
# Add climate entities to different areas:
|
||||
# climate_1 => living room
|
||||
# climate_2 => bedroom
|
||||
# nothing in office
|
||||
# nothing in bathroom
|
||||
# nothing in office yet
|
||||
# nothing in attic yet
|
||||
living_room_area = area_registry.async_create(name="Living Room")
|
||||
bedroom_area = area_registry.async_create(name="Bedroom")
|
||||
office_area = area_registry.async_create(name="Office")
|
||||
attic_area = area_registry.async_create(name="Attic")
|
||||
bathroom_area = area_registry.async_create(name="Bathroom")
|
||||
|
||||
entity_registry.async_update_entity(
|
||||
climate_1.entity_id, area_id=living_room_area.id
|
||||
)
|
||||
entity_registry.async_update_entity(climate_2.entity_id, area_id=bedroom_area.id)
|
||||
|
||||
# First climate entity will be selected (no area)
|
||||
# Put areas on different floors:
|
||||
# first floor => living room and office
|
||||
# 2nd floor => bedroom
|
||||
# 3rd floor => attic
|
||||
floor_registry = fr.async_get(hass)
|
||||
first_floor = floor_registry.async_create("First floor")
|
||||
living_room_area = area_registry.async_update(
|
||||
living_room_area.id, floor_id=first_floor.floor_id
|
||||
)
|
||||
office_area = area_registry.async_update(
|
||||
office_area.id, floor_id=first_floor.floor_id
|
||||
)
|
||||
|
||||
second_floor = floor_registry.async_create("Second floor")
|
||||
bedroom_area = area_registry.async_update(
|
||||
bedroom_area.id, floor_id=second_floor.floor_id
|
||||
)
|
||||
bathroom_area = area_registry.async_update(
|
||||
bathroom_area.id, floor_id=second_floor.floor_id
|
||||
)
|
||||
|
||||
third_floor = floor_registry.async_create("Third floor")
|
||||
attic_area = area_registry.async_update(
|
||||
attic_area.id, floor_id=third_floor.floor_id
|
||||
)
|
||||
|
||||
# Add temperature sensors to each area that should *not* be selected
|
||||
for area in (living_room_area, office_area, bedroom_area, attic_area):
|
||||
wrong_temperature_entry = entity_registry.async_get_or_create(
|
||||
"sensor", "test", f"wrong_temperature_{area.id}"
|
||||
)
|
||||
hass.states.async_set(
|
||||
wrong_temperature_entry.entity_id,
|
||||
"10.0",
|
||||
{
|
||||
ATTR_TEMPERATURE: "Temperature",
|
||||
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
|
||||
},
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
wrong_temperature_entry.entity_id, area_id=area.id
|
||||
)
|
||||
|
||||
# Create temperature sensor and assign them to the office/attic
|
||||
office_temperature_id = "sensor.office_temperature"
|
||||
attic_temperature_id = "sensor.attic_temperature"
|
||||
hass.states.async_set(
|
||||
office_temperature_id,
|
||||
"15.5",
|
||||
{
|
||||
ATTR_TEMPERATURE: "Temperature",
|
||||
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
|
||||
},
|
||||
)
|
||||
office_area = area_registry.async_update(
|
||||
office_area.id, temperature_entity_id=office_temperature_id
|
||||
)
|
||||
|
||||
hass.states.async_set(
|
||||
attic_temperature_id,
|
||||
"18.1",
|
||||
{
|
||||
ATTR_TEMPERATURE: "Temperature",
|
||||
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
|
||||
},
|
||||
)
|
||||
attic_area = area_registry.async_update(
|
||||
attic_area.id, temperature_entity_id=attic_temperature_id
|
||||
)
|
||||
|
||||
# Multiple climate entities match (error)
|
||||
with pytest.raises(intent.MatchFailedError) as error:
|
||||
await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_GET_TEMPERATURE,
|
||||
{},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
|
||||
# Exception should contain details of what we tried to match
|
||||
assert isinstance(error.value, intent.MatchFailedError)
|
||||
assert (
|
||||
error.value.result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS
|
||||
)
|
||||
|
||||
# Select by area (office_temperature)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_GET_TEMPERATURE,
|
||||
{},
|
||||
{"area": {"value": office_area.name}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert response.matched_states
|
||||
assert response.matched_states[0].entity_id == climate_1.entity_id
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == office_temperature_id
|
||||
state = response.matched_states[0]
|
||||
assert state.attributes["current_temperature"] == 10.0
|
||||
assert state.state == "15.5"
|
||||
|
||||
# Select by preferred area (attic_temperature)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_GET_TEMPERATURE,
|
||||
{"preferred_area_id": {"value": attic_area.id}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == attic_temperature_id
|
||||
state = response.matched_states[0]
|
||||
assert state.state == "18.1"
|
||||
|
||||
# Select by area (climate_2)
|
||||
response = await intent.async_handle(
|
||||
@ -215,7 +326,7 @@ async def test_get_temperature(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_GET_TEMPERATURE,
|
||||
{"area": {"value": office_area.name}},
|
||||
{"area": {"value": bathroom_area.name}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
|
||||
@ -224,7 +335,7 @@ async def test_get_temperature(
|
||||
assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA
|
||||
constraints = error.value.constraints
|
||||
assert constraints.name is None
|
||||
assert constraints.area_name == office_area.name
|
||||
assert constraints.area_name == bathroom_area.name
|
||||
assert constraints.domains and (set(constraints.domains) == {CLIMATE_DOMAIN})
|
||||
assert constraints.device_classes is None
|
||||
|
||||
@ -262,6 +373,48 @@ async def test_get_temperature(
|
||||
assert constraints.domains and (set(constraints.domains) == {CLIMATE_DOMAIN})
|
||||
assert constraints.device_classes is None
|
||||
|
||||
# Select by floor (climate_1)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_GET_TEMPERATURE,
|
||||
{"floor": {"value": first_floor.name}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_1.entity_id
|
||||
state = response.matched_states[0]
|
||||
assert state.attributes["current_temperature"] == 10.0
|
||||
|
||||
# Select by preferred area (climate_2)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_GET_TEMPERATURE,
|
||||
{"preferred_area_id": {"value": bedroom_area.id}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_2.entity_id
|
||||
state = response.matched_states[0]
|
||||
assert state.attributes["current_temperature"] == 22.0
|
||||
|
||||
# Select by preferred floor (climate_1)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_GET_TEMPERATURE,
|
||||
{"preferred_floor_id": {"value": first_floor.floor_id}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||
assert len(response.matched_states) == 1
|
||||
assert response.matched_states[0].entity_id == climate_1.entity_id
|
||||
state = response.matched_states[0]
|
||||
assert state.attributes["current_temperature"] == 10.0
|
||||
|
||||
|
||||
async def test_get_temperature_no_entities(
|
||||
hass: HomeAssistant,
|
||||
|
Loading…
x
Reference in New Issue
Block a user