From 3ab2e8169b138527b5e844316825db9edd994333 Mon Sep 17 00:00:00 2001 From: Austin Tyler Conn Date: Mon, 20 Jul 2026 14:23:24 +0000 Subject: [PATCH 1/3] Add sync module snooze support - Add snoozed async property to BlinkSyncModule - Add async_snooze() method to BlinkSyncModule - Add request_sync_snooze() API function - Tests --- blinkpy/api.py | 15 ++++++++++ blinkpy/sync_module.py | 34 +++++++++++++++++++++ tests/test_api.py | 8 +++++ tests/test_sync_module.py | 63 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+) diff --git a/blinkpy/api.py b/blinkpy/api.py index d3221d71..997827b1 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -1065,3 +1065,18 @@ async def oauth_refresh_token(auth, refresh_token, hardware_id): return await response.json() return None + + +async def request_sync_snooze(blink, network, data=None): + """ + Update sync snooze configuration. + + :param blink: Blink instance. + :param network: Sync module network id. + :param data: string w/JSON dict of parameters/values to update + """ + url = ( + f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" + f"/networks/{network}/snooze" + ) + return await http_post(blink, url, json=True, data=data) diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 63b6aef6..3ff7c038 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -127,6 +127,40 @@ async def async_arm(self, value): return await api.request_system_arm(self.blink, self.network_id) return await api.request_system_disarm(self.blink, self.network_id) + @property + async def snoozed(self): + """Return snooze status as boolean.""" + res = None + try: + res = await api.request_sync_snooze( + self.blink, + self.network_id, + ) + if res is None: + return False + snooze_value = res.get("snooze_till") + return bool(snooze_value) + except TypeError: + return False + except KeyError as e: + _LOGGER.warning( + "Exception %s: Encountered a likely malformed response " + "from the snooze API endpoint. Response: %s", + e, + res, + ) + return False + + async def async_snooze(self, snooze_time=240): + """Set sync snooze status.""" + data = json_dumps({"snooze_time": snooze_time}) + res = await api.request_sync_snooze( + self.blink, + self.network_id, + data=data, + ) + return res + async def start(self): """Initialize the system.""" _LOGGER.debug("Initializing the sync module") diff --git a/tests/test_api.py b/tests/test_api.py index f8f77b89..f7ffed32 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -189,6 +189,14 @@ async def test_request_update_config(self, mock_resp): ) ) + async def test_request_sync_snooze(self, mock_resp): + """Test sync snooze request.""" + mock_resp.return_value = {"message": "Sync snoozed"} + response = await api.request_sync_snooze( + self.blink, "network", '{"snooze": 300}' + ) + self.assertEqual(response, {"message": "Sync snoozed"}) + async def test_wait_for_command(self, mock_resp): """Test Motion detect enable.""" mock_resp.side_effect = (COMMAND_NOT_COMPLETE, COMMAND_COMPLETE) diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index b35f56fd..02a699be 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -100,6 +100,69 @@ def test_get_unique_info_invalid_device(self, mock_resp) -> None: self.blink.homescreen = {"doorbells": [device], "owls": []} self.assertEqual(self.blink.sync["test"].get_unique_info("doorbell2"), None) + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value={"snooze_till": "2026-02-15T12:00:00+00:00"}), + ) + async def test_snoozed(self, mock_resp) -> None: + """Check that we get snoozed status.""" + result = await self.blink.sync["test"].snoozed + self.assertTrue(result) + + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value=None), + ) + async def test_snoozed_none(self, mock_resp) -> None: + """Check that we handle None response.""" + result = await self.blink.sync["test"].snoozed + self.assertFalse(result) + + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value={}), + ) + async def test_snoozed_malformed(self, mock_resp) -> None: + """Check that we handle malformed response.""" + result = await self.blink.sync["test"].snoozed + self.assertFalse(result) + + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value={"snooze_till": ""}), + ) + async def test_snoozed_empty_string(self, mock_resp) -> None: + """Check that we handle empty string response.""" + result = await self.blink.sync["test"].snoozed + self.assertFalse(result) + + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value={"status": 200}), + ) + async def test_async_snooze(self, mock_resp) -> None: + """Check that we can set snooze.""" + result = await self.blink.sync["test"].async_snooze(300) + self.assertEqual(result, {"status": 200}) + + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value={"status": 400}), + ) + async def test_async_snooze_failure(self, mock_resp) -> None: + """Check that we handle snooze failure.""" + result = await self.blink.sync["test"].async_snooze(300) + self.assertEqual(result, {"status": 400}) + + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value=None), + ) + async def test_async_snooze_none_response(self, mock_resp) -> None: + """Check that we handle None response when setting snooze.""" + result = await self.blink.sync["test"].async_snooze(300) + self.assertIsNone(result) + async def test_get_events(self, mock_resp) -> None: """Test get events function.""" mock_resp.return_value = {"event": True} From e9843ec6777629bc7edecf44e49e978f1c721087 Mon Sep 17 00:00:00 2001 From: Austin Tyler Conn Date: Mon, 20 Jul 2026 16:21:40 +0000 Subject: [PATCH 2/3] Fix snoozed expiry check, use GET for status read, fix test payload key and timestamp --- blinkpy/api.py | 7 +++++-- blinkpy/sync_module.py | 26 ++++++++++---------------- tests/test_api.py | 2 +- tests/test_sync_module.py | 2 +- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/blinkpy/api.py b/blinkpy/api.py index 997827b1..40fec030 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -1069,14 +1069,17 @@ async def oauth_refresh_token(auth, refresh_token, hardware_id): async def request_sync_snooze(blink, network, data=None): """ - Update sync snooze configuration. + Get or update sync snooze configuration. :param blink: Blink instance. :param network: Sync module network id. - :param data: string w/JSON dict of parameters/values to update + :param data: string w/JSON dict of parameters/values to update. + If None, performs a GET to read current snooze state. """ url = ( f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/snooze" ) + if data is None: + return await http_get(blink, url) return await http_post(blink, url, json=True, data=data) diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 3ff7c038..0fc94a3a 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -132,23 +132,17 @@ async def snoozed(self): """Return snooze status as boolean.""" res = None try: - res = await api.request_sync_snooze( - self.blink, - self.network_id, - ) - if res is None: + res = await api.request_sync_snooze(self.blink, self.network_id) + if not isinstance(res, dict): return False - snooze_value = res.get("snooze_till") - return bool(snooze_value) - except TypeError: - return False - except KeyError as e: - _LOGGER.warning( - "Exception %s: Encountered a likely malformed response " - "from the snooze API endpoint. Response: %s", - e, - res, - ) + snooze_till = res.get("snooze_till") + if not snooze_till: + return False + expiry = datetime.datetime.fromisoformat(snooze_till) + if expiry.tzinfo is None: + expiry = expiry.replace(tzinfo=datetime.timezone.utc) + return expiry > datetime.datetime.now(datetime.timezone.utc) + except (TypeError, ValueError): return False async def async_snooze(self, snooze_time=240): diff --git a/tests/test_api.py b/tests/test_api.py index f7ffed32..dba6959d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -193,7 +193,7 @@ async def test_request_sync_snooze(self, mock_resp): """Test sync snooze request.""" mock_resp.return_value = {"message": "Sync snoozed"} response = await api.request_sync_snooze( - self.blink, "network", '{"snooze": 300}' + self.blink, "network", '{"snooze_time": 300}' ) self.assertEqual(response, {"message": "Sync snoozed"}) diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index 02a699be..21516b0e 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -102,7 +102,7 @@ def test_get_unique_info_invalid_device(self, mock_resp) -> None: @mock.patch( "blinkpy.api.request_sync_snooze", - mock.AsyncMock(return_value={"snooze_till": "2026-02-15T12:00:00+00:00"}), + mock.AsyncMock(return_value={"snooze_till": "2099-01-01T12:00:00+00:00"}), ) async def test_snoozed(self, mock_resp) -> None: """Check that we get snoozed status.""" From 8daedf52d4662480ce6251db5019cdfdb81f880d Mon Sep 17 00:00:00 2001 From: Austin Tyler Conn Date: Mon, 20 Jul 2026 18:01:16 +0000 Subject: [PATCH 3/3] Fix Z-suffix ISO parsing, compact snooze payload, add expired snooze test --- blinkpy/sync_module.py | 4 ++-- tests/test_sync_module.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 0fc94a3a..333e90d7 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -138,7 +138,7 @@ async def snoozed(self): snooze_till = res.get("snooze_till") if not snooze_till: return False - expiry = datetime.datetime.fromisoformat(snooze_till) + expiry = datetime.datetime.fromisoformat(snooze_till.replace("Z", "+00:00")) if expiry.tzinfo is None: expiry = expiry.replace(tzinfo=datetime.timezone.utc) return expiry > datetime.datetime.now(datetime.timezone.utc) @@ -147,7 +147,7 @@ async def snoozed(self): async def async_snooze(self, snooze_time=240): """Set sync snooze status.""" - data = json_dumps({"snooze_time": snooze_time}) + data = json_dumps({"snooze_time": snooze_time}, indent=None) res = await api.request_sync_snooze( self.blink, self.network_id, diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index 21516b0e..61b90e49 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -136,6 +136,24 @@ async def test_snoozed_empty_string(self, mock_resp) -> None: result = await self.blink.sync["test"].snoozed self.assertFalse(result) + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value={"snooze_till": "2000-01-01T00:00:00+00:00"}), + ) + async def test_snoozed_expired(self, mock_resp) -> None: + """Check that expired snooze_till returns False.""" + result = await self.blink.sync["test"].snoozed + self.assertFalse(result) + + @mock.patch( + "blinkpy.api.request_sync_snooze", + mock.AsyncMock(return_value={"snooze_till": "2099-01-01T00:00:00Z"}), + ) + async def test_snoozed_z_suffix(self, mock_resp) -> None: + """Check that Z-suffix timestamps are parsed correctly.""" + result = await self.blink.sync["test"].snoozed + self.assertTrue(result) + @mock.patch( "blinkpy.api.request_sync_snooze", mock.AsyncMock(return_value={"status": 200}),