mirror of
https://github.com/home-assistant/core.git
synced 2026-09-27 09:58:07 -04:00
Add wyoming integration with stt (#91579)
* Add wyoming integration with stt/tts * Forward config entry setup * Use SpeechToTextEntity * Add strings to config flow * Move connection into config flow * Add tests * On load/unload used platforms * Tweaks * Add unload test * Fix stt * Add missing file * Add test for no services * Improve coverage * Finish test coverage --------- Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
co-authored by
Paulus Schoutsen
parent
f74103c57e
commit
85d57a046c
@@ -0,0 +1,22 @@
|
||||
"""Tests for the Wyoming integration."""
|
||||
from wyoming.info import AsrModel, AsrProgram, Attribution, Info
|
||||
|
||||
TEST_ATTR = Attribution(name="Test", url="http://www.test.com")
|
||||
STT_INFO = Info(
|
||||
asr=[
|
||||
AsrProgram(
|
||||
name="Test ASR",
|
||||
installed=True,
|
||||
attribution=TEST_ATTR,
|
||||
models=[
|
||||
AsrModel(
|
||||
name="Test Model",
|
||||
installed=True,
|
||||
attribution=TEST_ATTR,
|
||||
languages=["en-US"],
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
EMPTY_INFO = Info()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Common fixtures for the Wyoming tests."""
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import STT_INFO
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry(hass: HomeAssistant) -> ConfigEntry:
|
||||
"""Create a config entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain="wyoming",
|
||||
data={
|
||||
"host": "1.2.3.4",
|
||||
"port": 1234,
|
||||
},
|
||||
title="Test ASR",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_wyoming_stt(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Initialize Wyoming."""
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.data.load_wyoming_info",
|
||||
return_value=STT_INFO,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -0,0 +1,42 @@
|
||||
# serializer version: 1
|
||||
# name: test_streaming_audio
|
||||
list([
|
||||
dict({
|
||||
'data': dict({
|
||||
'channels': 1,
|
||||
'rate': 16000,
|
||||
'timestamp': None,
|
||||
'width': 2,
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'audio-start',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'channels': 1,
|
||||
'rate': 16000,
|
||||
'timestamp': None,
|
||||
'width': 2,
|
||||
}),
|
||||
'payload': 'chunk1',
|
||||
'type': 'audio-chunk',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'channels': 1,
|
||||
'rate': 16000,
|
||||
'timestamp': None,
|
||||
'width': 2,
|
||||
}),
|
||||
'payload': 'chunk2',
|
||||
'type': 'audio-chunk',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'timestamp': None,
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'audio-stop',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Test the Wyoming config flow."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.wyoming.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import EMPTY_INFO, STT_INFO
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.data.load_wyoming_info",
|
||||
return_value=STT_INFO,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"port": 1234,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Test ASR"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"port": 1234,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.data.load_wyoming_info",
|
||||
return_value=None,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"port": 1234,
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_no_supported_services(hass: HomeAssistant) -> None:
|
||||
"""Test we handle no supported services error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.data.load_wyoming_info",
|
||||
return_value=EMPTY_INFO,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"port": 1234,
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.ABORT
|
||||
assert result2["reason"] == "no_services"
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Test init."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def test_cannot_connect(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.data.load_wyoming_info",
|
||||
return_value=None,
|
||||
):
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_unload(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, init_wyoming_stt
|
||||
) -> None:
|
||||
"""Test unload."""
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Test stt."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from wyoming.event import Event
|
||||
|
||||
from homeassistant.components import stt
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
class MockAsyncTcpClient:
|
||||
"""Mock AsyncTcpClient."""
|
||||
|
||||
def __init__(self, responses) -> None:
|
||||
"""Initialize."""
|
||||
self.host = None
|
||||
self.port = None
|
||||
self.written = []
|
||||
self.responses = responses
|
||||
|
||||
async def write_event(self, event):
|
||||
"""Send."""
|
||||
self.written.append(event)
|
||||
|
||||
async def read_event(self):
|
||||
"""Receive."""
|
||||
return self.responses.pop(0)
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Enter."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
"""Exit."""
|
||||
|
||||
def __call__(self, host, port):
|
||||
"""Call."""
|
||||
self.host = host
|
||||
self.port = port
|
||||
return self
|
||||
|
||||
|
||||
async def test_support(hass: HomeAssistant, init_wyoming_stt) -> None:
|
||||
"""Test streaming audio."""
|
||||
state = hass.states.get("stt.wyoming")
|
||||
assert state is not None
|
||||
|
||||
entity = stt.async_get_speech_to_text_entity(hass, "stt.wyoming")
|
||||
|
||||
assert entity.supported_languages == ["en-US"]
|
||||
assert entity.supported_formats == [stt.AudioFormats.WAV]
|
||||
assert entity.supported_codecs == [stt.AudioCodecs.PCM]
|
||||
assert entity.supported_bit_rates == [stt.AudioBitRates.BITRATE_16]
|
||||
assert entity.supported_sample_rates == [stt.AudioSampleRates.SAMPLERATE_16000]
|
||||
assert entity.supported_channels == [stt.AudioChannels.CHANNEL_MONO]
|
||||
|
||||
|
||||
async def test_streaming_audio(hass: HomeAssistant, init_wyoming_stt, snapshot) -> None:
|
||||
"""Test streaming audio."""
|
||||
entity = stt.async_get_speech_to_text_entity(hass, "stt.wyoming")
|
||||
|
||||
async def audio_stream():
|
||||
yield "chunk1"
|
||||
yield "chunk2"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.stt.AsyncTcpClient",
|
||||
MockAsyncTcpClient([Event(type="transcript", data={"text": "Hello world"})]),
|
||||
) as mock_client:
|
||||
result = await entity.async_process_audio_stream(None, audio_stream())
|
||||
|
||||
assert result.result == stt.SpeechResultState.SUCCESS
|
||||
assert result.text == "Hello world"
|
||||
assert mock_client.written == snapshot
|
||||
|
||||
|
||||
async def test_streaming_audio_connection_lost(
|
||||
hass: HomeAssistant, init_wyoming_stt
|
||||
) -> None:
|
||||
"""Test streaming audio and losing connection."""
|
||||
entity = stt.async_get_speech_to_text_entity(hass, "stt.wyoming")
|
||||
|
||||
async def audio_stream():
|
||||
yield "chunk1"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.stt.AsyncTcpClient",
|
||||
MockAsyncTcpClient([None]),
|
||||
):
|
||||
result = await entity.async_process_audio_stream(None, audio_stream())
|
||||
|
||||
assert result.result == stt.SpeechResultState.ERROR
|
||||
assert result.text is None
|
||||
|
||||
|
||||
async def test_streaming_audio_oserror(hass: HomeAssistant, init_wyoming_stt) -> None:
|
||||
"""Test streaming audio and error raising."""
|
||||
entity = stt.async_get_speech_to_text_entity(hass, "stt.wyoming")
|
||||
|
||||
async def audio_stream():
|
||||
yield "chunk1"
|
||||
|
||||
mock_client = MockAsyncTcpClient(
|
||||
[Event(type="transcript", data={"text": "Hello world"})]
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wyoming.stt.AsyncTcpClient",
|
||||
mock_client,
|
||||
), patch.object(mock_client, "read_event", side_effect=OSError("Boom!")):
|
||||
result = await entity.async_process_audio_stream(None, audio_stream())
|
||||
|
||||
assert result.result == stt.SpeechResultState.ERROR
|
||||
assert result.text is None
|
||||
Reference in New Issue
Block a user