2021-04-25 17:20:21 +00:00
|
|
|
"""Entity representing a Sonos player."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-10-23 21:11:27 +00:00
|
|
|
from abc import abstractmethod
|
2021-05-28 10:07:58 +00:00
|
|
|
import datetime
|
2021-04-25 17:20:21 +00:00
|
|
|
import logging
|
|
|
|
|
2021-07-28 17:19:09 +00:00
|
|
|
import soco.config as soco_config
|
2021-07-22 22:40:30 +00:00
|
|
|
from soco.core import SoCo
|
2021-04-25 17:20:21 +00:00
|
|
|
|
2022-01-31 18:21:21 +00:00
|
|
|
from homeassistant.components import persistent_notification
|
2021-04-25 17:20:21 +00:00
|
|
|
import homeassistant.helpers.device_registry as dr
|
2021-12-22 04:36:12 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2021-05-01 22:37:19 +00:00
|
|
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
2021-04-25 17:20:21 +00:00
|
|
|
|
2022-04-21 17:37:16 +00:00
|
|
|
from .const import DATA_SONOS, DOMAIN, SONOS_FALLBACK_POLL, SONOS_STATE_UPDATED
|
2022-02-08 18:17:05 +00:00
|
|
|
from .exception import SonosUpdateError
|
2021-04-25 17:20:21 +00:00
|
|
|
from .speaker import SonosSpeaker
|
|
|
|
|
2022-01-31 18:21:21 +00:00
|
|
|
SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements"
|
|
|
|
|
2021-04-25 17:20:21 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class SonosEntity(Entity):
|
|
|
|
"""Representation of a Sonos entity."""
|
|
|
|
|
2021-10-23 21:11:27 +00:00
|
|
|
_attr_should_poll = False
|
|
|
|
|
2021-04-30 05:01:09 +00:00
|
|
|
def __init__(self, speaker: SonosSpeaker) -> None:
|
2021-04-25 17:20:21 +00:00
|
|
|
"""Initialize a SonosEntity."""
|
|
|
|
self.speaker = speaker
|
|
|
|
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
|
|
"""Handle common setup when added to hass."""
|
2022-01-05 15:22:36 +00:00
|
|
|
self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id] = self.speaker
|
2021-04-25 17:20:21 +00:00
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
2022-02-08 18:17:05 +00:00
|
|
|
f"{SONOS_FALLBACK_POLL}-{self.soco.uid}",
|
|
|
|
self.async_fallback_poll,
|
2021-04-25 17:20:21 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
|
|
|
f"{SONOS_STATE_UPDATED}-{self.soco.uid}",
|
2021-04-26 21:59:04 +00:00
|
|
|
self.async_write_ha_state,
|
2021-04-25 17:20:21 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2022-01-05 15:22:36 +00:00
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
|
|
"""Clean up when entity is removed."""
|
|
|
|
del self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id]
|
|
|
|
|
2022-02-08 18:17:05 +00:00
|
|
|
async def async_fallback_poll(self, now: datetime.datetime) -> None:
|
2021-05-28 10:07:58 +00:00
|
|
|
"""Poll the entity if subscriptions fail."""
|
2021-06-10 04:31:14 +00:00
|
|
|
if not self.speaker.subscriptions_failed:
|
2021-07-28 17:19:09 +00:00
|
|
|
if soco_config.EVENT_ADVERTISE_IP:
|
|
|
|
listener_msg = f"{self.speaker.subscription_address} (advertising as {soco_config.EVENT_ADVERTISE_IP})"
|
|
|
|
else:
|
|
|
|
listener_msg = self.speaker.subscription_address
|
2022-01-31 18:21:21 +00:00
|
|
|
message = f"{self.speaker.zone_name} cannot reach {listener_msg}, falling back to polling, functionality may be limited"
|
|
|
|
log_link_msg = f", see {SUB_FAIL_URL} for more details"
|
|
|
|
notification_link_msg = f'.\n\nSee <a href="{SUB_FAIL_URL}">Sonos documentation</a> for more details.'
|
|
|
|
_LOGGER.warning(message + log_link_msg)
|
|
|
|
persistent_notification.async_create(
|
|
|
|
self.hass,
|
|
|
|
message + notification_link_msg,
|
|
|
|
"Sonos networking issue",
|
|
|
|
"sonos_subscriptions_failed",
|
2021-05-28 10:07:58 +00:00
|
|
|
)
|
2021-06-10 04:31:14 +00:00
|
|
|
self.speaker.subscriptions_failed = True
|
|
|
|
await self.speaker.async_unsubscribe()
|
2021-06-03 04:10:27 +00:00
|
|
|
try:
|
2022-02-08 18:17:05 +00:00
|
|
|
await self._async_fallback_poll()
|
|
|
|
except SonosUpdateError as err:
|
|
|
|
_LOGGER.debug("Could not fallback poll: %s", err)
|
2021-05-28 10:07:58 +00:00
|
|
|
|
2021-10-23 21:11:27 +00:00
|
|
|
@abstractmethod
|
2022-02-08 18:17:05 +00:00
|
|
|
async def _async_fallback_poll(self) -> None:
|
|
|
|
"""Poll the specific functionality if subscriptions fail.
|
|
|
|
|
|
|
|
Should be implemented by platforms if needed.
|
|
|
|
"""
|
2021-10-23 21:11:27 +00:00
|
|
|
|
2021-04-25 17:20:21 +00:00
|
|
|
@property
|
|
|
|
def soco(self) -> SoCo:
|
|
|
|
"""Return the speaker SoCo instance."""
|
|
|
|
return self.speaker.soco
|
|
|
|
|
|
|
|
@property
|
2021-05-01 22:37:19 +00:00
|
|
|
def device_info(self) -> DeviceInfo:
|
2021-04-25 17:20:21 +00:00
|
|
|
"""Return information about the device."""
|
2021-10-22 15:40:13 +00:00
|
|
|
return DeviceInfo(
|
|
|
|
identifiers={(DOMAIN, self.soco.uid)},
|
|
|
|
name=self.speaker.zone_name,
|
|
|
|
model=self.speaker.model_name.replace("Sonos ", ""),
|
|
|
|
sw_version=self.speaker.version,
|
|
|
|
connections={
|
2021-09-27 15:36:47 +00:00
|
|
|
(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address),
|
|
|
|
(dr.CONNECTION_UPNP, f"uuid:{self.speaker.uid}"),
|
|
|
|
},
|
2021-10-22 15:40:13 +00:00
|
|
|
manufacturer="Sonos",
|
|
|
|
suggested_area=self.speaker.zone_name,
|
|
|
|
configuration_url=f"http://{self.soco.ip_address}:1400/support/review",
|
|
|
|
)
|
2021-04-25 17:20:21 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def available(self) -> bool:
|
|
|
|
"""Return whether this device is available."""
|
|
|
|
return self.speaker.available
|
2022-02-08 18:17:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SonosPollingEntity(SonosEntity):
|
|
|
|
"""Representation of a Sonos entity which may not support updating by subscriptions."""
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def poll_state(self) -> None:
|
|
|
|
"""Poll the device for the current state."""
|
|
|
|
|
|
|
|
def update(self) -> None:
|
|
|
|
"""Update the state using the built-in entity poller."""
|
|
|
|
if not self.available:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
self.poll_state()
|
|
|
|
except SonosUpdateError as err:
|
|
|
|
_LOGGER.debug("Could not poll: %s", err)
|