diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index d3b627f8292..b73da5fbfd6 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -339,25 +339,43 @@ async def async_get_still_stream( return True event = asyncio.Event() + timed_out = False @callback def _async_image_state_update(_event: Event[EventStateChangedData]) -> None: """Write image to stream.""" event.set() + @callback + def _async_timeout_reached() -> None: + """Handle timeout.""" + nonlocal timed_out + timed_out = True + event.set() + hass = request.app[KEY_HASS] + loop = hass.loop remove = async_track_state_change_event( hass, image_entity.entity_id, _async_image_state_update, ) + timeout_handle = None try: while True: if not await _write_frame(): return response + # Ensure that an image is sent at least every 55 seconds + # Otherwise some devices go blank + timeout_handle = loop.call_later(55, _async_timeout_reached) await event.wait() event.clear() + if not timed_out: + timeout_handle.cancel() + timed_out = False finally: + if timeout_handle: + timeout_handle.cancel() remove() diff --git a/tests/components/image/test_init.py b/tests/components/image/test_init.py index 75816e1350f..717e82a652d 100644 --- a/tests/components/image/test_init.py +++ b/tests/components/image/test_init.py @@ -1,11 +1,12 @@ """The tests for the image component.""" -import datetime +from datetime import datetime from http import HTTPStatus import ssl from unittest.mock import MagicMock, patch from aiohttp import hdrs +from freezegun.api import FrozenDateTimeFactory import httpx import pytest import respx @@ -24,7 +25,12 @@ from .conftest import ( MockURLImageEntity, ) -from tests.common import MockModule, mock_integration, mock_platform +from tests.common import ( + MockModule, + async_fire_time_changed, + mock_integration, + mock_platform, +) from tests.typing import ClientSessionGenerator @@ -292,7 +298,9 @@ async def test_fetch_image_url_wrong_content_type( async def test_image_stream( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test image stream.""" @@ -323,18 +331,26 @@ async def test_image_stream( assert not resp.closed assert resp.status == HTTPStatus.OK - mock_image.image_last_updated = datetime.datetime.now() + mock_image.image_last_updated = datetime.now() mock_image.async_write_ha_state() # Two blocks to ensure the frame is written await hass.async_block_till_done() await hass.async_block_till_done() + with patch.object(mock_image, "async_image", return_value=b"") as mock: + # Simulate a "keep alive" frame + freezer.tick(55) + async_fire_time_changed(hass) + # Two blocks to ensure the frame is written + await hass.async_block_till_done() + await hass.async_block_till_done() + mock.assert_called_once() + with patch.object(mock_image, "async_image", return_value=None): - mock_image.image_last_updated = datetime.datetime.now() - mock_image.async_write_ha_state() + freezer.tick(55) + async_fire_time_changed(hass) # Two blocks to ensure the frame is written await hass.async_block_till_done() await hass.async_block_till_done() await close_future - assert resp.closed