Skip to content

Commit

Permalink
Add Assist timers (#117199)
Browse files Browse the repository at this point in the history
* First pass at timers

* Move to separate file

* Refactor to using events

* Add pause/unpause/status

* Add ordinal

* Add test for timed Assist command

* Fix name matching

* Fix IntentHandleError

* Fix again

* Refactor to callbacks

* is_paused -> is_active

* Rename "set timer" to "start timer"

* Move tasks to timer manager

* More fixes

* Remove assist command

* Remove cancel by ordinal

* More tests

* Remove async on callbacks

* Export async_register_timer_handler
  • Loading branch information
synesthesiam committed May 14, 2024
1 parent 458cc83 commit add6ffa
Show file tree
Hide file tree
Showing 8 changed files with 1,918 additions and 29 deletions.
28 changes: 15 additions & 13 deletions homeassistant/components/conversation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@
async_get_agent,
get_agent_manager,
)
from .const import HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT
from .const import (
ATTR_AGENT_ID,
ATTR_CONVERSATION_ID,
ATTR_LANGUAGE,
ATTR_TEXT,
DOMAIN,
HOME_ASSISTANT_AGENT,
OLD_HOME_ASSISTANT_AGENT,
SERVICE_PROCESS,
SERVICE_RELOAD,
)
from .default_agent import async_get_default_agent, async_setup_default_agent
from .entity import ConversationEntity
from .http import async_setup as async_setup_conversation_http
Expand All @@ -52,19 +62,8 @@

_LOGGER = logging.getLogger(__name__)

ATTR_TEXT = "text"
ATTR_LANGUAGE = "language"
ATTR_AGENT_ID = "agent_id"
ATTR_CONVERSATION_ID = "conversation_id"

DOMAIN = "conversation"

REGEX_TYPE = type(re.compile(""))

SERVICE_PROCESS = "process"
SERVICE_RELOAD = "reload"


SERVICE_PROCESS_SCHEMA = vol.Schema(
{
vol.Required(ATTR_TEXT): cv.string,
Expand Down Expand Up @@ -183,7 +182,10 @@ def async_get_agent_info(

async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the process service."""
entity_component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
entity_component: EntityComponent[ConversationEntity] = EntityComponent(
_LOGGER, DOMAIN, hass
)
hass.data[DOMAIN] = entity_component

await async_setup_default_agent(
hass, entity_component, config.get(DOMAIN, {}).get("intents", {})
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/conversation/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
HOME_ASSISTANT_AGENT = "conversation.home_assistant"
OLD_HOME_ASSISTANT_AGENT = "homeassistant"

ATTR_TEXT = "text"
ATTR_LANGUAGE = "language"
ATTR_AGENT_ID = "agent_id"
ATTR_CONVERSATION_ID = "conversation_id"

SERVICE_PROCESS = "process"
SERVICE_RELOAD = "reload"
44 changes: 30 additions & 14 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,18 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
assert lang_intents is not None

# Slot values to pass to the intent
slots = {
entity.name: {"value": entity.value, "text": entity.text or entity.value}
for entity in result.entities_list
}
slots: dict[str, Any] = {}

# Automatically add device id
if user_input.device_id is not None:
slots["device_id"] = user_input.device_id

# Add entities from match
for entity in result.entities_list:
slots[entity.name] = {
"value": entity.value,
"text": entity.text or entity.value,
}

try:
intent_response = await intent.async_handle(
Expand All @@ -364,14 +372,16 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
),
conversation_id,
)
except intent.IntentHandleError:
except intent.IntentHandleError as err:
# Intent was valid and entities matched constraints, but an error
# occurred during handling.
_LOGGER.exception("Intent handling error")
return _make_error_result(
language,
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents),
self._get_error_text(
err.response_key or ErrorKey.HANDLE_ERROR, lang_intents
),
conversation_id,
)
except intent.IntentUnexpectedError:
Expand Down Expand Up @@ -412,7 +422,6 @@ def _recognize(
language: str,
) -> RecognizeResult | None:
"""Search intents for a match to user input."""
# Prioritize matches with entity names above area names
maybe_result: RecognizeResult | None = None
for result in recognize_all(
user_input.text,
Expand Down Expand Up @@ -518,13 +527,16 @@ async def _build_speech(
state1 = unmatched[0]

# Render response template
speech_slots = {
entity_name: entity_value.text or entity_value.value
for entity_name, entity_value in recognize_result.entities.items()
}
speech_slots.update(intent_response.speech_slots)

speech = response_template.async_render(
{
# Slots from intent recognizer
"slots": {
entity_name: entity_value.text or entity_value.value
for entity_name, entity_value in recognize_result.entities.items()
},
# Slots from intent recognizer and response
"slots": speech_slots,
# First matched or unmatched state
"state": (
template.TemplateState(self.hass, state1)
Expand Down Expand Up @@ -849,15 +861,19 @@ def _make_intent_context(

def _get_error_text(
self,
error_key: ErrorKey,
error_key: ErrorKey | str,
lang_intents: LanguageIntents | None,
**response_args,
) -> str:
"""Get response error text by type."""
if lang_intents is None:
return _DEFAULT_ERROR_TEXT

response_key = error_key.value
if isinstance(error_key, ErrorKey):
response_key = error_key.value
else:
response_key = error_key

response_str = (
lang_intents.error_responses.get(response_key) or _DEFAULT_ERROR_TEXT
)
Expand Down
27 changes: 26 additions & 1 deletion homeassistant/components/intent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,33 @@
from homeassistant.helpers import config_validation as cv, integration_platform, intent
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN
from .const import DOMAIN, TIMER_DATA
from .timers import (
CancelTimerIntentHandler,
DecreaseTimerIntentHandler,
IncreaseTimerIntentHandler,
PauseTimerIntentHandler,
StartTimerIntentHandler,
TimerManager,
TimerStatusIntentHandler,
UnpauseTimerIntentHandler,
async_register_timer_handler,
)

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

__all__ = [
"async_register_timer_handler",
"DOMAIN",
]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Intent component."""
hass.data[TIMER_DATA] = TimerManager(hass)

hass.http.register_view(IntentHandleView())

await integration_platform.async_process_integration_platforms(
Expand Down Expand Up @@ -74,6 +92,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
NevermindIntentHandler(),
)
intent.async_register(hass, SetPositionIntentHandler())
intent.async_register(hass, StartTimerIntentHandler())
intent.async_register(hass, CancelTimerIntentHandler())
intent.async_register(hass, IncreaseTimerIntentHandler())
intent.async_register(hass, DecreaseTimerIntentHandler())
intent.async_register(hass, PauseTimerIntentHandler())
intent.async_register(hass, UnpauseTimerIntentHandler())
intent.async_register(hass, TimerStatusIntentHandler())

return True

Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/intent/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Constants for the Intent integration."""

DOMAIN = "intent"

TIMER_DATA = f"{DOMAIN}.timer"

0 comments on commit add6ffa

Please sign in to comment.