"""Configuration for Sonos tests.""" from copy import copy from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry class SonosMockService: """Mock a Sonos Service used in callbacks.""" def __init__(self, service_type): """Initialize the instance.""" self.service_type = service_type self.subscribe = AsyncMock() class SonosMockEvent: """Mock a sonos Event used in callbacks.""" def __init__(self, soco, service, variables): """Initialize the instance.""" self.sid = f"{soco.uid}_sub0000000001" self.seq = "0" self.timestamp = 1621000000.0 self.service = service self.variables = variables def increment_variable(self, var_name): """Increment the value of the var_name key in variables dict attribute. Assumes value has a format of :. """ self.variables = copy(self.variables) base, count = self.variables[var_name].split(":") newcount = int(count) + 1 self.variables[var_name] = ":".join([base, str(newcount)]) return self.variables[var_name] @pytest.fixture def zeroconf_payload(): """Return a default zeroconf payload.""" return zeroconf.ZeroconfServiceInfo( host="192.168.4.2", addresses=["192.168.4.2"], hostname="Sonos-aaa", name="Sonos-aaa@Living Room._sonos._tcp.local.", port=None, properties={"bootseq": "1234"}, type="mock_type", ) @pytest.fixture async def async_autosetup_sonos(async_setup_sonos): """Set up a Sonos integration instance on test run.""" await async_setup_sonos() @pytest.fixture def async_setup_sonos(hass, config_entry): """Return a coroutine to set up a Sonos integration instance on demand.""" async def _wrapper(): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return _wrapper @pytest.fixture(name="config_entry") def config_entry_fixture(): """Create a mock Sonos config entry.""" return MockConfigEntry(domain=DOMAIN, title="Sonos") @pytest.fixture(name="soco") def soco_fixture( music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock ): """Create a mock soco SoCo fixture.""" with patch("homeassistant.components.sonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" ): mock_soco = mock.return_value mock_soco.ip_address = "192.168.42.2" mock_soco.uid = "RINCON_test" mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library mock_soco.get_current_track_info.return_value = current_track_info_empty mock_soco.music_source_from_uri = SoCo.music_source_from_uri mock_soco.get_speaker_info.return_value = speaker_info mock_soco.avTransport = SonosMockService("AVTransport") mock_soco.renderingControl = SonosMockService("RenderingControl") mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology") mock_soco.contentDirectory = SonosMockService("ContentDirectory") mock_soco.deviceProperties = SonosMockService("DeviceProperties") mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_level = True mock_soco.loudness = True mock_soco.volume = 19 mock_soco.audio_delay = 2 mock_soco.bass = 1 mock_soco.treble = -1 mock_soco.mic_enabled = False mock_soco.sub_enabled = False mock_soco.sub_gain = 5 mock_soco.surround_enabled = True mock_soco.surround_mode = True mock_soco.surround_level = 3 mock_soco.music_surround_level = 4 mock_soco.soundbar_audio_input_format = "Dolby 5.1" mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = {mock_soco} mock_soco.visible_zones = {mock_soco} mock_soco.group.coordinator = mock_soco yield mock_soco @pytest.fixture(autouse=True) async def silent_ssdp_scanner(hass): """Start SSDP component and get Scanner, prevent actual SSDP traffic.""" with patch( "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "homeassistant.components.ssdp.Scanner.async_scan" ), patch( "homeassistant.components.ssdp.Server._async_start_upnp_servers" ), patch( "homeassistant.components.ssdp.Server._async_stop_upnp_servers" ): yield @pytest.fixture(name="discover", autouse=True) def discover_fixture(soco): """Create a mock soco discover fixture.""" async def do_callback(hass, callback, *args, **kwargs): await callback( ssdp.SsdpServiceInfo( ssdp_location=f"http://{soco.ip_address}/", ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1", ssdp_usn=f"uuid:{soco.uid}_MR::urn:schemas-upnp-org:service:GroupRenderingControl:1", upnp={ ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", }, ), ssdp.SsdpChange.ALIVE, ) return MagicMock() with patch( "homeassistant.components.ssdp.async_register_callback", side_effect=do_callback ) as mock: yield mock @pytest.fixture(name="config") def config_fixture(): """Create hass config fixture.""" return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.1"]}}} @pytest.fixture(name="music_library") def music_library_fixture(): """Create music_library fixture.""" music_library = MagicMock() music_library.get_sonos_favorites.return_value.update_id = 1 return music_library @pytest.fixture(name="alarm_clock") def alarm_clock_fixture(): """Create alarmClock fixture.""" alarm_clock = SonosMockService("AlarmClock") alarm_clock.ListAlarms = Mock() alarm_clock.ListAlarms.return_value = { "CurrentAlarmListVersion": "RINCON_test:14", "CurrentAlarmList": "" '' "", } return alarm_clock @pytest.fixture(name="alarm_clock_extended") def alarm_clock_fixture_extended(): """Create alarmClock fixture.""" alarm_clock = SonosMockService("AlarmClock") alarm_clock.ListAlarms = Mock() alarm_clock.ListAlarms.return_value = { "CurrentAlarmListVersion": "RINCON_test:15", "CurrentAlarmList": "" '' '' "", } return alarm_clock @pytest.fixture(name="speaker_info") def speaker_info_fixture(): """Create speaker_info fixture.""" return { "zone_name": "Zone A", "uid": "RINCON_test", "model_name": "Model Name", "model_number": "S12", "hardware_version": "1.20.1.6-1.1", "software_version": "49.2-64250", "mac_address": "00-11-22-33-44-55", "display_version": "13.1", } @pytest.fixture(name="current_track_info_empty") def current_track_info_empty_fixture(): """Create current_track_info_empty fixture.""" return { "title": "", "artist": "", "album": "", "album_art": "", "position": "NOT_IMPLEMENTED", "playlist_position": "1", "duration": "NOT_IMPLEMENTED", "uri": "", "metadata": "NOT_IMPLEMENTED", } @pytest.fixture(name="battery_info") def battery_info_fixture(): """Create battery_info fixture.""" return { "Health": "GREEN", "Level": 100, "Temperature": "NORMAL", "PowerSource": "SONOS_CHARGING_RING", } @pytest.fixture(name="device_properties_event") def device_properties_event_fixture(soco): """Create device_properties_event fixture.""" variables = { "zone_name": "Zone A", "mic_enabled": "1", "more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25", } return SonosMockEvent(soco, soco.deviceProperties, variables) @pytest.fixture(name="alarm_event") def alarm_event_fixture(soco): """Create alarm_event fixture.""" variables = { "time_zone": "ffc40a000503000003000502ffc4", "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", "time_generation": "20000001", "alarm_list_version": "RINCON_test:1", "time_format": "INV", "date_format": "INV", "daily_index_refresh_time": None, } return SonosMockEvent(soco, soco.alarmClock, variables) @pytest.fixture(name="no_media_event") def no_media_event_fixture(soco): """Create no_media_event_fixture.""" variables = { "current_crossfade_mode": "0", "current_play_mode": "NORMAL", "current_section": "0", "current_track_meta_data": "", "current_track_uri": "", "enqueued_transport_uri": "", "enqueued_transport_uri_meta_data": "", "number_of_tracks": "0", "transport_state": "STOPPED", } return SonosMockEvent(soco, soco.avTransport, variables) @pytest.fixture(name="tv_event") def tv_event_fixture(soco): """Create alarm_event fixture.""" variables = { "transport_state": "PLAYING", "current_play_mode": "NORMAL", "current_crossfade_mode": "0", "number_of_tracks": "1", "current_track": "1", "current_section": "0", "current_track_uri": f"x-sonos-htastream:{soco.uid}:spdif", "current_track_duration": "", "current_track_meta_data": { "title": " ", "parent_id": "-1", "item_id": "-1", "restricted": True, "resources": [], "desc": None, }, "next_track_uri": "", "next_track_meta_data": "", "enqueued_transport_uri": "", "enqueued_transport_uri_meta_data": "", "playback_storage_medium": "NETWORK", "av_transport_uri": f"x-sonos-htastream:{soco.uid}:spdif", "av_transport_uri_meta_data": { "title": soco.uid, "parent_id": "0", "item_id": "spdif-input", "restricted": False, "resources": [], "desc": None, }, "current_transport_actions": "Set, Play", "current_valid_play_modes": "", } return SonosMockEvent(soco, soco.avTransport, variables) @pytest.fixture(autouse=True) def mock_get_source_ip(mock_get_source_ip): """Mock network util's async_get_source_ip in all sonos tests.""" return mock_get_source_ip