From a0795b3b9689219791f0b3a966747667ca7772d0 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Thu, 20 Aug 2026 03:35:44 -0400 Subject: [PATCH] Prevent Sonos from pinging disabled device (#176893) --- homeassistant/components/sonos/__init__.py | 20 ++++++- tests/components/sonos/test_init.py | 62 ++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index e17f7c8abf2e..e953b4110f43 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -73,6 +73,12 @@ DISCOVERY_IGNORED_MODELS = ["Sonos Boost"] ZGS_SUBSCRIPTION_TIMEOUT = 2 SHUTDOWN_TIMEOUT = 10 + +def _get_soco_uid(soco: SoCo) -> str: + """Get SoCo uid as a typed helper for executor jobs.""" + return soco.uid + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -531,9 +537,11 @@ class SonosDiscoveryManager: ), None, ) - if not known_speaker: + if known_speaker: + uid = known_speaker.uid + else: try: - uid = await self.hass.async_add_executor_job(getattr, soco, "uid") + uid = await self.hass.async_add_executor_job(_get_soco_uid, soco) except HTTPError as err: await self._process_http_connection_error(err, ip_addr) continue @@ -545,6 +553,14 @@ class SonosDiscoveryManager: ) as ex: _LOGGER.warning("Could not get Sonos uid from %s: %s", ip_addr, ex) continue + + if self.is_device_disabled(uid): + _LOGGER.debug( + "Skipping manual poll for disabled Sonos device: %s", + uid, + ) + continue + if not known_speaker: try: await self._async_handle_discovery_message( uid, diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index dde22673d85a..7169a95a5bb9 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -8,6 +8,7 @@ import logging from typing import Any from unittest.mock import MagicMock, Mock, PropertyMock, patch +from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory import pytest from requests import Response @@ -605,6 +606,67 @@ async def test_async_poll_manual_hosts_6( await hass.async_block_till_done(wait_background_tasks=True) +async def test_async_poll_manual_hosts_skips_ping_for_disabled_device( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test disabled manual-host speakers are not pinged on heartbeat.""" + soco = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") + soco.renderingControl = Mock() + soco.renderingControl.GetVolume = Mock() + + await _setup_hass(hass) + + assert "media_player.living_room" in entity_registry.entities + + # Mark the speaker unavailable via ZGS event with VanishedDevices. + async def fire_vanish_event(): + subscription = soco.zoneGroupTopology.subscribe.return_value + sub_callback = await subscription.wait_for_callback_to_be_set() + zgs_with_vanished = f""" + + + + + + + + + """ + event = SonosMockEvent( + soco, soco.zoneGroupTopology, {"ZoneGroupState": zgs_with_vanished} + ) + sub_callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + + await fire_vanish_event() + + # Verify the speaker is marked unavailable. + state = hass.states.get("media_player.living_room") + assert state is not None + assert state.state == "unavailable" + + # Now disable the device. + device = device_registry.async_get_device(identifiers={(sonos.DOMAIN, soco.uid)}) + assert device is not None + device_registry.async_update_device( + device.id, + disabled_by=dr.DeviceEntryDisabler.USER, + ) + + # SonosSpeaker.ping uses RenderingControl.GetVolume under the hood. + soco.renderingControl.GetVolume.reset_mock() + with freeze_time(dt_util.utcnow()) as freezer: + freezer.tick(DISCOVERY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # The disabled speaker should not have been pinged. + soco.renderingControl.GetVolume.assert_not_called() + + async def test_async_poll_manual_hosts_7( hass: HomeAssistant, soco_factory: SoCoMockFactory,