diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 570efdce..ddb7c9ce 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,8 +33,9 @@ jobs: run: | tox -r -e cov - name: Codecov - uses: codecov/codecov-action@v1.0.6 + uses: codecov/codecov-action@v1 with: + token: ${{ secrets.CODECOV_TOKEN }} flags: unittests file: ./coverage.xml name: blinkpy diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f3a324b..98d80a3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,12 +8,12 @@ repos: - --quiet files: ^((blinkpy|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.2 + rev: 3.8.3 hooks: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==5.0.2 + - pydocstyle==5.1.1 files: ^(blinkpy|tests)/.+\.py$ - repo: https://github.com/Lucas-C/pre-commit-hooks-markup rev: v1.0.0 diff --git a/CHANGES.rst b/CHANGES.rst index 9646499b..d8171ab8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,31 @@ Changelog A list of changes between each release +0.16.4 (2020-11-22) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes:** + +- Updated liveview endpoint (`#389 `__) +- Fixed mini thumbnail not updating (`#388 `__) +- Add exception catch to prevent NoneType error on refresh, added test to check behavior as well (`#401 `__) + - Unrelated: had to add two force methods to refresh for testing purposes. Should not change normal usage. +- Fix malformed stream url (`#395 `__) + +**All:** + +- Moved testtools to requirements_test.txt (`#387 `__) +- Bumped pytest to 6.1.1 +- Bumped flake8 to 3.8.4 +- Fixed README spelling ((`#381 `__) via @rohitsud) +- Bumped pygments to 2.7.1 +- Bumped coverage to 5.3 +- Bumped pydocstyle to 5.1.1 +- Bumped pre-commit to 2.7.1 +- Bumped pylint to 2.6.0 +- Bumped pytest-cov to 2.10.1 + + 0.16.3 (2020-08-02) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index 25e8221a..32d867c6 100644 --- a/README.rst +++ b/README.rst @@ -174,7 +174,7 @@ Similar methods exist for individual cameras: Download videos ---------------- -You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``page=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specidy one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras. +You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``stop=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specify one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras. Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory: diff --git a/blinkpy/api.py b/blinkpy/api.py index 642e8af0..d72ca0d7 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -244,9 +244,7 @@ def request_camera_liveview(blink, network, camera_id): :param network: Sync module network id. :param camera_id: Camera ID of camera to request liveview from. """ - url = ( - f"{blink.urls.base_url}/api/v3/networks/{network}/cameras/{camera_id}/liveview" - ) + url = f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}/networks/{network}/cameras/{camera_id}/liveview" return http_post(blink, url) diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 913aa407..82054589 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -76,20 +76,22 @@ def __init__( self.no_owls = no_owls @util.Throttle(seconds=MIN_THROTTLE_TIME) - def refresh(self, force=False): + def refresh(self, force=False, force_cache=False): """ Perform a system refresh. - :param force: Force an update of the camera data + :param force: Used to override throttle, resets refresh + :param force_cache: Used to force update without overriding throttle """ - if self.check_if_ok_to_update() or force: + if self.check_if_ok_to_update() or force or force_cache: if not self.available: self.setup_post_verify() + self.get_homescreen() for sync_name, sync_module in self.sync.items(): _LOGGER.debug("Attempting refresh of sync %s", sync_name) - sync_module.refresh(force_cache=force) - if not force: + sync_module.refresh(force_cache=(force or force_cache)) + if not force_cache: # Prevents rapid clearing of motion detect property self.last_refresh = int(time.time()) return True diff --git a/blinkpy/camera.py b/blinkpy/camera.py index 1715feee..186b8559 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -98,6 +98,15 @@ def arm(self, value): self.sync.blink, self.network_id, self.camera_id ) + def get_media(self, media_type="image"): + """Download media (image or video).""" + url = self.thumbnail + if media_type.lower() == "video": + url = self.clip + return api.http_get( + self.sync.blink, url=url, stream=True, json=False, timeout=TIMEOUT_MEDIA, + ) + def snap_picture(self): """Take a picture with camera to create a new thumbnail.""" return api.request_new_image(self.sync.blink, self.network_id, self.camera_id) @@ -180,21 +189,10 @@ def update_images(self, config, force_cache=False): update_cached_video = True if new_thumbnail is not None and (update_cached_image or force_cache): - self._cached_image = api.http_get( - self.sync.blink, - url=self.thumbnail, - stream=True, - json=False, - timeout=TIMEOUT_MEDIA, - ) + self._cached_image = self.get_media() + if clip_addr is not None and (update_cached_video or force_cache): - self._cached_video = api.http_get( - self.sync.blink, - url=self.clip, - stream=True, - json=False, - timeout=TIMEOUT_MEDIA, - ) + self._cached_video = self.get_media(media_type="video") def get_liveview(self): """Get livewview rtsps link.""" @@ -210,7 +208,7 @@ def image_to_file(self, path): :param path: Path to write file """ _LOGGER.debug("Writing image from %s to %s", self.name, path) - response = self._cached_image + response = self.get_media() if response.status_code == 200: with open(path, "wb") as imgfile: copyfileobj(response.raw, imgfile) @@ -226,7 +224,7 @@ def video_to_file(self, path): :param path: Path to write file """ _LOGGER.debug("Writing video from %s to %s", self.name, path) - response = self._cached_video + response = self.get_media(media_type="video") if response is None: _LOGGER.error("No saved video exist for %s.", self.name) return @@ -268,6 +266,6 @@ def get_liveview(self): response = api.http_post(self.sync.blink, url) server = response["server"] server_split = server.split(":") - server_split[0] = "rtsps" + server_split[0] = "rtsps:" link = "".join(server_split) return link diff --git a/blinkpy/helpers/constants.py b/blinkpy/helpers/constants.py index bf43b5c2..1862d3dc 100644 --- a/blinkpy/helpers/constants.py +++ b/blinkpy/helpers/constants.py @@ -4,7 +4,7 @@ MAJOR_VERSION = 0 MINOR_VERSION = 16 -PATCH_VERSION = 3 +PATCH_VERSION = 4 __version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}" diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index ad780a16..4e317569 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -145,7 +145,7 @@ def get_owl_info(self, name): for owl in self.blink.homescreen["owls"]: if owl["name"] == name: return owl - except KeyError: + except (TypeError, KeyError): pass return None @@ -270,7 +270,7 @@ def get_camera_info(self, camera_id, **kwargs): if owl["name"] == self.name: self.status = owl["enabled"] return owl - except KeyError: + except (TypeError, KeyError): pass return None diff --git a/requirements.txt b/requirements.txt index 384d3bda..47ea4e01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ python-dateutil>=2.8.1 requests>=2.24.0 python-slugify>=4.0.1 -testtools>=2.4.0 diff --git a/requirements_test.txt b/requirements_test.txt index b0f58b02..2cb1cc7a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,14 @@ black==19.10b0 -coverage==5.2.1 -flake8==3.8.3 +coverage==5.3 +flake8==3.8.4 flake8-docstrings==1.5.0 -pre-commit==2.6.0 -pylint==2.5.3 -pydocstyle==5.0.2 -pytest==6.0.1 -pytest-cov==2.10.0 +pre-commit==2.7.1 +pylint==2.6.0 +pydocstyle==5.1.1 +pytest==6.1.1 +pytest-cov==2.10.1 pytest-sugar==0.9.4 pytest-timeout==1.4.2 restructuredtext-lint==1.3.1 -pygments==2.6.1 +pygments==2.7.1 +testtools>=2.4.0 diff --git a/tests/test_blink_functions.py b/tests/test_blink_functions.py index 89a45dc6..8a71294b 100644 --- a/tests/test_blink_functions.py +++ b/tests/test_blink_functions.py @@ -5,29 +5,23 @@ from blinkpy import blinkpy from blinkpy.sync_module import BlinkSyncModule +from blinkpy.camera import BlinkCamera from blinkpy.helpers.util import get_time, BlinkURLHandler class MockSyncModule(BlinkSyncModule): - """Mock http requests from sync module.""" + """Mock blink sync module object.""" - def __init__(self, blink, header): - """Create mock sync module instance.""" - super().__init__(blink, header, network_id=None, camera_list=None) - self.blink = blink - self.header = header - self.return_value = None - self.return_value2 = None + def get_network_info(self): + """Mock network info method.""" + return True - def http_get(self, url, stream=False, json=True): - """Mock get request.""" - if stream and self.return_value2 is not None: - return self.return_value2 - return self.return_value - def http_post(self, url): - """Mock post request.""" - return self.return_value +class MockCamera(BlinkCamera): + """Mock blink camera object.""" + + def update(self, config, force_cache=False, **kwargs): + """Mock camera update method.""" class TestBlinkFunctions(unittest.TestCase): @@ -121,3 +115,16 @@ def test_parse_camera_not_in_list(self, mock_req): with self.assertLogs() as dl_log: blink.download_videos("/tmp", camera="bar", stop=2) self.assertEqual(dl_log.output, expected_log) + + @mock.patch("blinkpy.blinkpy.api.request_network_update") + @mock.patch("blinkpy.auth.Auth.query") + def test_refresh(self, mock_req, mock_update): + """Test ability to refresh system.""" + mock_update.return_value = {"network": {"sync_module_error": False}} + mock_req.return_value = None + self.blink.last_refresh = 0 + self.blink.available = True + self.blink.sync["foo"] = MockSyncModule(self.blink, "foo", 1, []) + self.blink.cameras = {"bar": MockCamera(self.blink.sync)} + self.blink.sync["foo"].cameras = self.blink.cameras + self.assertTrue(self.blink.refresh()) diff --git a/tests/test_blinkpy.py b/tests/test_blinkpy.py index 821edf05..c632215d 100644 --- a/tests/test_blinkpy.py +++ b/tests/test_blinkpy.py @@ -68,8 +68,8 @@ def test_throttle(self, mock_time): self.assertEqual(self.blink.last_refresh, None) with mock.patch( "blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True - ): - self.blink.refresh() + ), mock.patch("blinkpy.blinkpy.Blink.get_homescreen", return_value=True): + self.blink.refresh(force=True) self.assertEqual(self.blink.last_refresh, now) self.assertEqual(self.blink.check_if_ok_to_update(), False) diff --git a/tests/test_cameras.py b/tests/test_cameras.py index 25bdaf06..8cf7d76f 100644 --- a/tests/test_cameras.py +++ b/tests/test_cameras.py @@ -176,3 +176,10 @@ def test_mini_missing_attributes(self, mock_resp): attr = camera.attributes for key in attr: self.assertEqual(attr[key], None) + + def test_camera_stream(self, mock_resp): + """Test that camera stream returns correct url.""" + mock_resp.return_value = {"server": "rtsps://foo.bar"} + mini_camera = BlinkCameraMini(self.blink.sync["test"]) + self.assertEqual(self.camera.get_liveview(), "rtsps://foo.bar") + self.assertEqual(mini_camera.get_liveview(), "rtsps://foo.bar")