"""Test the SoundTouch component.""" from datetime import timedelta from typing import Any from requests_mock import Mocker from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MEDIA_PLAYER_DOMAIN, ) from homeassistant.components.soundtouch.const import ( DOMAIN, SERVICE_ADD_ZONE_SLAVE, SERVICE_CREATE_ZONE, SERVICE_PLAY_EVERYWHERE, SERVICE_REMOVE_ZONE_SLAVE, ) from homeassistant.components.soundtouch.media_player import ( ATTR_SOUNDTOUCH_GROUP, ATTR_SOUNDTOUCH_ZONE, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt from .conftest import DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID from tests.common import MockConfigEntry, async_fire_time_changed async def setup_soundtouch(hass: HomeAssistant, *mock_entries: MockConfigEntry): """Initialize media_player for tests.""" assert await async_setup_component(hass, MEDIA_PLAYER_DOMAIN, {}) for mock_entry in mock_entries: mock_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() async def _test_key_service( hass: HomeAssistant, requests_mock_key, service: str, service_data: dict[str, Any], key_name: str, ): """Test API calls that use the /key endpoint to emulate physical button clicks.""" requests_mock_key.reset() await hass.services.async_call("media_player", service, service_data, True) assert requests_mock_key.call_count == 2 assert f">{key_name}" in requests_mock_key.last_request.text async def test_playing_media( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test playing media info.""" await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.state == STATE_PLAYING assert entity_state.attributes[ATTR_MEDIA_TITLE] == "MockArtist - MockTrack" assert entity_state.attributes[ATTR_MEDIA_TRACK] == "MockTrack" assert entity_state.attributes[ATTR_MEDIA_ARTIST] == "MockArtist" assert entity_state.attributes[ATTR_MEDIA_ALBUM_NAME] == "MockAlbum" assert entity_state.attributes[ATTR_MEDIA_DURATION] == 42 async def test_playing_radio( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_radio, ): """Test playing radio info.""" await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.state == STATE_PLAYING assert entity_state.attributes[ATTR_MEDIA_TITLE] == "MockStation" async def test_playing_aux( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_aux, ): """Test playing AUX info.""" await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.state == STATE_PLAYING assert entity_state.attributes[ATTR_INPUT_SOURCE] == "AUX" async def test_playing_bluetooth( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_bluetooth, ): """Test playing Bluetooth info.""" await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.state == STATE_PLAYING assert entity_state.attributes[ATTR_INPUT_SOURCE] == "BLUETOOTH" assert entity_state.attributes[ATTR_MEDIA_TRACK] == "MockTrack" assert entity_state.attributes[ATTR_MEDIA_ARTIST] == "MockArtist" assert entity_state.attributes[ATTR_MEDIA_ALBUM_NAME] == "MockAlbum" async def test_get_volume_level( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test volume level.""" await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.attributes["volume_level"] == 0.12 async def test_get_state_off( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_standby, ): """Test state device is off.""" await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.state == STATE_OFF async def test_get_state_pause( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, ): """Test state device is paused.""" await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.state == STATE_PAUSED async def test_is_muted( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_volume_muted: str, ): """Test device volume is muted.""" with Mocker(real_http=True) as mocker: mocker.get("/volume", text=device1_volume_muted) await setup_soundtouch(hass, device1_config) entity_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_state.attributes[ATTR_MEDIA_VOLUME_MUTED] async def test_should_turn_off( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): """Test device is turned off.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "turn_off", {"entity_id": DEVICE_1_ENTITY_ID}, "POWER", ) async def test_should_turn_on( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_key, ): """Test device is turned on.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "turn_on", {"entity_id": DEVICE_1_ENTITY_ID}, "POWER", ) async def test_volume_up( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): """Test volume up.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "volume_up", {"entity_id": DEVICE_1_ENTITY_ID}, "VOLUME_UP", ) async def test_volume_down( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): """Test volume down.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "volume_down", {"entity_id": DEVICE_1_ENTITY_ID}, "VOLUME_DOWN", ) async def test_set_volume_level( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_volume, ): """Test set volume level.""" await setup_soundtouch(hass, device1_config) assert device1_requests_mock_volume.call_count == 0 await hass.services.async_call( "media_player", "volume_set", {"entity_id": DEVICE_1_ENTITY_ID, "volume_level": 0.17}, True, ) assert device1_requests_mock_volume.call_count == 1 assert "17" in device1_requests_mock_volume.last_request.text async def test_mute( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): """Test mute volume.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "volume_mute", {"entity_id": DEVICE_1_ENTITY_ID, "is_volume_muted": True}, "MUTE", ) async def test_play( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, device1_requests_mock_key, ): """Test play command.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "media_play", {"entity_id": DEVICE_1_ENTITY_ID}, "PLAY", ) async def test_pause( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): """Test pause command.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "media_pause", {"entity_id": DEVICE_1_ENTITY_ID}, "PAUSE", ) async def test_play_pause( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): """Test play/pause.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "media_play_pause", {"entity_id": DEVICE_1_ENTITY_ID}, "PLAY_PAUSE", ) async def test_next_previous_track( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): """Test next/previous track.""" await setup_soundtouch(hass, device1_config) await _test_key_service( hass, device1_requests_mock_key, "media_next_track", {"entity_id": DEVICE_1_ENTITY_ID}, "NEXT_TRACK", ) await _test_key_service( hass, device1_requests_mock_key, "media_previous_track", {"entity_id": DEVICE_1_ENTITY_ID}, "PREV_TRACK", ) async def test_play_media( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): """Test play preset 1.""" await setup_soundtouch(hass, device1_config) assert device1_requests_mock_select.call_count == 0 await hass.services.async_call( "media_player", "play_media", { "entity_id": DEVICE_1_ENTITY_ID, ATTR_MEDIA_CONTENT_TYPE: "PLAYLIST", ATTR_MEDIA_CONTENT_ID: 1, }, True, ) assert device1_requests_mock_select.call_count == 1 assert ( 'location="http://homeassistant:8123/media/local/test.mp3"' in device1_requests_mock_select.last_request.text ) await hass.services.async_call( "media_player", "play_media", { "entity_id": DEVICE_1_ENTITY_ID, ATTR_MEDIA_CONTENT_TYPE: "PLAYLIST", ATTR_MEDIA_CONTENT_ID: 2, }, True, ) assert device1_requests_mock_select.call_count == 2 assert "MockStation" in device1_requests_mock_select.last_request.text async def test_play_media_url( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_dlna, ): """Test play preset 1.""" await setup_soundtouch(hass, device1_config) assert device1_requests_mock_dlna.call_count == 0 await hass.services.async_call( "media_player", "play_media", { "entity_id": DEVICE_1_ENTITY_ID, ATTR_MEDIA_CONTENT_TYPE: "MUSIC", ATTR_MEDIA_CONTENT_ID: "http://fqdn/file.mp3", }, True, ) assert device1_requests_mock_dlna.call_count == 1 assert "http://fqdn/file.mp3" in device1_requests_mock_dlna.last_request.text async def test_select_source_aux( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): """Test select AUX.""" await setup_soundtouch(hass, device1_config) assert device1_requests_mock_select.call_count == 0 await hass.services.async_call( "media_player", "select_source", {"entity_id": DEVICE_1_ENTITY_ID, ATTR_INPUT_SOURCE: "AUX"}, True, ) assert device1_requests_mock_select.call_count == 1 assert "AUX" in device1_requests_mock_select.last_request.text async def test_select_source_bluetooth( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): """Test select Bluetooth.""" await setup_soundtouch(hass, device1_config) assert device1_requests_mock_select.call_count == 0 await hass.services.async_call( "media_player", "select_source", {"entity_id": DEVICE_1_ENTITY_ID, ATTR_INPUT_SOURCE: "BLUETOOTH"}, True, ) assert device1_requests_mock_select.call_count == 1 assert "BLUETOOTH" in device1_requests_mock_select.last_request.text async def test_select_source_invalid_source( hass: HomeAssistant, device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): """Test select unsupported source.""" await setup_soundtouch(hass, device1_config) assert not device1_requests_mock_select.called await hass.services.async_call( "media_player", "select_source", { "entity_id": DEVICE_1_ENTITY_ID, ATTR_INPUT_SOURCE: "SOMETHING_UNSUPPORTED", }, True, ) assert not device1_requests_mock_select.called async def test_play_everywhere( hass: HomeAssistant, device1_config: MockConfigEntry, device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, ): """Test play everywhere.""" await setup_soundtouch(hass, device1_config) # no slaves, set zone must not be called await hass.services.async_call( DOMAIN, SERVICE_PLAY_EVERYWHERE, {"master": DEVICE_1_ENTITY_ID}, True, ) assert device1_requests_mock_set_zone.call_count == 0 await setup_soundtouch(hass, device2_config) # one master, one slave => set zone await hass.services.async_call( DOMAIN, SERVICE_PLAY_EVERYWHERE, {"master": DEVICE_1_ENTITY_ID}, True, ) assert device1_requests_mock_set_zone.call_count == 1 # unknown master, set zone must not be called await hass.services.async_call( DOMAIN, SERVICE_PLAY_EVERYWHERE, {"master": "media_player.entity_X"}, True, ) assert device1_requests_mock_set_zone.call_count == 1 async def test_create_zone( hass: HomeAssistant, device1_config: MockConfigEntry, device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, ): """Test creating a zone.""" await setup_soundtouch(hass, device1_config, device2_config) assert device1_requests_mock_set_zone.call_count == 0 # one master, one slave => set zone await hass.services.async_call( DOMAIN, SERVICE_CREATE_ZONE, { "master": DEVICE_1_ENTITY_ID, "slaves": [DEVICE_2_ENTITY_ID], }, True, ) assert device1_requests_mock_set_zone.call_count == 1 # unknown master, set zone must not be called await hass.services.async_call( DOMAIN, SERVICE_CREATE_ZONE, {"master": "media_player.entity_X", "slaves": [DEVICE_2_ENTITY_ID]}, True, ) assert device1_requests_mock_set_zone.call_count == 1 # no slaves, set zone must not be called await hass.services.async_call( DOMAIN, SERVICE_CREATE_ZONE, {"master": DEVICE_1_ENTITY_ID, "slaves": []}, True, ) assert device1_requests_mock_set_zone.call_count == 1 async def test_remove_zone_slave( hass: HomeAssistant, device1_config: MockConfigEntry, device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_remove_zone_slave, ): """Test removing a slave from an existing zone.""" await setup_soundtouch(hass, device1_config, device2_config) # remove one slave await hass.services.async_call( DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, { "master": DEVICE_1_ENTITY_ID, "slaves": [DEVICE_2_ENTITY_ID], }, True, ) assert device1_requests_mock_remove_zone_slave.call_count == 1 # unknown master, remove zone slave is not called await hass.services.async_call( DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, {"master": "media_player.entity_X", "slaves": [DEVICE_2_ENTITY_ID]}, True, ) assert device1_requests_mock_remove_zone_slave.call_count == 1 # no slave to remove, remove zone slave is not called await hass.services.async_call( DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, {"master": DEVICE_1_ENTITY_ID, "slaves": []}, True, ) assert device1_requests_mock_remove_zone_slave.call_count == 1 async def test_add_zone_slave( hass: HomeAssistant, device1_config: MockConfigEntry, device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_add_zone_slave, ): """Test adding a slave to a zone.""" await setup_soundtouch(hass, device1_config, device2_config) # add one slave await hass.services.async_call( DOMAIN, SERVICE_ADD_ZONE_SLAVE, { "master": DEVICE_1_ENTITY_ID, "slaves": [DEVICE_2_ENTITY_ID], }, True, ) assert device1_requests_mock_add_zone_slave.call_count == 1 # unknown master, add zone slave is not called await hass.services.async_call( DOMAIN, SERVICE_ADD_ZONE_SLAVE, {"master": "media_player.entity_X", "slaves": [DEVICE_2_ENTITY_ID]}, True, ) assert device1_requests_mock_add_zone_slave.call_count == 1 # no slave to add, add zone slave is not called await hass.services.async_call( DOMAIN, SERVICE_ADD_ZONE_SLAVE, {"master": DEVICE_1_ENTITY_ID, "slaves": ["media_player.entity_X"]}, True, ) assert device1_requests_mock_add_zone_slave.call_count == 1 async def test_zone_attributes( hass: HomeAssistant, device1_config: MockConfigEntry, device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, ): """Test zone attributes.""" await setup_soundtouch(hass, device1_config, device2_config) # Fast-forward time to allow all entities to be set up and updated again async_fire_time_changed( hass, dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) await hass.async_block_till_done() entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] assert ( entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["master"] == DEVICE_1_ENTITY_ID ) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["slaves"] == [ DEVICE_2_ENTITY_ID ] assert entity_1_state.attributes[ATTR_SOUNDTOUCH_GROUP] == [ DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID, ] entity_2_state = hass.states.get(DEVICE_2_ENTITY_ID) assert not entity_2_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] assert ( entity_2_state.attributes[ATTR_SOUNDTOUCH_ZONE]["master"] == DEVICE_1_ENTITY_ID ) assert entity_2_state.attributes[ATTR_SOUNDTOUCH_ZONE]["slaves"] == [ DEVICE_2_ENTITY_ID ] assert entity_2_state.attributes[ATTR_SOUNDTOUCH_GROUP] == [ DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID, ]