diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py
index 72e5a33ca28c32613574b1ef1b597db75bac1f38..c8defb1ff14f9ec2c706ea2fd103c2096c2edc65 100644
--- a/homeassistant/components/sonos/__init__.py
+++ b/homeassistant/components/sonos/__init__.py
@@ -5,6 +5,7 @@ import asyncio
 from collections import OrderedDict
 import datetime
 from enum import Enum
+from functools import partial
 import logging
 import socket
 from urllib.parse import urlparse
@@ -22,17 +23,19 @@ from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP
 from homeassistant.core import Event, HomeAssistant, callback
 from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
+from homeassistant.helpers.dispatcher import async_dispatcher_send
 
 from .alarms import SonosAlarms
 from .const import (
+    AVAILABILITY_CHECK_INTERVAL,
     DATA_SONOS,
     DATA_SONOS_DISCOVERY_MANAGER,
     DISCOVERY_INTERVAL,
     DOMAIN,
     PLATFORMS,
+    SONOS_CHECK_ACTIVITY,
     SONOS_REBOOTED,
-    SONOS_SEEN,
+    SONOS_SPEAKER_ACTIVITY,
     UPNP_ST,
 )
 from .favorites import SonosFavorites
@@ -187,7 +190,7 @@ class SonosDiscoveryManager:
 
     async def _async_stop_event_listener(self, event: Event | None = None) -> None:
         await asyncio.gather(
-            *(speaker.async_unsubscribe() for speaker in self.data.discovered.values())
+            *(speaker.async_offline() for speaker in self.data.discovered.values())
         )
         if events_asyncio.event_listener:
             await events_asyncio.event_listener.async_stop()
@@ -212,7 +215,7 @@ class SonosDiscoveryManager:
                     new_coordinator = coordinator(self.hass, soco.household_id)
                     new_coordinator.setup(soco)
                     coord_dict[soco.household_id] = new_coordinator
-            speaker.setup()
+            speaker.setup(self.entry)
         except (OSError, SoCoException):
             _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True)
 
@@ -228,10 +231,7 @@ class SonosDiscoveryManager:
                 ),
                 None,
             )
-
-            if known_uid:
-                dispatcher_send(self.hass, f"{SONOS_SEEN}-{known_uid}")
-            else:
+            if not known_uid:
                 soco = self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED)
                 if soco and soco.is_visible:
                     self._discovered_player(soco)
@@ -261,7 +261,9 @@ class SonosDiscoveryManager:
                 ):
                     async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}", soco)
             else:
-                async_dispatcher_send(self.hass, f"{SONOS_SEEN}-{uid}")
+                async_dispatcher_send(
+                    self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", "discovery"
+                )
 
     async def _async_ssdp_discovered_player(self, info, change):
         if change == ssdp.SsdpChange.BYEBYE:
@@ -327,3 +329,14 @@ class SonosDiscoveryManager:
                 self.hass, self._async_ssdp_discovered_player, {"st": UPNP_ST}
             )
         )
+
+        self.entry.async_on_unload(
+            self.hass.helpers.event.async_track_time_interval(
+                partial(
+                    async_dispatcher_send,
+                    self.hass,
+                    SONOS_CHECK_ACTIVITY,
+                ),
+                AVAILABILITY_CHECK_INTERVAL,
+            )
+        )
diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py
index abd0465293689e9f255038e3b3c6ed0770c23b7f..05abc662b480badff4b0becfc1ffe5a55c289806 100644
--- a/homeassistant/components/sonos/const.py
+++ b/homeassistant/components/sonos/const.py
@@ -135,6 +135,7 @@ PLAYABLE_MEDIA_TYPES = [
     MEDIA_TYPE_TRACK,
 ]
 
+SONOS_CHECK_ACTIVITY = "sonos_check_activity"
 SONOS_CREATE_ALARM = "sonos_create_alarm"
 SONOS_CREATE_BATTERY = "sonos_create_battery"
 SONOS_CREATE_SWITCHES = "sonos_create_switches"
@@ -143,18 +144,17 @@ SONOS_ENTITY_CREATED = "sonos_entity_created"
 SONOS_POLL_UPDATE = "sonos_poll_update"
 SONOS_ALARMS_UPDATED = "sonos_alarms_updated"
 SONOS_FAVORITES_UPDATED = "sonos_favorites_updated"
+SONOS_SPEAKER_ACTIVITY = "sonos_speaker_activity"
 SONOS_SPEAKER_ADDED = "sonos_speaker_added"
 SONOS_STATE_UPDATED = "sonos_state_updated"
 SONOS_REBOOTED = "sonos_rebooted"
-SONOS_SEEN = "sonos_seen"
 
 SOURCE_LINEIN = "Line-in"
 SOURCE_TV = "TV"
 
+AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1)
+AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5
 BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15)
 SCAN_INTERVAL = datetime.timedelta(seconds=10)
 DISCOVERY_INTERVAL = datetime.timedelta(seconds=60)
-SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL
 SUBSCRIPTION_TIMEOUT = 1200
-
-MDNS_SERVICE = "_sonos._tcp.local."
diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py
index 0579c4f5c9b2307a712685f8739a76cdbfcff76f..d8196ffdfa6336374f06d316eec8331743f06435 100644
--- a/homeassistant/components/sonos/entity.py
+++ b/homeassistant/components/sonos/entity.py
@@ -39,8 +39,6 @@ class SonosEntity(Entity):
 
     async def async_added_to_hass(self) -> None:
         """Handle common setup when added to hass."""
-        await self.speaker.async_seen()
-
         self.async_on_remove(
             async_dispatcher_connect(
                 self.hass,
diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py
index 3d5a1230bcbad32a5b8996a28955c3f205838c14..d7f1a2e6a9630bf335dfde83270df4cfbdfc132d 100644
--- a/homeassistant/components/sonos/exception.py
+++ b/homeassistant/components/sonos/exception.py
@@ -1,6 +1,11 @@
 """Sonos specific exceptions."""
 from homeassistant.components.media_player.errors import BrowseError
+from homeassistant.exceptions import HomeAssistantError
 
 
 class UnknownMediaType(BrowseError):
     """Unknown media type."""
+
+
+class SpeakerUnavailable(HomeAssistantError):
+    """Speaker is unavailable."""
diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py
index 490bcdefba589f06e502dc1b52767036376e6fb7..e80d16a491b2fe241f709e5fca96e080bc615283 100644
--- a/homeassistant/components/sonos/helpers.py
+++ b/homeassistant/components/sonos/helpers.py
@@ -1,32 +1,43 @@
 """Helper methods for common tasks."""
 from __future__ import annotations
 
-from collections.abc import Callable
-import functools as ft
 import logging
-from typing import Any
+from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
 
 from soco.exceptions import SoCoException, SoCoUPnPException
 
 from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.dispatcher import dispatcher_send
+
+from .const import SONOS_SPEAKER_ACTIVITY
+from .exception import SpeakerUnavailable
+
+if TYPE_CHECKING:
+    from .entity import SonosEntity
+    from .speaker import SonosSpeaker
 
 UID_PREFIX = "RINCON_"
 UID_POSTFIX = "01400"
 
+WrapFuncType = TypeVar("WrapFuncType", bound=Callable[..., Any])
+
 _LOGGER = logging.getLogger(__name__)
 
 
-def soco_error(errorcodes: list[str] | None = None) -> Callable:
+def soco_error(
+    errorcodes: list[str] | None = None, raise_on_err: bool = True
+) -> Callable:
     """Filter out specified UPnP errors and raise exceptions for service calls."""
 
-    def decorator(funct: Callable) -> Callable:
+    def decorator(funct: WrapFuncType) -> WrapFuncType:
         """Decorate functions."""
 
-        @ft.wraps(funct)
-        def wrapper(*args: Any, **kwargs: Any) -> Any:
+        def wrapper(self: SonosSpeaker | SonosEntity, *args: Any, **kwargs: Any) -> Any:
             """Wrap for all soco UPnP exception."""
             try:
-                return funct(*args, **kwargs)
+                result = funct(self, *args, **kwargs)
+            except SpeakerUnavailable:
+                return None
             except (OSError, SoCoException, SoCoUPnPException) as err:
                 error_code = getattr(err, "error_code", None)
                 function = funct.__name__
@@ -34,10 +45,25 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable:
                     _LOGGER.debug(
                         "Error code %s ignored in call to %s", error_code, function
                     )
-                    return
-                raise HomeAssistantError(f"Error calling {function}: {err}") from err
+                    return None
+
+                # Prefer the entity_id if available, zone name as a fallback
+                # Needed as SonosSpeaker instances are not entities
+                zone_name = getattr(self, "speaker", self).zone_name
+                target = getattr(self, "entity_id", zone_name)
+                message = f"Error calling {function} on {target}: {err}"
+                if raise_on_err:
+                    raise HomeAssistantError(message) from err
+
+                _LOGGER.warning(message)
+                return None
+
+            dispatcher_send(
+                self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", funct.__name__
+            )
+            return result
 
-        return wrapper
+        return cast(WrapFuncType, wrapper)
 
     return decorator
 
diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py
index 593bd8f034aa612ffdbf03fc0336517d3e176be9..1acb814ee178cb21813ade45697f550f37098497 100644
--- a/homeassistant/components/sonos/speaker.py
+++ b/homeassistant/components/sonos/speaker.py
@@ -7,6 +7,7 @@ import contextlib
 import datetime
 from functools import partial
 import logging
+import time
 from typing import Any
 import urllib.parse
 
@@ -19,30 +20,30 @@ from soco.music_library import MusicLibrary
 from soco.plugins.sharelink import ShareLinkPlugin
 from soco.snapshot import Snapshot
 
-from homeassistant.components import zeroconf
 from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
 from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
 from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
 from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers import entity_registry as ent_reg
 from homeassistant.helpers.dispatcher import (
+    async_dispatcher_connect,
     async_dispatcher_send,
-    dispatcher_connect,
     dispatcher_send,
 )
 from homeassistant.util import dt as dt_util
 
 from .alarms import SonosAlarms
 from .const import (
+    AVAILABILITY_TIMEOUT,
     BATTERY_SCAN_INTERVAL,
     DATA_SONOS,
     DOMAIN,
-    MDNS_SERVICE,
     PLATFORMS,
     SCAN_INTERVAL,
-    SEEN_EXPIRE_TIME,
+    SONOS_CHECK_ACTIVITY,
     SONOS_CREATE_ALARM,
     SONOS_CREATE_BATTERY,
     SONOS_CREATE_MEDIA_PLAYER,
@@ -50,7 +51,7 @@ from .const import (
     SONOS_ENTITY_CREATED,
     SONOS_POLL_UPDATE,
     SONOS_REBOOTED,
-    SONOS_SEEN,
+    SONOS_SPEAKER_ACTIVITY,
     SONOS_SPEAKER_ADDED,
     SONOS_STATE_PLAYING,
     SONOS_STATE_TRANSITIONING,
@@ -154,6 +155,7 @@ class SonosSpeaker:
         self.household_id: str = soco.household_id
         self.media = SonosMedia(soco)
         self._share_link_plugin: ShareLinkPlugin | None = None
+        self.available = True
 
         # Synchronization helpers
         self._is_ready: bool = False
@@ -164,16 +166,13 @@ class SonosSpeaker:
         self._subscriptions: list[SubscriptionBase] = []
         self._resubscription_lock: asyncio.Lock | None = None
         self._event_dispatchers: dict[str, Callable] = {}
+        self._last_activity: datetime.datetime | None = None
 
         # Scheduled callback handles
         self._poll_timer: Callable | None = None
-        self._seen_timer: Callable | None = None
 
         # Dispatcher handles
-        self._entity_creation_dispatcher: Callable | None = None
-        self._group_dispatcher: Callable | None = None
-        self._reboot_dispatcher: Callable | None = None
-        self._seen_dispatcher: Callable | None = None
+        self.dispatchers: list[Callable] = []
 
         # Device information
         self.mac_address = speaker_info["mac_address"]
@@ -208,26 +207,32 @@ class SonosSpeaker:
         self.snapshot_group: list[SonosSpeaker] | None = None
         self._group_members_missing: set[str] = set()
 
-    def setup(self) -> None:
+    async def async_setup_dispatchers(self, entry: ConfigEntry) -> None:
+        """Connect dispatchers in async context during setup."""
+        dispatch_pairs = (
+            (SONOS_CHECK_ACTIVITY, self.async_check_activity),
+            (SONOS_SPEAKER_ADDED, self.update_group_for_uid),
+            (f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.async_handle_new_entity),
+            (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted),
+            (f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity),
+        )
+
+        for (signal, target) in dispatch_pairs:
+            entry.async_on_unload(
+                async_dispatcher_connect(
+                    self.hass,
+                    signal,
+                    target,
+                )
+            )
+
+    def setup(self, entry: ConfigEntry) -> None:
         """Run initial setup of the speaker."""
         self.set_basic_info()
-
-        self._entity_creation_dispatcher = dispatcher_connect(
-            self.hass,
-            f"{SONOS_ENTITY_CREATED}-{self.soco.uid}",
-            self.async_handle_new_entity,
-        )
-        self._seen_dispatcher = dispatcher_connect(
-            self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen
-        )
-        self._reboot_dispatcher = dispatcher_connect(
-            self.hass, f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted
-        )
-        self._group_dispatcher = dispatcher_connect(
-            self.hass,
-            SONOS_SPEAKER_ADDED,
-            self.update_group_for_uid,
+        future = asyncio.run_coroutine_threadsafe(
+            self.async_setup_dispatchers(entry), self.hass.loop
         )
+        future.result(timeout=10)
 
         if battery_info := fetch_battery_info_or_none(self.soco):
             self.battery_info = battery_info
@@ -291,11 +296,6 @@ class SonosSpeaker:
     #
     # Properties
     #
-    @property
-    def available(self) -> bool:
-        """Return whether this speaker is available."""
-        return self._seen_timer is not None
-
     @property
     def alarms(self) -> SonosAlarms:
         """Return the SonosAlarms instance for this household."""
@@ -408,7 +408,7 @@ class SonosSpeaker:
                     self.zone_name,
                     exc_info=exception,
                 )
-            await self.async_unseen()
+            await self.async_offline()
 
     @callback
     def async_dispatch_event(self, event: SonosEvent) -> None:
@@ -420,6 +420,8 @@ class SonosSpeaker:
             self._poll_timer()
             self._poll_timer = None
 
+        self.speaker_activity(f"{event.service.service_type} subscription")
+
         dispatcher = self._event_dispatchers[event.service.service_type]
         dispatcher(event)
 
@@ -500,65 +502,43 @@ class SonosSpeaker:
     # Speaker availability methods
     #
     @callback
-    def _async_reset_seen_timer(self):
-        """Reset the _seen_timer scheduler."""
-        if self._seen_timer:
-            self._seen_timer()
-        self._seen_timer = self.hass.helpers.event.async_call_later(
-            SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen
-        )
-
-    async def async_seen(self, soco: SoCo | None = None) -> None:
-        """Record that this speaker was seen right now."""
-        if soco is not None:
-            self.soco = soco
-
+    def speaker_activity(self, source):
+        """Track the last activity on this speaker, set availability and resubscribe."""
+        _LOGGER.debug("Activity on %s from %s", self.zone_name, source)
+        self._last_activity = time.monotonic()
         was_available = self.available
-
-        self._async_reset_seen_timer()
-
-        if was_available:
+        self.available = True
+        if not was_available:
             self.async_write_entity_states()
-            return
+            self.hass.async_create_task(self.async_subscribe())
 
-        _LOGGER.debug(
-            "%s [%s] was not available, setting up",
-            self.zone_name,
-            self.soco.ip_address,
-        )
-
-        if self._is_ready and not self.subscriptions_failed:
-            done = await self.async_subscribe()
-            if not done:
-                await self.async_unseen()
+    async def async_check_activity(self, now: datetime.datetime) -> None:
+        """Validate availability of the speaker based on recent activity."""
+        if time.monotonic() - self._last_activity < AVAILABILITY_TIMEOUT:
+            return
 
-        self.async_write_entity_states()
+        try:
+            _ = await self.hass.async_add_executor_job(getattr, self.soco, "volume")
+        except (OSError, SoCoException):
+            pass
+        else:
+            self.speaker_activity("timeout poll")
+            return
 
-    async def async_unseen(
-        self, callback_timestamp: datetime.datetime | None = None
-    ) -> None:
-        """Make this player unavailable when it was not seen recently."""
-        data = self.hass.data[DATA_SONOS]
-        if (zcname := data.mdns_names.get(self.soco.uid)) and callback_timestamp:
-            # Called by a _seen_timer timeout, check mDNS one more time
-            # This should not be checked in an "active" unseen scenario
-            aiozeroconf = await zeroconf.async_get_async_instance(self.hass)
-            if await aiozeroconf.async_get_service_info(MDNS_SERVICE, zcname):
-                # We can still see the speaker via zeroconf check again later.
-                self._async_reset_seen_timer()
-                return
+        if not self.available:
+            return
 
         _LOGGER.debug(
-            "No activity and could not locate %s on the network. Marking unavailable",
-            zcname,
+            "No recent activity and cannot reach %s, marking unavailable",
+            self.zone_name,
         )
+        await self.async_offline()
 
+    async def async_offline(self) -> None:
+        """Handle removal of speaker when unavailable."""
+        self.available = False
         self._share_link_plugin = None
 
-        if self._seen_timer:
-            self._seen_timer()
-            self._seen_timer = None
-
         if self._poll_timer:
             self._poll_timer()
             self._poll_timer = None
@@ -575,11 +555,9 @@ class SonosSpeaker:
             self.zone_name,
             soco,
         )
-        await self.async_unsubscribe()
+        await self.async_offline()
         self.soco = soco
-        await self.async_subscribe()
-        self._async_reset_seen_timer()
-        self.async_write_entity_states()
+        self.speaker_activity("reboot")
 
     #
     # Battery management
diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py
index 830f3b09481e45e1a02f9202790e172975e55c09..78469dc8bbce983aaeeba785e0f7b32d2696f1fb 100644
--- a/homeassistant/components/sonos/switch.py
+++ b/homeassistant/components/sonos/switch.py
@@ -20,6 +20,8 @@ from .const import (
     SONOS_CREATE_SWITCHES,
 )
 from .entity import SonosEntity
+from .exception import SpeakerUnavailable
+from .helpers import soco_error
 from .speaker import SonosSpeaker
 
 _LOGGER = logging.getLogger(__name__)
@@ -144,8 +146,12 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity):
         if not self.should_poll:
             await self.hass.async_add_executor_job(self.update)
 
+    @soco_error(raise_on_err=False)
     def update(self) -> None:
         """Fetch switch state if necessary."""
+        if not self.available:
+            raise SpeakerUnavailable
+
         state = getattr(self.soco, self.feature_type)
         setattr(self.speaker, self.feature_type, state)
 
@@ -164,6 +170,7 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity):
         """Turn the entity off."""
         self.send_command(False)
 
+    @soco_error()
     def send_command(self, enable: bool) -> None:
         """Enable or disable the feature on the device."""
         if self.needs_coordinator: