Expose Sonos features as switch entities (#54502)

Co-authored-by: Tobias Sauerwein <cgtobi@users.noreply.github.com>
This commit is contained in:
jjlawren 2021-10-23 16:11:27 -05:00 committed by GitHub
parent 21daffe905
commit 084fd2d19f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 184 additions and 82 deletions

View file

@ -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()