From c51a2317e1f05ed9ff935a009651381987bc9a73 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Mar 2025 09:48:10 -0500 Subject: [PATCH] Add timer support to VoIP (#139763) --- .../components/voip/assist_satellite.py | 34 +++++++++- homeassistant/components/voip/manifest.json | 2 +- .../components/voip/test_assist_satellite.py | 62 +++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/components/voip/test_assist_satellite.py diff --git a/homeassistant/components/voip/assist_satellite.py b/homeassistant/components/voip/assist_satellite.py index 6d18d8254f2..2c0a3b9641a 100644 --- a/homeassistant/components/voip/assist_satellite.py +++ b/homeassistant/components/voip/assist_satellite.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from datetime import timedelta from enum import IntFlag from functools import partial import io @@ -16,7 +17,7 @@ import wave from voip_utils import SIP_PORT, RtpDatagramProtocol from voip_utils.sip import SipDatagramProtocol, SipEndpoint, get_sip_endpoint -from homeassistant.components import tts +from homeassistant.components import intent, tts from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType from homeassistant.components.assist_satellite import ( AssistSatelliteAnnouncement, @@ -25,6 +26,7 @@ from homeassistant.components.assist_satellite import ( AssistSatelliteEntityDescription, AssistSatelliteEntityFeature, ) +from homeassistant.components.intent import TimerEventType, TimerInfo from homeassistant.components.network import async_get_source_ip from homeassistant.config_entries import ConfigEntry from homeassistant.core import Context, HomeAssistant, callback @@ -161,6 +163,13 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol await super().async_added_to_hass() self.voip_device.protocol = self + assert self.device_entry is not None + self.async_on_remove( + intent.async_register_timer_handler( + self.hass, self.device_entry.id, self.async_handle_timer_event + ) + ) + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() @@ -174,6 +183,29 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol """Get the current satellite configuration.""" raise NotImplementedError + @callback + def async_handle_timer_event( + self, + event_type: TimerEventType, + timer_info: TimerInfo, + ) -> None: + """Handle timer event.""" + if event_type != TimerEventType.FINISHED: + return + + if timer_info.name: + message = f"{timer_info.name} finished" + else: + message = f"{timedelta(seconds=timer_info.created_seconds)} timer finished" + + async def announce_message(): + announcement = await self._resolve_announcement_media_id(message, None) + await self.async_announce(announcement) + + self.config_entry.async_create_background_task( + self.hass, announce_message(), "voip_announce_timer" + ) + async def async_set_configuration( self, config: AssistSatelliteConfiguration ) -> None: diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index e3b2861dbe5..1e4c249c720 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -3,7 +3,7 @@ "name": "Voice over IP", "codeowners": ["@balloob", "@synesthesiam"], "config_flow": true, - "dependencies": ["assist_pipeline", "assist_satellite", "network"], + "dependencies": ["assist_pipeline", "assist_satellite", "intent", "network"], "documentation": "https://www.home-assistant.io/integrations/voip", "iot_class": "local_push", "quality_scale": "internal", diff --git a/tests/components/voip/test_assist_satellite.py b/tests/components/voip/test_assist_satellite.py new file mode 100644 index 00000000000..f3e2611631e --- /dev/null +++ b/tests/components/voip/test_assist_satellite.py @@ -0,0 +1,62 @@ +"""Test the Assist Satellite platform.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.voip.devices import VoIPDevice +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent as intent_helper + + +@pytest.mark.parametrize( + ("intent_args", "message"), + [ + ( + {}, + "0:02:00 timer finished", + ), + ( + {"name": {"value": "pizza"}}, + "pizza finished", + ), + ], +) +async def test_timer_events( + hass: HomeAssistant, voip_device: VoIPDevice, intent_args: dict, message: str +) -> None: + """Test for timer events.""" + + await intent_helper.async_handle( + hass, + "test", + intent_helper.INTENT_START_TIMER, + { + "minutes": {"value": 2}, + } + | intent_args, + device_id=voip_device.device_id, + ) + + with ( + patch( + "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._resolve_announcement_media_id", + ) as mock_resolve, + patch( + "homeassistant.components.voip.assist_satellite.VoipAssistSatellite.async_announce", + ) as mock_announce, + ): + await intent_helper.async_handle( + hass, + "test", + intent_helper.INTENT_DECREASE_TIMER, + { + "minutes": {"value": 2}, + }, + device_id=voip_device.device_id, + ) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(mock_resolve.mock_calls) == 1 + assert len(mock_announce.mock_calls) == 1 + assert mock_resolve.mock_calls[0][1][0] == message