mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 17:04:04 -04:00
Always ensure a new image when shuffling (#181564)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user