Always ensure a new image when shuffling (#181564)

This commit is contained in:
karwosts
2026-09-08 07:10:31 +02:00
committed by GitHub
parent 384c153186
commit 9abd3a82cf
5 changed files with 124 additions and 5 deletions
@@ -53,6 +53,7 @@ class CollectionImageImageEntity(ImageEntity):
"""Implement the image entity for Collection Image."""
path: Path | None
_current_image_id: str | None = None
def __init__(
self,
@@ -105,12 +106,23 @@ class CollectionImageImageEntity(ImageEntity):
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 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)
@@ -122,6 +134,8 @@ class CollectionImageImageEntity(ImageEntity):
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
+19 -5
View File
@@ -13,9 +13,13 @@ from homeassistant.core import HomeAssistant
from .const import (
MOCK_MEDIA_DIR_URI_1,
MOCK_MEDIA_DIR_URI_2,
MOCK_MEDIA_DIR_URI_BROWSE_ERROR,
MOCK_MEDIA_DIR_URI_EMPTY,
MOCK_MEDIA_IMAGE_URI_1,
MOCK_MEDIA_IMAGE_URI_2,
MOCK_MEDIA_IMAGE_URI_3,
MOCK_MEDIA_IMAGE_URI_4,
TEST_IMAGE,
)
from .helpers import directory, image
@@ -60,6 +64,11 @@ def config_entry() -> MockConfigEntry:
@pytest.fixture
def media_source_state() -> MediaSourceState:
"""Return default configurable responses for the media-source mock."""
generic_resolve = PlayMedia(
url="",
mime_type="image/png",
path=TEST_IMAGE,
)
return MediaSourceState(
browse_results={
MOCK_MEDIA_DIR_URI_1: directory(
@@ -74,6 +83,12 @@ def media_source_state() -> MediaSourceState:
),
image(MOCK_MEDIA_IMAGE_URI_1),
),
MOCK_MEDIA_DIR_URI_2: directory(
"Three pictures",
image(MOCK_MEDIA_IMAGE_URI_2),
image(MOCK_MEDIA_IMAGE_URI_3),
image(MOCK_MEDIA_IMAGE_URI_4),
),
MOCK_MEDIA_DIR_URI_EMPTY: directory("Empty folder"),
},
browse_exceptions={
@@ -82,11 +97,10 @@ def media_source_state() -> MediaSourceState:
)
},
resolve_results={
MOCK_MEDIA_IMAGE_URI_1: PlayMedia(
url="",
mime_type="image/png",
path=TEST_IMAGE,
),
MOCK_MEDIA_IMAGE_URI_1: generic_resolve,
MOCK_MEDIA_IMAGE_URI_2: generic_resolve,
MOCK_MEDIA_IMAGE_URI_3: generic_resolve,
MOCK_MEDIA_IMAGE_URI_4: generic_resolve,
},
)
@@ -3,10 +3,15 @@
from pathlib import Path
TEST_IMAGE = Path(__file__).parent / "test.png"
TEST_IMAGE_2 = Path(__file__).parent / "test2.png"
DEFAULT_ENTITY_ID = "image.random_image"
MOCK_MEDIA_DIR_URI_1 = "media-source://mymedia"
MOCK_MEDIA_DIR_URI_2 = "media-source://mymedia_multi"
MOCK_MEDIA_DIR_URI_EMPTY = "media-source://mymedia_empty"
MOCK_MEDIA_DIR_URI_BROWSE_ERROR = "media-source://mymedia_error"
MOCK_MEDIA_IMAGE_URI_1 = "media-source://mymedia/photo"
MOCK_MEDIA_IMAGE_URI_2 = "media-source://mymedia/photo2"
MOCK_MEDIA_IMAGE_URI_3 = "media-source://mymedia/photo3"
MOCK_MEDIA_IMAGE_URI_4 = "media-source://mymedia/photo4"
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

@@ -22,10 +22,15 @@ from homeassistant.exceptions import HomeAssistantError
from .conftest import MediaSourceMocks, MediaSourceState
from .const import (
DEFAULT_ENTITY_ID,
MOCK_MEDIA_DIR_URI_2,
MOCK_MEDIA_DIR_URI_BROWSE_ERROR,
MOCK_MEDIA_DIR_URI_EMPTY,
MOCK_MEDIA_IMAGE_URI_1,
MOCK_MEDIA_IMAGE_URI_2,
MOCK_MEDIA_IMAGE_URI_3,
MOCK_MEDIA_IMAGE_URI_4,
TEST_IMAGE,
TEST_IMAGE_2,
)
from .helpers import config_entry_from_uri
@@ -33,6 +38,7 @@ from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
TEST_TIME = "2025-11-08T12:00:00+00:00"
TEST_TIME_NEXT = "2025-11-08T13:00:00+00:00"
async def _verify_path_image(
@@ -284,3 +290,83 @@ async def test_image_file_read_error(
client = await hass_client()
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
@pytest.mark.usefixtures("mock_media_source")
async def test_multi_shuffle(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
media_source_state: MediaSourceState,
) -> None:
"""Check that shuffling random image does not return the same image."""
media_source_state.resolve_results[MOCK_MEDIA_IMAGE_URI_4] = PlayMedia(
url="",
mime_type="image/png",
path=TEST_IMAGE_2,
)
config_entry = config_entry_from_uri(MOCK_MEDIA_DIR_URI_2)
with (
freeze_time(TEST_TIME),
patch(
"homeassistant.components.collection_image.image.random.choice",
return_value=media_source_state.browse_results[
MOCK_MEDIA_DIR_URI_2
].children[1],
) as mock_choice,
):
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert [image.media_content_id for image in mock_choice.call_args.args[0]] == [
MOCK_MEDIA_IMAGE_URI_2,
MOCK_MEDIA_IMAGE_URI_3,
MOCK_MEDIA_IMAGE_URI_4,
]
state = hass.states.get(DEFAULT_ENTITY_ID)
assert state and state.state == TEST_TIME
client = await hass_client()
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
assert resp.status == HTTPStatus.OK
assert resp.content_type == "image/png"
expected_data = await hass.async_add_executor_job(TEST_IMAGE.read_bytes)
body = await resp.read()
assert body == expected_data
with (
freeze_time(TEST_TIME_NEXT),
patch(
"homeassistant.components.collection_image.image.random.choice",
return_value=media_source_state.browse_results[
MOCK_MEDIA_DIR_URI_2
].children[2],
) as mock_choice,
):
await hass.services.async_call(
DOMAIN,
"shuffle",
{ATTR_ENTITY_ID: DEFAULT_ENTITY_ID},
blocking=True,
)
# On the second call, URI_3 will not be included as it is the current image.
assert [image.media_content_id for image in mock_choice.call_args.args[0]] == [
MOCK_MEDIA_IMAGE_URI_2,
MOCK_MEDIA_IMAGE_URI_4,
]
state = hass.states.get(DEFAULT_ENTITY_ID)
assert state and state.state == TEST_TIME_NEXT
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
assert resp.status == HTTPStatus.OK
assert resp.content_type == "image/png"
expected_data = await hass.async_add_executor_job(TEST_IMAGE_2.read_bytes)
body = await resp.read()
assert body == expected_data