hass-core/tests/components/songpal/test_media_player.py
BestPig 7c5fcec062
Fix songpal crash for soundbars without sound modes (#119999)
Getting soundField on soundbar that doesn't support it crash raise an exception, so it make the whole components unavailable. As there is no simple way to know if soundField is supported, I just get all sound settings, and then pick soundField one if present. If not present, then return None to make it continue, it will just have to effect to display no sound mode and not able to select one (Exactly what we want).
2024-06-20 13:06:30 +02:00

468 lines
17 KiB
Python

"""Test songpal media_player."""
from datetime import timedelta
import logging
from unittest.mock import AsyncMock, MagicMock, call, patch
import pytest
from songpal import (
ConnectChange,
ContentChange,
PowerChange,
SongpalException,
VolumeChange,
)
from songpal.notification import SettingChange
from homeassistant.components import media_player, songpal
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.songpal.const import (
ERROR_REQUEST_RETRY,
SET_SOUND_SETTING,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from . import (
CONF_DATA,
CONF_ENDPOINT,
CONF_NAME,
ENDPOINT,
ENTITY_ID,
FRIENDLY_NAME,
MAC,
MODEL,
SW_VERSION,
WIRELESS_MAC,
_create_mocked_device,
_patch_media_player_device,
)
from tests.common import MockConfigEntry, async_fire_time_changed
SUPPORT_SONGPAL = (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
| MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
)
def _get_attributes(hass):
state = hass.states.get(ENTITY_ID)
return state.as_dict()["attributes"]
async def _call(hass, service, **argv):
await hass.services.async_call(
media_player.DOMAIN,
service,
{"entity_id": ENTITY_ID, **argv},
blocking=True,
)
async def test_setup_platform(hass: HomeAssistant) -> None:
"""Test the legacy setup platform."""
mocked_device = _create_mocked_device(throw_exception=True)
with _patch_media_player_device(mocked_device):
await async_setup_component(
hass,
media_player.DOMAIN,
{
media_player.DOMAIN: [
{
"platform": songpal.DOMAIN,
CONF_NAME: FRIENDLY_NAME,
CONF_ENDPOINT: ENDPOINT,
}
],
},
)
await hass.async_block_till_done()
# No device is set up
mocked_device.assert_not_called()
all_states = hass.states.async_all()
assert len(all_states) == 0
async def test_setup_failed(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test failed to set up the entity."""
mocked_device = _create_mocked_device(throw_exception=True)
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
all_states = hass.states.async_all()
assert len(all_states) == 0
assert "[name(http://0.0.0.0:10000/sony)] Unable to connect" in caplog.text
assert "Platform songpal not ready yet: Unable to do POST request" in caplog.text
assert not any(x.levelno == logging.ERROR for x in caplog.records)
caplog.clear()
utcnow = dt_util.utcnow()
type(mocked_device).get_supported_methods = AsyncMock()
with _patch_media_player_device(mocked_device):
async_fire_time_changed(hass, utcnow + timedelta(seconds=30))
await hass.async_block_till_done()
all_states = hass.states.async_all()
assert len(all_states) == 1
assert not any(x.levelno == logging.WARNING for x in caplog.records)
assert not any(x.levelno == logging.ERROR for x in caplog.records)
async def test_state(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test state of the entity."""
mocked_device = _create_mocked_device()
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state.name == FRIENDLY_NAME
assert state.state == STATE_ON
attributes = state.as_dict()["attributes"]
assert attributes["volume_level"] == 0.5
assert attributes["is_volume_muted"] is False
assert attributes["source_list"] == ["title1", "title2"]
assert attributes["source"] == "title2"
assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"]
assert attributes["sound_mode"] == "Sound Mode 2"
assert attributes["supported_features"] == SUPPORT_SONGPAL
device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)})
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)}
assert device.manufacturer == "Sony Corporation"
assert device.name == FRIENDLY_NAME
assert device.sw_version == SW_VERSION
assert device.model == MODEL
entity = entity_registry.async_get(ENTITY_ID)
assert entity.unique_id == MAC
async def test_state_nosoundmode(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test state of the entity with no soundField in sound settings."""
mocked_device = _create_mocked_device(no_soundfield=True)
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state.name == FRIENDLY_NAME
assert state.state == STATE_ON
attributes = state.as_dict()["attributes"]
assert attributes["volume_level"] == 0.5
assert attributes["is_volume_muted"] is False
assert attributes["source_list"] == ["title1", "title2"]
assert attributes["source"] == "title2"
assert "sound_mode_list" not in attributes
assert "sound_mode" not in attributes
assert attributes["supported_features"] == SUPPORT_SONGPAL
device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)})
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)}
assert device.manufacturer == "Sony Corporation"
assert device.name == FRIENDLY_NAME
assert device.sw_version == SW_VERSION
assert device.model == MODEL
entity = entity_registry.async_get(ENTITY_ID)
assert entity.unique_id == MAC
async def test_state_wireless(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test state of the entity with only Wireless MAC."""
mocked_device = _create_mocked_device(wired_mac=None, wireless_mac=WIRELESS_MAC)
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state.name == FRIENDLY_NAME
assert state.state == STATE_ON
attributes = state.as_dict()["attributes"]
assert attributes["volume_level"] == 0.5
assert attributes["is_volume_muted"] is False
assert attributes["source_list"] == ["title1", "title2"]
assert attributes["source"] == "title2"
assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"]
assert attributes["sound_mode"] == "Sound Mode 2"
assert attributes["supported_features"] == SUPPORT_SONGPAL
device = device_registry.async_get_device(
identifiers={(songpal.DOMAIN, WIRELESS_MAC)}
)
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, WIRELESS_MAC)}
assert device.manufacturer == "Sony Corporation"
assert device.name == FRIENDLY_NAME
assert device.sw_version == SW_VERSION
assert device.model == MODEL
entity = entity_registry.async_get(ENTITY_ID)
assert entity.unique_id == WIRELESS_MAC
async def test_state_both(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test state of the entity with both Wired and Wireless MAC."""
mocked_device = _create_mocked_device(wired_mac=MAC, wireless_mac=WIRELESS_MAC)
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state.name == FRIENDLY_NAME
assert state.state == STATE_ON
attributes = state.as_dict()["attributes"]
assert attributes["volume_level"] == 0.5
assert attributes["is_volume_muted"] is False
assert attributes["source_list"] == ["title1", "title2"]
assert attributes["source"] == "title2"
assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"]
assert attributes["sound_mode"] == "Sound Mode 2"
assert attributes["supported_features"] == SUPPORT_SONGPAL
device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)})
assert device.connections == {
(dr.CONNECTION_NETWORK_MAC, MAC),
(dr.CONNECTION_NETWORK_MAC, WIRELESS_MAC),
}
assert device.manufacturer == "Sony Corporation"
assert device.name == FRIENDLY_NAME
assert device.sw_version == SW_VERSION
assert device.model == MODEL
entity = entity_registry.async_get(ENTITY_ID)
# We prefer the wired mac if present.
assert entity.unique_id == MAC
async def test_services(hass: HomeAssistant) -> None:
"""Test services."""
mocked_device = _create_mocked_device()
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await _call(hass, media_player.SERVICE_TURN_ON)
await _call(hass, media_player.SERVICE_TURN_OFF)
await _call(hass, media_player.SERVICE_TOGGLE)
assert mocked_device.set_power.call_count == 3
mocked_device.set_power.assert_has_calls([call(True), call(False), call(False)])
await _call(hass, media_player.SERVICE_VOLUME_SET, volume_level=0.6)
await _call(hass, media_player.SERVICE_VOLUME_UP)
await _call(hass, media_player.SERVICE_VOLUME_DOWN)
assert mocked_device.volume1.set_volume.call_count == 3
mocked_device.volume1.set_volume.assert_has_calls([call(60), call(51), call(49)])
await _call(hass, media_player.SERVICE_VOLUME_MUTE, is_volume_muted=True)
mocked_device.volume1.set_mute.assert_called_once_with(True)
await _call(hass, media_player.SERVICE_SELECT_SOURCE, source="none")
mocked_device.input1.activate.assert_not_called()
await _call(hass, media_player.SERVICE_SELECT_SOURCE, source="title1")
mocked_device.input1.activate.assert_called_once()
await hass.services.async_call(
songpal.DOMAIN,
SET_SOUND_SETTING,
{"entity_id": ENTITY_ID, "name": "name", "value": "value"},
blocking=True,
)
mocked_device.set_sound_settings.assert_called_once_with("name", "value")
mocked_device.set_sound_settings.reset_mock()
mocked_device2 = _create_mocked_device(wired_mac="mac2")
entry2 = MockConfigEntry(
domain=songpal.DOMAIN, data={CONF_NAME: "d2", CONF_ENDPOINT: ENDPOINT}
)
entry2.add_to_hass(hass)
with _patch_media_player_device(mocked_device2):
await hass.config_entries.async_setup(entry2.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
songpal.DOMAIN,
SET_SOUND_SETTING,
{"entity_id": "all", "name": "name", "value": "value"},
blocking=True,
)
mocked_device.set_sound_settings.assert_called_once_with("name", "value")
mocked_device2.set_sound_settings.assert_called_once_with("name", "value")
mocked_device.set_sound_settings.reset_mock()
mocked_device2.set_sound_settings.reset_mock()
mocked_device3 = _create_mocked_device(wired_mac=None, wireless_mac=WIRELESS_MAC)
entry3 = MockConfigEntry(
domain=songpal.DOMAIN, data={CONF_NAME: "d2", CONF_ENDPOINT: ENDPOINT}
)
entry3.add_to_hass(hass)
with _patch_media_player_device(mocked_device3):
await hass.config_entries.async_setup(entry3.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
songpal.DOMAIN,
SET_SOUND_SETTING,
{"entity_id": "all", "name": "name", "value": "value"},
blocking=True,
)
mocked_device.set_sound_settings.assert_called_once_with("name", "value")
mocked_device2.set_sound_settings.assert_called_once_with("name", "value")
mocked_device3.set_sound_settings.assert_called_once_with("name", "value")
await _call(hass, media_player.SERVICE_SELECT_SOUND_MODE, sound_mode="Sound Mode 1")
mocked_device.set_sound_settings.assert_called_with("soundField", "sound_mode1")
async def test_websocket_events(hass: HomeAssistant) -> None:
"""Test websocket events."""
mocked_device = _create_mocked_device()
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
mocked_device.listen_notifications.assert_called_once()
assert mocked_device.on_notification.call_count == 5
notification_callbacks = mocked_device.notification_callbacks
volume_change = MagicMock()
volume_change.mute = True
volume_change.volume = 20
await notification_callbacks[VolumeChange](volume_change)
attributes = _get_attributes(hass)
assert attributes["is_volume_muted"] is True
assert attributes["volume_level"] == 0.2
content_change = MagicMock()
content_change.is_input = False
content_change.uri = "uri1"
await notification_callbacks[ContentChange](content_change)
assert _get_attributes(hass)["source"] == "title2"
content_change.is_input = True
await notification_callbacks[ContentChange](content_change)
assert _get_attributes(hass)["source"] == "title1"
sound_mode_change = MagicMock()
sound_mode_change.target = "soundField"
sound_mode_change.currentValue = "sound_mode1"
await notification_callbacks[SettingChange](sound_mode_change)
assert _get_attributes(hass)["sound_mode"] == "Sound Mode 1"
sound_mode_change.currentValue = "sound_mode2"
await notification_callbacks[SettingChange](sound_mode_change)
assert _get_attributes(hass)["sound_mode"] == "Sound Mode 2"
power_change = MagicMock()
power_change.status = False
await notification_callbacks[PowerChange](power_change)
assert hass.states.get(ENTITY_ID).state == STATE_OFF
async def test_disconnected(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test disconnected behavior."""
mocked_device = _create_mocked_device()
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
async def _assert_state():
state = hass.states.get(ENTITY_ID)
assert state.state == STATE_UNAVAILABLE
connect_change = MagicMock()
connect_change.exception = "disconnected"
type(mocked_device).get_supported_methods = AsyncMock(
side_effect=[SongpalException(""), SongpalException(""), _assert_state]
)
notification_callbacks = mocked_device.notification_callbacks
with patch("homeassistant.components.songpal.media_player.INITIAL_RETRY_DELAY", 0):
await notification_callbacks[ConnectChange](connect_change)
warning_records = [x for x in caplog.records if x.levelno == logging.WARNING]
assert len(warning_records) == 2
assert warning_records[0].message.endswith("Got disconnected, trying to reconnect")
assert warning_records[1].message.endswith("Connection reestablished")
assert not any(x.levelno == logging.ERROR for x in caplog.records)
@pytest.mark.parametrize(
"service", [media_player.SERVICE_TURN_ON, media_player.SERVICE_TURN_OFF]
)
@pytest.mark.parametrize(
("error_code", "swallow"), [(ERROR_REQUEST_RETRY, True), (1234, False)]
)
async def test_error_swallowing(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, service, error_code, swallow
) -> None:
"""Test swallowing specific errors on turn_on and turn_off."""
mocked_device = _create_mocked_device()
entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA)
entry.add_to_hass(hass)
with _patch_media_player_device(mocked_device):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
type(mocked_device).set_power = AsyncMock(
side_effect=[
SongpalException("Error to swallow", error=(error_code, "Error to swallow"))
]
)
if swallow:
await _call(hass, service)
assert "Swallowing" in caplog.text
else:
with pytest.raises(SongpalException):
await _call(hass, service)