Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/voip/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["voip_utils"],
"quality_scale": "internal",
"requirements": ["voip-utils==0.4.0"]
"requirements": ["voip-utils==0.4.2"]
}
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

231 changes: 132 additions & 99 deletions tests/components/voip/test_voip.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,32 +339,35 @@ def tts_response_finished():
satellite._tones = Tones(0)
satellite.connection_made(Mock())

assert satellite.state == AssistSatelliteState.IDLE
try:
assert satellite.state == AssistSatelliteState.IDLE

# Ensure audio queue is cleared before pipeline starts
satellite._audio_queue.put_nowait(bad_chunk)
# Ensure audio queue is cleared before pipeline starts
satellite._audio_queue.put_nowait(bad_chunk)

def send_audio(*args, **kwargs):
# Don't send audio
pass
def send_audio(*args, **kwargs):
# Don't send audio
pass

satellite.send_audio = Mock(side_effect=send_audio)
satellite.send_audio = Mock(side_effect=send_audio)

# silence
satellite.on_chunk(bytes(_ONE_SECOND))
# silence
satellite.on_chunk(bytes(_ONE_SECOND))

# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))

# silence
satellite.on_chunk(bytes(_ONE_SECOND))
# silence
satellite.on_chunk(bytes(_ONE_SECOND))

# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(1):
await done.wait()
# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(1):
await done.wait()

# Finished speaking
assert satellite.state == AssistSatelliteState.IDLE
# Finished speaking
assert satellite.state == AssistSatelliteState.IDLE
finally:
satellite.disconnect()


async def test_stt_stream_timeout(
Expand Down Expand Up @@ -475,32 +478,35 @@ async def async_send_audio(audio_bytes: bytes, **kwargs):
satellite._tone_bytes[tone] = tone_bytes

satellite.connection_made(Mock())
satellite.send_audio = Mock()
try:
satellite.send_audio = Mock()

original_send_tts = satellite._send_tts
original_send_tts = satellite._send_tts

async def send_tts(*args, **kwargs):
# Call original then end test successfully
with pytest.raises(TimeoutError):
await original_send_tts(*args, **kwargs)
async def send_tts(*args, **kwargs):
# Call original then end test successfully
with pytest.raises(TimeoutError):
await original_send_tts(*args, **kwargs)

done.set()
done.set()

satellite._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign]
satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign]
satellite._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign]
satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign]

# silence
satellite.on_chunk(bytes(_ONE_SECOND))
# silence
satellite.on_chunk(bytes(_ONE_SECOND))

# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))

# silence
satellite.on_chunk(bytes(_ONE_SECOND))
# silence
satellite.on_chunk(bytes(_ONE_SECOND))

# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(1):
await done.wait()
# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(1):
await done.wait()
finally:
satellite.disconnect()


async def test_tts_wrong_extension(
Expand Down Expand Up @@ -572,28 +578,33 @@ async def send_tts(*args, **kwargs):

satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign]

satellite.send_audio = Mock()

satellite.connection_made(Mock())

# silence
satellite.on_chunk(bytes(_ONE_SECOND))
try:
# silence
satellite.on_chunk(bytes(_ONE_SECOND))

# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))

# silence (assumes relaxed VAD sensitivity)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
# silence (assumes relaxed VAD sensitivity)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))

# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(3):
await done.wait()
# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(3):
await done.wait()
finally:
satellite.disconnect()


async def test_tts_wrong_wav_format(
Expand Down Expand Up @@ -665,28 +676,33 @@ async def send_tts(*args, **kwargs):

satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign]

satellite.send_audio = Mock()

satellite.connection_made(Mock())

# silence
satellite.on_chunk(bytes(_ONE_SECOND))
try:
# silence
satellite.on_chunk(bytes(_ONE_SECOND))

# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))

# silence (assumes relaxed VAD sensitivity)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
# silence (assumes relaxed VAD sensitivity)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))

# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(3):
await done.wait()
# Wait for mock pipeline to exhaust the audio stream
async with asyncio.timeout(3):
await done.wait()
finally:
satellite.disconnect()


async def test_empty_tts_output(
Expand All @@ -701,6 +717,8 @@ async def test_empty_tts_output(
satellite.addr = ("192.168.1.1", 12345)
assert isinstance(satellite, VoipAssistSatellite)

done = asyncio.Event()

async def async_pipeline_from_audio_stream(*args, **kwargs):
stt_stream = kwargs["stt_stream"]
event_callback = kwargs["event_callback"]
Expand Down Expand Up @@ -738,6 +756,7 @@ async def async_pipeline_from_audio_stream(*args, **kwargs):
data={"tts_output": {}},
)
)
done.set()

with (
patch(
Expand All @@ -748,30 +767,35 @@ async def async_pipeline_from_audio_stream(*args, **kwargs):
"homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts",
) as mock_send_tts,
):
satellite.send_audio = Mock()

satellite.connection_made(Mock())

# silence
satellite.on_chunk(bytes(_ONE_SECOND))
try:
# silence
satellite.on_chunk(bytes(_ONE_SECOND))

# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
# "speech"
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))

# silence (assumes relaxed VAD sensitivity)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
# silence (assumes relaxed VAD sensitivity)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))
await asyncio.sleep(0.2)
satellite.on_chunk(bytes(_ONE_SECOND))

# Wait for mock pipeline to finish
async with asyncio.timeout(2):
await satellite._tts_done.wait()
# Wait for mock pipeline to finish
async with asyncio.timeout(2):
await done.wait()

mock_send_tts.assert_not_called()
mock_send_tts.assert_not_called()
finally:
satellite.disconnect()


async def test_pipeline_error(
Expand Down Expand Up @@ -814,16 +838,19 @@ async def async_send_audio(audio_bytes: bytes, **kwargs):
):
satellite._tones = Tones.ERROR
satellite.connection_made(Mock())
satellite._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign]
try:
satellite._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign]

satellite.on_chunk(bytes(_ONE_SECOND))
satellite.on_chunk(bytes(_ONE_SECOND))

# Wait for error tone to be played
async with asyncio.timeout(1):
await done.wait()
# Wait for error tone to be played
async with asyncio.timeout(1):
await done.wait()

assert sum(played_audio_bytes) > 0
assert played_audio_bytes == snapshot
assert sum(played_audio_bytes) > 0
assert played_audio_bytes == snapshot
finally:
satellite.disconnect()


@pytest.mark.usefixtures("socket_enabled")
Expand Down Expand Up @@ -995,8 +1022,11 @@ async def test_announce_timeout(
),
):
satellite.connection_made(Mock())
with pytest.raises(TimeoutError):
await satellite.async_announce(announcement)
try:
with pytest.raises(TimeoutError):
await satellite.async_announce(announcement)
finally:
satellite.disconnect()


@pytest.mark.usefixtures("socket_enabled")
Expand Down Expand Up @@ -1149,5 +1179,8 @@ async def test_start_conversation_user_doesnt_pick_up(
):
satellite.connection_made(Mock())

with pytest.raises(TimeoutError):
await satellite.async_start_conversation(announcement)
try:
with pytest.raises(TimeoutError):
await satellite.async_start_conversation(announcement)
finally:
satellite.disconnect()
Loading