Fix OpenDisplay upload_image timeout with image and camera entities (#183974)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Josef Zweck
2026-10-05 11:54:54 +00:00
committed by Franck Nijhof
co-authored by Copilot
parent 265c7d9530
commit cbf7ca6601
2 changed files with 89 additions and 10 deletions
@@ -21,6 +21,7 @@ from opendisplay import (
from PIL import Image as PILImage, ImageOps
import probatio
from homeassistant.components import camera, image as image_component
from homeassistant.components.bluetooth import (
BluetoothReachabilityIntent,
async_address_reachability_diagnostics,
@@ -128,6 +129,31 @@ async def _async_download_image(hass: HomeAssistant, url: str) -> PILImage.Image
return await hass.async_add_executor_job(_load_image_from_bytes, data)
async def _async_get_pil_image(
hass: HomeAssistant, image_data: dict[str, Any]
) -> PILImage.Image:
"""Return a PIL Image for the selected media."""
media_content_id: str = image_data["media_content_id"]
# Camera and image entities resolve to never-ending MJPEG streams,
# so fetch a single snapshot from the entity instead.
for integration in camera, image_component:
media_source_prefix = f"media-source://{integration.DOMAIN}/"
if not media_content_id.startswith(media_source_prefix):
continue
entity_id = media_content_id.removeprefix(media_source_prefix)
snapshot = await integration.async_get_image(hass, entity_id)
return await hass.async_add_executor_job(
_load_image_from_bytes, snapshot.content
)
media = await async_resolve_media(hass, media_content_id, None)
if media.path is not None:
return await hass.async_add_executor_job(_load_image, str(media.path))
return await _async_download_image(hass, media.url)
async def _async_upload_image(call: ServiceCall) -> None:
"""Handle the upload_image service call."""
entry = _get_entry_for_device(call)
@@ -166,16 +192,7 @@ async def _async_upload_image(call: ServiceCall) -> None:
entry.runtime_data.upload_task = current
try:
media = await async_resolve_media(
call.hass, image_data["media_content_id"], None
)
if media.path is not None:
pil_image = await call.hass.async_add_executor_job(
_load_image, str(media.path)
)
else:
pil_image = await _async_download_image(call.hass, media.url)
pil_image = await _async_get_pil_image(call.hass, image_data)
raw_key = entry.data.get(CONF_ENCRYPTION_KEY)
if raw_key is not None and len(raw_key) != 32:
@@ -17,6 +17,8 @@ import probatio
import pytest
from homeassistant import config_entries
from homeassistant.components.camera import Image as CameraImage
from homeassistant.components.image import Image as ImageEntityImage
from homeassistant.components.opendisplay.const import CONF_ENCRYPTION_KEY, DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
@@ -128,6 +130,66 @@ async def test_upload_image_remote_url(
mock_upload_device.upload_image.assert_called_once()
@pytest.mark.parametrize(
("media_content_id", "patch_target", "entity_id", "image_cls"),
[
pytest.param(
"media-source://image/image.collection",
"homeassistant.components.image.async_get_image",
"image.collection",
ImageEntityImage,
id="image_entity",
),
pytest.param(
"media-source://camera/camera.front_door",
"homeassistant.components.camera.async_get_image",
"camera.front_door",
CameraImage,
id="camera_entity",
),
],
)
async def test_upload_image_from_entity(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_upload_device: MagicMock,
media_content_id: str,
patch_target: str,
entity_id: str,
image_cls: type[ImageEntityImage | CameraImage],
) -> None:
"""Test uploading a snapshot from an image or camera entity."""
device_id = _device_id(hass, mock_config_entry)
buf = io.BytesIO()
PILImage.new("RGB", (10, 10)).save(buf, format="PNG")
with (
patch(
patch_target, return_value=image_cls("image/png", buf.getvalue())
) as mock_get_image,
patch(
"homeassistant.components.opendisplay.services.async_resolve_media"
) as mock_resolve,
):
await hass.services.async_call(
DOMAIN,
"upload_image",
{
"device_id": device_id,
"image": {
"media_content_id": media_content_id,
"media_content_type": "image/png",
},
},
blocking=True,
)
mock_get_image.assert_awaited_once_with(hass, entity_id)
mock_resolve.assert_not_called()
mock_upload_device.upload_image.assert_called_once()
async def test_upload_image_invalid_device_id(
hass: HomeAssistant,
) -> None: