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 asyncio import timeout
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from http import HTTPStatus
import logging import logging
from pysqueezebox import Player, Server from pysqueezebox import Player, Server
@ -16,7 +17,11 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant 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 import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
@ -93,15 +98,61 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
status = await lms.async_query( status = await lms.async_query(
"serverstatus", "-", "-", "prefs:libraryname" "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( raise ConfigEntryNotReady(
f"Error communicating config not read for {host}" translation_domain=DOMAIN,
translation_key="init_timeout",
translation_placeholders={
"host": str(host),
},
) from err ) from err
if not status: 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) _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] lms.uuid = status[STATUS_QUERY_UUID]
_LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid) _LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid)
lms.name = ( lms.name = (

View File

@ -158,6 +158,18 @@
} }
}, },
"exceptions": { "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": { "invalid_announce_media_type": {
"message": "Only type 'music' can be played as announcement (received type {media_type})." "message": "Only type 'music' can be played as announcement (received type {media_type})."
}, },

View File

@ -1,7 +1,9 @@
"""Test squeezebox initialization.""" """Test squeezebox initialization."""
from http import HTTPStatus
from unittest.mock import patch from unittest.mock import patch
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry 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) 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"
)