Fix Openhome players becoming unavailable between polls (#181238)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Barry Williams
2026-09-11 18:05:25 +00:00
committed by Franck Nijhof
co-authored by Claude Opus 5
parent a0759acb9a
commit f35f3afaec
6 changed files with 50 additions and 18 deletions
@@ -2,15 +2,15 @@
import logging
import aiohttp
from async_upnp_client.client import UpnpError
from openhomedevice.device import Device
from openhomedevice.exceptions import OpenhomeError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
@@ -37,11 +37,11 @@ async def async_setup_entry(
"""Set up the configuration config entry."""
_LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
device = await hass.async_add_executor_job(Device, config_entry.data[CONF_HOST])
device = Device(config_entry.data[CONF_HOST], session=async_get_clientsession(hass))
try:
await device.init()
except (TimeoutError, aiohttp.ClientError, UpnpError) as exc:
except OpenhomeError as exc:
raise ConfigEntryNotReady from exc
_LOGGER.debug("Initialised device: %s", device.uuid())
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["async_upnp_client", "openhomedevice"],
"requirements": ["openhomedevice==2.2.0"],
"requirements": ["openhomedevice==2.5"],
"ssdp": [
{
"st": "urn:av-openhome-org:service:Product:1"
@@ -5,8 +5,7 @@ import functools
import logging
from typing import Any, Concatenate, override
import aiohttp
from async_upnp_client.client import UpnpError
from openhomedevice.exceptions import OpenhomeError
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -58,7 +57,7 @@ type _ReturnFuncType[_T, **_P, _R] = Callable[
def catch_request_errors[_OpenhomeDeviceT: OpenhomeDevice, **_P, _R]() -> Callable[
[_FuncType[_OpenhomeDeviceT, _P, _R]], _ReturnFuncType[_OpenhomeDeviceT, _P, _R]
]:
"""Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
"""Catch OpenhomeError errors."""
def call_wrapper(
func: _FuncType[_OpenhomeDeviceT, _P, _R],
@@ -69,11 +68,11 @@ def catch_request_errors[_OpenhomeDeviceT: OpenhomeDevice, **_P, _R]() -> Callab
async def wrapper(
self: _OpenhomeDeviceT, *args: _P.args, **kwargs: _P.kwargs
) -> _R | None:
"""Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
"""Catch OpenhomeError errors."""
try:
return await func(self, *args, **kwargs)
except TimeoutError, aiohttp.ClientError, UpnpError:
_LOGGER.error("Error during call %s", func.__name__)
except OpenhomeError as err:
_LOGGER.error("Error during call %s: %s", func.__name__, err)
return None
return wrapper
@@ -167,7 +166,9 @@ class OpenhomeDevice(MediaPlayerEntity):
self._attr_state = MediaPlayerState.PLAYING
self._attr_available = True
except TimeoutError, aiohttp.ClientError, UpnpError:
except OpenhomeError as err:
if self._attr_available:
_LOGGER.warning("Error updating %s: %s", self.entity_id, err)
self._attr_available = False
# pylint: disable-next=home-assistant-action-swallowed-exception
@@ -261,8 +262,8 @@ class OpenhomeDevice(MediaPlayerEntity):
await self._device.invoke_pin(pin)
else:
_LOGGER.error("Pins service not supported")
except UpnpError:
_LOGGER.error("Error invoking pin %s", pin)
except OpenhomeError as err:
_LOGGER.error("Error invoking pin %s: %s", pin, err)
# pylint: disable-next=home-assistant-action-swallowed-exception
@catch_request_errors()
+2 -3
View File
@@ -3,8 +3,7 @@
import logging
from typing import Any, override
import aiohttp
from async_upnp_client.client import UpnpError
from openhomedevice.exceptions import OpenhomeError
from homeassistant.components.update import (
UpdateDeviceClass,
@@ -92,7 +91,7 @@ class OpenhomeUpdateEntity(UpdateEntity):
try:
if self.latest_version:
await self._device.update_firmware()
except (TimeoutError, aiohttp.ClientError, UpnpError) as err:
except OpenhomeError as err:
raise HomeAssistantError(
f"Error updating {self._device.device.friendly_name}: {err}"
) from err
+1 -1
View File
@@ -1815,7 +1815,7 @@ openai==2.45.0
openerz-api==0.3.0
# homeassistant.components.openhome
openhomedevice==2.2.0
openhomedevice==2.5
# homeassistant.components.openrgb
openrgb-python==0.3.6
+32
View File
@@ -0,0 +1,32 @@
"""Tests for the Openhome integration setup."""
from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.components.openhome.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from tests.common import MockConfigEntry
HOST = "http://localhost"
async def test_device_uses_shared_session(hass: HomeAssistant) -> None:
"""Test the device is given Home Assistant's shared aiohttp session."""
entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: HOST}, unique_id="uuid")
entry.add_to_hass(hass)
with (
patch("homeassistant.components.openhome.PLATFORMS", []),
patch("homeassistant.components.openhome.Device", MagicMock()) as mock_device,
):
mock_device.return_value.init = AsyncMock()
mock_device.return_value.uuid = MagicMock(return_value="uuid")
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
mock_device.assert_called_once_with(HOST, session=async_get_clientsession(hass))