From c51a2317e1f05ed9ff935a009651381987bc9a73 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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
-- 
GitLab