From 363cad21334f8d99154c46b26a3125b8c780ef30 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 15 Sep 2023 19:32:33 +0000 Subject: [PATCH] add check command to PUT --- blinkpy/api.py | 59 ++++++++++++++++++++++++++++++++------- blinkpy/camera.py | 20 ++++++++++--- blinkpy/sync_module.py | 14 +++------- tests/mock_responses.py | 4 +++ tests/test_api.py | 43 +++++++++++++++++++++++----- tests/test_sync_module.py | 42 ++++++++++++++++++---------- 6 files changed, 136 insertions(+), 46 deletions(-) diff --git a/blinkpy/api.py b/blinkpy/api.py index 74a68dfc..4d01c08f 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -3,6 +3,7 @@ import logging import string from json import dumps +from asyncio import sleep from blinkpy.helpers.util import ( get_time, Throttle, @@ -13,6 +14,7 @@ _LOGGER = logging.getLogger(__name__) MIN_THROTTLE_TIME = 5 +COMMAND_POLL_TIME = 1 async def request_login( @@ -94,7 +96,9 @@ async def request_network_update(blink, network): :param network: Sync module network id. """ url = f"{blink.urls.base_url}/network/{network}/update" - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response async def request_user(blink): @@ -137,7 +141,9 @@ async def request_system_arm(blink, network): f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/state/arm" ) - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response @Throttle(seconds=MIN_THROTTLE_TIME) @@ -152,7 +158,9 @@ async def request_system_disarm(blink, network): f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}" f"/networks/{network}/state/disarm" ) - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response async def request_command_status(blink, network, command_id): @@ -196,7 +204,9 @@ async def request_new_image(blink, network, camera_id): :param camera_id: Camera ID of camera to request new image from. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/thumbnail" - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response @Throttle(seconds=MIN_THROTTLE_TIME) @@ -209,7 +219,9 @@ async def request_new_video(blink, network, camera_id): :param camera_id: Camera ID of camera to request new video from. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/clip" - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response @Throttle(seconds=MIN_THROTTLE_TIME) @@ -280,7 +292,9 @@ async def request_camera_liveview(blink, network, camera_id): f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}" f"/networks/{network}/cameras/{camera_id}/liveview" ) - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response async def request_camera_sensors(blink, network, camera_id): @@ -305,7 +319,9 @@ async def request_motion_detection_enable(blink, network, camera_id): :param camera_id: Camera ID of camera to enable. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/enable" - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response @Throttle(seconds=MIN_THROTTLE_TIME) @@ -317,7 +333,9 @@ async def request_motion_detection_disable(blink, network, camera_id): :param camera_id: Camera ID of camera to disable. """ url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/disable" - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response async def request_local_storage_manifest(blink, network, sync_id): @@ -335,7 +353,9 @@ async def request_local_storage_manifest(blink, network, sync_id): f"/networks/{network}/sync_modules/{sync_id}" f"/local_storage/manifest/request" ) - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response async def get_local_storage_manifest(blink, network, sync_id, manifest_request_id): @@ -373,7 +393,9 @@ async def request_local_storage_clip(blink, network, sync_id, manifest_id, clip_ manifest_id=manifest_id, clip_id=clip_id, ) - return await http_post(blink, url) + response = await http_post(blink, url) + await wait_for_command(blink, response) + return response async def request_get_config(blink, network, camera_id, product_type="owl"): @@ -467,3 +489,20 @@ async def http_post(blink, url, is_retry=False, data=None, json=True, timeout=TI json_resp=json, data=data, ) + + +async def wait_for_command(blink, json_data: dict) -> bool: + """Wait for command to complete.""" + _LOGGER.debug("Command Wait %s", json_data) + network_id = json_data.get("network_id") + command_id = json_data.get("id") + if command_id and network_id: + while True: + _LOGGER.debug("Making GET request waiting for command") + status = await request_command_status(blink, network_id, command_id) + _LOGGER.debug("command status %s", status) + if status.get("complete"): + return True + if status.get("status_code", 0) != 908: + return False + await sleep(COMMAND_POLL_TIME) diff --git a/blinkpy/camera.py b/blinkpy/camera.py index eeb850e2..e3af62e9 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -471,7 +471,9 @@ async def async_arm(self, value): f"{self.network_id}/owls/{self.camera_id}/config" ) data = dumps({"enabled": value}) - return await api.http_post(self.sync.blink, url, json=False, data=data) + response = await api.http_post(self.sync.blink, url, data=data) + await api.wait_for_command(self.sync.blink, response) + return response async def snap_picture(self): """Snap picture for a blink mini camera.""" @@ -480,7 +482,9 @@ async def snap_picture(self): f"{self.sync.blink.account_id}/networks/" f"{self.network_id}/owls/{self.camera_id}/thumbnail" ) - return await api.http_post(self.sync.blink, url) + response = await api.http_post(self.sync.blink, url) + await api.wait_for_command(self.sync.blink, response) + return response async def get_sensor_info(self): """Get sensor info for blink mini camera.""" @@ -493,6 +497,7 @@ async def get_liveview(self): f"{self.network_id}/owls/{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"] server_split = server.split(":") server_split[0] = "rtsps:" @@ -524,7 +529,10 @@ async def async_arm(self, value): url = f"{url}/enable" else: url = f"{url}/disable" - return await api.http_post(self.sync.blink, url) + + response = await api.http_post(self.sync.blink, url) + await api.wait_for_command(self.sync.blink, response) + return response async def snap_picture(self): """Snap picture for a blink doorbell camera.""" @@ -533,7 +541,10 @@ async def snap_picture(self): f"{self.sync.blink.account_id}/networks/" f"{self.sync.network_id}/doorbells/{self.camera_id}/thumbnail" ) - return await api.http_post(self.sync.blink, url) + + response = await api.http_post(self.sync.blink, url) + await api.wait_for_command(self.sync.blink, response) + return response async def get_sensor_info(self): """Get sensor info for blink doorbell camera.""" @@ -546,6 +557,7 @@ async def get_liveview(self): 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 diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index f7d47b4d..ea1fcfb8 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -679,17 +679,11 @@ def url(self, manifest_id=None): async def prepare_download(self, blink, max_retries=4): """Initiate upload of media item from the sync module to Blink cloud servers.""" + if max_retries == 0: + return None url = blink.urls.base_url + self.url() - response = None - for retry in range(max_retries): - response = await api.http_post(blink, url) - if "id" in response: - break - seconds = backoff_seconds(retry=retry, default_time=3) - _LOGGER.debug( - "[retry=%d] Retrying in %d seconds: %s", retry + 1, seconds, url - ) - await asyncio.sleep(seconds) + response = await api.http_post(blink, url) + await api.wait_for_command(blink, response) return response async def delete_video(self, blink, max_retries=4) -> bool: diff --git a/tests/mock_responses.py b/tests/mock_responses.py index 2cbfe9f6..2e249d21 100644 --- a/tests/mock_responses.py +++ b/tests/mock_responses.py @@ -17,3 +17,7 @@ def __init__(self, json_data, status_code, headers={}, raw_data=None): async def json(self): """Return json data from get_request.""" return self.json_data + + def get(self, name): + """Return field for json.""" + return self.json_data[name] diff --git a/tests/test_api.py b/tests/test_api.py index 8a37d0dd..197c0885 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,6 +7,10 @@ from blinkpy.auth import Auth import tests.mock_responses as mresp +COMMAND_RESPONSE = {"network_id": "12345", "id": "54321"} +COMMAND_COMPLETE = {"complete": True, "status_code": 908} +COMMAND_NOT_COMPLETE = {"complete": False, "status_code": 908} + @mock.patch("blinkpy.auth.Auth.query") class TestAPI(IsolatedAsyncioTestCase): @@ -57,7 +61,7 @@ async def test_request_network_status(self, mock_resp): async def test_request_command_status(self, mock_resp): """Test command_status.""" - mock_resp.return_value = {"command": "done"} + mock_resp.side_effect = ({"command": "done"}, COMMAND_COMPLETE) self.assertEqual( await api.request_command_status(self.blink, "network", "command"), {"command": "done"}, @@ -65,13 +69,19 @@ async def test_request_command_status(self, mock_resp): async def test_request_new_image(self, mock_resp): """Test api request new image.""" - mock_resp.return_value = mresp.MockResponse({}, 200) + mock_resp.side_effect = ( + mresp.MockResponse(COMMAND_RESPONSE, 200), + COMMAND_COMPLETE, + ) response = await api.request_new_image(self.blink, "network", "camera") self.assertEqual(response.status, 200) async def test_request_new_video(self, mock_resp): """Test api request new Video.""" - mock_resp.return_value = mresp.MockResponse({}, 200) + mock_resp.side_effect = ( + mresp.MockResponse(COMMAND_RESPONSE, 200), + COMMAND_COMPLETE, + ) response = await api.request_new_video(self.blink, "network", "camera") self.assertEqual(response.status, 200) @@ -97,7 +107,10 @@ async def test_request_camera_usage(self, mock_resp): async def test_request_motion_detection_enable(self, mock_resp): """Test Motion detect enable.""" - mock_resp.return_value = mresp.MockResponse({}, 200) + mock_resp.side_effect = ( + mresp.MockResponse(COMMAND_RESPONSE, 200), + COMMAND_COMPLETE, + ) response = await api.request_motion_detection_enable( self.blink, "network", "camera" ) @@ -105,7 +118,10 @@ async def test_request_motion_detection_enable(self, mock_resp): async def test_request_motion_detection_disable(self, mock_resp): """Test Motion detect enable.""" - mock_resp.return_value = mresp.MockResponse({}, 200) + mock_resp.side_effect = ( + mresp.MockResponse(COMMAND_RESPONSE, 200), + COMMAND_COMPLETE, + ) response = await api.request_motion_detection_disable( self.blink, "network", "camera" ) @@ -113,7 +129,10 @@ async def test_request_motion_detection_disable(self, mock_resp): async def test_request_local_storage_clip(self, mock_resp): """Test Motion detect enable.""" - mock_resp.return_value = mresp.MockResponse({}, 200) + mock_resp.side_effect = ( + mresp.MockResponse(COMMAND_RESPONSE, 200), + COMMAND_COMPLETE, + ) response = await api.request_local_storage_clip( self.blink, "network", "sync_id", "manifest_id", "clip_id" ) @@ -135,7 +154,7 @@ async def test_request_get_config(self, mock_resp): async def test_request_update_config(self, mock_resp): """Test Motion detect enable.""" - mock_resp.return_value = mresp.MockResponse({}, 200) + mock_resp.return_value = mresp.MockResponse(COMMAND_RESPONSE, 200) response = await api.request_update_config( self.blink, "network", "camera_id", "owl" ) @@ -149,3 +168,13 @@ async def test_request_update_config(self, mock_resp): self.blink, "network", "camera_id", "other_camera" ) ) + + async def test_wait_for_command(self, mock_resp): + """Test Motion detect enable.""" + mock_resp.side_effect = (COMMAND_NOT_COMPLETE, COMMAND_COMPLETE) + response = await api.wait_for_command(self.blink, COMMAND_RESPONSE) + assert response + + mock_resp.side_effect = (COMMAND_NOT_COMPLETE, {}) + response = await api.wait_for_command(self.blink, COMMAND_RESPONSE) + self.assertFalse(response) diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index 8d01ee11..b67eee1d 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -14,6 +14,7 @@ from blinkpy.camera import BlinkCamera from tests.test_blink_functions import MockCamera import tests.mock_responses as mresp +from .test_api import COMMAND_RESPONSE, COMMAND_COMPLETE @mock.patch("blinkpy.auth.Auth.query") @@ -123,12 +124,12 @@ async def test_get_network_info(self, mock_resp) -> None: async def test_get_network_info_failure(self, mock_resp) -> None: """Test failed network retrieval.""" - mock_resp.return_value = {} + mock_resp.side_effect = (COMMAND_RESPONSE, COMMAND_COMPLETE) self.blink.sync["test"].available = True self.assertFalse(await self.blink.sync["test"].get_network_info()) self.assertFalse(self.blink.sync["test"].available) self.blink.sync["test"].available = True - mock_resp.return_value = None + mock_resp.side_effect = None self.assertFalse(await self.blink.sync["test"].get_network_info()) self.assertFalse(self.blink.sync["test"].available) @@ -238,7 +239,8 @@ async def test_update_local_storage_manifest(self, mock_resp) -> None: test_sync._local_storage["status"] = True test_sync.sync_id = 1234 mock_resp.side_effect = [ - {"id": 387372591, "network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, { "version": "1.0", "manifest_id": "4321", @@ -305,7 +307,8 @@ async def test_check_new_videos_with_local_storage(self, mock_resp) -> None: datetime.datetime.utcnow() - datetime.timedelta(seconds=60) ).isoformat() mock_resp.side_effect = [ - {"id": 387372591, "network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, { "version": "1.0", "manifest_id": "4321", @@ -325,11 +328,15 @@ async def test_check_new_videos_with_local_storage(self, mock_resp) -> None: ], }, {"media": []}, - {"id": 489371591, "network_id": 123456}, - {"id": 489371592, "network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, + COMMAND_RESPONSE, + COMMAND_COMPLETE, {"media": []}, - {"id": 489371592, "network_id": 123456}, - {"id": 489371592, "network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, + COMMAND_RESPONSE, + COMMAND_COMPLETE, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" @@ -362,7 +369,7 @@ async def test_check_no_missing_id_with_update_local_storage_manifest( test_sync.cameras["Back Door"] = MockCamera(self.blink.sync) test_sync.cameras["Front_Door"] = MockCamera(self.blink.sync) mock_poll.return_value = [ - {"network_id": 123456}, + {"network_id": "12345", "id": "54321"}, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" @@ -385,7 +392,8 @@ async def test_check_missing_manifest_id_with_update_local_storage_manifest( ).isoformat() mock_resp.side_effect = [ - {"id": 387372591, "network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, { "version": "1.0", "clips": [ @@ -426,7 +434,8 @@ async def test_check_malformed_clips_with_update_local_storage_manifest( ).isoformat() mock_resp.side_effect = [ - {"id": 489371591, "network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, { "version": "1.0", "manifest_id": "4321", @@ -461,7 +470,8 @@ async def test_check_poll_local_storage_manifest_retry(self, mock_resp) -> None: test_sync.cameras["Front_Door"] = MockCamera(self.blink.sync) mock_resp.side_effect = [ - {"network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, ] test_sync._names_table[to_alphanumeric("Front_Door")] = "Front_Door" test_sync._names_table[to_alphanumeric("Back Door")] = "Back Door" @@ -469,7 +479,7 @@ async def test_check_poll_local_storage_manifest_retry(self, mock_resp) -> None: response = await test_sync.poll_local_storage_manifest(max_retries=1) self.assertEqual( response, - {"network_id": 123456}, + {"network_id": "12345", "id": "54321"}, ) async def test_sync_owl_init(self, mock_resp): @@ -524,11 +534,13 @@ async def test_local_storage_media_item(self, mock_resp): self.assertFalse(item == item2) mock_resp.side_effect = [ - {"network_id": 123456}, + COMMAND_RESPONSE, + COMMAND_COMPLETE, ] self.assertEqual( - await item.prepare_download(blink, max_retries=1), {"network_id": 123456} + await item.prepare_download(blink, max_retries=1), + {"network_id": "12345", "id": "54321"}, ) with mock.patch("blinkpy.api.http_post", return_value=""):