diff --git a/homeassistant/components/weather/intent.py b/homeassistant/components/weather/intent.py new file mode 100644 index 00000000000..4fd22ceb0a9 --- /dev/null +++ b/homeassistant/components/weather/intent.py @@ -0,0 +1,85 @@ +"""Intents for the weather integration.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent + +from . import DOMAIN, WeatherEntity + +INTENT_GET_WEATHER = "HassGetWeather" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the weather intents.""" + intent.async_register(hass, GetWeatherIntent()) + + +class GetWeatherIntent(intent.IntentHandler): + """Handle GetWeather intents.""" + + intent_type = INTENT_GET_WEATHER + slot_schema = {vol.Optional("name"): cv.string} + + 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) + + weather: WeatherEntity | None = None + weather_state: State | None = None + component: EntityComponent[WeatherEntity] = hass.data[DOMAIN] + entities = list(component.entities) + + if "name" in slots: + # Named weather entity + weather_name = slots["name"]["value"] + + # Find matching weather entity + matching_states = intent.async_match_states( + hass, name=weather_name, domains=[DOMAIN] + ) + for maybe_weather_state in matching_states: + weather = component.get_entity(maybe_weather_state.entity_id) + if weather is not None: + weather_state = maybe_weather_state + break + + if weather is None: + raise intent.IntentHandleError( + f"No weather entity named {weather_name}" + ) + elif entities: + # First weather entity + weather = entities[0] + weather_name = weather.name + weather_state = hass.states.get(weather.entity_id) + + if weather is None: + raise intent.IntentHandleError("No weather entity") + + if weather_state is None: + raise intent.IntentHandleError(f"No state for weather: {weather.name}") + + assert weather is not None + assert weather_state is not None + + # Create response + response = intent_obj.create_response() + response.response_type = intent.IntentResponseType.QUERY_ANSWER + response.async_set_results( + success_results=[ + intent.IntentResponseTarget( + type=intent.IntentResponseTargetType.ENTITY, + name=weather_name, + id=weather.entity_id, + ) + ] + ) + + response.async_set_states(matched_states=[weather_state]) + + return response diff --git a/tests/components/weather/test_intent.py b/tests/components/weather/test_intent.py new file mode 100644 index 00000000000..1a171da7fae --- /dev/null +++ b/tests/components/weather/test_intent.py @@ -0,0 +1,108 @@ +"""Test weather intents.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.weather import ( + DOMAIN, + WeatherEntity, + intent as weather_intent, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent +from homeassistant.setup import async_setup_component + + +async def test_get_weather(hass: HomeAssistant) -> None: + """Test get weather for first entity and by name.""" + assert await async_setup_component(hass, "weather", {"weather": {}}) + + entity1 = WeatherEntity() + entity1._attr_name = "Weather 1" + entity1.entity_id = "weather.test_1" + + entity2 = WeatherEntity() + entity2._attr_name = "Weather 2" + entity2.entity_id = "weather.test_2" + + await hass.data[DOMAIN].async_add_entities([entity1, entity2]) + + await weather_intent.async_setup_intents(hass) + + # First entity will be chosen + response = await intent.async_handle( + hass, "test", weather_intent.INTENT_GET_WEATHER, {} + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + state = response.matched_states[0] + assert state.entity_id == entity1.entity_id + + # Named entity will be chosen + response = await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {"name": {"value": "Weather 2"}}, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + state = response.matched_states[0] + assert state.entity_id == entity2.entity_id + + +async def test_get_weather_wrong_name(hass: HomeAssistant) -> None: + """Test get weather with the wrong name.""" + assert await async_setup_component(hass, "weather", {"weather": {}}) + + entity1 = WeatherEntity() + entity1._attr_name = "Weather 1" + entity1.entity_id = "weather.test_1" + + await hass.data[DOMAIN].async_add_entities([entity1]) + + await weather_intent.async_setup_intents(hass) + + # Incorrect name + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {"name": {"value": "not the right name"}}, + ) + + +async def test_get_weather_no_entities(hass: HomeAssistant) -> None: + """Test get weather with no weather entities.""" + assert await async_setup_component(hass, "weather", {"weather": {}}) + await weather_intent.async_setup_intents(hass) + + # No weather entities + with pytest.raises(intent.IntentHandleError): + await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {}) + + +async def test_get_weather_no_state(hass: HomeAssistant) -> None: + """Test get weather when state is not returned.""" + assert await async_setup_component(hass, "weather", {"weather": {}}) + + entity1 = WeatherEntity() + entity1._attr_name = "Weather 1" + entity1.entity_id = "weather.test_1" + + await hass.data[DOMAIN].async_add_entities([entity1]) + + await weather_intent.async_setup_intents(hass) + + # Success with state + response = await intent.async_handle( + hass, "test", weather_intent.INTENT_GET_WEATHER, {} + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + + # Failure without state + with patch("homeassistant.core.StateMachine.get", return_value=None), pytest.raises( + intent.IntentHandleError + ): + await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {})