diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index ab36280c0a42..1863e4c1dd05 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -601,21 +601,23 @@ class ProtectMediaSource(MediaSource): if not build_children: return source - if data.api.bootstrap.recording_start is not None: - recording_start = data.api.bootstrap.recording_start.date() - start = max(recording_start, start) - - recording_end = dt_util.now().date() - - end = start.replace(day=monthrange(start.year, start.month)[1]) - end = min(recording_end, end) + # The requested month bounds the days offered: recording may have + # started partway into it, and it cannot reach past today. Keep those + # bounds off `start` so every child stays within the month asked for. + end = min( + dt_util.now().date(), + start.replace(day=monthrange(start.year, start.month)[1]), + ) + day = start + if (recording_start := data.api.bootstrap.recording_start) is not None: + day = max(dt_util.as_local(recording_start).date(), day) children = [self._build_days(data, camera_id, event_type, start, is_all=True)] - while start <= end: + while day <= end: children.append( - self._build_days(data, camera_id, event_type, start, is_all=False) + self._build_days(data, camera_id, event_type, day, is_all=False) ) - start = start + timedelta(hours=24) + day = day + timedelta(days=1) camera: Camera | None = None if camera_id != "all": diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 3eef784b9a89..720c6ab6c7e5 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -1127,3 +1127,77 @@ async def test_public_only_entry_skipped( source = await async_get_media_source(hass) assert source.data_sources == {} + + +@pytest.mark.freeze_time("2022-09-15 03:00:00-07:00") +async def test_browse_media_month_before_recording_started( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +) -> None: + """Test browsing a month that is entirely before recording started.""" + start = datetime.fromisoformat("2022-09-03 03:00:00-07:00") + ufp.api.bootstrap._recording_start = dt_util.as_utc(start) + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = f"test_id:browse:{doorbell.id}:all:range:2022:2" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title.endswith("February 2022") + assert browse.identifier == base_id + # Nothing was recorded that month, so only the whole month selector is + # offered, and no days belonging to the month recording started in. + assert len(browse.children) == 1 + assert browse.children[0].title == "Whole Month" + assert browse.children[0].identifier == f"{base_id}:all" + + +@pytest.mark.freeze_time("2022-09-15 03:00:00-07:00") +async def test_browse_media_month_without_recording_start( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +) -> None: + """Test browsing a month when the console reports no recording start.""" + # Falls back to the earliest camera recording, so clear that as well. + doorbell.stats.video.recording_start = None + ufp.api.bootstrap._recording_start = None + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = f"test_id:browse:{doorbell.id}:all:range:2022:9" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert browse.identifier == base_id + + +@pytest.mark.freeze_time("2022-09-15 03:00:00-07:00") +async def test_browse_media_recording_started_on_last_local_day( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +) -> None: + """Test the last day of a month is offered across a UTC date boundary.""" + await hass.config.async_set_time_zone("US/Pacific") + + # 2022-08-31 19:00 local, which is already 2022-09-01 in UTC. + ufp.api.bootstrap._recording_start = datetime.fromisoformat( + "2022-09-01 02:00:00+00:00" + ) + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = f"test_id:browse:{doorbell.id}:all:range:2022:8" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert [child.identifier for child in browse.children] == [ + f"{base_id}:all", + f"{base_id}:31", + ]