core/homeassistant/components/sonos/number.py

140 lines
4.8 KiB
Python

"""Entity representing a Sonos number control."""
from __future__ import annotations
import logging
from typing import cast
from homeassistant.components.number import NumberEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SONOS_CREATE_LEVELS
from .entity import SonosEntity
from .helpers import soco_error
from .speaker import SonosSpeaker
LEVEL_TYPES = {
"audio_delay": (0, 5),
"bass": (-10, 10),
"balance": (-100, 100),
"treble": (-10, 10),
"sub_crossover": (50, 110),
"sub_gain": (-15, 15),
"surround_level": (-15, 15),
"music_surround_level": (-15, 15),
}
type 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,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Sonos number platform from a config entry."""
def available_soco_attributes(speaker: SonosSpeaker) -> SocoFeatures:
features: SocoFeatures = []
for level_type, valid_range in LEVEL_TYPES.items():
if (state := getattr(speaker.soco, level_type, None)) is not None:
setattr(speaker, level_type, state)
features.append((level_type, valid_range))
return features
async def _async_create_entities(speaker: SonosSpeaker) -> None:
entities = []
available_features = await hass.async_add_executor_job(
available_soco_attributes, speaker
)
for level_type, valid_range in available_features:
_LOGGER.debug(
"Creating %s number control on %s", level_type, speaker.zone_name
)
entities.append(SonosLevelEntity(speaker, level_type, valid_range))
async_add_entities(entities)
config_entry.async_on_unload(
async_dispatcher_connect(hass, SONOS_CREATE_LEVELS, _async_create_entities)
)
class SonosLevelEntity(SonosEntity, NumberEntity):
"""Representation of a Sonos level entity."""
_attr_entity_category = EntityCategory.CONFIG
def __init__(
self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int, int]
) -> None:
"""Initialize the level entity."""
super().__init__(speaker)
self._attr_unique_id = f"{self.soco.uid}-{level_type}"
self._attr_translation_key = level_type
self.level_type = level_type
self._attr_native_min_value, self._attr_native_max_value = valid_range
async def _async_fallback_poll(self) -> None:
"""Poll the value if subscriptions are not working."""
await self.hass.async_add_executor_job(self.poll_state)
@soco_error()
def poll_state(self) -> None:
"""Poll the device for the current state."""
state = getattr(self.soco, self.level_type)
setattr(self.speaker, self.level_type, state)
@soco_error()
def set_native_value(self, value: float) -> None:
"""Set a new 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."""
to_number = LEVEL_TO_NUMBER.get(self.level_type, int)
return cast(float, to_number(getattr(self.speaker, self.level_type)))