mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
Create repair issue if Sonos subscriptions fail (#87437)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
93a87d3c82
commit
d5a6840588
@ -26,7 +26,11 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
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,
|
||||||
|
issue_registry as ir,
|
||||||
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -43,6 +47,8 @@ from .const import (
|
|||||||
SONOS_REBOOTED,
|
SONOS_REBOOTED,
|
||||||
SONOS_SPEAKER_ACTIVITY,
|
SONOS_SPEAKER_ACTIVITY,
|
||||||
SONOS_VANISHED,
|
SONOS_VANISHED,
|
||||||
|
SUB_FAIL_ISSUE_ID,
|
||||||
|
SUB_FAIL_URL,
|
||||||
SUBSCRIPTION_TIMEOUT,
|
SUBSCRIPTION_TIMEOUT,
|
||||||
UPNP_ST,
|
UPNP_ST,
|
||||||
)
|
)
|
||||||
@ -227,6 +233,24 @@ class SonosDiscoveryManager:
|
|||||||
|
|
||||||
async def async_subscription_failed(now: datetime.datetime) -> None:
|
async def async_subscription_failed(now: datetime.datetime) -> None:
|
||||||
"""Fallback logic if the subscription callback never arrives."""
|
"""Fallback logic if the subscription callback never arrives."""
|
||||||
|
addr, port = sub.event_listener.address
|
||||||
|
listener_address = f"{addr}:{port}"
|
||||||
|
if advertise_ip := soco_config.EVENT_ADVERTISE_IP:
|
||||||
|
listener_address += f" (advertising as {advertise_ip})"
|
||||||
|
ir.async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
SUB_FAIL_ISSUE_ID,
|
||||||
|
is_fixable=False,
|
||||||
|
severity=ir.IssueSeverity.ERROR,
|
||||||
|
translation_key="subscriptions_failed",
|
||||||
|
translation_placeholders={
|
||||||
|
"device_ip": ip_address,
|
||||||
|
"listener_address": listener_address,
|
||||||
|
"sub_fail_url": SUB_FAIL_URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Subscription to %s failed, attempting to poll directly", ip_address
|
"Subscription to %s failed, attempting to poll directly", ip_address
|
||||||
)
|
)
|
||||||
@ -256,6 +280,11 @@ class SonosDiscoveryManager:
|
|||||||
"""Create SonosSpeakers when subscription callbacks successfully arrive."""
|
"""Create SonosSpeakers when subscription callbacks successfully arrive."""
|
||||||
_LOGGER.debug("Subscription to %s succeeded", ip_address)
|
_LOGGER.debug("Subscription to %s succeeded", ip_address)
|
||||||
cancel_failure_callback()
|
cancel_failure_callback()
|
||||||
|
ir.async_delete_issue(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
SUB_FAIL_ISSUE_ID,
|
||||||
|
)
|
||||||
_async_add_visible_zones(subscription_succeeded=True)
|
_async_add_visible_zones(subscription_succeeded=True)
|
||||||
|
|
||||||
sub.callback = _async_subscription_succeeded
|
sub.callback = _async_subscription_succeeded
|
||||||
|
@ -19,6 +19,9 @@ PLATFORMS = [
|
|||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SUB_FAIL_ISSUE_ID = "subscriptions_failed"
|
||||||
|
SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements"
|
||||||
|
|
||||||
SONOS_ARTIST = "artists"
|
SONOS_ARTIST = "artists"
|
||||||
SONOS_ALBUM = "albums"
|
SONOS_ALBUM = "albums"
|
||||||
SONOS_PLAYLISTS = "playlists"
|
SONOS_PLAYLISTS = "playlists"
|
||||||
|
@ -5,10 +5,8 @@ from abc import abstractmethod
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import soco.config as soco_config
|
|
||||||
from soco.core import SoCo
|
from soco.core import SoCo
|
||||||
|
|
||||||
from homeassistant.components import persistent_notification
|
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||||
@ -17,8 +15,6 @@ from .const import DATA_SONOS, DOMAIN, SONOS_FALLBACK_POLL, SONOS_STATE_UPDATED
|
|||||||
from .exception import SonosUpdateError
|
from .exception import SonosUpdateError
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements"
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -57,29 +53,6 @@ class SonosEntity(Entity):
|
|||||||
async def async_fallback_poll(self, now: datetime.datetime) -> None:
|
async def async_fallback_poll(self, now: datetime.datetime) -> None:
|
||||||
"""Poll the entity if subscriptions fail."""
|
"""Poll the entity if subscriptions fail."""
|
||||||
if not self.speaker.subscriptions_failed:
|
if not self.speaker.subscriptions_failed:
|
||||||
if soco_config.EVENT_ADVERTISE_IP:
|
|
||||||
listener_msg = (
|
|
||||||
f"{self.speaker.subscription_address}"
|
|
||||||
f" (advertising as {soco_config.EVENT_ADVERTISE_IP})"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
listener_msg = self.speaker.subscription_address
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
self.speaker.subscriptions_failed = True
|
self.speaker.subscriptions_failed = True
|
||||||
await self.speaker.async_unsubscribe()
|
await self.speaker.async_unsubscribe()
|
||||||
try:
|
try:
|
||||||
|
@ -10,5 +10,11 @@
|
|||||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"subscriptions_failed": {
|
||||||
|
"title": "Networking error: subscriptions failed",
|
||||||
|
"description": "Falling back to polling, functionality may be limited.\n\nSonos device at {device_ip} cannot reach Home Assistant at {listener_address}.\n\nSee our [documentation]({sub_fail_url}) for more information on how to solve this issue."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
tests/components/sonos/test_repairs.py
Normal file
47
tests/components/sonos/test_repairs.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Test repairs handling for Sonos."""
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from homeassistant.components.sonos.const import (
|
||||||
|
DOMAIN,
|
||||||
|
SCAN_INTERVAL,
|
||||||
|
SUB_FAIL_ISSUE_ID,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .conftest import SonosMockEvent
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_subscription_repair_issues(
|
||||||
|
hass: HomeAssistant, config_entry: MockConfigEntry, soco, zgs_discovery
|
||||||
|
):
|
||||||
|
"""Test repair issues handling for failed subscriptions."""
|
||||||
|
issue_registry = async_get_issue_registry(hass)
|
||||||
|
|
||||||
|
subscription = soco.zoneGroupTopology.subscribe.return_value
|
||||||
|
subscription.event_listener = Mock(address=("192.168.4.2", 1400))
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Ensure an issue is registered on subscription failure
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID)
|
||||||
|
|
||||||
|
# Ensure the issue still exists after reload
|
||||||
|
assert await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID)
|
||||||
|
|
||||||
|
# Ensure the issue has been removed after a successful subscription callback
|
||||||
|
variables = {"ZoneGroupState": zgs_discovery}
|
||||||
|
event = SonosMockEvent(soco, soco.zoneGroupTopology, variables)
|
||||||
|
sub_callback = subscription.callback
|
||||||
|
sub_callback(event)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID)
|
@ -17,6 +17,7 @@ async def test_fallback_to_polling(
|
|||||||
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
||||||
assert speaker.soco is soco
|
assert speaker.soco is soco
|
||||||
assert speaker._subscriptions
|
assert speaker._subscriptions
|
||||||
|
assert not speaker.subscriptions_failed
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
@ -29,7 +30,6 @@ async def test_fallback_to_polling(
|
|||||||
|
|
||||||
assert not speaker._subscriptions
|
assert not speaker._subscriptions
|
||||||
assert speaker.subscriptions_failed
|
assert speaker.subscriptions_failed
|
||||||
assert "falling back to polling" in caplog.text
|
|
||||||
assert "Activity on Zone A from SonosSpeaker.update_volume" in caplog.text
|
assert "Activity on Zone A from SonosSpeaker.update_volume" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user