Add collection_image.shuffle service (#180075)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
karwosts
2026-08-26 07:40:11 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent becedbc788
commit 46deaf3c0d
7 changed files with 176 additions and 9 deletions
@@ -3,9 +3,22 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .services import async_setup_services
PLATFORMS: list[Platform] = [Platform.IMAGE]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up is called when Home Assistant is loading our component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
@@ -0,0 +1,7 @@
{
"services": {
"shuffle": {
"service": "mdi:shuffle"
}
}
}
@@ -1,8 +1,6 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions.
action-setup: done
appropriate-polling:
status: exempt
comment: Integration does not poll.
@@ -15,9 +13,7 @@ rules:
dependency-transparency:
status: exempt
comment: No dependencies.
docs-actions:
status: exempt
comment: No actions.
docs-actions: done
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
@@ -48,9 +44,7 @@ rules:
comment: Integration does not connect to any physical object or service.
# Silver
action-exceptions:
status: exempt
comment: No current actions.
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
@@ -0,0 +1,23 @@
"""Collection image services."""
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import service
from .const import DOMAIN
SERVICE_SHUFFLE = "shuffle"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register Collection image services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SHUFFLE,
entity_domain=IMAGE_DOMAIN,
schema={},
func="get_next_image",
)
@@ -0,0 +1,4 @@
shuffle:
target:
entity:
integration: collection_image
@@ -22,5 +22,11 @@
"message": "Error reading image from {path}: {error}"
}
},
"services": {
"shuffle": {
"description": "Update the image entity to a random image from the configured media.",
"name": "Shuffle"
}
},
"title": "Collection Image"
}
@@ -0,0 +1,120 @@
"""Tests for the Collection Image integration services."""
from pathlib import Path
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.collection_image.const import DOMAIN
from homeassistant.components.collection_image.image import CollectionImageImageEntity
from homeassistant.components.media_player import BrowseMedia, MediaClass
from homeassistant.components.media_source import BrowseMediaSource, PlayMedia
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
ENTITY_ID = "image.random_image"
TEST_IMAGE = Path(__file__).parent / "test.png"
@pytest.fixture
def config_entry() -> MockConfigEntry:
"""Return a Collection Image config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="Random Image",
data={
"media": {
"media_content_id": "media-source://mymedia",
"media_content_type": "",
}
},
)
@pytest.fixture
def browse_media() -> BrowseMediaSource:
"""Return a media folder containing one image and one non-image."""
return BrowseMediaSource(
domain=None,
identifier=None,
media_class="",
media_content_type="",
title="",
can_play=False,
can_expand=True,
children=[
BrowseMedia(
media_class=MediaClass.MUSIC,
media_content_id="media-source://mymedia/music",
media_content_type="audio/mp3",
title="a music track",
can_play=True,
can_expand=False,
),
BrowseMedia(
media_class=MediaClass.IMAGE,
media_content_id="media-source://mymedia/photo",
media_content_type="image/png",
title="a picture",
can_play=True,
can_expand=False,
),
],
)
@pytest.fixture
def mock_media_source(browse_media: BrowseMediaSource):
"""Mock browsing and resolving the configured media source."""
with (
patch(
"homeassistant.components.collection_image.image.async_browse_media",
new=AsyncMock(return_value=browse_media),
) as mock_browse,
patch(
"homeassistant.components.collection_image.image.async_resolve_media",
new=AsyncMock(
return_value=PlayMedia(
url="",
mime_type="image/png",
path=TEST_IMAGE,
)
),
) as mock_resolve,
):
yield mock_browse, mock_resolve
async def _setup_integration(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Set up the Collection Image integration."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async def test_shuffle_action(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_media_source,
) -> None:
"""Test that shuffle calls get_next_image on the target entity."""
await _setup_integration(hass, config_entry)
with patch.object(
CollectionImageImageEntity,
"get_next_image",
new_callable=AsyncMock,
) as mock_get_next_image:
await hass.services.async_call(
DOMAIN,
"shuffle",
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
mock_get_next_image.assert_awaited_once()