Fix collection_image unavailable handling (#180973)

This commit is contained in:
karwosts
2026-09-01 11:33:52 +02:00
committed by Bram Kragten
parent ae16e7196c
commit cc34eb4546
4 changed files with 71 additions and 68 deletions
@@ -5,9 +5,10 @@ from pathlib import Path
import random
from typing import override
from homeassistant.components.image import ImageEntity
from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity
from homeassistant.components.media_player import (
BrowseError,
BrowseMedia,
MediaClass,
async_process_play_media_url,
)
@@ -51,8 +52,6 @@ async def async_setup_entry(
class CollectionImageImageEntity(ImageEntity):
"""Implement the image entity for Collection Image."""
_unavailable_logged: bool = False
path: Path | None
def __init__(
@@ -69,80 +68,80 @@ class CollectionImageImageEntity(ImageEntity):
self._attr_name = name
self.media_content_id = media_content_id
async def get_next_image(self) -> None:
"""Update the image entity with the next image from the source media."""
def set_unavailable(self) -> None:
"""Set the entity to unavailable state."""
self._attr_available = False
self.path = None
self._attr_image_url = UNDEFINED
self._cached_image = None
self.async_write_ha_state()
def set_unavailable() -> None:
self._unavailable_logged = True
self._attr_available = False
self.path = None
self._attr_image_url = UNDEFINED
self.async_write_ha_state()
async def get_valid_images(self) -> list[BrowseMedia]:
"""Given the configured media directory for the entity, get a list of all child images."""
try:
media = await async_browse_media(self.hass, self.media_content_id)
except BrowseError as err:
if not self._unavailable_logged:
_LOGGER.info("%s: %s", self.entity_id, str(err))
set_unavailable()
return
_LOGGER.warning("%s: %s", self.entity_id, str(err))
return []
if media.children and (
filtered := [
item for item in media.children if item.media_class == MediaClass.IMAGE
]
):
child = random.choice(filtered)
try:
resolved = await async_resolve_media(
self.hass, child.media_content_id, self.entity_id
)
except Unresolvable as err:
if not self._unavailable_logged:
_LOGGER.info("%s: %s", self.entity_id, str(err))
set_unavailable()
return
if resolved.url:
self.path = None
self._attr_image_url = async_process_play_media_url(
self.hass, resolved.url
)
else:
self.path = resolved.path
self._attr_image_url = UNDEFINED
self._attr_content_type = resolved.mime_type
self._attr_available = True
self._attr_image_last_updated = dt_util.utcnow()
if self._unavailable_logged:
_LOGGER.info(
"%s: Has become available again",
self.entity_id,
)
self._unavailable_logged = False
self.async_write_ha_state()
return
if not self._unavailable_logged:
_LOGGER.info(
images = [
item
for item in (media.children or [])
if item.media_class == MediaClass.IMAGE
]
if not images:
_LOGGER.warning(
"%s: No valid images in %s",
self.entity_id,
self.media_content_id,
)
set_unavailable()
return
return images
async def get_random_image(self) -> None:
"""Update the image entity with a random image from the source media."""
filtered = await self.get_valid_images()
if not filtered:
self.set_unavailable()
return
child = random.choice(filtered)
self._attr_available = True
await self.update_image(child.media_content_id)
async def update_image(self, image_id: str):
"""Update the entity from the image_id."""
self._cached_image = None
try:
resolved = await async_resolve_media(self.hass, image_id, self.entity_id)
except Unresolvable as err:
_LOGGER.warning("%s: %s", self.entity_id, str(err))
self._attr_image_last_updated = None
self.path = None
self._attr_image_url = UNDEFINED
self._attr_content_type = DEFAULT_CONTENT_TYPE
self.async_write_ha_state()
return
if resolved.url:
self.path = None
self._attr_image_url = async_process_play_media_url(self.hass, resolved.url)
else:
self.path = resolved.path
self._attr_image_url = UNDEFINED
self._attr_content_type = resolved.mime_type
self._attr_image_last_updated = dt_util.utcnow()
self.async_write_ha_state()
@override
async def async_added_to_hass(self) -> None:
"""Initialize the first image after entity has been created."""
async def get_next_image_on_start(_hass: HomeAssistant) -> None:
await self.get_next_image()
async def get_random_image_on_start(_hass: HomeAssistant) -> None:
await self.get_random_image()
self.async_on_remove(async_at_started(self.hass, get_next_image_on_start))
self.async_on_remove(async_at_started(self.hass, get_random_image_on_start))
@override
def image(self) -> bytes | None:
@@ -19,5 +19,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
SERVICE_SHUFFLE,
entity_domain=IMAGE_DOMAIN,
schema={},
func="get_next_image",
func="get_random_image",
)
@@ -9,7 +9,11 @@ import pytest
from homeassistant.components.image import Image, async_get_image
from homeassistant.components.media_source import BrowseMediaSource, PlayMedia
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, STATE_UNAVAILABLE
from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -216,7 +220,7 @@ async def test_unresolvable(
state = hass.states.get(DEFAULT_ENTITY_ID)
assert state and state.state == STATE_UNAVAILABLE
assert state and state.state == STATE_UNKNOWN
await hass.async_block_till_done(wait_background_tasks=True)
@@ -27,14 +27,14 @@ async def test_shuffle_action(
config_entry: MockConfigEntry,
mock_media_source,
) -> None:
"""Test that shuffle calls get_next_image on the target entity."""
"""Test that shuffle calls get_random_image on the target entity."""
await _setup_integration(hass, config_entry)
with patch.object(
CollectionImageImageEntity,
"get_next_image",
"get_random_image",
new_callable=AsyncMock,
) as mock_get_next_image:
) as mock_get_random_image:
await hass.services.async_call(
DOMAIN,
"shuffle",
@@ -42,4 +42,4 @@ async def test_shuffle_action(
blocking=True,
)
mock_get_next_image.assert_awaited_once()
mock_get_random_image.assert_awaited_once()