mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Use subscription callbacks to discover Sonos speakers (#85411)
fixes undefined
This commit is contained in:
parent
d81febd3f4
commit
1b592e6885
@ -11,9 +11,10 @@ import socket
|
|||||||
from typing import TYPE_CHECKING, Any, Optional, cast
|
from typing import TYPE_CHECKING, Any, Optional, cast
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from soco import events_asyncio
|
from soco import events_asyncio, zonegroupstate
|
||||||
import soco.config as soco_config
|
import soco.config as soco_config
|
||||||
from soco.core import SoCo
|
from soco.core import SoCo
|
||||||
|
from soco.events_base import Event as SonosEvent, SubscriptionBase
|
||||||
from soco.exceptions import SoCoException
|
from soco.exceptions import SoCoException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -24,8 +25,8 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_track_time_interval, call_later
|
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .alarms import SonosAlarms
|
from .alarms import SonosAlarms
|
||||||
@ -40,6 +41,7 @@ from .const import (
|
|||||||
SONOS_REBOOTED,
|
SONOS_REBOOTED,
|
||||||
SONOS_SPEAKER_ACTIVITY,
|
SONOS_SPEAKER_ACTIVITY,
|
||||||
SONOS_VANISHED,
|
SONOS_VANISHED,
|
||||||
|
SUBSCRIPTION_TIMEOUT,
|
||||||
UPNP_ST,
|
UPNP_ST,
|
||||||
)
|
)
|
||||||
from .exception import SonosUpdateError
|
from .exception import SonosUpdateError
|
||||||
@ -51,7 +53,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
CONF_ADVERTISE_ADDR = "advertise_addr"
|
CONF_ADVERTISE_ADDR = "advertise_addr"
|
||||||
CONF_INTERFACE_ADDR = "interface_addr"
|
CONF_INTERFACE_ADDR = "interface_addr"
|
||||||
DISCOVERY_IGNORED_MODELS = ["Sonos Boost"]
|
DISCOVERY_IGNORED_MODELS = ["Sonos Boost"]
|
||||||
|
ZGS_SUBSCRIPTION_TIMEOUT = 2
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -122,6 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
"""Set up Sonos from a config entry."""
|
"""Set up Sonos from a config entry."""
|
||||||
soco_config.EVENTS_MODULE = events_asyncio
|
soco_config.EVENTS_MODULE = events_asyncio
|
||||||
soco_config.REQUEST_TIMEOUT = 9.5
|
soco_config.REQUEST_TIMEOUT = 9.5
|
||||||
|
zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT
|
||||||
|
|
||||||
if DATA_SONOS not in hass.data:
|
if DATA_SONOS not in hass.data:
|
||||||
hass.data[DATA_SONOS] = SonosData()
|
hass.data[DATA_SONOS] = SonosData()
|
||||||
@ -172,6 +175,7 @@ class SonosDiscoveryManager:
|
|||||||
self.data = data
|
self.data = data
|
||||||
self.hosts = set(hosts)
|
self.hosts = set(hosts)
|
||||||
self.discovery_lock = asyncio.Lock()
|
self.discovery_lock = asyncio.Lock()
|
||||||
|
self.creation_lock = asyncio.Lock()
|
||||||
self._known_invisible: set[SoCo] = set()
|
self._known_invisible: set[SoCo] = set()
|
||||||
self._manual_config_required = bool(hosts)
|
self._manual_config_required = bool(hosts)
|
||||||
|
|
||||||
@ -184,21 +188,70 @@ class SonosDiscoveryManager:
|
|||||||
"""Check if device at provided IP is known to be invisible."""
|
"""Check if device at provided IP is known to be invisible."""
|
||||||
return any(x for x in self._known_invisible if x.ip_address == ip_address)
|
return any(x for x in self._known_invisible if x.ip_address == ip_address)
|
||||||
|
|
||||||
def _create_visible_speakers(self, ip_address: str) -> None:
|
async def async_subscribe_to_zone_updates(self, ip_address: str) -> None:
|
||||||
"""Create all visible SonosSpeaker instances with the provided seed IP."""
|
"""Test subscriptions and create SonosSpeakers based on results."""
|
||||||
try:
|
soco = SoCo(ip_address)
|
||||||
soco = SoCo(ip_address)
|
# Cache now to avoid household ID lookup during first ZoneGroupState processing
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
getattr,
|
||||||
|
soco,
|
||||||
|
"household_id",
|
||||||
|
)
|
||||||
|
sub = await soco.zoneGroupTopology.subscribe()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_add_visible_zones(subscription_succeeded: bool = False) -> None:
|
||||||
|
"""Determine visible zones and create SonosSpeaker instances."""
|
||||||
|
zones_to_add = set()
|
||||||
|
subscription = None
|
||||||
|
if subscription_succeeded:
|
||||||
|
subscription = sub
|
||||||
|
|
||||||
visible_zones = soco.visible_zones
|
visible_zones = soco.visible_zones
|
||||||
self._known_invisible = soco.all_zones - visible_zones
|
self._known_invisible = soco.all_zones - visible_zones
|
||||||
except (OSError, SoCoException) as ex:
|
for zone in visible_zones:
|
||||||
_LOGGER.warning(
|
if zone.uid not in self.data.discovered:
|
||||||
"Failed to request visible zones from %s: %s", ip_address, ex
|
zones_to_add.add(zone)
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
for zone in visible_zones:
|
if not zones_to_add:
|
||||||
if zone.uid not in self.data.discovered:
|
return
|
||||||
self._add_speaker(zone)
|
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.async_add_speakers(zones_to_add, subscription, soco.uid)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_subscription_failed(now: datetime.datetime) -> None:
|
||||||
|
"""Fallback logic if the subscription callback never arrives."""
|
||||||
|
await sub.unsubscribe()
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Subscription to %s failed, attempting to poll directly", ip_address
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(soco.zone_group_state.poll, soco)
|
||||||
|
except (OSError, SoCoException) as ex:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Fallback pollling to %s failed, setup cannot continue: %s",
|
||||||
|
ip_address,
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
_LOGGER.debug("Fallback ZoneGroupState poll to %s succeeded", ip_address)
|
||||||
|
_async_add_visible_zones()
|
||||||
|
|
||||||
|
cancel_failure_callback = async_call_later(
|
||||||
|
self.hass, ZGS_SUBSCRIPTION_TIMEOUT, async_subscription_failed
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_subscription_succeeded(event: SonosEvent) -> None:
|
||||||
|
"""Create SonosSpeakers when subscription callbacks successfully arrive."""
|
||||||
|
_LOGGER.debug("Subscription to %s succeeded", ip_address)
|
||||||
|
cancel_failure_callback()
|
||||||
|
_async_add_visible_zones(subscription_succeeded=True)
|
||||||
|
|
||||||
|
sub.callback = _async_subscription_succeeded
|
||||||
|
# Hold lock to prevent concurrent subscription attempts
|
||||||
|
await asyncio.sleep(ZGS_SUBSCRIPTION_TIMEOUT * 2)
|
||||||
|
|
||||||
async def _async_stop_event_listener(self, event: Event | None = None) -> None:
|
async def _async_stop_event_listener(self, event: Event | None = None) -> None:
|
||||||
for speaker in self.data.discovered.values():
|
for speaker in self.data.discovered.values():
|
||||||
@ -227,14 +280,35 @@ class SonosDiscoveryManager:
|
|||||||
self.data.hosts_heartbeat()
|
self.data.hosts_heartbeat()
|
||||||
self.data.hosts_heartbeat = None
|
self.data.hosts_heartbeat = None
|
||||||
|
|
||||||
def _add_speaker(self, soco: SoCo) -> None:
|
async def async_add_speakers(
|
||||||
|
self,
|
||||||
|
socos: set[SoCo],
|
||||||
|
zgs_subscription: SubscriptionBase | None,
|
||||||
|
zgs_subscription_uid: str | None,
|
||||||
|
) -> None:
|
||||||
|
"""Create and set up new SonosSpeaker instances."""
|
||||||
|
|
||||||
|
def _add_speakers():
|
||||||
|
"""Add all speakers in a single executor job."""
|
||||||
|
for soco in socos:
|
||||||
|
sub = None
|
||||||
|
if soco.uid == zgs_subscription_uid and zgs_subscription:
|
||||||
|
sub = zgs_subscription
|
||||||
|
self._add_speaker(soco, sub)
|
||||||
|
|
||||||
|
async with self.creation_lock:
|
||||||
|
await self.hass.async_add_executor_job(_add_speakers)
|
||||||
|
|
||||||
|
def _add_speaker(
|
||||||
|
self, soco: SoCo, zone_group_state_sub: SubscriptionBase | None
|
||||||
|
) -> None:
|
||||||
"""Create and set up a new SonosSpeaker instance."""
|
"""Create and set up a new SonosSpeaker instance."""
|
||||||
try:
|
try:
|
||||||
speaker_info = soco.get_speaker_info(True, timeout=7)
|
speaker_info = soco.get_speaker_info(True, timeout=7)
|
||||||
if soco.uid not in self.data.boot_counts:
|
if soco.uid not in self.data.boot_counts:
|
||||||
self.data.boot_counts[soco.uid] = soco.boot_seqnum
|
self.data.boot_counts[soco.uid] = soco.boot_seqnum
|
||||||
_LOGGER.debug("Adding new speaker: %s", speaker_info)
|
_LOGGER.debug("Adding new speaker: %s", speaker_info)
|
||||||
speaker = SonosSpeaker(self.hass, soco, speaker_info)
|
speaker = SonosSpeaker(self.hass, soco, speaker_info, zone_group_state_sub)
|
||||||
self.data.discovered[soco.uid] = speaker
|
self.data.discovered[soco.uid] = speaker
|
||||||
for coordinator, coord_dict in (
|
for coordinator, coord_dict in (
|
||||||
(SonosAlarms, self.data.alarms),
|
(SonosAlarms, self.data.alarms),
|
||||||
@ -250,13 +324,25 @@ class SonosDiscoveryManager:
|
|||||||
except (OSError, SoCoException):
|
except (OSError, SoCoException):
|
||||||
_LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True)
|
_LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True)
|
||||||
|
|
||||||
def _poll_manual_hosts(self, now: datetime.datetime | None = None) -> None:
|
async def async_poll_manual_hosts(
|
||||||
|
self, now: datetime.datetime | None = None
|
||||||
|
) -> None:
|
||||||
"""Add and maintain Sonos devices from a manual configuration."""
|
"""Add and maintain Sonos devices from a manual configuration."""
|
||||||
|
|
||||||
|
def get_sync_attributes(soco: SoCo) -> set[SoCo]:
|
||||||
|
"""Ensure I/O attributes are cached and return visible zones."""
|
||||||
|
_ = soco.household_id
|
||||||
|
_ = soco.uid
|
||||||
|
return soco.visible_zones
|
||||||
|
|
||||||
for host in self.hosts:
|
for host in self.hosts:
|
||||||
ip_addr = socket.gethostbyname(host)
|
ip_addr = socket.gethostbyname(host)
|
||||||
soco = SoCo(ip_addr)
|
soco = SoCo(ip_addr)
|
||||||
try:
|
try:
|
||||||
visible_zones = soco.visible_zones
|
visible_zones = await self.hass.async_add_executor_job(
|
||||||
|
get_sync_attributes,
|
||||||
|
soco,
|
||||||
|
)
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.warning("Could not get visible Sonos devices from %s", ip_addr)
|
_LOGGER.warning("Could not get visible Sonos devices from %s", ip_addr)
|
||||||
else:
|
else:
|
||||||
@ -267,7 +353,7 @@ class SonosDiscoveryManager:
|
|||||||
}:
|
}:
|
||||||
_LOGGER.debug("Adding to manual hosts: %s", new_hosts)
|
_LOGGER.debug("Adding to manual hosts: %s", new_hosts)
|
||||||
self.hosts.update(new_hosts)
|
self.hosts.update(new_hosts)
|
||||||
dispatcher_send(
|
async_dispatcher_send(
|
||||||
self.hass,
|
self.hass,
|
||||||
f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}",
|
f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}",
|
||||||
"manual zone scan",
|
"manual zone scan",
|
||||||
@ -290,7 +376,9 @@ class SonosDiscoveryManager:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if not known_speaker:
|
if not known_speaker:
|
||||||
self._create_visible_speakers(ip_addr)
|
await self._async_handle_discovery_message(
|
||||||
|
soco.uid, ip_addr, "manual zone scan"
|
||||||
|
)
|
||||||
elif not known_speaker.available:
|
elif not known_speaker.available:
|
||||||
try:
|
try:
|
||||||
known_speaker.ping()
|
known_speaker.ping()
|
||||||
@ -299,33 +387,32 @@ class SonosDiscoveryManager:
|
|||||||
"Manual poll to %s failed, keeping unavailable", ip_addr
|
"Manual poll to %s failed, keeping unavailable", ip_addr
|
||||||
)
|
)
|
||||||
|
|
||||||
self.data.hosts_heartbeat = call_later(
|
self.data.hosts_heartbeat = async_call_later(
|
||||||
self.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts
|
self.hass, DISCOVERY_INTERVAL.total_seconds(), self.async_poll_manual_hosts
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_handle_discovery_message(
|
async def _async_handle_discovery_message(
|
||||||
self, uid: str, discovered_ip: str, boot_seqnum: int | None
|
self,
|
||||||
|
uid: str,
|
||||||
|
discovered_ip: str,
|
||||||
|
source: str,
|
||||||
|
boot_seqnum: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle discovered player creation and activity."""
|
"""Handle discovered player creation and activity."""
|
||||||
async with self.discovery_lock:
|
async with self.discovery_lock:
|
||||||
if not self.data.discovered:
|
if not self.data.discovered:
|
||||||
# Initial discovery, attempt to add all visible zones
|
# Initial discovery, attempt to add all visible zones
|
||||||
await self.hass.async_add_executor_job(
|
await self.async_subscribe_to_zone_updates(discovered_ip)
|
||||||
self._create_visible_speakers,
|
|
||||||
discovered_ip,
|
|
||||||
)
|
|
||||||
elif uid not in self.data.discovered:
|
elif uid not in self.data.discovered:
|
||||||
if self.is_device_invisible(discovered_ip):
|
if self.is_device_invisible(discovered_ip):
|
||||||
return
|
return
|
||||||
await self.hass.async_add_executor_job(
|
await self.async_subscribe_to_zone_updates(discovered_ip)
|
||||||
self._add_speaker, SoCo(discovered_ip)
|
|
||||||
)
|
|
||||||
elif boot_seqnum and boot_seqnum > self.data.boot_counts[uid]:
|
elif boot_seqnum and boot_seqnum > self.data.boot_counts[uid]:
|
||||||
self.data.boot_counts[uid] = boot_seqnum
|
self.data.boot_counts[uid] = boot_seqnum
|
||||||
async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}")
|
async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}")
|
||||||
else:
|
else:
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", "discovery"
|
self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", source
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_ssdp_discovered_player(
|
async def _async_ssdp_discovered_player(
|
||||||
@ -389,7 +476,10 @@ class SonosDiscoveryManager:
|
|||||||
self.data.discovery_known.add(uid)
|
self.data.discovery_known.add(uid)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self._async_handle_discovery_message(
|
self._async_handle_discovery_message(
|
||||||
uid, discovered_ip, cast(Optional[int], boot_seqnum)
|
uid,
|
||||||
|
discovered_ip,
|
||||||
|
"discovery",
|
||||||
|
boot_seqnum=cast(Optional[int], boot_seqnum),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -408,7 +498,7 @@ class SonosDiscoveryManager:
|
|||||||
EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat
|
EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await self.hass.async_add_executor_job(self._poll_manual_hosts)
|
await self.async_poll_manual_hosts()
|
||||||
|
|
||||||
self.entry.async_on_unload(
|
self.entry.async_on_unload(
|
||||||
await ssdp.async_register_callback(
|
await ssdp.async_register_callback(
|
||||||
|
@ -69,14 +69,14 @@ EVENT_CHARGING = {
|
|||||||
"CHARGING": True,
|
"CHARGING": True,
|
||||||
"NOT_CHARGING": False,
|
"NOT_CHARGING": False,
|
||||||
}
|
}
|
||||||
SUBSCRIPTION_SERVICES = [
|
SUBSCRIPTION_SERVICES = {
|
||||||
"alarmClock",
|
"alarmClock",
|
||||||
"avTransport",
|
"avTransport",
|
||||||
"contentDirectory",
|
"contentDirectory",
|
||||||
"deviceProperties",
|
"deviceProperties",
|
||||||
"renderingControl",
|
"renderingControl",
|
||||||
"zoneGroupTopology",
|
"zoneGroupTopology",
|
||||||
]
|
}
|
||||||
SUPPORTED_VANISH_REASONS = ("sleeping", "switch to bluetooth", "upgrade")
|
SUPPORTED_VANISH_REASONS = ("sleeping", "switch to bluetooth", "upgrade")
|
||||||
UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"]
|
UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"]
|
||||||
|
|
||||||
@ -88,7 +88,11 @@ class SonosSpeaker:
|
|||||||
"""Representation of a Sonos speaker."""
|
"""Representation of a Sonos speaker."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any]
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
soco: SoCo,
|
||||||
|
speaker_info: dict[str, Any],
|
||||||
|
zone_group_state_sub: SubscriptionBase | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a SonosSpeaker."""
|
"""Initialize a SonosSpeaker."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -112,6 +116,9 @@ class SonosSpeaker:
|
|||||||
# Subscriptions and events
|
# Subscriptions and events
|
||||||
self.subscriptions_failed: bool = False
|
self.subscriptions_failed: bool = False
|
||||||
self._subscriptions: list[SubscriptionBase] = []
|
self._subscriptions: list[SubscriptionBase] = []
|
||||||
|
if zone_group_state_sub:
|
||||||
|
zone_group_state_sub.callback = self.async_dispatch_event
|
||||||
|
self._subscriptions.append(zone_group_state_sub)
|
||||||
self._subscription_lock: asyncio.Lock | None = None
|
self._subscription_lock: asyncio.Lock | None = None
|
||||||
self._event_dispatchers: dict[str, Callable] = {}
|
self._event_dispatchers: dict[str, Callable] = {}
|
||||||
self._last_activity: float = NEVER_TIME
|
self._last_activity: float = NEVER_TIME
|
||||||
@ -289,6 +296,12 @@ class SonosSpeaker:
|
|||||||
addr, port = self._subscriptions[0].event_listener.address
|
addr, port = self._subscriptions[0].event_listener.address
|
||||||
return ":".join([addr, str(port)])
|
return ":".join([addr, str(port)])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def missing_subscriptions(self) -> set[str]:
|
||||||
|
"""Return a list of missing service subscriptions."""
|
||||||
|
subscribed_services = {sub.service.service_type for sub in self._subscriptions}
|
||||||
|
return SUBSCRIPTION_SERVICES - subscribed_services
|
||||||
|
|
||||||
#
|
#
|
||||||
# Subscription handling and event dispatchers
|
# Subscription handling and event dispatchers
|
||||||
#
|
#
|
||||||
@ -321,8 +334,6 @@ class SonosSpeaker:
|
|||||||
self._subscription_lock = asyncio.Lock()
|
self._subscription_lock = asyncio.Lock()
|
||||||
|
|
||||||
async with self._subscription_lock:
|
async with self._subscription_lock:
|
||||||
if self._subscriptions:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
await self._async_subscribe()
|
await self._async_subscribe()
|
||||||
except SonosSubscriptionsFailed:
|
except SonosSubscriptionsFailed:
|
||||||
@ -331,12 +342,14 @@ class SonosSpeaker:
|
|||||||
|
|
||||||
async def _async_subscribe(self) -> None:
|
async def _async_subscribe(self) -> None:
|
||||||
"""Create event subscriptions."""
|
"""Create event subscriptions."""
|
||||||
_LOGGER.debug("Creating subscriptions for %s", self.zone_name)
|
|
||||||
|
|
||||||
subscriptions = [
|
subscriptions = [
|
||||||
self._subscribe(getattr(self.soco, service), self.async_dispatch_event)
|
self._subscribe(getattr(self.soco, service), self.async_dispatch_event)
|
||||||
for service in SUBSCRIPTION_SERVICES
|
for service in self.missing_subscriptions
|
||||||
]
|
]
|
||||||
|
if not subscriptions:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug("Creating subscriptions for %s", self.zone_name)
|
||||||
results = await asyncio.gather(*subscriptions, return_exceptions=True)
|
results = await asyncio.gather(*subscriptions, return_exceptions=True)
|
||||||
for result in results:
|
for result in results:
|
||||||
self.log_subscription_result(
|
self.log_subscription_result(
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
|||||||
from homeassistant.components.sonos import DOMAIN
|
from homeassistant.components.sonos import DOMAIN
|
||||||
from homeassistant.const import CONF_HOSTS
|
from homeassistant.const import CONF_HOSTS
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
|
||||||
class SonosMockService:
|
class SonosMockService:
|
||||||
@ -66,13 +66,14 @@ async def async_autosetup_sonos(async_setup_sonos):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def async_setup_sonos(hass, config_entry):
|
def async_setup_sonos(hass, config_entry, fire_zgs_event):
|
||||||
"""Return a coroutine to set up a Sonos integration instance on demand."""
|
"""Return a coroutine to set up a Sonos integration instance on demand."""
|
||||||
|
|
||||||
async def _wrapper():
|
async def _wrapper():
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
await fire_zgs_event()
|
||||||
|
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
@ -349,3 +350,24 @@ def tv_event_fixture(soco):
|
|||||||
def mock_get_source_ip(mock_get_source_ip):
|
def mock_get_source_ip(mock_get_source_ip):
|
||||||
"""Mock network util's async_get_source_ip in all sonos tests."""
|
"""Mock network util's async_get_source_ip in all sonos tests."""
|
||||||
return mock_get_source_ip
|
return mock_get_source_ip
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="zgs_discovery", scope="session")
|
||||||
|
def zgs_discovery_fixture():
|
||||||
|
"""Load ZoneGroupState discovery payload and return it."""
|
||||||
|
return load_fixture("sonos/zgs_discovery.xml")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="fire_zgs_event")
|
||||||
|
def zgs_event_fixture(hass, soco, zgs_discovery):
|
||||||
|
"""Create alarm_event fixture."""
|
||||||
|
variables = {"ZoneGroupState": zgs_discovery}
|
||||||
|
|
||||||
|
async def _wrapper():
|
||||||
|
event = SonosMockEvent(soco, soco.zoneGroupTopology, variables)
|
||||||
|
subscription = soco.zoneGroupTopology.subscribe.return_value
|
||||||
|
sub_callback = subscription.callback
|
||||||
|
sub_callback(event)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
|
7
tests/components/sonos/fixtures/zgs_discovery.xml
Normal file
7
tests/components/sonos/fixtures/zgs_discovery.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<ZoneGroupState>
|
||||||
|
<ZoneGroups>
|
||||||
|
<ZoneGroup Coordinator="RINCON_test" ID="RINCON_test:1384750254">
|
||||||
|
<ZoneGroupMember UUID="RINCON_test" Location="http://192.168.4.2:1400/xml/device_description.xml" ZoneName="Zone A" Icon="" Configuration="1" SoftwareVersion="70.4-36090" SWGen="2" MinCompatibleVersion="69.0-00000" LegacyCompatibleVersion="58.0-00000" BootSeq="1234" TVConfigurationError="0" HdmiCecAvailable="0" WirelessMode="1" WirelessLeafOnly="0" ChannelFreq="5180" BehindWifiExtender="0" WifiEnabled="1" EthLink="0" Orientation="0" RoomCalibrationState="4" SecureRegState="3" VoiceConfigState="0" MicEnabled="1" AirPlayEnabled="1" IdleState="1" MoreInfo="" SSLPort="1443" HHSSLPort="1843"/>
|
||||||
|
</ZoneGroup>
|
||||||
|
</ZoneGroups>
|
||||||
|
</ZoneGroupState>
|
@ -62,8 +62,12 @@ async def test_user_form(
|
|||||||
async def test_user_form_already_created(hass: core.HomeAssistant):
|
async def test_user_form_already_created(hass: core.HomeAssistant):
|
||||||
"""Ensure we abort a flow if the entry is already created from config."""
|
"""Ensure we abort a flow if the entry is already created from config."""
|
||||||
config = {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: "192.168.4.2"}}}
|
config = {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: "192.168.4.2"}}}
|
||||||
await async_setup_component(hass, DOMAIN, config)
|
with patch(
|
||||||
await hass.async_block_till_done()
|
"homeassistant.components.sonos.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
await async_setup_component(hass, DOMAIN, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
@ -184,7 +184,7 @@ async def test_microphone_binary_sensor(
|
|||||||
assert mic_binary_sensor_state.state == STATE_ON
|
assert mic_binary_sensor_state.state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
async def test_favorites_sensor(hass, async_autosetup_sonos, soco):
|
async def test_favorites_sensor(hass, async_autosetup_sonos, soco, fire_zgs_event):
|
||||||
"""Test Sonos favorites sensor."""
|
"""Test Sonos favorites sensor."""
|
||||||
entity_registry = ent_reg.async_get(hass)
|
entity_registry = ent_reg.async_get(hass)
|
||||||
favorites = entity_registry.entities["sensor.sonos_favorites"]
|
favorites = entity_registry.entities["sensor.sonos_favorites"]
|
||||||
@ -208,6 +208,9 @@ async def test_favorites_sensor(hass, async_autosetup_sonos, soco):
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Trigger subscription callback for speaker discovery
|
||||||
|
await fire_zgs_event()
|
||||||
|
|
||||||
favorites_updated_event = SonosMockEvent(
|
favorites_updated_event = SonosMockEvent(
|
||||||
soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"}
|
soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"}
|
||||||
)
|
)
|
||||||
|
@ -37,7 +37,7 @@ async def test_entity_registry(hass, async_autosetup_sonos):
|
|||||||
assert "switch.zone_a_touch_controls" in entity_registry.entities
|
assert "switch.zone_a_touch_controls" in entity_registry.entities
|
||||||
|
|
||||||
|
|
||||||
async def test_switch_attributes(hass, async_autosetup_sonos, soco):
|
async def test_switch_attributes(hass, async_autosetup_sonos, soco, fire_zgs_event):
|
||||||
"""Test for correct Sonos switch states."""
|
"""Test for correct Sonos switch states."""
|
||||||
entity_registry = ent_reg.async_get(hass)
|
entity_registry = ent_reg.async_get(hass)
|
||||||
|
|
||||||
@ -114,6 +114,9 @@ async def test_switch_attributes(hass, async_autosetup_sonos, soco):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert m.called
|
assert m.called
|
||||||
|
|
||||||
|
# Trigger subscription callback for speaker discovery
|
||||||
|
await fire_zgs_event()
|
||||||
|
|
||||||
status_light_state = hass.states.get(status_light.entity_id)
|
status_light_state = hass.states.get(status_light.entity_id)
|
||||||
assert status_light_state.state == STATE_ON
|
assert status_light_state.state == STATE_ON
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user