Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions homeassistant/components/collection_image/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_MEDIA): MediaSelector({"accept": ["directory"]}),
vol.Required(CONF_MEDIA): MediaSelector(
{"accept": ["directory"], "multiple": True}
),
Comment thread
karwosts marked this conversation as resolved.
}
)

Expand All @@ -28,24 +30,37 @@ async def async_step_user(
"""Handle the initial step."""
errors: dict[str, str] = {}
placeholders: dict[str, str] = {}
found_pictures = False
title = "Unnamed collection"
if user_input is not None:
user_media = user_input[CONF_MEDIA]
try:
browse = await async_browse_media(
self.hass, user_media["media_content_id"]
)
except BrowseError as err:
errors["media"] = "failed_browse"
placeholders["error"] = str(err)
else:
if browse.children and any(
item.media_class == MediaClass.IMAGE for item in browse.children
):
user_media_list = user_input[CONF_MEDIA]
for user_media in user_media_list:
Comment thread
karwosts marked this conversation as resolved.
try:
browse = await async_browse_media(
self.hass, user_media["media_content_id"]
)
except BrowseError as err:
errors["media"] = "failed_browse"
placeholders["error"] = str(err)
break
else:
if (
not found_pictures
and browse.children
and any(
item.media_class == MediaClass.IMAGE
for item in browse.children
)
):
found_pictures = True
if browse.title:
title = f"{browse.title} collection"
if "media" not in errors:
if found_pictures:
return self.async_create_entry(
title=f"{browse.title or 'Unnamed'} collection",
title=title,
data=user_input,
)

errors["media"] = "selected_media_no_images"

return self.async_show_form(
Expand Down
115 changes: 68 additions & 47 deletions homeassistant/components/collection_image/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from homeassistant.components.image import ImageEntity
from homeassistant.components.media_player import (
BrowseError,
BrowseMedia,
MediaClass,
async_process_play_media_url,
)
Expand Down Expand Up @@ -36,11 +37,15 @@ async def async_setup_entry(
) -> 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_id=media["media_content_id"],
media_content_ids=content_ids,
unique_id=entry.entry_id,
hass=hass,
)
Expand All @@ -58,7 +63,7 @@ class CollectionImageImageEntity(ImageEntity):
def __init__(
self,
name: str,
media_content_id: str,
media_content_ids: list[str],
unique_id: str,
hass: HomeAssistant,
) -> None:
Expand All @@ -67,11 +72,10 @@ def __init__(
self.path = None
self._attr_unique_id = unique_id
self._attr_name = name
self.media_content_id = media_content_id
self.media_content_ids = media_content_ids

async def get_next_image(self) -> None:
"""Update the image entity with the next image from the source media."""

"""Update the image entity with a random image from configured media sources."""
self._cached_image = None

def set_unavailable() -> None:
Expand All @@ -81,59 +85,76 @@ def set_unavailable() -> None:
self._attr_image_url = UNDEFINED
self.async_write_ha_state()

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
images: list[BrowseMedia] = []

if media.children and (
filtered := [
item for item in media.children if item.media_class == MediaClass.IMAGE
]
):
child = random.choice(filtered)
for media_content_id in self.media_content_ids:
try:
resolved = await async_resolve_media(
self.hass, child.media_content_id, self.entity_id
)
except Unresolvable as err:
media = await async_browse_media(self.hass, media_content_id)
except BrowseError 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
_LOGGER.info(
"%s: Unable to browse %s: %s",
self.entity_id,
media_content_id,
err,
)
continue
Comment thread
karwosts marked this conversation as resolved.

if media.children:
images.extend(
item
for item in media.children
if item.media_class == MediaClass.IMAGE
)
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:

if not images:
if not self._unavailable_logged:
_LOGGER.info(
"%s: Has become available again",
"%s: No valid images in %s",
self.entity_id,
Comment on lines +110 to 114

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's worthwhile to add extra complexity to avoid just a single extra log line in a pathological case. Right now we get one log per unbrowseable directory, and an overall error if we can't find anything at all. That seems reasonable to me.

self.media_content_ids,
)
self._unavailable_logged = False
self.async_write_ha_state()
set_unavailable()
return

if not self._unavailable_logged:
_LOGGER.info(
"%s: No valid images in %s",
child = random.choice(images)

try:
resolved = await async_resolve_media(
self.hass,
child.media_content_id,
self.entity_id,
self.media_content_id,
)
set_unavailable()
return
except Unresolvable as err:
if not self._unavailable_logged:
_LOGGER.info(
"%s: Unable to resolve %s: %s",
self.entity_id,
child.media_content_id,
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()

@override
async def async_added_to_hass(self) -> None:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/image/media_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async def async_browse_media(
can_expand=False,
)
for image in self.hass.data[DATA_COMPONENT].entities
if self.hass.states.get(image.entity_id)
Comment thread
karwosts marked this conversation as resolved.
Outdated
Comment thread
karwosts marked this conversation as resolved.
Outdated
]

return BrowseMediaSource(
Expand Down
50 changes: 47 additions & 3 deletions tests/components/collection_image/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from homeassistant.components.collection_image.const import DOMAIN
from homeassistant.components.media_player import BrowseMedia, MediaClass
from homeassistant.components.media_source import BrowseMediaSource, PlayMedia
from homeassistant.core import HomeAssistant

from .const import TEST_IMAGE

Expand All @@ -30,7 +31,7 @@ def config_entry() -> MockConfigEntry:

@pytest.fixture
def browse_media_result() -> BrowseMediaSource:
"""Return a default collection containing one image."""
"""Return the default collection containing one image."""
return BrowseMediaSource(
domain=None,
identifier=None,
Expand Down Expand Up @@ -61,12 +62,55 @@ def browse_media_result() -> BrowseMediaSource:


@pytest.fixture
def mock_media_source(browse_media_result: BrowseMediaSource):
def browse_media_result_2() -> BrowseMediaSource:
"""Return a collection containing three images."""
return BrowseMediaSource(
domain=None,
identifier=None,
media_class="",
media_content_type="",
title="Three images",
can_play=False,
can_expand=True,
children=[
BrowseMedia(
media_class=MediaClass.IMAGE,
media_content_id=f"media-source://mymedia_2/photo_{number}",
media_content_type="image/png",
title=f"picture {number}",
can_play=True,
can_expand=False,
)
for number in range(1, 4)
],
)


@pytest.fixture
def mock_media_source(
browse_media_result: BrowseMediaSource,
browse_media_result_2: BrowseMediaSource,
):
"""Mock browsing and resolving the configured media source."""

async def browse_side_effect(
_hass: HomeAssistant,
media_content_id,
*,
content_filter=None,
):
if media_content_id == "media-source://mymedia":
return browse_media_result

if media_content_id == "media-source://mymedia_2":
return browse_media_result_2

raise ValueError(f"Unexpected media content ID: {media_content_id}")

with (
patch(
"homeassistant.components.collection_image.image.async_browse_media",
new=AsyncMock(return_value=browse_media_result),
new=AsyncMock(side_effect=browse_side_effect),
) as mock_browse,
patch(
"homeassistant.components.collection_image.image.async_resolve_media",
Expand Down
40 changes: 24 additions & 16 deletions tests/components/collection_image/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,24 @@ async def _assert_successful_configure(
result = await hass.config_entries.flow.async_configure(
previous_step["flow_id"],
{
"media": {
"media_content_id": "media-source://mymedia",
"media_content_type": "",
},
"media": [
{
"media_content_id": "media-source://mymedia",
"media_content_type": "",
}
],
},
)

assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == "My pictures collection"
assert result.get("data") == {
"media": {
"media_content_id": "media-source://mymedia",
"media_content_type": "",
},
"media": [
{
"media_content_id": "media-source://mymedia",
"media_content_type": "",
}
],
}
assert len(mock_setup_entry.mock_calls) == 1

Expand Down Expand Up @@ -105,10 +109,12 @@ async def test_config_flow_with_error(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"media": {
"media_content_id": "media-source://mymedia_empty",
"media_content_type": "",
},
"media": [
{
"media_content_id": "media-source://mymedia_empty",
"media_content_type": "",
}
],
},
)
await hass.async_block_till_done()
Expand Down Expand Up @@ -141,10 +147,12 @@ async def test_config_flow_with_exception(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"media": {
"media_content_id": "media-source://mymedia",
"media_content_type": "",
},
"media": [
{
"media_content_id": "media-source://mymedia",
"media_content_type": "",
}
],
},
)
await hass.async_block_till_done()
Expand Down
Loading
Loading