diff --git a/blinkpy/api.py b/blinkpy/api.py index 24f2b9f8..372c9d98 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -601,6 +601,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 d8332e67..70536301 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -177,6 +177,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( @@ -540,3 +565,30 @@ def __init__(self, sync): async def get_sensor_info(self): """Get sensor info for blink doorbell camera.""" + + async def get_liveview(self): + """Get liveview link.""" + url = ( + f"{self.sync.urls.base_url}/api/v1/accounts/" + f"{self.sync.blink.account_id}/networks/" + f"{self.sync.network_id}/doorbells/{self.camera_id}/liveview" + ) + response = await api.http_post(self.sync.blink, url) + await api.wait_for_command(self.sync.blink, response) + 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/tests/test_api.py b/tests/test_api.py index f8f77b89..b9b4ccbf 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -205,3 +205,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 b35f56fd..7a1d3392 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)