[esphome] Assist timers (#118275)

* [esphome] Assist timers

* Add intent to manifest dependencies

* Add test

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Jesse Hills 2024-05-29 06:57:58 +12:00 committed by GitHub
parent 06d6f99964
commit 7f530ee0e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 105 additions and 4 deletions

View File

@ -27,6 +27,7 @@ from awesomeversion import AwesomeVersion
import voluptuous as vol
from homeassistant.components import tag, zeroconf
from homeassistant.components.intent import async_register_timer_handler
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_ID,
@ -77,6 +78,7 @@ from .voice_assistant import (
VoiceAssistantAPIPipeline,
VoiceAssistantPipeline,
VoiceAssistantUDPPipeline,
handle_timer_event,
)
_LOGGER = logging.getLogger(__name__)
@ -517,6 +519,12 @@ class ESPHomeManager:
handle_stop=self._handle_pipeline_stop,
)
)
if flags & VoiceAssistantFeature.TIMERS:
entry_data.disconnect_callbacks.add(
async_register_timer_handler(
hass, self.device_id, partial(handle_timer_event, cli)
)
)
cli.subscribe_states(entry_data.async_update_state)
cli.subscribe_service_calls(self.async_on_service_call)

View File

@ -4,7 +4,7 @@
"after_dependencies": ["zeroconf", "tag"],
"codeowners": ["@OttoWinter", "@jesserockz", "@kbx81", "@bdraco"],
"config_flow": true,
"dependencies": ["assist_pipeline", "bluetooth"],
"dependencies": ["assist_pipeline", "bluetooth", "intent"],
"dhcp": [
{
"registered_devices": true

View File

@ -16,6 +16,7 @@ from aioesphomeapi import (
VoiceAssistantCommandFlag,
VoiceAssistantEventType,
VoiceAssistantFeature,
VoiceAssistantTimerEventType,
)
from homeassistant.components import stt, tts
@ -33,6 +34,7 @@ from homeassistant.components.assist_pipeline.error import (
WakeWordDetectionAborted,
WakeWordDetectionError,
)
from homeassistant.components.intent.timers import TimerEventType, TimerInfo
from homeassistant.components.media_player import async_process_play_media_url
from homeassistant.core import Context, HomeAssistant, callback
@ -65,6 +67,17 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[
}
)
_TIMER_EVENT_TYPES: EsphomeEnumMapper[VoiceAssistantTimerEventType, TimerEventType] = (
EsphomeEnumMapper(
{
VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_STARTED: TimerEventType.STARTED,
VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_UPDATED: TimerEventType.UPDATED,
VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_CANCELLED: TimerEventType.CANCELLED,
VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_FINISHED: TimerEventType.FINISHED,
}
)
)
class VoiceAssistantPipeline:
"""Base abstract pipeline class."""
@ -438,3 +451,23 @@ class VoiceAssistantAPIPipeline(VoiceAssistantPipeline):
self.started = False
self.stop_requested = True
def handle_timer_event(
api_client: APIClient, event_type: TimerEventType, timer_info: TimerInfo
) -> None:
"""Handle timer events."""
try:
native_event_type = _TIMER_EVENT_TYPES.from_hass(event_type)
except KeyError:
_LOGGER.debug("Received unknown timer event type: %s", event_type)
return
api_client.send_voice_assistant_timer_event(
native_event_type,
timer_info.id,
timer_info.name,
timer_info.seconds,
timer_info.seconds_left,
timer_info.is_active,
)

View File

@ -1,13 +1,21 @@
"""Test ESPHome voice assistant server."""
import asyncio
from collections.abc import Callable
from collections.abc import Awaitable, Callable
import io
import socket
from unittest.mock import Mock, patch
from unittest.mock import ANY, Mock, patch
import wave
from aioesphomeapi import APIClient, VoiceAssistantEventType
from aioesphomeapi import (
APIClient,
EntityInfo,
EntityState,
UserService,
VoiceAssistantEventType,
VoiceAssistantFeature,
VoiceAssistantTimerEventType,
)
import pytest
from homeassistant.components.assist_pipeline import (
@ -25,6 +33,10 @@ from homeassistant.components.esphome.voice_assistant import (
VoiceAssistantUDPPipeline,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent as intent_helper
import homeassistant.helpers.device_registry as dr
from .conftest import MockESPHomeDevice
_TEST_INPUT_TEXT = "This is an input test"
_TEST_OUTPUT_TEXT = "This is an output test"
@ -720,3 +732,51 @@ async def test_wake_word_abort_exception(
)
mock_handle_event.assert_not_called()
async def test_timer_events(
hass: HomeAssistant,
mock_client: APIClient,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test that injecting timer events results in the correct api client calls."""
mock_device: MockESPHomeDevice = await mock_esphome_device(
mock_client=mock_client,
entity_info=[],
user_service=[],
states=[],
device_info={
"voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT
| VoiceAssistantFeature.TIMERS
},
)
dev_reg = dr.async_get(hass)
dev = dev_reg.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)}
)
await intent_helper.async_handle(
hass,
"test",
intent_helper.INTENT_START_TIMER,
{
"name": {"value": "test timer"},
"hours": {"value": 1},
"minutes": {"value": 2},
"seconds": {"value": 3},
},
device_id=dev.id,
)
mock_client.send_voice_assistant_timer_event.assert_called_with(
VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_STARTED,
ANY,
"test timer",
3723,
3723,
True,
)