mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Cancel call if user does not pick up (#136858)
This commit is contained in:
parent
b500fde468
commit
d206553a0d
@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, Any, Final
|
||||
import wave
|
||||
|
||||
from voip_utils import SIP_PORT, RtpDatagramProtocol
|
||||
from voip_utils.sip import SipEndpoint, get_sip_endpoint
|
||||
from voip_utils.sip import SipDatagramProtocol, SipEndpoint, get_sip_endpoint
|
||||
|
||||
from homeassistant.components import tts
|
||||
from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType
|
||||
@ -43,6 +43,7 @@ _PIPELINE_TIMEOUT_SEC: Final = 30
|
||||
_ANNOUNCEMENT_BEFORE_DELAY: Final = 0.5
|
||||
_ANNOUNCEMENT_AFTER_DELAY: Final = 1.0
|
||||
_ANNOUNCEMENT_HANGUP_SEC: Final = 0.5
|
||||
_ANNOUNCEMENT_RING_TIMEOUT: Final = 30
|
||||
|
||||
|
||||
class Tones(IntFlag):
|
||||
@ -116,7 +117,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
self._processing_tone_done = asyncio.Event()
|
||||
|
||||
self._announcement: AssistSatelliteAnnouncement | None = None
|
||||
self._announcement_done = asyncio.Event()
|
||||
self._announcement_future: asyncio.Future[Any] = asyncio.Future()
|
||||
self._announcment_start_time: float = 0.0
|
||||
self._check_announcement_ended_task: asyncio.Task | None = None
|
||||
self._last_chunk_time: float | None = None
|
||||
self._rtp_port: int | None = None
|
||||
@ -170,7 +172,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
|
||||
Plays announcement in a loop, blocking until the caller hangs up.
|
||||
"""
|
||||
self._announcement_done.clear()
|
||||
self._announcement_future = asyncio.Future()
|
||||
|
||||
if self._rtp_port is None:
|
||||
# Choose random port for RTP
|
||||
@ -194,16 +196,34 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
host=self.voip_device.voip_id, port=SIP_PORT
|
||||
)
|
||||
|
||||
# Reset state so we can time out if needed
|
||||
self._last_chunk_time = None
|
||||
self._announcment_start_time = time.monotonic()
|
||||
self._announcement = announcement
|
||||
|
||||
# Make the call
|
||||
self.hass.data[DOMAIN].protocol.outgoing_call(
|
||||
sip_protocol: SipDatagramProtocol = self.hass.data[DOMAIN].protocol
|
||||
call_info = sip_protocol.outgoing_call(
|
||||
source=source_endpoint,
|
||||
destination=destination_endpoint,
|
||||
rtp_port=self._rtp_port,
|
||||
)
|
||||
|
||||
await self._announcement_done.wait()
|
||||
# Check if caller hung up or didn't pick up
|
||||
self._check_announcement_ended_task = (
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._check_announcement_ended(),
|
||||
"voip_announcement_ended",
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
await self._announcement_future
|
||||
except TimeoutError:
|
||||
# Stop ringing
|
||||
sip_protocol.cancel_call(call_info)
|
||||
raise
|
||||
|
||||
async def _check_announcement_ended(self) -> None:
|
||||
"""Continuously checks if an audio chunk was received within a time limit.
|
||||
@ -211,12 +231,32 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
If not, the caller is presumed to have hung up and the announcement is ended.
|
||||
"""
|
||||
while self._announcement is not None:
|
||||
current_time = time.monotonic()
|
||||
_LOGGER.debug(
|
||||
"%s %s %s",
|
||||
self._last_chunk_time,
|
||||
current_time,
|
||||
self._announcment_start_time,
|
||||
)
|
||||
if (self._last_chunk_time is None) and (
|
||||
(current_time - self._announcment_start_time)
|
||||
> _ANNOUNCEMENT_RING_TIMEOUT
|
||||
):
|
||||
# Ring timeout
|
||||
self._announcement = None
|
||||
self._check_announcement_ended_task = None
|
||||
self._announcement_future.set_exception(
|
||||
TimeoutError("User did not pick up in time")
|
||||
)
|
||||
_LOGGER.debug("Timed out waiting for the user to pick up the phone")
|
||||
break
|
||||
|
||||
if (self._last_chunk_time is not None) and (
|
||||
(time.monotonic() - self._last_chunk_time) > _ANNOUNCEMENT_HANGUP_SEC
|
||||
(current_time - self._last_chunk_time) > _ANNOUNCEMENT_HANGUP_SEC
|
||||
):
|
||||
# Caller hung up
|
||||
self._announcement = None
|
||||
self._announcement_done.set()
|
||||
self._announcement_future.set_result(None)
|
||||
self._check_announcement_ended_task = None
|
||||
_LOGGER.debug("Announcement ended")
|
||||
break
|
||||
@ -248,16 +288,6 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
self._audio_queue.put_nowait(audio_bytes)
|
||||
elif self._run_pipeline_task is None:
|
||||
# Announcement only
|
||||
if self._check_announcement_ended_task is None:
|
||||
# Check if caller hung up
|
||||
self._check_announcement_ended_task = (
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._check_announcement_ended(),
|
||||
"voip_announcement_ended",
|
||||
)
|
||||
)
|
||||
|
||||
# Play announcement (will repeat)
|
||||
self._run_pipeline_task = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/voip",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["voip-utils==0.3.0"]
|
||||
"requirements": ["voip-utils==0.3.1"]
|
||||
}
|
||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -2991,7 +2991,7 @@ venstarcolortouch==0.19
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
# homeassistant.components.voip
|
||||
voip-utils==0.3.0
|
||||
voip-utils==0.3.1
|
||||
|
||||
# homeassistant.components.volkszaehler
|
||||
volkszaehler==0.4.0
|
||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -2407,7 +2407,7 @@ venstarcolortouch==0.19
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
# homeassistant.components.voip
|
||||
voip-utils==0.3.0
|
||||
voip-utils==0.3.1
|
||||
|
||||
# homeassistant.components.volvooncall
|
||||
volvooncall==0.10.3
|
||||
|
@ -941,3 +941,43 @@ async def test_voip_id_is_ip_address(
|
||||
await announce_task
|
||||
|
||||
mock_send_tts.assert_called_once_with(_MEDIA_ID, wait_for_tone=False)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("socket_enabled")
|
||||
async def test_announce_timeout(
|
||||
hass: HomeAssistant,
|
||||
voip_devices: VoIPDevices,
|
||||
voip_device: VoIPDevice,
|
||||
) -> None:
|
||||
"""Test announcement when user does not pick up the phone in time."""
|
||||
assert await async_setup_component(hass, "voip", {})
|
||||
|
||||
satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id)
|
||||
assert isinstance(satellite, VoipAssistSatellite)
|
||||
assert (
|
||||
satellite.supported_features
|
||||
& assist_satellite.AssistSatelliteEntityFeature.ANNOUNCE
|
||||
)
|
||||
|
||||
announcement = assist_satellite.AssistSatelliteAnnouncement(
|
||||
message="test announcement",
|
||||
media_id=_MEDIA_ID,
|
||||
original_media_id=_MEDIA_ID,
|
||||
media_id_source="tts",
|
||||
)
|
||||
|
||||
# Protocol has already been mocked, but some methods are not async
|
||||
mock_protocol: AsyncMock = hass.data[DOMAIN].protocol
|
||||
mock_protocol.outgoing_call = Mock()
|
||||
mock_protocol.cancel_call = Mock()
|
||||
|
||||
# Very short timeout which will trigger because we don't send any audio in
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.voip.assist_satellite._ANNOUNCEMENT_RING_TIMEOUT",
|
||||
0.01,
|
||||
),
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
with pytest.raises(TimeoutError):
|
||||
await satellite.async_announce(announcement)
|
||||
|
Loading…
x
Reference in New Issue
Block a user