mirror of https://github.com/home-assistant/core
140 lines
4.8 KiB
Python
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)))
|