mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +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
|
import wave
|
||||||
|
|
||||||
from voip_utils import SIP_PORT, RtpDatagramProtocol
|
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 import tts
|
||||||
from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType
|
from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType
|
||||||
@ -43,6 +43,7 @@ _PIPELINE_TIMEOUT_SEC: Final = 30
|
|||||||
_ANNOUNCEMENT_BEFORE_DELAY: Final = 0.5
|
_ANNOUNCEMENT_BEFORE_DELAY: Final = 0.5
|
||||||
_ANNOUNCEMENT_AFTER_DELAY: Final = 1.0
|
_ANNOUNCEMENT_AFTER_DELAY: Final = 1.0
|
||||||
_ANNOUNCEMENT_HANGUP_SEC: Final = 0.5
|
_ANNOUNCEMENT_HANGUP_SEC: Final = 0.5
|
||||||
|
_ANNOUNCEMENT_RING_TIMEOUT: Final = 30
|
||||||
|
|
||||||
|
|
||||||
class Tones(IntFlag):
|
class Tones(IntFlag):
|
||||||
@ -116,7 +117,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
|||||||
self._processing_tone_done = asyncio.Event()
|
self._processing_tone_done = asyncio.Event()
|
||||||
|
|
||||||
self._announcement: AssistSatelliteAnnouncement | None = None
|
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._check_announcement_ended_task: asyncio.Task | None = None
|
||||||
self._last_chunk_time: float | None = None
|
self._last_chunk_time: float | None = None
|
||||||
self._rtp_port: int | 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.
|
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:
|
if self._rtp_port is None:
|
||||||
# Choose random port for RTP
|
# Choose random port for RTP
|
||||||
@ -194,16 +196,34 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
|||||||
host=self.voip_device.voip_id, port=SIP_PORT
|
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
|
self._announcement = announcement
|
||||||
|
|
||||||
# Make the call
|
# 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,
|
source=source_endpoint,
|
||||||
destination=destination_endpoint,
|
destination=destination_endpoint,
|
||||||
rtp_port=self._rtp_port,
|
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:
|
async def _check_announcement_ended(self) -> None:
|
||||||
"""Continuously checks if an audio chunk was received within a time limit.
|
"""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.
|
If not, the caller is presumed to have hung up and the announcement is ended.
|
||||||
"""
|
"""
|
||||||
while self._announcement is not None:
|
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 (
|
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
|
# Caller hung up
|
||||||
self._announcement = None
|
self._announcement = None
|
||||||
self._announcement_done.set()
|
self._announcement_future.set_result(None)
|
||||||
self._check_announcement_ended_task = None
|
self._check_announcement_ended_task = None
|
||||||
_LOGGER.debug("Announcement ended")
|
_LOGGER.debug("Announcement ended")
|
||||||
break
|
break
|
||||||
@ -248,16 +288,6 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
|||||||
self._audio_queue.put_nowait(audio_bytes)
|
self._audio_queue.put_nowait(audio_bytes)
|
||||||
elif self._run_pipeline_task is None:
|
elif self._run_pipeline_task is None:
|
||||||
# Announcement only
|
# 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)
|
# Play announcement (will repeat)
|
||||||
self._run_pipeline_task = self.config_entry.async_create_background_task(
|
self._run_pipeline_task = self.config_entry.async_create_background_task(
|
||||||
self.hass,
|
self.hass,
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/voip",
|
"documentation": "https://www.home-assistant.io/integrations/voip",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "internal",
|
"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
|
vilfo-api-client==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.voip
|
# homeassistant.components.voip
|
||||||
voip-utils==0.3.0
|
voip-utils==0.3.1
|
||||||
|
|
||||||
# homeassistant.components.volkszaehler
|
# homeassistant.components.volkszaehler
|
||||||
volkszaehler==0.4.0
|
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
|
vilfo-api-client==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.voip
|
# homeassistant.components.voip
|
||||||
voip-utils==0.3.0
|
voip-utils==0.3.1
|
||||||
|
|
||||||
# homeassistant.components.volvooncall
|
# homeassistant.components.volvooncall
|
||||||
volvooncall==0.10.3
|
volvooncall==0.10.3
|
||||||
|
@ -941,3 +941,43 @@ async def test_voip_id_is_ip_address(
|
|||||||
await announce_task
|
await announce_task
|
||||||
|
|
||||||
mock_send_tts.assert_called_once_with(_MEDIA_ID, wait_for_tone=False)
|
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