Add balance entity for Sonos speakers (#85205)

This commit is contained in:
[pʲɵs] 2023-04-23 01:18:17 +02:00 committed by GitHub
parent 942a955a77
commit 6013584b7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 3 deletions

View file

@ -19,6 +19,7 @@ from .speaker import SonosSpeaker
LEVEL_TYPES = {
"audio_delay": (0, 5),
"bass": (-10, 10),
"balance": (-100, 100),
"treble": (-10, 10),
"sub_gain": (-15, 15),
"surround_level": (-15, 15),
@ -30,6 +31,40 @@ SocoFeatures = list[tuple[str, tuple[int, int]]]
_LOGGER = logging.getLogger(__name__)
def _balance_to_number(state: tuple[int, int]) -> float:
"""Represent a balance measure returned by SoCo as a number.
SoCo returns a pair of volumes, one for the left side and one
for the right side. When the two are equal, sound is centered;
HA will show that as 0. When the left side is louder, HA will
show a negative value, and a positive value means the right
side is louder. Maximum absolute value is 100, which means only
one side produces sound at all.
"""
left, right = state
return (right - left) * 100 // max(right, left)
def _balance_from_number(value: float) -> tuple[int, int]:
"""Convert a balance value from -100 to 100 into SoCo format.
0 becomes (100, 100), fully enabling both sides. Note that
the master volume control is separate, so this does not
turn up the speakers to maximum volume. Negative values
reduce the volume of the right side, and positive values
reduce the volume of the left side. -100 becomes (100, 0),
fully muting the right side, and +100 becomes (0, 100),
muting the left side.
"""
left = min(100, 100 - int(value))
right = min(100, int(value) + 100)
return left, right
LEVEL_TO_NUMBER = {"balance": _balance_to_number}
LEVEL_FROM_NUMBER = {"balance": _balance_from_number}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -92,9 +127,11 @@ class SonosLevelEntity(SonosEntity, NumberEntity):
@soco_error()
def set_native_value(self, value: float) -> None:
"""Set a new value."""
setattr(self.soco, self.level_type, value)
from_number = LEVEL_FROM_NUMBER.get(self.level_type, int)
setattr(self.soco, self.level_type, from_number(value))
@property
def native_value(self) -> float:
"""Return the current value."""
return cast(float, getattr(self.speaker, self.level_type))
to_number = LEVEL_TO_NUMBER.get(self.level_type, int)
return cast(float, to_number(getattr(self.speaker, self.level_type)))

View file

@ -145,6 +145,7 @@ class SonosSpeaker:
self.volume: int | None = None
self.muted: bool | None = None
self.cross_fade: bool | None = None
self.balance: tuple[int, int] | None = None
self.bass: int | None = None
self.treble: int | None = None
self.loudness: bool | None = None
@ -536,7 +537,10 @@ class SonosSpeaker:
variables = event.variables
if "volume" in variables:
self.volume = int(variables["volume"]["Master"])
volume = variables["volume"]
self.volume = int(volume["Master"])
if "LF" in volume and "RF" in volume:
self.balance = (int(volume["LF"]), int(volume["RF"]))
if "mute" in variables:
self.muted = variables["mute"]["Master"] == "1"

View file

@ -112,6 +112,7 @@ def soco_fixture(
mock_soco.loudness = True
mock_soco.volume = 19
mock_soco.audio_delay = 2
mock_soco.balance = (61, 100)
mock_soco.bass = 1
mock_soco.treble = -1
mock_soco.mic_enabled = False

View file

@ -11,6 +11,10 @@ async def test_number_entities(
hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry
) -> None:
"""Test number entities."""
balance_number = entity_registry.entities["number.zone_a_balance"]
balance_state = hass.states.get(balance_number.entity_id)
assert balance_state.state == "39"
bass_number = entity_registry.entities["number.zone_a_bass"]
bass_state = hass.states.get(bass_number.entity_id)
assert bass_state.state == "1"