1 parent
4a0695ab57
commit
9b2a8b4b30
24 files changed
+3002
-24
No files matched your search
@@ -0,0 +1,424 @@
|
||||
"""Unit tests for ESPHome entity classes."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
|
||||
ListEntitiesMediaPlayerResponse,
|
||||
ListEntitiesNumberResponse,
|
||||
ListEntitiesRequest,
|
||||
ListEntitiesSelectResponse,
|
||||
ListEntitiesSwitchResponse,
|
||||
MediaPlayerCommandRequest,
|
||||
MediaPlayerStateResponse,
|
||||
NumberCommandRequest,
|
||||
NumberStateResponse,
|
||||
SelectCommandRequest,
|
||||
SelectStateResponse,
|
||||
SubscribeHomeAssistantStatesRequest,
|
||||
SwitchCommandRequest,
|
||||
SwitchStateResponse,
|
||||
)
|
||||
from aioesphomeapi.model import MediaPlayerCommand, MediaPlayerState
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_server():
|
||||
"""Minimal mock APIServer."""
|
||||
server = MagicMock()
|
||||
server.state = MagicMock()
|
||||
return server
|
||||
|
||||
|
||||
def make_media_player(server=None, key=1, initial_volume=1.0):
|
||||
from linux_voice_assistant.entity import MediaPlayerEntity
|
||||
|
||||
server = server or make_server()
|
||||
return MediaPlayerEntity(
|
||||
server=server,
|
||||
key=key,
|
||||
name="Media Player",
|
||||
object_id="test_media_player",
|
||||
music_player=MagicMock(),
|
||||
announce_player=MagicMock(),
|
||||
initial_volume=initial_volume,
|
||||
)
|
||||
|
||||
|
||||
def make_mute_switch(server=None, key=2, initial_muted=False):
|
||||
from linux_voice_assistant.entity import MuteSwitchEntity
|
||||
|
||||
server = server or make_server()
|
||||
get_muted = MagicMock(return_value=initial_muted)
|
||||
set_muted = MagicMock()
|
||||
entity = MuteSwitchEntity(
|
||||
server=server,
|
||||
key=key,
|
||||
name="Mute",
|
||||
object_id="mute",
|
||||
get_muted=get_muted,
|
||||
set_muted=set_muted,
|
||||
)
|
||||
entity._get_muted = get_muted
|
||||
entity._set_muted = set_muted
|
||||
return entity
|
||||
|
||||
|
||||
def make_mic_setting(server=None, key=3, options=None, value=0.0):
|
||||
from linux_voice_assistant.entity import MicSettingEntity
|
||||
|
||||
server = server or make_server()
|
||||
get_value = MagicMock(return_value=value)
|
||||
set_value = MagicMock()
|
||||
entity = MicSettingEntity(
|
||||
server=server,
|
||||
key=key,
|
||||
name="Mic Gain",
|
||||
object_id="mic_gain",
|
||||
get_value=get_value,
|
||||
set_value=set_value,
|
||||
min_value=0.0,
|
||||
max_value=31.0,
|
||||
options=options,
|
||||
)
|
||||
entity._get_value_mock = get_value
|
||||
entity._set_value_mock = set_value
|
||||
return entity
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MediaPlayerEntity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMediaPlayerEntityInit:
|
||||
def test_initial_volume_clamped_above_one(self):
|
||||
entity = make_media_player(initial_volume=1.5)
|
||||
assert entity.volume == 1.0
|
||||
|
||||
def test_initial_volume_clamped_below_zero(self):
|
||||
entity = make_media_player(initial_volume=-0.5)
|
||||
assert entity.volume == 0.0
|
||||
|
||||
def test_initial_volume_stored(self):
|
||||
entity = make_media_player(initial_volume=0.7)
|
||||
assert abs(entity.volume - 0.7) < 0.001
|
||||
|
||||
def test_initial_state_is_idle(self):
|
||||
entity = make_media_player()
|
||||
assert entity.state == MediaPlayerState.IDLE
|
||||
|
||||
def test_not_muted_by_default(self):
|
||||
entity = make_media_player()
|
||||
assert entity.muted is False
|
||||
|
||||
|
||||
class TestMediaPlayerEntityListEntities:
|
||||
def test_list_entities_request_yields_response(self):
|
||||
entity = make_media_player(key=5)
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
assert len(msgs) == 1
|
||||
assert isinstance(msgs[0], ListEntitiesMediaPlayerResponse)
|
||||
|
||||
def test_list_entities_response_has_correct_key(self):
|
||||
entity = make_media_player(key=5)
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
assert msgs[0].key == 5
|
||||
|
||||
def test_list_entities_response_has_correct_object_id(self):
|
||||
entity = make_media_player()
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
assert msgs[0].object_id == "test_media_player"
|
||||
|
||||
def test_list_entities_supports_pause(self):
|
||||
entity = make_media_player()
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
assert msgs[0].supports_pause is True
|
||||
|
||||
|
||||
class TestMediaPlayerEntitySubscribeStates:
|
||||
def test_subscribe_states_yields_state_response(self):
|
||||
entity = make_media_player()
|
||||
msgs = list(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
||||
assert len(msgs) == 1
|
||||
assert isinstance(msgs[0], MediaPlayerStateResponse)
|
||||
|
||||
def test_subscribe_states_has_correct_key(self):
|
||||
entity = make_media_player(key=7)
|
||||
msgs = list(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
||||
assert msgs[0].key == 7
|
||||
|
||||
|
||||
class TestMediaPlayerEntityVolume:
|
||||
def test_apply_volume_sets_both_players(self):
|
||||
entity = make_media_player(initial_volume=1.0)
|
||||
entity._apply_volume(0.5, persist=False)
|
||||
entity.music_player.set_volume.assert_called_with(50)
|
||||
entity.announce_player.set_volume.assert_called_with(50)
|
||||
|
||||
def test_apply_volume_clamps_above_one(self):
|
||||
entity = make_media_player()
|
||||
entity._apply_volume(1.5, persist=False)
|
||||
assert entity.volume == 1.0
|
||||
|
||||
def test_apply_volume_clamps_below_zero(self):
|
||||
entity = make_media_player()
|
||||
entity._apply_volume(-0.1, persist=False)
|
||||
assert entity.volume == 0.0
|
||||
|
||||
def test_apply_volume_stores_previous_volume(self):
|
||||
entity = make_media_player(initial_volume=0.8)
|
||||
entity._apply_volume(0.4, persist=False, remember=True)
|
||||
assert abs(entity.previous_volume - 0.4) < 0.001
|
||||
|
||||
def test_apply_volume_persist_calls_callback(self):
|
||||
callback = MagicMock()
|
||||
entity = make_media_player()
|
||||
entity._on_volume_changed = callback
|
||||
entity._apply_volume(0.5, persist=True)
|
||||
callback.assert_called_once_with(0.5)
|
||||
|
||||
def test_apply_volume_no_persist_skips_callback(self):
|
||||
callback = MagicMock()
|
||||
entity = make_media_player()
|
||||
entity._on_volume_changed = callback
|
||||
entity._apply_volume(0.5, persist=False)
|
||||
callback.assert_not_called()
|
||||
|
||||
def test_volume_command_yields_state_response(self):
|
||||
entity = make_media_player(key=1)
|
||||
msg = MediaPlayerCommandRequest(key=1, has_volume=True, volume=0.6)
|
||||
msgs = list(entity.handle_message(msg))
|
||||
assert any(isinstance(m, MediaPlayerStateResponse) for m in msgs)
|
||||
|
||||
def test_volume_command_wrong_key_ignored(self):
|
||||
entity = make_media_player(key=1)
|
||||
msg = MediaPlayerCommandRequest(key=99, has_volume=True, volume=0.6)
|
||||
msgs = list(entity.handle_message(msg))
|
||||
assert msgs == [] or not any(isinstance(m, MediaPlayerStateResponse) for m in msgs)
|
||||
|
||||
|
||||
class TestMediaPlayerEntityMuteUnmute:
|
||||
def test_mute_sets_volume_to_zero(self):
|
||||
entity = make_media_player(initial_volume=0.8)
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.MUTE)
|
||||
list(entity.handle_message(msg))
|
||||
assert entity.volume == 0.0
|
||||
|
||||
def test_mute_saves_previous_volume(self):
|
||||
entity = make_media_player(initial_volume=0.8)
|
||||
entity.volume = 0.8
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.MUTE)
|
||||
list(entity.handle_message(msg))
|
||||
assert abs(entity.previous_volume - 0.8) < 0.001
|
||||
|
||||
def test_mute_sets_muted_flag(self):
|
||||
entity = make_media_player(key=1)
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.MUTE)
|
||||
list(entity.handle_message(msg))
|
||||
assert entity.muted is True
|
||||
|
||||
def test_unmute_restores_previous_volume(self):
|
||||
entity = make_media_player(key=1, initial_volume=0.8)
|
||||
entity.previous_volume = 0.8
|
||||
entity.muted = True
|
||||
entity.volume = 0.0
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.UNMUTE)
|
||||
list(entity.handle_message(msg))
|
||||
assert abs(entity.volume - 0.8) < 0.001
|
||||
|
||||
def test_unmute_clears_muted_flag(self):
|
||||
entity = make_media_player(key=1)
|
||||
entity.muted = True
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.UNMUTE)
|
||||
list(entity.handle_message(msg))
|
||||
assert entity.muted is False
|
||||
|
||||
def test_double_mute_does_not_overwrite_previous_volume(self):
|
||||
entity = make_media_player(key=1, initial_volume=0.9)
|
||||
entity.volume = 0.9
|
||||
entity.previous_volume = 0.9
|
||||
|
||||
msg_mute = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.MUTE)
|
||||
list(entity.handle_message(msg_mute))
|
||||
# Mute again — should not overwrite previous_volume with 0
|
||||
list(entity.handle_message(msg_mute))
|
||||
assert abs(entity.previous_volume - 0.9) < 0.001
|
||||
|
||||
|
||||
class TestMediaPlayerEntityPlayback:
|
||||
def test_pause_command_calls_music_player_pause(self):
|
||||
entity = make_media_player(key=1)
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.PAUSE)
|
||||
list(entity.handle_message(msg))
|
||||
entity.music_player.pause.assert_called_once()
|
||||
|
||||
def test_stop_command_calls_music_player_stop(self):
|
||||
entity = make_media_player(key=1)
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.STOP)
|
||||
list(entity.handle_message(msg))
|
||||
entity.music_player.stop.assert_called_once()
|
||||
|
||||
def test_play_command_calls_music_player_resume(self):
|
||||
entity = make_media_player(key=1)
|
||||
msg = MediaPlayerCommandRequest(key=1, has_command=True, command=MediaPlayerCommand.PLAY)
|
||||
list(entity.handle_message(msg))
|
||||
entity.music_player.resume.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MuteSwitchEntity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMuteSwitchEntity:
|
||||
def test_initial_state_synced_from_get_muted(self):
|
||||
entity = make_mute_switch(initial_muted=True)
|
||||
assert entity._switch_state is True
|
||||
|
||||
def test_switch_command_calls_set_muted(self):
|
||||
entity = make_mute_switch(key=2)
|
||||
msg = SwitchCommandRequest(key=2, state=True)
|
||||
list(entity.handle_message(msg))
|
||||
entity._set_muted.assert_called_once_with(True)
|
||||
|
||||
def test_switch_command_updates_internal_state(self):
|
||||
entity = make_mute_switch(key=2)
|
||||
msg = SwitchCommandRequest(key=2, state=True)
|
||||
list(entity.handle_message(msg))
|
||||
assert entity._switch_state is True
|
||||
|
||||
def test_switch_command_yields_switch_state_response(self):
|
||||
entity = make_mute_switch(key=2)
|
||||
msg = SwitchCommandRequest(key=2, state=True)
|
||||
msgs = list(entity.handle_message(msg))
|
||||
assert any(isinstance(m, SwitchStateResponse) for m in msgs)
|
||||
|
||||
def test_switch_command_wrong_key_ignored(self):
|
||||
entity = make_mute_switch(key=2)
|
||||
msg = SwitchCommandRequest(key=99, state=True)
|
||||
list(entity.handle_message(msg))
|
||||
entity._set_muted.assert_not_called()
|
||||
|
||||
def test_list_entities_request_yields_switch_response(self):
|
||||
entity = make_mute_switch()
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
assert any(isinstance(m, ListEntitiesSwitchResponse) for m in msgs)
|
||||
|
||||
def test_subscribe_states_yields_switch_state_response(self):
|
||||
entity = make_mute_switch()
|
||||
msgs = list(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
||||
assert any(isinstance(m, SwitchStateResponse) for m in msgs)
|
||||
|
||||
def test_sync_with_state_updates_switch_state(self):
|
||||
entity = make_mute_switch(initial_muted=False)
|
||||
entity._get_muted.return_value = True
|
||||
entity.sync_with_state()
|
||||
assert entity._switch_state is True
|
||||
|
||||
def test_update_set_muted_replaces_callback(self):
|
||||
entity = make_mute_switch(key=2)
|
||||
new_set_muted = MagicMock()
|
||||
entity.update_set_muted(new_set_muted)
|
||||
msg = SwitchCommandRequest(key=2, state=True)
|
||||
list(entity.handle_message(msg))
|
||||
new_set_muted.assert_called_once_with(True)
|
||||
|
||||
def test_update_get_muted_replaces_callback(self):
|
||||
entity = make_mute_switch()
|
||||
new_get_muted = MagicMock(return_value=True)
|
||||
entity.update_get_muted(new_get_muted)
|
||||
entity.sync_with_state()
|
||||
assert entity._switch_state is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MicSettingEntity — number mode (no options)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMicSettingEntityNumber:
|
||||
def test_list_entities_yields_number_response(self):
|
||||
entity = make_mic_setting(key=3, options=None)
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
assert any(isinstance(m, ListEntitiesNumberResponse) for m in msgs)
|
||||
|
||||
def test_number_command_calls_set_value(self):
|
||||
entity = make_mic_setting(key=3, options=None)
|
||||
msg = NumberCommandRequest(key=3, state=5.0)
|
||||
list(entity.handle_message(msg))
|
||||
entity._set_value_mock.assert_called_once_with(5.0)
|
||||
|
||||
def test_number_command_yields_number_state_response(self):
|
||||
entity = make_mic_setting(key=3, options=None)
|
||||
msg = NumberCommandRequest(key=3, state=5.0)
|
||||
msgs = list(entity.handle_message(msg))
|
||||
assert any(isinstance(m, NumberStateResponse) for m in msgs)
|
||||
|
||||
def test_number_command_wrong_key_ignored(self):
|
||||
entity = make_mic_setting(key=3, options=None)
|
||||
msg = NumberCommandRequest(key=99, state=5.0)
|
||||
list(entity.handle_message(msg))
|
||||
entity._set_value_mock.assert_not_called()
|
||||
|
||||
def test_subscribe_states_yields_number_state_response(self):
|
||||
entity = make_mic_setting(key=3, options=None, value=2.0)
|
||||
msgs = list(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
||||
assert any(isinstance(m, NumberStateResponse) for m in msgs)
|
||||
|
||||
def test_sync_with_state_updates_internal_state(self):
|
||||
entity = make_mic_setting(key=3, options=None, value=0.0)
|
||||
entity._get_value_mock.return_value = 7.0
|
||||
entity.sync_with_state()
|
||||
assert entity._state == 7.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MicSettingEntity — select mode (with options)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMicSettingEntitySelect:
|
||||
NOISE_OPTIONS = ["Off", "Low", "Medium", "High", "Max"]
|
||||
|
||||
def test_list_entities_yields_select_response(self):
|
||||
entity = make_mic_setting(key=4, options=self.NOISE_OPTIONS)
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
assert any(isinstance(m, ListEntitiesSelectResponse) for m in msgs)
|
||||
|
||||
def test_select_response_has_correct_options(self):
|
||||
entity = make_mic_setting(key=4, options=self.NOISE_OPTIONS)
|
||||
msgs = list(entity.handle_message(ListEntitiesRequest()))
|
||||
select_msg = next(m for m in msgs if isinstance(m, ListEntitiesSelectResponse))
|
||||
assert list(select_msg.options) == self.NOISE_OPTIONS
|
||||
|
||||
def test_select_command_calls_set_value(self):
|
||||
entity = make_mic_setting(key=4, options=self.NOISE_OPTIONS)
|
||||
msg = SelectCommandRequest(key=4, state="High")
|
||||
list(entity.handle_message(msg))
|
||||
entity._set_value_mock.assert_called_once_with("High")
|
||||
|
||||
def test_select_command_yields_select_state_response(self):
|
||||
entity = make_mic_setting(key=4, options=self.NOISE_OPTIONS)
|
||||
msg = SelectCommandRequest(key=4, state="Medium")
|
||||
msgs = list(entity.handle_message(msg))
|
||||
assert any(isinstance(m, SelectStateResponse) for m in msgs)
|
||||
|
||||
def test_select_command_wrong_key_ignored(self):
|
||||
entity = make_mic_setting(key=4, options=self.NOISE_OPTIONS)
|
||||
msg = SelectCommandRequest(key=99, state="Low")
|
||||
list(entity.handle_message(msg))
|
||||
entity._set_value_mock.assert_not_called()
|
||||
|
||||
def test_subscribe_states_yields_select_state_response(self):
|
||||
entity = make_mic_setting(key=4, options=self.NOISE_OPTIONS, value="Off")
|
||||
msgs = list(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
||||
assert any(isinstance(m, SelectStateResponse) for m in msgs)
|
||||
|
||||
def test_subscribe_states_response_has_string_state(self):
|
||||
entity = make_mic_setting(key=4, options=self.NOISE_OPTIONS, value="Low")
|
||||
msgs = list(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
||||
select_msg = next(m for m in msgs if isinstance(m, SelectStateResponse))
|
||||
assert isinstance(select_msg.state, str)
|
||||
Reference in new issue
Block a user