Add HassClimateGetTemperature intent (#102831)

This commit is contained in:
Michael Hansen 2023-11-25 07:50:44 -06:00 committed by GitHub
parent eb472d9f71
commit 71268bd407
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 289 additions and 0 deletions

View File

@ -0,0 +1,68 @@
"""Intents for the client integration."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import intent
from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, ClimateEntity
INTENT_GET_TEMPERATURE = "HassClimateGetTemperature"
async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the climate intents."""
intent.async_register(hass, GetTemperatureIntent())
class GetTemperatureIntent(intent.IntentHandler):
"""Handle GetTemperature intents."""
intent_type = INTENT_GET_TEMPERATURE
slot_schema = {vol.Optional("area"): str}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
component: EntityComponent[ClimateEntity] = hass.data[DOMAIN]
entities: list[ClimateEntity] = list(component.entities)
climate_entity: ClimateEntity | None = None
climate_state: State | None = None
if not entities:
raise intent.IntentHandleError("No climate entities")
if "area" in slots:
# Filter by area
area_name = slots["area"]["value"]
for maybe_climate in intent.async_match_states(
hass, area_name=area_name, domains=[DOMAIN]
):
climate_state = maybe_climate
break
if climate_state is None:
raise intent.IntentHandleError(f"No climate entity in area {area_name}")
climate_entity = component.get_entity(climate_state.entity_id)
else:
# First entity
climate_entity = entities[0]
climate_state = hass.states.get(climate_entity.entity_id)
assert climate_entity is not None
if climate_state is None:
raise intent.IntentHandleError(f"No state for {climate_entity.name}")
assert climate_state is not None
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.QUERY_ANSWER
response.async_set_states(matched_states=[climate_state])
return response

View File

@ -0,0 +1,221 @@
"""Test climate intents."""
from collections.abc import Generator
from unittest.mock import patch
import pytest
from homeassistant.components.climate import (
DOMAIN,
ClimateEntity,
HVACMode,
intent as climate_intent,
)
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers import area_registry as ar, entity_registry as er, intent
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from tests.common import (
MockConfigEntry,
MockModule,
MockPlatform,
mock_config_flow,
mock_integration,
mock_platform,
)
TEST_DOMAIN = "test"
class MockFlow(ConfigFlow):
"""Test flow."""
@pytest.fixture(autouse=True)
def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]:
"""Mock config flow."""
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
with mock_config_flow(TEST_DOMAIN, MockFlow):
yield
@pytest.fixture(autouse=True)
def mock_setup_integration(hass: HomeAssistant) -> None:
"""Fixture to set up a mock integration."""
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
return True
async def async_unload_entry_init(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
await hass.config_entries.async_unload_platforms(config_entry, [Platform.TODO])
return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
mock_integration(
hass,
MockModule(
TEST_DOMAIN,
async_setup_entry=async_setup_entry_init,
async_unload_entry=async_unload_entry_init,
),
)
async def create_mock_platform(
hass: HomeAssistant,
entities: list[ClimateEntity],
) -> MockConfigEntry:
"""Create a todo platform with the specified entities."""
async def async_setup_entry_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up test event platform via config entry."""
async_add_entities(entities)
mock_platform(
hass,
f"{TEST_DOMAIN}.{DOMAIN}",
MockPlatform(async_setup_entry=async_setup_entry_platform),
)
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
class MockClimateEntity(ClimateEntity):
"""Mock Climate device to use in tests."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_mode = HVACMode.OFF
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
async def test_get_temperature(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test HassClimateGetTemperature intent."""
await climate_intent.async_setup_intents(hass)
climate_1 = MockClimateEntity()
climate_1._attr_name = "Climate 1"
climate_1._attr_unique_id = "1234"
climate_1._attr_current_temperature = 10.0
entity_registry.async_get_or_create(
DOMAIN, "test", "1234", suggested_object_id="climate_1"
)
climate_2 = MockClimateEntity()
climate_2._attr_name = "Climate 2"
climate_2._attr_unique_id = "5678"
climate_2._attr_current_temperature = 22.0
entity_registry.async_get_or_create(
DOMAIN, "test", "5678", suggested_object_id="climate_2"
)
await create_mock_platform(hass, [climate_1, climate_2])
# Add climate entities to different areas:
# climate_1 => living room
# climate_2 => bedroom
living_room_area = area_registry.async_create(name="Living Room")
bedroom_area = area_registry.async_create(name="Bedroom")
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)
response = await intent.async_handle(
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {}
)
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 area instead (climate_2)
response = await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": "Bedroom"}},
)
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
async def test_get_temperature_no_entities(
hass: HomeAssistant,
) -> None:
"""Test HassClimateGetTemperature intent with no climate entities."""
await climate_intent.async_setup_intents(hass)
await create_mock_platform(hass, [])
with pytest.raises(intent.IntentHandleError):
await intent.async_handle(
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {}
)
async def test_get_temperature_no_state(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test HassClimateGetTemperature intent when states are missing."""
await climate_intent.async_setup_intents(hass)
climate_1 = MockClimateEntity()
climate_1._attr_name = "Climate 1"
climate_1._attr_unique_id = "1234"
entity_registry.async_get_or_create(
DOMAIN, "test", "1234", suggested_object_id="climate_1"
)
await create_mock_platform(hass, [climate_1])
living_room_area = area_registry.async_create(name="Living Room")
entity_registry.async_update_entity(
climate_1.entity_id, area_id=living_room_area.id
)
with patch("homeassistant.core.StateMachine.get", return_value=None), pytest.raises(
intent.IntentHandleError
):
await intent.async_handle(
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {}
)
with patch(
"homeassistant.core.StateMachine.async_all", return_value=[]
), pytest.raises(intent.IntentHandleError):
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": "Living Room"}},
)