Update exception handling for initialization for Squeezebox (#144674)

* initial

* tests

* translate exceptions

* updates

* tests updates

* remove bare exception

* merge fix
This commit is contained in:
peteS-UK 2025-05-21 21:37:07 +01:00 committed by GitHub
parent e2b9e21c6a
commit fbab6741af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 128 additions and 4 deletions

View File

@ -3,6 +3,7 @@
from asyncio import timeout
from dataclasses import dataclass
from datetime import datetime
from http import HTTPStatus
import logging
from pysqueezebox import Player, Server
@ -16,7 +17,11 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import (
@ -93,15 +98,61 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
status = await lms.async_query(
"serverstatus", "-", "-", "prefs:libraryname"
)
except Exception as err:
except TimeoutError as err: # Specifically catch timeout
_LOGGER.warning("Timeout connecting to LMS %s: %s", host, err)
raise ConfigEntryNotReady(
f"Error communicating config not read for {host}"
translation_domain=DOMAIN,
translation_key="init_timeout",
translation_placeholders={
"host": str(host),
},
) from err
if not status:
raise ConfigEntryNotReady(f"Error Config Not read for {host}")
# pysqueezebox's async_query returns None on various issues,
# including HTTP errors where it sets lms.http_status.
http_status = getattr(lms, "http_status", "N/A")
if http_status == HTTPStatus.UNAUTHORIZED:
_LOGGER.warning("Authentication failed for Squeezebox server %s", host)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="init_auth_failed",
translation_placeholders={
"host": str(host),
},
)
# For other errors where status is None (e.g., server error, connection refused by server)
_LOGGER.warning(
"LMS %s returned no status or an error (HTTP status: %s). Retrying setup",
host,
http_status,
)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="init_get_status_failed",
translation_placeholders={
"host": str(host),
"http_status": str(http_status),
},
)
# If we are here, status is a valid dictionary
_LOGGER.debug("LMS Status for setup = %s", status)
# Check for essential keys in status before using them
if STATUS_QUERY_UUID not in status:
_LOGGER.error("LMS %s status response missing UUID", host)
# This is a non-recoverable error with the current server response
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="init_missing_uuid",
translation_placeholders={
"host": str(host),
},
)
lms.uuid = status[STATUS_QUERY_UUID]
_LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid)
lms.name = (

View File

@ -158,6 +158,18 @@
}
},
"exceptions": {
"init_timeout": {
"message": "Timeout connecting to LMS {host}."
},
"init_auth_failed": {
"message": "Authentication failed for {host)."
},
"init_get_status_failed": {
"message": "Failed to get status from LMS {host} (HTTP status: {http_status}). Will retry."
},
"init_missing_uuid": {
"message": "LMS {host} status response missing essential data (UUID)."
},
"invalid_announce_media_type": {
"message": "Only type 'music' can be played as announcement (received type {media_type})."
},

View File

@ -1,7 +1,9 @@
"""Test squeezebox initialization."""
from http import HTTPStatus
from unittest.mock import patch
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@ -21,3 +23,62 @@ async def test_init_api_fail(
),
):
assert not await hass.config_entries.async_setup(config_entry.entry_id)
async def test_init_timeout_error(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test init fail due to TimeoutError."""
# Setup component to raise TimeoutError
with (
patch(
"homeassistant.components.squeezebox.Server.async_query",
side_effect=TimeoutError,
),
):
assert not await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_init_unauthorized(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test init fail due to unauthorized error."""
# Setup component to simulate unauthorized response
with (
patch(
"homeassistant.components.squeezebox.Server.async_query",
return_value=False, # async_query returns False on auth failure
),
patch(
"homeassistant.components.squeezebox.Server", # Patch the Server class itself
autospec=True,
) as mock_server_instance,
):
mock_server_instance.return_value.http_status = HTTPStatus.UNAUTHORIZED
assert not await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_init_missing_uuid(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test init fail due to missing UUID in server status."""
# A response that is truthy but does not contain STATUS_QUERY_UUID
mock_status_without_uuid = {"name": "Test Server"}
with patch(
"homeassistant.components.squeezebox.Server.async_query",
return_value=mock_status_without_uuid,
) as mock_async_query:
# ConfigEntryError is raised, caught by setup, and returns False
assert not await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_ERROR
mock_async_query.assert_called_once_with(
"serverstatus", "-", "-", "prefs:libraryname"
)