Add Sound Mode selection in soundpal components (#106589)

This commit is contained in:
BestPig 2024-04-16 17:45:48 +02:00 committed by GitHub
parent 135fe26704
commit 586d27320e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 109 additions and 1 deletions

View file

@ -11,9 +11,11 @@ from songpal import (
ContentChange,
Device,
PowerChange,
SettingChange,
SongpalException,
VolumeChange,
)
from songpal.containers import Setting
import voluptuous as vol
from homeassistant.components.media_player import (
@ -99,6 +101,7 @@ class SongpalEntity(MediaPlayerEntity):
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
| MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
)
@ -124,6 +127,8 @@ class SongpalEntity(MediaPlayerEntity):
self._active_source = None
self._sources = {}
self._active_sound_mode = None
self._sound_modes = {}
async def async_added_to_hass(self) -> None:
"""Run when entity is added to hass."""
@ -133,6 +138,28 @@ class SongpalEntity(MediaPlayerEntity):
"""Run when entity will be removed from hass."""
await self._dev.stop_listen_notifications()
async def _get_sound_modes_info(self):
"""Get available sound modes and the active one."""
settings = await self._dev.get_sound_settings("soundField")
if isinstance(settings, Setting):
settings = [settings]
sound_modes = {}
active_sound_mode = None
for setting in settings:
cur = setting.currentValue
for opt in setting.candidate:
if not opt.isAvailable:
continue
if opt.value == cur:
active_sound_mode = opt.value
sound_modes[opt.value] = opt
_LOGGER.debug("Got sound modes: %s", sound_modes)
_LOGGER.debug("Active sound mode: %s", active_sound_mode)
return active_sound_mode, sound_modes
async def async_activate_websocket(self):
"""Activate websocket for listening if wanted."""
_LOGGER.info("Activating websocket connection")
@ -152,6 +179,16 @@ class SongpalEntity(MediaPlayerEntity):
else:
_LOGGER.debug("Got non-handled content change: %s", content)
async def _setting_changed(setting: SettingChange):
_LOGGER.debug("Setting changed: %s", setting)
if setting.target == "soundField":
self._active_sound_mode = setting.currentValue
_LOGGER.debug("New active sound mode: %s", self._active_sound_mode)
self.async_write_ha_state()
else:
_LOGGER.debug("Got non-handled setting change: %s", setting)
async def _power_changed(power: PowerChange):
_LOGGER.debug("Power changed: %s", power)
self._state = power.status
@ -192,6 +229,7 @@ class SongpalEntity(MediaPlayerEntity):
self._dev.on_notification(VolumeChange, _volume_changed)
self._dev.on_notification(ContentChange, _source_changed)
self._dev.on_notification(PowerChange, _power_changed)
self._dev.on_notification(SettingChange, _setting_changed)
self._dev.on_notification(ConnectChange, _try_reconnect)
async def handle_stop(event):
@ -271,6 +309,11 @@ class SongpalEntity(MediaPlayerEntity):
_LOGGER.debug("Active source: %s", self._active_source)
(
self._active_sound_mode,
self._sound_modes,
) = await self._get_sound_modes_info()
self._attr_available = True
except SongpalException as ex:
@ -291,6 +334,27 @@ class SongpalEntity(MediaPlayerEntity):
"""Return list of available sources."""
return [src.title for src in self._sources.values()]
async def async_select_sound_mode(self, sound_mode: str) -> None:
"""Select sound mode."""
for mode in self._sound_modes.values():
if mode.title == sound_mode:
await self._dev.set_sound_settings("soundField", mode.value)
return
_LOGGER.error("Unable to find sound mode: %s", sound_mode)
@property
def sound_mode_list(self) -> list[str] | None:
"""Return list of available sound modes.
When active mode is None it means that sound mode is unavailable on the sound bar.
Can be due to incompatible sound bar or the sound bar is in a mode that does not
support sound mode changes.
"""
if not self._active_sound_mode:
return None
return [sound_mode.title for sound_mode in self._sound_modes.values()]
@property
def state(self) -> MediaPlayerState:
"""Return current state."""
@ -304,6 +368,12 @@ class SongpalEntity(MediaPlayerEntity):
# Avoid a KeyError when _active_source is not (yet) populated
return getattr(self._active_source, "title", None)
@property
def sound_mode(self) -> str | None:
"""Return currently active sound_mode."""
active_sound_mode = self._sound_modes.get(self._active_sound_mode)
return active_sound_mode.title if active_sound_mode else None
@property
def volume_level(self):
"""Return volume level."""

View file

@ -85,6 +85,24 @@ def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=Non
input2.active = True
type(mocked_device).get_inputs = AsyncMock(return_value=[input1, input2])
sound_mode1 = MagicMock()
sound_mode1.title = "Sound Mode 1"
sound_mode1.value = "sound_mode1"
sound_mode1.isAvailable = True
sound_mode2 = MagicMock()
sound_mode2.title = "Sound Mode 2"
sound_mode2.value = "sound_mode2"
sound_mode2.isAvailable = True
sound_mode3 = MagicMock()
sound_mode3.title = "Sound Mode 3"
sound_mode3.value = "sound_mode3"
sound_mode3.isAvailable = False
soundField = MagicMock()
soundField.currentValue = "sound_mode2"
soundField.candidate = [sound_mode1, sound_mode2, sound_mode3]
type(mocked_device).get_sound_settings = AsyncMock(return_value=[soundField])
type(mocked_device).set_power = AsyncMock()
type(mocked_device).set_sound_settings = AsyncMock()
type(mocked_device).listen_notifications = AsyncMock()

View file

@ -12,6 +12,7 @@ from songpal import (
SongpalException,
VolumeChange,
)
from songpal.notification import SettingChange
from homeassistant.components import media_player, songpal
from homeassistant.components.media_player import MediaPlayerEntityFeature
@ -47,6 +48,7 @@ SUPPORT_SONGPAL = (
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
| MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
)
@ -138,6 +140,8 @@ async def test_state(hass: HomeAssistant) -> None:
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_registry = dr.async_get(hass)
@ -171,6 +175,8 @@ async def test_state_wireless(hass: HomeAssistant) -> None:
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_registry = dr.async_get(hass)
@ -206,6 +212,8 @@ async def test_state_both(hass: HomeAssistant) -> None:
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_registry = dr.async_get(hass)
@ -303,6 +311,9 @@ async def test_services(hass: HomeAssistant) -> None:
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."""
@ -315,7 +326,7 @@ async def test_websocket_events(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
mocked_device.listen_notifications.assert_called_once()
assert mocked_device.on_notification.call_count == 4
assert mocked_device.on_notification.call_count == 5
notification_callbacks = mocked_device.notification_callbacks
@ -336,6 +347,15 @@ async def test_websocket_events(hass: HomeAssistant) -> None:
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)