Add Sound Mode selection in soundpal components (#106589)
This commit is contained in:
parent
135fe26704
commit
586d27320e
3 changed files with 109 additions and 1 deletions
|
@ -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."""
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue