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, ContentChange,
Device, Device,
PowerChange, PowerChange,
SettingChange,
SongpalException, SongpalException,
VolumeChange, VolumeChange,
) )
from songpal.containers import Setting
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -99,6 +101,7 @@ class SongpalEntity(MediaPlayerEntity):
| MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
| MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_OFF
) )
@ -124,6 +127,8 @@ class SongpalEntity(MediaPlayerEntity):
self._active_source = None self._active_source = None
self._sources = {} self._sources = {}
self._active_sound_mode = None
self._sound_modes = {}
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when entity is added to hass.""" """Run when entity is added to hass."""
@ -133,6 +138,28 @@ class SongpalEntity(MediaPlayerEntity):
"""Run when entity will be removed from hass.""" """Run when entity will be removed from hass."""
await self._dev.stop_listen_notifications() 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): async def async_activate_websocket(self):
"""Activate websocket for listening if wanted.""" """Activate websocket for listening if wanted."""
_LOGGER.info("Activating websocket connection") _LOGGER.info("Activating websocket connection")
@ -152,6 +179,16 @@ class SongpalEntity(MediaPlayerEntity):
else: else:
_LOGGER.debug("Got non-handled content change: %s", content) _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): async def _power_changed(power: PowerChange):
_LOGGER.debug("Power changed: %s", power) _LOGGER.debug("Power changed: %s", power)
self._state = power.status self._state = power.status
@ -192,6 +229,7 @@ class SongpalEntity(MediaPlayerEntity):
self._dev.on_notification(VolumeChange, _volume_changed) self._dev.on_notification(VolumeChange, _volume_changed)
self._dev.on_notification(ContentChange, _source_changed) self._dev.on_notification(ContentChange, _source_changed)
self._dev.on_notification(PowerChange, _power_changed) self._dev.on_notification(PowerChange, _power_changed)
self._dev.on_notification(SettingChange, _setting_changed)
self._dev.on_notification(ConnectChange, _try_reconnect) self._dev.on_notification(ConnectChange, _try_reconnect)
async def handle_stop(event): async def handle_stop(event):
@ -271,6 +309,11 @@ class SongpalEntity(MediaPlayerEntity):
_LOGGER.debug("Active source: %s", self._active_source) _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 self._attr_available = True
except SongpalException as ex: except SongpalException as ex:
@ -291,6 +334,27 @@ class SongpalEntity(MediaPlayerEntity):
"""Return list of available sources.""" """Return list of available sources."""
return [src.title for src in self._sources.values()] 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 @property
def state(self) -> MediaPlayerState: def state(self) -> MediaPlayerState:
"""Return current state.""" """Return current state."""
@ -304,6 +368,12 @@ class SongpalEntity(MediaPlayerEntity):
# Avoid a KeyError when _active_source is not (yet) populated # Avoid a KeyError when _active_source is not (yet) populated
return getattr(self._active_source, "title", None) 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 @property
def volume_level(self): def volume_level(self):
"""Return volume level.""" """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 input2.active = True
type(mocked_device).get_inputs = AsyncMock(return_value=[input1, input2]) 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_power = AsyncMock()
type(mocked_device).set_sound_settings = AsyncMock() type(mocked_device).set_sound_settings = AsyncMock()
type(mocked_device).listen_notifications = AsyncMock() type(mocked_device).listen_notifications = AsyncMock()

View file

@ -12,6 +12,7 @@ from songpal import (
SongpalException, SongpalException,
VolumeChange, VolumeChange,
) )
from songpal.notification import SettingChange
from homeassistant.components import media_player, songpal from homeassistant.components import media_player, songpal
from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature
@ -47,6 +48,7 @@ SUPPORT_SONGPAL = (
| MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
| MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_OFF
) )
@ -138,6 +140,8 @@ async def test_state(hass: HomeAssistant) -> None:
assert attributes["is_volume_muted"] is False assert attributes["is_volume_muted"] is False
assert attributes["source_list"] == ["title1", "title2"] assert attributes["source_list"] == ["title1", "title2"]
assert attributes["source"] == "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 assert attributes["supported_features"] == SUPPORT_SONGPAL
device_registry = dr.async_get(hass) 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["is_volume_muted"] is False
assert attributes["source_list"] == ["title1", "title2"] assert attributes["source_list"] == ["title1", "title2"]
assert attributes["source"] == "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 assert attributes["supported_features"] == SUPPORT_SONGPAL
device_registry = dr.async_get(hass) 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["is_volume_muted"] is False
assert attributes["source_list"] == ["title1", "title2"] assert attributes["source_list"] == ["title1", "title2"]
assert attributes["source"] == "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 assert attributes["supported_features"] == SUPPORT_SONGPAL
device_registry = dr.async_get(hass) 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_device2.set_sound_settings.assert_called_once_with("name", "value")
mocked_device3.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: async def test_websocket_events(hass: HomeAssistant) -> None:
"""Test websocket events.""" """Test websocket events."""
@ -315,7 +326,7 @@ async def test_websocket_events(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
mocked_device.listen_notifications.assert_called_once() 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 notification_callbacks = mocked_device.notification_callbacks
@ -336,6 +347,15 @@ async def test_websocket_events(hass: HomeAssistant) -> None:
await notification_callbacks[ContentChange](content_change) await notification_callbacks[ContentChange](content_change)
assert _get_attributes(hass)["source"] == "title1" 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 = MagicMock()
power_change.status = False power_change.status = False
await notification_callbacks[PowerChange](power_change) await notification_callbacks[PowerChange](power_change)