Expose Sonos features as switch entities (#54502)
Co-authored-by: Tobias Sauerwein <cgtobi@users.noreply.github.com>
This commit is contained in:
parent
21daffe905
commit
084fd2d19f
10 changed files with 184 additions and 82 deletions
|
@ -4,10 +4,10 @@ from __future__ import annotations
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
from soco.exceptions import SoCoException, SoCoUPnPException
|
||||
from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException
|
||||
|
||||
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
||||
from homeassistant.const import ATTR_TIME
|
||||
from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -17,6 +17,7 @@ from .const import (
|
|||
DOMAIN as SONOS_DOMAIN,
|
||||
SONOS_ALARMS_UPDATED,
|
||||
SONOS_CREATE_ALARM,
|
||||
SONOS_CREATE_SWITCHES,
|
||||
)
|
||||
from .entity import SonosEntity
|
||||
from .speaker import SonosSpeaker
|
||||
|
@ -31,11 +32,48 @@ ATTR_SCHEDULED_TODAY = "scheduled_today"
|
|||
ATTR_VOLUME = "volume"
|
||||
ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones"
|
||||
|
||||
ATTR_CROSSFADE = "cross_fade"
|
||||
ATTR_NIGHT_SOUND = "night_mode"
|
||||
ATTR_SPEECH_ENHANCEMENT = "dialog_mode"
|
||||
ATTR_STATUS_LIGHT = "status_light"
|
||||
ATTR_TOUCH_CONTROLS = "buttons_enabled"
|
||||
|
||||
ALL_FEATURES = (
|
||||
ATTR_TOUCH_CONTROLS,
|
||||
ATTR_CROSSFADE,
|
||||
ATTR_NIGHT_SOUND,
|
||||
ATTR_SPEECH_ENHANCEMENT,
|
||||
ATTR_STATUS_LIGHT,
|
||||
)
|
||||
|
||||
COORDINATOR_FEATURES = ATTR_CROSSFADE
|
||||
|
||||
POLL_REQUIRED = (
|
||||
ATTR_TOUCH_CONTROLS,
|
||||
ATTR_STATUS_LIGHT,
|
||||
)
|
||||
|
||||
FRIENDLY_NAMES = {
|
||||
ATTR_CROSSFADE: "Crossfade",
|
||||
ATTR_NIGHT_SOUND: "Night Sound",
|
||||
ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement",
|
||||
ATTR_STATUS_LIGHT: "Status Light",
|
||||
ATTR_TOUCH_CONTROLS: "Touch Controls",
|
||||
}
|
||||
|
||||
FEATURE_ICONS = {
|
||||
ATTR_NIGHT_SOUND: "mdi:chat-sleep",
|
||||
ATTR_SPEECH_ENHANCEMENT: "mdi:ear-hearing",
|
||||
ATTR_CROSSFADE: "mdi:swap-horizontal",
|
||||
ATTR_STATUS_LIGHT: "mdi:led-on",
|
||||
ATTR_TOUCH_CONTROLS: "mdi:gesture-tap",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Sonos from a config entry."""
|
||||
|
||||
async def _async_create_entity(speaker: SonosSpeaker, alarm_ids: list[str]) -> None:
|
||||
async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> None:
|
||||
entities = []
|
||||
created_alarms = (
|
||||
hass.data[DATA_SONOS].alarms[speaker.household_id].created_alarm_ids
|
||||
|
@ -48,9 +86,93 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
entities.append(SonosAlarmEntity(alarm_id, speaker))
|
||||
async_add_entities(entities)
|
||||
|
||||
def available_soco_attributes(speaker: SonosSpeaker) -> list[tuple[str, bool]]:
|
||||
features = []
|
||||
for feature_type in ALL_FEATURES:
|
||||
try:
|
||||
if (state := getattr(speaker.soco, feature_type, None)) is not None:
|
||||
setattr(speaker, feature_type, state)
|
||||
features.append(feature_type)
|
||||
except SoCoSlaveException:
|
||||
features.append(feature_type)
|
||||
return features
|
||||
|
||||
async def _async_create_switches(speaker: SonosSpeaker) -> None:
|
||||
entities = []
|
||||
available_features = await hass.async_add_executor_job(
|
||||
available_soco_attributes, speaker
|
||||
)
|
||||
for feature_type in available_features:
|
||||
_LOGGER.debug(
|
||||
"Creating %s switch on %s",
|
||||
FRIENDLY_NAMES[feature_type],
|
||||
speaker.zone_name,
|
||||
)
|
||||
entities.append(SonosSwitchEntity(feature_type, speaker))
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity)
|
||||
async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_alarms)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SONOS_CREATE_SWITCHES, _async_create_switches)
|
||||
)
|
||||
|
||||
|
||||
class SonosSwitchEntity(SonosEntity, SwitchEntity):
|
||||
"""Representation of a Sonos feature switch."""
|
||||
|
||||
def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(speaker)
|
||||
self.feature_type = feature_type
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(
|
||||
f"sonos_{speaker.zone_name}_{FRIENDLY_NAMES[feature_type]}"
|
||||
)
|
||||
self.needs_coordinator = feature_type in COORDINATOR_FEATURES
|
||||
self._attr_entity_category = ENTITY_CATEGORY_CONFIG
|
||||
self._attr_name = f"{speaker.zone_name} {FRIENDLY_NAMES[feature_type]}"
|
||||
self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}"
|
||||
self._attr_icon = FEATURE_ICONS.get(feature_type)
|
||||
|
||||
if feature_type in POLL_REQUIRED:
|
||||
self._attr_should_poll = True
|
||||
|
||||
async def _async_poll(self) -> None:
|
||||
"""Handle polling for subscription-based switches when subscription fails."""
|
||||
if not self.should_poll:
|
||||
await self.hass.async_add_executor_job(self.update)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Fetch switch state if necessary."""
|
||||
state = getattr(self.soco, self.feature_type)
|
||||
setattr(self.speaker, self.feature_type, state)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
if self.needs_coordinator and not self.speaker.is_coordinator:
|
||||
return getattr(self.speaker.coordinator, self.feature_type)
|
||||
return getattr(self.speaker, self.feature_type)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on."""
|
||||
self.send_command(True)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
self.send_command(False)
|
||||
|
||||
def send_command(self, enable: bool) -> None:
|
||||
"""Enable or disable the feature on the device."""
|
||||
if self.needs_coordinator:
|
||||
soco = self.soco.group.coordinator
|
||||
else:
|
||||
soco = self.soco
|
||||
try:
|
||||
setattr(soco, self.feature_type, enable)
|
||||
except SoCoUPnPException as exc:
|
||||
_LOGGER.warning("Could not toggle %s: %s", self.entity_id, exc)
|
||||
|
||||
|
||||
class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
||||
|
@ -99,7 +221,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
|||
str(self.alarm.start_time)[0:5],
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def _async_poll(self) -> None:
|
||||
"""Call the central alarm polling method."""
|
||||
await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue