mirror of
https://github.com/home-assistant/core.git
synced 2026-09-13 04:01:03 +01:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
247 lines
7.8 KiB
Python
247 lines
7.8 KiB
Python
"""Support for Collection Image image."""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
import random
|
|
from typing import Literal, override
|
|
|
|
from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity
|
|
from homeassistant.components.media_player import (
|
|
BrowseError,
|
|
BrowseMedia,
|
|
MediaClass,
|
|
async_process_play_media_url,
|
|
)
|
|
from homeassistant.components.media_source import (
|
|
Unresolvable,
|
|
async_browse_media,
|
|
async_resolve_media,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.helpers.start import async_at_started
|
|
from homeassistant.helpers.typing import UNDEFINED
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from .const import CONF_MEDIA, DOMAIN
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Collection Image image entities."""
|
|
media = entry.data[CONF_MEDIA]
|
|
if isinstance(media, dict):
|
|
content_ids = [media["media_content_id"]]
|
|
else:
|
|
content_ids = [item["media_content_id"] for item in media]
|
|
async_add_entities(
|
|
[
|
|
CollectionImageImageEntity(
|
|
name=entry.title,
|
|
media_content_ids=content_ids,
|
|
unique_id=entry.entry_id,
|
|
hass=hass,
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
class CollectionImageImageEntity(ImageEntity):
|
|
"""Implement the image entity for Collection Image."""
|
|
|
|
path: Path | None
|
|
_current_image_id: str | None = None
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
media_content_ids: list[str],
|
|
unique_id: str,
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Initialize the entity."""
|
|
super().__init__(hass)
|
|
self.path = None
|
|
self._attr_unique_id = unique_id
|
|
self._attr_name = name
|
|
self.media_content_ids = media_content_ids
|
|
|
|
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()
|
|
|
|
async def get_valid_images(self) -> list[BrowseMedia]:
|
|
"""Given the configured media directory for the entity, get a list of all child images."""
|
|
|
|
images: list[BrowseMedia] = []
|
|
|
|
for media_content_id in self.media_content_ids:
|
|
try:
|
|
media = await async_browse_media(self.hass, media_content_id)
|
|
except BrowseError as err:
|
|
_LOGGER.warning("%s: %s", self.entity_id, str(err))
|
|
continue
|
|
|
|
directory_images = [
|
|
item
|
|
for item in (media.children or [])
|
|
if item.media_class == MediaClass.IMAGE
|
|
]
|
|
if directory_images:
|
|
images.extend(directory_images)
|
|
else:
|
|
_LOGGER.warning(
|
|
"%s: No valid images in %s",
|
|
self.entity_id,
|
|
media_content_id,
|
|
)
|
|
|
|
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
|
|
|
|
# Don't allow random shuffle to return the same image we are currently viewing.
|
|
if self._current_image_id:
|
|
filtered_new = [
|
|
item
|
|
for item in filtered
|
|
if item.media_content_id != self._current_image_id
|
|
]
|
|
if filtered_new:
|
|
filtered = filtered_new
|
|
|
|
child = random.choice(filtered)
|
|
self._attr_available = True
|
|
await self.update_image(child.media_content_id)
|
|
|
|
async def get_first_image(self) -> None:
|
|
"""Get the first image."""
|
|
await self._get_image_at_position(0)
|
|
|
|
async def get_last_image(self) -> None:
|
|
"""Get the last image."""
|
|
await self._get_image_at_position(-1)
|
|
|
|
async def get_next_image(self, wrap: bool = False) -> None:
|
|
"""Get the next image."""
|
|
await self._get_next_sequential_image(False, wrap)
|
|
|
|
async def get_previous_image(self, wrap: bool = False) -> None:
|
|
"""Get the previous image."""
|
|
await self._get_next_sequential_image(True, wrap)
|
|
|
|
async def _get_image_at_position(self, position: Literal[0, -1]) -> None:
|
|
"""Get the first or last image."""
|
|
|
|
filtered = await self.get_valid_images()
|
|
if not filtered:
|
|
self.set_unavailable()
|
|
return
|
|
|
|
child = filtered[position]
|
|
self._attr_available = True
|
|
await self.update_image(child.media_content_id)
|
|
|
|
async def _get_next_sequential_image(
|
|
self, reverse: bool = False, wrap: bool = False
|
|
) -> None:
|
|
"""Get the next or previous image."""
|
|
|
|
filtered = await self.get_valid_images()
|
|
if not filtered:
|
|
self.set_unavailable()
|
|
return
|
|
|
|
current_index = next(
|
|
(
|
|
i
|
|
for i, item in enumerate(filtered)
|
|
if item.media_content_id == self._current_image_id
|
|
),
|
|
None,
|
|
)
|
|
if current_index is None:
|
|
new_index = -1 if reverse else 0
|
|
else:
|
|
new_index = current_index + (-1 if reverse else 1)
|
|
if new_index < 0:
|
|
new_index = -1 if wrap else 0
|
|
elif new_index >= len(filtered):
|
|
new_index = 0 if wrap else (len(filtered) - 1)
|
|
|
|
child = filtered[new_index]
|
|
self._attr_available = True
|
|
await self.update_image(child.media_content_id)
|
|
|
|
async def update_image(self, image_id: str) -> None:
|
|
"""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
|
|
finally:
|
|
self._current_image_id = image_id
|
|
|
|
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_random_image_on_start(_hass: HomeAssistant) -> None:
|
|
await self.get_random_image()
|
|
|
|
self.async_on_remove(async_at_started(self.hass, get_random_image_on_start))
|
|
|
|
@override
|
|
def image(self) -> bytes | None:
|
|
"""Return bytes of image."""
|
|
if self.path:
|
|
try:
|
|
return self.path.read_bytes()
|
|
except OSError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="image_read_error",
|
|
translation_placeholders={
|
|
"path": str(self.path),
|
|
"error": str(err),
|
|
},
|
|
) from err
|
|
|
|
return None
|