"""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)