From 08ab10d470f227ca2491f1c09edfcd94cb6ebadc Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 1 Sep 2022 04:49:36 -0400 Subject: [PATCH] Fix timezone edge cases for Unifi Protect media source (#77636) * Fixes timezone edge cases for Unifi Protect media source * linting --- .../components/unifiprotect/media_source.py | 19 ++- .../unifiprotect/test_media_source.py | 133 ++++++++++++++++-- 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 58b14ab9b3b..4910c18cf5f 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -101,12 +101,12 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: @callback -def _get_start_end(hass: HomeAssistant, start: datetime) -> tuple[datetime, datetime]: +def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]: start = dt_util.as_local(start) end = dt_util.now() - start = start.replace(day=1, hour=1, minute=0, second=0, microsecond=0) - end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + start = start.replace(day=1, hour=0, minute=0, second=1, microsecond=0) + end = end.replace(day=1, hour=0, minute=0, second=2, microsecond=0) return start, end @@ -571,9 +571,16 @@ class ProtectMediaSource(MediaSource): if not build_children: return source - month = start.month + 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(month=start.month + 1) - timedelta(days=1) + end = min(recording_end, end) + children = [self._build_days(data, camera_id, event_type, start, is_all=True)] - while start.month == month: + while start <= end: children.append( self._build_days(data, camera_id, event_type, start, is_all=False) ) @@ -702,7 +709,7 @@ class ProtectMediaSource(MediaSource): self._build_recent(data, camera_id, event_type, 30), ] - start, end = _get_start_end(self.hass, data.api.bootstrap.recording_start) + start, end = _get_month_start_end(data.api.bootstrap.recording_start) while end > start: children.append(self._build_month(data, camera_id, event_type, end.date())) end = (end - timedelta(days=1)).replace(day=1) diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index bb3bc8aa345..74a007e0ba0 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -4,7 +4,9 @@ from datetime import datetime, timedelta from ipaddress import IPv4Address from unittest.mock import AsyncMock, Mock, patch +from freezegun import freeze_time import pytest +import pytz from pyunifiprotect.data import ( Bootstrap, Camera, @@ -28,6 +30,7 @@ from homeassistant.components.unifiprotect.media_source import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .conftest import MockUFPFixture from .utils import init_entry @@ -430,13 +433,52 @@ async def test_browse_media_event_type( assert browse.children[3].identifier == "test_id:browse:all:smart" +ONE_MONTH_SIMPLE = ( + datetime( + year=2022, + month=9, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_SIMPLE = ( + datetime( + year=2022, + month=8, + day=31, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_SIMPLE, TWO_MONTH_SIMPLE], +) +@freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_time( - hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, ): """Test browsing time selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + end = datetime.fromisoformat("2022-09-15 03:00:00-07:00") + end_local = dt_util.as_local(end) + + 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) @@ -449,17 +491,89 @@ async def test_browse_media_time( assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" assert browse.identifier == base_id - assert len(browse.children) == 4 + assert len(browse.children) == 3 + months assert browse.children[0].title == "Last 24 Hours" assert browse.children[0].identifier == f"{base_id}:recent:1" assert browse.children[1].title == "Last 7 Days" assert browse.children[1].identifier == f"{base_id}:recent:7" assert browse.children[2].title == "Last 30 Days" assert browse.children[2].identifier == f"{base_id}:recent:30" - assert browse.children[3].title == f"{fixed_now.strftime('%B %Y')}" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" assert ( browse.children[3].identifier - == f"{base_id}:range:{fixed_now.year}:{fixed_now.month}" + == f"{base_id}:range:{end_local.year}:{end_local.month}" + ) + + +ONE_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=8, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=7, + day=31, + hour=21, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_TIMEZONE, TWO_MONTH_TIMEZONE], +) +@freeze_time("2022-08-31 21:00:00-07:00") +async def test_browse_media_time_timezone( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, +): + """Test browsing time selector level media.""" + + end = datetime.fromisoformat("2022-08-31 21:00:00-07:00") + end_local = dt_util.as_local(end) + + 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" + 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 == f"UnifiProtect > {doorbell.name} > All Events" + assert browse.identifier == base_id + assert len(browse.children) == 3 + months + assert browse.children[0].title == "Last 24 Hours" + assert browse.children[0].identifier == f"{base_id}:recent:1" + assert browse.children[1].title == "Last 7 Days" + assert browse.children[1].identifier == f"{base_id}:recent:7" + assert browse.children[2].title == "Last 30 Days" + assert browse.children[2].identifier == f"{base_id}:recent:30" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" + assert ( + browse.children[3].identifier + == f"{base_id}:range:{end_local.year}:{end_local.month}" ) @@ -599,13 +713,14 @@ async def test_browse_media_eventthumb( assert browse.media_class == MEDIA_CLASS_IMAGE +@freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_day( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime ): """Test browsing day selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + 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) @@ -623,7 +738,7 @@ async def test_browse_media_day( == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" ) assert browse.identifier == base_id - assert len(browse.children) in (29, 30, 31, 32) + assert len(browse.children) == 14 assert browse.children[0].title == "Whole Month" assert browse.children[0].identifier == f"{base_id}:all"