Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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