"""tests/test_receiver_switch.py — a plain switch as the Receiver (t61). The user's request, verbatim: "it would be nice if a receiver could just be a switch, I don't need my volume scripts then because the receiver portion could just make sure the receiver is on if it's a switch". So the playback entry points — ``turn_on``, ``play_media`` and ``select_source`` — ensure a ``switch.*`` receiver is ON, and only when it is actually off. The four states the contract names are all pinned here: * switch off + playback starts → exactly one ``switch.turn_on``; * switch already on → NO call (idempotent, not a toggle); * switch unavailable/unknown/missing/failing → playback continues, nothing raises; * a ``media_player.*`` receiver → never switched; its behaviour is unchanged. No real Shield, no real switch and no browser take part: the entity comes from the same stub harness every other entity test uses, the receiver's state is a value in ``FakeStates`` and the service call is recorded. """ import entity_stublib import pytest from nsh.const import CONF_APP_NAME_MAP, CONF_RECEIVER_ENTITY from test_command_routing import make_player TIVIMATE = "ar.tvplayer.tv" SOUNDBAR = "switch.soundbar" AVR = "media_player.avr" @pytest.fixture(name="modules", autouse=True) def scoped_modules(): """Stub scope + loaded entity modules for one test (t7 discipline).""" snapshot = entity_stublib.snapshot_modules() entity_stublib.install() try: yield entity_stublib.load_entity_modules() finally: entity_stublib.restore_modules(snapshot) def _player(modules, *, receiver=SOUNDBAR, state=None, app_map=None): """A player with a Receiver configured (a switch unless told otherwise).""" player, served, _intents = make_player(modules) player._entry.options[CONF_RECEIVER_ENTITY] = receiver if app_map is not None: player._entry.options[CONF_APP_NAME_MAP] = app_map if state is not None: player.hass.states.set(receiver, state) return player, served class TestTheSwitchReceiverIsEnsuredOn: """Every playback entry point turns an off switch receiver on — once.""" async def test_play_media_turns_an_off_receiver_on(self, modules): player, served = _player(modules, state="off") await player.async_play_media("app", TIVIMATE) # the ensure is the ONLY service call: no volume, no toggle, and the # app still launches (which is why the volume scripts are unnecessary) assert served == [("switch", "turn_on", {"entity_id": SOUNDBAR})] assert player._manager._protocol.launch_calls == [TIVIMATE] async def test_turn_on_turns_an_off_receiver_on(self, modules): player, served = _player(modules, state="off") await player.async_turn_on() assert served == [("switch", "turn_on", {"entity_id": SOUNDBAR})] async def test_select_source_turns_an_off_receiver_on_once(self, modules): """select_source dispatches internally, so the switch is read once.""" player, served = _player( modules, state="off", app_map={TIVIMATE: "TiviMate"} ) await player.async_select_source("TiviMate") assert served == [("switch", "turn_on", {"entity_id": SOUNDBAR})] assert player._manager._protocol.launch_calls == [TIVIMATE] async def test_an_on_receiver_is_never_called(self, modules): """Idempotent: every entry point may run, and NOT ONE call goes out.""" player, served = _player(modules, state="on") await player.async_turn_on() await player.async_play_media("app", TIVIMATE) await player.async_select_source("TiviMate") assert served == [] async def test_no_volume_service_is_ever_aimed_at_the_receiver(self, modules): """The user's point: a switch receiver means no volume scripts.""" player, served = _player(modules, state="off") await player.async_play_media("app", TIVIMATE) assert [(domain, service) for domain, service, _data in served] == [ ("switch", "turn_on") ] class TestAnUnusableReceiverNeverBreaksPlayback: """A receiver we cannot read, or that fails, is logged — never raised.""" async def test_an_unavailable_switch_is_left_alone(self, modules): player, served = _player(modules, state="unavailable") await player.async_play_media("app", TIVIMATE) assert served == [] # never guessed at assert player._manager._protocol.launch_calls == [TIVIMATE] # still plays async def test_an_unknown_state_is_not_guessed_at(self, modules): player, served = _player(modules, state="unknown") await player.async_turn_on() assert served == [] async def test_a_missing_entity_is_left_alone(self, modules): player, served = _player(modules) # the receiver is not in the machine await player.async_play_media("app", TIVIMATE) assert served == [] assert player._manager._protocol.launch_calls == [TIVIMATE] async def test_a_failing_service_call_does_not_stop_playback(self, modules): from homeassistant.exceptions import HomeAssistantError player, _served = _player(modules, state="off") class _BrokenServices: async def async_call(self, domain, service, data=None, **kwargs): raise HomeAssistantError("the switch entity is gone") player.hass.services = _BrokenServices() await player.async_play_media("app", TIVIMATE) # must not raise assert player._manager._protocol.launch_calls == [TIVIMATE] class TestAMediaPlayerReceiverIsUntouched: """An AVR stays an AVR: input selection and enrichment, never a switch call.""" async def test_a_media_player_receiver_is_never_switched(self, modules): player, served = _player(modules, receiver=AVR, state="off") player.hass.states.set(AVR, "idle") await player.async_turn_on() await player.async_play_media("app", TIVIMATE) assert served == [] assert player._manager._protocol.launch_calls == [TIVIMATE] async def test_the_receiver_option_still_reaches_the_attributes(self, modules): from nsh.const import ATTR_MEDIA_DAG_RECEIVER_ENTITY player, _served = _player(modules, receiver=AVR) assert ( player.extra_state_attributes[ATTR_MEDIA_DAG_RECEIVER_ENTITY] == AVR ) class TestAnIdleShieldLeavesTheReceiverAlone: """Switching the receiver on is a PLAYBACK action, not a synchronisation.""" async def test_reading_state_and_attributes_calls_nothing(self, modules): player, served = _player(modules, state="off") assert player.state is not None # a state read assert isinstance(player.extra_state_attributes, dict) # what the frontend reads player._on_manager_update() # the connection manager pushed an update assert served == []