Add unit tests for PeripheralAPIServer (#413)

Adds unit tests for the PeripheralAPIServer, covering initialization, state management, command dispatching, and event emission.
This commit is contained in:
Omar Amin authored and GitHub committed 2026-08-26 03:54:40 +03:00
1 parent adcef575b7
commit b0c53c41c1
1 file changed
+915
+915
View File
@@ -0,0 +1,915 @@
"""Unit tests for the peripheral WebSocket API (PeripheralAPIServer)."""
import asyncio
import json
from typing import Any, Dict, Optional
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from linux_voice_assistant.peripheral_api import LVACommand, LVAEvent, PeripheralAPIServer
from tests.unit.conftest import make_state
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_server(state=None) -> PeripheralAPIServer:
"""Return a PeripheralAPIServer, optionally attached to a ServerState."""
server = PeripheralAPIServer()
if state is not None:
server.set_state(state)
return server
def make_media_entity(key: int = 1, volume: float = 0.5, muted: bool = False):
entity = MagicMock()
entity.key = key
entity.volume = volume
entity.muted = muted
entity.previous_volume = volume
return entity
async def dispatch(server: PeripheralAPIServer, command: str, data: Optional[dict] = None) -> None:
payload: Dict[str, Any] = {"command": command}
if data is not None:
payload["data"] = data
await server._dispatch_command(json.dumps(payload)) # pylint: disable=protected-access
# ---------------------------------------------------------------------------
# __init__ / set_state
# ---------------------------------------------------------------------------
class TestInit:
def test_default_host_and_port(self):
server = PeripheralAPIServer()
assert server._host == "0.0.0.0"
assert server._port == 6055
def test_custom_host_and_port(self):
server = PeripheralAPIServer(host="127.0.0.1", port=9999)
assert server._host == "127.0.0.1"
assert server._port == 9999
def test_default_volume_step(self):
server = PeripheralAPIServer()
assert server._volume_step == PeripheralAPIServer.DEFAULT_VOLUME_STEP
def test_custom_volume_step(self):
server = PeripheralAPIServer(volume_step=0.1)
assert server._volume_step == pytest.approx(0.1)
def test_no_clients_on_init(self):
server = PeripheralAPIServer()
assert server._clients == set()
def test_no_state_on_init(self):
server = PeripheralAPIServer()
assert server._state is None
def test_no_current_state_on_init(self):
server = PeripheralAPIServer()
assert server._current_state is None
class TestSetState:
def test_attaches_state(self, tmp_path):
state = make_state(tmp_path)
server = make_server()
server.set_state(state)
assert server._state is state
# ---------------------------------------------------------------------------
# start() / stop()
# ---------------------------------------------------------------------------
class TestStartStop:
@pytest.mark.asyncio
async def test_start_creates_server(self):
server = make_server()
fake_server = MagicMock()
with patch("websockets.server.serve", AsyncMock(return_value=fake_server)) as mock_serve:
await server.start()
mock_serve.assert_called_once()
assert server._server is fake_server
@pytest.mark.asyncio
async def test_start_uses_configured_host_and_port(self):
server = make_server()
server._host = "127.0.0.1"
server._port = 1234
with patch("websockets.server.serve", AsyncMock(return_value=MagicMock())) as mock_serve:
await server.start()
_, args, kwargs = mock_serve.mock_calls[0]
assert "127.0.0.1" in args or kwargs.get("host") == "127.0.0.1"
@pytest.mark.asyncio
async def test_start_sets_loop(self):
server = make_server()
with patch("websockets.server.serve", AsyncMock(return_value=MagicMock())):
await server.start()
assert server._loop is asyncio.get_running_loop()
@pytest.mark.asyncio
async def test_stop_closes_server(self):
server = make_server()
fake_server = MagicMock()
fake_server.wait_closed = AsyncMock()
server._server = fake_server
await server.stop()
fake_server.close.assert_called_once()
fake_server.wait_closed.assert_awaited_once()
@pytest.mark.asyncio
async def test_stop_without_server_does_not_raise(self):
server = make_server()
await server.stop() # should not raise
@pytest.mark.asyncio
async def test_stop_cancels_pending_reconnect_task(self):
server = make_server()
pending_task = MagicMock()
server._pending_entity_reconnect_task = pending_task
await server.stop()
pending_task.cancel.assert_called_once()
assert server._pending_entity_reconnect_task is None
# ---------------------------------------------------------------------------
# _send_snapshot()
# ---------------------------------------------------------------------------
class TestSendSnapshot:
@pytest.mark.asyncio
async def test_no_state_does_nothing(self):
server = make_server()
websocket = AsyncMock()
await server._send_snapshot(websocket) # pylint: disable=protected-access
websocket.send.assert_not_called()
@pytest.mark.asyncio
async def test_sends_snapshot_payload(self, tmp_path):
state = make_state(tmp_path, volume=0.75, muted=False, connected=True)
server = make_server(state)
websocket = AsyncMock()
await server._send_snapshot(websocket) # pylint: disable=protected-access
sent = json.loads(websocket.send.call_args_list[0].args[0])
assert sent["event"] == "snapshot"
assert sent["data"]["volume"] == pytest.approx(0.75)
assert sent["data"]["muted"] is False
assert sent["data"]["ha_connected"] is True
@pytest.mark.asyncio
async def test_snapshot_includes_last_conversation_text(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
server._last_stt_text = "turn on the lights" # pylint: disable=protected-access
server._last_tts_text = "OK" # pylint: disable=protected-access
websocket = AsyncMock()
await server._send_snapshot(websocket) # pylint: disable=protected-access
sent = json.loads(websocket.send.call_args_list[0].args[0])
assert sent["data"]["last_stt_text"] == "turn on the lights"
assert sent["data"]["last_tts_text"] == "OK"
@pytest.mark.asyncio
async def test_no_current_state_skips_replay(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
websocket = AsyncMock()
await server._send_snapshot(websocket) # pylint: disable=protected-access
assert websocket.send.await_count == 1
@pytest.mark.asyncio
async def test_replays_current_state(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
server._current_state = LVAEvent.THINKING # pylint: disable=protected-access
websocket = AsyncMock()
await server._send_snapshot(websocket) # pylint: disable=protected-access
assert websocket.send.await_count == 2
replayed = json.loads(websocket.send.call_args_list[1].args[0])
assert replayed["event"] == "thinking"
@pytest.mark.asyncio
async def test_replays_current_state_with_data(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
server._current_state = LVAEvent.TIMER_TICKING # pylint: disable=protected-access
server._current_state_data = {"id": "1", "seconds_left": 30} # pylint: disable=protected-access
websocket = AsyncMock()
await server._send_snapshot(websocket) # pylint: disable=protected-access
replayed = json.loads(websocket.send.call_args_list[1].args[0])
assert replayed["data"]["seconds_left"] == 30
@pytest.mark.asyncio
async def test_skips_replay_of_disconnected_when_connected(self, tmp_path):
state = make_state(tmp_path, connected=True)
server = make_server(state)
server._current_state = LVAEvent.DISCONNECTED # pylint: disable=protected-access
websocket = AsyncMock()
await server._send_snapshot(websocket) # pylint: disable=protected-access
# Only the snapshot itself should have been sent
assert websocket.send.await_count == 1
# ---------------------------------------------------------------------------
# _dispatch_command() — basic parsing
# ---------------------------------------------------------------------------
class TestDispatchCommandParsing:
@pytest.mark.asyncio
async def test_invalid_json_does_not_raise(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
await server._dispatch_command("not json") # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_missing_command_is_noop(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
await server._dispatch_command(json.dumps({})) # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_no_state_attached_is_noop(self):
server = make_server()
await dispatch(server, LVACommand.START_LISTENING.value) # should not raise
# ---------------------------------------------------------------------------
# _dispatch_command() — voice pipeline
# ---------------------------------------------------------------------------
class TestDispatchStartListening:
@pytest.mark.asyncio
async def test_calls_satellite_start_listening(self, tmp_path):
state = make_state(tmp_path, muted=False)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.START_LISTENING.value)
satellite.start_listening.assert_called_once()
@pytest.mark.asyncio
async def test_noop_when_muted(self, tmp_path):
state = make_state(tmp_path, muted=True)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.START_LISTENING.value)
satellite.start_listening.assert_not_called()
@pytest.mark.asyncio
async def test_noop_when_no_satellite(self, tmp_path):
state = make_state(tmp_path, muted=False)
state.satellite = None
server = make_server(state)
await dispatch(server, LVACommand.START_LISTENING.value) # should not raise
class TestDispatchStopPipeline:
@pytest.mark.asyncio
async def test_calls_satellite_stop(self, tmp_path):
state = make_state(tmp_path)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.STOP_PIPELINE.value)
satellite.stop.assert_called_once()
@pytest.mark.asyncio
async def test_noop_when_no_satellite(self, tmp_path):
state = make_state(tmp_path)
state.satellite = None
server = make_server(state)
await dispatch(server, LVACommand.STOP_PIPELINE.value) # should not raise
# ---------------------------------------------------------------------------
# _dispatch_command() — microphone mute
# ---------------------------------------------------------------------------
class TestDispatchMute:
@pytest.mark.asyncio
async def test_mute_mic_calls_set_muted(self, tmp_path):
state = make_state(tmp_path, muted=False)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.MUTE_MIC.value)
satellite._set_muted.assert_called_once_with(True) # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_mute_mic_noop_when_already_muted(self, tmp_path):
state = make_state(tmp_path, muted=True)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.MUTE_MIC.value)
satellite._set_muted.assert_not_called() # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_unmute_mic_calls_set_muted(self, tmp_path):
state = make_state(tmp_path, muted=True)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.UNMUTE_MIC.value)
satellite._set_muted.assert_called_once_with(False) # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_unmute_mic_noop_when_already_unmuted(self, tmp_path):
state = make_state(tmp_path, muted=False)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.UNMUTE_MIC.value)
satellite._set_muted.assert_not_called() # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_mute_mic_pushes_switch_state_to_ha(self, tmp_path):
state = make_state(tmp_path, muted=False)
state.mute_switch_entity = MagicMock(key=1)
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.MUTE_MIC.value)
satellite.send_messages.assert_called_once()
# ---------------------------------------------------------------------------
# _dispatch_command() — volume
# ---------------------------------------------------------------------------
class TestDispatchVolume:
@pytest.mark.asyncio
async def test_volume_up_increases_volume(self, tmp_path):
state = make_state(tmp_path, volume=0.5)
server = make_server(state)
await dispatch(server, LVACommand.VOLUME_UP.value)
assert state.volume == pytest.approx(0.5 + PeripheralAPIServer.DEFAULT_VOLUME_STEP)
@pytest.mark.asyncio
async def test_volume_down_decreases_volume(self, tmp_path):
state = make_state(tmp_path, volume=0.5)
server = make_server(state)
await dispatch(server, LVACommand.VOLUME_DOWN.value)
assert state.volume == pytest.approx(0.5 - PeripheralAPIServer.DEFAULT_VOLUME_STEP)
@pytest.mark.asyncio
async def test_volume_up_clamped_at_one(self, tmp_path):
state = make_state(tmp_path, volume=1.0)
server = make_server(state)
await dispatch(server, LVACommand.VOLUME_UP.value)
assert state.volume == pytest.approx(1.0)
@pytest.mark.asyncio
async def test_volume_down_clamped_at_zero(self, tmp_path):
state = make_state(tmp_path, volume=0.0)
server = make_server(state)
await dispatch(server, LVACommand.VOLUME_DOWN.value)
assert state.volume == pytest.approx(0.0)
@pytest.mark.asyncio
async def test_volume_up_updates_music_and_tts_players(self, tmp_path):
state = make_state(tmp_path, volume=0.5)
server = make_server(state)
await dispatch(server, LVACommand.VOLUME_UP.value)
state.music_player.set_volume.assert_called_once()
state.tts_player.set_volume.assert_called_once()
@pytest.mark.asyncio
async def test_volume_up_pushes_media_player_state(self, tmp_path):
state = make_state(tmp_path, volume=0.5)
state.media_player_entity = make_media_entity()
satellite = MagicMock()
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.VOLUME_UP.value)
satellite.send_messages.assert_called_once()
@pytest.mark.asyncio
async def test_set_volume_sets_exact_value(self, tmp_path):
state = make_state(tmp_path, volume=0.2)
server = make_server(state)
await dispatch(server, LVACommand.SET_VOLUME.value, {"volume": 0.9})
assert state.volume == pytest.approx(0.9)
@pytest.mark.asyncio
async def test_set_volume_clamps_above_one(self, tmp_path):
state = make_state(tmp_path, volume=0.2)
server = make_server(state)
await dispatch(server, LVACommand.SET_VOLUME.value, {"volume": 5.0})
assert state.volume == pytest.approx(1.0)
@pytest.mark.asyncio
async def test_set_volume_clamps_below_zero(self, tmp_path):
state = make_state(tmp_path, volume=0.2)
server = make_server(state)
await dispatch(server, LVACommand.SET_VOLUME.value, {"volume": -5.0})
assert state.volume == pytest.approx(0.0)
@pytest.mark.asyncio
async def test_set_volume_ignores_non_numeric(self, tmp_path):
state = make_state(tmp_path, volume=0.2)
server = make_server(state)
await dispatch(server, LVACommand.SET_VOLUME.value, {"volume": "loud"})
assert state.volume == pytest.approx(0.2)
@pytest.mark.asyncio
async def test_set_volume_accepts_int(self, tmp_path):
state = make_state(tmp_path, volume=0.2)
server = make_server(state)
await dispatch(server, LVACommand.SET_VOLUME.value, {"volume": 1})
assert state.volume == pytest.approx(1.0)
# ---------------------------------------------------------------------------
# _dispatch_command() — timer
# ---------------------------------------------------------------------------
class TestDispatchStopTimerRinging:
@pytest.mark.asyncio
async def test_stops_ringing_timer(self, tmp_path):
state = make_state(tmp_path)
satellite = MagicMock()
satellite._timer_finished = True # pylint: disable=protected-access
state.satellite = satellite
state.active_wake_words.add(state.stop_word.id)
server = make_server(state)
await dispatch(server, LVACommand.STOP_TIMER_RINGING.value)
assert satellite._timer_finished is False # pylint: disable=protected-access
assert state.stop_word.id not in state.active_wake_words
state.tts_player.stop.assert_called_once()
satellite.unduck.assert_called_once()
@pytest.mark.asyncio
async def test_noop_when_not_ringing(self, tmp_path):
state = make_state(tmp_path)
satellite = MagicMock()
satellite._timer_finished = False # pylint: disable=protected-access
state.satellite = satellite
server = make_server(state)
await dispatch(server, LVACommand.STOP_TIMER_RINGING.value)
state.tts_player.stop.assert_not_called()
@pytest.mark.asyncio
async def test_noop_when_no_satellite(self, tmp_path):
state = make_state(tmp_path)
state.satellite = None
server = make_server(state)
await dispatch(server, LVACommand.STOP_TIMER_RINGING.value) # should not raise
# ---------------------------------------------------------------------------
# _dispatch_command() — media player
# ---------------------------------------------------------------------------
class TestDispatchMediaPlayer:
@pytest.mark.asyncio
async def test_stop_media_player_stops_music(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
await dispatch(server, LVACommand.STOP_MEDIA_PLAYER.value)
state.music_player.stop.assert_called_once()
@pytest.mark.asyncio
async def test_pause_media_player_pauses_music(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
await dispatch(server, LVACommand.PAUSE_MEDIA_PLAYER.value)
state.music_player.pause.assert_called_once()
@pytest.mark.asyncio
async def test_resume_media_player_resumes_music(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
await dispatch(server, LVACommand.RESUME_MEDIA_PLAYER.value)
state.music_player.resume.assert_called_once()
@pytest.mark.asyncio
async def test_stop_media_player_updates_entity_state(self, tmp_path):
from aioesphomeapi.model import MediaPlayerState # pylint: disable=import-outside-toplevel
state = make_state(tmp_path)
state.media_player_entity = make_media_entity()
server = make_server(state)
await dispatch(server, LVACommand.STOP_MEDIA_PLAYER.value)
assert state.media_player_entity.state == MediaPlayerState.IDLE
# ---------------------------------------------------------------------------
# _dispatch_command() — button events
# ---------------------------------------------------------------------------
class TestDispatchButtonPress:
@pytest.mark.asyncio
async def test_single_press_updates_entity(self, tmp_path):
state = make_state(tmp_path)
state.button_event_sensor_entity = MagicMock()
server = make_server(state)
await dispatch(server, LVACommand.BUTTON_SINGLE_PRESS.value)
state.button_event_sensor_entity.update_state.assert_called_once_with("single_press")
@pytest.mark.asyncio
async def test_single_press_does_not_play_sound(self, tmp_path):
state = make_state(tmp_path)
state.button_event_sensor_entity = MagicMock()
server = make_server(state)
await dispatch(server, LVACommand.BUTTON_SINGLE_PRESS.value)
state.tts_player.play.assert_not_called()
@pytest.mark.asyncio
async def test_double_press_plays_sound_and_updates_entity(self, tmp_path):
state = make_state(tmp_path)
state.button_event_sensor_entity = MagicMock()
server = make_server(state)
await dispatch(server, LVACommand.BUTTON_DOUBLE_PRESS.value)
state.tts_player.play.assert_called_once_with(state.button_double_press_sound)
state.button_event_sensor_entity.update_state.assert_called_once_with("double_press")
@pytest.mark.asyncio
async def test_triple_press_plays_sound_and_updates_entity(self, tmp_path):
state = make_state(tmp_path)
state.button_event_sensor_entity = MagicMock()
server = make_server(state)
await dispatch(server, LVACommand.BUTTON_TRIPLE_PRESS.value)
state.tts_player.play.assert_called_once_with(state.button_triple_press_sound)
state.button_event_sensor_entity.update_state.assert_called_once_with("triple_press")
@pytest.mark.asyncio
async def test_long_press_plays_sound_and_updates_entity(self, tmp_path):
state = make_state(tmp_path)
state.button_event_sensor_entity = MagicMock()
server = make_server(state)
await dispatch(server, LVACommand.BUTTON_LONG_PRESS.value)
state.tts_player.play.assert_called_once_with(state.button_long_press_sound)
state.button_event_sensor_entity.update_state.assert_called_once_with("long_press")
@pytest.mark.asyncio
async def test_button_press_noop_when_entity_not_registered(self, tmp_path):
state = make_state(tmp_path)
state.button_event_sensor_entity = None
server = make_server(state)
await dispatch(server, LVACommand.BUTTON_SINGLE_PRESS.value) # should not raise
# ---------------------------------------------------------------------------
# register_light() / register_button()
# ---------------------------------------------------------------------------
class TestRegisterLight:
def test_missing_object_id_is_ignored(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
server._register_light({}, None) # pylint: disable=protected-access
assert state.pending_lights == []
def test_adds_light_registration(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
server._register_light( # pylint: disable=protected-access
{"name": "LEDs", "object_id": "leds", "effects": ["Voice Assistant"]},
None,
)
assert len(state.pending_lights) == 1
assert state.pending_lights[0].object_id == "leds"
def test_duplicate_object_id_is_ignored(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
data = {"name": "LEDs", "object_id": "leds"}
server._register_light(data, None) # pylint: disable=protected-access
server._register_light(data, None) # pylint: disable=protected-access
assert len(state.pending_lights) == 1
def test_calls_register_pending_lights_on_satellite(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
satellite = MagicMock()
server._register_light({"object_id": "leds"}, satellite) # pylint: disable=protected-access
satellite.register_pending_lights.assert_called_once()
def test_no_state_is_noop(self):
server = make_server()
server._register_light({"object_id": "leds"}, None) # pylint: disable=protected-access # should not raise
class TestRegisterButton:
def test_sets_pending_button(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
server._register_button(None) # pylint: disable=protected-access
assert state.pending_button is True
def test_idempotent_when_already_registered(self, tmp_path):
state = make_state(tmp_path)
state.pending_button = True
server = make_server(state)
satellite = MagicMock()
server._register_button(satellite) # pylint: disable=protected-access
satellite.register_pending_button.assert_not_called()
def test_calls_register_pending_button_on_satellite(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
satellite = MagicMock()
server._register_button(satellite) # pylint: disable=protected-access
satellite.register_pending_button.assert_called_once()
def test_no_state_is_noop(self):
server = make_server()
server._register_button(None) # pylint: disable=protected-access # should not raise
# ---------------------------------------------------------------------------
# _dispatch_command() — register_light / register_button routing
# ---------------------------------------------------------------------------
class TestDispatchRegistration:
@pytest.mark.asyncio
async def test_register_light_command_routes_correctly(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
await dispatch(server, LVACommand.REGISTER_LIGHT.value, {"object_id": "leds"})
assert len(state.pending_lights) == 1
@pytest.mark.asyncio
async def test_register_button_command_routes_correctly(self, tmp_path):
state = make_state(tmp_path)
server = make_server(state)
await dispatch(server, LVACommand.REGISTER_BUTTON.value)
assert state.pending_button is True
# ---------------------------------------------------------------------------
# emit_event()
# ---------------------------------------------------------------------------
class TestEmitEvent:
@pytest.mark.asyncio
async def test_broadcasts_to_all_clients(self):
server = make_server()
ws1, ws2 = AsyncMock(), AsyncMock()
server._clients = {ws1, ws2} # pylint: disable=protected-access
await server.emit_event(LVAEvent.IDLE)
ws1.send.assert_awaited_once()
ws2.send.assert_awaited_once()
@pytest.mark.asyncio
async def test_no_clients_does_not_raise(self):
server = make_server()
await server.emit_event(LVAEvent.IDLE) # should not raise
@pytest.mark.asyncio
async def test_payload_includes_data(self):
server = make_server()
ws = AsyncMock()
server._clients = {ws} # pylint: disable=protected-access
await server.emit_event(LVAEvent.STT_TEXT, {"text": "hello"})
sent = json.loads(ws.send.call_args.args[0])
assert sent["data"]["text"] == "hello"
@pytest.mark.asyncio
async def test_dead_client_is_removed(self):
server = make_server()
dead_ws = AsyncMock()
dead_ws.send.side_effect = ConnectionError
alive_ws = AsyncMock()
server._clients = {dead_ws, alive_ws} # pylint: disable=protected-access
await server.emit_event(LVAEvent.IDLE)
assert dead_ws not in server._clients # pylint: disable=protected-access
assert alive_ws in server._clients # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_caches_last_stt_text(self):
server = make_server()
await server.emit_event(LVAEvent.STT_TEXT, {"text": "turn on the lights"})
assert server._last_stt_text == "turn on the lights" # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_caches_last_tts_text(self):
server = make_server()
await server.emit_event(LVAEvent.TTS_TEXT, {"text": "OK"})
assert server._last_tts_text == "OK" # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_listening_clears_cached_text(self):
server = make_server()
server._last_stt_text = "old" # pylint: disable=protected-access
server._last_tts_text = "old" # pylint: disable=protected-access
await server.emit_event(LVAEvent.LISTENING)
assert server._last_stt_text is None # pylint: disable=protected-access
assert server._last_tts_text is None # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_tracks_current_state_for_state_events(self):
server = make_server()
await server.emit_event(LVAEvent.TTS_SPEAKING)
assert server._current_state == LVAEvent.TTS_SPEAKING # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_does_not_track_transient_events(self):
server = make_server()
await server.emit_event(LVAEvent.STT_TEXT, {"text": "hi"})
assert server._current_state is None # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_timer_updated_refreshes_ticking_data(self):
server = make_server()
await server.emit_event(LVAEvent.TIMER_TICKING, {"seconds_left": 30})
await server.emit_event(LVAEvent.TIMER_UPDATED, {"seconds_left": 25})
assert server._current_state == LVAEvent.TIMER_TICKING # pylint: disable=protected-access
assert server._current_state_data["seconds_left"] == 25 # pylint: disable=protected-access
@pytest.mark.asyncio
async def test_timer_updated_ignored_when_not_ticking(self):
server = make_server()
await server.emit_event(LVAEvent.IDLE)
await server.emit_event(LVAEvent.TIMER_UPDATED, {"seconds_left": 25})
assert server._current_state == LVAEvent.IDLE # pylint: disable=protected-access
# ---------------------------------------------------------------------------
# emit_event_sync()
# ---------------------------------------------------------------------------
class TestEmitEventSync:
def test_noop_when_no_loop(self):
server = make_server()
server.emit_event_sync(LVAEvent.IDLE) # should not raise
def test_noop_when_loop_closed(self):
server = make_server()
loop = MagicMock()
loop.is_closed.return_value = True
server._loop = loop # pylint: disable=protected-access
with patch("asyncio.run_coroutine_threadsafe") as mock_run:
server.emit_event_sync(LVAEvent.IDLE)
mock_run.assert_not_called()
def test_schedules_coroutine_on_loop(self):
server = make_server()
loop = MagicMock()
loop.is_closed.return_value = False
server._loop = loop # pylint: disable=protected-access
with patch("asyncio.run_coroutine_threadsafe") as mock_run:
server.emit_event_sync(LVAEvent.IDLE, {"foo": "bar"})
mock_run.assert_called_once()
assert mock_run.call_args.args[1] is loop
# ---------------------------------------------------------------------------
# _create_media_player_response()
# ---------------------------------------------------------------------------
class TestCreateMediaPlayerResponse:
def test_returns_response_with_entity_fields(self, tmp_path):
from aioesphomeapi.model import MediaPlayerState # pylint: disable=import-outside-toplevel
state = make_state(tmp_path)
state.media_player_entity = make_media_entity(key=3, volume=0.4, muted=True)
server = make_server(state)
response = server._create_media_player_response(MediaPlayerState.PLAYING) # pylint: disable=protected-access
assert response.key == 3
assert response.volume == pytest.approx(0.4)
assert response.muted is True
assert response.state == MediaPlayerState.PLAYING