From 90a1a58f2c38d553ad92cb7a5bc527b4eb7b8f84 Mon Sep 17 00:00:00 2001 From: Austin Tyler Conn Date: Fri, 5 Jul 2024 00:54:30 +0000 Subject: [PATCH] Add Per Camera Snooze Functionality --- blinkapp/blinkapp.py | 2 +- blinkpy/api.py | 52 ++++++++++++++++++++++++++++++++++ blinkpy/camera.py | 39 +++++++++++++++++++++++++ blinkpy/sync_module.py | 25 ++++++++++++++++ pyproject.toml | 2 +- requirements_test.txt | 12 ++++---- tests/test_api.py | 22 ++++++++++++++ tests/test_camera_functions.py | 25 ++++++++++++++++ tests/test_sync_module.py | 40 ++++++++++++++++++++++++++ 9 files changed, 211 insertions(+), 8 deletions(-) diff --git a/blinkapp/blinkapp.py b/blinkapp/blinkapp.py index 840ff58d..d0101ff3 100644 --- a/blinkapp/blinkapp.py +++ b/blinkapp/blinkapp.py @@ -9,7 +9,7 @@ from blinkpy.helpers.util import json_load CREDFILE = environ.get("CREDFILE") -TIMEDELTA = timedelta(environ.get("TIMEDELTA", 1)) +TIMEDELTA = timedelta(environ.get("TIMEDELTA", "1")) def get_date(): diff --git a/blinkpy/api.py b/blinkpy/api.py index 95dc280b..019089cf 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -492,6 +492,58 @@ async def request_update_config( return await http_post(blink, url, json=False, data=data) +async def request_camera_snooze( + blink, network, camera_id, product_type="owl", data=None +): + """ + Update camera snooze configuration. + + :param blink: Blink instance. + :param network: Sync module network id. + :param camera_id: ID of camera + :param product_type: Camera product type "owl" or "catalina" + :param data: string w/JSON dict of parameters/values to update + """ + if product_type == "catalina": + url = ( + f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/" + f"networks/{network}/cameras/{camera_id}/snooze" + ) + elif product_type == "owl": + url = ( + f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/" + f"networks/{network}/owls/{camera_id}/snooze" + ) + elif product_type == "doorbell": + url = ( + f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/" + f"networks/{network}/doorbells/{camera_id}/snooze" + ) + else: + _LOGGER.info( + "Camera %s with product type %s snooze update not implemented.", + camera_id, + product_type, + ) + return None + return await http_post(blink, url, json=True, data=data) + + +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) + + async def http_get( blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT ): diff --git a/blinkpy/camera.py b/blinkpy/camera.py index b3c4a4b7..99aeaaf7 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -170,6 +170,31 @@ async def async_set_night_vision(self, value): return await res.json() return None + @property + async def snooze_till(self): + """Return snooze_till status.""" + res = await api.request_get_config( + self.sync.blink, + self.network_id, + self.camera_id, + product_type=self.product_type, + ) + if res is None: + return None + return res.get("camera", [{}])[0].get("snooze_till") + + async def async_snooze(self, snooze_time=240): + """Set camera snooze status.""" + data = dumps({"snooze_time": snooze_time}) + res = await api.request_camera_snooze( + self.sync.blink, + self.network_id, + self.camera_id, + product_type=self.product_type, + data=data, + ) + return res + async def record(self): """Initiate clip recording.""" return await api.request_new_video( @@ -625,3 +650,17 @@ async def get_liveview(self): server = response["server"] link = server.replace("immis://", "rtsps://") return link + + async def async_snooze(self): + """Set camera snooze status.""" + data = dumps({"snooze_time": 240}) + res = await api.request_camera_snooze( + self.sync.blink, + self.network_id, + self.camera_id, + product_type="doorbell", + data=data, + ) + if res and res.status == 200: + return await res.json() + return None diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 63b6aef6..63084f19 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -3,6 +3,7 @@ import logging import string import datetime +from json import dumps import traceback import asyncio import aiofiles @@ -127,6 +128,30 @@ 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 snooze_till(self): + """Return snooze_till status.""" + res = await api.request_sync_snooze( + self.blink, + self.network_id, + ) + if res is None: + return None + res = res.get("snooze_till") + return res + + async def async_snooze(self, snooze_time=240): + """Set sync snooze status.""" + data = dumps({"snooze_time": snooze_time}) + res = await api.request_sync_snooze( + self.blink, + self.network_id, + data=data, + ) + if res and res.status == 200: + return await res.json() + return None + async def start(self): """Initialize the system.""" _LOGGER.debug("Initializing the sync module") diff --git a/pyproject.toml b/pyproject.toml index bee982a2..6572a855 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools~=68.0", "wheel~=0.40.0"] +requires = ["setuptools>=68,<81", "wheel~=0.40.0"] build-backend = "setuptools.build_meta" [project] diff --git a/requirements_test.txt b/requirements_test.txt index 162a94e4..809435e6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -ruff==0.5.5 +ruff==0.11.13 black==24.4.2 build==1.2.1 -coverage==7.6.0 -pytest==8.3.2 -pytest-cov==5.0.0 +coverage==7.8.2 +pytest==8.4.0 +pytest-cov==6.1.1 pytest-sugar==1.0.0 -pytest-timeout==2.3.1 +pytest-timeout==2.4.0 restructuredtext-lint==1.4.0 -pygments==2.18.0 +pygments==2.19.1 testtools>=2.4.0 sortedcontainers~=2.4.0 pytest-asyncio>=0.21.0 diff --git a/tests/test_api.py b/tests/test_api.py index 8077fdde..449bf4fe 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -203,3 +203,25 @@ async def test_wait_for_command(self, mock_resp): response = await api.wait_for_command(self.blink, None) self.assertFalse(response) + + async def test_request_camera_snooze(self, mock_resp): + """Test request_camera_snooze.""" + mock_resp.return_value = mresp.MockResponse({}, 200) + response = await api.request_camera_snooze( + self.blink, "network", "camera_id", "owl", {} + ) + self.assertEqual(response.status, 200) + response = await api.request_camera_snooze( + self.blink, "network", "camera_id", "catalina", {} + ) + self.assertEqual(response.status, 200) + response = await api.request_camera_snooze( + self.blink, "network", "camera_id", "doorbell", {} + ) + self.assertEqual(response.status, 200) + + async def test_request_sync_snooze(self, mock_resp): + """Test sync snooze update.""" + mock_resp.return_value = mresp.MockResponse({}, 200) + response = await api.request_sync_snooze(self.blink, "network", {}) + self.assertEqual(response.status, 200) diff --git a/tests/test_camera_functions.py b/tests/test_camera_functions.py index 0ca75699..d066e364 100644 --- a/tests/test_camera_functions.py +++ b/tests/test_camera_functions.py @@ -9,6 +9,7 @@ import datetime from unittest import mock from unittest import IsolatedAsyncioTestCase +from blinkpy import api from blinkpy.blinkpy import Blink from blinkpy.helpers.util import BlinkURLHandler from blinkpy.sync_module import BlinkSyncModule @@ -222,6 +223,30 @@ async def test_night_vision(self, mock_resp): mock_resp.return_value = mresp.MockResponse({"code": 400}, 400) self.assertIsNone(await self.camera.async_set_night_vision("on")) + async def test_snooze_till(self, mock_resp): + """Test snooze_till property.""" + mock_resp = {"camera": [{"snooze_till": 1234567890}]} + with mock.patch.object( + api, + "request_get_config", + return_value=mock_resp, + ): + result = await self.camera.snooze_till + self.assertEqual(result, {"camera": [{"snooze_till": 1234567890}]}) + + async def test_async_snooze(self, mock_resp): + """Test async_snooze function.""" + mock_resp = mresp.MockResponse({}, 200) + with mock.patch("blinkpy.api.request_camera_snooze", return_value=mock_resp): + response = await self.camera.async_snooze() + self.assertEqual(response, {}) + mock_resp = mresp.MockResponse({}, 200) + with mock.patch("blinkpy.api.request_camera_snooze", return_value=mock_resp): + response = await self.camera.async_snooze() + self.assertEqual(response, {}) + response = await self.camera.async_snooze("invalid_value") + self.assertIsNone(response) + async def test_record(self, mock_resp): """Test camera record function.""" with mock.patch( diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index a1c81cbf..eff1c529 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -1,6 +1,7 @@ """Tests camera and system functions.""" import datetime +from json import dumps import logging from unittest import IsolatedAsyncioTestCase from unittest import mock @@ -653,3 +654,42 @@ async def test_download_delete(self, mock_prepdl, mock_del, mock_dl, mock_resp): mock_del.return_value = mock.AsyncMock() mock_dl.return_value = False self.assertFalse(await item.download_video_delete(self.blink, "filename.mp4")) + + async def test_async_snooze(self, mock_resp): + """Test successful snooze.""" + with mock.patch( + "blinkpy.api.request_sync_snooze", new_callable=mock.AsyncMock + ) as mock_resp_local: + mock_resp_local.return_value.status = 200 + mock_resp_local.return_value.json.return_value = {"status": 200} + snooze_time = 240 + expected_data = dumps({"snooze_time": snooze_time}) + expected_response = {"status": 200} + + self.assertEqual( + await self.blink.sync["test"].async_snooze(snooze_time), + expected_response, + ) + mock_resp_local.assert_called_once_with( + self.blink, + self.blink.sync["test"].network_id, + data=expected_data, + ) + + mock_resp_local.return_value.status = 400 + mock_resp_local.return_value.json.return_value = None + expected_response = None + + self.assertEqual( + await self.blink.sync["test"].async_snooze(snooze_time), + expected_response, + ) + + async def test_snooze_till(self, mock_resp) -> None: + """Test snooze_till method.""" + mock_resp.return_value = {"snooze_till": "2022-01-01T00:00:00Z"} + self.assertEqual( + await self.blink.sync["test"].snooze_till, "2022-01-01T00:00:00Z" + ) + mock_resp.return_value = None + self.assertIsNone(await self.blink.sync["test"].snooze_till)